diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 06e643bdd..000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,115 +0,0 @@ -parameters: - -# v2: 11m. -defaults: &defaults - resource_class: large - docker: - - image: bepsays/ci-hugoreleaser:1.22400.20000 -environment: &buildenv - GOMODCACHE: /root/project/gomodcache -version: 2 -jobs: - prepare_release: - <<: *defaults - environment: &buildenv - GOMODCACHE: /root/project/gomodcache - steps: - - setup_remote_docker - - checkout: - path: hugo - - &git-config - run: - command: | - git config --global user.email "bjorn.erik.pedersen+hugoreleaser@gmail.com" - git config --global user.name "hugoreleaser" - - run: - command: | - cd hugo - go mod download - go run -tags release main.go release --step 1 - - save_cache: - key: git-sha-{{ .Revision }} - paths: - - hugo - - gomodcache - build_container1: - <<: [*defaults] - environment: - <<: [*buildenv] - steps: - - &restore-cache - restore_cache: - key: git-sha-{{ .Revision }} - - run: - no_output_timeout: 20m - command: | - mkdir -p /tmp/files/dist1 - cd hugo - hugoreleaser build -paths "builds/container1/**" -workers 3 -dist /tmp/files/dist1 -chunks $CIRCLE_NODE_TOTAL -chunk-index $CIRCLE_NODE_INDEX - - &persist-workspace - persist_to_workspace: - root: /tmp/files - paths: - - dist1 - - dist2 - parallelism: 7 - build_container2: - <<: [*defaults] - environment: - <<: [*buildenv] - docker: - - image: bepsays/ci-hugoreleaser-linux-arm64:1.22400.20000 - steps: - - *restore-cache - - &attach-workspace - attach_workspace: - at: /tmp/workspace - - run: - command: | - mkdir -p /tmp/files/dist2 - cd hugo - hugoreleaser build -paths "builds/container2/**" -workers 1 -dist /tmp/files/dist2 - - *persist-workspace - archive_and_release: - <<: [*defaults] - environment: - <<: [*buildenv] - steps: - - *restore-cache - - *attach-workspace - - *git-config - - run: - name: Add github.com to known hosts - command: ssh-keyscan github.com >> ~/.ssh/known_hosts - - run: - command: | - cp -a /tmp/workspace/dist1/. ./hugo/dist - cp -a /tmp/workspace/dist2/. ./hugo/dist - - run: - command: | - cd hugo - hugoreleaser archive - hugoreleaser release - go run -tags release main.go release --step 2 -workflows: - version: 2 - release: - jobs: - - prepare_release: - filters: - branches: - only: /release-.*/ - - build_container1: - requires: - - prepare_release - - build_container2: - requires: - - prepare_release - - archive_and_release: - context: org-global - requires: - - build_container1 - - build_container2 - - - diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index a183f6fcf..000000000 --- a/.dockerignore +++ /dev/null @@ -1,9 +0,0 @@ -*.md -*.log -*.txt -.git -.github -.circleci -docs -examples -Dockerfile diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 6994810cf..000000000 --- a/.gitattributes +++ /dev/null @@ -1,8 +0,0 @@ -# Text files have auto line endings -* text=auto - -# Go source files always have LF line endings -*.go text eol=lf - -# SVG files should not be modified -*.svg -text diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index fa2791492..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: 'Bug report' -labels: 'Bug, NeedsTriage' -assignees: '' -about: Create a report to help us improve ---- - - - - - -### What version of Hugo are you using (`hugo version`)? - -
-$ hugo version
-
-
- -### Does this issue reproduce with the latest release? diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index c84d3276b..000000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,5 +0,0 @@ -blank_issues_enabled: false -contact_links: - - name: SUPPORT, ISSUES and TROUBLESHOOTING - url: https://discourse.gohugo.io/ - about: Please DO NOT use Github for support requests. Please visit https://discourse.gohugo.io for support! You will be helped much faster there. If you ignore this request your issue might be closed with a discourse label. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index c114b3d7f..000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Proposal -about: Propose a new feature for Hugo -title: '' -labels: 'Proposal, NeedsTriage' -assignees: '' - ---- - - - \ No newline at end of file diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md deleted file mode 100644 index cc9de09ff..000000000 --- a/.github/SUPPORT.md +++ /dev/null @@ -1,3 +0,0 @@ -### 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/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 1801e72d9..000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,7 +0,0 @@ -# See https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates#package-ecosystem -version: 2 -updates: - - package-ecosystem: "gomod" - directory: "/" - schedule: - interval: "daily" diff --git a/.github/workflows/image.yml b/.github/workflows/image.yml deleted file mode 100644 index c4f3c34c3..000000000 --- a/.github/workflows/image.yml +++ /dev/null @@ -1,49 +0,0 @@ -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 deleted file mode 100644 index 249c1ab54..000000000 --- a/.github/workflows/stale.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: 'Close stale and lock closed issues and PRs' -on: - workflow_dispatch: - schedule: - - cron: '30 1 * * *' -permissions: - contents: read -jobs: - stale: - permissions: - issues: write - pull-requests: write - runs-on: ubuntu-latest - steps: - - uses: dessant/lock-threads@7de207be1d3ce97a9abe6ff1306222982d1ca9f9 # v5.0.1 - with: - issue-inactive-days: 21 - add-issue-labels: 'Outdated' - issue-comment: > - This issue has been automatically locked since there - has not been any recent activity after it was closed. - Please open a new issue for related bugs. - pr-comment: > - 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@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 - with: - operations-per-run: 999 - days-before-issue-stale: 365 - days-before-pr-stale: 365 - days-before-issue-close: 56 - days-before-pr-close: 56 - stale-issue-message: > - This issue has been automatically marked as stale because it has not had - recent activity. The resources of the Hugo team are limited, and so we are asking for your help. - - If this is a **bug** and you can still reproduce this error on the master branch, please reply with all of the information you have about it in order to keep the issue open. - - If this is a **feature request**, and you feel that it is still relevant and valuable, please tell us why. - - This issue will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions. - stale-pr-message: This PR has been automatically marked as stale because it has not had - recent activity. The resources of the Hugo team are limited, and so we are asking for your help. - - Please check https://github.com/gohugoio/hugo/blob/master/CONTRIBUTING.md#code-contribution and verify that this code contribution fits with the description. If yes, tell is in a comment. - - This PR will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions. - stale-issue-label: 'Stale' - exempt-issue-labels: 'Keep,Security' - stale-pr-label: 'Stale' - exempt-pr-labels: 'Keep,Security' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index c49c12371..000000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,132 +0,0 @@ -on: - push: - branches: [master] - pull_request: -name: Test -env: - 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.23.x, 1.24.x] - os: [ubuntu-latest, windows-latest] # macos disabled for now because of disk space issues. - runs-on: ${{ matrix.os }} - steps: - - 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 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 ddad69611..47721b7cb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,18 @@ +hugo +docs/public* +/.idea +hugo.exe +*.test +*.prof +nohup.out +cover.out +*.swp +*.swo +.DS_Store +*~ +vendor/*/ +*.bench +coverage*.out -*.test -imports.* -dist/ -public/ -.DS_Store \ No newline at end of file +GoBuilds +dist diff --git a/.goxc.json b/.goxc.json new file mode 100644 index 000000000..10f19d226 --- /dev/null +++ b/.goxc.json @@ -0,0 +1,6 @@ +{ + "ArtifactsDest": "GoBuilds/", + "OutPath": "{{.Dest}}{{.PS}}{{.AppName}}{{.PS}}{{.Version}}{{.PS}}{{.AppName}}_{{.Version}}_{{.Os}}_{{.Arch}}{{.Ext}}", + "BuildConstraints": "linux windows darwin freebsd netbsd openbsd dragonfly", + "ConfigVersion": "0.9" +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..514ab1ddf --- /dev/null +++ b/.travis.yml @@ -0,0 +1,23 @@ +language: go +sudo: required +go: + - 1.7.6 + - 1.8.3 + - tip +os: + - linux + - osx +matrix: + allow_failures: + - go: tip + fast_finish: true +install: + - make vendor +script: + - make hugo-race check + - ./hugo -s docs/ + - ./hugo --renderToMemory -s docs/ +before_install: + # gem install must be run with sudo on OSX + - sudo gem install asciidoctor | gem install asciidoctor + - sudo pip install docutils diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ddd3efcf2..0368e6666 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,3 @@ ->**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 We welcome contributions to Hugo of any kind including documentation, themes, @@ -9,10 +7,6 @@ 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. We created a [step by step guide](https://gohugo.io/tutorials/how-to-contribute-to-hugo/) if you're unfamiliar with GitHub or contributing to open source projects in general. -*Note that this repository only contains the actual source code of Hugo. For **only** documentation-related pull requests / issues please refer to the [hugoDocs](https://github.com/gohugoio/hugoDocs) repository.* - -*Changes to the codebase **and** related documentation, e.g. for a new feature, should still use a single pull request.* - ## Table of Contents * [Asking Support Questions](#asking-support-questions) @@ -20,8 +14,11 @@ The Hugo community and maintainers are [very active](https://github.com/gohugoio * [Submitting Patches](#submitting-patches) * [Code Contribution Guidelines](#code-contribution-guidelines) * [Git Commit Message Guidelines](#git-commit-message-guidelines) + * [Vendored Dependencies](#vendored-dependencies) * [Fetching the Sources From GitHub](#fetching-the-sources-from-github) - * [Building Hugo with Your Changes](#building-hugo-with-your-changes) + * [Using Git Remotes](#using-git-remotes) + * [Build Hugo with Your Changes](#build-hugo-with-your-changes) + * [Updating the Hugo Sources](#updating-the-hugo-sources) ## Asking Support Questions @@ -31,42 +28,22 @@ 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`) and your operating system. - -- [Hugo Issues · gohugoio/hugo](https://github.com/gohugoio/hugo/issues) -- [Hugo Documentation Issues · gohugoio/hugoDocs](https://github.com/gohugoio/hugoDocs/issues) -- [Hugo Website Theme Issues · gohugoio/hugoThemesSite](https://github.com/gohugoio/hugoThemesSite/issues) - -## Code Contribution - -Hugo has become a fully featured static site generator, so any new functionality must: - -* be useful to many. -* fit naturally into _what Hugo does best._ -* strive not to break existing sites. -* close or update an open [Hugo issue](https://github.com/gohugoio/hugo/issues) - -If it is of some complexity, the contributor is expected to maintain and support the new feature in the future (answer questions on the forum, fix any bugs etc.). - -Any non-trivial code change needs to update an open [issue](https://github.com/gohugoio/hugo/issues). A non-trivial code change without an issue reference with one of the labels `bug` or `enhancement` will not be merged. - -Note that we do not accept new features that require [CGO](https://github.com/golang/go/wiki/cgo). -We have one exception to this rule which is LibSASS. - -**Bug fixes are, of course, always welcome.** +the GitHub [issue tracker](https://github.com/gohugoio/hugo/issues) 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`) and your operating system. ## Submitting Patches -The Hugo project welcomes all contributors and contributions regardless of skill or experience level. If you are interested in helping with the project, we will help you with your contribution. +The Hugo project welcomes all contributors and contributions regardless of skill or experience level. +If you are interested in helping with the project, we will help you with your contribution. +Hugo is a very active project with many contributions happening daily. +Because we want to create the best possible product for our users and the best contribution experience for our developers, +we have a set of guidelines which ensure that all contributions are acceptable. +The guidelines are not intended as a filter or barrier to participation. +If you are unfamiliar with the contribution process, the Hugo team will help you and teach you how to bring your contribution in accordance with the guidelines. ### Code Contribution Guidelines -Because we want to create the best possible product for our users and the best contribution experience for our developers, we have a set of guidelines which ensure that all contributions are acceptable. The guidelines are not intended as a filter or barrier to participation. If you are unfamiliar with the contribution process, the Hugo team will help you and teach you how to bring your contribution in accordance with the guidelines. - To make the contribution process as seamless as possible, we ask for the following: * Go ahead and fork the project and make your changes. We encourage pull requests to allow for review and discussion of code changes. @@ -76,28 +53,24 @@ To make the contribution process as seamless as possible, we ask for the followi * Run `go fmt`. * Add documentation if you are adding new features or changing functionality. The docs site lives in `/docs`. * Squash your commits into a single commit. `git rebase -i`. It’s okay to force update your pull request with `git push -f`. - * Ensure that `mage check` succeeds. [Travis CI](https://travis-ci.org/gohugoio/hugo) (Windows, Linux and macOS) will fail the build if `mage check` fails. + * Ensure that `make check` succeeds. [Travis CI](https://travis-ci.org/gohugoio/hugo) (Linux and macOS) and [AppVeyor](https://ci.appveyor.com/project/gohugoio/hugo/branch/master) (Windows) will fail the build if `make check` fails. * Follow the **Git Commit Message Guidelines** below. ### Git Commit Message Guidelines -This [blog article](https://cbea.ms/git-commit/) is a good resource for learning how to write good commit messages, +This [blog article](http://chris.beams.io/posts/git-commit/) is a good resource for learning how to write good commit messages, the most important part being that each commit message should have a title/subject in imperative mood starting with a capital letter and no trailing period: -*"js: Return error when option x is not set"*, **NOT** *"returning some error."* - -Most title/subjects should have a lower-cased prefix with a colon and one whitespace. The prefix can be: - -* The name of the package where (most of) the changes are made (e.g. `media: Add text/calendar`) -* If the package name is deeply nested/long, try to shorten it from the left side, e.g. `markup/goldmark` is OK, `resources/resource_transformers/js` can be shortened to `js`. -* If this commit touches several packages with a common functional topic, use that as a prefix, e.g. `errors: Resolve correct line numbers`) -* 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:". +*"Return error on wrong use of the Paginator"*, **NOT** *"returning some error."* 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*. +Sometimes it makes sense to prefix the commit message with the packagename (or docs folder) all lowercased ending with a colon. +That is fine, but the rest of the rules above apply. +So it is "tpl: Add emojify template func", not "tpl: add emojify template func.", and "docs: Document emoji", not "doc: document emoji." + +Please consider to use a short and descriptive branch name, e.g. **NOT** "patch-1". It's very common but creates a naming conflict each time when a submission is pulled for a review. + An example: ```text @@ -111,35 +84,42 @@ new default function more useful for Hugo users. Fixes #1949 ``` -### Fetching the Sources From GitHub +### Vendored Dependencies -Since Hugo 0.48, Hugo uses the Go Modules support built into Go 1.11 to build. The easiest is to clone Hugo in a directory outside of `GOPATH`, as in the following example: +Hugo uses [govendor](https://github.com/kardianos/govendor) to vendor dependencies, but we don't commit the vendored packages themselves to the Hugo git repository. +Therefore, a simple `go get` is not supported since `go get` is not vendor-aware. +You **must use govendor** to fetch and manage Hugo's dependencies. -```bash -mkdir $HOME/src -cd $HOME/src -git clone https://github.com/gohugoio/hugo.git -cd hugo -go install +### Fetch the Sources From GitHub + +``` +go get github.com/kardianos/govendor +govendor get github.com/gohugoio/hugo ``` -For some convenient build and test targets, you also will want to install Mage: +### Using Git Remotes -```bash -go install github.com/magefile/mage -``` +Due to the way Go handles package imports, the best approach for working on a +Hugo fork is to use Git Remotes. Here's a simple walk-through for getting +started: -Now, to make a change to Hugo's source: +1. Fetch the Hugo sources as described above. + +1. Change to the Hugo source directory: + + ``` + cd $HOME/go/src/github.com/gohugoio/hugo + ``` 1. Create a new branch for your changes (the branch name is arbitrary): - ```bash + ``` git checkout -b iss1234 ``` 1. After making your changes, commit them to your new branch: - ```bash + ``` git commit -a -v ``` @@ -147,53 +127,35 @@ Now, to make a change to Hugo's source: 1. Add your fork as a new remote (the remote name, "fork" in this example, is arbitrary): - ```bash - git remote add fork git@github.com:USERNAME/hugo.git + ``` + git remote add fork git://github.com/USERNAME/hugo.git ``` 1. Push the changes to your new remote: - ```bash + ``` git push --set-upstream fork iss1234 ``` 1. You're now ready to submit a PR based upon the new branch in your forked repository. -### Building Hugo with Your Changes - -Hugo uses [mage](https://github.com/magefile/mage) to sync vendor dependencies, build Hugo, run the test suite and other things. You must run mage from the Hugo directory. +### Build Hugo with Your Changes ```bash cd $HOME/go/src/github.com/gohugoio/hugo +make hugo +# or to install in $HOME/go/bin: +make install ``` -To build Hugo: +### Updating the Hugo Sources -```bash -mage hugo +If you want to stay in sync with the Hugo repository, you can easily pull down +the source changes, but you'll need to keep the vendored packages up-to-date as +well. + +``` +git pull +make vendor ``` -To install hugo in `$HOME/go/bin`: - -```bash -mage install -``` - -To run the tests: - -```bash -mage hugoRace -mage -v check -``` - -To list all available commands along with descriptions: - -```bash -mage -l -``` - -**Note:** From Hugo 0.43 we have added a build tag, `extended` that adds **SCSS support**. This needs a C compiler installed to build. You can enable this when building by: - -```bash -HUGO_BUILD_TAGS=extended mage install -```` diff --git a/Dockerfile b/Dockerfile old mode 100755 new mode 100644 index a0e34353f..43284e4d9 --- a/Dockerfile +++ b/Dockerfile @@ -1,99 +1,27 @@ -# GitHub: https://github.com/gohugoio -# Twitter: https://twitter.com/gohugoio -# Website: https://gohugo.io/ +FROM golang:alpine3.6 -ARG GO_VERSION="1.24" -ARG ALPINE_VERSION="3.22" -ARG DART_SASS_VERSION="1.79.3" +ENV GOPATH /go -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 - - -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 - -# 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 <>_ + +### 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. diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..4508ab7cf --- /dev/null +++ b/Makefile @@ -0,0 +1,83 @@ +# A Self-Documenting Makefile: http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html + +PACKAGE = github.com/gohugoio/hugo +COMMIT_HASH = `git rev-parse --short HEAD 2>/dev/null` +BUILD_DATE = `date +%FT%T%z` +LDFLAGS = -ldflags "-X ${PACKAGE}/hugolib.CommitHash=${COMMIT_HASH} -X ${PACKAGE}/hugolib.BuildDate=${BUILD_DATE}" +NOGI_LDFLAGS = -ldflags "-X ${PACKAGE}/hugolib.BuildDate=${BUILD_DATE}" + +.PHONY: vendor docker check fmt lint test test-race vet test-cover-html help +.DEFAULT_GOAL := help + +vendor: ## Install govendor and sync Hugo's vendored dependencies + go get github.com/kardianos/govendor + govendor sync ${PACKAGE} + +hugo: vendor ## Build hugo binary + go build ${LDFLAGS} ${PACKAGE} + +hugo-race: vendor ## Build hugo binary with race detector enabled + go build -race ${LDFLAGS} ${PACKAGE} + +install: vendor ## Install hugo binary + go install ${LDFLAGS} ${PACKAGE} + +hugo-no-gitinfo: LDFLAGS = ${NOGI_LDFLAGS} +hugo-no-gitinfo: vendor hugo ## Build hugo without git info + +docker: ## Build hugo Docker container + docker build -t hugo . + docker rm -f hugo-build || true + docker run --name hugo-build hugo ls /go/bin + docker cp hugo-build:/go/bin/hugo . + docker rm hugo-build + +govendor: vendor # Deprecated: use "vendor" target +get: vendor # Deprecated: use "vendor" +gitinfo: hugo # Deprecated: use "hugo" target +install-gitinfo: install # Deprecated: use "install" target +no-git-info: hugo-no-gitinfo # Deprecated: use "hugo-no-gitinfo" target + +check: test-race test386 fmt vet ## Run tests and linters + +test386: ## Run tests in 32-bit mode + GOARCH=386 govendor test +local + +test: ## Run tests + govendor test +local + +test-race: ## Run tests with race detector + govendor test -race +local + +fmt: ## Run gofmt linter + @for d in `govendor list -no-status +local | sed 's/github.com.gohugoio.hugo/./'` ; do \ + if [ "`gofmt -l $$d/*.go | tee /dev/stderr`" ]; then \ + echo "^ improperly formatted go files" && echo && exit 1; \ + fi \ + done + +lint: ## Run golint linter + @for d in `govendor list -no-status +local | sed 's/github.com.gohugoio.hugo/./'` ; do \ + if [ "`golint $$d | tee /dev/stderr`" ]; then \ + echo "^ golint errors!" && echo && exit 1; \ + fi \ + done + +vet: ## Run go vet linter + @if [ "`govendor vet +local | tee /dev/stderr`" ]; then \ + echo "^ go vet errors!" && echo && exit 1; \ + fi + +test-cover-html: PACKAGES = $(shell govendor list -no-status +local | sed 's/github.com.gohugoio.hugo/./') +test-cover-html: ## Generate test coverage report + echo "mode: count" > coverage-all.out + $(foreach pkg,$(PACKAGES),\ + govendor test -coverprofile=coverage.out -covermode=count $(pkg);\ + tail -n +2 coverage.out >> coverage-all.out;) + go tool cover -html=coverage-all.out + +check-vendor: ## Verify that vendored packages match git HEAD + @git diff-index --quiet HEAD vendor/ || (echo "check-vendor target failed: vendored packages out of sync" && echo && git diff vendor/ && exit 1) + +help: + @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' diff --git a/README.md b/README.md index 9befa9c9d..e5591d5e0 100644 --- a/README.md +++ b/README.md @@ -1,282 +1,106 @@ -[bep]: https://github.com/bep -[bugs]: https://github.com/gohugoio/hugo/issues?q=is%3Aopen+is%3Aissue+label%3ABug -[contributing]: CONTRIBUTING.md -[create a proposal]: https://github.com/gohugoio/hugo/issues/new?labels=Proposal%2C+NeedsTriage&template=feature_request.md -[documentation repository]: https://github.com/gohugoio/hugoDocs -[documentation]: https://gohugo.io/documentation -[dragonfly bsd, freebsd, netbsd, and openbsd]: https://gohugo.io/installation/bsd -[features]: https://gohugo.io/about/features/ -[forum]: https://discourse.gohugo.io -[friends]: https://github.com/gohugoio/hugo/graphs/contributors -[go]: https://go.dev/ -[hugo modules]: https://gohugo.io/hugo-modules/ -[installation]: https://gohugo.io/installation -[issue queue]: https://github.com/gohugoio/hugo/issues -[linux]: https://gohugo.io/installation/linux -[macos]: https://gohugo.io/installation/macos -[prebuilt binary]: https://github.com/gohugoio/hugo/releases/latest -[requesting help]: https://discourse.gohugo.io/t/requesting-help/9132 -[spf13]: https://github.com/spf13 -[static site generator]: https://en.wikipedia.org/wiki/Static_site_generator -[support]: https://discourse.gohugo.io -[themes]: https://themes.gohugo.io/ -[website]: https://gohugo.io -[windows]: https://gohugo.io/installation/windows +![Hugo](https://raw.githubusercontent.com/gohugoio/hugoDocs/master/static/img/hugo-logo.png) -Hugo +A Fast and Flexible Static Site Generator built with love by [spf13](http://spf13.com/) and [friends](https://github.com/gohugoio/hugo/graphs/contributors) in [Go][]. -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) | +[Developer Chat (no support)](https://gitter.im/gohugoio/hugo) | +[Documentation](https://gohugo.io/overview/introduction/) | +[Installation Guide](https://gohugo.io/overview/installing/) | +[Contribution Guide](CONTRIBUTING.md) | +[Twitter](http://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) +[![Linux and macOS Build Status](https://api.travis-ci.org/gohugoio/hugo.svg?branch=master&label=Linux+and+macOS+build "Linux and macOS Build Status")](https://travis-ci.org/gohugoio/hugo) +[![Windows Build Status](https://ci.appveyor.com/api/projects/status/a5mr220vsd091kua?svg=true&label=Windows+build "Windows Build Status")](https://ci.appveyor.com/project/bep/hugo/branch/master) +[![Dev chat at https://gitter.im/gohugoio/hugo](https://img.shields.io/badge/gitter-developer_chat-46bc99.svg)](https://gitter.im/spf13/hugo?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Go Report Card](https://goreportcard.com/badge/github.com/gohugoio/hugo)](https://goreportcard.com/report/github.com/gohugoio/hugo) -[Website] | [Installation] | [Documentation] | [Support] | [Contributing] | Mastodon - ## Overview -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 is a static HTML and CSS website generator written in [Go][]. +It is optimized for speed, easy use and configurability. +Hugo takes a directory with content and templates and renders them into a full HTML website. -Due to its flexible framework, multilingual support, and powerful taxonomy system, Hugo is widely used to create: +Hugo relies on Markdown files with front matter for meta data. +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. -- Corporate, government, nonprofit, education, news, event, and project sites -- Documentation sites -- Image portfolios -- Landing pages -- Business, professional, and personal blogs -- Resumes and CVs +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. -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 is designed to work well for any kind of website including blogs, tumbles and docs. -Hugo's fast asset pipelines include: +#### Supported Architectures -- 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 +Currently, we provide pre-built Hugo binaries for Windows, Linux, FreeBSD, NetBSD and macOS (Darwin) and [Android](https://gist.github.com/bep/a0d8a26cf6b4f8bc992729b8e50b480b) for x64, i386 and ARM architectures. -And with [Hugo Modules], you can share content, assets, data, translations, themes, templates, and configuration with other projects via public or private Git repositories. +Hugo may also be compiled from source wherever the Go compiler tool chain can run, e.g. for other operating systems including DragonFly BSD, OpenBSD, Plan 9 and Solaris. -See the [features] section of the documentation for a comprehensive summary of Hugo's capabilities. +**Complete documentation is available at [Hugo Documentation][].** -## Sponsors +## Choose How to Install -

 

-

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

+If you want to use Hugo as your site generator, simply install the Hugo binaries. +The Hugo binaries have no external dependencies. -## Editions +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. -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. +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. -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: +### Install Hugo as Your Site Generator (Binary Install) -[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/ +Use the [installation instructions in the Hugo documentation](https://gohugo.io/overview/installing/). -Unless your specific deployment needs require the extended/deploy edition, we recommend the extended edition. +### Build and Install the Binaries from Source (Advanced Install) -## Installation +Add Hugo and its package dependencies to your go `src` directory. -Install Hugo from a [prebuilt binary], package manager, or package repository. Please see the installation instructions for your operating system: + go get -v github.com/gohugoio/hugo -- [macOS] -- [Linux] -- [Windows] -- [DragonFly BSD, FreeBSD, NetBSD, and OpenBSD] +Once the `get` completes, you should find your new `hugo` (or `hugo.exe`) executable sitting inside `$GOPATH/bin/`. -## Build from source +To update Hugo’s dependencies, use `go get` with the `-u` option. -Prerequisites to build Hugo from source: + go get -u -v github.com/gohugoio/hugo -- 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 - -Build the standard edition: - -```text -go install github.com/gohugoio/hugo@latest -``` - -Build the extended edition: - -```text -CGO_ENABLED=1 go install -tags extended github.com/gohugoio/hugo@latest -``` - -Build the extended/deploy edition: - -```text -CGO_ENABLED=1 go install -tags extended,withdeploy github.com/gohugoio/hugo@latest -``` - -## 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. +## Contributing to Hugo For a complete guide to contributing to Hugo, see the [Contribution Guide](CONTRIBUTING.md). -## Dependencies +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. -Hugo stands on the shoulders of great open source libraries. Run `hugo env --logLevel info` to display a list of dependencies. +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. -
-See current dependencies +### Asking Support Questions -```text -github.com/BurntSushi/locker="v0.0.0-20171006230638-a6e239ea1c69" -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.6.0" -github.com/bep/goat="v0.5.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/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.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.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.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/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/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.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/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.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/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/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" -``` -
+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`). + +### Submitting Patches + +The Hugo project welcomes all contributors and contributions regardless of skill or experience level. +If you are interested in helping with the project, we will help you with your contribution. +Hugo is a very active project with many contributions happening daily. +Because we want to create the best possible product for our users and the best contribution experience for our developers, +we have a set of guidelines which ensure that all contributions are acceptable. +The guidelines are not intended as a filter or barrier to participation. +If you are unfamiliar with the contribution process, the Hugo team will help you and teach you how to bring your contribution in accordance with the guidelines. + +For a complete guide to contributing code to Hugo, see the [Contribution Guide](CONTRIBUTING.md). + +[![Analytics](https://ga-beacon.appspot.com/UA-7131036-6/hugo/readme)](https://github.com/igrigorik/ga-beacon) + +[Go]: https://golang.org/ +[Hugo Documentation]: https://gohugo.io/overview/introduction/ diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index 6ac90f072..000000000 --- a/SECURITY.md +++ /dev/null @@ -1,7 +0,0 @@ -## Security Policy - -### Reporting a Vulnerability - -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/). diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000..423a6e416 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,17 @@ +init: + - copy c:\MinGW\bin\mingw32-make.exe c:\MinGW\bin\make.exe + - set PATH=%PATH%;C:\MinGW\bin;%GOPATH%\bin + - go version + - go env + +# clones and cd's to path +clone_folder: C:\GOPATH\src\github.com\gohugoio\hugo + +install: + - gem install asciidoctor + - pip install docutils + +build_script: + - make hugo-race check + - hugo -s docs/ + - hugo --renderToMemory -s docs/ diff --git a/bench.sh b/bench.sh new file mode 100755 index 000000000..367a74403 --- /dev/null +++ b/bench.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + + +# 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 +go test -test.run=NONE -bench="$benchFilter" -test.benchmem=true ./$PACKAGE > /tmp/bench-$PACKAGE-$BRANCH.txt + +git checkout master +go 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 \ No newline at end of file diff --git a/benchSite.sh b/benchSite.sh new file mode 100755 index 000000000..8130559f5 --- /dev/null +++ b/benchSite.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Send in a regexp mathing 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}" + +go test -run="NONE" -bench="BenchmarkSiteBuilding/${1}$" -test.benchmem=true ./hugolib -memprofile mem.prof -cpuprofile cpu.prof \ No newline at end of file diff --git a/bufferpool/bufpool.go b/bufferpool/bufpool.go index f05675e3e..c1e4105d0 100644 --- a/bufferpool/bufpool.go +++ b/bufferpool/bufpool.go @@ -20,7 +20,7 @@ import ( ) var bufferPool = &sync.Pool{ - New: func() any { + New: func() interface{} { return &bytes.Buffer{} }, } diff --git a/bufferpool/bufpool_test.go b/bufferpool/bufpool_test.go index 023724b97..cfa247f62 100644 --- a/bufferpool/bufpool_test.go +++ b/bufferpool/bufpool_test.go @@ -14,18 +14,14 @@ package bufferpool import ( + "github.com/stretchr/testify/assert" "testing" - - qt "github.com/frankban/quicktest" ) func TestBufferPool(t *testing.T) { - c := qt.New(t) - buff := GetBuffer() buff.WriteString("do be do be do") - c.Assert(buff.String(), qt.Equals, "do be do be do") + assert.Equal(t, "do be do be do", buff.String()) PutBuffer(buff) - - c.Assert(buff.Len(), qt.Equals, 0) + assert.Equal(t, 0, buff.Len()) } diff --git a/cache/docs.go b/cache/docs.go deleted file mode 100644 index b9c49840f..000000000 --- a/cache/docs.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package cache contains the different cache implementations. -package cache diff --git a/cache/dynacache/dynacache.go b/cache/dynacache/dynacache.go deleted file mode 100644 index 25d0f9b29..000000000 --- a/cache/dynacache/dynacache.go +++ /dev/null @@ -1,647 +0,0 @@ -// 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 deleted file mode 100644 index 78b2fc82e..000000000 --- a/cache/dynacache/dynacache_test.go +++ /dev/null @@ -1,230 +0,0 @@ -// 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 deleted file mode 100644 index 01c466ca6..000000000 --- a/cache/filecache/filecache.go +++ /dev/null @@ -1,496 +0,0 @@ -// 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 - -import ( - "bytes" - "errors" - "io" - "os" - "path/filepath" - "strings" - "sync" - "time" - - "github.com/gohugoio/httpcache" - "github.com/gohugoio/hugo/common/hugio" - "github.com/gohugoio/hugo/hugofs" - - "github.com/gohugoio/hugo/helpers" - - "github.com/BurntSushi/locker" - "github.com/spf13/afero" -) - -// ErrFatal can be used to signal an unrecoverable error. -var ErrFatal = errors.New("fatal filecache error") - -const ( - FilecacheRootDirname = "filecache" -) - -// Cache caches a set of files in a directory. This is usually a file on -// disk, but since this is backed by an Afero file system, it can be anything. -type Cache struct { - Fs afero.Fs - - // Max age for items in this cache. Negative duration means forever, - // 0 is effectively turning this cache off. - maxAge time.Duration - - // When set, we just remove this entire root directory on expiration. - pruneAllRootDir string - - nlocker *lockTracker - - initOnce sync.Once - initErr error -} - -type lockTracker struct { - seenMu sync.RWMutex - seen map[string]struct{} - - *locker.Locker -} - -// Lock tracks the ids in use. We use this information to do garbage collection -// after a Hugo build. -func (l *lockTracker) Lock(id string) { - l.seenMu.RLock() - if _, seen := l.seen[id]; !seen { - l.seenMu.RUnlock() - l.seenMu.Lock() - l.seen[id] = struct{}{} - l.seenMu.Unlock() - } else { - l.seenMu.RUnlock() - } - - l.Locker.Lock(id) -} - -// ItemInfo contains info about a cached file. -type ItemInfo struct { - // This is the file's name relative to the cache's filesystem. - Name string -} - -// NewCache creates a new file cache with the given filesystem and max age. -func NewCache(fs afero.Fs, maxAge time.Duration, pruneAllRootDir string) *Cache { - return &Cache{ - Fs: fs, - nlocker: &lockTracker{Locker: locker.NewLocker(), seen: make(map[string]struct{})}, - maxAge: maxAge, - pruneAllRootDir: pruneAllRootDir, - } -} - -// lockedFile is a file with a lock that is released on Close. -type lockedFile struct { - afero.File - unlock func() -} - -func (l *lockedFile) Close() error { - defer l.unlock() - 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) - - info := ItemInfo{Name: id} - - f, err := helpers.OpenFileForWriting(c.Fs, id) - if err != nil { - c.nlocker.Unlock(id) - return info, nil, err - } - - return info, &lockedFile{ - File: f, - unlock: func() { c.nlocker.Unlock(id) }, - }, nil -} - -// ReadOrCreate tries to lookup the file in cache. -// If found, it is passed to read and then closed. -// If not found a new file is created and passed to create, which should close -// 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) { - if err := c.init(); err != nil { - return ItemInfo{}, err - } - - id = cleanID(id) - - c.nlocker.Lock(id) - defer c.nlocker.Unlock(id) - - info = ItemInfo{Name: id} - - if r := c.getOrRemove(id); r != nil { - err = read(info, r) - defer r.Close() - if err == nil || err == ErrFatal { - // See https://github.com/gohugoio/hugo/issues/6401 - // To recover from file corruption we handle read errors - // as the cache item was not found. - // Any file permission issue will also fail in the next step. - return - } - } - - f, err := helpers.OpenFileForWriting(c.Fs, id) - if err != nil { - return - } - - err = create(info, f) - - 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) - defer c.nlocker.Unlock(id) - - info := ItemInfo{Name: id} - - if r := c.getOrRemove(id); r != nil { - return info, r, nil - } - - var ( - r io.ReadCloser - err error - ) - - r, err = create() - if err != nil { - return info, nil, err - } - - if c.maxAge == 0 { - // No caching. - return info, hugio.ToReadCloser(r), nil - } - - var buff bytes.Buffer - return info, - hugio.ToReadCloser(&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) - defer c.nlocker.Unlock(id) - - info := ItemInfo{Name: id} - - if r := c.getOrRemove(id); r != nil { - defer r.Close() - b, err := io.ReadAll(r) - return info, b, err - } - - var ( - b []byte - err error - ) - - b, err = create() - if err != nil { - return info, nil, err - } - - if c.maxAge == 0 { - return info, b, 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) - defer c.nlocker.Unlock(id) - - info := ItemInfo{Name: id} - - if r := c.getOrRemove(id); r != nil { - defer r.Close() - b, err := io.ReadAll(r) - return info, b, err - } - - return info, nil, nil -} - -// 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) - defer c.nlocker.Unlock(id) - - info := ItemInfo{Name: id} - - r := c.getOrRemove(id) - - return info, r, nil -} - -// getOrRemove gets the file with the given id. If it's expired, it will -// be removed. -func (c *Cache) getOrRemove(id string) hugio.ReadSeekCloser { - if c.maxAge == 0 { - // No caching. - return nil - } - - if removed, err := c.removeIfExpired(id); err != nil || removed { - return nil - } - - f, err := c.Fs.Open(id) - if err != nil { - return nil - } - - 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 - } - - // Note the use of time.Since here. - // We cannot use Hugo's global Clock for this. - return c.maxAge == 0 || time.Since(modTime) > c.maxAge -} - -// For testing -func (c *Cache) GetString(id string) string { - id = cleanID(id) - - c.nlocker.Lock(id) - defer c.nlocker.Unlock(id) - - f, err := c.Fs.Open(id) - if err != nil { - return "" - } - defer f.Close() - - b, _ := io.ReadAll(f) - return string(b) -} - -// Caches is a named set of caches. -type Caches map[string]*Cache - -// Get gets a named cache, nil if none found. -func (f Caches) Get(name string) *Cache { - return f[strings.ToLower(name)] -} - -// NewCaches creates a new set of file caches from the given -// configuration. -func NewCaches(p *helpers.PathSpec) (Caches, error) { - dcfg := p.Cfg.GetConfigSection("caches").(Configs) - fs := p.Fs.Source - - m := make(Caches) - for k, v := range dcfg { - var cfs afero.Fs - - if v.IsResourceDir { - cfs = p.BaseFs.ResourcesCache - } else { - cfs = fs - } - - if cfs == nil { - panic("nil fs") - } - - baseDir := v.DirCompiled - - bfs := hugofs.NewBasePathFs(cfs, baseDir) - - var pruneAllRootDir string - if k == CacheKeyModules { - pruneAllRootDir = "pkg" - } - - m[k] = NewCache(bfs, v.MaxAge, pruneAllRootDir) - } - - return m, nil -} - -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 deleted file mode 100644 index a71ddb474..000000000 --- a/cache/filecache/filecache_config.go +++ /dev/null @@ -1,247 +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 filecache provides a file based cache for Hugo. -package filecache - -import ( - "errors" - "fmt" - "path" - "path/filepath" - "strings" - "time" - - "github.com/gohugoio/hugo/common/maps" - "github.com/gohugoio/hugo/config" - - "github.com/mitchellh/mapstructure" - "github.com/spf13/afero" -) - -const ( - resourcesGenDir = ":resourceDir/_gen" - cacheDirProject = ":cacheDir/:project" -) - -var defaultCacheConfig = FileCacheConfig{ - MaxAge: -1, // Never expire - Dir: cacheDirProject, -} - -const ( - CacheKeyGetJSON = "getjson" - CacheKeyGetCSV = "getcsv" - CacheKeyImages = "images" - CacheKeyAssets = "assets" - CacheKeyModules = "modules" - CacheKeyGetResource = "getresource" - CacheKeyMisc = "misc" -) - -type Configs map[string]FileCacheConfig - -// For internal use. -func (c Configs) CacheDirModules() string { - return c[CacheKeyModules].DirCompiled -} - -var defaultCacheConfigs = Configs{ - CacheKeyModules: { - MaxAge: -1, - Dir: ":cacheDir/modules", - }, - CacheKeyGetJSON: defaultCacheConfig, - CacheKeyGetCSV: defaultCacheConfig, - CacheKeyImages: { - MaxAge: -1, - Dir: resourcesGenDir, - }, - CacheKeyAssets: { - MaxAge: -1, - Dir: resourcesGenDir, - }, - 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 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". - MaxAge time.Duration - - // The directory where files are stored. - Dir string - DirCompiled string `json:"-"` - - // Will resources/_gen will get its own composite filesystem that - // also checks any theme. - IsResourceDir bool `json:"-"` -} - -// GetJSONCache gets the file cache for getJSON. -func (f Caches) GetJSONCache() *Cache { - return f[CacheKeyGetJSON] -} - -// GetCSVCache gets the file cache for getCSV. -func (f Caches) GetCSVCache() *Cache { - return f[CacheKeyGetCSV] -} - -// ImageCache gets the file cache for processed images. -func (f Caches) ImageCache() *Cache { - return f[CacheKeyImages] -} - -// ModulesCache gets the file cache for Hugo Modules. -func (f Caches) ModulesCache() *Cache { - return f[CacheKeyModules] -} - -// AssetsCache gets the file cache for assets (processed resources, SCSS etc.). -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] -} - -func DecodeConfig(fs afero.Fs, bcfg config.BaseConfig, m map[string]any) (Configs, error) { - c := make(Configs) - valid := make(map[string]bool) - // Add defaults - for k, v := range defaultCacheConfigs { - c[k] = v - valid[k] = true - } - - _, isOsFs := fs.(*afero.OsFs) - - for k, v := range m { - if _, ok := v.(maps.Params); !ok { - continue - } - cc := defaultCacheConfig - - dc := &mapstructure.DecoderConfig{ - Result: &cc, - DecodeHook: mapstructure.StringToTimeDurationHookFunc(), - WeaklyTypedInput: true, - } - - decoder, err := mapstructure.NewDecoder(dc) - if err != nil { - return c, err - } - - if err := decoder.Decode(v); err != nil { - return nil, fmt.Errorf("failed to decode filecache config: %w", err) - } - - if cc.Dir == "" { - return c, errors.New("must provide cache Dir") - } - - name := strings.ToLower(k) - if !valid[name] { - return nil, fmt.Errorf("%q is not a valid cache name", name) - } - - c[name] = cc - } - - for k, v := range c { - dir := filepath.ToSlash(filepath.Clean(v.Dir)) - hadSlash := strings.HasPrefix(dir, "/") - parts := strings.Split(dir, "/") - - for i, part := range parts { - if strings.HasPrefix(part, ":") { - resolved, isResource, err := resolveDirPlaceholder(fs, bcfg, part) - if err != nil { - return c, err - } - if isResource { - v.IsResourceDir = true - } - parts[i] = resolved - } - } - - dir = path.Join(parts...) - if hadSlash { - dir = "/" + dir - } - v.DirCompiled = filepath.Clean(filepath.FromSlash(dir)) - - if !v.IsResourceDir { - if isOsFs && !filepath.IsAbs(v.DirCompiled) { - return c, fmt.Errorf("%q must resolve to an absolute directory", v.DirCompiled) - } - - // Avoid cache in root, e.g. / (Unix) or c:\ (Windows) - if len(strings.TrimPrefix(v.DirCompiled, filepath.VolumeName(v.DirCompiled))) == 1 { - return c, fmt.Errorf("%q is a root folder and not allowed as cache dir", v.DirCompiled) - } - } - - if !strings.HasPrefix(v.DirCompiled, "_gen") { - // We do cache eviction (file removes) and since the user can set - // his/hers own cache directory, we really want to make sure - // we do not delete any files that do not belong to this cache. - // We do add the cache name as the root, but this is an extra safe - // guard. We skip the files inside /resources/_gen/ because - // that would be breaking. - v.DirCompiled = filepath.Join(v.DirCompiled, FilecacheRootDirname, k) - } else { - v.DirCompiled = filepath.Join(v.DirCompiled, k) - } - - c[k] = v - } - - return c, nil -} - -// 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 - case ":cachedir": - return bcfg.CacheDir, false, nil - case ":project": - return filepath.Base(bcfg.WorkingDir), false, nil - } - - return "", false, fmt.Errorf("%q is not a valid placeholder (valid values are :cacheDir or :resourceDir)", placeholder) -} diff --git a/cache/filecache/filecache_config_test.go b/cache/filecache/filecache_config_test.go deleted file mode 100644 index c6d346dfc..000000000 --- a/cache/filecache/filecache_config_test.go +++ /dev/null @@ -1,146 +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 filecache_test - -import ( - "path/filepath" - "runtime" - "testing" - "time" - - "github.com/spf13/afero" - - "github.com/gohugoio/hugo/cache/filecache" - "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/config/testconfig" - - qt "github.com/frankban/quicktest" -) - -func TestDecodeConfig(t *testing.T) { - t.Parallel() - - c := qt.New(t) - - configStr := ` -resourceDir = "myresources" -contentDir = "content" -dataDir = "data" -i18nDir = "i18n" -layoutDir = "layouts" -assetDir = "assets" -archetypeDir = "archetypes" - -[caches] -[caches.getJSON] -maxAge = "10m" -dir = "/path/to/c1" -[caches.getCSV] -maxAge = "11h" -dir = "/path/to/c2" -[caches.images] -dir = "/path/to/c3" -[caches.getResource] -dir = "/path/to/c4" -` - - cfg, err := config.FromConfigString(configStr, "toml") - c.Assert(err, qt.IsNil) - fs := afero.NewMemMapFs() - decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches - c.Assert(len(decoded), qt.Equals, 7) - - c2 := decoded["getcsv"] - c.Assert(c2.MaxAge.String(), qt.Equals, "11h0m0s") - c.Assert(c2.DirCompiled, qt.Equals, filepath.FromSlash("/path/to/c2/filecache/getcsv")) - - c3 := decoded["images"] - c.Assert(c3.MaxAge, qt.Equals, time.Duration(-1)) - c.Assert(c3.DirCompiled, qt.Equals, filepath.FromSlash("/path/to/c3/filecache/images")) - - c4 := decoded["getresource"] - c.Assert(c4.MaxAge, qt.Equals, time.Duration(-1)) - c.Assert(c4.DirCompiled, qt.Equals, filepath.FromSlash("/path/to/c4/filecache/getresource")) -} - -func TestDecodeConfigIgnoreCache(t *testing.T) { - t.Parallel() - - c := qt.New(t) - - configStr := ` -resourceDir = "myresources" -contentDir = "content" -dataDir = "data" -i18nDir = "i18n" -layoutDir = "layouts" -assetDir = "assets" -archeTypedir = "archetypes" - -ignoreCache = true -[caches] -[caches.getJSON] -maxAge = 1234 -dir = "/path/to/c1" -[caches.getCSV] -maxAge = 3456 -dir = "/path/to/c2" -[caches.images] -dir = "/path/to/c3" -[caches.getResource] -dir = "/path/to/c4" -` - - cfg, err := config.FromConfigString(configStr, "toml") - c.Assert(err, qt.IsNil) - fs := afero.NewMemMapFs() - decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches - c.Assert(len(decoded), qt.Equals, 7) - - for _, v := range decoded { - c.Assert(v.MaxAge, qt.Equals, time.Duration(0)) - } -} - -func TestDecodeConfigDefault(t *testing.T) { - c := qt.New(t) - cfg := config.New() - - if runtime.GOOS == "windows" { - cfg.Set("resourceDir", "c:\\cache\\resources") - cfg.Set("cacheDir", "c:\\cache\\thecache") - - } else { - cfg.Set("resourceDir", "/cache/resources") - cfg.Set("cacheDir", "/cache/thecache") - } - cfg.Set("workingDir", filepath.FromSlash("/my/cool/hugoproject")) - - fs := afero.NewMemMapFs() - decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches - c.Assert(len(decoded), qt.Equals, 7) - - imgConfig := decoded[filecache.CacheKeyImages] - jsonConfig := decoded[filecache.CacheKeyGetJSON] - - if runtime.GOOS == "windows" { - c.Assert(imgConfig.DirCompiled, qt.Equals, filepath.FromSlash("_gen/images")) - } else { - c.Assert(imgConfig.DirCompiled, qt.Equals, "_gen/images") - c.Assert(jsonConfig.DirCompiled, qt.Equals, "/cache/thecache/hugoproject/filecache/getjson") - } - - c.Assert(imgConfig.IsResourceDir, qt.Equals, true) - c.Assert(jsonConfig.IsResourceDir, qt.Equals, false) -} diff --git a/cache/filecache/filecache_integration_test.go b/cache/filecache/filecache_integration_test.go deleted file mode 100644 index 1e920c29f..000000000 --- a/cache/filecache/filecache_integration_test.go +++ /dev/null @@ -1,106 +0,0 @@ -// 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 deleted file mode 100644 index 6f224cef4..000000000 --- a/cache/filecache/filecache_pruner.go +++ /dev/null @@ -1,137 +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 filecache - -import ( - "fmt" - "io" - "os" - - "github.com/gohugoio/hugo/common/herrors" - "github.com/gohugoio/hugo/hugofs" - - "github.com/spf13/afero" -) - -// Prune removes expired and unused items from this cache. -// The last one requires a full build so the cache usage can be tracked. -// Note that we operate directly on the filesystem here, so this is not -// thread safe. -func (c Caches) Prune() (int, error) { - counter := 0 - for k, cache := range c { - count, err := cache.Prune(false) - - counter += count - - if err != nil { - if herrors.IsNotExist(err) { - continue - } - return counter, fmt.Errorf("failed to prune cache %q: %w", k, err) - } - - } - - return counter, nil -} - -// Prune removes expired and unused items from this cache. -// If force is set, everything will be removed not considering expiry time. -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 - } - - name = cleanID(name) - - if info.IsDir() { - f, err := c.Fs.Open(name) - if err != nil { - // This cache dir may not exist. - return nil - } - _, err = f.Readdirnames(1) - f.Close() - if err == io.EOF { - // Empty dir. - if name == "." { - // e.g. /_gen/images -- keep it even if empty. - err = nil - } else { - err = c.Fs.Remove(name) - } - } - - if err != nil && !herrors.IsNotExist(err) { - return err - } - - return nil - } - - shouldRemove := force || c.isExpired(info.ModTime()) - - if !shouldRemove && len(c.nlocker.seen) > 0 { - // Remove it if it's not been touched/used in the last build. - _, seen := c.nlocker.seen[name] - shouldRemove = !seen - } - - if shouldRemove { - err := c.Fs.Remove(name) - if err == nil { - counter++ - } - - if err != nil && !herrors.IsNotExist(err) { - return err - } - - } - - return nil - }) - - return counter, err -} - -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) { - return 0, nil - } - return 0, err - } - - if !force && !c.isExpired(info.ModTime()) { - return 0, nil - } - - return hugofs.MakeReadableAndRemoveAllModulePkgDir(c.Fs, c.pruneAllRootDir) -} diff --git a/cache/filecache/filecache_pruner_test.go b/cache/filecache/filecache_pruner_test.go deleted file mode 100644 index b49ba7645..000000000 --- a/cache/filecache/filecache_pruner_test.go +++ /dev/null @@ -1,111 +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 filecache_test - -import ( - "fmt" - "testing" - "time" - - "github.com/gohugoio/hugo/cache/filecache" - "github.com/spf13/afero" - - qt "github.com/frankban/quicktest" -) - -func TestPrune(t *testing.T) { - t.Parallel() - - c := qt.New(t) - - configStr := ` -resourceDir = "myresources" -contentDir = "content" -dataDir = "data" -i18nDir = "i18n" -layoutDir = "layouts" -assetDir = "assets" -archeTypedir = "archetypes" - -[caches] -[caches.getjson] -maxAge = "200ms" -dir = "/cache/c" -[caches.getcsv] -maxAge = "200ms" -dir = "/cache/d" -[caches.assets] -maxAge = "200ms" -dir = ":resourceDir/_gen" -[caches.images] -maxAge = "200ms" -dir = ":resourceDir/_gen" -` - - for _, name := range []string{filecache.CacheKeyGetCSV, filecache.CacheKeyGetJSON, filecache.CacheKeyAssets, filecache.CacheKeyImages} { - msg := qt.Commentf("cache: %s", name) - p := newPathsSpec(t, afero.NewMemMapFs(), configStr) - caches, err := filecache.NewCaches(p) - c.Assert(err, qt.IsNil) - cache := caches[name] - for i := range 10 { - id := fmt.Sprintf("i%d", i) - cache.GetOrCreateBytes(id, func() ([]byte, error) { - return []byte("abc"), nil - }) - if i == 4 { - // This will expire the first 5 - time.Sleep(201 * time.Millisecond) - } - } - - count, err := caches.Prune() - c.Assert(err, qt.IsNil) - c.Assert(count, qt.Equals, 5, msg) - - for i := range 10 { - id := fmt.Sprintf("i%d", i) - v := cache.GetString(id) - if i < 5 { - c.Assert(v, qt.Equals, "") - } else { - c.Assert(v, qt.Equals, "abc") - } - } - - caches, err = filecache.NewCaches(p) - c.Assert(err, qt.IsNil) - cache = caches[name] - // Touch one and then prune. - cache.GetOrCreateBytes("i5", func() ([]byte, error) { - return []byte("abc"), nil - }) - - count, err = caches.Prune() - c.Assert(err, qt.IsNil) - c.Assert(count, qt.Equals, 4) - - // Now only the i5 should be left. - for i := range 10 { - id := fmt.Sprintf("i%d", i) - v := cache.GetString(id) - if i != 5 { - c.Assert(v, qt.Equals, "") - } else { - c.Assert(v, qt.Equals, "abc") - } - } - - } -} diff --git a/cache/filecache/filecache_test.go b/cache/filecache/filecache_test.go deleted file mode 100644 index a30aaa50b..000000000 --- a/cache/filecache/filecache_test.go +++ /dev/null @@ -1,276 +0,0 @@ -// 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 ( - "errors" - "fmt" - "io" - "strings" - "sync" - "testing" - "time" - - "github.com/gohugoio/hugo/cache/filecache" - "github.com/gohugoio/hugo/common/hugio" - "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/config/testconfig" - "github.com/gohugoio/hugo/helpers" - - "github.com/gohugoio/hugo/hugofs" - "github.com/spf13/afero" - - qt "github.com/frankban/quicktest" -) - -func TestFileCache(t *testing.T) { - t.Parallel() - c := qt.New(t) - - tempWorkingDir := t.TempDir() - tempCacheDir := t.TempDir() - - osfs := afero.NewOsFs() - - for _, test := range []struct { - cacheDir string - workingDir string - }{ - // Run with same dirs twice to make sure that works. - {tempCacheDir, tempWorkingDir}, - {tempCacheDir, tempWorkingDir}, - } { - - configStr := ` -workingDir = "WORKING_DIR" -resourceDir = "resources" -cacheDir = "CACHEDIR" -contentDir = "content" -dataDir = "data" -i18nDir = "i18n" -layoutDir = "layouts" -assetDir = "assets" -archeTypedir = "archetypes" - -[caches] -[caches.getJSON] -maxAge = "10h" -dir = ":cacheDir/c" - -` - - winPathSep := "\\\\" - - replacer := strings.NewReplacer("CACHEDIR", test.cacheDir, "WORKING_DIR", test.workingDir) - - configStr = replacer.Replace(configStr) - configStr = strings.Replace(configStr, "\\", winPathSep, -1) - - p := newPathsSpec(t, osfs, configStr) - - caches, err := filecache.NewCaches(p) - c.Assert(err, qt.IsNil) - - cache := caches.Get("GetJSON") - c.Assert(cache, qt.Not(qt.IsNil)) - - cache = caches.Get("Images") - c.Assert(cache, qt.Not(qt.IsNil)) - - rf := func(s string) func() (io.ReadCloser, error) { - return func() (io.ReadCloser, error) { - return struct { - io.ReadSeeker - io.Closer - }{ - strings.NewReader(s), - io.NopCloser(nil), - }, nil - } - } - - bf := func() ([]byte, error) { - return []byte("bcd"), nil - } - - for _, ca := range []*filecache.Cache{caches.ImageCache(), caches.AssetsCache(), caches.GetJSONCache(), caches.GetCSVCache()} { - for range 2 { - info, r, err := ca.GetOrCreate("a", rf("abc")) - c.Assert(err, qt.IsNil) - c.Assert(r, qt.Not(qt.IsNil)) - c.Assert(info.Name, qt.Equals, "a") - b, _ := io.ReadAll(r) - r.Close() - c.Assert(string(b), qt.Equals, "abc") - - info, b, err = ca.GetOrCreateBytes("b", bf) - c.Assert(err, qt.IsNil) - c.Assert(r, qt.Not(qt.IsNil)) - c.Assert(info.Name, qt.Equals, "b") - c.Assert(string(b), qt.Equals, "bcd") - - _, b, err = ca.GetOrCreateBytes("a", bf) - c.Assert(err, qt.IsNil) - c.Assert(string(b), qt.Equals, "abc") - - _, r, err = ca.GetOrCreate("a", rf("bcd")) - c.Assert(err, qt.IsNil) - b, _ = io.ReadAll(r) - r.Close() - c.Assert(string(b), qt.Equals, "abc") - } - } - - c.Assert(caches.Get("getJSON"), qt.Not(qt.IsNil)) - - info, w, err := caches.ImageCache().WriteCloser("mykey") - c.Assert(err, qt.IsNil) - c.Assert(info.Name, qt.Equals, "mykey") - io.WriteString(w, "Hugo is great!") - w.Close() - c.Assert(caches.ImageCache().GetString("mykey"), qt.Equals, "Hugo is great!") - - info, r, err := caches.ImageCache().Get("mykey") - c.Assert(err, qt.IsNil) - c.Assert(r, qt.Not(qt.IsNil)) - c.Assert(info.Name, qt.Equals, "mykey") - b, _ := io.ReadAll(r) - r.Close() - c.Assert(string(b), qt.Equals, "Hugo is great!") - - info, b, err = caches.ImageCache().GetBytes("mykey") - c.Assert(err, qt.IsNil) - c.Assert(info.Name, qt.Equals, "mykey") - c.Assert(string(b), qt.Equals, "Hugo is great!") - - } -} - -func TestFileCacheConcurrent(t *testing.T) { - t.Parallel() - - c := qt.New(t) - - configStr := ` -resourceDir = "myresources" -contentDir = "content" -dataDir = "data" -i18nDir = "i18n" -layoutDir = "layouts" -assetDir = "assets" -archeTypedir = "archetypes" - -[caches] -[caches.getjson] -maxAge = "1s" -dir = "/cache/c" - -` - - p := newPathsSpec(t, afero.NewMemMapFs(), configStr) - - caches, err := filecache.NewCaches(p) - c.Assert(err, qt.IsNil) - - const cacheName = "getjson" - - filenameData := func(i int) (string, string) { - data := fmt.Sprintf("data: %d", i) - filename := fmt.Sprintf("file%d", i) - return filename, data - } - - var wg sync.WaitGroup - - for i := range 50 { - wg.Add(1) - go func(i int) { - defer wg.Done() - for range 20 { - ca := caches.Get(cacheName) - c.Assert(ca, qt.Not(qt.IsNil)) - filename, data := filenameData(i) - _, r, err := ca.GetOrCreate(filename, func() (io.ReadCloser, error) { - return hugio.ToReadCloser(strings.NewReader(data)), nil - }) - c.Assert(err, qt.IsNil) - b, _ := io.ReadAll(r) - r.Close() - c.Assert(string(b), qt.Equals, data) - // Trigger some expiration. - time.Sleep(50 * time.Millisecond) - } - }(i) - - } - wg.Wait() -} - -func TestFileCacheReadOrCreateErrorInRead(t *testing.T) { - t.Parallel() - c := qt.New(t) - - var result string - - rf := func(failLevel int) func(info filecache.ItemInfo, r io.ReadSeeker) error { - return func(info filecache.ItemInfo, r io.ReadSeeker) error { - if failLevel > 0 { - if failLevel > 1 { - return filecache.ErrFatal - } - return errors.New("fail") - } - - b, _ := io.ReadAll(r) - result = string(b) - - return nil - } - } - - bf := func(s string) func(info filecache.ItemInfo, w io.WriteCloser) error { - return func(info filecache.ItemInfo, w io.WriteCloser) error { - defer w.Close() - result = s - _, err := w.Write([]byte(s)) - return err - } - } - - cache := filecache.NewCache(afero.NewMemMapFs(), 100*time.Hour, "") - - const id = "a32" - - _, err := cache.ReadOrCreate(id, rf(0), bf("v1")) - c.Assert(err, qt.IsNil) - c.Assert(result, qt.Equals, "v1") - _, err = cache.ReadOrCreate(id, rf(0), bf("v2")) - c.Assert(err, qt.IsNil) - c.Assert(result, qt.Equals, "v1") - _, err = cache.ReadOrCreate(id, rf(1), bf("v3")) - c.Assert(err, qt.IsNil) - c.Assert(result, qt.Equals, "v3") - _, err = cache.ReadOrCreate(id, rf(2), bf("v3")) - c.Assert(err, qt.Equals, filecache.ErrFatal) -} - -func newPathsSpec(t *testing.T, fs afero.Fs, configStr string) *helpers.PathSpec { - c := qt.New(t) - cfg, err := config.FromConfigString(configStr, "toml") - c.Assert(err, qt.IsNil) - acfg := testconfig.GetTestConfig(fs, cfg) - p, err := helpers.NewPathSpec(hugofs.NewFrom(fs, acfg.BaseConfig()), acfg, nil) - c.Assert(err, qt.IsNil) - return p -} diff --git a/cache/httpcache/httpcache.go b/cache/httpcache/httpcache.go deleted file mode 100644 index bd6d4bf7d..000000000 --- a/cache/httpcache/httpcache.go +++ /dev/null @@ -1,229 +0,0 @@ -// 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 deleted file mode 100644 index 4d6a5f718..000000000 --- a/cache/httpcache/httpcache_integration_test.go +++ /dev/null @@ -1,95 +0,0 @@ -// 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 deleted file mode 100644 index 60c07d056..000000000 --- a/cache/httpcache/httpcache_test.go +++ /dev/null @@ -1,73 +0,0 @@ -// 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/partitioned_lazy_cache.go b/cache/partitioned_lazy_cache.go new file mode 100644 index 000000000..9baf0377d --- /dev/null +++ b/cache/partitioned_lazy_cache.go @@ -0,0 +1,80 @@ +// Copyright 2017-present 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 cache + +import ( + "sync" +) + +// Partition represents a cache partition where Load is the callback +// for when the partition is needed. +type Partition struct { + Key string + Load func() (map[string]interface{}, error) +} + +type lazyPartition struct { + initSync sync.Once + cache map[string]interface{} + load func() (map[string]interface{}, error) +} + +func (l *lazyPartition) init() error { + var err error + l.initSync.Do(func() { + var c map[string]interface{} + c, err = l.load() + l.cache = c + }) + + return err +} + +// PartitionedLazyCache is a lazily loaded cache paritioned by a supplied string key. +type PartitionedLazyCache struct { + partitions map[string]*lazyPartition +} + +// NewPartitionedLazyCache creates a new NewPartitionedLazyCache with the supplied +// partitions. +func NewPartitionedLazyCache(partitions ...Partition) *PartitionedLazyCache { + lazyPartitions := make(map[string]*lazyPartition, len(partitions)) + for _, partition := range partitions { + lazyPartitions[partition.Key] = &lazyPartition{load: partition.Load} + } + cache := &PartitionedLazyCache{partitions: lazyPartitions} + + return cache +} + +// Get initializes the partition if not already done so, then looks up the given +// key in the given partition, returns nil if no value found. +func (c *PartitionedLazyCache) Get(partition, key string) (interface{}, error) { + p, found := c.partitions[partition] + + if !found { + return nil, nil + } + + if err := p.init(); err != nil { + return nil, err + } + + if v, found := p.cache[key]; found { + return v, nil + } + + return nil, nil + +} diff --git a/cache/partitioned_lazy_cache_test.go b/cache/partitioned_lazy_cache_test.go new file mode 100644 index 000000000..ba8b6a454 --- /dev/null +++ b/cache/partitioned_lazy_cache_test.go @@ -0,0 +1,138 @@ +// Copyright 2017-present 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 cache + +import ( + "errors" + "sync" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewPartitionedLazyCache(t *testing.T) { + t.Parallel() + + assert := require.New(t) + + p1 := Partition{ + Key: "p1", + Load: func() (map[string]interface{}, error) { + return map[string]interface{}{ + "p1_1": "p1v1", + "p1_2": "p1v2", + "p1_nil": nil, + }, nil + }, + } + + p2 := Partition{ + Key: "p2", + Load: func() (map[string]interface{}, error) { + return map[string]interface{}{ + "p2_1": "p2v1", + "p2_2": "p2v2", + "p2_3": "p2v3", + }, nil + }, + } + + cache := NewPartitionedLazyCache(p1, p2) + + v, err := cache.Get("p1", "p1_1") + assert.NoError(err) + assert.Equal("p1v1", v) + + v, err = cache.Get("p1", "p2_1") + assert.NoError(err) + assert.Nil(v) + + v, err = cache.Get("p1", "p1_nil") + assert.NoError(err) + assert.Nil(v) + + v, err = cache.Get("p2", "p2_3") + assert.NoError(err) + assert.Equal("p2v3", v) + + v, err = cache.Get("doesnotexist", "p1_1") + assert.NoError(err) + assert.Nil(v) + + v, err = cache.Get("p1", "doesnotexist") + assert.NoError(err) + assert.Nil(v) + + errorP := Partition{ + Key: "p3", + Load: func() (map[string]interface{}, error) { + return nil, errors.New("Failed") + }, + } + + cache = NewPartitionedLazyCache(errorP) + + v, err = cache.Get("p1", "doesnotexist") + assert.NoError(err) + assert.Nil(v) + + _, err = cache.Get("p3", "doesnotexist") + assert.Error(err) + +} + +func TestConcurrentPartitionedLazyCache(t *testing.T) { + t.Parallel() + + assert := require.New(t) + + var wg sync.WaitGroup + + p1 := Partition{ + Key: "p1", + Load: func() (map[string]interface{}, error) { + return map[string]interface{}{ + "p1_1": "p1v1", + "p1_2": "p1v2", + "p1_nil": nil, + }, nil + }, + } + + p2 := Partition{ + Key: "p2", + Load: func() (map[string]interface{}, error) { + return map[string]interface{}{ + "p2_1": "p2v1", + "p2_2": "p2v2", + "p2_3": "p2v3", + }, nil + }, + } + + cache := NewPartitionedLazyCache(p1, p2) + + for i := 0; i < 100; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 10; j++ { + v, err := cache.Get("p1", "p1_1") + assert.NoError(err) + assert.Equal("p1v1", v) + } + }() + } + wg.Wait() +} diff --git a/check_gofmt.sh b/check_gofmt.sh deleted file mode 100755 index c77517d3f..000000000 --- a/check_gofmt.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env bash -diff <(gofmt -d .) <(printf '') \ No newline at end of file diff --git a/codegen/methods.go b/codegen/methods.go deleted file mode 100644 index 08ac97b00..000000000 --- a/codegen/methods.go +++ /dev/null @@ -1,540 +0,0 @@ -// Copyright 2019 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 codegen contains helpers for code generation. -package codegen - -import ( - "fmt" - "go/ast" - "go/parser" - "go/token" - "os" - "path" - "path/filepath" - "reflect" - "regexp" - "slices" - "sort" - "strings" - "sync" -) - -// Make room for insertions -const weightWidth = 1000 - -// NewInspector creates a new Inspector given a source root. -func NewInspector(root string) *Inspector { - return &Inspector{ProjectRootDir: root} -} - -// Inspector provides methods to help code generation. It uses a combination -// of reflection and source code AST to do the heavy lifting. -type Inspector struct { - ProjectRootDir string - - init sync.Once - - // Determines method order. Go's reflect sorts lexicographically, so - // we must parse the source to preserve this order. - methodWeight map[string]map[string]int -} - -// MethodsFromTypes create a method set from the include slice, excluding any -// method in exclude. -func (c *Inspector) MethodsFromTypes(include []reflect.Type, exclude []reflect.Type) Methods { - c.parseSource() - - var methods Methods - - excludes := make(map[string]bool) - - if len(exclude) > 0 { - for _, m := range c.MethodsFromTypes(exclude, nil) { - excludes[m.Name] = true - } - } - - // There may be overlapping interfaces in types. Do a simple check for now. - seen := make(map[string]bool) - - nameAndPackage := func(t reflect.Type) (string, string) { - var name, pkg string - - isPointer := t.Kind() == reflect.Ptr - - if isPointer { - t = t.Elem() - } - - pkgPrefix := "" - if pkgPath := t.PkgPath(); pkgPath != "" { - pkgPath = strings.TrimSuffix(pkgPath, "/") - _, shortPath := path.Split(pkgPath) - pkgPrefix = shortPath + "." - pkg = pkgPath - } - - name = t.Name() - if name == "" { - // interface{} - name = t.String() - } - - if isPointer { - pkgPrefix = "*" + pkgPrefix - } - - name = pkgPrefix + name - - return name, pkg - } - - for _, t := range include { - for i := range t.NumMethod() { - - m := t.Method(i) - if excludes[m.Name] || seen[m.Name] { - continue - } - - seen[m.Name] = true - - if m.PkgPath != "" { - // Not exported - continue - } - - numIn := m.Type.NumIn() - - ownerName, _ := nameAndPackage(t) - - method := Method{Owner: t, OwnerName: ownerName, Name: m.Name} - - for i := range numIn { - in := m.Type.In(i) - - name, pkg := nameAndPackage(in) - - if pkg != "" { - method.Imports = append(method.Imports, pkg) - } - - method.In = append(method.In, name) - } - - numOut := m.Type.NumOut() - - if numOut > 0 { - for i := range numOut { - out := m.Type.Out(i) - name, pkg := nameAndPackage(out) - - if pkg != "" { - method.Imports = append(method.Imports, pkg) - } - - method.Out = append(method.Out, name) - } - } - - methods = append(methods, method) - } - } - - sort.SliceStable(methods, func(i, j int) bool { - mi, mj := methods[i], methods[j] - - wi := c.methodWeight[mi.OwnerName][mi.Name] - wj := c.methodWeight[mj.OwnerName][mj.Name] - - if wi == wj { - return mi.Name < mj.Name - } - - return wi < wj - }) - - return methods -} - -func (c *Inspector) parseSource() { - c.init.Do(func() { - if !strings.Contains(c.ProjectRootDir, "hugo") { - panic("dir must be set to the Hugo root") - } - - c.methodWeight = make(map[string]map[string]int) - dirExcludes := regexp.MustCompile("docs|examples") - fileExcludes := regexp.MustCompile("autogen") - var filenames []string - - filepath.Walk(c.ProjectRootDir, func(path string, info os.FileInfo, err error) error { - if info.IsDir() { - if dirExcludes.MatchString(info.Name()) { - return filepath.SkipDir - } - } - - if !strings.HasSuffix(path, ".go") || fileExcludes.MatchString(path) { - return nil - } - - filenames = append(filenames, path) - - return nil - }) - - for _, filename := range filenames { - - pkg := c.packageFromPath(filename) - - fset := token.NewFileSet() - node, err := parser.ParseFile(fset, filename, nil, parser.ParseComments) - if err != nil { - panic(err) - } - - ast.Inspect(node, func(n ast.Node) bool { - switch t := n.(type) { - case *ast.TypeSpec: - if t.Name.IsExported() { - switch it := t.Type.(type) { - case *ast.InterfaceType: - iface := pkg + "." + t.Name.Name - methodNames := collectMethodsRecursive(pkg, it.Methods.List) - weights := make(map[string]int) - weight := weightWidth - for _, name := range methodNames { - weights[name] = weight - weight += weightWidth - } - c.methodWeight[iface] = weights - } - } - } - return true - }) - - } - - // Complement - for _, v1 := range c.methodWeight { - for k2, w := range v1 { - if v, found := c.methodWeight[k2]; found { - for k3, v3 := range v { - v1[k3] = (v3 / weightWidth) + w - } - } - } - } - }) -} - -func (c *Inspector) packageFromPath(p string) string { - p = filepath.ToSlash(p) - base := path.Base(p) - if !strings.Contains(base, ".") { - return base - } - return path.Base(strings.TrimSuffix(p, base)) -} - -// Method holds enough information about it to recreate it. -type Method struct { - // The interface we extracted this method from. - Owner reflect.Type - - // String version of the above, on the form PACKAGE.NAME, e.g. - // page.Page - OwnerName string - - // Method name. - Name string - - // Imports needed to satisfy the method signature. - Imports []string - - // Argument types, including any package prefix, e.g. string, int, interface{}, - // net.Url - In []string - - // Return types. - Out []string -} - -// Declaration creates a method declaration (without any body) for the given receiver. -func (m Method) Declaration(receiver string) string { - return fmt.Sprintf("func (%s %s) %s%s %s", receiverShort(receiver), receiver, m.Name, m.inStr(), m.outStr()) -} - -// DeclarationNamed creates a method declaration (without any body) for the given receiver -// with named return values. -func (m Method) DeclarationNamed(receiver string) string { - return fmt.Sprintf("func (%s %s) %s%s %s", receiverShort(receiver), receiver, m.Name, m.inStr(), m.outStrNamed()) -} - -// Delegate creates a delegate call string. -func (m Method) Delegate(receiver, delegate string) string { - ret := "" - if len(m.Out) > 0 { - ret = "return " - } - return fmt.Sprintf("%s%s.%s.%s%s", ret, receiverShort(receiver), delegate, m.Name, m.inOutStr()) -} - -func (m Method) String() string { - return m.Name + m.inStr() + " " + m.outStr() + "\n" -} - -func (m Method) inOutStr() string { - if len(m.In) == 0 { - return "()" - } - - args := make([]string, len(m.In)) - for i := range args { - args[i] = fmt.Sprintf("arg%d", i) - } - return "(" + strings.Join(args, ", ") + ")" -} - -func (m Method) inStr() string { - if len(m.In) == 0 { - return "()" - } - - args := make([]string, len(m.In)) - for i := range args { - args[i] = fmt.Sprintf("arg%d %s", i, m.In[i]) - } - return "(" + strings.Join(args, ", ") + ")" -} - -func (m Method) outStr() string { - if len(m.Out) == 0 { - return "" - } - if len(m.Out) == 1 { - return m.Out[0] - } - - return "(" + strings.Join(m.Out, ", ") + ")" -} - -func (m Method) outStrNamed() string { - if len(m.Out) == 0 { - return "" - } - - outs := make([]string, len(m.Out)) - for i := range outs { - outs[i] = fmt.Sprintf("o%d %s", i, m.Out[i]) - } - - return "(" + strings.Join(outs, ", ") + ")" -} - -// Methods represents a list of methods for one or more interfaces. -// The order matches the defined order in their source file(s). -type Methods []Method - -// Imports returns a sorted list of package imports needed to satisfy the -// signatures of all methods. -func (m Methods) Imports() []string { - var pkgImports []string - for _, method := range m { - pkgImports = append(pkgImports, method.Imports...) - } - if len(pkgImports) > 0 { - pkgImports = uniqueNonEmptyStrings(pkgImports) - sort.Strings(pkgImports) - } - return pkgImports -} - -// ToMarshalJSON creates a MarshalJSON method for these methods. Any method name -// matching any of the regexps in excludes will be ignored. -func (m Methods) ToMarshalJSON(receiver, pkgPath string, excludes ...string) (string, []string) { - var sb strings.Builder - - r := receiverShort(receiver) - what := firstToUpper(trimAsterisk(receiver)) - pgkName := path.Base(pkgPath) - - fmt.Fprintf(&sb, "func Marshal%sToJSON(%s %s) ([]byte, error) {\n", what, r, receiver) - - var methods Methods - excludeRes := make([]*regexp.Regexp, len(excludes)) - - for i, exclude := range excludes { - excludeRes[i] = regexp.MustCompile(exclude) - } - - for _, method := range m { - // Exclude methods with arguments and incompatible return values - if len(method.In) > 0 || len(method.Out) == 0 || len(method.Out) > 2 { - continue - } - - if len(method.Out) == 2 { - if method.Out[1] != "error" { - continue - } - } - - for _, re := range excludeRes { - if re.MatchString(method.Name) { - continue - } - } - - methods = append(methods, method) - } - - for _, method := range methods { - varn := varName(method.Name) - if len(method.Out) == 1 { - fmt.Fprintf(&sb, "\t%s := %s.%s()\n", varn, r, method.Name) - } else { - fmt.Fprintf(&sb, "\t%s, err := %s.%s()\n", varn, r, method.Name) - fmt.Fprint(&sb, "\tif err != nil {\n\t\treturn nil, err\n\t}\n") - } - } - - fmt.Fprint(&sb, "\n\ts := struct {\n") - - for _, method := range methods { - fmt.Fprintf(&sb, "\t\t%s %s\n", method.Name, typeName(method.Out[0], pgkName)) - } - - fmt.Fprint(&sb, "\n\t}{\n") - - for _, method := range methods { - varn := varName(method.Name) - fmt.Fprintf(&sb, "\t\t%s: %s,\n", method.Name, varn) - } - - fmt.Fprint(&sb, "\n\t}\n\n") - fmt.Fprint(&sb, "\treturn json.Marshal(&s)\n}") - - pkgImports := append(methods.Imports(), "encoding/json") - - if pkgPath != "" { - // Exclude self - for i, pkgImp := range pkgImports { - if pkgImp == pkgPath { - pkgImports = slices.Delete(pkgImports, i, i+1) - } - } - } - - return sb.String(), pkgImports -} - -func collectMethodsRecursive(pkg string, f []*ast.Field) []string { - var methodNames []string - for _, m := range f { - if m.Names != nil { - methodNames = append(methodNames, m.Names[0].Name) - continue - } - - if ident, ok := m.Type.(*ast.Ident); ok && ident.Obj != nil { - switch tt := ident.Obj.Decl.(*ast.TypeSpec).Type.(type) { - case *ast.InterfaceType: - // Embedded interface - methodNames = append( - methodNames, - collectMethodsRecursive( - pkg, - tt.Methods.List)...) - } - } else { - // Embedded, but in a different file/package. Return the - // package.Name and deal with that later. - name := packageName(m.Type) - if !strings.Contains(name, ".") { - // Assume current package - name = pkg + "." + name - } - methodNames = append(methodNames, name) - } - } - - return methodNames -} - -func firstToLower(name string) string { - return strings.ToLower(name[:1]) + name[1:] -} - -func firstToUpper(name string) string { - return strings.ToUpper(name[:1]) + name[1:] -} - -func packageName(e ast.Expr) string { - switch tp := e.(type) { - case *ast.Ident: - return tp.Name - case *ast.SelectorExpr: - return fmt.Sprintf("%s.%s", packageName(tp.X), packageName(tp.Sel)) - } - return "" -} - -func receiverShort(receiver string) string { - return strings.ToLower(trimAsterisk(receiver))[:1] -} - -func trimAsterisk(name string) string { - return strings.TrimPrefix(name, "*") -} - -func typeName(name, pkg string) string { - return strings.TrimPrefix(name, pkg+".") -} - -func uniqueNonEmptyStrings(s []string) []string { - var unique []string - set := map[string]any{} - for _, val := range s { - if val == "" { - continue - } - if _, ok := set[val]; !ok { - unique = append(unique, val) - set[val] = val - } - } - return unique -} - -func varName(name string) string { - name = firstToLower(name) - - // Adjust some reserved keywords, see https://golang.org/ref/spec#Keywords - switch name { - case "type": - name = "typ" - case "package": - name = "pkg" - // Not reserved, but syntax highlighters has it as a keyword. - case "len": - name = "length" - } - - return name -} diff --git a/codegen/methods2_test.go b/codegen/methods2_test.go deleted file mode 100644 index bd36b5e80..000000000 --- a/codegen/methods2_test.go +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2019 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 codegen - -type IEmbed interface { - MethodEmbed3(s string) string - MethodEmbed1() string - MethodEmbed2() -} diff --git a/codegen/methods_test.go b/codegen/methods_test.go deleted file mode 100644 index 0aff43d0e..000000000 --- a/codegen/methods_test.go +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2019 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 codegen - -import ( - "fmt" - "net" - "os" - "reflect" - "testing" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/common/herrors" -) - -func TestMethods(t *testing.T) { - var ( - zeroIE = reflect.TypeOf((*IEmbed)(nil)).Elem() - zeroIEOnly = reflect.TypeOf((*IEOnly)(nil)).Elem() - zeroI = reflect.TypeOf((*I)(nil)).Elem() - ) - - dir, _ := os.Getwd() - insp := NewInspector(dir) - - t.Run("MethodsFromTypes", func(t *testing.T) { - c := qt.New(t) - - methods := insp.MethodsFromTypes([]reflect.Type{zeroI}, nil) - - methodsStr := fmt.Sprint(methods) - - c.Assert(methodsStr, qt.Contains, "Method1(arg0 herrors.ErrorContext)") - c.Assert(methodsStr, qt.Contains, "Method7() interface {}") - c.Assert(methodsStr, qt.Contains, "Method0() string\n Method4() string") - c.Assert(methodsStr, qt.Contains, "MethodEmbed3(arg0 string) string\n MethodEmbed1() string") - - c.Assert(methods.Imports(), qt.Contains, "github.com/gohugoio/hugo/common/herrors") - }) - - t.Run("EmbedOnly", func(t *testing.T) { - c := qt.New(t) - - methods := insp.MethodsFromTypes([]reflect.Type{zeroIEOnly}, nil) - - methodsStr := fmt.Sprint(methods) - - c.Assert(methodsStr, qt.Contains, "MethodEmbed3(arg0 string) string") - }) - - t.Run("ToMarshalJSON", func(t *testing.T) { - c := qt.New(t) - - m, pkg := insp.MethodsFromTypes( - []reflect.Type{zeroI}, - []reflect.Type{zeroIE}).ToMarshalJSON("*page", "page") - - c.Assert(m, qt.Contains, "method6 := p.Method6()") - c.Assert(m, qt.Contains, "Method0: method0,") - c.Assert(m, qt.Contains, "return json.Marshal(&s)") - - c.Assert(pkg, qt.Contains, "github.com/gohugoio/hugo/common/herrors") - c.Assert(pkg, qt.Contains, "encoding/json") - - fmt.Println(pkg) - }) -} - -type I interface { - IEmbed - Method0() string - Method4() string - Method1(myerr herrors.ErrorContext) - Method3(myint int, mystring string) - Method5() (string, error) - Method6() *net.IP - Method7() any - Method8() herrors.ErrorContext - method2() - method9() os.FileInfo -} - -type IEOnly interface { - IEmbed -} diff --git a/commands/benchmark.go b/commands/benchmark.go new file mode 100644 index 000000000..51f2be876 --- /dev/null +++ b/commands/benchmark.go @@ -0,0 +1,112 @@ +// Copyright 2015 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 ( + "os" + "runtime" + "runtime/pprof" + "time" + + "github.com/spf13/cobra" + jww "github.com/spf13/jwalterweatherman" +) + +var ( + benchmarkTimes int + cpuProfileFile string + memProfileFile string +) + +var benchmarkCmd = &cobra.Command{ + Use: "benchmark", + Short: "Benchmark Hugo by building a site a number of times.", + Long: `Hugo can build a site many times over and analyze the running process +creating a benchmark.`, +} + +func init() { + initHugoBuilderFlags(benchmarkCmd) + initBenchmarkBuildingFlags(benchmarkCmd) + + benchmarkCmd.Flags().StringVar(&cpuProfileFile, "cpuprofile", "", "path/filename for the CPU profile file") + benchmarkCmd.Flags().StringVar(&memProfileFile, "memprofile", "", "path/filename for the memory profile file") + benchmarkCmd.Flags().IntVarP(&benchmarkTimes, "count", "n", 13, "number of times to build the site") + + benchmarkCmd.RunE = benchmark +} + +func benchmark(cmd *cobra.Command, args []string) error { + cfg, err := InitializeConfig(benchmarkCmd) + if err != nil { + return err + } + + c, err := newCommandeer(cfg) + if err != nil { + return err + } + + var memProf *os.File + if memProfileFile != "" { + memProf, err = os.Create(memProfileFile) + if err != nil { + return err + } + } + + var cpuProf *os.File + if cpuProfileFile != "" { + cpuProf, err = os.Create(cpuProfileFile) + if err != nil { + return err + } + } + + var memStats runtime.MemStats + runtime.ReadMemStats(&memStats) + memAllocated := memStats.TotalAlloc + mallocs := memStats.Mallocs + if cpuProf != nil { + pprof.StartCPUProfile(cpuProf) + } + + t := time.Now() + for i := 0; i < benchmarkTimes; i++ { + if err = c.resetAndBuildSites(false); err != nil { + return err + } + } + totalTime := time.Since(t) + + if memProf != nil { + pprof.WriteHeapProfile(memProf) + memProf.Close() + } + if cpuProf != nil { + pprof.StopCPUProfile() + cpuProf.Close() + } + + runtime.ReadMemStats(&memStats) + totalMemAllocated := memStats.TotalAlloc - memAllocated + totalMallocs := memStats.Mallocs - mallocs + + jww.FEEDBACK.Println() + jww.FEEDBACK.Printf("Average time per operation: %vms\n", int(1000*totalTime.Seconds()/float64(benchmarkTimes))) + jww.FEEDBACK.Printf("Average memory allocated per operation: %vkB\n", totalMemAllocated/uint64(benchmarkTimes)/1024) + jww.FEEDBACK.Printf("Average allocations per operation: %v\n", totalMallocs/uint64(benchmarkTimes)) + + return nil +} diff --git a/commands/check.go b/commands/check.go new file mode 100644 index 000000000..e5dbc1ffa --- /dev/null +++ b/commands/check.go @@ -0,0 +1,23 @@ +// Copyright 2015 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/spf13/cobra" +) + +var checkCmd = &cobra.Command{ + Use: "check", + Short: "Contains some verification checks", +} diff --git a/commands/commandeer.go b/commands/commandeer.go index bf9655637..7de185d2f 100644 --- a/commands/commandeer.go +++ b/commands/commandeer.go @@ -1,4 +1,4 @@ -// Copyright 2024 The Hugo Authors. All rights reserved. +// Copyright 2017 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,666 +14,49 @@ package commands import ( - "context" - "errors" - "fmt" - "io" - "log" - "os" - "os/signal" - "path/filepath" - "runtime" - "strings" - "sync" - "sync/atomic" - "syscall" - "time" - - "go.uber.org/automaxprocs/maxprocs" - - "github.com/bep/clocks" - "github.com/bep/lazycache" - "github.com/bep/logg" - "github.com/bep/overlayfs" - "github.com/bep/simplecobra" - - "github.com/gohugoio/hugo/common/hstrings" - "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") +type commandeer struct { + *deps.DepsCfg + pathSpec *helpers.PathSpec + configured bool +} -// Execute executes a command. -func Execute(args []string) error { - // Default GOMAXPROCS to be CPU limit aware, still respecting GOMAXPROCS env. - maxprocs.Set() - x, err := newExec() +func (c *commandeer) Set(key string, value interface{}) { + if c.configured { + panic("commandeer cannot be changed") + } + c.Cfg.Set(key, value) +} + +// PathSpec lazily creates a new PathSpec, as all the paths must +// be configured before it is created. +func (c *commandeer) PathSpec() *helpers.PathSpec { + c.configured = true + return c.pathSpec +} + +func (c *commandeer) initFs(fs *hugofs.Fs) error { + c.DepsCfg.Fs = fs + ps, err := helpers.NewPathSpec(fs, c.Cfg) if err != nil { return err } - 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() - fmt.Println() - return nil - } - if simplecobra.IsCommandError(err) { - // Print the help, but also return the error to fail the command. - cd.CobraCommand.Help() - fmt.Println() - } - } - return err -} - -type commonConfig struct { - mu *sync.Mutex - configs *allconfig.Configs - cfg config.Provider - fs *hugofs.Fs -} - -type configKey struct { - counter int32 - ignoreModulesDoesNotExists bool -} - -// This is the root command. -type rootCommand struct { - Printf func(format string, v ...any) - Println func(a ...any) - StdOut io.Writer - StdErr io.Writer - - logger loggers.Logger - - // The main cache busting key for the caches below. - configVersionID atomic.Int32 - - // Some, but not all commands need access to these. - // Some needs more than one, so keep them in a small cache. - 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 - - // Flags - source string - buildWatch bool - environment string - - // Common build flags. - baseURL string - gc bool - poll string - forceSyncStatic bool - - // Profile flags (for debugging of performance problems) - cpuprofile string - memprofile string - mutexprofile string - traceprofile string - printm bool - - logLevel string - - quiet bool - devMode bool // Hidden flag. - - renderToMemory bool - - cfgFile string - cfgDir 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 - }) - } + c.pathSpec = ps return nil } -func (r *rootCommand) Build(cd *simplecobra.Commandeer, bcfg hugolib.BuildCfg, cfg config.Provider) (*hugolib.HugoSites, error) { - h, err := r.Hugo(cfg) +func newCommandeer(cfg *deps.DepsCfg) (*commandeer, error) { + l := cfg.Language + if l == nil { + l = helpers.NewDefaultLanguage(cfg.Cfg) + } + ps, err := helpers.NewPathSpec(cfg.Fs, l) if err != nil { return nil, err } - if err := h.Build(bcfg); err != nil { - return nil, err - } - - return h, nil -} - -func (r *rootCommand) Commands() []simplecobra.Commander { - return r.commands -} - -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, - IgnoreModuleDoesNotExist: key.ignoreModulesDoesNotExists, - }, - ) - if err != nil { - return nil, err - } - - if !configs.Base.C.Clock.IsZero() { - // TODO(bep) find a better place for this. - htime.Clock = clocks.Start(configs.Base.C.Clock) - } - - return &commonConfig{ - mu: oldConf.mu, - configs: configs, - cfg: oldConf.cfg, - fs: fs, - }, nil - }) - - return cc, err -} - -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 configKey) (*commonConfig, error) { - var dir string - if r.source != "" { - dir, _ = filepath.Abs(r.source) - } else { - dir, _ = os.Getwd() - } - - if cfg == nil { - cfg = config.New() - } - - if !cfg.IsSet("workingDir") { - cfg.Set("workingDir", dir) - } else { - if err := os.MkdirAll(cfg.GetString("workingDir"), 0o777); err != nil { - return nil, fmt.Errorf("failed to create workingDir: %w", err) - } - } - - // 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, - IgnoreModuleDoesNotExist: key.ignoreModulesDoesNotExists, - }, - ) - if err != nil { - return nil, err - } - - base := configs.Base - - cfg.Set("publishDir", base.PublishDir) - cfg.Set("publishDirStatic", base.PublishDir) - cfg.Set("publishDirDynamic", base.PublishDir) - - renderStaticToDisk := cfg.GetBool("renderStaticToDisk") - - sourceFs := hugofs.Os - var destinationFs afero.Fs - if cfg.GetBool("renderToMemory") { - destinationFs = afero.NewMemMapFs() - if renderStaticToDisk { - // Hybrid, render dynamic content to Root. - cfg.Set("publishDirDynamic", "/") - } else { - // Rendering to memoryFS, publish to Root regardless of publishDir. - cfg.Set("publishDirDynamic", "/") - cfg.Set("publishDirStatic", "/") - } - } else { - destinationFs = hugofs.Os - } - - 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 := hugofs.NewBasePathFs(afero.NewOsFs(), absPublishDirStatic) - - // Serve from both the static and dynamic fs, - // the first will take priority. - // THis is a read-only filesystem, - // we do all the writes to - // fs.Destination and fs.DestinationStatic. - fs.PublishDirServer = overlayfs.New( - overlayfs.Options{ - Fss: []afero.Fs{ - dynamicFs, - staticFs, - }, - }, - ) - fs.PublishDirStatic = staticFs - - } - - if !base.C.Clock.IsZero() { - // TODO(bep) find a better place for this. - htime.Clock = clocks.Start(configs.Base.C.Clock) - } - - if base.PrintPathWarnings { - // Note that we only care about the "dynamic creates" here, - // so skip the static fs. - fs.PublishDir = hugofs.NewCreateCountingFs(fs.PublishDir) - } - - commonConfig := &commonConfig{ - mu: &sync.Mutex{}, - configs: configs, - cfg: cfg, - fs: fs, - } - - return commonConfig, nil - }) - - return cc, err -} - -func (r *rootCommand) HugFromConfig(conf *commonConfig) (*hugolib.HugoSites, error) { - 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) { - 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 := 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 { - b := newHugoBuilder(r, nil) - - if !r.buildWatch { - defer b.postBuild("Total", time.Now()) - } - - if err := b.loadConfig(cd, false); err != nil { - return err - } - - err := func() error { - if r.buildWatch { - defer r.timeTrack(time.Now(), "Built") - } - err := b.build() - if err != nil { - return err - } - return nil - }() - if err != nil { - return err - } - - if !r.buildWatch { - // Done. - return nil - } - - watchDirs, err := b.getDirList() - if err != nil { - return err - } - - watchGroups := helpers.ExtractAndGroupRootPaths(watchDirs) - - for _, group := range watchGroups { - r.Printf("Watching for changes in %s\n", group) - } - watcher, err := b.newWatcher(r.poll, watchDirs...) - if err != nil { - return err - } - - defer watcher.Close() - - r.Println("Press Ctrl+C to stop") - - sigs := make(chan os.Signal, 1) - signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) - - <-sigs - - return nil -} - -func (r *rootCommand) PreRun(cd, runner *simplecobra.Commandeer) error { - r.StdOut = os.Stdout - r.StdErr = os.Stderr - if r.quiet { - r.StdOut = io.Discard - r.StdErr = io.Discard - } - // Used by mkcert (server). - log.SetOutput(r.StdOut) - - r.Printf = func(format string, v ...any) { - if !r.quiet { - fmt.Fprintf(r.StdOut, format, v...) - } - } - r.Println = func(a ...any) { - if !r.quiet { - fmt.Fprintln(r.StdOut, a...) - } - } - _, running := runner.Command.(*serverCommand) - var err error - r.logger, err = r.createLogger(running) - if err != nil { - return err - } - // Set up the global logger early to allow info deprecations during config load. - loggers.SetGlobalLogger(r.logger) - - 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) { - level := logg.LevelWarn - - 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) - } - } - } - - optsLogger := loggers.Options{ - DistinctLevel: logg.LevelWarn, - Level: level, - StdOut: r.StdOut, - StdErr: r.StdErr, - StoreErrors: running, - } - - return loggers.New(optsLogger), nil -} - -func (r *rootCommand) resetLogs() { - r.logger.Reset() - loggers.Log().Reset() -} - -// IsTestRun reports whether the command is running as a test. -func (r *rootCommand) IsTestRun() bool { - return os.Getenv("HUGO_TESTRUN") != "" -} - -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 - 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.MarkFlagDirname("source") - cmd.PersistentFlags().StringP("destination", "d", "", "filesystem path to write files to") - _ = 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)") - - 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.PersistentFlags().MarkHidden("devMode") - - // Configure local flags - applyLocalFlagsBuild(cmd, r) - - 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") - _ = cmd.MarkFlagDirname("cacheDir") - cmd.Flags().StringP("contentDir", "c", "", "filesystem path to content directory") - 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) -func applyLocalFlagsBuild(cmd *cobra.Command, r *rootCommand) { - applyLocalFlagsBuildConfig(cmd, r) - cmd.Flags().Bool("cleanDestinationDir", false, "remove files from destination not found in static directories") - cmd.Flags().BoolP("buildDrafts", "D", false, "include content marked as draft") - cmd.Flags().BoolP("buildFuture", "F", false, "include content with publishdate in the future") - cmd.Flags().BoolP("buildExpired", "E", false, "include expired content") - 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.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("printI18nWarnings", "", false, "print missing translations") - 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") - cmd.Flags().StringVarP(&r.mutexprofile, "profile-mutex", "", "", "write Mutex profile to `file`") - cmd.Flags().StringVarP(&r.traceprofile, "trace", "", "", "write trace to `file` (not useful in general)") - - // Hide these for now. - cmd.Flags().MarkHidden("profile-cpu") - cmd.Flags().MarkHidden("profile-mem") - 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.)") -} - -func (r *rootCommand) timeTrack(start time.Time, name string) { - elapsed := time.Since(start) - r.Printf("%s in %v ms\n", name, int(1000*elapsed.Seconds())) -} - -type simpleCommand struct { - use string - name string - short string - long string - run func(ctx context.Context, cd *simplecobra.Commandeer, rootCmd *rootCommand, args []string) error - withc func(cmd *cobra.Command, r *rootCommand) - initc func(cd *simplecobra.Commandeer) error - - commands []simplecobra.Commander - - rootCmd *rootCommand -} - -func (c *simpleCommand) Commands() []simplecobra.Commander { - return c.commands -} - -func (c *simpleCommand) Name() string { - return c.name -} - -func (c *simpleCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error { - if c.run == nil { - return nil - } - return c.run(ctx, cd, c.rootCmd, args) -} - -func (c *simpleCommand) Init(cd *simplecobra.Commandeer) error { - c.rootCmd = cd.Root.Command.(*rootCommand) - cmd := cd.CobraCommand - cmd.Short = c.short - cmd.Long = c.long - if c.use != "" { - cmd.Use = c.use - } - if c.withc != nil { - c.withc(cmd, c.rootCmd) - } - return nil -} - -func (c *simpleCommand) PreRun(cd, runner *simplecobra.Commandeer) error { - if c.initc != nil { - return c.initc(cd) - } - return nil -} - -func mapLegacyArgs(args []string) []string { - if len(args) > 1 && args[0] == "new" && !hstrings.EqualAny(args[1], "site", "theme", "content") { - // Insert "content" as the second argument - args = append(args[:1], append([]string{"content"}, args[1:]...)...) - } - return args + return &commandeer{DepsCfg: cfg, pathSpec: ps}, nil } diff --git a/commands/commands.go b/commands/commands.go deleted file mode 100644 index 10ab106e2..000000000 --- a/commands/commands.go +++ /dev/null @@ -1,73 +0,0 @@ -// 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 ( - "context" - - "github.com/bep/simplecobra" -) - -// newExec wires up all of Hugo's CLI. -func newExec() (*simplecobra.Exec, error) { - rootCmd := &rootCommand{ - commands: []simplecobra.Commander{ - newHugoBuildCmd(), - newVersionCmd(), - newEnvCommand(), - newServerCommand(), - newDeployCommand(), - newConfigCommand(), - newNewCommand(), - newConvertCommand(), - newImportCommand(), - newListCommand(), - newModCommands(), - newGenCommand(), - newReleaseCommand(), - }, - } - - 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 deleted file mode 100644 index 7d166b9b8..000000000 --- a/commands/config.go +++ /dev/null @@ -1,239 +0,0 @@ -// 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 ( - "bytes" - "context" - "encoding/json" - "fmt" - "os" - "strings" - "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. -func newConfigCommand() *configCommand { - return &configCommand{ - commands: []simplecobra.Commander{ - &configMountsCommand{}, - }, - } -} - -type configCommand struct { - r *rootCommand - - format string - lang string - printZero bool - - commands []simplecobra.Commander -} - -func (c *configCommand) Commands() []simplecobra.Commander { - return c.commands -} - -func (c *configCommand) Name() string { - return "config" -} - -func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error { - conf, err := c.r.ConfigFromProvider(configKey{counter: c.r.configVersionID.Load()}, flagsToCfg(cd, nil)) - if err != nil { - return err - } - 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: !c.printZero}); err != nil { - return err - } - - format := strings.ToLower(c.format) - - switch format { - case "json": - 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]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) - case "toml": - return parser.InterfaceToConfig(m, metadecoders.TOML, os.Stdout) - default: - return fmt.Errorf("unsupported format: %q", format) - } - } - - return nil -} - -func (c *configCommand) Init(cd *simplecobra.Commandeer) error { - c.r = cd.Root.Command.(*rootCommand) - cmd := cd.CobraCommand - 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 -} - -func (c *configCommand) PreRun(cd, runner *simplecobra.Commandeer) error { - return nil -} - -type configModMount struct { - Source string `json:"source"` - Target string `json:"target"` - Lang string `json:"lang,omitempty"` -} - -type configModMounts struct { - verbose bool - m modules.Module -} - -// MarshalJSON is for internal use only. -func (m *configModMounts) MarshalJSON() ([]byte, error) { - var mounts []configModMount - - for _, mount := range m.m.Mounts() { - mounts = append(mounts, configModMount{ - Source: mount.Source, - Target: mount.Target, - Lang: mount.Lang, - }) - } - - var ownerPath string - if m.m.Owner() != nil { - ownerPath = m.m.Owner().Path() - } - - if m.verbose { - config := m.m.Config() - return json.Marshal(&struct { - Path string `json:"path"` - Version string `json:"version"` - Time time.Time `json:"time"` - Owner string `json:"owner"` - Dir string `json:"dir"` - Meta map[string]any `json:"meta"` - HugoVersion modules.HugoVersion `json:"hugoVersion"` - - Mounts []configModMount `json:"mounts"` - }{ - Path: m.m.Path(), - Version: m.m.Version(), - Time: m.m.Time(), - Owner: ownerPath, - Dir: m.m.Dir(), - Meta: config.Params, - HugoVersion: config.HugoVersion, - Mounts: mounts, - }) - } - - return json.Marshal(&struct { - Path string `json:"path"` - Version string `json:"version"` - Time time.Time `json:"time"` - Owner string `json:"owner"` - Dir string `json:"dir"` - Mounts []configModMount `json:"mounts"` - }{ - Path: m.m.Path(), - Version: m.m.Version(), - Time: m.m.Time(), - Owner: ownerPath, - Dir: m.m.Dir(), - Mounts: mounts, - }) -} - -type configMountsCommand struct { - r *rootCommand - configCmd *configCommand -} - -func (c *configMountsCommand) Commands() []simplecobra.Commander { - return nil -} - -func (c *configMountsCommand) Name() string { - return "mounts" -} - -func (c *configMountsCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error { - r := c.configCmd.r - 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.isVerbose()}, metadecoders.JSON, os.Stdout); err != nil { - return err - } - } - return nil -} - -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 -} - -func (c *configMountsCommand) PreRun(cd, runner *simplecobra.Commandeer) error { - c.configCmd = cd.Parent.Command.(*configCommand) - return nil -} diff --git a/commands/convert.go b/commands/convert.go index ebf81cfb3..298ff6019 100644 --- a/commands/convert.go +++ b/commands/convert.go @@ -1,4 +1,4 @@ -// Copyright 2024 The Hugo Authors. All rights reserved. +// Copyright 2015 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,216 +14,145 @@ package commands import ( - "bytes" - "context" + "errors" "fmt" "path/filepath" - "strings" "time" - "github.com/bep/simplecobra" - "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/hugolib" "github.com/gohugoio/hugo/parser" - "github.com/gohugoio/hugo/parser/metadecoders" - "github.com/gohugoio/hugo/parser/pageparser" - "github.com/gohugoio/hugo/resources/page" + "github.com/spf13/cast" "github.com/spf13/cobra" ) -func newConvertCommand() *convertCommand { - var c *convertCommand - c = &convertCommand{ - commands: []simplecobra.Commander{ - &simpleCommand{ - name: "toJSON", - short: "Convert front matter to JSON", - long: `toJSON converts all front matter in the content directory +var outputDir string +var unsafe bool + +var convertCmd = &cobra.Command{ + Use: "convert", + Short: "Convert your content to different formats", + Long: `Convert your content (e.g. front matter) to different formats. + +See convert's subcommands toJSON, toTOML and toYAML for more information.`, + RunE: nil, +} + +var toJSONCmd = &cobra.Command{ + Use: "toJSON", + Short: "Convert front matter to JSON", + Long: `toJSON converts all front matter in the content directory to use JSON for the front matter.`, - run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { - return c.convertContents(metadecoders.JSON) - }, - withc: func(cmd *cobra.Command, r *rootCommand) { - cmd.ValidArgsFunction = cobra.NoFileCompletions - }, - }, - &simpleCommand{ - name: "toTOML", - short: "Convert front matter to TOML", - long: `toTOML converts all front matter in the content directory + RunE: func(cmd *cobra.Command, args []string) error { + return convertContents(rune([]byte(parser.JSONLead)[0])) + }, +} + +var toTOMLCmd = &cobra.Command{ + Use: "toTOML", + Short: "Convert front matter to TOML", + Long: `toTOML converts all front matter in the content directory to use TOML for the front matter.`, - run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { - return c.convertContents(metadecoders.TOML) - }, - withc: func(cmd *cobra.Command, r *rootCommand) { - cmd.ValidArgsFunction = cobra.NoFileCompletions - }, - }, - &simpleCommand{ - name: "toYAML", - short: "Convert front matter to YAML", - long: `toYAML converts all front matter in the content directory + RunE: func(cmd *cobra.Command, args []string) error { + return convertContents(rune([]byte(parser.TOMLLead)[0])) + }, +} + +var toYAMLCmd = &cobra.Command{ + Use: "toYAML", + Short: "Convert front matter to YAML", + Long: `toYAML converts all front matter in the content directory to use YAML for the front matter.`, - run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { - return c.convertContents(metadecoders.YAML) - }, - withc: func(cmd *cobra.Command, r *rootCommand) { - cmd.ValidArgsFunction = cobra.NoFileCompletions - }, - }, - }, - } - return c + RunE: func(cmd *cobra.Command, args []string) error { + return convertContents(rune([]byte(parser.YAMLLead)[0])) + }, } -type convertCommand struct { - // Flags. - outputDir string - unsafe bool - - // Deps. - r *rootCommand - h *hugolib.HugoSites - - // Commands. - commands []simplecobra.Commander +func init() { + convertCmd.AddCommand(toJSONCmd) + convertCmd.AddCommand(toTOMLCmd) + convertCmd.AddCommand(toYAMLCmd) + convertCmd.PersistentFlags().StringVarP(&outputDir, "output", "o", "", "filesystem path to write files to") + convertCmd.PersistentFlags().StringVarP(&source, "source", "s", "", "filesystem path to read files relative from") + convertCmd.PersistentFlags().BoolVar(&unsafe, "unsafe", false, "enable less safe operations, please backup first") + convertCmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{}) } -func (c *convertCommand) Commands() []simplecobra.Commander { - return c.commands -} - -func (c *convertCommand) Name() string { - return "convert" -} - -func (c *convertCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error { - return nil -} - -func (c *convertCommand) Init(cd *simplecobra.Commandeer) error { - cmd := cd.CobraCommand - 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 -} - -func (c *convertCommand) PreRun(cd, runner *simplecobra.Commandeer) error { - c.r = cd.Root.Command.(*rootCommand) - cfg := config.New() - cfg.Set("buildDrafts", true) - h, err := c.r.Hugo(flagsToCfg(cd, cfg)) +func convertContents(mark rune) error { + cfg, err := InitializeConfig() if err != nil { return err } - c.h = h - return nil -} -func (c *convertCommand) convertAndSavePage(p page.Page, site *hugolib.Site, targetFormat metadecoders.Format) error { - // The resources are not in .Site.AllPages. - for _, r := range p.Resources().ByType("page") { - if err := c.convertAndSavePage(r.(page.Page), site, targetFormat); err != nil { + h, err := hugolib.NewHugoSites(*cfg) + if err != nil { + return err + } + + site := h.Sites[0] + + if err = site.Initialise(); err != nil { + return err + } + + if site.Source == nil { + panic("site.Source not set") + } + if len(site.Source.Files()) < 1 { + return errors.New("No source files found") + } + + contentDir := site.PathSpec.AbsPathify(site.Cfg.GetString("contentDir")) + site.Log.FEEDBACK.Println("processing", len(site.Source.Files()), "content files") + for _, file := range site.Source.Files() { + site.Log.INFO.Println("Attempting to convert", file.LogicalName()) + page, err := site.NewPage(file.LogicalName()) + if err != nil { return err } - } - if p.File() == nil { - // No content file. - return nil - } + psr, err := parser.ReadFrom(file.Contents) + if err != nil { + site.Log.ERROR.Println("Error processing file:", file.Path()) + return err + } + metadata, err := psr.Metadata() + if err != nil { + site.Log.ERROR.Println("Error processing file:", file.Path()) + return err + } - errMsg := fmt.Errorf("error processing file %q", p.File().Path()) + // better handling of dates in formats that don't have support for them + if mark == parser.FormatToLeadRune("json") || mark == parser.FormatToLeadRune("yaml") || mark == parser.FormatToLeadRune("toml") { + newMetadata := cast.ToStringMap(metadata) + for k, v := range newMetadata { + switch vv := v.(type) { + case time.Time: + newMetadata[k] = vv.Format(time.RFC3339) + } + } + metadata = newMetadata + } - site.Log.Infoln("attempting to convert", p.File().Filename()) + page.SetDir(filepath.Join(contentDir, file.Dir())) + page.SetSourceContent(psr.Content()) + if err = page.SetSourceMetaData(metadata, mark); err != nil { + site.Log.ERROR.Printf("Failed to set source metadata for file %q: %s. For more info see For more info see https://github.com/gohugoio/hugo/issues/2458", page.FullFilePath(), err) + continue + } - f := p.File() - file, err := f.FileInfo().Meta().Open() - if err != nil { - site.Log.Errorln(errMsg) - file.Close() - return nil - } - - pf, err := pageparser.ParseFrontMatterAndContent(file) - if err != nil { - site.Log.Errorln(errMsg) - file.Close() - return err - } - - file.Close() - - // better handling of dates in formats that don't have support for them - if pf.FrontMatterFormat == metadecoders.JSON || pf.FrontMatterFormat == metadecoders.YAML || pf.FrontMatterFormat == metadecoders.TOML { - for k, v := range pf.FrontMatter { - switch vv := v.(type) { - case time.Time: - pf.FrontMatter[k] = vv.Format(time.RFC3339) + if outputDir != "" { + if err = page.SaveSourceAs(filepath.Join(outputDir, page.FullFilePath())); err != nil { + return fmt.Errorf("Failed to save file %q: %s", page.FullFilePath(), err) + } + } else { + if unsafe { + if err = page.SaveSource(); err != nil { + return fmt.Errorf("Failed to save file %q: %s", page.FullFilePath(), err) + } + } else { + site.Log.FEEDBACK.Println("Unsafe operation not allowed, use --unsafe or set a different output path") } } } - - var newContent bytes.Buffer - err = parser.InterfaceToFrontMatter(pf.FrontMatter, targetFormat, &newContent) - if err != nil { - site.Log.Errorln(errMsg) - return err - } - - newContent.Write(pf.Content) - - newFilename := p.File().Filename() - - if c.outputDir != "" { - contentDir := strings.TrimSuffix(newFilename, p.File().Path()) - contentDir = filepath.Base(contentDir) - - newFilename = filepath.Join(c.outputDir, contentDir, p.File().Path()) - } - - fs := hugofs.Os - if err := helpers.WriteToDisk(newFilename, &newContent, fs); err != nil { - return fmt.Errorf("failed to save file %q:: %w", newFilename, err) - } - - return nil -} - -func (c *convertCommand) convertContents(format metadecoders.Format) error { - if c.outputDir == "" && !c.unsafe { - return newUserError("Unsafe operation not allowed, use --unsafe or set a different output path") - } - - if err := c.h.Build(hugolib.BuildCfg{SkipRender: true}); err != nil { - return err - } - - site := c.h.Sites[0] - - var pagesBackedByFile page.Pages - for _, p := range site.AllPages() { - if p.File() == nil { - continue - } - pagesBackedByFile = append(pagesBackedByFile, p) - } - - site.Log.Println("processing", len(pagesBackedByFile), "content files") - for _, p := range site.AllPages() { - if err := c.convertAndSavePage(p, site, format); err != nil { - return err - } - } return nil } diff --git a/commands/deploy.go b/commands/deploy.go deleted file mode 100644 index 3e9d3df20..000000000 --- a/commands/deploy.go +++ /dev/null @@ -1,51 +0,0 @@ -// 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 commands - -import ( - "context" - - "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 - -See https://gohugo.io/hosting-and-deployment/hugo-deploy/ for detailed -documentation. -`, - run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { - h, err := r.Hugo(flagsToCfgWithAdditionalConfigBase(cd, nil, "deployment")) - if err != nil { - return err - } - 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) { - applyDeployFlags(cmd, r) - }, - } -} diff --git a/commands/deploy_flags.go b/commands/deploy_flags.go deleted file mode 100644 index d4326547a..000000000 --- a/commands/deploy_flags.go +++ /dev/null @@ -1,33 +0,0 @@ -// 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 deleted file mode 100644 index 8f5eaa2de..000000000 --- a/commands/deploy_off.go +++ /dev/null @@ -1,50 +0,0 @@ -// 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 - -// 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 ( - "context" - "errors" - - "github.com/bep/simplecobra" - "github.com/spf13/cobra" -) - -func newDeployCommand() simplecobra.Commander { - return &simpleCommand{ - name: "deploy", - run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { - 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 753522560..54c98d527 100644 --- a/commands/env.go +++ b/commands/env.go @@ -1,4 +1,4 @@ -// Copyright 2024 The Hugo Authors. All rights reserved. +// Copyright 2016 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,57 +14,22 @@ package commands import ( - "context" "runtime" - "github.com/bep/simplecobra" - "github.com/gohugoio/hugo/common/hugo" "github.com/spf13/cobra" + jww "github.com/spf13/jwalterweatherman" ) -func newEnvCommand() simplecobra.Commander { - return &simpleCommand{ - name: "env", - 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()) +var envCmd = &cobra.Command{ + Use: "env", + Short: "Print Hugo version and environment info", + Long: `Print Hugo version and environment info. This is useful in Hugo bug reports.`, + RunE: func(cmd *cobra.Command, args []string) error { + printHugoVersion() + jww.FEEDBACK.Printf("GOOS=%q\n", runtime.GOOS) + jww.FEEDBACK.Printf("GOARCH=%q\n", runtime.GOARCH) + jww.FEEDBACK.Printf("GOVERSION=%q\n", runtime.Version()) - if r.isVerbose() { - deps := hugo.GetDependencyList() - for _, dep := range deps { - r.Printf("%s\n", dep) - } - } else { - // These are also included in the GetDependencyList above; - // always print these as these are most likely the most useful to know about. - deps := hugo.GetDependencyListNonGo() - for _, dep := range deps { - r.Printf("%s\n", dep) - } - } - return nil - }, - withc: func(cmd *cobra.Command, r *rootCommand) { - cmd.ValidArgsFunction = cobra.NoFileCompletions - }, - } -} - -func newVersionCmd() simplecobra.Commander { - return &simpleCommand{ - name: "version", - run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { - r.Println(hugo.BuildVersionString()) - return nil - }, - 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 - }, - } + return nil + }, } diff --git a/commands/gen.go b/commands/gen.go index 1c5361840..62a84b0d0 100644 --- a/commands/gen.go +++ b/commands/gen.go @@ -1,4 +1,4 @@ -// Copyright 2024 The Hugo Authors. All rights reserved. +// Copyright 2015 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,290 +14,10 @@ package commands import ( - "bytes" - "context" - "encoding/json" - "fmt" - "os" - "path" - "path/filepath" - "slices" - "strings" - - "github.com/alecthomas/chroma/v2" - "github.com/alecthomas/chroma/v2/formatters/html" - "github.com/alecthomas/chroma/v2/styles" - "github.com/bep/simplecobra" - "github.com/gohugoio/hugo/common/hugo" - "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 { - var ( - // Flags. - gendocdir string - genmandir string - - // Chroma flags. - style string - highlightStyle string - lineNumbersInlineStyle string - lineNumbersTableStyle string - omitEmpty bool - ) - - newChromaStyles := func() simplecobra.Commander { - return &simpleCommand{ - name: "chromastyles", - short: "Generate CSS stylesheet for the Chroma code highlighter", - long: `Generate CSS stylesheet for the Chroma code highlighter for a given style. This stylesheet is needed if markup.highlight.noClasses is disabled in config. - -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 lineNumbersInlineStyle != "" { - builder.Add(chroma.LineNumbers, lineNumbersInlineStyle) - } - if lineNumbersTableStyle != "" { - builder.Add(chroma.LineNumbersTable, lineNumbersTableStyle) - } - style, err := builder.Build() - if err != nil { - return err - } - - 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.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) - }, - } - } - - newMan := func() simplecobra.Commander { - return &simpleCommand{ - name: "man", - short: "Generate man pages for the Hugo CLI", - long: `This command automatically generates up-to-date man pages of Hugo's - command-line interface. By default, it creates the man page files - in the "man" directory under the current directory.`, - - run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { - header := &doc.GenManHeader{ - Section: "1", - Manual: "Hugo Manual", - Source: fmt.Sprintf("Hugo %s", hugo.CurrentVersion), - } - if !strings.HasSuffix(genmandir, helpers.FilePathSeparator) { - genmandir += helpers.FilePathSeparator - } - if found, _ := helpers.Exists(genmandir, hugofs.Os); !found { - r.Println("Directory", genmandir, "does not exist, creating...") - if err := hugofs.Os.MkdirAll(genmandir, 0o777); err != nil { - return err - } - } - cd.CobraCommand.Root().DisableAutoGenTag = true - - r.Println("Generating Hugo man pages in", genmandir, "...") - doc.GenManTree(cd.CobraCommand.Root(), header, genmandir) - - r.Println("Done.") - - 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.") - _ = cmd.MarkFlagDirname("dir") - }, - } - } - - newGen := func() simplecobra.Commander { - const gendocFrontmatterTemplate = `--- -title: "%s" -slug: %s -url: %s ---- -` - - return &simpleCommand{ - name: "doc", - 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 { - cd.CobraCommand.VisitParents(func(c *cobra.Command) { - // Disable the "Auto generated by spf13/cobra on DATE" - // as it creates a lot of diffs. - c.DisableAutoGenTag = true - }) - if !strings.HasSuffix(gendocdir, helpers.FilePathSeparator) { - gendocdir += helpers.FilePathSeparator - } - if found, _ := helpers.Exists(gendocdir, hugofs.Os); !found { - r.Println("Directory", gendocdir, "does not exist, creating...") - 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 := "/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 "/docs/reference/commands/" + strings.ToLower(base) + "/" - } - r.Println("Generating Hugo command-line documentation in", gendocdir, "...") - doc.GenMarkdownTreeCustom(cd.CobraCommand.Root(), gendocdir, prepender, linkHandler) - r.Println("Done.") - - 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.") - _ = cmd.MarkFlagDirname("dir") - }, - } - } - - var docsHelperTarget string - - newDocsHelper := func() simplecobra.Commander { - return &simpleCommand{ - name: "docshelper", - 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) - - 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() - yamlEnc := yaml.NewEncoder(f) - if err := yamlEnc.Encode(m); err != nil { - return err - } - - r.Println("Done!") - return nil - }, - withc: func(cmd *cobra.Command, r *rootCommand) { - cmd.Hidden = true - cmd.ValidArgsFunction = cobra.NoFileCompletions - cmd.PersistentFlags().StringVarP(&docsHelperTarget, "dir", "", "docs/data", "data dir") - }, - } - } - - return &genCommand{ - commands: []simplecobra.Commander{ - newChromaStyles(), - newGen(), - newMan(), - newDocsHelper(), - }, - } -} - -type genCommand struct { - rootCmd *rootCommand - - commands []simplecobra.Commander -} - -func (c *genCommand) Commands() []simplecobra.Commander { - return c.commands -} - -func (c *genCommand) Name() string { - return "gen" -} - -func (c *genCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error { - return nil -} - -func (c *genCommand) Init(cd *simplecobra.Commandeer) error { - cmd := cd.CobraCommand - 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 -} - -func (c *genCommand) PreRun(cd, runner *simplecobra.Commandeer) error { - c.rootCmd = cd.Root.Command.(*rootCommand) - return nil +var genCmd = &cobra.Command{ + Use: "gen", + Short: "A collection of several useful generators.", } diff --git a/commands/genautocomplete.go b/commands/genautocomplete.go new file mode 100644 index 000000000..c2004ab22 --- /dev/null +++ b/commands/genautocomplete.go @@ -0,0 +1,70 @@ +// Copyright 2015 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/spf13/cobra" + jww "github.com/spf13/jwalterweatherman" +) + +var autocompleteTarget string + +// bash for now (zsh and others will come) +var autocompleteType string + +var genautocompleteCmd = &cobra.Command{ + Use: "autocomplete", + Short: "Generate shell autocompletion script for Hugo", + Long: `Generates a shell autocompletion script for Hugo. + +NOTE: The current version supports Bash only. + This should work for *nix systems with Bash installed. + +By default, the file is written directly to /etc/bash_completion.d +for convenience, and the command may need superuser rights, e.g.: + + $ sudo hugo gen autocomplete + +Add ` + "`--completionfile=/path/to/file`" + ` flag to set alternative +file-path and name. + +Logout and in again to reload the completion scripts, +or just source them in directly: + + $ . /etc/bash_completion`, + + RunE: func(cmd *cobra.Command, args []string) error { + if autocompleteType != "bash" { + return newUserError("Only Bash is supported for now") + } + + err := cmd.Root().GenBashCompletionFile(autocompleteTarget) + + if err != nil { + return err + } + + jww.FEEDBACK.Println("Bash completion file for Hugo saved to", autocompleteTarget) + + return nil + }, +} + +func init() { + genautocompleteCmd.PersistentFlags().StringVarP(&autocompleteTarget, "completionfile", "", "/etc/bash_completion.d/hugo.sh", "autocompletion file") + genautocompleteCmd.PersistentFlags().StringVarP(&autocompleteType, "type", "", "bash", "autocompletion type (currently only bash supported)") + + // For bash-completion + genautocompleteCmd.PersistentFlags().SetAnnotation("completionfile", cobra.BashCompFilenameExt, []string{}) +} diff --git a/commands/gendoc.go b/commands/gendoc.go new file mode 100644 index 000000000..c4840050b --- /dev/null +++ b/commands/gendoc.go @@ -0,0 +1,86 @@ +// Copyright 2016 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 ( + "fmt" + "path" + "path/filepath" + "strings" + "time" + + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugofs" + "github.com/spf13/cobra" + "github.com/spf13/cobra/doc" + jww "github.com/spf13/jwalterweatherman" +) + +const gendocFrontmatterTemplate = `--- +date: %s +title: "%s" +slug: %s +url: %s +--- +` + +var gendocdir string +var gendocCmd = &cobra.Command{ + Use: "doc", + 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 http://gohugo.io/. + +It creates one Markdown file per command with front matter suitable +for rendering in Hugo.`, + + RunE: func(cmd *cobra.Command, args []string) error { + if !strings.HasSuffix(gendocdir, helpers.FilePathSeparator) { + gendocdir += helpers.FilePathSeparator + } + if found, _ := helpers.Exists(gendocdir, hugofs.Os); !found { + jww.FEEDBACK.Println("Directory", gendocdir, "does not exist, creating...") + if err := hugofs.Os.MkdirAll(gendocdir, 0777); err != nil { + return err + } + } + now := time.Now().Format(time.RFC3339) + prepender := func(filename string) string { + name := filepath.Base(filename) + base := strings.TrimSuffix(name, path.Ext(name)) + url := "/commands/" + strings.ToLower(base) + "/" + return fmt.Sprintf(gendocFrontmatterTemplate, now, strings.Replace(base, "_", " ", -1), base, url) + } + + linkHandler := func(name string) string { + base := strings.TrimSuffix(name, path.Ext(name)) + return "/commands/" + strings.ToLower(base) + "/" + } + + jww.FEEDBACK.Println("Generating Hugo command-line documentation in", gendocdir, "...") + doc.GenMarkdownTreeCustom(cmd.Root(), gendocdir, prepender, linkHandler) + jww.FEEDBACK.Println("Done.") + + return nil + }, +} + +func init() { + gendocCmd.PersistentFlags().StringVar(&gendocdir, "dir", "/tmp/hugodoc/", "the directory to write the doc.") + + // For bash-completion + gendocCmd.PersistentFlags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{}) +} diff --git a/commands/gendocshelper.go b/commands/gendocshelper.go new file mode 100644 index 000000000..ca781242e --- /dev/null +++ b/commands/gendocshelper.go @@ -0,0 +1,70 @@ +// Copyright 2017-present 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 ( + "encoding/json" + "fmt" + "os" + "path/filepath" + + "github.com/gohugoio/hugo/docshelper" + "github.com/spf13/cobra" +) + +type genDocsHelper struct { + target string + cmd *cobra.Command +} + +func createGenDocsHelper() *genDocsHelper { + g := &genDocsHelper{ + cmd: &cobra.Command{ + Use: "docshelper", + Short: "Generate some data files for the Hugo docs.", + Hidden: true, + }, + } + + g.cmd.RunE = func(cmd *cobra.Command, args []string) error { + return g.generate() + } + + g.cmd.PersistentFlags().StringVarP(&g.target, "dir", "", "docs/data", "data dir") + + return g +} + +func (g *genDocsHelper) generate() error { + fmt.Println("Generate docs data to", g.target) + + targetFile := filepath.Join(g.target, "docs.json") + + f, err := os.Create(targetFile) + if err != nil { + return err + } + defer f.Close() + + enc := json.NewEncoder(f) + enc.SetIndent("", " ") + + if err := enc.Encode(docshelper.DocProviders); err != nil { + return err + } + + fmt.Println("Done!") + return nil + +} diff --git a/commands/genman.go b/commands/genman.go new file mode 100644 index 000000000..004e669e7 --- /dev/null +++ b/commands/genman.go @@ -0,0 +1,66 @@ +// Copyright 2016 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 ( + "fmt" + "strings" + + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugofs" + "github.com/spf13/cobra" + "github.com/spf13/cobra/doc" + jww "github.com/spf13/jwalterweatherman" +) + +var genmandir string +var genmanCmd = &cobra.Command{ + Use: "man", + Short: "Generate man pages for the Hugo CLI", + Long: `This command automatically generates up-to-date man pages of Hugo's +command-line interface. By default, it creates the man page files +in the "man" directory under the current directory.`, + + RunE: func(cmd *cobra.Command, args []string) error { + header := &doc.GenManHeader{ + Section: "1", + Manual: "Hugo Manual", + Source: fmt.Sprintf("Hugo %s", helpers.CurrentHugoVersion), + } + if !strings.HasSuffix(genmandir, helpers.FilePathSeparator) { + genmandir += helpers.FilePathSeparator + } + if found, _ := helpers.Exists(genmandir, hugofs.Os); !found { + jww.FEEDBACK.Println("Directory", genmandir, "does not exist, creating...") + if err := hugofs.Os.MkdirAll(genmandir, 0777); err != nil { + return err + } + } + cmd.Root().DisableAutoGenTag = true + + jww.FEEDBACK.Println("Generating Hugo man pages in", genmandir, "...") + doc.GenManTree(cmd.Root(), header, genmandir) + + jww.FEEDBACK.Println("Done.") + + return nil + }, +} + +func init() { + genmanCmd.PersistentFlags().StringVar(&genmandir, "dir", "man/", "the directory to write the man pages.") + + // For bash-completion + genmanCmd.PersistentFlags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{}) +} diff --git a/commands/helpers.go b/commands/helpers.go deleted file mode 100644 index a13bdebc2..000000000 --- a/commands/helpers.go +++ /dev/null @@ -1,121 +0,0 @@ -// 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 ( - "errors" - "fmt" - "log" - "os" - "path/filepath" - "strings" - - "github.com/bep/simplecobra" - "github.com/gohugoio/hugo/config" - "github.com/spf13/pflag" -) - -const ( - ansiEsc = "\u001B" - clearLine = "\r\033[K" - hideCursor = ansiEsc + "[?25l" - showCursor = ansiEsc + "[?25h" -) - -func newUserError(a ...any) *simplecobra.CommandError { - return &simplecobra.CommandError{Err: errors.New(fmt.Sprint(a...))} -} - -func setValueFromFlag(flags *pflag.FlagSet, key string, cfg config.Provider, targetKey string, force bool) { - key = strings.TrimSpace(key) - if (force && flags.Lookup(key) != nil) || flags.Changed(key) { - f := flags.Lookup(key) - configKey := key - if targetKey != "" { - configKey = targetKey - } - // Gotta love this API. - switch f.Value.Type() { - case "bool": - bv, _ := flags.GetBool(key) - cfg.Set(configKey, bv) - case "string": - cfg.Set(configKey, f.Value.String()) - case "stringSlice": - bv, _ := flags.GetStringSlice(key) - cfg.Set(configKey, bv) - case "int": - iv, _ := flags.GetInt(key) - cfg.Set(configKey, iv) - default: - panic(fmt.Sprintf("update switch with %s", f.Value.Type())) - } - - } -} - -func flagsToCfg(cd *simplecobra.Commandeer, cfg config.Provider) config.Provider { - return flagsToCfgWithAdditionalConfigBase(cd, cfg, "") -} - -func flagsToCfgWithAdditionalConfigBase(cd *simplecobra.Commandeer, cfg config.Provider, additionalConfigBase string) config.Provider { - if cfg == nil { - cfg = config.New() - } - - // Flags with a different name in the config. - keyMap := map[string]string{ - "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, - "liveReloadPort": true, - "renderToMemory": true, - "clock": true, - } - - cmd := cd.CobraCommand - flags := cmd.Flags() - - flags.VisitAll(func(f *pflag.Flag) { - if f.Changed { - targetKey := f.Name - if internalKeySet[targetKey] { - targetKey = "internal." + targetKey - } else if mapped, ok := keyMap[targetKey]; ok { - targetKey = mapped - } - setValueFromFlag(flags, f.Name, cfg, targetKey, false) - if additionalConfigBase != "" { - setValueFromFlag(flags, f.Name, cfg, additionalConfigBase+"."+targetKey, true) - } - } - }) - - return cfg -} - -func mkdir(x ...string) { - p := filepath.Join(x...) - err := os.MkdirAll(p, 0o777) // before umask - if err != nil { - log.Fatal(err) - } -} diff --git a/commands/hugo.go b/commands/hugo.go new file mode 100644 index 000000000..b939ce6e9 --- /dev/null +++ b/commands/hugo.go @@ -0,0 +1,1078 @@ +// Copyright 2016 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 defines and implements command-line commands and flags +// used by Hugo. Commands and flags are implemented using Cobra. +package commands + +import ( + "fmt" + "io/ioutil" + + "github.com/gohugoio/hugo/hugofs" + + "log" + "net/http" + "os" + "path/filepath" + "runtime" + "strings" + "sync" + "time" + + "github.com/gohugoio/hugo/config" + + "github.com/gohugoio/hugo/parser" + flag "github.com/spf13/pflag" + + "regexp" + + "github.com/fsnotify/fsnotify" + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugolib" + "github.com/gohugoio/hugo/livereload" + "github.com/gohugoio/hugo/utils" + "github.com/gohugoio/hugo/watcher" + "github.com/spf13/afero" + "github.com/spf13/cobra" + "github.com/spf13/fsync" + jww "github.com/spf13/jwalterweatherman" + "github.com/spf13/nitro" + "github.com/spf13/viper" +) + +// Hugo represents the Hugo sites to build. This variable is exported as it +// is used by at least one external library (the Hugo caddy plugin). We should +// provide a cleaner external API, but until then, this is it. +var Hugo *hugolib.HugoSites + +// Reset resets Hugo ready for a new full build. This is mainly only useful +// for benchmark testing etc. via the CLI commands. +func Reset() error { + Hugo = nil + return nil +} + +// commandError is an error used to signal different error situations in command handling. +type commandError struct { + s string + userError bool +} + +func (c commandError) Error() string { + return c.s +} + +func (c commandError) isUserError() bool { + return c.userError +} + +func newUserError(a ...interface{}) commandError { + return commandError{s: fmt.Sprintln(a...), userError: true} +} + +func newSystemError(a ...interface{}) commandError { + return commandError{s: fmt.Sprintln(a...), userError: false} +} + +func newSystemErrorF(format string, a ...interface{}) commandError { + return commandError{s: fmt.Sprintf(format, a...), userError: false} +} + +// Catch some of the obvious user errors from Cobra. +// We don't want to show the usage message for every error. +// The below may be to generic. Time will show. +var userErrorRegexp = regexp.MustCompile("argument|flag|shorthand") + +func isUserError(err error) bool { + if cErr, ok := err.(commandError); ok && cErr.isUserError() { + return true + } + + return userErrorRegexp.MatchString(err.Error()) +} + +// HugoCmd is Hugo's root command. +// Every other command attached to HugoCmd is a child command to it. +var HugoCmd = &cobra.Command{ + Use: "hugo", + Short: "hugo builds your site", + Long: `hugo 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 http://gohugo.io/.`, + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := InitializeConfig() + if err != nil { + return err + } + + c, err := newCommandeer(cfg) + if err != nil { + return err + } + + if buildWatch { + cfg.Cfg.Set("disableLiveReload", true) + c.watchConfig() + } + + return c.build() + }, +} + +var hugoCmdV *cobra.Command + +// Flags that are to be added to commands. +var ( + buildWatch bool + logging bool + renderToMemory bool // for benchmark testing + verbose bool + verboseLog bool + quiet bool +) + +var ( + baseURL string + cacheDir string + contentDir string + layoutDir string + cfgFile string + destination string + logFile string + theme string + themesDir string + source string + logI18nWarnings bool + disableKinds []string +) + +// Execute adds all child commands to the root command HugoCmd and sets flags appropriately. +func Execute() { + HugoCmd.SetGlobalNormalizationFunc(helpers.NormalizeHugoFlags) + + HugoCmd.SilenceUsage = true + + AddCommands() + + if c, err := HugoCmd.ExecuteC(); err != nil { + if isUserError(err) { + c.Println("") + c.Println(c.UsageString()) + } + + os.Exit(-1) + } +} + +// AddCommands adds child commands to the root command HugoCmd. +func AddCommands() { + HugoCmd.AddCommand(serverCmd) + HugoCmd.AddCommand(versionCmd) + HugoCmd.AddCommand(envCmd) + HugoCmd.AddCommand(configCmd) + HugoCmd.AddCommand(checkCmd) + HugoCmd.AddCommand(benchmarkCmd) + HugoCmd.AddCommand(convertCmd) + HugoCmd.AddCommand(newCmd) + HugoCmd.AddCommand(listCmd) + HugoCmd.AddCommand(undraftCmd) + HugoCmd.AddCommand(importCmd) + + HugoCmd.AddCommand(genCmd) + genCmd.AddCommand(genautocompleteCmd) + genCmd.AddCommand(gendocCmd) + genCmd.AddCommand(genmanCmd) + genCmd.AddCommand(createGenDocsHelper().cmd) +} + +// initHugoBuilderFlags initializes all common flags, typically used by the +// core build commands, namely hugo itself, server, check and benchmark. +func initHugoBuilderFlags(cmd *cobra.Command) { + initHugoBuildCommonFlags(cmd) +} + +func initRootPersistentFlags() { + HugoCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is path/config.yaml|json|toml)") + HugoCmd.PersistentFlags().BoolVar(&quiet, "quiet", false, "build in quiet mode") + + // Set bash-completion + validConfigFilenames := []string{"json", "js", "yaml", "yml", "toml", "tml"} + _ = HugoCmd.PersistentFlags().SetAnnotation("config", cobra.BashCompFilenameExt, validConfigFilenames) +} + +// initHugoBuildCommonFlags initialize common flags related to the Hugo build. +// Called by initHugoBuilderFlags. +func initHugoBuildCommonFlags(cmd *cobra.Command) { + cmd.Flags().Bool("cleanDestinationDir", false, "remove files from destination not found in static directories") + cmd.Flags().BoolP("buildDrafts", "D", false, "include content marked as draft") + cmd.Flags().BoolP("buildFuture", "F", false, "include content with publishdate in the future") + cmd.Flags().BoolP("buildExpired", "E", false, "include expired content") + cmd.Flags().Bool("disable404", false, "do not render 404 page") + cmd.Flags().Bool("disableRSS", false, "do not build RSS files") + cmd.Flags().Bool("disableSitemap", false, "do not build Sitemap file") + cmd.Flags().StringVarP(&source, "source", "s", "", "filesystem path to read files relative from") + cmd.Flags().StringVarP(&contentDir, "contentDir", "c", "", "filesystem path to content directory") + cmd.Flags().StringVarP(&layoutDir, "layoutDir", "l", "", "filesystem path to layout directory") + cmd.Flags().StringVarP(&cacheDir, "cacheDir", "", "", "filesystem path to cache directory. Defaults: $TMPDIR/hugo_cache/") + cmd.Flags().BoolP("ignoreCache", "", false, "ignores the cache directory") + cmd.Flags().StringVarP(&destination, "destination", "d", "", "filesystem path to write files to") + cmd.Flags().StringVarP(&theme, "theme", "t", "", "theme to use (located in /themes/THEMENAME/)") + cmd.Flags().StringVarP(&themesDir, "themesDir", "", "", "filesystem path to themes directory") + cmd.Flags().Bool("uglyURLs", false, "if true, use /filename.html instead of /filename/") + cmd.Flags().Bool("canonifyURLs", false, "if true, all relative URLs will be canonicalized using baseURL") + cmd.Flags().StringVarP(&baseURL, "baseURL", "b", "", "hostname (and path) to the root, e.g. http://spf13.com/") + cmd.Flags().Bool("enableGitInfo", false, "add Git revision, date and author info to the pages") + + cmd.Flags().BoolVar(&nitro.AnalysisOn, "stepAnalysis", false, "display memory and timing of different steps of the program") + cmd.Flags().Bool("pluralizeListTitles", true, "pluralize titles in lists using inflect") + cmd.Flags().Bool("preserveTaxonomyNames", false, `preserve taxonomy names as written ("Gérard Depardieu" vs "gerard-depardieu")`) + cmd.Flags().BoolP("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().BoolVarP(&logI18nWarnings, "i18n-warnings", "", false, "print missing translations") + + cmd.Flags().StringSliceVar(&disableKinds, "disableKinds", []string{}, "disable different kind of pages (home, RSS etc.)") + + // Set bash-completion. + // Each flag must first be defined before using the SetAnnotation() call. + _ = cmd.Flags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{}) + _ = cmd.Flags().SetAnnotation("cacheDir", cobra.BashCompSubdirsInDir, []string{}) + _ = cmd.Flags().SetAnnotation("destination", cobra.BashCompSubdirsInDir, []string{}) + _ = cmd.Flags().SetAnnotation("theme", cobra.BashCompSubdirsInDir, []string{"themes"}) +} + +func initBenchmarkBuildingFlags(cmd *cobra.Command) { + cmd.Flags().BoolVar(&renderToMemory, "renderToMemory", false, "render to memory (only useful for benchmark testing)") +} + +// init initializes flags. +func init() { + HugoCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output") + HugoCmd.PersistentFlags().BoolVar(&logging, "log", false, "enable Logging") + HugoCmd.PersistentFlags().StringVar(&logFile, "logFile", "", "log File path (if set, logging enabled automatically)") + HugoCmd.PersistentFlags().BoolVar(&verboseLog, "verboseLog", false, "verbose logging") + + initRootPersistentFlags() + initHugoBuilderFlags(HugoCmd) + initBenchmarkBuildingFlags(HugoCmd) + + HugoCmd.Flags().BoolVarP(&buildWatch, "watch", "w", false, "watch filesystem for changes and recreate as needed") + hugoCmdV = HugoCmd + + // Set bash-completion + _ = HugoCmd.PersistentFlags().SetAnnotation("logFile", cobra.BashCompFilenameExt, []string{}) +} + +// InitializeConfig initializes a config file with sensible default configuration flags. +func InitializeConfig(subCmdVs ...*cobra.Command) (*deps.DepsCfg, error) { + + var cfg *deps.DepsCfg = &deps.DepsCfg{} + + // Init file systems. This may be changed at a later point. + osFs := hugofs.Os + + config, err := hugolib.LoadConfig(osFs, source, cfgFile) + if err != nil { + return cfg, err + } + + // Init file systems. This may be changed at a later point. + cfg.Cfg = config + + c, err := newCommandeer(cfg) + if err != nil { + return nil, err + } + + for _, cmdV := range append([]*cobra.Command{hugoCmdV}, subCmdVs...) { + c.initializeFlags(cmdV) + } + + if len(disableKinds) > 0 { + c.Set("disableKinds", disableKinds) + } + + logger, err := createLogger(cfg.Cfg) + if err != nil { + return cfg, err + } + + cfg.Logger = logger + + config.Set("logI18nWarnings", logI18nWarnings) + + if baseURL != "" { + config.Set("baseURL", baseURL) + } + + if !config.GetBool("relativeURLs") && config.GetString("baseURL") == "" { + cfg.Logger.ERROR.Println("No 'baseURL' set in configuration or as a flag. Features like page menus will not work without one.") + } + + if theme != "" { + config.Set("theme", theme) + } + + if themesDir != "" { + config.Set("themesDir", themesDir) + } + + if destination != "" { + config.Set("publishDir", destination) + } + + var dir string + if source != "" { + dir, _ = filepath.Abs(source) + } else { + dir, _ = os.Getwd() + } + config.Set("workingDir", dir) + + fs := hugofs.NewFrom(osFs, config) + + // Hugo writes the output to memory instead of the disk. + // This is only used for benchmark testing. Cause the content is only visible + // in memory. + if renderToMemory { + fs.Destination = new(afero.MemMapFs) + // Rendering to memoryFS, publish to Root regardless of publishDir. + c.Set("publishDir", "/") + } + + if contentDir != "" { + config.Set("contentDir", contentDir) + } + + if layoutDir != "" { + config.Set("layoutDir", layoutDir) + } + + if cacheDir != "" { + config.Set("cacheDir", cacheDir) + } + + cacheDir = config.GetString("cacheDir") + if cacheDir != "" { + if helpers.FilePathSeparator != cacheDir[len(cacheDir)-1:] { + cacheDir = cacheDir + helpers.FilePathSeparator + } + isDir, err := helpers.DirExists(cacheDir, fs.Source) + utils.CheckErr(cfg.Logger, err) + if !isDir { + mkdir(cacheDir) + } + config.Set("cacheDir", cacheDir) + } else { + config.Set("cacheDir", helpers.GetTempDir("hugo_cache", fs.Source)) + } + + if err := c.initFs(fs); err != nil { + return nil, err + } + + cfg.Logger.INFO.Println("Using config file:", viper.ConfigFileUsed()) + + themeDir := c.PathSpec().GetThemeDir() + if themeDir != "" { + if _, err := cfg.Fs.Source.Stat(themeDir); os.IsNotExist(err) { + return cfg, newSystemError("Unable to find theme Directory:", themeDir) + } + } + + themeVersionMismatch, minVersion := c.isThemeVsHugoVersionMismatch() + + if themeVersionMismatch { + cfg.Logger.ERROR.Printf("Current theme does not support Hugo version %s. Minimum version required is %s\n", + helpers.CurrentHugoVersion.ReleaseVersion(), minVersion) + } + + return cfg, nil + +} + +func createLogger(cfg config.Provider) (*jww.Notepad, error) { + var ( + logHandle = ioutil.Discard + logThreshold = jww.LevelWarn + logFile = cfg.GetString("logFile") + outHandle = os.Stdout + stdoutThreshold = jww.LevelError + ) + + if verboseLog || logging || (logFile != "") { + var err error + if logFile != "" { + logHandle, err = os.OpenFile(logFile, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666) + if err != nil { + return nil, newSystemError("Failed to open log file:", logFile, err) + } + } else { + logHandle, err = ioutil.TempFile("", "hugo") + if err != nil { + return nil, newSystemError(err) + } + } + } else if !quiet && cfg.GetBool("verbose") { + stdoutThreshold = jww.LevelInfo + } + + if verboseLog { + logThreshold = jww.LevelInfo + } + + // The global logger is used in some few cases. + jww.SetLogOutput(logHandle) + jww.SetLogThreshold(logThreshold) + jww.SetStdoutThreshold(stdoutThreshold) + helpers.InitLoggers() + + return jww.NewNotepad(stdoutThreshold, logThreshold, outHandle, logHandle, "", log.Ldate|log.Ltime), nil +} + +func (c *commandeer) initializeFlags(cmd *cobra.Command) { + persFlagKeys := []string{"verbose", "logFile"} + flagKeys := []string{ + "cleanDestinationDir", + "buildDrafts", + "buildFuture", + "buildExpired", + "uglyURLs", + "canonifyURLs", + "disable404", + "disableRSS", + "disableSitemap", + "enableRobotsTXT", + "enableGitInfo", + "pluralizeListTitles", + "preserveTaxonomyNames", + "ignoreCache", + "forceSyncStatic", + "noTimes", + "noChmod", + } + + // Remove these in Hugo 0.23. + if cmd.Flags().Changed("disable404") { + helpers.Deprecated("command line", "--disable404", "Use --disableKinds=404", false) + } + + if cmd.Flags().Changed("disableRSS") { + helpers.Deprecated("command line", "--disableRSS", "Use --disableKinds=RSS", false) + } + + if cmd.Flags().Changed("disableSitemap") { + helpers.Deprecated("command line", "--disableSitemap", "Use --disableKinds=sitemap", false) + } + + for _, key := range persFlagKeys { + c.setValueFromFlag(cmd.PersistentFlags(), key) + } + for _, key := range flagKeys { + c.setValueFromFlag(cmd.Flags(), key) + } + +} + +func (c *commandeer) setValueFromFlag(flags *flag.FlagSet, key string) { + if flags.Changed(key) { + f := flags.Lookup(key) + c.Set(key, f.Value.String()) + } +} + +func (c *commandeer) watchConfig() { + v := c.Cfg.(*viper.Viper) + v.WatchConfig() + v.OnConfigChange(func(e fsnotify.Event) { + c.Logger.FEEDBACK.Println("Config file changed:", e.Name) + // Force a full rebuild + utils.CheckErr(c.Logger, c.recreateAndBuildSites(true)) + if !c.Cfg.GetBool("disableLiveReload") { + // Will block forever trying to write to a channel that nobody is reading if livereload isn't initialized + livereload.ForceRefresh() + } + }) +} + +func (c *commandeer) build(watches ...bool) error { + if err := c.copyStatic(); err != nil { + return fmt.Errorf("Error copying static files to %s: %s", c.PathSpec().AbsPathify(c.Cfg.GetString("publishDir")), err) + } + watch := false + if len(watches) > 0 && watches[0] { + watch = true + } + if err := c.buildSites(buildWatch || watch); err != nil { + return fmt.Errorf("Error building site: %s", err) + } + + if buildWatch { + c.Logger.FEEDBACK.Println("Watching for changes in", c.PathSpec().AbsPathify(c.Cfg.GetString("contentDir"))) + c.Logger.FEEDBACK.Println("Press Ctrl+C to stop") + utils.CheckErr(c.Logger, c.newWatcher(0)) + } + + return nil +} + +func (c *commandeer) getStaticSourceFs() afero.Fs { + source := c.Fs.Source + themeDir, err := c.PathSpec().GetThemeStaticDirPath() + staticDir := c.PathSpec().GetStaticDirPath() + helpers.FilePathSeparator + useTheme := true + useStatic := true + + if err != nil { + if err != helpers.ErrThemeUndefined { + c.Logger.WARN.Println(err) + } + useTheme = false + } else { + if _, err := source.Stat(themeDir); os.IsNotExist(err) { + c.Logger.WARN.Println("Unable to find Theme Static Directory:", themeDir) + useTheme = false + } + } + + if _, err := source.Stat(staticDir); os.IsNotExist(err) { + c.Logger.WARN.Println("Unable to find Static Directory:", staticDir) + useStatic = false + } + + if !useStatic && !useTheme { + return nil + } + + if !useStatic { + c.Logger.INFO.Println(themeDir, "is the only static directory available to sync from") + return afero.NewReadOnlyFs(afero.NewBasePathFs(source, themeDir)) + } + + if !useTheme { + c.Logger.INFO.Println(staticDir, "is the only static directory available to sync from") + return afero.NewReadOnlyFs(afero.NewBasePathFs(source, staticDir)) + } + + c.Logger.INFO.Println("using a UnionFS for static directory comprised of:") + c.Logger.INFO.Println("Base:", themeDir) + c.Logger.INFO.Println("Overlay:", staticDir) + base := afero.NewReadOnlyFs(afero.NewBasePathFs(source, themeDir)) + overlay := afero.NewReadOnlyFs(afero.NewBasePathFs(source, staticDir)) + return afero.NewCopyOnWriteFs(base, overlay) +} + +func (c *commandeer) copyStatic() error { + publishDir := c.PathSpec().AbsPathify(c.Cfg.GetString("publishDir")) + helpers.FilePathSeparator + + // If root, remove the second '/' + if publishDir == "//" { + publishDir = helpers.FilePathSeparator + } + + // Includes both theme/static & /static + staticSourceFs := c.getStaticSourceFs() + + if staticSourceFs == nil { + c.Logger.WARN.Println("No static directories found to sync") + return nil + } + + syncer := fsync.NewSyncer() + syncer.NoTimes = c.Cfg.GetBool("noTimes") + syncer.NoChmod = c.Cfg.GetBool("noChmod") + syncer.SrcFs = staticSourceFs + syncer.DestFs = c.Fs.Destination + // Now that we are using a unionFs for the static directories + // We can effectively clean the publishDir on initial sync + syncer.Delete = c.Cfg.GetBool("cleanDestinationDir") + + if syncer.Delete { + c.Logger.INFO.Println("removing all files from destination that don't exist in static dirs") + + syncer.DeleteFilter = func(f os.FileInfo) bool { + return f.IsDir() && strings.HasPrefix(f.Name(), ".") + } + } + c.Logger.INFO.Println("syncing static files to", publishDir) + + // because we are using a baseFs (to get the union right). + // set sync src to root + return syncer.Sync(publishDir, helpers.FilePathSeparator) +} + +// getDirList provides NewWatcher() with a list of directories to watch for changes. +func (c *commandeer) getDirList() []string { + var a []string + dataDir := c.PathSpec().AbsPathify(c.Cfg.GetString("dataDir")) + i18nDir := c.PathSpec().AbsPathify(c.Cfg.GetString("i18nDir")) + layoutDir := c.PathSpec().GetLayoutDirPath() + staticDir := c.PathSpec().GetStaticDirPath() + + walker := func(path string, fi os.FileInfo, err error) error { + if err != nil { + if path == dataDir && os.IsNotExist(err) { + c.Logger.WARN.Println("Skip dataDir:", err) + return nil + } + + if path == i18nDir && os.IsNotExist(err) { + c.Logger.WARN.Println("Skip i18nDir:", err) + return nil + } + + if path == layoutDir && os.IsNotExist(err) { + c.Logger.WARN.Println("Skip layoutDir:", err) + return nil + } + + if path == staticDir && os.IsNotExist(err) { + c.Logger.WARN.Println("Skip staticDir:", err) + return nil + } + + if os.IsNotExist(err) { + // Ignore. + return nil + } + + c.Logger.ERROR.Println("Walker: ", err) + return nil + } + + // Skip .git directories. + // Related to https://github.com/gohugoio/hugo/issues/3468. + if fi.Name() == ".git" { + return nil + } + + if fi.Mode()&os.ModeSymlink == os.ModeSymlink { + link, err := filepath.EvalSymlinks(path) + if err != nil { + c.Logger.ERROR.Printf("Cannot read symbolic link '%s', error was: %s", path, err) + return nil + } + linkfi, err := c.Fs.Source.Stat(link) + if err != nil { + c.Logger.ERROR.Printf("Cannot stat '%s', error was: %s", link, err) + return nil + } + if !linkfi.Mode().IsRegular() { + c.Logger.ERROR.Printf("Symbolic links for directories not supported, skipping '%s'", path) + } + return nil + } + + if fi.IsDir() { + if fi.Name() == ".git" || + fi.Name() == "node_modules" || fi.Name() == "bower_components" { + return filepath.SkipDir + } + a = append(a, path) + } + return nil + } + + // SymbolicWalk will log anny ERRORs + _ = helpers.SymbolicWalk(c.Fs.Source, dataDir, walker) + _ = helpers.SymbolicWalk(c.Fs.Source, c.PathSpec().AbsPathify(c.Cfg.GetString("contentDir")), walker) + _ = helpers.SymbolicWalk(c.Fs.Source, i18nDir, walker) + _ = helpers.SymbolicWalk(c.Fs.Source, layoutDir, walker) + _ = helpers.SymbolicWalk(c.Fs.Source, staticDir, walker) + + if c.PathSpec().ThemeSet() { + themesDir := c.PathSpec().GetThemeDir() + _ = helpers.SymbolicWalk(c.Fs.Source, filepath.Join(themesDir, "layouts"), walker) + _ = helpers.SymbolicWalk(c.Fs.Source, filepath.Join(themesDir, "static"), walker) + _ = helpers.SymbolicWalk(c.Fs.Source, filepath.Join(themesDir, "i18n"), walker) + _ = helpers.SymbolicWalk(c.Fs.Source, filepath.Join(themesDir, "data"), walker) + } + + return a +} + +func (c *commandeer) recreateAndBuildSites(watching bool) (err error) { + if err := c.initSites(); err != nil { + return err + } + if !quiet { + c.Logger.FEEDBACK.Println("Started building sites ...") + } + return Hugo.Build(hugolib.BuildCfg{CreateSitesFromConfig: true, Watching: watching, PrintStats: !quiet}) +} + +func (c *commandeer) resetAndBuildSites(watching bool) (err error) { + if err = c.initSites(); err != nil { + return + } + if !quiet { + c.Logger.FEEDBACK.Println("Started building sites ...") + } + return Hugo.Build(hugolib.BuildCfg{ResetState: true, Watching: watching, PrintStats: !quiet}) +} + +func (c *commandeer) initSites() error { + if Hugo != nil { + return nil + } + h, err := hugolib.NewHugoSites(*c.DepsCfg) + + if err != nil { + return err + } + Hugo = h + + return nil +} + +func (c *commandeer) buildSites(watching bool) (err error) { + if err := c.initSites(); err != nil { + return err + } + if !quiet { + c.Logger.FEEDBACK.Println("Started building sites ...") + } + return Hugo.Build(hugolib.BuildCfg{Watching: watching, PrintStats: !quiet}) +} + +func (c *commandeer) rebuildSites(events []fsnotify.Event) error { + if err := c.initSites(); err != nil { + return err + } + return Hugo.Build(hugolib.BuildCfg{PrintStats: !quiet, Watching: true}, events...) +} + +// newWatcher creates a new watcher to watch filesystem events. +func (c *commandeer) newWatcher(port int) error { + if runtime.GOOS == "darwin" { + tweakLimit() + } + + watcher, err := watcher.New(1 * time.Second) + var wg sync.WaitGroup + + if err != nil { + return err + } + + defer watcher.Close() + + wg.Add(1) + + for _, d := range c.getDirList() { + if d != "" { + _ = watcher.Add(d) + } + } + + go func() { + for { + select { + case evs := <-watcher.Events: + c.Logger.INFO.Println("Received System Events:", evs) + + staticEvents := []fsnotify.Event{} + dynamicEvents := []fsnotify.Event{} + + for _, ev := range evs { + ext := filepath.Ext(ev.Name) + baseName := filepath.Base(ev.Name) + istemp := strings.HasSuffix(ext, "~") || + (ext == ".swp") || // vim + (ext == ".swx") || // vim + (ext == ".tmp") || // generic temp file + (ext == ".DS_Store") || // OSX Thumbnail + baseName == "4913" || // vim + strings.HasPrefix(ext, ".goutputstream") || // gnome + strings.HasSuffix(ext, "jb_old___") || // intelliJ + strings.HasSuffix(ext, "jb_tmp___") || // intelliJ + strings.HasSuffix(ext, "jb_bak___") || // intelliJ + strings.HasPrefix(ext, ".sb-") || // byword + strings.HasPrefix(baseName, ".#") || // emacs + strings.HasPrefix(baseName, "#") // emacs + if istemp { + continue + } + // Sometimes during rm -rf operations a '"": REMOVE' is triggered. Just ignore these + if ev.Name == "" { + 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 os.FileInfo, err error) error { + if f.IsDir() { + c.Logger.FEEDBACK.Println("adding created directory to watchlist", path) + if err := watcher.Add(path); err != nil { + return err + } + } else if !c.isStatic(path) { + // Hugo's rebuilding logic is entirely file based. When you drop a new folder into + // /content on OSX, the above logic will handle future watching of those files, + // but the initial CREATE is lost. + dynamicEvents = append(dynamicEvents, fsnotify.Event{Name: path, Op: fsnotify.Create}) + } + return nil + } + + // 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 s, err := c.Fs.Source.Stat(ev.Name); err == nil && s.Mode().IsDir() { + _ = helpers.SymbolicWalk(c.Fs.Source, ev.Name, walkAdder) + } + } + + if c.isStatic(ev.Name) { + staticEvents = append(staticEvents, ev) + } else { + dynamicEvents = append(dynamicEvents, ev) + } + } + + if len(staticEvents) > 0 { + publishDir := c.PathSpec().AbsPathify(c.Cfg.GetString("publishDir")) + helpers.FilePathSeparator + + // If root, remove the second '/' + if publishDir == "//" { + publishDir = helpers.FilePathSeparator + } + + c.Logger.FEEDBACK.Println("\nStatic file changes detected") + const layout = "2006-01-02 15:04 -0700" + c.Logger.FEEDBACK.Println(time.Now().Format(layout)) + + if c.Cfg.GetBool("forceSyncStatic") { + c.Logger.FEEDBACK.Printf("Syncing all static files\n") + err := c.copyStatic() + if err != nil { + utils.StopOnErr(c.Logger, err, fmt.Sprintf("Error copying static files to %s", publishDir)) + } + } else { + staticSourceFs := c.getStaticSourceFs() + + if staticSourceFs == nil { + c.Logger.WARN.Println("No static directories found to sync") + return + } + + syncer := fsync.NewSyncer() + syncer.NoTimes = c.Cfg.GetBool("noTimes") + syncer.NoChmod = c.Cfg.GetBool("noChmod") + syncer.SrcFs = staticSourceFs + syncer.DestFs = c.Fs.Destination + + // prevent spamming the log on changes + logger := helpers.NewDistinctFeedbackLogger() + + for _, ev := range staticEvents { + // Due to our approach of layering both directories and the content's rendered output + // into one we can't accurately remove a file not in one of the source directories. + // If a file is in the local static dir and also in the theme static dir and we remove + // it from one of those locations we expect it to still exist in the destination + // + // If Hugo generates a file (from the content dir) over a static file + // the content generated file should take precedence. + // + // Because we are now watching and handling individual events it is possible that a static + // event that occupies the same path as a content generated file will take precedence + // until a regeneration of the content takes places. + // + // Hugo assumes that these cases are very rare and will permit this bad behavior + // The alternative is to track every single file and which pipeline rendered it + // and then to handle conflict resolution on every event. + + fromPath := ev.Name + + // If we are here we already know the event took place in a static dir + relPath, err := c.PathSpec().MakeStaticPathRelative(fromPath) + if err != nil { + c.Logger.ERROR.Println(err) + continue + } + + // Remove || rename is harder and will require an assumption. + // Hugo takes the following approach: + // If the static file exists in any of the static source directories after this event + // Hugo will re-sync it. + // If it does not exist in all of the static directories Hugo will remove it. + // + // This assumes that Hugo has not generated content on top of a static file and then removed + // the source of that static file. In this case Hugo will incorrectly remove that file + // from the published directory. + if ev.Op&fsnotify.Rename == fsnotify.Rename || ev.Op&fsnotify.Remove == fsnotify.Remove { + if _, err := staticSourceFs.Stat(relPath); os.IsNotExist(err) { + // If file doesn't exist in any static dir, remove it + toRemove := filepath.Join(publishDir, relPath) + logger.Println("File no longer exists in static dir, removing", toRemove) + _ = c.Fs.Destination.RemoveAll(toRemove) + } else if err == nil { + // If file still exists, sync it + logger.Println("Syncing", relPath, "to", publishDir) + if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil { + c.Logger.ERROR.Println(err) + } + } else { + c.Logger.ERROR.Println(err) + } + + continue + } + + // For all other event operations Hugo will sync static. + logger.Println("Syncing", relPath, "to", publishDir) + if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil { + c.Logger.ERROR.Println(err) + } + } + } + + if !buildWatch && !c.Cfg.GetBool("disableLiveReload") { + // 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 len(staticEvents) > 0 { + for _, ev := range staticEvents { + path, _ := c.PathSpec().MakeStaticPathRelative(ev.Name) + livereload.RefreshPath(path) + } + + } else { + livereload.ForceRefresh() + } + } + } + + if len(dynamicEvents) > 0 { + c.Logger.FEEDBACK.Println("\nChange detected, rebuilding site") + const layout = "2006-01-02 15:04 -0700" + c.Logger.FEEDBACK.Println(time.Now().Format(layout)) + + if err := c.rebuildSites(dynamicEvents); err != nil { + c.Logger.ERROR.Println("Failed to rebuild site:", err) + } + + if !buildWatch && !c.Cfg.GetBool("disableLiveReload") { + + navigate := c.Cfg.GetBool("navigateToChanged") + + var p *hugolib.Page + + if navigate { + + // It is probably more confusing than useful + // to navigate to a new URL on RENAME etc. + // so for now we use the WRITE event only. + name := pickOneWritePath(dynamicEvents) + + if name != "" { + p = Hugo.GetContentPage(name) + } + } + + if p != nil { + livereload.NavigateToPath(p.RelPermalink()) + } else { + livereload.ForceRefresh() + } + } + } + case err := <-watcher.Errors: + if err != nil { + c.Logger.ERROR.Println(err) + } + } + } + }() + + if port > 0 { + if !c.Cfg.GetBool("disableLiveReload") { + livereload.Initialize() + http.HandleFunc("/livereload.js", livereload.ServeJS) + http.HandleFunc("/livereload", livereload.Handler) + } + + go c.serve(port) + } + + wg.Wait() + return nil +} + +func pickOneWritePath(events []fsnotify.Event) string { + name := "" + + for _, ev := range events { + if ev.Op&fsnotify.Write == fsnotify.Write && len(ev.Name) > len(name) { + name = ev.Name + } + } + + return name +} + +func (c *commandeer) isStatic(path string) bool { + return strings.HasPrefix(path, c.PathSpec().GetStaticDirPath()) || (len(c.PathSpec().GetThemesDirPath()) > 0 && strings.HasPrefix(path, c.PathSpec().GetThemesDirPath())) +} + +// isThemeVsHugoVersionMismatch returns whether the current Hugo version is +// less than the theme's min_version. +func (c *commandeer) isThemeVsHugoVersionMismatch() (mismatch bool, requiredMinVersion string) { + if !c.PathSpec().ThemeSet() { + return + } + + themeDir := c.PathSpec().GetThemeDir() + + path := filepath.Join(themeDir, "theme.toml") + + exists, err := helpers.Exists(path, c.Fs.Source) + + if err != nil || !exists { + return + } + + b, err := afero.ReadFile(c.Fs.Source, path) + + tomlMeta, err := parser.HandleTOMLMetaData(b) + + if err != nil { + return + } + + config := tomlMeta.(map[string]interface{}) + + if minVersion, ok := config["min_version"]; ok { + return helpers.CompareVersion(minVersion) > 0, fmt.Sprint(minVersion) + } + + return +} diff --git a/commands/hugo_windows.go b/commands/hugo_windows.go index c354e889d..7342f21a0 100644 --- a/commands/hugo_windows.go +++ b/commands/hugo_windows.go @@ -1,4 +1,4 @@ -// Copyright 2024 The Hugo Authors. All rights reserved. +// Copyright 2015 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,21 +13,15 @@ package commands -import ( - // For time zone lookups on Windows without Go installed. - // See #8892 - _ "time/tzdata" - - "github.com/spf13/cobra" -) +import "github.com/spf13/cobra" 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 websites. + Hugo is a command-line tool for generating static website. - You need to open PowerShell and run Hugo from there. - - Visit https://gohugo.io/ for more information.` + You need to open cmd.exe and run Hugo from there. + + Visit http://gohugo.io/ for more information.` } diff --git a/commands/hugobuilder.go b/commands/hugobuilder.go deleted file mode 100644 index 3b57ac5e9..000000000 --- a/commands/hugobuilder.go +++ /dev/null @@ -1,1157 +0,0 @@ -// 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 ( - "context" - "errors" - "fmt" - "os" - "path/filepath" - "runtime" - "runtime/pprof" - "runtime/trace" - "strings" - "sync" - "sync/atomic" - "time" - - "github.com/bep/simplecobra" - "github.com/fsnotify/fsnotify" - "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" - "github.com/gohugoio/hugo/helpers" - "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/watcher" - "github.com/spf13/fsync" - "golang.org/x/sync/errgroup" - "golang.org/x/sync/semaphore" -) - -type hugoBuilder struct { - r *rootCommand - - confmu sync.Mutex - conf *commonConfig - - // May be nil. - s *serverCommand - - // Currently only set when in "fast render mode". - changeDetector *fileChangeDetector - visitedURLs *types.EvictingQueue[string] - - fullRebuildSem *semaphore.Weighted - debounce func(f func()) - - onConfigLoaded func(reloaded bool) error - - fastRenderMode 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) -} - -func (c *hugoBuilder) withConf(fn func(conf *commonConfig)) { - c.confmu.Lock() - defer c.confmu.Unlock() - fn(c.conf) -} - -type hugoBuilderErrState struct { - mu sync.Mutex - paused bool - builderr error - waserr bool -} - -func (e *hugoBuilderErrState) setPaused(p bool) { - e.mu.Lock() - defer e.mu.Unlock() - e.paused = p -} - -func (e *hugoBuilderErrState) isPaused() bool { - e.mu.Lock() - defer e.mu.Unlock() - return e.paused -} - -func (e *hugoBuilderErrState) setBuildErr(err error) { - e.mu.Lock() - defer e.mu.Unlock() - e.builderr = err -} - -func (e *hugoBuilderErrState) buildErr() error { - e.mu.Lock() - defer e.mu.Unlock() - return e.builderr -} - -func (e *hugoBuilderErrState) setWasErr(w bool) { - e.mu.Lock() - defer e.mu.Unlock() - e.waserr = w -} - -func (e *hugoBuilderErrState) wasErr() bool { - e.mu.Lock() - defer e.mu.Unlock() - return e.waserr -} - -// getDirList provides NewWatcher() with a list of directories to watch for changes. -func (c *hugoBuilder) getDirList() ([]string, error) { - h, err := c.hugo() - if err != nil { - return nil, err - } - - return helpers.UniqueStringsSorted(h.PathSpec.BaseFs.WatchFilenames()), nil -} - -func (c *hugoBuilder) initCPUProfile() (func(), error) { - if c.r.cpuprofile == "" { - return nil, nil - } - - f, err := os.Create(c.r.cpuprofile) - if err != nil { - return nil, fmt.Errorf("failed to create CPU profile: %w", err) - } - if err := pprof.StartCPUProfile(f); err != nil { - return nil, fmt.Errorf("failed to start CPU profile: %w", err) - } - return func() { - pprof.StopCPUProfile() - f.Close() - }, nil -} - -func (c *hugoBuilder) initMemProfile() { - if c.r.memprofile == "" { - return - } - - f, err := os.Create(c.r.memprofile) - if err != nil { - c.r.logger.Errorf("could not create memory profile: ", err) - } - defer f.Close() - runtime.GC() // get up-to-date statistics - if err := pprof.WriteHeapProfile(f); err != nil { - c.r.logger.Errorf("could not write memory profile: ", err) - } -} - -func (c *hugoBuilder) initMemTicker() func() { - memticker := time.NewTicker(5 * time.Second) - quit := make(chan struct{}) - printMem := func() { - var m runtime.MemStats - runtime.ReadMemStats(&m) - fmt.Printf("\n\nAlloc = %v\nTotalAlloc = %v\nSys = %v\nNumGC = %v\n\n", formatByteCount(m.Alloc), formatByteCount(m.TotalAlloc), formatByteCount(m.Sys), m.NumGC) - } - - go func() { - for { - select { - case <-memticker.C: - printMem() - case <-quit: - memticker.Stop() - printMem() - return - } - } - }() - - return func() { - close(quit) - } -} - -func (c *hugoBuilder) initMutexProfile() (func(), error) { - if c.r.mutexprofile == "" { - return nil, nil - } - - f, err := os.Create(c.r.mutexprofile) - if err != nil { - return nil, err - } - - runtime.SetMutexProfileFraction(1) - - return func() { - pprof.Lookup("mutex").WriteTo(f, 0) - f.Close() - }, nil -} - -func (c *hugoBuilder) initProfiling() (func(), error) { - stopCPUProf, err := c.initCPUProfile() - if err != nil { - return nil, err - } - - stopMutexProf, err := c.initMutexProfile() - if err != nil { - return nil, err - } - - stopTraceProf, err := c.initTraceProfile() - if err != nil { - return nil, err - } - - var stopMemTicker func() - if c.r.printm { - stopMemTicker = c.initMemTicker() - } - - return func() { - c.initMemProfile() - - if stopCPUProf != nil { - stopCPUProf() - } - if stopMutexProf != nil { - stopMutexProf() - } - - if stopTraceProf != nil { - stopTraceProf() - } - - if stopMemTicker != nil { - stopMemTicker() - } - }, nil -} - -func (c *hugoBuilder) initTraceProfile() (func(), error) { - if c.r.traceprofile == "" { - return nil, nil - } - - f, err := os.Create(c.r.traceprofile) - if err != nil { - return nil, fmt.Errorf("failed to create trace file: %w", err) - } - - if err := trace.Start(f); err != nil { - return nil, fmt.Errorf("failed to start trace: %w", err) - } - - return func() { - trace.Stop() - f.Close() - }, nil -} - -// newWatcher creates a new watcher to watch filesystem events. -func (c *hugoBuilder) newWatcher(pollIntervalStr string, dirList ...string) (*watcher.Batcher, error) { - staticSyncer := &staticSyncer{c: c} - - var pollInterval time.Duration - poll := pollIntervalStr != "" - if poll { - pollInterval, err := types.ToDurationE(pollIntervalStr) - if err != nil { - return nil, fmt.Errorf("invalid value for flag poll: %s", err) - } - c.r.logger.Printf("Use watcher with poll interval %v", pollInterval) - } - - if pollInterval == 0 { - pollInterval = 500 * time.Millisecond - } - - watcher, err := watcher.New(500*time.Millisecond, pollInterval, poll) - if err != nil { - return nil, err - } - - h, err := c.hugo() - if err != nil { - return nil, err - } - spec := h.Deps.SourceSpec - - for _, d := range dirList { - if d != "" { - if spec.IgnoreFile(d) { - continue - } - _ = watcher.Add(d) - } - } - - // Identifies changes to config (config.toml) files. - configSet := make(map[string]bool) - var configFiles []string - c.withConf(func(conf *commonConfig) { - configFiles = conf.configs.LoadingInfo.ConfigFiles - }) - - c.r.Println("Watching for config changes in", strings.Join(configFiles, ", ")) - for _, configFile := range configFiles { - watcher.Add(configFile) - configSet[configFile] = true - } - - 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 { - c.r.logger.Errorln("Failed to acquire a build lock: %s", err) - return - } - c.handleEvents(watcher, staticSyncer, evs, configSet) - if c.showErrorInBrowser && c.errState.buildErr() != nil { - // Need to reload browser to show the error - livereload.ForceRefresh() - } - unlock() - case err := <-watcher.Errors(): - if err != nil && !herrors.IsNotExist(err) { - c.r.logger.Errorln("Error while watching:", err) - } - } - } - }() - - return watcher, nil -} - -func (c *hugoBuilder) build() error { - stopProfiling, err := c.initProfiling() - if err != nil { - return err - } - - defer func() { - if stopProfiling != nil { - stopProfiling() - } - }() - - if err := c.fullBuild(false); err != nil { - return err - } - - if !c.r.quiet { - c.r.Println() - h, err := c.hugo() - if err != nil { - return err - } - - h.PrintProcessingStats(os.Stdout) - c.r.Println() - } - - return nil -} - -func (c *hugoBuilder) buildSites(noBuildLock bool) (err error) { - defer func() { - c.errState.setBuildErr(err) - }() - - var h *hugolib.HugoSites - h, err = c.hugo() - if err != nil { - return - } - err = h.Build(hugolib.BuildCfg{NoBuildLock: noBuildLock}) - return -} - -func (c *hugoBuilder) copyStatic() (map[string]uint64, error) { - m, err := c.doWithPublishDirs(c.copyStaticTo) - if err == nil || herrors.IsNotExist(err) { - return m, nil - } - return m, err -} - -func (c *hugoBuilder) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint64, error) { - infol := c.r.logger.InfoCommand("static") - publishDir := helpers.FilePathSeparator - - if sourceFs.PublishFolder != "" { - publishDir = filepath.Join(publishDir, sourceFs.PublishFolder) - } - - fs := &countingStatFs{Fs: sourceFs.Fs} - - syncer := fsync.NewSyncer() - c.withConf(func(conf *commonConfig) { - syncer.NoTimes = conf.configs.Base.NoTimes - syncer.NoChmod = conf.configs.Base.NoChmod - syncer.ChmodFilter = chmodFilter - - syncer.DestFs = conf.fs.PublishDirStatic - // Now that we are using a unionFs for the static directories - // We can effectively clean the publishDir on initial sync - syncer.Delete = conf.configs.Base.CleanDestinationDir - }) - - syncer.SrcFs = fs - - if syncer.Delete { - infol.Logf("removing all files from destination that don't exist in static dirs") - - syncer.DeleteFilter = func(f fsync.FileInfo) bool { - return f.IsDir() && strings.HasPrefix(f.Name(), ".") - } - } - start := time.Now() - - // because we are using a baseFs (to get the union right). - // set sync src to root - err := syncer.Sync(publishDir, helpers.FilePathSeparator) - if err != nil { - return 0, err - } - loggers.TimeTrackf(infol, start, nil, "syncing static files to %s", publishDir) - - // Sync runs Stat 2 times for every source file. - numFiles := fs.statCounter / 2 - - return numFiles, err -} - -func (c *hugoBuilder) doWithPublishDirs(f func(sourceFs *filesystems.SourceFilesystem) (uint64, error)) (map[string]uint64, error) { - langCount := make(map[string]uint64) - - h, err := c.hugo() - if err != nil { - return nil, err - } - staticFilesystems := h.BaseFs.SourceFilesystems.Static - - if len(staticFilesystems) == 0 { - c.r.logger.Infoln("No static directories found to sync") - return langCount, nil - } - - for lang, fs := range staticFilesystems { - cnt, err := f(fs) - if err != nil { - return langCount, err - } - if lang == "" { - // Not multihost - c.withConf(func(conf *commonConfig) { - for _, l := range conf.configs.Languages { - langCount[l.Lang] = cnt - } - }) - } else { - langCount[lang] = cnt - } - } - - return langCount, nil -} - -func (c *hugoBuilder) fullBuild(noBuildLock bool) error { - var ( - g errgroup.Group - langCount map[string]uint64 - ) - - 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 { - cnt, err := c.copyStatic() - if err != nil { - return fmt.Errorf("error copying static files: %w", err) - } - langCount = cnt - return nil - } - buildSitesFunc := func() error { - if err := c.buildSites(noBuildLock); err != nil { - return fmt.Errorf("error building site: %w", err) - } - return nil - } - // Do not copy static files and build sites in parallel if cleanDestinationDir is enabled. - // This flag deletes all static resources in /public folder that are missing in /static, - // and it does so at the end of copyStatic() call. - var cleanDestinationDir bool - c.withConf(func(conf *commonConfig) { - cleanDestinationDir = conf.configs.Base.CleanDestinationDir - }) - if cleanDestinationDir { - if err := copyStaticFunc(); err != nil { - return err - } - if err := buildSitesFunc(); err != nil { - return err - } - } else { - g.Go(copyStaticFunc) - g.Go(buildSitesFunc) - if err := g.Wait(); err != nil { - return err - } - } - - h, err := c.hugo() - if err != nil { - return err - } - for _, s := range h.Sites { - s.ProcessingStats.Static = langCount[s.Language().Lang] - } - - if c.r.gc { - count, err := h.GC() - if err != nil { - return err - } - for _, s := range h.Sites { - // We have no way of knowing what site the garbage belonged to. - s.ProcessingStats.Cleaned = uint64(count) - } - } - - return nil -} - -func (c *hugoBuilder) fullRebuild(changeType string) { - if changeType == configChangeGoMod { - // go.mod may be changed during the build itself, and - // we really want to prevent superfluous builds. - if !c.fullRebuildSem.TryAcquire(1) { - return - } - c.fullRebuildSem.Release(1) - } - - c.fullRebuildSem.Acquire(context.Background(), 1) - - go func() { - defer c.fullRebuildSem.Release(1) - - c.printChangeDetected(changeType) - - defer func() { - // Allow any file system events to arrive basimplecobra. - // This will block any rebuild on config changes for the - // duration of the sleep. - time.Sleep(2 * time.Second) - }() - - 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) - } - - if !c.errState.isPaused() { - _, err := c.copyStatic() - if err != nil { - c.r.logger.Errorln(err) - return - } - err = c.buildSites(false) - if err != nil { - c.r.logger.Errorln(err) - } else if c.s != nil && c.s.doLiveReload { - livereload.ForceRefresh() - } - } - }() -} - -func (c *hugoBuilder) handleBuildErr(err error, msg string) { - c.errState.setBuildErr(err) - c.r.logger.Errorln(msg + ": " + cleanErrorLog(err.Error())) -} - -func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher, - staticSyncer *staticSyncer, - evs []fsnotify.Event, - 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 - if isConfig { - if strings.Contains(ev.Name, "go.mod") { - configChangeType = configChangeGoMod - } - if strings.Contains(ev.Name, ".work") { - configChangeType = configChangeGoWork - } - } - if !isConfig { - // It may be one of the /config folders - dirname := filepath.Dir(ev.Name) - if dirname != "." && configSet[dirname] { - isConfig = true - } - } - - if isConfig { - isHandled = true - - if ev.Op&fsnotify.Chmod == fsnotify.Chmod { - continue - } - - if ev.Op&fsnotify.Remove == fsnotify.Remove || ev.Op&fsnotify.Rename == fsnotify.Rename { - c.withConf(func(conf *commonConfig) { - for _, configFile := range conf.configs.LoadingInfo.ConfigFiles { - counter := 0 - for watcher.Add(configFile) != nil { - counter++ - if counter >= 100 { - break - } - time.Sleep(100 * time.Millisecond) - } - } - }) - } - - // Config file(s) changed. Need full rebuild. - c.fullRebuild(configChangeType) - - return - } - } - - if isHandled { - return - } - - if c.errState.isPaused() { - // Wait for the server to get into a consistent state before - // we continue with processing. - return - } - - if len(evs) > 50 { - // This is probably a mass edit of the content dir. - // Schedule a full rebuild for when it slows down. - c.debounce(func() { - c.fullRebuild("") - }) - return - } - - c.r.logger.Debugln("Received System Events:", evs) - - staticEvents := []fsnotify.Event{} - dynamicEvents := []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 - } - evs[n] = ev - n++ - } - evs = evs[:n] - - for _, ev := range evs { - ext := filepath.Ext(ev.Name) - baseName := filepath.Base(ev.Name) - 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 - strings.HasPrefix(ext, ".goutputstream") || // gnome - strings.HasSuffix(ext, "jb_old___") || // intelliJ - strings.HasSuffix(ext, "jb_tmp___") || // intelliJ - strings.HasSuffix(ext, "jb_bak___") || // intelliJ - strings.HasPrefix(ext, ".sb-") || // byword - strings.HasPrefix(baseName, ".#") || // emacs - strings.HasPrefix(baseName, "#") // emacs - if istemp { - continue - } - - if h.Deps.SourceSpec.IgnoreFile(ev.Name) { - continue - } - // Sometimes during rm -rf operations a '"": REMOVE' is triggered. Just ignore these - if ev.Name == "" { - continue - } - - 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 { - return err - } - } else if !staticSyncer.isStatic(h, path) { - // Hugo's rebuilding logic is entirely file based. When you drop a new folder into - // /content on OSX, the above logic will handle future watching of those files, - // but the initial CREATE is lost. - dynamicEvents = append(dynamicEvents, fsnotify.Event{Name: path, Op: fsnotify.Create}) - } - return nil - } - - // recursively add new directories to watch list - 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.Walk(conf.fs.Source, ev.Name, walkAdder) - } - }) - } - - if staticSyncer.isStatic(h, ev.Name) { - staticEvents = append(staticEvents, ev) - } else { - dynamicEvents = append(dynamicEvents, ev) - } - } - - lrl := c.r.logger.InfoCommand("livereload") - - staticEvents = filterDuplicateEvents(staticEvents) - dynamicEvents = filterDuplicateEvents(dynamicEvents) - - if len(staticEvents) > 0 { - c.printChangeDetected("Static files") - - if c.r.forceSyncStatic { - c.r.logger.Printf("Syncing all static files\n") - _, err := c.copyStatic() - if err != nil { - c.r.logger.Errorln("Error copying static files to publish dir:", err) - return - } - } else { - if err := staticSyncer.syncsStaticEvents(staticEvents); err != nil { - c.r.logger.Errorln("Error syncing static files to publish dir:", err) - return - } - } - - 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 - - if !c.errState.wasErr() && len(staticEvents) == 1 { - h, err := c.hugo() - if err != nil { - c.r.logger.Errorln("Error getting the Hugo object:", err) - return - } - - 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() - } - } - } - - if len(dynamicEvents) > 0 { - partitionedEvents := partitionDynamicEvents( - h.BaseFs.SourceFilesystems, - dynamicEvents) - - onePageName := pickOneWriteOrCreatePath(h.Conf.ContentTypes(), partitionedEvents.ContentEvents) - - c.printChangeDetected("") - c.changeDetector.PrepareNew() - - func() { - 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 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) - } - if len(changed) == 0 { - // Nothing has changed. - return - } - } - - // 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 { - otherChanges = append(otherChanges, ev) - } - } - - if len(partitionedEvents.ContentEvents) > 0 { - navigate := c.s != nil && c.s.navigateToChanged - // We have fetched the same page above, but it may have - // changed. - var p page.Page - - if navigate { - if onePageName != "" { - p = h.GetContentPage(onePageName) - } - } - - 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 - } - - if c.s != nil { - // A running server, register the media types. - for _, s := range h.Sites { - s.RegisterMediaTypes() - } - } - return h, nil -} - -func (c *hugoBuilder) hugoTry() *hugolib.HugoSites { - var h *hugolib.HugoSites - c.withConf(func(conf *commonConfig) { - h, _ = c.r.HugFromConfig(conf) - }) - return h -} - -func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error { - cfg := config.New() - 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. - // Check if the user has set it in env. - if env := os.Getenv("HUGO_ENVIRONMENT"); env != "" { - c.r.environment = env - } else if env := os.Getenv("HUGO_ENV"); env != "" { - c.r.environment = env - } else { - if c.s != nil { - // The server defaults to development. - c.r.environment = hugo.EnvironmentDevelopment - } else { - c.r.environment = hugo.EnvironmentProduction - } - } - } - cfg.Set("environment", c.r.environment) - - cfg.Set("internal", maps.Params{ - "running": running, - "watch": watch, - "verbose": c.r.isVerbose(), - "fastRenderMode": c.fastRenderMode, - }) - - 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.") - } - - c.conf = conf - if c.onConfigLoaded != nil { - if err := c.onConfigLoaded(false); err != nil { - return err - } - } - - return nil -} - -var rebuildCounter atomic.Uint64 - -func (c *hugoBuilder) printChangeDetected(typ string) { - msg := "\nChange" - if typ != "" { - msg += " of " + typ - } - 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) (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}) - } - } - var h *hugolib.HugoSites - h, err = c.hugo() - if err != nil { - return - } - 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 - } - 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.resetLogs() - c.r.configVersionID.Add(1) - - if err := c.withConfE(func(conf *commonConfig) error { - oldConf := conf - newConf, err := c.r.ConfigFromConfig(configKey{counter: c.r.configVersionID.Load()}, conf) - if err != nil { - return err - } - sameLen := len(oldConf.configs.Languages) == len(newConf.configs.Languages) - if !sameLen { - if oldConf.configs.IsMultihost || newConf.configs.IsMultihost { - return errors.New("multihost change detected, please restart server") - } - } - c.conf = newConf - return nil - }); err != nil { - return err - } - - if c.onConfigLoaded != nil { - if err := c.onConfigLoaded(true); err != nil { - return err - } - } - - return nil -} diff --git a/commands/import.go b/commands/import.go deleted file mode 100644 index 37a6b0dbf..000000000 --- a/commands/import.go +++ /dev/null @@ -1,618 +0,0 @@ -// 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 ( - "bytes" - "context" - "errors" - "fmt" - "io" - "log" - "os" - "path/filepath" - "regexp" - "strconv" - "strings" - "time" - "unicode" - - "github.com/bep/simplecobra" - "github.com/gohugoio/hugo/common/htime" - "github.com/gohugoio/hugo/common/hugio" - "github.com/gohugoio/hugo/common/maps" - "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/hugofs" - "github.com/gohugoio/hugo/parser" - "github.com/gohugoio/hugo/parser/metadecoders" - "github.com/gohugoio/hugo/parser/pageparser" - "github.com/spf13/afero" - "github.com/spf13/cobra" -) - -func newImportCommand() *importCommand { - var c *importCommand - c = &importCommand{ - commands: []simplecobra.Commander{ - &simpleCommand{ - name: "jekyll", - short: "hugo import from Jekyll", - long: `hugo import from Jekyll. - -Import from Jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.", - run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { - if len(args) < 2 { - return newUserError(`import from jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.") - } - 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") - }, - }, - }, - } - - return c -} - -type importCommand struct { - r *rootCommand - - force bool - - commands []simplecobra.Commander -} - -func (c *importCommand) Commands() []simplecobra.Commander { - return c.commands -} - -func (c *importCommand) Name() string { - return "import" -} - -func (c *importCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error { - return nil -} - -func (c *importCommand) Init(cd *simplecobra.Commandeer) error { - cmd := cd.CobraCommand - 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 -} - -func (c *importCommand) PreRun(cd, runner *simplecobra.Commandeer) error { - c.r = cd.Root.Command.(*rootCommand) - return nil -} - -func (i *importCommand) createConfigFromJekyll(fs afero.Fs, inpath string, kind metadecoders.Format, jekyllConfig map[string]any) (err error) { - title := "My New Hugo Site" - baseURL := "http://example.org/" - - for key, value := range jekyllConfig { - lowerKey := strings.ToLower(key) - - switch lowerKey { - case "title": - if str, ok := value.(string); ok { - title = str - } - - case "url": - if str, ok := value.(string); ok { - baseURL = str - } - } - } - - in := map[string]any{ - "baseURL": baseURL, - "title": title, - "languageCode": "en-us", - "disablePathToLower": true, - } - - var buf bytes.Buffer - err = parser.InterfaceToConfig(in, kind, &buf) - if err != nil { - return err - } - - return helpers.WriteToDisk(filepath.Join(inpath, "hugo."+string(kind)), &buf, fs) -} - -func (c *importCommand) getJekyllDirInfo(fs afero.Fs, jekyllRoot string) (map[string]bool, bool) { - postDirs := make(map[string]bool) - hasAnyPost := false - if entries, err := os.ReadDir(jekyllRoot); err == nil { - for _, entry := range entries { - if entry.IsDir() { - subDir := filepath.Join(jekyllRoot, entry.Name()) - if isPostDir, hasAnyPostInDir := c.retrieveJekyllPostDir(fs, subDir); isPostDir { - postDirs[entry.Name()] = hasAnyPostInDir - if hasAnyPostInDir { - hasAnyPost = true - } - } - } - } - } - return postDirs, hasAnyPost -} - -func (c *importCommand) createSiteFromJekyll(jekyllRoot, targetDir string, jekyllPostDirs map[string]bool) error { - fs := &afero.OsFs{} - if exists, _ := helpers.Exists(targetDir, fs); exists { - if isDir, _ := helpers.IsDir(targetDir, fs); !isDir { - return errors.New("target path \"" + targetDir + "\" exists but is not a directory") - } - - isEmpty, _ := helpers.IsEmpty(targetDir, fs) - - if !isEmpty && !c.force { - return errors.New("target path \"" + targetDir + "\" exists and is not empty") - } - } - - jekyllConfig := c.loadJekyllConfig(fs, jekyllRoot) - - mkdir(targetDir, "layouts") - mkdir(targetDir, "content") - mkdir(targetDir, "archetypes") - mkdir(targetDir, "static") - mkdir(targetDir, "data") - mkdir(targetDir, "themes") - - c.createConfigFromJekyll(fs, targetDir, "yaml", jekyllConfig) - - c.copyJekyllFilesAndFolders(jekyllRoot, filepath.Join(targetDir, "static"), jekyllPostDirs) - - return nil -} - -func (c *importCommand) convertJekyllContent(m any, content string) (string, error) { - metadata, _ := maps.ToStringMapE(m) - - lines := strings.Split(content, "\n") - var resultLines []string - for _, line := range lines { - resultLines = append(resultLines, strings.Trim(line, "\r\n")) - } - - content = strings.Join(resultLines, "\n") - - excerptSep := "" - if value, ok := metadata["excerpt_separator"]; ok { - if str, strOk := value.(string); strOk { - content = strings.Replace(content, strings.TrimSpace(str), excerptSep, -1) - } - } - - replaceList := []struct { - re *regexp.Regexp - replace string - }{ - {regexp.MustCompile("(?i)"), ""}, - {regexp.MustCompile(`\{%\s*raw\s*%\}\s*(.*?)\s*\{%\s*endraw\s*%\}`), "$1"}, - {regexp.MustCompile(`{%\s*endhighlight\s*%}`), "{{< / highlight >}}"}, - } - - for _, replace := range replaceList { - content = replace.re.ReplaceAllString(content, replace.replace) - } - - replaceListFunc := []struct { - re *regexp.Regexp - replace func(string) string - }{ - // Octopress image tag: http://octopress.org/docs/plugins/image-tag/ - {regexp.MustCompile(`{%\s+img\s*(.*?)\s*%}`), c.replaceImageTag}, - {regexp.MustCompile(`{%\s*highlight\s*(.*?)\s*%}`), c.replaceHighlightTag}, - } - - for _, replace := range replaceListFunc { - content = replace.re.ReplaceAllStringFunc(content, replace.replace) - } - - var buf bytes.Buffer - if len(metadata) != 0 { - err := parser.InterfaceToFrontMatter(m, metadecoders.YAML, &buf) - if err != nil { - return "", err - } - } - buf.WriteString(content) - - return buf.String(), nil -} - -func (c *importCommand) convertJekyllMetaData(m any, postName string, postDate time.Time, draft bool) (any, error) { - metadata, err := maps.ToStringMapE(m) - if err != nil { - return nil, err - } - - if draft { - metadata["draft"] = true - } - - for key, value := range metadata { - lowerKey := strings.ToLower(key) - - switch lowerKey { - case "layout": - delete(metadata, key) - case "permalink": - if str, ok := value.(string); ok { - metadata["url"] = str - } - delete(metadata, key) - case "category": - if str, ok := value.(string); ok { - metadata["categories"] = []string{str} - } - delete(metadata, key) - case "excerpt_separator": - if key != lowerKey { - delete(metadata, key) - metadata[lowerKey] = value - } - case "date": - if str, ok := value.(string); ok { - re := regexp.MustCompile(`(\d+):(\d+):(\d+)`) - r := re.FindAllStringSubmatch(str, -1) - if len(r) > 0 { - hour, _ := strconv.Atoi(r[0][1]) - minute, _ := strconv.Atoi(r[0][2]) - second, _ := strconv.Atoi(r[0][3]) - postDate = time.Date(postDate.Year(), postDate.Month(), postDate.Day(), hour, minute, second, 0, time.UTC) - } - } - delete(metadata, key) - } - - } - - metadata["date"] = postDate.Format(time.RFC3339) - - return metadata, nil -} - -func (c *importCommand) convertJekyllPost(path, relPath, targetDir string, draft bool) error { - log.Println("Converting", path) - - filename := filepath.Base(path) - postDate, postName, err := c.parseJekyllFilename(filename) - if err != nil { - c.r.Printf("Failed to parse filename '%s': %s. Skipping.", filename, err) - return nil - } - - log.Println(filename, postDate, postName) - - targetFile := filepath.Join(targetDir, relPath) - targetParentDir := filepath.Dir(targetFile) - os.MkdirAll(targetParentDir, 0o777) - - contentBytes, err := os.ReadFile(path) - if err != nil { - c.r.logger.Errorln("Read file error:", path) - return err - } - pf, err := pageparser.ParseFrontMatterAndContent(bytes.NewReader(contentBytes)) - if err != nil { - return fmt.Errorf("failed to parse file %q: %s", filename, err) - } - newmetadata, err := c.convertJekyllMetaData(pf.FrontMatter, postName, postDate, draft) - if err != nil { - return fmt.Errorf("failed to convert metadata for file %q: %s", filename, err) - } - - content, err := c.convertJekyllContent(newmetadata, string(pf.Content)) - if err != nil { - return fmt.Errorf("failed to convert content for file %q: %s", filename, err) - } - - fs := hugofs.Os - if err := helpers.WriteToDisk(targetFile, strings.NewReader(content), fs); err != nil { - return fmt.Errorf("failed to save file %q: %s", filename, err) - } - return nil -} - -func (c *importCommand) copyJekyllFilesAndFolders(jekyllRoot, dest string, jekyllPostDirs map[string]bool) (err error) { - fs := hugofs.Os - - fi, err := fs.Stat(jekyllRoot) - if err != nil { - return err - } - if !fi.IsDir() { - return errors.New(jekyllRoot + " is not a directory") - } - err = os.MkdirAll(dest, fi.Mode()) - if err != nil { - return err - } - entries, err := os.ReadDir(jekyllRoot) - if err != nil { - return err - } - - for _, entry := range entries { - sfp := filepath.Join(jekyllRoot, entry.Name()) - dfp := filepath.Join(dest, entry.Name()) - if entry.IsDir() { - if entry.Name()[0] != '_' && entry.Name()[0] != '.' { - if _, ok := jekyllPostDirs[entry.Name()]; !ok { - err = hugio.CopyDir(fs, sfp, dfp, nil) - if err != nil { - c.r.logger.Errorln(err) - } - } - } - } else { - lowerEntryName := strings.ToLower(entry.Name()) - exceptSuffix := []string{ - ".md", ".markdown", ".html", ".htm", - ".xml", ".textile", "rakefile", "gemfile", ".lock", - } - isExcept := false - for _, suffix := range exceptSuffix { - if strings.HasSuffix(lowerEntryName, suffix) { - isExcept = true - break - } - } - - if !isExcept && entry.Name()[0] != '.' && entry.Name()[0] != '_' { - err = hugio.CopyFile(fs, sfp, dfp) - if err != nil { - c.r.logger.Errorln(err) - } - } - } - - } - return nil -} - -func (c *importCommand) importFromJekyll(args []string) error { - jekyllRoot, err := filepath.Abs(filepath.Clean(args[0])) - if err != nil { - return newUserError("path error:", args[0]) - } - - targetDir, err := filepath.Abs(filepath.Clean(args[1])) - if err != nil { - return newUserError("path error:", args[1]) - } - - c.r.Println("Import Jekyll from:", jekyllRoot, "to:", targetDir) - - if strings.HasPrefix(filepath.Dir(targetDir), jekyllRoot) { - return newUserError("abort: target path should not be inside the Jekyll root") - } - - fs := afero.NewOsFs() - jekyllPostDirs, hasAnyPost := c.getJekyllDirInfo(fs, jekyllRoot) - if !hasAnyPost { - return errors.New("abort: jekyll root contains neither posts nor drafts") - } - - err = c.createSiteFromJekyll(jekyllRoot, targetDir, jekyllPostDirs) - if err != nil { - return newUserError(err) - } - - c.r.Println("Importing...") - - fileCount := 0 - callback := func(path string, fi hugofs.FileMetaInfo) error { - if fi.IsDir() { - return nil - } - - relPath, err := filepath.Rel(jekyllRoot, path) - if err != nil { - return newUserError("get rel path error:", path) - } - - relPath = filepath.ToSlash(relPath) - draft := false - - switch { - case strings.Contains(relPath, "_posts/"): - relPath = filepath.Join("content/post", strings.Replace(relPath, "_posts/", "", -1)) - case strings.Contains(relPath, "_drafts/"): - relPath = filepath.Join("content/draft", strings.Replace(relPath, "_drafts/", "", -1)) - draft = true - default: - return nil - } - - fileCount++ - return c.convertJekyllPost(path, relPath, targetDir, draft) - } - - for jekyllPostDir, hasAnyPostInDir := range jekyllPostDirs { - if hasAnyPostInDir { - 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") - 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 -} - -func (c *importCommand) loadJekyllConfig(fs afero.Fs, jekyllRoot string) map[string]any { - path := filepath.Join(jekyllRoot, "_config.yml") - - exists, err := helpers.Exists(path, fs) - - if err != nil || !exists { - c.r.Println("_config.yaml not found: Is the specified Jekyll root correct?") - return nil - } - - f, err := fs.Open(path) - if err != nil { - return nil - } - - defer f.Close() - - b, err := io.ReadAll(f) - if err != nil { - return nil - } - - m, err := metadecoders.Default.UnmarshalToMap(b, metadecoders.YAML) - if err != nil { - return nil - } - - return m -} - -func (c *importCommand) parseJekyllFilename(filename string) (time.Time, string, error) { - re := regexp.MustCompile(`(\d+-\d+-\d+)-(.+)\..*`) - r := re.FindAllStringSubmatch(filename, -1) - if len(r) == 0 { - return htime.Now(), "", errors.New("filename not match") - } - - postDate, err := time.Parse("2006-1-2", r[0][1]) - if err != nil { - return htime.Now(), "", err - } - - postName := r[0][2] - - return postDate, postName, nil -} - -func (c *importCommand) replaceHighlightTag(match string) string { - r := regexp.MustCompile(`{%\s*highlight\s*(.*?)\s*%}`) - parts := r.FindStringSubmatch(match) - lastQuote := rune(0) - f := func(c rune) bool { - switch { - case c == lastQuote: - lastQuote = rune(0) - return false - case lastQuote != rune(0): - return false - case unicode.In(c, unicode.Quotation_Mark): - lastQuote = c - return false - default: - return unicode.IsSpace(c) - } - } - // splitting string by space but considering quoted section - items := strings.FieldsFunc(parts[1], f) - - result := bytes.NewBufferString("{{< highlight ") - result.WriteString(items[0]) // language - options := items[1:] - for i, opt := range options { - opt = strings.Replace(opt, "\"", "", -1) - if opt == "linenos" { - opt = "linenos=table" - } - if i == 0 { - opt = " \"" + opt - } - if i < len(options)-1 { - opt += "," - } else if i == len(options)-1 { - opt += "\"" - } - result.WriteString(opt) - } - - result.WriteString(" >}}") - return result.String() -} - -func (c *importCommand) replaceImageTag(match string) string { - r := regexp.MustCompile(`{%\s+img\s*(\p{L}*)\s+([\S]*/[\S]+)\s+(\d*)\s*(\d*)\s*(.*?)\s*%}`) - result := bytes.NewBufferString("{{< figure ") - parts := r.FindStringSubmatch(match) - // Index 0 is the entire string, ignore - c.replaceOptionalPart(result, "class", parts[1]) - c.replaceOptionalPart(result, "src", parts[2]) - c.replaceOptionalPart(result, "width", parts[3]) - c.replaceOptionalPart(result, "height", parts[4]) - // title + alt - part := parts[5] - if len(part) > 0 { - splits := strings.Split(part, "'") - lenSplits := len(splits) - if lenSplits == 1 { - c.replaceOptionalPart(result, "title", splits[0]) - } else if lenSplits == 3 { - c.replaceOptionalPart(result, "title", splits[1]) - } else if lenSplits == 5 { - c.replaceOptionalPart(result, "title", splits[1]) - c.replaceOptionalPart(result, "alt", splits[3]) - } - } - result.WriteString(">}}") - return result.String() -} - -func (c *importCommand) replaceOptionalPart(buffer *bytes.Buffer, partName string, part string) { - if len(part) > 0 { - buffer.WriteString(partName + "=\"" + part + "\" ") - } -} - -func (c *importCommand) retrieveJekyllPostDir(fs afero.Fs, dir string) (bool, bool) { - if strings.HasSuffix(dir, "_posts") || strings.HasSuffix(dir, "_drafts") { - isEmpty, _ := helpers.IsEmpty(dir, fs) - return true, !isEmpty - } - - if entries, err := os.ReadDir(dir); err == nil { - for _, entry := range entries { - if entry.IsDir() { - subDir := filepath.Join(dir, entry.Name()) - if isPostDir, hasAnyPost := c.retrieveJekyllPostDir(fs, subDir); isPostDir { - return isPostDir, hasAnyPost - } - } - } - } - - return false, true -} diff --git a/commands/import_jekyll.go b/commands/import_jekyll.go new file mode 100644 index 000000000..3d89fee0d --- /dev/null +++ b/commands/import_jekyll.go @@ -0,0 +1,614 @@ +// Copyright 2016 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 ( + "bytes" + "errors" + "io" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "time" + + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugofs" + "github.com/gohugoio/hugo/hugolib" + "github.com/gohugoio/hugo/parser" + "github.com/spf13/afero" + "github.com/spf13/cast" + "github.com/spf13/cobra" + jww "github.com/spf13/jwalterweatherman" +) + +func init() { + importCmd.AddCommand(importJekyllCmd) +} + +var importCmd = &cobra.Command{ + Use: "import", + Short: "Import your site from others.", + Long: `Import your site from other web site generators like Jekyll. + +Import requires a subcommand, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.", + RunE: nil, +} + +var importJekyllCmd = &cobra.Command{ + Use: "jekyll", + Short: "hugo import from Jekyll", + Long: `hugo import from Jekyll. + +Import from Jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.", + RunE: importFromJekyll, +} + +func init() { + importJekyllCmd.Flags().Bool("force", false, "allow import into non-empty target directory") +} + +func importFromJekyll(cmd *cobra.Command, args []string) error { + + if len(args) < 2 { + return newUserError(`Import from Jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.") + } + + jekyllRoot, err := filepath.Abs(filepath.Clean(args[0])) + if err != nil { + return newUserError("Path error:", args[0]) + } + + targetDir, err := filepath.Abs(filepath.Clean(args[1])) + if err != nil { + return newUserError("Path error:", args[1]) + } + + jww.INFO.Println("Import Jekyll from:", jekyllRoot, "to:", targetDir) + + if strings.HasPrefix(filepath.Dir(targetDir), jekyllRoot) { + return newUserError("Target path should not be inside the Jekyll root, aborting.") + } + + forceImport, _ := cmd.Flags().GetBool("force") + + fs := afero.NewOsFs() + jekyllPostDirs, hasAnyPost := getJekyllDirInfo(fs, jekyllRoot) + if !hasAnyPost { + return errors.New("Your Jekyll root contains neither posts nor drafts, aborting.") + } + + site, err := createSiteFromJekyll(jekyllRoot, targetDir, jekyllPostDirs, forceImport) + + if err != nil { + return newUserError(err) + } + + jww.FEEDBACK.Println("Importing...") + + fileCount := 0 + callback := func(path string, fi os.FileInfo, err error) error { + if err != nil { + return err + } + + if fi.IsDir() { + return nil + } + + relPath, err := filepath.Rel(jekyllRoot, path) + if err != nil { + return newUserError("Get rel path error:", path) + } + + relPath = filepath.ToSlash(relPath) + draft := false + + switch { + case strings.Contains(relPath, "_posts/"): + relPath = filepath.Join("content/post", strings.Replace(relPath, "_posts/", "", -1)) + case strings.Contains(relPath, "_drafts/"): + relPath = filepath.Join("content/draft", strings.Replace(relPath, "_drafts/", "", -1)) + draft = true + default: + return nil + } + + fileCount++ + return convertJekyllPost(site, path, relPath, targetDir, draft) + } + + for jekyllPostDir, hasAnyPostInDir := range jekyllPostDirs { + if hasAnyPostInDir { + if err = helpers.SymbolicWalk(hugofs.Os, filepath.Join(jekyllRoot, jekyllPostDir), callback); err != nil { + return err + } + } + } + + jww.FEEDBACK.Println("Congratulations!", fileCount, "post(s) imported!") + jww.FEEDBACK.Println("Now, start Hugo by yourself:\n" + + "$ git clone https://github.com/spf13/herring-cove.git " + args[1] + "/themes/herring-cove") + jww.FEEDBACK.Println("$ cd " + args[1] + "\n$ hugo server --theme=herring-cove") + + jww.FEEDBACK.Println("Congratulations!", fileCount, "post(s) imported!") + jww.FEEDBACK.Println("Now, start Hugo by yourself:\n" + + "$ git clone https://github.com/spf13/herring-cove.git " + args[1] + "/themes/herring-cove") + jww.FEEDBACK.Println("$ cd " + args[1] + "\n$ hugo server --theme=herring-cove") + + return nil +} + +func getJekyllDirInfo(fs afero.Fs, jekyllRoot string) (map[string]bool, bool) { + postDirs := make(map[string]bool) + hasAnyPost := false + if entries, err := ioutil.ReadDir(jekyllRoot); err == nil { + for _, entry := range entries { + if entry.IsDir() { + subDir := filepath.Join(jekyllRoot, entry.Name()) + if isPostDir, hasAnyPostInDir := retrieveJekyllPostDir(fs, subDir); isPostDir { + postDirs[entry.Name()] = hasAnyPostInDir + if hasAnyPostInDir { + hasAnyPost = true + } + } + } + } + } + return postDirs, hasAnyPost +} + +func retrieveJekyllPostDir(fs afero.Fs, dir string) (bool, bool) { + if strings.HasSuffix(dir, "_posts") || strings.HasSuffix(dir, "_drafts") { + isEmpty, _ := helpers.IsEmpty(dir, fs) + return true, !isEmpty + } + + if entries, err := ioutil.ReadDir(dir); err == nil { + for _, entry := range entries { + if entry.IsDir() { + subDir := filepath.Join(dir, entry.Name()) + if isPostDir, hasAnyPost := retrieveJekyllPostDir(fs, subDir); isPostDir { + return isPostDir, hasAnyPost + } + } + } + } + + return false, true +} + +func createSiteFromJekyll(jekyllRoot, targetDir string, jekyllPostDirs map[string]bool, force bool) (*hugolib.Site, error) { + s, err := hugolib.NewSiteDefaultLang() + if err != nil { + return nil, err + } + + fs := s.Fs.Source + if exists, _ := helpers.Exists(targetDir, fs); exists { + if isDir, _ := helpers.IsDir(targetDir, fs); !isDir { + return nil, errors.New("Target path \"" + targetDir + "\" already exists but not a directory") + } + + isEmpty, _ := helpers.IsEmpty(targetDir, fs) + + if !isEmpty && !force { + return nil, errors.New("Target path \"" + targetDir + "\" already exists and is not empty") + } + } + + jekyllConfig := loadJekyllConfig(fs, jekyllRoot) + + mkdir(targetDir, "layouts") + mkdir(targetDir, "content") + mkdir(targetDir, "archetypes") + mkdir(targetDir, "static") + mkdir(targetDir, "data") + mkdir(targetDir, "themes") + + createConfigFromJekyll(fs, targetDir, "yaml", jekyllConfig) + + copyJekyllFilesAndFolders(jekyllRoot, filepath.Join(targetDir, "static"), jekyllPostDirs) + + return s, nil +} + +func loadJekyllConfig(fs afero.Fs, jekyllRoot string) map[string]interface{} { + path := filepath.Join(jekyllRoot, "_config.yml") + + exists, err := helpers.Exists(path, fs) + + if err != nil || !exists { + jww.WARN.Println("_config.yaml not found: Is the specified Jekyll root correct?") + return nil + } + + f, err := fs.Open(path) + if err != nil { + return nil + } + + defer f.Close() + + b, err := ioutil.ReadAll(f) + + if err != nil { + return nil + } + + c, err := parser.HandleYAMLMetaData(b) + + if err != nil { + return nil + } + + return c.(map[string]interface{}) +} + +func createConfigFromJekyll(fs afero.Fs, inpath string, kind string, jekyllConfig map[string]interface{}) (err error) { + title := "My New Hugo Site" + baseURL := "http://example.org/" + + for key, value := range jekyllConfig { + lowerKey := strings.ToLower(key) + + switch lowerKey { + case "title": + if str, ok := value.(string); ok { + title = str + } + + case "url": + if str, ok := value.(string); ok { + baseURL = str + } + } + } + + in := map[string]interface{}{ + "baseURL": baseURL, + "title": title, + "languageCode": "en-us", + "disablePathToLower": true, + } + kind = parser.FormatSanitize(kind) + + var buf bytes.Buffer + err = parser.InterfaceToConfig(in, parser.FormatToLeadRune(kind), &buf) + if err != nil { + return err + } + + return helpers.WriteToDisk(filepath.Join(inpath, "config."+kind), &buf, fs) +} + +func copyFile(source string, dest string) error { + sf, err := os.Open(source) + if err != nil { + return err + } + defer sf.Close() + df, err := os.Create(dest) + if err != nil { + return err + } + defer df.Close() + _, err = io.Copy(df, sf) + if err == nil { + si, err := os.Stat(source) + if err != nil { + err = os.Chmod(dest, si.Mode()) + + if err != nil { + return err + } + } + + } + return nil +} + +func copyDir(source string, dest string) error { + fi, err := os.Stat(source) + if err != nil { + return err + } + if !fi.IsDir() { + return errors.New(source + " is not a directory") + } + err = os.MkdirAll(dest, fi.Mode()) + if err != nil { + return err + } + entries, err := ioutil.ReadDir(source) + for _, entry := range entries { + sfp := filepath.Join(source, entry.Name()) + dfp := filepath.Join(dest, entry.Name()) + if entry.IsDir() { + err = copyDir(sfp, dfp) + if err != nil { + jww.ERROR.Println(err) + } + } else { + err = copyFile(sfp, dfp) + if err != nil { + jww.ERROR.Println(err) + } + } + + } + return nil +} + +func copyJekyllFilesAndFolders(jekyllRoot, dest string, jekyllPostDirs map[string]bool) (err error) { + fi, err := os.Stat(jekyllRoot) + if err != nil { + return err + } + if !fi.IsDir() { + return errors.New(jekyllRoot + " is not a directory") + } + err = os.MkdirAll(dest, fi.Mode()) + if err != nil { + return err + } + entries, err := ioutil.ReadDir(jekyllRoot) + for _, entry := range entries { + sfp := filepath.Join(jekyllRoot, entry.Name()) + dfp := filepath.Join(dest, entry.Name()) + if entry.IsDir() { + if entry.Name()[0] != '_' && entry.Name()[0] != '.' { + if _, ok := jekyllPostDirs[entry.Name()]; !ok { + err = copyDir(sfp, dfp) + if err != nil { + jww.ERROR.Println(err) + } + } + } + } else { + lowerEntryName := strings.ToLower(entry.Name()) + exceptSuffix := []string{".md", ".markdown", ".html", ".htm", + ".xml", ".textile", "rakefile", "gemfile", ".lock"} + isExcept := false + for _, suffix := range exceptSuffix { + if strings.HasSuffix(lowerEntryName, suffix) { + isExcept = true + break + } + } + + if !isExcept && entry.Name()[0] != '.' && entry.Name()[0] != '_' { + err = copyFile(sfp, dfp) + if err != nil { + jww.ERROR.Println(err) + } + } + } + + } + return nil +} + +func parseJekyllFilename(filename string) (time.Time, string, error) { + re := regexp.MustCompile(`(\d+-\d+-\d+)-(.+)\..*`) + r := re.FindAllStringSubmatch(filename, -1) + if len(r) == 0 { + return time.Now(), "", errors.New("filename not match") + } + + postDate, err := time.Parse("2006-1-2", r[0][1]) + if err != nil { + return time.Now(), "", err + } + + postName := r[0][2] + + return postDate, postName, nil +} + +func convertJekyllPost(s *hugolib.Site, path, relPath, targetDir string, draft bool) error { + jww.TRACE.Println("Converting", path) + + filename := filepath.Base(path) + postDate, postName, err := parseJekyllFilename(filename) + if err != nil { + jww.WARN.Printf("Failed to parse filename '%s': %s. Skipping.", filename, err) + return nil + } + + jww.TRACE.Println(filename, postDate, postName) + + targetFile := filepath.Join(targetDir, relPath) + targetParentDir := filepath.Dir(targetFile) + os.MkdirAll(targetParentDir, 0777) + + contentBytes, err := ioutil.ReadFile(path) + if err != nil { + jww.ERROR.Println("Read file error:", path) + return err + } + + psr, err := parser.ReadFrom(bytes.NewReader(contentBytes)) + if err != nil { + jww.ERROR.Println("Parse file error:", path) + return err + } + + metadata, err := psr.Metadata() + if err != nil { + jww.ERROR.Println("Processing file error:", path) + return err + } + + newmetadata, err := convertJekyllMetaData(metadata, postName, postDate, draft) + if err != nil { + jww.ERROR.Println("Convert metadata error:", path) + return err + } + + jww.TRACE.Println(newmetadata) + content := convertJekyllContent(newmetadata, string(psr.Content())) + + page, err := s.NewPage(filename) + if err != nil { + jww.ERROR.Println("New page error", filename) + return err + } + + page.SetDir(targetParentDir) + page.SetSourceContent([]byte(content)) + page.SetSourceMetaData(newmetadata, parser.FormatToLeadRune("yaml")) + page.SaveSourceAs(targetFile) + + jww.TRACE.Println("Target file:", targetFile) + + return nil +} + +func convertJekyllMetaData(m interface{}, postName string, postDate time.Time, draft bool) (interface{}, error) { + url := postDate.Format("/2006/01/02/") + postName + "/" + + metadata, err := cast.ToStringMapE(m) + if err != nil { + return nil, err + } + + if draft { + metadata["draft"] = true + } + + for key, value := range metadata { + lowerKey := strings.ToLower(key) + + switch lowerKey { + case "layout": + delete(metadata, key) + case "permalink": + if str, ok := value.(string); ok { + url = str + } + delete(metadata, key) + case "category": + if str, ok := value.(string); ok { + metadata["categories"] = []string{str} + } + delete(metadata, key) + case "excerpt_separator": + if key != lowerKey { + delete(metadata, key) + metadata[lowerKey] = value + } + case "date": + if str, ok := value.(string); ok { + re := regexp.MustCompile(`(\d+):(\d+):(\d+)`) + r := re.FindAllStringSubmatch(str, -1) + if len(r) > 0 { + hour, _ := strconv.Atoi(r[0][1]) + minute, _ := strconv.Atoi(r[0][2]) + second, _ := strconv.Atoi(r[0][3]) + postDate = time.Date(postDate.Year(), postDate.Month(), postDate.Day(), hour, minute, second, 0, time.UTC) + } + } + delete(metadata, key) + } + + } + + metadata["url"] = url + metadata["date"] = postDate.Format(time.RFC3339) + + return metadata, nil +} + +func convertJekyllContent(m interface{}, content string) string { + metadata, _ := cast.ToStringMapE(m) + + lines := strings.Split(content, "\n") + var resultLines []string + for _, line := range lines { + resultLines = append(resultLines, strings.Trim(line, "\r\n")) + } + + content = strings.Join(resultLines, "\n") + + excerptSep := "" + if value, ok := metadata["excerpt_separator"]; ok { + if str, strOk := value.(string); strOk { + content = strings.Replace(content, strings.TrimSpace(str), excerptSep, -1) + } + } + + replaceList := []struct { + re *regexp.Regexp + replace string + }{ + {regexp.MustCompile("(?i)"), ""}, + {regexp.MustCompile(`\{%\s*raw\s*%\}\s*(.*?)\s*\{%\s*endraw\s*%\}`), "$1"}, + {regexp.MustCompile(`{%\s*highlight\s*(.*?)\s*%}`), "{{< highlight $1 >}}"}, + {regexp.MustCompile(`{%\s*endhighlight\s*%}`), "{{< / highlight >}}"}, + } + + for _, replace := range replaceList { + content = replace.re.ReplaceAllString(content, replace.replace) + } + + replaceListFunc := []struct { + re *regexp.Regexp + replace func(string) string + }{ + // Octopress image tag: http://octopress.org/docs/plugins/image-tag/ + {regexp.MustCompile(`{%\s+img\s*(.*?)\s*%}`), replaceImageTag}, + } + + for _, replace := range replaceListFunc { + content = replace.re.ReplaceAllStringFunc(content, replace.replace) + } + + return content +} + +func replaceImageTag(match string) string { + r := regexp.MustCompile(`{%\s+img\s*(\p{L}*)\s+([\S]*/[\S]+)\s+(\d*)\s*(\d*)\s*(.*?)\s*%}`) + result := bytes.NewBufferString("{{< figure ") + parts := r.FindStringSubmatch(match) + // Index 0 is the entire string, ignore + replaceOptionalPart(result, "class", parts[1]) + replaceOptionalPart(result, "src", parts[2]) + replaceOptionalPart(result, "width", parts[3]) + replaceOptionalPart(result, "height", parts[4]) + // title + alt + part := parts[5] + if len(part) > 0 { + splits := strings.Split(part, "'") + lenSplits := len(splits) + if lenSplits == 1 { + replaceOptionalPart(result, "title", splits[0]) + } else if lenSplits == 3 { + replaceOptionalPart(result, "title", splits[1]) + } else if lenSplits == 5 { + replaceOptionalPart(result, "title", splits[1]) + replaceOptionalPart(result, "alt", splits[3]) + } + } + result.WriteString(">}}") + return result.String() + +} +func replaceOptionalPart(buffer *bytes.Buffer, partName string, part string) { + if len(part) > 0 { + buffer.WriteString(partName + "=\"" + part + "\" ") + } +} diff --git a/commands/import_jekyll_test.go b/commands/import_jekyll_test.go new file mode 100644 index 000000000..90a05c01c --- /dev/null +++ b/commands/import_jekyll_test.go @@ -0,0 +1,126 @@ +// Copyright 2015 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 ( + "encoding/json" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestParseJekyllFilename(t *testing.T) { + filenameArray := []string{ + "2015-01-02-test.md", + "2012-03-15-中文.markup", + } + + expectResult := []struct { + postDate time.Time + postName string + }{ + {time.Date(2015, time.January, 2, 0, 0, 0, 0, time.UTC), "test"}, + {time.Date(2012, time.March, 15, 0, 0, 0, 0, time.UTC), "中文"}, + } + + for i, filename := range filenameArray { + postDate, postName, err := parseJekyllFilename(filename) + assert.Equal(t, err, nil) + assert.Equal(t, expectResult[i].postDate.Format("2006-01-02"), postDate.Format("2006-01-02")) + assert.Equal(t, expectResult[i].postName, postName) + } +} + +func TestConvertJekyllMetadata(t *testing.T) { + testDataList := []struct { + metadata interface{} + postName string + postDate time.Time + draft bool + expect string + }{ + {map[interface{}]interface{}{}, "testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false, + `{"date":"2015-10-01T00:00:00Z","url":"/2015/10/01/testPost/"}`}, + {map[interface{}]interface{}{}, "testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), true, + `{"date":"2015-10-01T00:00:00Z","draft":true,"url":"/2015/10/01/testPost/"}`}, + {map[interface{}]interface{}{"Permalink": "/permalink.html", "layout": "post"}, + "testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false, + `{"date":"2015-10-01T00:00:00Z","url":"/permalink.html"}`}, + {map[interface{}]interface{}{"permalink": "/permalink.html"}, + "testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false, + `{"date":"2015-10-01T00:00:00Z","url":"/permalink.html"}`}, + {map[interface{}]interface{}{"category": nil, "permalink": 123}, + "testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false, + `{"date":"2015-10-01T00:00:00Z","url":"/2015/10/01/testPost/"}`}, + {map[interface{}]interface{}{"Excerpt_Separator": "sep"}, + "testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false, + `{"date":"2015-10-01T00:00:00Z","excerpt_separator":"sep","url":"/2015/10/01/testPost/"}`}, + {map[interface{}]interface{}{"category": "book", "layout": "post", "Others": "Goods", "Date": "2015-10-01 12:13:11"}, + "testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false, + `{"Others":"Goods","categories":["book"],"date":"2015-10-01T12:13:11Z","url":"/2015/10/01/testPost/"}`}, + } + + for _, data := range testDataList { + result, err := convertJekyllMetaData(data.metadata, data.postName, data.postDate, data.draft) + assert.Equal(t, nil, err) + jsonResult, err := json.Marshal(result) + assert.Equal(t, nil, err) + assert.Equal(t, data.expect, string(jsonResult)) + } +} + +func TestConvertJekyllContent(t *testing.T) { + testDataList := []struct { + metadata interface{} + content string + expect string + }{ + {map[interface{}]interface{}{}, + `Test content\n\npart2 content`, `Test content\n\npart2 content`}, + {map[interface{}]interface{}{}, + `Test content\n\npart2 content`, `Test content\n\npart2 content`}, + {map[interface{}]interface{}{"excerpt_separator": ""}, + `Test content\n\npart2 content`, `Test content\n\npart2 content`}, + {map[interface{}]interface{}{}, "{% raw %}text{% endraw %}", "text"}, + {map[interface{}]interface{}{}, "{%raw%} text2 {%endraw %}", "text2"}, + {map[interface{}]interface{}{}, + "{% highlight go %}\nvar s int\n{% endhighlight %}", + "{{< highlight go >}}\nvar s int\n{{< / highlight >}}"}, + + // Octopress image tag + {map[interface{}]interface{}{}, + "{% img http://placekitten.com/890/280 %}", + "{{< figure src=\"http://placekitten.com/890/280\" >}}"}, + {map[interface{}]interface{}{}, + "{% img left http://placekitten.com/320/250 Place Kitten #2 %}", + "{{< figure class=\"left\" src=\"http://placekitten.com/320/250\" title=\"Place Kitten #2\" >}}"}, + {map[interface{}]interface{}{}, + "{% img right http://placekitten.com/300/500 150 250 'Place Kitten #3' %}", + "{{< figure class=\"right\" src=\"http://placekitten.com/300/500\" width=\"150\" height=\"250\" title=\"Place Kitten #3\" >}}"}, + {map[interface{}]interface{}{}, + "{% img right http://placekitten.com/300/500 150 250 'Place Kitten #4' 'An image of a very cute kitten' %}", + "{{< figure class=\"right\" src=\"http://placekitten.com/300/500\" width=\"150\" height=\"250\" title=\"Place Kitten #4\" alt=\"An image of a very cute kitten\" >}}"}, + {map[interface{}]interface{}{}, + "{% img http://placekitten.com/300/500 150 250 'Place Kitten #4' 'An image of a very cute kitten' %}", + "{{< figure src=\"http://placekitten.com/300/500\" width=\"150\" height=\"250\" title=\"Place Kitten #4\" alt=\"An image of a very cute kitten\" >}}"}, + {map[interface{}]interface{}{}, + "{% img right /placekitten/300/500 'Place Kitten #4' 'An image of a very cute kitten' %}", + "{{< figure class=\"right\" src=\"/placekitten/300/500\" title=\"Place Kitten #4\" alt=\"An image of a very cute kitten\" >}}"}, + } + + for _, data := range testDataList { + result := convertJekyllContent(data.metadata, data.content) + assert.Equal(t, data.expect, result) + } +} diff --git a/commands/limit_darwin.go b/commands/limit_darwin.go new file mode 100644 index 000000000..9246f4497 --- /dev/null +++ b/commands/limit_darwin.go @@ -0,0 +1,85 @@ +// Copyright 2015 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. + +// Copyright 2015 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 ( + "syscall" + + "github.com/spf13/cobra" + jww "github.com/spf13/jwalterweatherman" +) + +func init() { + checkCmd.AddCommand(limit) +} + +var limit = &cobra.Command{ + Use: "ulimit", + Short: "Check system ulimit settings", + Long: `Hugo will inspect the current ulimit settings on the system. +This is primarily to ensure that Hugo can watch enough files on some OSs`, + RunE: func(cmd *cobra.Command, args []string) error { + var rLimit syscall.Rlimit + err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit) + if err != nil { + return newSystemError("Error Getting Rlimit ", err) + } + + jww.FEEDBACK.Println("Current rLimit:", rLimit) + + jww.FEEDBACK.Println("Attempting to increase limit") + rLimit.Max = 999999 + rLimit.Cur = 999999 + err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit) + if err != nil { + return newSystemError("Error Setting rLimit ", err) + } + err = syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit) + if err != nil { + return newSystemError("Error Getting rLimit ", err) + } + jww.FEEDBACK.Println("rLimit after change:", rLimit) + + return nil + }, +} + +func tweakLimit() { + var rLimit syscall.Rlimit + err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit) + if err != nil { + jww.ERROR.Println("Unable to obtain rLimit", err) + } + if rLimit.Cur < rLimit.Max { + rLimit.Max = 64000 + rLimit.Cur = 64000 + err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit) + if err != nil { + jww.WARN.Println("Unable to increase number of open files limit", err) + } + } +} diff --git a/commands/limit_others.go b/commands/limit_others.go new file mode 100644 index 000000000..c757f174e --- /dev/null +++ b/commands/limit_others.go @@ -0,0 +1,32 @@ +// Copyright 2015 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. + +// +build !darwin +// Copyright 2015 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 + +func tweakLimit() { + // nothing to do +} diff --git a/commands/list.go b/commands/list.go index 42f3408ba..b2a6b5395 100644 --- a/commands/list.go +++ b/commands/list.go @@ -1,4 +1,4 @@ -// Copyright 2024 The Hugo Authors. All rights reserved. +// Copyright 2015 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,200 +14,148 @@ package commands import ( - "context" - "encoding/csv" - "os" "path/filepath" - "strconv" - "strings" - "time" - "github.com/bep/simplecobra" "github.com/gohugoio/hugo/hugolib" - "github.com/gohugoio/hugo/resources/page" - "github.com/gohugoio/hugo/resources/resource" "github.com/spf13/cobra" + jww "github.com/spf13/jwalterweatherman" ) -// 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))), - p.Slug(), - p.Title(), - p.Date().Format(time.RFC3339), - p.ExpiryDate().Format(time.RFC3339), - p.PublishDate().Format(time.RFC3339), - strconv.FormatBool(p.Draft()), - p.Permalink(), - p.Kind(), - p.Section(), - } - } +func init() { + listCmd.AddCommand(listDraftsCmd) + listCmd.AddCommand(listFutureCmd) + listCmd.AddCommand(listExpiredCmd) + listCmd.PersistentFlags().StringVarP(&source, "source", "s", "", "filesystem path to read files relative from") + listCmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{}) +} - list := func(cd *simplecobra.Commandeer, r *rootCommand, shouldInclude func(page.Page) bool, opts ...any) error { - bcfg := hugolib.BuildCfg{SkipRender: true} - cfg := flagsToCfg(cd, nil) - for i := 0; i < len(opts); i += 2 { - cfg.Set(opts[i].(string), opts[i+1]) - } - h, err := r.Build(cd, bcfg, cfg) +var listCmd = &cobra.Command{ + Use: "list", + Short: "Listing out various types of content", + Long: `Listing out various types of content. + +List requires a subcommand, e.g. ` + "`hugo list drafts`.", + RunE: nil, +} + +var listDraftsCmd = &cobra.Command{ + Use: "drafts", + Short: "List all drafts", + Long: `List all of the drafts in your content directory.`, + RunE: func(cmd *cobra.Command, args []string) error { + + cfg, err := InitializeConfig() if err != nil { return err } - writer := csv.NewWriter(r.StdOut) - defer writer.Flush() + c, err := newCommandeer(cfg) + if err != nil { + return err + } - writer.Write([]string{ - "path", - "slug", - "title", - "date", - "expiryDate", - "publishDate", - "draft", - "permalink", - "kind", - "section", - }) + c.Set("buildDrafts", true) - for _, p := range h.Pages() { - if shouldInclude(p) { - record := createRecord(h.Conf.BaseConfig().WorkingDir, p) - if err := writer.Write(record); err != nil { - return err - } + sites, err := hugolib.NewHugoSites(*cfg) + + if err != nil { + return newSystemError("Error creating sites", err) + } + + if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil { + return newSystemError("Error Processing Source Content", err) + } + + for _, p := range sites.Pages() { + if p.IsDraft() { + jww.FEEDBACK.Println(filepath.Join(p.File.Dir(), p.File.LogicalName())) } + } return nil - } - return &listCommand{ - commands: []simplecobra.Commander{ - &simpleCommand{ - name: "drafts", - 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() == nil { - return false - } - return true - } - return list(cd, r, shouldInclude, - "buildDrafts", true, - "buildFuture", true, - "buildExpired", true, - ) - }, - withc: func(cmd *cobra.Command, r *rootCommand) { - cmd.ValidArgsFunction = cobra.NoFileCompletions - }, - }, - &simpleCommand{ - name: "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() == 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 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() == nil { - return false - } - return true - } - return list(cd, r, shouldInclude, - "buildExpired", true, - "buildDrafts", true, - ) - }, - withc: func(cmd *cobra.Command, r *rootCommand) { - cmd.ValidArgsFunction = cobra.NoFileCompletions - }, - }, - &simpleCommand{ - name: "all", - 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() != 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 { - commands []simplecobra.Commander +var listFutureCmd = &cobra.Command{ + Use: "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.`, + RunE: func(cmd *cobra.Command, args []string) error { + + cfg, err := InitializeConfig() + if err != nil { + return err + } + + c, err := newCommandeer(cfg) + if err != nil { + return err + } + + c.Set("buildFuture", true) + + sites, err := hugolib.NewHugoSites(*cfg) + + if err != nil { + return newSystemError("Error creating sites", err) + } + + if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil { + return newSystemError("Error Processing Source Content", err) + } + + for _, p := range sites.Pages() { + if p.IsFuture() { + jww.FEEDBACK.Println(filepath.Join(p.File.Dir(), p.File.LogicalName())) + } + + } + + return nil + + }, } -func (c *listCommand) Commands() []simplecobra.Commander { - return c.commands -} - -func (c *listCommand) Name() string { - return "list" -} - -func (c *listCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error { - // Do nothing. - return nil -} - -func (c *listCommand) Init(cd *simplecobra.Commandeer) error { - cmd := cd.CobraCommand - cmd.Short = "List content" - cmd.Long = `List content. - -List requires a subcommand, e.g. hugo list drafts` - - cmd.RunE = nil - return nil -} - -func (c *listCommand) PreRun(cd, runner *simplecobra.Commandeer) error { - return nil +var listExpiredCmd = &cobra.Command{ + Use: "expired", + Short: "List all posts already expired", + Long: `List all of the posts in your content directory which has already +expired.`, + RunE: func(cmd *cobra.Command, args []string) error { + + cfg, err := InitializeConfig() + if err != nil { + return err + } + + c, err := newCommandeer(cfg) + if err != nil { + return err + } + + c.Set("buildExpired", true) + + sites, err := hugolib.NewHugoSites(*cfg) + + if err != nil { + return newSystemError("Error creating sites", err) + } + + if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil { + return newSystemError("Error Processing Source Content", err) + } + + for _, p := range sites.Pages() { + if p.IsExpired() { + jww.FEEDBACK.Println(filepath.Join(p.File.Dir(), p.File.LogicalName())) + } + + } + + return nil + + }, } diff --git a/commands/list_config.go b/commands/list_config.go new file mode 100644 index 000000000..f47f6e144 --- /dev/null +++ b/commands/list_config.go @@ -0,0 +1,66 @@ +// Copyright 2015 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.Print the version number of Hug + +package commands + +import ( + "reflect" + "sort" + + "github.com/spf13/cobra" + jww "github.com/spf13/jwalterweatherman" + "github.com/spf13/viper" +) + +var configCmd = &cobra.Command{ + Use: "config", + Short: "Print the site configuration", + Long: `Print the site configuration, both default and custom settings.`, +} + +func init() { + configCmd.RunE = printConfig +} + +func printConfig(cmd *cobra.Command, args []string) error { + cfg, err := InitializeConfig(configCmd) + + if err != nil { + return err + } + + allSettings := cfg.Cfg.(*viper.Viper).AllSettings() + + var separator string + if allSettings["metadataformat"] == "toml" { + separator = " = " + } else { + separator = ": " + } + + var keys []string + for k := range allSettings { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + kv := reflect.ValueOf(allSettings[k]) + if kv.Kind() == reflect.String { + jww.FEEDBACK.Printf("%s%s\"%+v\"\n", k, separator, allSettings[k]) + } else { + jww.FEEDBACK.Printf("%s%s%+v\n", k, separator, allSettings[k]) + } + } + + return nil +} diff --git a/commands/mod.go b/commands/mod.go deleted file mode 100644 index 58155f9be..000000000 --- a/commands/mod.go +++ /dev/null @@ -1,344 +0,0 @@ -// 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 ( - "context" - "errors" - "os" - "path/filepath" - - "github.com/bep/simplecobra" - "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/modules/npm" - "github.com/spf13/cobra" -) - -const commonUsageMod = ` -Note that Hugo will always start out by resolving the components defined in the site -configuration, provided by a _vendor directory (if no --ignoreVendorPaths flag provided), -Go Modules, or a folder inside the themes directory, in that order. - -See https://gohugo.io/hugo-modules/ for more information. - -` - -// buildConfigCommands creates a new config command and its subcommands. -func newModCommands() *modCommands { - var ( - clean bool - pattern string - all bool - ) - - npmCommand := &simpleCommand{ - name: "npm", - 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", - 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. - -This set will be merged with all "package.hugo.json" files found in the dependency tree, picking the version closest to the project. - -This command is marked as 'Experimental'. We think it's a great idea, so it's not likely to be -removed from Hugo, but we need to test this out in "real life" to get a feel of it, -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 { - h, err := r.Hugo(flagsToCfg(cd, nil)) - if err != nil { - return err - } - return npm.Pack(h.BaseFs.ProjectSourceFs, h.BaseFs.AssetsWithDuplicatesPreserved.Fs) - }, - }, - }, - } - - return &modCommands{ - commands: []simplecobra.Commander{ - &simpleCommand{ - name: "init", - 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.getOrCreateHugo(flagsToCfg(cd, nil), true) - if err != nil { - return err - } - var initPath string - if len(args) >= 1 { - initPath = args[0] - } - c := h.Configs.ModulesClient - if err := c.Init(initPath); err != nil { - return err - } - return nil - }, - }, - &simpleCommand{ - name: "verify", - 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(configKey{counter: r.configVersionID.Load()}, flagsToCfg(cd, nil)) - if err != nil { - return err - } - client := conf.configs.ModulesClient - return client.Verify(clean) - }, - }, - &simpleCommand{ - name: "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(configKey{counter: r.configVersionID.Load()}, flagsToCfg(cd, nil)) - if err != nil { - return err - } - client := conf.configs.ModulesClient - return client.Graph(os.Stdout) - }, - }, - &simpleCommand{ - name: "clean", - 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 { - h, err := r.Hugo(flagsToCfg(cd, nil)) - if err != nil { - return err - } - if all { - modCache := h.ResourceSpec.FileCaches.ModulesCache() - count, err := modCache.Prune(true) - r.Printf("Deleted %d files from module cache.", count) - return err - } - - return h.Configs.ModulesClient.Clean(pattern) - }, - }, - &simpleCommand{ - name: "tidy", - 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 { - h, err := r.Hugo(flagsToCfg(cd, nil)) - if err != nil { - return err - } - return h.Configs.ModulesClient.Tidy() - }, - }, - &simpleCommand{ - name: "vendor", - 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 { - h, err := r.Hugo(flagsToCfg(cd, nil)) - if err != nil { - return err - } - return h.Configs.ModulesClient.Vendor() - }, - }, - - &simpleCommand{ - name: "get", - short: "Resolves dependencies in your current Hugo project", - long: ` -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 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) - -Run "go help get" for more information. All flags available for "go get" is also relevant here. -` + 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" || args[0] == "--help") { - return errHelp - } - - var lastArg string - if len(args) != 0 { - lastArg = args[len(args)-1] - } - - if lastArg == "./..." { - args = args[:len(args)-1] - // Do a recursive update. - dirname, err := os.Getwd() - if err != nil { - return err - } - - // Sanity chesimplecobra. We do recursive walking and want to avoid - // accidents. - if len(dirname) < 5 { - return errors.New("must not be run from the file system root") - } - - filepath.Walk(dirname, func(path string, info os.FileInfo, err error) error { - if info.IsDir() { - return nil - } - if info.Name() == "go.mod" { - // Found a module. - dir := filepath.Dir(path) - - cfg := config.New() - cfg.Set("workingDir", dir) - 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...) - - } - return nil - }) - return nil - } else { - conf, err := r.ConfigFromProvider(configKey{counter: r.configVersionID.Load()}, flagsToCfg(cd, nil)) - if err != nil { - return err - } - client := conf.configs.ModulesClient - return client.Get(args...) - } - }, - }, - npmCommand, - }, - } -} - -type modCommands struct { - r *rootCommand - - commands []simplecobra.Commander -} - -func (c *modCommands) Commands() []simplecobra.Commander { - return c.commands -} - -func (c *modCommands) Name() string { - return "mod" -} - -func (c *modCommands) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error { - _, err := c.r.ConfigFromProvider(configKey{counter: c.r.configVersionID.Load()}, nil) - if err != nil { - return err - } - // config := conf.configs.Base - - return nil -} - -func (c *modCommands) Init(cd *simplecobra.Commandeer) error { - cmd := cd.CobraCommand - 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". - -` + commonUsageMod - cmd.RunE = nil - return nil -} - -func (c *modCommands) PreRun(cd, runner *simplecobra.Commandeer) error { - c.r = cd.Root.Command.(*rootCommand) - return nil -} diff --git a/commands/new.go b/commands/new.go index 81e1c65a4..5ed1fe8e0 100644 --- a/commands/new.go +++ b/commands/new.go @@ -1,4 +1,4 @@ -// Copyright 2024 The Hugo Authors. All rights reserved. +// Copyright 2016 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,213 +15,385 @@ package commands import ( "bytes" - "context" + "errors" + "fmt" + "os" "path/filepath" "strings" + "time" - "github.com/bep/simplecobra" - "github.com/gohugoio/hugo/common/paths" - "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/create" - "github.com/gohugoio/hugo/create/skeletons" + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugofs" + "github.com/gohugoio/hugo/hugolib" + "github.com/gohugoio/hugo/parser" + "github.com/spf13/afero" "github.com/spf13/cobra" + jww "github.com/spf13/jwalterweatherman" + "github.com/spf13/viper" ) -func newNewCommand() *newCommand { - var ( - force bool - contentType string - format string - ) +var ( + configFormat string + contentEditor string + contentType string +) - var c *newCommand - c = &newCommand{ - commands: []simplecobra.Commander{ - &simpleCommand{ - name: "content", - use: "content [path]", - short: "Create new content", - long: `Create a new content file and automatically set the date and title. +func init() { + newSiteCmd.Flags().StringVarP(&configFormat, "format", "f", "toml", "config & frontmatter format") + newSiteCmd.Flags().Bool("force", false, "init inside non-empty directory") + newCmd.Flags().StringVarP(&contentType, "kind", "k", "", "content type to create") + newCmd.PersistentFlags().StringVarP(&source, "source", "s", "", "filesystem path to read files relative from") + newCmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{}) + newCmd.Flags().StringVar(&contentEditor, "editor", "", "edit new content with this editor, if provided") + + newCmd.AddCommand(newSiteCmd) + newCmd.AddCommand(newThemeCmd) + +} + +var newCmd = &cobra.Command{ + Use: "new [path]", + Short: "Create new content for your site", + Long: `Create a new content file and automatically set the date and title. It will guess which kind of file to create based on the path provided. You can also specify the kind with ` + "`-k KIND`" + `. -If archetypes are provided in your theme or site, they will be used. +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 newUserError("path needs to be provided") - } - h, err := r.Hugo(flagsToCfg(cd, nil)) - if err != nil { - return err - } - 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") - applyLocalFlagsBuildConfig(cmd, r) - }, - }, - &simpleCommand{ - name: "site", - use: "site [path]", - 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 newUserError("path needs to be provided") - } - createpath, err := filepath.Abs(filepath.Clean(args[0])) - if err != nil { - return err - } + RunE: NewContent, +} - cfg := config.New() - cfg.Set("workingDir", createpath) - cfg.Set("publishDir", "public") +var newSiteCmd = &cobra.Command{ + 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.`, + RunE: NewSite, +} - conf, err := r.ConfigFromProvider(configKey{counter: r.configVersionID.Load()}, flagsToCfg(cd, cfg)) - if err != nil { - return err - } - sourceFs := conf.fs.Source +var newThemeCmd = &cobra.Command{ + Use: "theme [name]", + Short: "Create a new theme", + Long: `Create a new theme (skeleton) called [name] in the current directory. +New theme is a skeleton. Please add content to the touched files. Add your +name to the copyright line in the license and adjust the theme.toml file +as you see fit.`, + RunE: NewTheme, +} - err = skeletons.CreateSite(createpath, sourceFs, force, format) - if err != nil { - return err - } +// NewContent adds new content to a Hugo site. +func NewContent(cmd *cobra.Command, args []string) error { + cfg, err := InitializeConfig() - 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 [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 { - 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 - } - 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) - - err = skeletons.CreateTheme(createpath, sourceFs, format) - if err != nil { - return err - } - - 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)) - }, - }, - }, + if err != nil { + return err } - return c + c, err := newCommandeer(cfg) + if err != nil { + return err + } + + if cmd.Flags().Changed("editor") { + c.Set("newContentEditor", contentEditor) + } + + if len(args) < 1 { + return newUserError("path needs to be provided") + } + + createPath := args[0] + + var kind string + + createPath, kind = newContentPathSection(createPath) + + if contentType != "" { + kind = contentType + } + + ps, err := helpers.NewPathSpec(cfg.Fs, cfg.Cfg) + if err != nil { + return err + } + + // If a site isn't in use in the archetype template, we can skip the build. + siteFactory := func(filename string, siteUsed bool) (*hugolib.Site, error) { + if !siteUsed { + return hugolib.NewSite(*cfg) + } + var s *hugolib.Site + if err := c.initSites(); err != nil { + return nil, err + } + + if err := Hugo.Build(hugolib.BuildCfg{SkipRender: true, PrintStats: false}); err != nil { + return nil, err + } + + s = Hugo.Sites[0] + + if len(Hugo.Sites) > 1 { + // Find the best match. + for _, ss := range Hugo.Sites { + if strings.Contains(createPath, "."+ss.Language.Lang) { + s = ss + break + } + } + } + return s, nil + } + + return create.NewContent(ps, siteFactory, kind, createPath) } -type newCommand struct { - rootCmd *rootCommand +func doNewSite(fs *hugofs.Fs, basepath string, force bool) error { + archeTypePath := filepath.Join(basepath, "archetypes") + dirs := []string{ + filepath.Join(basepath, "layouts"), + filepath.Join(basepath, "content"), + archeTypePath, + filepath.Join(basepath, "static"), + filepath.Join(basepath, "data"), + filepath.Join(basepath, "themes"), + } - commands []simplecobra.Commander -} + if exists, _ := helpers.Exists(basepath, fs.Source); exists { + if isDir, _ := helpers.IsDir(basepath, fs.Source); !isDir { + return errors.New(basepath + " already exists but not a directory") + } -func (c *newCommand) Commands() []simplecobra.Commander { - return c.commands -} + isEmpty, _ := helpers.IsEmpty(basepath, fs.Source) -func (c *newCommand) Name() string { - return "new" -} + switch { + case !isEmpty && !force: + return errors.New(basepath + " already exists and is not empty") + + case !isEmpty && force: + all := append(dirs, filepath.Join(basepath, "config."+configFormat)) + for _, path := range all { + if exists, _ := helpers.Exists(path, fs.Source); exists { + return errors.New(path + " already exists") + } + } + } + } + + for _, dir := range dirs { + if err := fs.Source.MkdirAll(dir, 0777); err != nil { + return fmt.Errorf("Failed to create dir: %s", err) + } + } + + createConfig(fs, basepath, configFormat) + + // Create a defaul archetype file. + helpers.SafeWriteToDisk(filepath.Join(archeTypePath, "default.md"), + strings.NewReader(create.ArchetypeTemplateTemplate), fs.Source) + + jww.FEEDBACK.Printf("Congratulations! Your new Hugo site is created in %s.\n\n", basepath) + jww.FEEDBACK.Println(nextStepsText()) -func (c *newCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error { return nil } -func (c *newCommand) Init(cd *simplecobra.Commandeer) error { - cmd := cd.CobraCommand - 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. - -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 -} - -func (c *newCommand) PreRun(cd, runner *simplecobra.Commandeer) error { - c.rootCmd = cd.Root.Command.(*rootCommand) - return nil -} - -func (c *newCommand) newSiteNextStepsText(path string, format string) string { - format = strings.ToLower(format) +func nextStepsText() string { var nextStepsText bytes.Buffer - nextStepsText.WriteString(`Just a few more steps... + nextStepsText.WriteString(`Just a few more steps and you're ready to go: -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 `) +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 `) nextStepsText.WriteString(filepath.Join("", ".")) nextStepsText.WriteString(`". -5. Start the embedded web server with the command "hugo server --buildDrafts". +3. Start the built-in live server via "hugo server". -See documentation at https://gohugo.io/.`) +Visit https://gohugo.io/ for quickstart guide and full documentation.`) return nextStepsText.String() } + +// NewSite creates a new Hugo site and initializes a structured Hugo directory. +func NewSite(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return newUserError("path needs to be provided") + } + + createpath, err := filepath.Abs(filepath.Clean(args[0])) + if err != nil { + return newUserError(err) + } + + forceNew, _ := cmd.Flags().GetBool("force") + + return doNewSite(hugofs.NewDefault(viper.New()), createpath, forceNew) +} + +// NewTheme creates a new Hugo theme. +func NewTheme(cmd *cobra.Command, args []string) error { + cfg, err := InitializeConfig() + + if err != nil { + return err + } + + if len(args) < 1 { + return newUserError("theme name needs to be provided") + } + + c, err := newCommandeer(cfg) + if err != nil { + return err + } + + createpath := c.PathSpec().AbsPathify(filepath.Join(c.Cfg.GetString("themesDir"), args[0])) + jww.INFO.Println("creating theme at", createpath) + + if x, _ := helpers.Exists(createpath, cfg.Fs.Source); x { + return newUserError(createpath, "already exists") + } + + mkdir(createpath, "layouts", "_default") + mkdir(createpath, "layouts", "partials") + + touchFile(cfg.Fs.Source, createpath, "layouts", "index.html") + touchFile(cfg.Fs.Source, createpath, "layouts", "404.html") + touchFile(cfg.Fs.Source, createpath, "layouts", "_default", "list.html") + touchFile(cfg.Fs.Source, createpath, "layouts", "_default", "single.html") + + touchFile(cfg.Fs.Source, createpath, "layouts", "partials", "header.html") + touchFile(cfg.Fs.Source, createpath, "layouts", "partials", "footer.html") + + mkdir(createpath, "archetypes") + + archDefault := []byte("+++\n+++\n") + + err = helpers.WriteToDisk(filepath.Join(createpath, "archetypes", "default.md"), bytes.NewReader(archDefault), cfg.Fs.Source) + if err != nil { + return err + } + + mkdir(createpath, "static", "js") + mkdir(createpath, "static", "css") + + by := []byte(`The MIT License (MIT) + +Copyright (c) ` + time.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.md"), bytes.NewReader(by), cfg.Fs.Source) + if err != nil { + return err + } + + createThemeMD(cfg.Fs, createpath) + + return nil +} + +func mkdir(x ...string) { + p := filepath.Join(x...) + + err := os.MkdirAll(p, 0777) // before umask + if err != nil { + jww.FATAL.Fatalln(err) + } +} + +func touchFile(fs afero.Fs, x ...string) { + inpath := filepath.Join(x...) + mkdir(filepath.Dir(inpath)) + err := helpers.WriteToDisk(inpath, bytes.NewReader([]byte{}), fs) + if err != nil { + jww.FATAL.Fatalln(err) + } +} + +func createThemeMD(fs *hugofs.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.md" +description = "" +homepage = "http://example.com/" +tags = [] +features = [] +min_version = "0.25" + +[author] + name = "" + homepage = "" + +# If porting an existing theme +[original] + name = "" + homepage = "" + repo = "" +`) + + err = helpers.WriteToDisk(filepath.Join(inpath, "theme.toml"), bytes.NewReader(by), fs.Source) + if err != nil { + return + } + + return nil +} + +func newContentPathSection(path string) (string, string) { + // Forward slashes is used in all examples. Convert if needed. + // Issue #1133 + createpath := filepath.FromSlash(path) + var section string + // assume the first directory is the section (kind) + if strings.Contains(createpath[1:], helpers.FilePathSeparator) { + section = helpers.GuessSection(createpath) + } + + return createpath, section +} + +func createConfig(fs *hugofs.Fs, inpath string, kind string) (err error) { + in := map[string]string{ + "baseURL": "http://example.org/", + "title": "My New Hugo Site", + "languageCode": "en-us", + } + kind = parser.FormatSanitize(kind) + + var buf bytes.Buffer + err = parser.InterfaceToConfig(in, parser.FormatToLeadRune(kind), &buf) + if err != nil { + return err + } + + return helpers.WriteToDisk(filepath.Join(inpath, "config."+kind), &buf, fs.Source) +} diff --git a/commands/new_test.go b/commands/new_test.go new file mode 100644 index 000000000..c9adb83d4 --- /dev/null +++ b/commands/new_test.go @@ -0,0 +1,122 @@ +// Copyright 2016 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 ( + "path/filepath" + "testing" + + "github.com/gohugoio/hugo/hugofs" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Issue #1133 +func TestNewContentPathSectionWithForwardSlashes(t *testing.T) { + p, s := newContentPathSection("/post/new.md") + assert.Equal(t, filepath.FromSlash("/post/new.md"), p) + assert.Equal(t, "post", s) +} + +func checkNewSiteInited(fs *hugofs.Fs, basepath string, t *testing.T) { + + paths := []string{ + filepath.Join(basepath, "layouts"), + filepath.Join(basepath, "content"), + filepath.Join(basepath, "archetypes"), + filepath.Join(basepath, "static"), + filepath.Join(basepath, "data"), + filepath.Join(basepath, "config.toml"), + } + + for _, path := range paths { + _, err := fs.Source.Stat(path) + require.NoError(t, err) + } +} + +func TestDoNewSite(t *testing.T) { + basepath := filepath.Join("base", "blog") + _, fs := newTestCfg() + + require.NoError(t, doNewSite(fs, basepath, false)) + + checkNewSiteInited(fs, basepath, t) +} + +func TestDoNewSite_noerror_base_exists_but_empty(t *testing.T) { + basepath := filepath.Join("base", "blog") + _, fs := newTestCfg() + + require.NoError(t, fs.Source.MkdirAll(basepath, 777)) + + require.NoError(t, doNewSite(fs, basepath, false)) +} + +func TestDoNewSite_error_base_exists(t *testing.T) { + basepath := filepath.Join("base", "blog") + _, fs := newTestCfg() + + require.NoError(t, fs.Source.MkdirAll(basepath, 777)) + _, err := fs.Source.Create(filepath.Join(basepath, "foo")) + require.NoError(t, err) + // Since the directory already exists and isn't empty, expect an error + require.Error(t, doNewSite(fs, basepath, false)) + +} + +func TestDoNewSite_force_empty_dir(t *testing.T) { + basepath := filepath.Join("base", "blog") + _, fs := newTestCfg() + + require.NoError(t, fs.Source.MkdirAll(basepath, 777)) + + require.NoError(t, doNewSite(fs, basepath, true)) + + checkNewSiteInited(fs, basepath, t) +} + +func TestDoNewSite_error_force_dir_inside_exists(t *testing.T) { + basepath := filepath.Join("base", "blog") + _, fs := newTestCfg() + + contentPath := filepath.Join(basepath, "content") + + require.NoError(t, fs.Source.MkdirAll(contentPath, 777)) + require.Error(t, doNewSite(fs, basepath, true)) +} + +func TestDoNewSite_error_force_config_inside_exists(t *testing.T) { + basepath := filepath.Join("base", "blog") + _, fs := newTestCfg() + + configPath := filepath.Join(basepath, "config.toml") + require.NoError(t, fs.Source.MkdirAll(basepath, 777)) + _, err := fs.Source.Create(configPath) + require.NoError(t, err) + + require.Error(t, doNewSite(fs, basepath, true)) +} + +func newTestCfg() (*viper.Viper, *hugofs.Fs) { + + v := viper.New() + fs := hugofs.NewMem(v) + + v.SetFs(fs.Source) + + return v, fs + +} diff --git a/commands/release.go b/commands/release.go index 059f04eb8..0764685f0 100644 --- a/commands/release.go +++ b/commands/release.go @@ -1,4 +1,6 @@ -// Copyright 2024 The Hugo Authors. All rights reserved. +// +build release + +// Copyright 2017-present 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,40 +16,53 @@ package commands import ( - "context" + "errors" - "github.com/bep/simplecobra" "github.com/gohugoio/hugo/releaser" "github.com/spf13/cobra" ) -// 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 - try bool - ) +func init() { + HugoCmd.AddCommand(createReleaser().cmd) +} - return &simpleCommand{ - name: "release", - 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 { - return err - } +type releaseCommandeer struct { + cmd *cobra.Command - return rel.Run() - }, - 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)) + version string + + skipPublish bool + try bool + + step int +} + +func createReleaser() *releaseCommandeer { + // 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. + r := &releaseCommandeer{ + cmd: &cobra.Command{ + Use: "release", + Short: "Release a new version of Hugo.", + Hidden: true, }, } + + r.cmd.RunE = func(cmd *cobra.Command, args []string) error { + return r.release() + } + + r.cmd.PersistentFlags().StringVarP(&r.version, "rel", "r", "", "new release version, i.e. 0.25.1") + r.cmd.PersistentFlags().IntVarP(&r.step, "step", "s", -1, "release step, defaults to -1 for all steps.") + r.cmd.PersistentFlags().BoolVarP(&r.skipPublish, "skip-publish", "", false, "skip all publishing pipes of the release") + r.cmd.PersistentFlags().BoolVarP(&r.try, "try", "", false, "simulate a release, i.e. no changes") + + return r +} + +func (r *releaseCommandeer) release() error { + if r.version == "" { + return errors.New("must set the --rel flag to the relevant version number") + } + return releaser.New(r.version, r.step, r.skipPublish, r.try).Run() } diff --git a/commands/server.go b/commands/server.go index c8895b9a1..89d5c239e 100644 --- a/commands/server.go +++ b/commands/server.go @@ -1,4 +1,4 @@ -// Copyright 2024 The Hugo Authors. All rights reserved. +// Copyright 2016 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,417 +14,60 @@ package commands import ( - "bytes" - "context" - "crypto/tls" - "crypto/x509" - "encoding/json" - "encoding/pem" - "errors" "fmt" - "io" - "maps" "net" "net/http" - _ "net/http/pprof" "net/url" "os" - "os/signal" - "path" - "path/filepath" - "regexp" - "sort" + "runtime" "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/hugolib" - "github.com/gohugoio/hugo/hugolib/filesystems" - "github.com/gohugoio/hugo/livereload" - "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" + jww "github.com/spf13/jwalterweatherman" ) var ( - logDuplicateTemplateExecuteRe = regexp.MustCompile(`: template: .*?:\d+:\d+: executing ".*?"`) - logDuplicateTemplateParseRe = regexp.MustCompile(`: template: .*?:\d+:\d*`) + disableLiveReload bool + navigateToChanged bool + renderToDisk bool + serverAppend bool + serverInterface string + serverPort int + serverWatch bool ) -var logReplacer = strings.NewReplacer( - "can't", "can’t", // Chroma lexer doesn't do well with "can't" - "*hugolib.pageState", "page.Page", // Page is the public interface. - "Rebuild failed:", "", -) +var serverCmd = &cobra.Command{ + Use: "server", + Aliases: []string{"serve"}, + Short: "A high performance webserver", + 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. -const ( - configChangeConfig = "config file" - configChangeGoMod = "go.mod file" - configChangeGoWork = "go work file" -) +'hugo server' will avoid writing the rendered and served content to disk, +preferring to store it in memory. -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: visitedURLs, - fullRebuildSem: semaphore.NewWeighted(1), - debounce: debounce.New(4 * time.Second), - onConfigLoaded: func(reloaded bool) error { - for _, wc := range onConfigLoaded { - if err := wc(reloaded); err != nil { - return err - } - } - return nil - }, - } -} - -func newServerCommand() *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 -} - -func (fs *countingStatFs) Stat(name string) (os.FileInfo, error) { - f, err := fs.Fs.Stat(name) - if err == nil { - if !f.IsDir() { - atomic.AddUint64(&fs.statCounter, 1) - } - } - return f, err -} - -// dynamicEvents contains events that is considered dynamic, as in "not static". -// Both of these categories will trigger a new build, but the asset events -// does not fit into the "navigate to changed" logic. -type dynamicEvents struct { - ContentEvents []fsnotify.Event - AssetEvents []fsnotify.Event -} - -type fileChangeDetector struct { - sync.Mutex - current map[string]uint64 - prev map[string]uint64 - - irrelevantRe *regexp.Regexp -} - -func (f *fileChangeDetector) OnFileClose(name string, checksum uint64) { - f.Lock() - defer f.Unlock() - f.current[name] = checksum -} - -func (f *fileChangeDetector) PrepareNew() { - if f == nil { - return - } - - f.Lock() - defer f.Unlock() - - if f.current == nil { - f.current = make(map[string]uint64) - f.prev = make(map[string]uint64) - return - } - - f.prev = make(map[string]uint64) - maps.Copy(f.prev, f.current) - f.current = make(map[string]uint64) -} - -func (f *fileChangeDetector) changed() []string { - if f == nil { - return nil - } - f.Lock() - defer f.Unlock() - var c []string - for k, v := range f.current { - vv, found := f.prev[k] - if !found || v != vv { - c = append(c, k) - } - } - - return f.filterIrrelevantAndSort(c) -} - -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 []urls.BaseURL - roots []string - errorTemplate func(err any) (io.Reader, error) - c *serverCommand -} - -func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string, string, error) { - r := f.c.r - baseURL := f.baseURLs[i] - root := f.roots[i] - port := f.c.serverPorts[i].p - listener := f.c.serverPorts[i].ln - logger := f.c.r.logger - - if i == 0 { - 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.Printf("Serving pages from %s\n", mainTarget) - } - } - - var httpFs *afero.HttpFs - f.c.withConf(func(conf *commonConfig) { - httpFs = afero.NewHttpFs(conf.fs.PublishDirServer) - }) - - fs := filesOnlyFs{httpFs.Dir(path.Join("/", root))} - if i == 0 && f.c.fastRenderMode { - r.Println("Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender") - } - - decorate := func(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if f.c.showErrorInBrowser { - // First check the error state - err := f.c.getErrorWithContext() - if err != nil { - f.c.errState.setWasErr(true) - w.WriteHeader(500) - r, err := f.errorTemplate(err) - if err != nil { - logger.Errorln(err) - } - - port = 1313 - f.c.withConf(func(conf *commonConfig) { - if lrport := conf.configs.GetFirstLanguageConfig().BaseURLLiveReload().Port(); lrport != 0 { - port = lrport - } - }) - lr := baseURL.URL() - lr.Host = fmt.Sprintf("%s:%d", lr.Hostname(), port) - fmt.Fprint(w, injectLiveReloadScript(r, lr)) - - return - } - } - - if f.c.noHTTPCache { - w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0") - w.Header().Set("Pragma", "no-cache") - } - - var serverConfig config.Server - f.c.withConf(func(conf *commonConfig) { - serverConfig = conf.configs.Base.Server - }) - - // Ignore any query params for the operations below. - requestURI, _ := url.PathUnescape(strings.TrimSuffix(r.RequestURI, "?"+r.URL.RawQuery)) - - for _, header := range serverConfig.MatchHeaders(requestURI) { - w.Header().Set(header.Key, header.Value) - } - - 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 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 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 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 { - f.c.handleBuildErr(err, fmt.Sprintf("Failed to render %q", requestURI)) - if f.c.showErrorInBrowser { - http.Redirect(w, r, requestURI, http.StatusMovedPermanently) - return - } - } - } - - f.c.visitedURLs.Add(requestURI) - - } - } - - h.ServeHTTP(w, r) - }) - } - - fileserver := decorate(http.FileServer(fs)) - mu := http.NewServeMux() - if baseURL.Path() == "" || baseURL.Path() == "/" { - mu.Handle("/", fileserver) - } else { - mu.Handle(baseURL.Path(), http.StripPrefix(baseURL.Path(), fileserver)) - } - if r.IsTestRun() { - var shutDownOnce sync.Once - mu.HandleFunc("/__stop", func(w http.ResponseWriter, r *http.Request) { - shutDownOnce.Do(func() { - close(f.c.quit) - }) - }) - } - - endpoint := net.JoinHostPort(f.c.serverInterface, strconv.Itoa(port)) - - return mu, listener, baseURL.String(), endpoint, nil -} - -func (f *fileServer) rewriteRequest(r *http.Request, toPath string) *http.Request { - r2 := new(http.Request) - *r2 = *r - r2.URL = new(url.URL) - *r2.URL = *r.URL - r2.URL.Path = toPath - r2.Header.Set("X-Rewrite-Original-URI", r.URL.RequestURI()) - - return r2 +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 +and push the latest content to them. As most Hugo sites are built in a fraction +of a second, you will be able to save and see your changes nearly instantly.`, + //RunE: server, } type filesOnlyFs struct { fs http.FileSystem } +type noDirFile struct { + http.File +} + func (fs filesOnlyFs) Open(name string) (http.File, error) { f, err := fs.fs.Open(name) if err != nil { @@ -433,825 +76,240 @@ func (fs filesOnlyFs) Open(name string) (http.File, error) { return noDirFile{f}, nil } -type noDirFile struct { - http.File -} - func (f noDirFile) Readdir(count int) ([]os.FileInfo, error) { return nil, nil } -type serverCommand struct { - r *rootCommand +func init() { + initHugoBuilderFlags(serverCmd) - commands []simplecobra.Commander + serverCmd.Flags().IntVarP(&serverPort, "port", "p", 1313, "port on which the server will listen") + serverCmd.Flags().StringVarP(&serverInterface, "bind", "", "127.0.0.1", "interface to which the server will bind") + serverCmd.Flags().BoolVarP(&serverWatch, "watch", "w", true, "watch filesystem for changes and recreate as needed") + serverCmd.Flags().BoolVarP(&serverAppend, "appendPort", "", true, "append port to baseURL") + serverCmd.Flags().BoolVar(&disableLiveReload, "disableLiveReload", false, "watch without enabling live browser reload on rebuild") + serverCmd.Flags().BoolVar(&navigateToChanged, "navigateToChanged", false, "navigate to changed content file on live browser reload") + serverCmd.Flags().BoolVar(&renderToDisk, "renderToDisk", false, "render to Destination path (default is render to memory & serve from there)") + serverCmd.Flags().String("memstats", "", "log memory usage to this file") + serverCmd.Flags().String("meminterval", "100ms", "interval to poll memory usage (requires --memstats), valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\".") - *hugoBuilder + serverCmd.RunE = server - quit chan bool // Closed when the server should shut down. Used in tests only. - serverPorts []serverPortListener - doLiveReload bool - - // Flags. - renderStaticToDisk bool - navigateToChanged bool - openBrowser bool - serverAppend bool - serverInterface string - tlsCertFile string - tlsKeyFile string - tlsAuto bool - pprof bool - serverPort int - liveReloadPort int - serverWatch bool - noHTTPCache bool - disableLiveReload bool - disableFastRender bool - disableBrowserError bool } -func (c *serverCommand) Name() string { - return "server" -} - -func (c *serverCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error { - if c.pprof { - go func() { - http.ListenAndServe("localhost:8080", nil) - }() - } - // Watch runs its own server as part of the routine - if c.serverWatch { - - watchDirs, err := c.getDirList() - if err != nil { - return err - } - - watchGroups := helpers.ExtractAndGroupRootPaths(watchDirs) - - for _, group := range watchGroups { - c.r.Printf("Watching for changes in %s\n", group) - } - watcher, err := c.newWatcher(c.r.poll, watchDirs...) - if err != nil { - return err - } - - defer watcher.Close() - - } - - err := func() error { - defer c.r.timeTrack(time.Now(), "Built") - return c.build() - }() +func server(cmd *cobra.Command, args []string) error { + cfg, err := InitializeConfig(serverCmd) if err != nil { return err } - return c.serve() -} - -func (c *serverCommand) Init(cd *simplecobra.Commandeer) error { - cmd := cd.CobraCommand - 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. - -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 -and push the latest content to them. As most Hugo sites are built in a fraction -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().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") - - r := cd.Root.Command.(*rootCommand) - applyLocalFlagsBuild(cmd, r) - - return nil -} - -func (c *serverCommand) PreRun(cd, runner *simplecobra.Commandeer) error { - c.r = cd.Root.Command.(*rootCommand) - - c.hugoBuilder = newHugoBuilder( - c.r, - c, - func(reloaded bool) error { - if !reloaded { - 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.setServerInfoInConfig(); err != nil { - return err - } - - if !reloaded && c.fastRenderMode { - c.withConf(func(conf *commonConfig) { - conf.fs.PublishDir = hugofs.NewHashingFs(conf.fs.PublishDir, c.changeDetector) - conf.fs.PublishDirStatic = hugofs.NewHashingFs(conf.fs.PublishDirStatic, c.changeDetector) - }) - } - - return nil - }, - ) - - destinationFlag := cd.CobraCommand.Flags().Lookup("destination") - 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 - - if c.fastRenderMode { - // For now, fast render mode only. It should, however, be fast enough - // for the full variant, too. - c.changeDetector = &fileChangeDetector{ - // We use this detector to decide to do a Hot reload of a single path or not. - // We need to filter out source maps and possibly some other to be able - // to make that decision. - irrelevantRe: regexp.MustCompile(`\.map$`), - } - - c.changeDetector.PrepareNew() - - } - - err := c.loadConfig(cd, true) + c, err := newCommandeer(cfg) if err != nil { return err } - return nil -} - -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.LanguagesDefaultFirst { - isMultihost := conf.configs.IsMultihost - var serverPort int - if isMultihost { - serverPort = c.serverPorts[i].p - } else { - serverPort = c.serverPorts[0].p - } - langConfig := conf.configs.LanguageConfigMap[language.Lang] - baseURLStr, err := c.fixURL(langConfig.BaseURL, c.r.baseURL, serverPort) - if err != nil { - return err - } - baseURL, err := urls.NewBaseURLFromString(baseURLStr) - if err != nil { - return fmt.Errorf("failed to create baseURL from %q: %s", baseURLStr, err) - } - - baseURLLiveReload := baseURL - if c.liveReloadPort != -1 { - baseURLLiveReload, _ = baseURLLiveReload.WithPort(c.liveReloadPort) - } - langConfig.C.SetServerInfo(baseURL, baseURLLiveReload, c.serverInterface) - - } - return nil - }) -} - -func (c *serverCommand) getErrorWithContext() any { - buildErr := c.errState.buildErr() - if buildErr == nil { - return nil + if cmd.Flags().Changed("disableLiveReload") { + c.Set("disableLiveReload", disableLiveReload) } - m := make(map[string]any) - - m["Error"] = cleanErrorLog(c.r.logger.Errors()) - - m["Version"] = hugo.BuildVersionString() - 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() + if cmd.Flags().Changed("navigateToChanged") { + c.Set("navigateToChanged", navigateToChanged) } - // 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 - } + if serverWatch { + c.Set("watch", true) } - c.tlsCertFile = filepath.Join(keyDir, fmt.Sprintf("%s.pem", hostname)) - c.tlsKeyFile = filepath.Join(keyDir, fmt.Sprintf("%s-key.pem", hostname)) + if c.Cfg.GetBool("watch") { + serverWatch = true + c.watchConfig() + } - // Check if the certificate already exists and is valid. - certPEM, err := os.ReadFile(c.tlsCertFile) + l, err := net.Listen("tcp", net.JoinHostPort(serverInterface, strconv.Itoa(serverPort))) 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 - } + l.Close() + } else { + if serverCmd.Flags().Changed("port") { + // port set explicitly by user -- he/she probably meant it! + return newSystemErrorF("Server startup failed: %s", err) + } + jww.ERROR.Println("port", serverPort, "already in use, attempting to use an available port") + sp, err := helpers.FindAvailablePort() + if err != nil { + return newSystemError("Unable to find alternative port to use:", err) + } + serverPort = sp.Port + } + + c.Set("port", serverPort) + + baseURL, err = fixURL(c.Cfg, baseURL) + if err != nil { + return err + } + c.Set("baseURL", baseURL) + + if err := memStats(); err != nil { + jww.ERROR.Println("memstats error:", err) + } + + // If a Destination is provided via flag write to disk + if destination != "" { + renderToDisk = true + } + + // Hugo writes the output to memory instead of the disk + if !renderToDisk { + cfg.Fs.Destination = new(afero.MemMapFs) + // Rendering to memoryFS, publish to Root regardless of publishDir. + c.Set("publishDir", "/") + } + + if err := c.build(serverWatch); err != nil { + return err + } + + for _, s := range Hugo.Sites { + s.RegisterMediaTypes() + } + + // Watch runs its own server as part of the routine + if serverWatch { + watchDirs := c.getDirList() + baseWatchDir := c.Cfg.GetString("workingDir") + for i, dir := range watchDirs { + watchDirs[i], _ = helpers.GetRelativePath(dir, baseWatchDir) + } + + rootWatchDirs := strings.Join(helpers.UniqueStrings(helpers.ExtractRootPaths(watchDirs)), ",") + + jww.FEEDBACK.Printf("Watching for changes in %s%s{%s}\n", baseWatchDir, helpers.FilePathSeparator, rootWatchDirs) + err := c.newWatcher(serverPort) + + if err != nil { + return err } } - 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()) - } + c.serve(serverPort) 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 - c.serverPorts = make([]serverPortListener, 1) - if isMultihost { - if !c.serverAppend { - cerr = errors.New("--appendPort=false not supported when in multihost mode") - return - } - c.serverPorts = make([]serverPortListener, len(conf.configs.Languages)) - } - currentServerPort := c.serverPort - 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} - } else { - if i == 0 && flags.Changed("port") { - // port set explicitly by user -- he/she probably meant it! - cerr = fmt.Errorf("server startup failed: %s", err) - return - } - c.r.Println("port", currentServerPort, "already in use, attempting to use an available port") - l, sp, err := helpers.TCPListen() - if err != nil { - cerr = fmt.Errorf("unable to find alternative port to use: %s", err) - return - } - c.serverPorts[i] = serverPortListener{ln: l, p: sp.Port} - } +func (c *commandeer) serve(port int) { + if renderToDisk { + jww.FEEDBACK.Println("Serving pages from " + c.PathSpec().AbsPathify(c.Cfg.GetString("publishDir"))) + } else { + jww.FEEDBACK.Println("Serving pages from memory") + } - currentServerPort = c.serverPorts[i].p + 1 - } - }) + httpFs := afero.NewHttpFs(c.Fs.Destination) + fs := filesOnlyFs{httpFs.Dir(c.PathSpec().AbsPathify(c.Cfg.GetString("publishDir")))} + fileserver := http.FileServer(fs) - return cerr + // We're only interested in the path + u, err := url.Parse(c.Cfg.GetString("baseURL")) + if err != nil { + jww.ERROR.Fatalf("Invalid baseURL: %s", err) + } + if u.Path == "" || u.Path == "/" { + http.Handle("/", fileserver) + } else { + http.Handle(u.Path, http.StripPrefix(u.Path, fileserver)) + } + + jww.FEEDBACK.Printf("Web Server is available at %s (bind address %s)\n", u.String(), serverInterface) + jww.FEEDBACK.Println("Press Ctrl+C to stop") + + endpoint := net.JoinHostPort(serverInterface, strconv.Itoa(port)) + err = http.ListenAndServe(endpoint, nil) + if err != nil { + jww.ERROR.Printf("Error: %s\n", err.Error()) + os.Exit(1) + } } // fixURL massages the baseURL into a form needed for serving // all pages correctly. -func (c *serverCommand) fixURL(baseURLFromConfig, baseURLFromFlag string, port int) (string, error) { - certsSet := (c.tlsCertFile != "" && c.tlsKeyFile != "") || c.tlsAuto +func fixURL(cfg config.Provider, s string) (string, error) { useLocalhost := false - baseURL := baseURLFromFlag - if baseURL == "" { - baseURL = baseURLFromConfig + if s == "" { + s = cfg.GetString("baseURL") useLocalhost = true } - if !strings.HasSuffix(baseURL, "/") { - baseURL = baseURL + "/" + if !strings.HasSuffix(s, "/") { + s = s + "/" } // do an initial parse of the input string - u, err := url.Parse(baseURL) + u, err := url.Parse(s) 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 == "" && baseURL != "/" { - baseURL = "//" + baseURL + if u.Host == "" && s != "/" { + s = "//" + s - u, err = url.Parse(baseURL) + u, err = url.Parse(s) if err != nil { return "", err } } if useLocalhost { - if certsSet { - u.Scheme = "https" - } else if u.Scheme == "https" { + if u.Scheme == "https" { u.Scheme = "http" } u.Host = "localhost" } - if c.serverAppend { + if serverAppend { if strings.Contains(u.Host, ":") { u.Host, _, err = net.SplitHostPort(u.Host) if err != nil { - return "", fmt.Errorf("failed to split baseURL hostport: %w", err) + return "", fmt.Errorf("Failed to split baseURL hostpost: %s", err) } } - u.Host += fmt.Sprintf(":%d", port) + u.Host += fmt.Sprintf(":%d", serverPort) } return u.String(), nil } -func (c *serverCommand) partialReRender(urls ...string) (err error) { - defer func() { - c.errState.setWasErr(false) - }() - visited := types.NewEvictingQueue[string](len(urls)) - for _, url := range urls { - visited.Add(url) - } +func memStats() error { + memstats := serverCmd.Flags().Lookup("memstats").Value.String() + if memstats != "" { + interval, err := time.ParseDuration(serverCmd.Flags().Lookup("meminterval").Value.String()) + if err != nil { + interval, _ = time.ParseDuration("100ms") + } - var h *hugolib.HugoSites - h, err = c.hugo() - if err != nil { - return - } - - // Note: We do not set NoBuildLock as the file lock is not acquired at this stage. - err = h.Build(hugolib.BuildCfg{NoBuildLock: false, RecentlyTouched: visited, PartialReRender: true, ErrRecovery: c.errState.wasErr()}) - - return -} - -func (c *serverCommand) serve() error { - var ( - baseURLs []urls.BaseURL - roots []string - h *hugolib.HugoSites - ) - err := c.withConfE(func(conf *commonConfig) error { - isMultihost := conf.configs.IsMultihost - var err error - h, err = c.r.HugFromConfig(conf) + fileMemStats, err := os.Create(memstats) if err != nil { return err } - // We need the server to share the same logger as the Hugo build (for error counts etc.) - c.r.logger = h.Log + fileMemStats.WriteString("# Time\tHeapSys\tHeapAlloc\tHeapIdle\tHeapReleased\n") - if isMultihost { - for _, l := range conf.configs.ConfigLangs() { - baseURLs = append(baseURLs, l.BaseURL()) - roots = append(roots, l.Language().Lang) - } - } else { - l := conf.configs.GetFirstLanguageConfig() - baseURLs = []urls.BaseURL{l.BaseURL()} - roots = []string{""} - } + go func() { + var stats runtime.MemStats - return nil - }) - if err != nil { - return err - } + start := time.Now().UnixNano() - // Cache it here. The HugoSites object may be unavailable later on due to intermittent configuration errors. - // 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 *tplimpl.TemplInfo - templHandler *tplimpl.TemplateStore - ) - getErrorTemplateAndHandler := func(h *hugolib.HugoSites) (*tplimpl.TemplInfo, *tplimpl.TemplateStore) { - if h == nil { - return errTempl, templHandler - } - templHandler := h.GetTemplateStore() - errTempl := templHandler.LookupByPath("/_server/error.html") - if errTempl == nil { - panic("template server/error.html not found") - } - return errTempl, templHandler - } - errTempl, templHandler = getErrorTemplateAndHandler(h) - - srv := &fileServer{ - baseURLs: baseURLs, - roots: roots, - c: c, - errorTemplate: func(ctx any) (io.Reader, error) { - // hugoTry does not block, getErrorTemplateAndHandler will fall back - // to cached values if nil. - templ, handler := getErrorTemplateAndHandler(c.hugoTry()) - b := &bytes.Buffer{} - err := handler.ExecuteWithContext(context.Background(), templ, b, ctx) - return b, err - }, - } - - doLiveReload := !c.disableLiveReload - - if doLiveReload { - livereload.Initialize() - } - - sigs := make(chan os.Signal, 1) - signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) - var servers []*http.Server - - wg1, ctx := errgroup.WithContext(context.Background()) - - for i := range baseURLs { - mu, listener, serverURL, endpoint, err := srv.createEndpoint(i) - 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 { - 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) %s\n", serverURL, c.serverInterface, roots[i]) - wg1.Go(func() error { - 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 - } - return nil - }) - } - - 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": baseURLs, - } - - dir := os.Getenv("WORK") - if dir != "" { - readyFile := filepath.Join(dir, ".ready") - // encode the test info as JSON into the .ready file. - b, err := json.Marshal(testInfo) - if err != nil { - return err - } - err = os.WriteFile(readyFile, b, 0o777) - if err != nil { - return err - } - } - - } - - 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 { - case <-c.quit: - return nil - case <-sigs: - return nil - case <-ctx.Done(): - return ctx.Err() - } - } - }() - if err != nil { - c.r.Println("Error:", err) - } - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - wg2, ctx := errgroup.WithContext(ctx) - for _, srv := range servers { - srv := srv - wg2.Go(func() error { - return srv.Shutdown(ctx) - }) - } - - err1, err2 := wg1.Wait(), wg2.Wait() - if err1 != nil { - return err1 - } - return err2 -} - -type serverPortListener struct { - p int - ln net.Listener -} - -type staticSyncer struct { - c *hugoBuilder -} - -func (s *staticSyncer) isStatic(h *hugolib.HugoSites, filename string) bool { - return h.BaseFs.SourceFilesystems.IsStatic(filename) -} - -func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error { - c := s.c - - syncFn := func(sourceFs *filesystems.SourceFilesystem) (uint64, error) { - publishDir := helpers.FilePathSeparator - - if sourceFs.PublishFolder != "" { - publishDir = filepath.Join(publishDir, sourceFs.PublishFolder) - } - - syncer := fsync.NewSyncer() - c.withConf(func(conf *commonConfig) { - syncer.NoTimes = conf.configs.Base.NoTimes - syncer.NoChmod = conf.configs.Base.NoChmod - syncer.ChmodFilter = chmodFilter - syncer.SrcFs = sourceFs.Fs - syncer.DestFs = conf.fs.PublishDir - if c.s != nil && c.s.renderStaticToDisk { - syncer.DestFs = conf.fs.PublishDirStatic - } - }) - - logger := s.c.r.logger - - for _, ev := range staticEvents { - // Due to our approach of layering both directories and the content's rendered output - // into one we can't accurately remove a file not in one of the source directories. - // If a file is in the local static dir and also in the theme static dir and we remove - // it from one of those locations we expect it to still exist in the destination - // - // If Hugo generates a file (from the content dir) over a static file - // the content generated file should take precedence. - // - // Because we are now watching and handling individual events it is possible that a static - // event that occupies the same path as a content generated file will take precedence - // until a regeneration of the content takes places. - // - // Hugo assumes that these cases are very rare and will permit this bad behavior - // The alternative is to track every single file and which pipeline rendered it - // and then to handle conflict resolution on every event. - - fromPath := ev.Name - - relPath, found := sourceFs.MakePathRelative(fromPath, true) - - if !found { - // Not member of this virtual host. - continue - } - - // Remove || rename is harder and will require an assumption. - // Hugo takes the following approach: - // If the static file exists in any of the static source directories after this event - // Hugo will re-sync it. - // If it does not exist in all of the static directories Hugo will remove it. - // - // This assumes that Hugo has not generated content on top of a static file and then removed - // the source of that static file. In this case Hugo will incorrectly remove that file - // from the published directory. - if ev.Op&fsnotify.Rename == fsnotify.Rename || ev.Op&fsnotify.Remove == fsnotify.Remove { - if _, err := sourceFs.Fs.Stat(relPath); herrors.IsNotExist(err) { - // If file doesn't exist in any static dir, remove it - logger.Println("File no longer exists in static dir, removing", relPath) - c.withConf(func(conf *commonConfig) { - _ = conf.fs.PublishDirStatic.RemoveAll(relPath) - }) - - } else if err == nil { - // If file still exists, sync it - logger.Println("Syncing", relPath, "to", publishDir) - - if err := syncer.Sync(relPath, relPath); err != nil { - c.r.logger.Errorln(err) - } + for { + runtime.ReadMemStats(&stats) + if fileMemStats != nil { + fileMemStats.WriteString(fmt.Sprintf("%d\t%d\t%d\t%d\t%d\n", + (time.Now().UnixNano()-start)/1000000, stats.HeapSys, stats.HeapAlloc, stats.HeapIdle, stats.HeapReleased)) + time.Sleep(interval) } else { - c.r.logger.Errorln(err) + break } - - continue } - - // For all other event operations Hugo will sync static. - logger.Println("Syncing", relPath, "to", publishDir) - if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil { - c.r.logger.Errorln(err) - } - } - - return 0, nil + }() } - - _, err := c.doWithPublishDirs(syncFn) - return err -} - -func chmodFilter(dst, src os.FileInfo) bool { - // Hugo publishes data from multiple sources, potentially - // with overlapping directory structures. We cannot sync permissions - // for directories as that would mean that we might end up with write-protected - // directories inside /public. - // One example of this would be syncing from the Go Module cache, - // which have 0555 directories. - return src.IsDir() -} - -func cleanErrorLog(content string) string { - content = strings.ReplaceAll(content, "\n", " ") - content = logReplacer.Replace(content) - content = logDuplicateTemplateExecuteRe.ReplaceAllString(content, "") - content = logDuplicateTemplateParseRe.ReplaceAllString(content, "") - seen := make(map[string]bool) - parts := strings.Split(content, ": ") - keep := make([]string, 0, len(parts)) - for _, part := range parts { - if seen[part] { - continue - } - seen[part] = true - keep = append(keep, part) - } - return strings.Join(keep, ": ") -} - -func injectLiveReloadScript(src io.Reader, baseURL *url.URL) string { - var b bytes.Buffer - chain := transform.Chain{livereloadinject.New(baseURL)} - chain.Apply(&b, src) - - return b.String() -} - -func partitionDynamicEvents(sourceFs *filesystems.SourceFilesystems, events []fsnotify.Event) (de dynamicEvents) { - for _, e := range events { - if !sourceFs.IsContent(e.Name) { - de.AssetEvents = append(de.AssetEvents, e) - } else { - de.ContentEvents = append(de.ContentEvents, e) - } - } - return -} - -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 contentTypes.IsIndexContentFile(ev.Name) { - return ev.Name - } - - if contentTypes.IsContentFile(ev.Name) { - name = ev.Name - } - - } - } - - return name -} - -func formatByteCount(b uint64) string { - const unit = 1000 - if b < unit { - return fmt.Sprintf("%d B", b) - } - div, exp := int64(unit), 0 - for n := b / unit; n >= unit; n /= unit { - div *= unit - exp++ - } - 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, ".") + return nil } diff --git a/commands/server_test.go b/commands/server_test.go new file mode 100644 index 000000000..3f1518aaa --- /dev/null +++ b/commands/server_test.go @@ -0,0 +1,58 @@ +// Copyright 2015 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 ( + "testing" + + "github.com/spf13/viper" +) + +func TestFixURL(t *testing.T) { + type data struct { + TestName string + CLIBaseURL string + CfgBaseURL string + AppendPort bool + Port int + Result string + } + tests := []data{ + {"Basic http localhost", "", "http://foo.com", true, 1313, "http://localhost:1313/"}, + {"Basic https production, http localhost", "", "https://foo.com", true, 1313, "http://localhost:1313/"}, + {"Basic subdir", "", "http://foo.com/bar", true, 1313, "http://localhost:1313/bar/"}, + {"Basic production", "http://foo.com", "http://foo.com", false, 80, "http://foo.com/"}, + {"Production subdir", "http://foo.com/bar", "http://foo.com/bar", false, 80, "http://foo.com/bar/"}, + {"No http", "", "foo.com", true, 1313, "//localhost:1313/"}, + {"Override configured port", "", "foo.com:2020", true, 1313, "//localhost:1313/"}, + {"No http production", "foo.com", "foo.com", false, 80, "//foo.com/"}, + {"No http production with port", "foo.com", "foo.com", true, 2020, "//foo.com:2020/"}, + {"No config", "", "", true, 1313, "//localhost:1313/"}, + } + + for i, test := range tests { + v := viper.New() + baseURL = test.CLIBaseURL + v.Set("baseURL", test.CfgBaseURL) + serverAppend = test.AppendPort + serverPort = test.Port + result, err := fixURL(v, baseURL) + if err != nil { + t.Errorf("Test #%d %s: unexpected error %s", i, test.TestName, err) + } + if result != test.Result { + t.Errorf("Test #%d %s: expected %q, got %q", i, test.TestName, test.Result, result) + } + } +} diff --git a/commands/undraft.go b/commands/undraft.go new file mode 100644 index 000000000..8d4bffb93 --- /dev/null +++ b/commands/undraft.go @@ -0,0 +1,155 @@ +// Copyright 2015 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 ( + "bytes" + "errors" + "os" + "time" + + "github.com/gohugoio/hugo/parser" + "github.com/spf13/cobra" +) + +var undraftCmd = &cobra.Command{ + Use: "undraft path/to/content", + Short: "Undraft resets the content's draft status", + Long: `Undraft resets the content's draft status +and updates the date to the current date and time. +If the content's draft status is 'False', nothing is done.`, + RunE: Undraft, +} + +// Undraft publishes the specified content by setting its draft status +// to false and setting its publish date to now. If the specified content is +// not a draft, it will log an error. +func Undraft(cmd *cobra.Command, args []string) error { + cfg, err := InitializeConfig() + + if err != nil { + return err + } + + if len(args) < 1 { + return newUserError("a piece of content needs to be specified") + } + + location := args[0] + // open the file + f, err := cfg.Fs.Source.Open(location) + if err != nil { + return err + } + + // get the page from file + p, err := parser.ReadFrom(f) + f.Close() + if err != nil { + return err + } + + w, err := undraftContent(p) + if err != nil { + return newSystemErrorF("an error occurred while undrafting %q: %s", location, err) + } + + f, err = cfg.Fs.Source.OpenFile(location, os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return newSystemErrorF("%q not be undrafted due to error opening file to save changes: %q\n", location, err) + } + defer f.Close() + _, err = w.WriteTo(f) + if err != nil { + return newSystemErrorF("%q not be undrafted due to save error: %q\n", location, err) + } + return nil +} + +// undraftContent: if the content is a draft, change its draft status to +// 'false' and set the date to time.Now(). If the draft status is already +// 'false', don't do anything. +func undraftContent(p parser.Page) (bytes.Buffer, error) { + var buff bytes.Buffer + // get the metadata; easiest way to see if it's a draft + meta, err := p.Metadata() + if err != nil { + return buff, err + } + // since the metadata was obtainable, we can also get the key/value separator for + // Front Matter + fm := p.FrontMatter() + if fm == nil { + return buff, errors.New("Front Matter was found, nothing was finalized") + } + + var isDraft, gotDate bool + var date string +L: + for k, v := range meta.(map[string]interface{}) { + switch k { + case "draft": + if !v.(bool) { + return buff, errors.New("not a Draft: nothing was done") + } + isDraft = true + if gotDate { + break L + } + case "date": + date = v.(string) // capture the value to make replacement easier + gotDate = true + if isDraft { + break L + } + } + } + + // if draft wasn't found in FrontMatter, it isn't a draft. + if !isDraft { + return buff, errors.New("not a Draft: nothing was done") + } + + // get the front matter as bytes and split it into lines + var lineEnding []byte + fmLines := bytes.Split(fm, []byte("\n")) + if len(fmLines) == 1 { // if the result is only 1 element, try to split on dos line endings + fmLines = bytes.Split(fm, []byte("\r\n")) + if len(fmLines) == 1 { + return buff, errors.New("unable to split FrontMatter into lines") + } + lineEnding = append(lineEnding, []byte("\r\n")...) + } else { + lineEnding = append(lineEnding, []byte("\n")...) + } + + // Write the front matter lines to the buffer, replacing as necessary + for _, v := range fmLines { + pos := bytes.Index(v, []byte("draft")) + if pos != -1 { + continue + } + pos = bytes.Index(v, []byte("date")) + if pos != -1 { // if date field wasn't found, add it + v = bytes.Replace(v, []byte(date), []byte(time.Now().Format(time.RFC3339)), 1) + } + buff.Write(v) + buff.Write(lineEnding) + } + + // append the actual content + buff.Write(p.Content()) + + return buff, nil +} diff --git a/commands/undraft_test.go b/commands/undraft_test.go new file mode 100644 index 000000000..259e3479b --- /dev/null +++ b/commands/undraft_test.go @@ -0,0 +1,87 @@ +// Copyright 2015 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 + +// TODO Support Mac Encoding (\r) + +import ( + "bytes" + "strings" + "testing" + "time" + + "github.com/gohugoio/hugo/parser" +) + +var ( + jsonFM = "{\n \"date\": \"12-04-06\",\n \"title\": \"test json\"\n}" + jsonDraftFM = "{\n \"draft\": true,\n \"date\": \"12-04-06\",\n \"title\":\"test json\"\n}" + tomlFM = "+++\n date= \"12-04-06\"\n title= \"test toml\"\n+++" + tomlDraftFM = "+++\n draft= true\n date= \"12-04-06\"\n title=\"test toml\"\n+++" + yamlFM = "---\n date: \"12-04-06\"\n title: \"test yaml\"\n---" + yamlDraftFM = "---\n draft: true\n date: \"12-04-06\"\n title: \"test yaml\"\n---" + yamlYesDraftFM = "---\n draft: yes\n date: \"12-04-06\"\n title: \"test yaml\"\n---" +) + +func TestUndraftContent(t *testing.T) { + tests := []struct { + fm string + expectedErr string + }{ + {jsonFM, "not a Draft: nothing was done"}, + {jsonDraftFM, ""}, + {tomlFM, "not a Draft: nothing was done"}, + {tomlDraftFM, ""}, + {yamlFM, "not a Draft: nothing was done"}, + {yamlDraftFM, ""}, + {yamlYesDraftFM, ""}, + } + + for i, test := range tests { + r := bytes.NewReader([]byte(test.fm)) + p, _ := parser.ReadFrom(r) + res, err := undraftContent(p) + if test.expectedErr != "" { + if err == nil { + t.Errorf("[%d] Expected error, got none", i) + continue + } + if err.Error() != test.expectedErr { + t.Errorf("[%d] Expected %q, got %q", i, test.expectedErr, err) + continue + } + } else { + r = bytes.NewReader(res.Bytes()) + p, _ = parser.ReadFrom(r) + meta, err := p.Metadata() + if err != nil { + t.Errorf("[%d] unexpected error %q", i, err) + continue + } + for k, v := range meta.(map[string]interface{}) { + if k == "draft" { + if v.(bool) { + t.Errorf("[%d] Expected %q to be \"false\", got \"true\"", i, k) + continue + } + } + if k == "date" { + if !strings.HasPrefix(v.(string), time.Now().Format("2006-01-02")) { + t.Errorf("[%d] Expected %v to start with %v", i, v.(string), time.Now().Format("2006-01-02")) + } + } + } + } + } +} diff --git a/commands/version.go b/commands/version.go new file mode 100644 index 000000000..5cd398b2b --- /dev/null +++ b/commands/version.go @@ -0,0 +1,80 @@ +// Copyright 2015 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 ( + "os" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugolib" + "github.com/kardianos/osext" + "github.com/spf13/cobra" + jww "github.com/spf13/jwalterweatherman" +) + +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Print the version number of Hugo", + Long: `All software has versions. This is Hugo's.`, + RunE: func(cmd *cobra.Command, args []string) error { + printHugoVersion() + return nil + }, +} + +func printHugoVersion() { + if hugolib.BuildDate == "" { + setBuildDate() // set the build date from executable's mdate + } else { + formatBuildDate() // format the compile time + } + if hugolib.CommitHash == "" { + jww.FEEDBACK.Printf("Hugo Static Site Generator v%s %s/%s BuildDate: %s\n", helpers.CurrentHugoVersion, runtime.GOOS, runtime.GOARCH, hugolib.BuildDate) + } else { + jww.FEEDBACK.Printf("Hugo Static Site Generator v%s-%s %s/%s BuildDate: %s\n", helpers.CurrentHugoVersion, strings.ToUpper(hugolib.CommitHash), runtime.GOOS, runtime.GOARCH, hugolib.BuildDate) + } +} + +// setBuildDate checks the ModTime of the Hugo executable and returns it as a +// formatted string. This assumes that the executable name is Hugo, if it does +// not exist, an empty string will be returned. This is only called if the +// hugolib.BuildDate wasn't set during compile time. +// +// osext is used for cross-platform. +func setBuildDate() { + fname, _ := osext.Executable() + dir, err := filepath.Abs(filepath.Dir(fname)) + if err != nil { + jww.ERROR.Println(err) + return + } + fi, err := os.Lstat(filepath.Join(dir, filepath.Base(fname))) + if err != nil { + jww.ERROR.Println(err) + return + } + t := fi.ModTime() + hugolib.BuildDate = t.Format(time.RFC3339) +} + +// formatBuildDate formats the hugolib.BuildDate according to the value in +// .Params.DateFormat, if it's set. +func formatBuildDate() { + t, _ := time.Parse("2006-01-02T15:04:05-0700", hugolib.BuildDate) + hugolib.BuildDate = t.Format(time.RFC3339) +} diff --git a/common/collections/append.go b/common/collections/append.go deleted file mode 100644 index db9db8bf3..000000000 --- a/common/collections/append.go +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright 2019 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 ( - "fmt" - "reflect" -) - -// Append appends from to a slice to and returns the resulting slice. -// 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 - } - - // If we get []string []string, we append the from slice to to - if tot == fromt { - return reflect.AppendSlice(tov, fromv).Interface(), nil - } else if !fromt.AssignableTo(tot) { - // Fall back to a []interface{} slice. - return appendToInterfaceSliceFromValues(tov, fromv) - } - - } - } - } - - if toIsNil { - return Slice(from...), nil - } - - for _, f := range from { - fv := reflect.ValueOf(f) - if !fv.IsValid() || !fv.Type().AssignableTo(tot) { - // Fall back to a []interface{} slice. - tov, _ := indirect(reflect.ValueOf(to)) - return appendToInterfaceSlice(tov, from...) - } - tov = reflect.Append(tov, fv) - } - - return tov.Interface(), nil -} - -func appendToInterfaceSliceFromValues(slice1, slice2 reflect.Value) ([]any, error) { - var tos []any - - for _, slice := range []reflect.Value{slice1, slice2} { - if !slice.IsValid() { - tos = append(tos, nil) - continue - } - for i := range slice.Len() { - tos = append(tos, slice.Index(i).Interface()) - } - } - - return tos, nil -} - -func appendToInterfaceSlice(tov reflect.Value, from ...any) ([]any, error) { - var tos []any - - for i := range tov.Len() { - tos = append(tos, tov.Index(i).Interface()) - } - - tos = append(tos, from...) - - return tos, nil -} - -// indirect is borrowed from the Go stdlib: 'text/template/exec.go' -// TODO(bep) consolidate -func indirect(v reflect.Value) (rv reflect.Value, isNil bool) { - for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() { - if v.IsNil() { - return v, true - } - if v.Kind() == reflect.Interface && v.NumMethod() > 0 { - break - } - } - return v, false -} diff --git a/common/collections/append_test.go b/common/collections/append_test.go deleted file mode 100644 index 62d9015ce..000000000 --- a/common/collections/append_test.go +++ /dev/null @@ -1,213 +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 collections - -import ( - "html/template" - "reflect" - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestAppend(t *testing.T) { - t.Parallel() - c := qt.New(t) - - for i, test := range []struct { - start any - addend []any - expected any - }{ - {[]string{"a", "b"}, []any{"c"}, []string{"a", "b", "c"}}, - {[]string{"a", "b"}, []any{"c", "d", "e"}, []string{"a", "b", "c", "d", "e"}}, - {[]string{"a", "b"}, []any{[]string{"c", "d", "e"}}, []string{"a", "b", "c", "d", "e"}}, - {[]string{"a"}, []any{"b", template.HTML("c")}, []any{"a", "b", template.HTML("c")}}, - {nil, []any{"a", "b"}, []string{"a", "b"}}, - {nil, []any{nil}, []any{nil}}, - {[]any{}, []any{[]string{"c", "d", "e"}}, []string{"c", "d", "e"}}, - { - tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}}, - []any{&tstSlicer{"c"}}, - tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}, &tstSlicer{"c"}}, - }, - { - &tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}}, - []any{&tstSlicer{"c"}}, - tstSlicers{ - &tstSlicer{"a"}, - &tstSlicer{"b"}, - &tstSlicer{"c"}, - }, - }, - { - testSlicerInterfaces{&tstSlicerIn1{"a"}, &tstSlicerIn1{"b"}}, - []any{&tstSlicerIn1{"c"}}, - testSlicerInterfaces{&tstSlicerIn1{"a"}, &tstSlicerIn1{"b"}, &tstSlicerIn1{"c"}}, - }, - // https://github.com/gohugoio/hugo/issues/5361 - { - []string{"a", "b"}, - []any{tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}}}, - []any{"a", "b", &tstSlicer{"a"}, &tstSlicer{"b"}}, - }, - { - []string{"a", "b"}, - []any{&tstSlicer{"a"}}, - []any{"a", "b", &tstSlicer{"a"}}, - }, - // Errors - {"", []any{[]string{"a", "b"}}, false}, - // No string concatenation. - { - "ab", - []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...) - - if b, ok := test.expected.(bool); ok && !b { - - c.Assert(err, qt.Not(qt.IsNil)) - continue - } - - c.Assert(err, qt.IsNil) - 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/collections.go b/common/collections/collections.go deleted file mode 100644 index 0b46abee9..000000000 --- a/common/collections/collections.go +++ /dev/null @@ -1,21 +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 collections contains common Hugo functionality related to collection -// handling. -package collections - -// Grouper defines a very generic way to group items by a given key. -type Grouper interface { - Group(key any, items any) (any, error) -} diff --git a/common/collections/order.go b/common/collections/order.go deleted file mode 100644 index 4bdc3b4ac..000000000 --- a/common/collections/order.go +++ /dev/null @@ -1,20 +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 collections - -type Order interface { - // Ordinal is a zero-based ordinal that represents the order of an object - // in a collection. - Ordinal() int -} diff --git a/common/collections/slice.go b/common/collections/slice.go deleted file mode 100644 index 731f489f9..000000000 --- a/common/collections/slice.go +++ /dev/null @@ -1,95 +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 collections - -import ( - "reflect" - "sort" -) - -// Slicer defines a very generic way to create a typed slice. This is used -// in collections.Slice template func to get types such as Pages, PageGroups etc. -// instead of the less useful []interface{}. -type Slicer interface { - Slice(items any) (any, error) -} - -// Slice returns a slice of all passed arguments. -func Slice(args ...any) any { - if len(args) == 0 { - return args - } - - first := args[0] - firstType := reflect.TypeOf(first) - - if firstType == nil { - return args - } - - if g, ok := first.(Slicer); ok { - v, err := g.Slice(args) - if err == nil { - return v - } - - // If Slice fails, the items are not of the same type and - // []interface{} is the best we can do. - return args - } - - if len(args) > 1 { - // This can be a mix of types. - for i := 1; i < len(args); i++ { - if firstType != reflect.TypeOf(args[i]) { - // []interface{} is the best we can do - return args - } - } - } - - slice := reflect.MakeSlice(reflect.SliceOf(firstType), len(args), len(args)) - for i, arg := range args { - slice.Index(i).Set(reflect.ValueOf(arg)) - } - return slice.Interface() -} - -// StringSliceToInterfaceSlice converts ss to []interface{}. -func StringSliceToInterfaceSlice(ss []string) []any { - result := make([]any, len(ss)) - for i, s := range ss { - result[i] = s - } - return result -} - -type SortedStringSlice []string - -// Contains returns true if s is in ss. -func (ss SortedStringSlice) Contains(s string) bool { - i := sort.SearchStrings(ss, s) - return i < len(ss) && ss[i] == s -} - -// Count returns the number of times s is in ss. -func (ss SortedStringSlice) Count(s string) int { - var count int - i := sort.SearchStrings(ss, s) - for i < len(ss) && ss[i] == s { - count++ - i++ - } - return count -} diff --git a/common/collections/slice_test.go b/common/collections/slice_test.go deleted file mode 100644 index 4008a5e6c..000000000 --- a/common/collections/slice_test.go +++ /dev/null @@ -1,172 +0,0 @@ -// Copyright 2019 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 ( - "errors" - "testing" - - qt "github.com/frankban/quicktest" -) - -var ( - _ Slicer = (*tstSlicer)(nil) - _ Slicer = (*tstSlicerIn1)(nil) - _ Slicer = (*tstSlicerIn2)(nil) - _ testSlicerInterface = (*tstSlicerIn1)(nil) - _ testSlicerInterface = (*tstSlicerIn1)(nil) -) - -type testSlicerInterface interface { - Name() string -} - -type testSlicerInterfaces []testSlicerInterface - -type tstSlicerIn1 struct { - TheName string -} - -type tstSlicerIn2 struct { - TheName string -} - -type tstSlicer struct { - TheName string -} - -func (p *tstSlicerIn1) Slice(in any) (any, error) { - items := in.([]any) - result := make(testSlicerInterfaces, len(items)) - for i, v := range items { - switch vv := v.(type) { - case testSlicerInterface: - result[i] = vv - default: - return nil, errors.New("invalid type") - } - } - return result, nil -} - -func (p *tstSlicerIn2) Slice(in any) (any, error) { - items := in.([]any) - result := make(testSlicerInterfaces, len(items)) - for i, v := range items { - switch vv := v.(type) { - case testSlicerInterface: - result[i] = vv - default: - return nil, errors.New("invalid type") - } - } - return result, nil -} - -func (p *tstSlicerIn1) Name() string { - return p.TheName -} - -func (p *tstSlicerIn2) Name() string { - return p.TheName -} - -func (p *tstSlicer) Slice(in any) (any, error) { - items := in.([]any) - result := make(tstSlicers, len(items)) - for i, v := range items { - switch vv := v.(type) { - case *tstSlicer: - result[i] = vv - default: - return nil, errors.New("invalid type") - } - } - return result, nil -} - -type tstSlicers []*tstSlicer - -func TestSlice(t *testing.T) { - t.Parallel() - c := qt.New(t) - - for i, test := range []struct { - args []any - expected any - }{ - {[]any{"a", "b"}, []string{"a", "b"}}, - {[]any{&tstSlicer{"a"}, &tstSlicer{"b"}}, tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}}}, - {[]any{&tstSlicer{"a"}, "b"}, []any{&tstSlicer{"a"}, "b"}}, - {[]any{}, []any{}}, - {[]any{nil}, []any{nil}}, - {[]any{5, "b"}, []any{5, "b"}}, - {[]any{&tstSlicerIn1{"a"}, &tstSlicerIn2{"b"}}, testSlicerInterfaces{&tstSlicerIn1{"a"}, &tstSlicerIn2{"b"}}}, - {[]any{&tstSlicerIn1{"a"}, &tstSlicer{"b"}}, []any{&tstSlicerIn1{"a"}, &tstSlicer{"b"}}}, - } { - errMsg := qt.Commentf("[%d] %v", i, test.args) - - result := Slice(test.args...) - - c.Assert(test.expected, qt.DeepEquals, result, errMsg) - } -} - -func TestSortedStringSlice(t *testing.T) { - t.Parallel() - c := qt.New(t) - - var s SortedStringSlice = []string{"a", "b", "b", "b", "c", "d"} - - c.Assert(s.Contains("a"), qt.IsTrue) - c.Assert(s.Contains("b"), qt.IsTrue) - c.Assert(s.Contains("z"), qt.IsFalse) - 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 deleted file mode 100644 index ff0db2f02..000000000 --- a/common/collections/stack.go +++ /dev/null @@ -1,82 +0,0 @@ -// 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 deleted file mode 100644 index 965d4dbc8..000000000 --- a/common/collections/stack_test.go +++ /dev/null @@ -1,77 +0,0 @@ -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 deleted file mode 100644 index c7bbaa541..000000000 --- a/common/constants/constants.go +++ /dev/null @@ -1,49 +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 constants - -// Error/Warning IDs. -// Do not change these values. -const ( - // 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/docs.go b/common/docs.go deleted file mode 100644 index 041a62a01..000000000 --- a/common/docs.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package common provides common helper functionality for Hugo. -package common diff --git a/common/hashing/hashing.go b/common/hashing/hashing.go deleted file mode 100644 index e45356758..000000000 --- a/common/hashing/hashing.go +++ /dev/null @@ -1,194 +0,0 @@ -// 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 deleted file mode 100644 index 105b6d8b5..000000000 --- a/common/hashing/hashing_test.go +++ /dev/null @@ -1,157 +0,0 @@ -// 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 deleted file mode 100644 index 9524ef284..000000000 --- a/common/hcontext/context.go +++ /dev/null @@ -1,46 +0,0 @@ -// 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 deleted file mode 100644 index acaebb4bc..000000000 --- a/common/herrors/error_locator.go +++ /dev/null @@ -1,170 +0,0 @@ -// 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 herrors contains common Hugo errors and error related utilities. -package herrors - -import ( - "io" - "path/filepath" - "strings" - - "github.com/gohugoio/hugo/common/text" -) - -// LineMatcher contains the elements used to match an error to a line -type LineMatcher struct { - Position text.Position - Error error - - LineNumber int - Offset int - Line string -} - -// 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 determined. Returns -1 if no line match. -type LineMatcherFn func(m LineMatcher) int - -// SimpleLineMatcher simply matches by line number. -var SimpleLineMatcher = func(m LineMatcher) int { - if m.Position.LineNumber == m.LineNumber { - // We found the line, but don't know the column. - return 0 - } - return -1 -} - -// NopLineMatcher is a matcher that always returns 1. -// This will effectively give line 1, column 1. -var NopLineMatcher = func(m LineMatcher) int { - return 1 -} - -// OffsetMatcher is a line matcher that matches by offset. -var OffsetMatcher = func(m LineMatcher) int { - if m.Offset+len(m.Line) >= m.Position.Offset { - // We found the line, but return 0 to signal that we want to determine - // the column from the error. - return 0 - } - return -1 -} - -// ContainsMatcher is a line matcher that matches by line content. -func ContainsMatcher(text string) func(m LineMatcher) int { - return func(m LineMatcher) int { - if idx := strings.Index(m.Line, text); idx != -1 { - return idx + 1 - } - return -1 - } -} - -// 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 - - // The position of the error in the Lines above. 0 based. - LinesPos int - - // The position of the content in the file. Note that this may be different from the error's position set - // in FileError. - Position text.Position - - // The lexer to use for syntax highlighting. - // https://gohugo.io/content-management/syntax-highlighting/#list-of-chroma-highlighting-languages - ChromaLexer string -} - -func chromaLexerFromType(fileType string) string { - switch fileType { - case "html", "htm": - return "go-html-template" - } - return fileType -} - -func extNoDelimiter(filename string) string { - return strings.TrimPrefix(filepath.Ext(filename), ".") -} - -func chromaLexerFromFilename(filename string) string { - if strings.Contains(filename, "layouts") { - return "go-html-template" - } - - ext := extNoDelimiter(filename) - return chromaLexerFromType(ext) -} - -func locateErrorInString(src string, matcher LineMatcherFn) *ErrorContext { - return locateError(strings.NewReader(src), &fileError{}, matcher) -} - -func locateError(r io.Reader, le FileError, matches LineMatcherFn) *ErrorContext { - if le == nil { - panic("must provide an error") - } - - ectx := &ErrorContext{LinesPos: -1, Position: text.Position{Offset: -1}} - - b, err := io.ReadAll(r) - if err != nil { - return ectx - } - - lines := strings.Split(string(b), "\n") - - lineNo := 0 - posBytes := 0 - - for li, line := range lines { - lineNo = li + 1 - m := LineMatcher{ - Position: le.Position(), - Error: le, - LineNumber: lineNo, - Offset: posBytes, - Line: line, - } - v := matches(m) - if ectx.LinesPos == -1 && v != -1 { - ectx.Position.LineNumber = lineNo - ectx.Position.ColumnNumber = v - break - } - - posBytes += len(line) - } - - if ectx.Position.LineNumber > 0 { - low := max(ectx.Position.LineNumber-3, 0) - - if ectx.Position.LineNumber > 2 { - ectx.LinesPos = 2 - } else { - ectx.LinesPos = ectx.Position.LineNumber - 1 - } - - high := min(ectx.Position.LineNumber+2, len(lines)) - - ectx.Lines = lines[low:high] - - } - - return ectx -} diff --git a/common/herrors/error_locator_test.go b/common/herrors/error_locator_test.go deleted file mode 100644 index 62f15213d..000000000 --- a/common/herrors/error_locator_test.go +++ /dev/null @@ -1,152 +0,0 @@ -// 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 herrors contains common Hugo errors and error related utilities. -package herrors - -import ( - "strings" - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestErrorLocator(t *testing.T) { - c := qt.New(t) - - lineMatcher := func(m LineMatcher) int { - if strings.Contains(m.Line, "THEONE") { - return 1 - } - return -1 - } - - lines := `LINE 1 -LINE 2 -LINE 3 -LINE 4 -This is THEONE -LINE 6 -LINE 7 -LINE 8 -` - - location := locateErrorInString(lines, lineMatcher) - pos := location.Position - c.Assert(location.Lines, qt.DeepEquals, []string{"LINE 3", "LINE 4", "This is THEONE", "LINE 6", "LINE 7"}) - - c.Assert(pos.LineNumber, qt.Equals, 5) - c.Assert(location.LinesPos, qt.Equals, 2) - - locate := func(s string, m LineMatcherFn) *ErrorContext { - ctx := locateErrorInString(s, m) - return ctx - } - - c.Assert(locate(`This is THEONE`, lineMatcher).Lines, qt.DeepEquals, []string{"This is THEONE"}) - - location = locateErrorInString(`L1 -This is THEONE -L2 -`, lineMatcher) - pos = location.Position - c.Assert(pos.LineNumber, qt.Equals, 2) - c.Assert(location.LinesPos, qt.Equals, 1) - c.Assert(location.Lines, qt.DeepEquals, []string{"L1", "This is THEONE", "L2", ""}) - - location = locate(`This is THEONE -L2 -`, lineMatcher) - c.Assert(location.LinesPos, qt.Equals, 0) - c.Assert(location.Lines, qt.DeepEquals, []string{"This is THEONE", "L2", ""}) - - location = locate(`L1 -This THEONE -`, lineMatcher) - c.Assert(location.Lines, qt.DeepEquals, []string{"L1", "This THEONE", ""}) - c.Assert(location.LinesPos, qt.Equals, 1) - - location = locate(`L1 -L2 -This THEONE -`, lineMatcher) - c.Assert(location.Lines, qt.DeepEquals, []string{"L1", "L2", "This THEONE", ""}) - c.Assert(location.LinesPos, qt.Equals, 2) - - location = locateErrorInString("NO MATCH", lineMatcher) - pos = location.Position - c.Assert(pos.LineNumber, qt.Equals, 0) - c.Assert(location.LinesPos, qt.Equals, -1) - c.Assert(len(location.Lines), qt.Equals, 0) - - lineMatcher = func(m LineMatcher) int { - if m.LineNumber == 6 { - return 1 - } - return -1 - } - - location = locateErrorInString(`A -B -C -D -E -F -G -H -I -J`, lineMatcher) - pos = location.Position - - c.Assert(location.Lines, qt.DeepEquals, []string{"D", "E", "F", "G", "H"}) - c.Assert(pos.LineNumber, qt.Equals, 6) - c.Assert(location.LinesPos, qt.Equals, 2) - - // Test match EOF - lineMatcher = func(m LineMatcher) int { - if m.LineNumber == 4 { - return 1 - } - return -1 - } - - location = locateErrorInString(`A -B -C -`, lineMatcher) - - pos = location.Position - - c.Assert(location.Lines, qt.DeepEquals, []string{"B", "C", ""}) - c.Assert(pos.LineNumber, qt.Equals, 4) - c.Assert(location.LinesPos, qt.Equals, 2) - - offsetMatcher := func(m LineMatcher) int { - if m.Offset == 1 { - return 1 - } - return -1 - } - - location = locateErrorInString(`A -B -C -D -E`, offsetMatcher) - - pos = location.Position - - c.Assert(location.Lines, qt.DeepEquals, []string{"A", "B", "C", "D"}) - c.Assert(pos.LineNumber, qt.Equals, 2) - c.Assert(location.LinesPos, qt.Equals, 1) -} diff --git a/common/herrors/errors.go b/common/herrors/errors.go deleted file mode 100644 index c7ee90dd0..000000000 --- a/common/herrors/errors.go +++ /dev/null @@ -1,187 +0,0 @@ -// 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 herrors contains common Hugo errors and error related utilities. -package herrors - -import ( - "errors" - "fmt" - "io" - "os" - "regexp" - "runtime" - "runtime/debug" - "strings" - "time" -) - -// PrintStackTrace prints the current stacktrace to w. -func PrintStackTrace(w io.Writer) { - buf := make([]byte, 1<<16) - runtime.Stack(buf, true) - fmt.Fprintf(w, "%s", buf) -} - -// ErrorSender is a, typically, non-blocking error handler. -type ErrorSender interface { - SendError(err error) -} - -// Recover is a helper function that can be used to capture panics. -// Put this at the top of a method/function that crashes in a template: -// -// defer herrors.Recover() -func Recover(args ...any) { - if r := recover(); r != nil { - fmt.Println("ERR:", r) - args = append(args, "stacktrace from panic: \n"+string(debug.Stack()), "\n") - fmt.Println(args...) - } -} - -// 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 = &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) { - if err != nil { - panic(err) - } -} - -// IsNotExist returns true if the error is a file not found error. -// Unlike os.IsNotExist, this also considers wrapped errors. -func IsNotExist(err error) bool { - if os.IsNotExist(err) { - return true - } - - // os.IsNotExist does not consider wrapped errors. - if os.IsNotExist(errors.Unwrap(err)) { - return true - } - - 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 deleted file mode 100644 index 2f53a1e89..000000000 --- a/common/herrors/errors_test.go +++ /dev/null @@ -1,45 +0,0 @@ -// 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 herrors - -import ( - "errors" - "fmt" - "testing" - - qt "github.com/frankban/quicktest" - "github.com/spf13/afero" -) - -func TestIsNotExist(t *testing.T) { - c := qt.New(t) - - c.Assert(IsNotExist(afero.ErrFileNotFound), qt.Equals, true) - c.Assert(IsNotExist(afero.ErrFileExists), qt.Equals, false) - c.Assert(IsNotExist(afero.ErrDestinationExists), qt.Equals, false) - c.Assert(IsNotExist(nil), qt.Equals, false) - - c.Assert(IsNotExist(fmt.Errorf("foo")), qt.Equals, false) - - // 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 deleted file mode 100644 index 38b198656..000000000 --- a/common/herrors/file_error.go +++ /dev/null @@ -1,430 +0,0 @@ -// 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 lfmtaw 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 herrors - -import ( - "encoding/json" - "errors" - "fmt" - "io" - "path/filepath" - - "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" -) - -// FileError represents an error when handling a file: Parsing a config file, -// execute a template etc. -type FileError interface { - error - - // ErrorContext holds some context information about the error. - ErrorContext() *ErrorContext - - text.Positioner - - // UpdatePosition updates the position of the error. - UpdatePosition(pos text.Position) FileError - - // 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. -type Unwrapper interface { - Unwrap() error -} - -var ( - _ FileError = (*fileError)(nil) - _ 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 == "" { - _, fe.fileType = paths.FileAndExtNoDelimiter(filepath.Clean(pos.Filename)) - } - if pos.Filename == "" { - pos.Filename = oldFilename - } - fe.position = pos - return fe -} - -func (fe *fileError) UpdateContent(r io.Reader, linematcher LineMatcherFn) FileError { - if linematcher == nil { - linematcher = SimpleLineMatcher - } - - var ( - posle = fe.position - ectx *ErrorContext - ) - - if posle.LineNumber <= 1 && posle.Offset > 0 { - // Try to locate the line number from the content if offset is set. - ectx = locateError(r, fe, func(m LineMatcher) int { - if posle.Offset >= m.Offset && posle.Offset < m.Offset+len(m.Line) { - lno := posle.LineNumber - m.Position.LineNumber + m.LineNumber - m.Position = text.Position{LineNumber: lno} - return linematcher(m) - } - return -1 - }) - } else { - ectx = locateError(r, fe, linematcher) - } - - if ectx.ChromaLexer == "" { - if fe.fileType != "" { - ectx.ChromaLexer = chromaLexerFromType(fe.fileType) - } else { - ectx.ChromaLexer = chromaLexerFromFilename(fe.Position().Filename) - } - } - - fe.errorContext = ectx - - if ectx.Position.LineNumber > 0 { - fe.position.LineNumber = ectx.Position.LineNumber - } - - if ectx.Position.ColumnNumber > 0 { - fe.position.ColumnNumber = ectx.Position.ColumnNumber - } - - return fe -} - -type fileError struct { - position text.Position - errorContext *ErrorContext - - fileType string - - cause error -} - -func (e *fileError) ErrorContext() *ErrorContext { - return e.errorContext -} - -// Position returns the text position of this error. -func (e fileError) Position() text.Position { - return e.position -} - -func (e *fileError) Error() string { - return fmt.Sprintf("%s: %s", e.position, e.causeString()) -} - -func (e *fileError) causeString() string { - if e.cause == nil { - return "" - } - switch v := e.cause.(type) { - // Avoid repeating the file info in the error message. - case godartsass.SassError: - return v.Message - case libsasserrors.Error: - return v.Message - default: - return v.Error() - } -} - -func (e *fileError) Unwrap() error { - return e.cause -} - -// NewFileError creates a new FileError that wraps err. -// It will try to extract the filename and line number from err. -func NewFileError(err error) FileError { - // Filetype is used to determine the Chroma lexer to use. - fileType, pos := extractFileTypePos(err) - return &fileError{cause: err, fileType: fileType, position: pos} -} - -// NewFileErrorFromName creates a new FileError that wraps err. -// The value for name should identify the file, the best -// being the full filename to the file on disk. -func NewFileErrorFromName(err error, name string) FileError { - // Filetype is used to determine the Chroma lexer to use. - fileType, pos := extractFileTypePos(err) - pos.Filename = name - if fileType == "" { - _, fileType = paths.FileAndExtNoDelimiter(filepath.Clean(name)) - } - - 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. -func NewFileErrorFromPos(err error, pos text.Position) FileError { - // Filetype is used to determine the Chroma lexer to use. - fileType, _ := extractFileTypePos(err) - if fileType == "" { - _, 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 { - fe := NewFileError(err) - pos := fe.Position() - if pos.Filename == "" { - return fe - } - - f, realFilename, err2 := openFile(pos.Filename, fs) - if err2 != nil { - return fe - } - - pos.Filename = realFilename - defer f.Close() - return fe.UpdateContent(f, linematcher) -} - -func NewFileErrorFromFileInPos(err error, pos text.Position, fs afero.Fs, linematcher LineMatcherFn) FileError { - if err == nil { - panic("err is nil") - } - f, realFilename, err2 := openFile(pos.Filename, fs) - if err2 != nil { - return NewFileErrorFromPos(err, pos) - } - pos.Filename = realFilename - defer f.Close() - return NewFileErrorFromPos(err, pos).UpdateContent(f, linematcher) -} - -// NewFileErrorFromFile is a convenience method to create a new FileError from a file. -func NewFileErrorFromFile(err error, filename string, fs afero.Fs, linematcher LineMatcherFn) FileError { - if err == nil { - panic("err is nil") - } - f, realFilename, err2 := openFile(filename, fs) - if err2 != nil { - return NewFileErrorFromName(err, realFilename) - } - defer f.Close() - return NewFileErrorFromName(err, realFilename).UpdateContent(f, linematcher) -} - -func openFile(filename string, fs afero.Fs) (afero.File, string, error) { - realFilename := filename - - // We want the most specific filename possible in the error message. - fi, err2 := fs.Stat(filename) - if err2 == nil { - if s, ok := fi.(interface { - Filename() string - }); ok { - realFilename = s.Filename() - } - } - - f, err2 := fs.Open(filename) - if err2 != nil { - return nil, realFilename, err2 - } - - return f, realFilename, nil -} - -// 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 - } - return err -} - -func extractFileTypePos(err error) (string, text.Position) { - err = Unwrap(err) - - var fileType string - - // LibSass, DartSass - if pos := extractPosition(err); pos.LineNumber > 0 || pos.Offset > 0 { - _, fileType = paths.FileAndExtNoDelimiter(pos.Filename) - return fileType, pos - } - - // Default to line 1 col 1 if we don't find any better. - pos := text.Position{ - Offset: -1, - LineNumber: 1, - ColumnNumber: 1, - } - - // JSON errors. - offset, typ := extractOffsetAndType(err) - if fileType == "" { - fileType = typ - } - - if offset >= 0 { - pos.Offset = offset - } - - // The error type from the minifier contains line number and column number. - if line, col := extractLineNumberAndColumnNumber(err); line >= 0 { - pos.LineNumber = line - pos.ColumnNumber = col - return fileType, pos - } - - // Look in the error message for the line number. - for _, handle := range lineNumberExtractors { - lno, col := handle(err) - if lno > 0 { - pos.ColumnNumber = col - pos.LineNumber = lno - break - } - } - - if fileType == "" && pos.Filename != "" { - _, fileType = paths.FileAndExtNoDelimiter(pos.Filename) - } - - return fileType, pos -} - -// UnwrapFileError tries to unwrap a FileError from err. -// It returns nil if this is not possible. -func UnwrapFileError(err error) FileError { - for err != nil { - switch v := err.(type) { - case FileError: - return v - default: - err = errors.Unwrap(err) - } - } - return nil -} - -// UnwrapFileErrors tries to unwrap all FileError. -func UnwrapFileErrors(err error) []FileError { - var errs []FileError - for err != nil { - if v, ok := err.(FileError); ok { - errs = append(errs, v) - } - err = errors.Unwrap(err) - } - return errs -} - -// UnwrapFileErrorsWithErrorContext tries to unwrap all FileError in err that has an ErrorContext. -func UnwrapFileErrorsWithErrorContext(err error) []FileError { - var errs []FileError - for err != nil { - if v, ok := err.(FileError); ok && v.ErrorContext() != nil { - errs = append(errs, v) - } - err = errors.Unwrap(err) - } - return errs -} - -func extractOffsetAndType(e error) (int, string) { - switch v := e.(type) { - case *json.UnmarshalTypeError: - return int(v.Offset), "json" - case *json.SyntaxError: - return int(v.Offset), "json" - default: - return -1, "" - } -} - -func extractLineNumberAndColumnNumber(e error) (int, int) { - switch v := e.(type) { - case *parse.Error: - return v.Line, v.Column - case *toml.DecodeError: - return v.Position() - - } - - return -1, -1 -} - -func extractPosition(e error) (pos text.Position) { - switch v := e.(type) { - case godartsass.SassError: - span := v.Span - start := span.Start - filename, _ := paths.UrlStringToFilename(span.Url) - pos.Filename = filename - pos.Offset = start.Offset - pos.ColumnNumber = start.Column - case libsasserrors.Error: - pos.Filename = v.File - pos.LineNumber = v.Line - pos.ColumnNumber = v.Column - } - return -} - -// TextSegmentError is an error with a text segment attached. -type TextSegmentError struct { - Segment string - Err error -} - -func (e TextSegmentError) Unwrap() error { - return e.Err -} - -func (e TextSegmentError) Error() string { - return e.Err.Error() -} diff --git a/common/herrors/file_error_test.go b/common/herrors/file_error_test.go deleted file mode 100644 index 7aca08405..000000000 --- a/common/herrors/file_error_test.go +++ /dev/null @@ -1,80 +0,0 @@ -// 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 herrors - -import ( - "errors" - "fmt" - "strings" - "testing" - - "github.com/gohugoio/hugo/common/text" - - qt "github.com/frankban/quicktest" -) - -func TestNewFileError(t *testing.T) { - t.Parallel() - - c := qt.New(t) - - fe := NewFileErrorFromName(errors.New("bar"), "foo.html") - c.Assert(fe.Error(), qt.Equals, `"foo.html:1:1": bar`) - - lines := "" - for i := 1; i <= 100; i++ { - lines += fmt.Sprintf("line %d\n", i) - } - - fe.UpdatePosition(text.Position{LineNumber: 32, ColumnNumber: 2}) - c.Assert(fe.Error(), qt.Equals, `"foo.html:32:2": bar`) - fe.UpdatePosition(text.Position{LineNumber: 0, ColumnNumber: 0, Offset: 212}) - fe.UpdateContent(strings.NewReader(lines), nil) - c.Assert(fe.Error(), qt.Equals, `"foo.html:32:0": bar`) - errorContext := fe.ErrorContext() - c.Assert(errorContext, qt.IsNotNil) - 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) { - t.Parallel() - - c := qt.New(t) - - for i, test := range []struct { - in error - offset int - lineNumber int - columnNumber int - }{ - {errors.New("no line number for you"), 0, 1, 1}, - {errors.New(`template: _default/single.html:4:15: executing "_default/single.html" at <.Titles>: can't evaluate field Titles in type *hugolib.PageOutput`), 0, 4, 15}, - {errors.New("parse failed: template: _default/bundle-resource-meta.html:11: unexpected in operand"), 0, 11, 1}, - {errors.New(`failed:: template: _default/bundle-resource-meta.html:2:7: executing "main" at <.Titles>`), 0, 2, 7}, - {errors.New(`failed to load translations: (6, 7): was expecting token =, but got "g" instead`), 0, 6, 7}, - {errors.New(`execute of template failed: template: index.html:2:5: executing "index.html" at : error calling partial: "/layouts/partials/foo.html:3:6": execute of template failed: template: partials/foo.html:3:6: executing "partials/foo.html" at <.ThisDoesNotExist>: can't evaluate field ThisDoesNotExist in type *hugolib.pageStat`), 0, 2, 5}, - } { - - got := NewFileErrorFromName(test.in, "test.txt") - - errMsg := qt.Commentf("[%d][%T]", i, got) - - pos := got.Position() - c.Assert(pos.LineNumber, qt.Equals, test.lineNumber, errMsg) - c.Assert(pos.ColumnNumber, qt.Equals, test.columnNumber, errMsg) - c.Assert(errors.Unwrap(got), qt.Not(qt.IsNil)) - } -} diff --git a/common/herrors/line_number_extractors.go b/common/herrors/line_number_extractors.go deleted file mode 100644 index f70a2691f..000000000 --- a/common/herrors/line_number_extractors.go +++ /dev/null @@ -1,63 +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 herrors - -import ( - "regexp" - "strconv" -) - -var lineNumberExtractors = []lineNumberExtractor{ - // Template/shortcode parse errors - newLineNumberErrHandlerFromRegexp(`:(\d+):(\d*):`), - newLineNumberErrHandlerFromRegexp(`:(\d+):`), - - // YAML parse errors - newLineNumberErrHandlerFromRegexp(`line (\d+):`), - - // i18n bundle errors - newLineNumberErrHandlerFromRegexp(`\((\d+),\s(\d*)`), -} - -type lineNumberExtractor func(e error) (int, int) - -func newLineNumberErrHandlerFromRegexp(expression string) lineNumberExtractor { - re := regexp.MustCompile(expression) - return extractLineNo(re) -} - -func extractLineNo(re *regexp.Regexp) lineNumberExtractor { - return func(e error) (int, int) { - if e == nil { - panic("no error") - } - col := 1 - s := e.Error() - m := re.FindStringSubmatch(s) - if len(m) >= 2 { - lno, _ := strconv.Atoi(m[1]) - if len(m) > 2 { - col, _ = strconv.Atoi(m[2]) - } - - if col <= 0 { - col = 1 - } - - return lno, col - } - - return 0, col - } -} diff --git a/common/hexec/exec.go b/common/hexec/exec.go deleted file mode 100644 index c3a6ebf57..000000000 --- a/common/hexec/exec.go +++ /dev/null @@ -1,389 +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 hexec - -import ( - "bytes" - "context" - "errors" - "fmt" - "io" - "os" - "os/exec" - "path/filepath" - "regexp" - "strings" - "sync" - - "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" -) - -var WithDir = func(dir string) func(c *commandeer) { - return func(c *commandeer) { - c.dir = dir - } -} - -var WithContext = func(ctx context.Context) func(c *commandeer) { - return func(c *commandeer) { - c.ctx = ctx - } -} - -var WithStdout = func(w io.Writer) func(c *commandeer) { - return func(c *commandeer) { - c.stdout = w - } -} - -var WithStderr = func(w io.Writer) func(c *commandeer) { - return func(c *commandeer) { - c.stderr = w - } -} - -var WithStdin = func(r io.Reader) func(c *commandeer) { - return func(c *commandeer) { - c.stdin = r - } -} - -var WithEnviron = func(env []string) func(c *commandeer) { - return func(c *commandeer) { - setOrAppend := func(s string) { - k1, _ := config.SplitEnvVar(s) - var found bool - for i, v := range c.env { - k2, _ := config.SplitEnvVar(v) - if k1 == k2 { - found = true - c.env[i] = s - } - } - - if !found { - c.env = append(c.env, s) - } - } - - for _, s := range env { - setOrAppend(s) - } - } -} - -// New creates a new Exec using the provided security config. -func New(cfg security.Config, workingDir string, log loggers.Logger) *Exec { - var baseEnviron []string - for _, v := range os.Environ() { - k, _ := config.SplitEnvVar(v) - if cfg.Exec.OsEnv.Accept(k) { - baseEnviron = append(baseEnviron, v) - } - } - - return &Exec{ - sc: cfg, - workingDir: workingDir, - infol: log.InfoCommand("exec"), - baseEnviron: baseEnviron, - newNPXRunnerCache: maps.NewCache[string, func(arg ...any) (Runner, error)](), - } -} - -// IsNotFound reports whether this is an error about a binary not found. -func IsNotFound(err error) bool { - var notFoundErr *NotFoundError - return errors.As(err, ¬FoundErr) -} - -// Exec enforces a security policy for commands run via os/exec. -type Exec struct { - 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, fullyQualifiedName string, arg ...any) (Runner, error) { - if err := e.sc.CheckAllowedExec(name); err != nil { - return nil, err - } - - env := make([]string, len(e.baseEnviron)) - copy(env, e.baseEnviron) - - cm := &commandeer{ - name: name, - fullyQualifiedName: fullyQualifiedName, - env: env, - } - - return cm.command(arg...) -} - -type binaryLocation int - -func (b binaryLocation) String() string { - switch b { - case binaryLocationNodeModules: - return "node_modules/.bin" - case binaryLocationNpx: - return "npx" - case binaryLocationPath: - return "PATH" - } - return "unknown" -} - -const ( - binaryLocationNodeModules binaryLocation = iota + 1 - binaryLocationNpx - binaryLocationPath -) - -// Npx will in order: -// 1. Try fo find the binary in the WORKINGDIR/node_modules/.bin directory. -// 2. If not found, and npx is available, run 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) { - 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 0 - case reflect.Bool: - truth = val.Bool() - case reflect.Complex64, reflect.Complex128: - truth = val.Complex() != 0 - case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Interface: - truth = !val.IsNil() - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - truth = val.Int() != 0 - case reflect.Float32, reflect.Float64: - truth = val.Float() != 0 - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - truth = val.Uint() != 0 - case reflect.Struct: - truth = true // Struct values are always true. - default: - return - } - - return -} - -type methodKey struct { - typ reflect.Type - name string -} - -var methodCache sync.Map - -// GetMethodByName is the same as reflect.Value.MethodByName, but it caches the -// type lookup. -func GetMethodByName(v reflect.Value, name string) reflect.Value { - index := GetMethodIndexByName(v.Type(), name) - - if index == -1 { - return reflect.Value{} - } - - return v.Method(index) -} - -// GetMethodIndexByName returns the index of the method with the given name, or -// -1 if no such method exists. -func GetMethodIndexByName(tp reflect.Type, name string) int { - k := methodKey{tp, name} - v, found := methodCache.Load(k) - if found { - return v.(int) - } - m, ok := tp.MethodByName(name) - index := m.Index - if !ok { - index = -1 - } - methodCache.Store(k, index) - - if !ok { - return -1 - } - - return m.Index -} - -var ( - timeType = reflect.TypeOf((*time.Time)(nil)).Elem() - asTimeProviderType = reflect.TypeOf((*htime.AsTimeProvider)(nil)).Elem() -) - -// IsTime returns whether tp is a time.Time type or if it can be converted into one -// in ToTime. -func IsTime(tp reflect.Type) bool { - if tp == timeType { - return true - } - - if tp.Implements(asTimeProviderType) { - return true - } - return false -} - -// IsValid returns whether v is not nil and a valid value. -func IsValid(v reflect.Value) bool { - if !v.IsValid() { - return false - } - - switch v.Kind() { - case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: - return !v.IsNil() - } - - return true -} - -// AsTime returns v as a time.Time if possible. -// The given location is only used if the value implements AsTimeProvider (e.g. go-toml local). -// A zero Time and false is returned if this isn't possible. -// Note that this function does not accept string dates. -func AsTime(v reflect.Value, loc *time.Location) (time.Time, bool) { - if v.Kind() == reflect.Interface { - return AsTime(v.Elem(), loc) - } - - if v.Type() == timeType { - return v.Interface().(time.Time), true - } - - if v.Type().Implements(asTimeProviderType) { - return v.Interface().(htime.AsTimeProvider).AsTime(loc), true - } - - return time.Time{}, false -} - -// ToSliceAny converts the given value to a slice of any if possible. -func ToSliceAny(v any) ([]any, bool) { - if v == nil { - return nil, false - } - switch vv := v.(type) { - case []any: - return vv, true - default: - vvv := reflect.ValueOf(v) - if vvv.Kind() == reflect.Slice { - out := make([]any, vvv.Len()) - for i := range vvv.Len() { - out[i] = vvv.Index(i).Interface() - } - return out, true - } - } - return nil, false -} - -func CallMethodByName(cxt context.Context, name string, v reflect.Value) []reflect.Value { - fn := v.MethodByName(name) - var args []reflect.Value - tp := fn.Type() - if tp.NumIn() > 0 { - if tp.NumIn() > 1 { - panic("not supported") - } - first := tp.In(0) - if IsContextType(first) { - args = append(args, reflect.ValueOf(cxt)) - } - } - - return fn.Call(args) -} - -// Based on: https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L931 -func indirectInterface(v reflect.Value) reflect.Value { - if v.Kind() != reflect.Interface { - return v - } - if v.IsNil() { - return reflect.Value{} - } - return v.Elem() -} - -var contextInterface = reflect.TypeOf((*context.Context)(nil)).Elem() - -var isContextCache = maps.NewCache[reflect.Type, bool]() - -type k string - -var contextTypeValue = reflect.TypeOf(context.WithValue(context.Background(), k("key"), 32)) - -// IsContextType returns whether tp is a context.Context type. -func IsContextType(tp reflect.Type) bool { - if tp == contextTypeValue { - return true - } - if tp == contextInterface { - return true - } - - isContext, _ := isContextCache.GetOrCreate(tp, func() (bool, error) { - return tp.Implements(contextInterface), nil - }) - return isContext -} diff --git a/common/hreflect/helpers_test.go b/common/hreflect/helpers_test.go deleted file mode 100644 index cbcad0f22..000000000 --- a/common/hreflect/helpers_test.go +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright 2019 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 hreflect - -import ( - "context" - "reflect" - "testing" - "time" - - qt "github.com/frankban/quicktest" -) - -func TestIsTruthful(t *testing.T) { - c := qt.New(t) - - c.Assert(IsTruthful(true), qt.Equals, true) - c.Assert(IsTruthful(false), qt.Equals, false) - c.Assert(IsTruthful(time.Now()), qt.Equals, true) - c.Assert(IsTruthful(time.Time{}), qt.Equals, false) -} - -func TestGetMethodByName(t *testing.T) { - c := qt.New(t) - v := reflect.ValueOf(&testStruct{}) - tp := v.Type() - - c.Assert(GetMethodIndexByName(tp, "Method1"), qt.Equals, 0) - c.Assert(GetMethodIndexByName(tp, "Method3"), qt.Equals, 2) - c.Assert(GetMethodIndexByName(tp, "Foo"), qt.Equals, -1) -} - -func TestIsContextType(t *testing.T) { - c := qt.New(t) - type k string - ctx := context.Background() - valueCtx := context.WithValue(ctx, k("key"), 32) - c.Assert(IsContextType(reflect.TypeOf(ctx)), qt.IsTrue) - c.Assert(IsContextType(reflect.TypeOf(valueCtx)), qt.IsTrue) -} - -func TestToSliceAny(t *testing.T) { - c := qt.New(t) - - checkOK := func(in any, expected []any) { - out, ok := ToSliceAny(in) - c.Assert(ok, qt.Equals, true) - c.Assert(out, qt.DeepEquals, expected) - } - - checkOK([]any{1, 2, 3}, []any{1, 2, 3}) - checkOK([]int{1, 2, 3}, []any{1, 2, 3}) -} - -func BenchmarkIsContextType(b *testing.B) { - type k string - b.Run("value", func(b *testing.B) { - ctx := context.Background() - ctxs := make([]reflect.Type, b.N) - for i := 0; i < b.N; i++ { - ctxs[i] = reflect.TypeOf(context.WithValue(ctx, k("key"), i)) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - if !IsContextType(ctxs[i]) { - b.Fatal("not context") - } - } - }) - - b.Run("background", func(b *testing.B) { - var ctxt reflect.Type = reflect.TypeOf(context.Background()) - for i := 0; i < b.N; i++ { - if !IsContextType(ctxt) { - b.Fatal("not context") - } - } - }) -} - -func BenchmarkIsTruthFul(b *testing.B) { - v := reflect.ValueOf("Hugo") - - b.ResetTimer() - for i := 0; i < b.N; i++ { - if !IsTruthfulValue(v) { - b.Fatal("not truthful") - } - } -} - -type testStruct struct{} - -func (t *testStruct) Method1() string { - return "Hugo" -} - -func (t *testStruct) Method2() string { - return "Hugo" -} - -func (t *testStruct) Method3() string { - return "Hugo" -} - -func (t *testStruct) Method4() string { - return "Hugo" -} - -func (t *testStruct) Method5() string { - return "Hugo" -} - -func BenchmarkGetMethodByName(b *testing.B) { - v := reflect.ValueOf(&testStruct{}) - methods := []string{"Method1", "Method2", "Method3", "Method4", "Method5"} - - b.ResetTimer() - for i := 0; i < b.N; i++ { - for _, method := range methods { - _ = GetMethodByName(v, method) - } - } -} - -func BenchmarkGetMethodByNamePara(b *testing.B) { - v := reflect.ValueOf(&testStruct{}) - methods := []string{"Method1", "Method2", "Method3", "Method4", "Method5"} - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - for _, method := range methods { - _ = GetMethodByName(v, method) - } - } - }) -} diff --git a/common/hstrings/strings.go b/common/hstrings/strings.go deleted file mode 100644 index 1de38678f..000000000 --- a/common/hstrings/strings.go +++ /dev/null @@ -1,134 +0,0 @@ -// 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 hstrings - -import ( - "fmt" - "regexp" - "slices" - "strings" - "sync" - - "github.com/gohugoio/hugo/compare" -) - -var _ compare.Eqer = StringEqualFold("") - -// StringEqualFold is a string that implements the compare.Eqer interface and considers -// two strings equal if they are equal when folded to lower case. -// The compare.Eqer interface is used in Hugo to compare values in templates (e.g. using the eq template function). -type StringEqualFold string - -func (s StringEqualFold) EqualFold(s2 string) bool { - return strings.EqualFold(string(s), s2) -} - -func (s StringEqualFold) String() string { - return string(s) -} - -func (s StringEqualFold) Eq(s2 any) bool { - switch ss := s2.(type) { - case string: - return s.EqualFold(ss) - case fmt.Stringer: - return s.EqualFold(ss.String()) - } - - return false -} - -// EqualAny returns whether a string is equal to any of the given strings. -func EqualAny(a string, b ...string) bool { - return slices.Contains(b, a) -} - -// regexpCache represents a cache of regexp objects protected by a mutex. -type regexpCache struct { - mu sync.RWMutex - re map[string]*regexp.Regexp -} - -func (rc *regexpCache) getOrCompileRegexp(pattern string) (re *regexp.Regexp, err error) { - var ok bool - - if re, ok = rc.get(pattern); !ok { - re, err = regexp.Compile(pattern) - if err != nil { - return nil, err - } - rc.set(pattern, re) - } - - return re, nil -} - -func (rc *regexpCache) get(key string) (re *regexp.Regexp, ok bool) { - rc.mu.RLock() - re, ok = rc.re[key] - rc.mu.RUnlock() - return -} - -func (rc *regexpCache) set(key string, re *regexp.Regexp) { - rc.mu.Lock() - rc.re[key] = re - rc.mu.Unlock() -} - -var reCache = regexpCache{re: make(map[string]*regexp.Regexp)} - -// GetOrCompileRegexp retrieves a regexp object from the cache based upon the pattern. -// If the pattern is not found in the cache, the pattern is compiled and added to -// the cache. -func GetOrCompileRegexp(pattern string) (re *regexp.Regexp, err error) { - return reCache.getOrCompileRegexp(pattern) -} - -// InSlice checks if a string is an element of a slice of strings -// and returns a boolean value. -func InSlice(arr []string, el string) bool { - return slices.Contains(arr, el) -} - -// InSlicEqualFold checks if a string is an element of a slice of strings -// and returns a boolean value. -// It uses strings.EqualFold to compare. -func InSlicEqualFold(arr []string, el string) bool { - for _, v := range arr { - if strings.EqualFold(v, el) { - return true - } - } - return false -} - -// ToString converts the given value to a string. -// Note that this is a more strict version compared to cast.ToString, -// as it will not try to convert numeric values to strings, -// but only accept strings or fmt.Stringer. -func ToString(v any) (string, bool) { - switch vv := v.(type) { - case string: - return vv, true - case fmt.Stringer: - return vv.String(), true - } - return "", false -} - -type ( - Strings2 [2]string - Strings3 [3]string -) diff --git a/common/hstrings/strings_test.go b/common/hstrings/strings_test.go deleted file mode 100644 index d8e9e204a..000000000 --- a/common/hstrings/strings_test.go +++ /dev/null @@ -1,56 +0,0 @@ -// 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 hstrings - -import ( - "regexp" - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestStringEqualFold(t *testing.T) { - c := qt.New(t) - - s1 := "A" - s2 := "a" - - c.Assert(StringEqualFold(s1).EqualFold(s2), qt.Equals, true) - c.Assert(StringEqualFold(s1).EqualFold(s1), qt.Equals, true) - c.Assert(StringEqualFold(s2).EqualFold(s1), qt.Equals, true) - c.Assert(StringEqualFold(s2).EqualFold(s2), qt.Equals, true) - c.Assert(StringEqualFold(s1).EqualFold("b"), qt.Equals, false) - c.Assert(StringEqualFold(s1).Eq(s2), qt.Equals, true) - c.Assert(StringEqualFold(s1).Eq("b"), qt.Equals, false) -} - -func TestGetOrCompileRegexp(t *testing.T) { - c := qt.New(t) - - re, err := GetOrCompileRegexp(`\d+`) - c.Assert(err, qt.IsNil) - c.Assert(re.MatchString("123"), qt.Equals, true) -} - -func BenchmarkGetOrCompileRegexp(b *testing.B) { - for i := 0; i < b.N; i++ { - GetOrCompileRegexp(`\d+`) - } -} - -func BenchmarkCompileRegexp(b *testing.B) { - for i := 0; i < b.N; i++ { - regexp.MustCompile(`\d+`) - } -} diff --git a/common/htime/htime_integration_test.go b/common/htime/htime_integration_test.go deleted file mode 100644 index 8090add12..000000000 --- a/common/htime/htime_integration_test.go +++ /dev/null @@ -1,78 +0,0 @@ -// 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 htime_test - -import ( - "testing" - - "github.com/gohugoio/hugo/hugolib" -) - -// Issue #11267 -func TestApplyWithContext(t *testing.T) { - t.Parallel() - - files := ` --- config.toml -- -defaultContentLanguage = 'it' --- layouts/index.html -- -{{ $dates := slice - "2022-01-03" - "2022-02-01" - "2022-03-02" - "2022-04-07" - "2022-05-06" - "2022-06-04" - "2022-07-03" - "2022-08-01" - "2022-09-06" - "2022-10-05" - "2022-11-03" - "2022-12-02" -}} -{{ range $dates }} - {{ . | time.Format "month: _January_ weekday: _Monday_" }} - {{ . | time.Format "month: _Jan_ weekday: _Mon_" }} -{{ end }} - ` - - b := hugolib.Test(t, files) - - b.AssertFileContent("public/index.html", ` -month: _gennaio_ weekday: _lunedì_ -month: _gen_ weekday: _lun_ -month: _febbraio_ weekday: _martedì_ -month: _feb_ weekday: _mar_ -month: _marzo_ weekday: _mercoledì_ -month: _mar_ weekday: _mer_ -month: _aprile_ weekday: _giovedì_ -month: _apr_ weekday: _gio_ -month: _maggio_ weekday: _venerdì_ -month: _mag_ weekday: _ven_ -month: _giugno_ weekday: _sabato_ -month: _giu_ weekday: _sab_ -month: _luglio_ weekday: _domenica_ -month: _lug_ weekday: _dom_ -month: _agosto_ weekday: _lunedì_ -month: _ago_ weekday: _lun_ -month: _settembre_ weekday: _martedì_ -month: _set_ weekday: _mar_ -month: _ottobre_ weekday: _mercoledì_ -month: _ott_ weekday: _mer_ -month: _novembre_ weekday: _giovedì_ -month: _nov_ weekday: _gio_ -month: _dicembre_ weekday: _venerdì_ -month: _dic_ weekday: _ven_ -`) -} diff --git a/common/htime/time.go b/common/htime/time.go deleted file mode 100644 index c71e39ee4..000000000 --- a/common/htime/time.go +++ /dev/null @@ -1,177 +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 htime - -import ( - "log" - "strings" - "time" - - "github.com/bep/clocks" - "github.com/spf13/cast" - - "github.com/gohugoio/locales" -) - -var ( - longDayNames = []string{ - "Sunday", - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday", - } - - shortDayNames = []string{ - "Sun", - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - } - - shortMonthNames = []string{ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", - } - - longMonthNames = []string{ - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December", - } - - Clock = clocks.System() -) - -func NewTimeFormatter(ltr locales.Translator) TimeFormatter { - if ltr == nil { - panic("must provide a locales.Translator") - } - return TimeFormatter{ - ltr: ltr, - } -} - -// TimeFormatter is locale aware. -type TimeFormatter struct { - ltr locales.Translator -} - -func (f TimeFormatter) Format(t time.Time, layout string) string { - if layout == "" { - return "" - } - - if layout[0] == ':' { - // It may be one of Hugo's custom layouts. - switch strings.ToLower(layout[1:]) { - case "date_full": - return f.ltr.FmtDateFull(t) - case "date_long": - return f.ltr.FmtDateLong(t) - case "date_medium": - return f.ltr.FmtDateMedium(t) - case "date_short": - return f.ltr.FmtDateShort(t) - case "time_full": - return f.ltr.FmtTimeFull(t) - case "time_long": - return f.ltr.FmtTimeLong(t) - case "time_medium": - return f.ltr.FmtTimeMedium(t) - case "time_short": - return f.ltr.FmtTimeShort(t) - } - } - - s := t.Format(layout) - - monthIdx := t.Month() - 1 // Month() starts at 1. - dayIdx := t.Weekday() - - if strings.Contains(layout, "January") { - s = strings.ReplaceAll(s, longMonthNames[monthIdx], f.ltr.MonthWide(t.Month())) - } else if strings.Contains(layout, "Jan") { - s = strings.ReplaceAll(s, shortMonthNames[monthIdx], f.ltr.MonthAbbreviated(t.Month())) - } - - if strings.Contains(layout, "Monday") { - s = strings.ReplaceAll(s, longDayNames[dayIdx], f.ltr.WeekdayWide(t.Weekday())) - } else if strings.Contains(layout, "Mon") { - s = strings.ReplaceAll(s, shortDayNames[dayIdx], f.ltr.WeekdayAbbreviated(t.Weekday())) - } - - return s -} - -func ToTimeInDefaultLocationE(i any, location *time.Location) (tim time.Time, err error) { - switch vv := i.(type) { - case AsTimeProvider: - return vv.AsTime(location), nil - // issue #8895 - // datetimes parsed by `go-toml` have empty zone name - // convert back them into string and use `cast` - // TODO(bep) add tests, make sure we really need this. - case time.Time: - i = vv.Format(time.RFC3339) - } - return cast.ToTimeInDefaultLocationE(i, location) -} - -// Now returns time.Now() or time value based on the `clock` flag. -// Use this function to fake time inside hugo. -func Now() time.Time { - return Clock.Now() -} - -func Since(t time.Time) time.Duration { - return Clock.Since(t) -} - -// AsTimeProvider is implemented by go-toml's LocalDate and LocalDateTime. -type AsTimeProvider interface { - AsTime(zone *time.Location) time.Time -} - -// StopWatch is a simple helper to measure time during development. -func StopWatch(name string) func() { - start := time.Now() - return func() { - log.Printf("StopWatch %q took %s", name, time.Since(start)) - } -} diff --git a/common/htime/time_test.go b/common/htime/time_test.go deleted file mode 100644 index 78954887e..000000000 --- a/common/htime/time_test.go +++ /dev/null @@ -1,144 +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 htime - -import ( - "testing" - "time" - - qt "github.com/frankban/quicktest" - translators "github.com/gohugoio/localescompressed" -) - -func TestTimeFormatter(t *testing.T) { - c := qt.New(t) - - june06, _ := time.Parse("2006-Jan-02", "2018-Jun-06") - june06 = june06.Add(7777 * time.Second) - - jan06, _ := time.Parse("2006-Jan-02", "2018-Jan-06") - jan06 = jan06.Add(32 * time.Second) - - mondayNovemberFirst, _ := time.Parse("2006-Jan-02", "2021-11-01") - mondayNovemberFirst = mondayNovemberFirst.Add(33 * time.Second) - - c.Run("Norsk nynorsk", func(c *qt.C) { - f := NewTimeFormatter(translators.GetTranslator("nn")) - - c.Assert(f.Format(june06, "Monday Jan 2 2006"), qt.Equals, "onsdag juni 6 2018") - c.Assert(f.Format(june06, "Mon January 2 2006"), qt.Equals, "on. juni 6 2018") - c.Assert(f.Format(june06, "Mon Mon"), qt.Equals, "on. on.") - }) - - c.Run("Custom layouts Norsk nynorsk", func(c *qt.C) { - f := NewTimeFormatter(translators.GetTranslator("nn")) - - c.Assert(f.Format(june06, ":date_full"), qt.Equals, "onsdag 6. juni 2018") - c.Assert(f.Format(june06, ":date_long"), qt.Equals, "6. juni 2018") - c.Assert(f.Format(june06, ":date_medium"), qt.Equals, "6. juni 2018") - c.Assert(f.Format(june06, ":date_short"), qt.Equals, "06.06.2018") - - c.Assert(f.Format(june06, ":time_full"), qt.Equals, "kl. 02:09:37 UTC") - c.Assert(f.Format(june06, ":time_long"), qt.Equals, "02:09:37 UTC") - c.Assert(f.Format(june06, ":time_medium"), qt.Equals, "02:09:37") - c.Assert(f.Format(june06, ":time_short"), qt.Equals, "02:09") - }) - - c.Run("Custom layouts English", func(c *qt.C) { - f := NewTimeFormatter(translators.GetTranslator("en")) - - c.Assert(f.Format(june06, ":date_full"), qt.Equals, "Wednesday, June 6, 2018") - c.Assert(f.Format(june06, ":date_long"), qt.Equals, "June 6, 2018") - c.Assert(f.Format(june06, ":date_medium"), qt.Equals, "Jun 6, 2018") - c.Assert(f.Format(june06, ":date_short"), qt.Equals, "6/6/18") - - c.Assert(f.Format(june06, ":time_full"), qt.Equals, "2:09:37 am UTC") - c.Assert(f.Format(june06, ":time_long"), qt.Equals, "2:09:37 am UTC") - c.Assert(f.Format(june06, ":time_medium"), qt.Equals, "2:09:37 am") - c.Assert(f.Format(june06, ":time_short"), qt.Equals, "2:09 am") - }) - - c.Run("English", func(c *qt.C) { - f := NewTimeFormatter(translators.GetTranslator("en")) - - c.Assert(f.Format(june06, "Monday Jan 2 2006"), qt.Equals, "Wednesday Jun 6 2018") - c.Assert(f.Format(june06, "Mon January 2 2006"), qt.Equals, "Wed June 6 2018") - c.Assert(f.Format(june06, "Mon Mon"), qt.Equals, "Wed Wed") - }) - - c.Run("Weekdays German", func(c *qt.C) { - tr := translators.GetTranslator("de") - f := NewTimeFormatter(tr) - - // Issue #9107 - for i, weekDayWideGerman := range []string{"Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"} { - date := mondayNovemberFirst.Add(time.Duration(i*24) * time.Hour) - c.Assert(tr.WeekdayWide(date.Weekday()), qt.Equals, weekDayWideGerman) - c.Assert(f.Format(date, "Monday"), qt.Equals, weekDayWideGerman) - } - - for i, weekDayAbbreviatedGerman := range []string{"Mo.", "Di.", "Mi.", "Do.", "Fr.", "Sa.", "So."} { - date := mondayNovemberFirst.Add(time.Duration(i*24) * time.Hour) - c.Assert(tr.WeekdayAbbreviated(date.Weekday()), qt.Equals, weekDayAbbreviatedGerman) - c.Assert(f.Format(date, "Mon"), qt.Equals, weekDayAbbreviatedGerman) - } - }) - - c.Run("Months German", func(c *qt.C) { - tr := translators.GetTranslator("de") - f := NewTimeFormatter(tr) - - // Issue #9107 - for i, monthWideNorway := range []string{"Januar", "Februar", "März", "April", "Mai", "Juni", "Juli"} { - date := jan06.Add(time.Duration(i*24*31) * time.Hour) - c.Assert(tr.MonthWide(date.Month()), qt.Equals, monthWideNorway) - c.Assert(f.Format(date, "January"), qt.Equals, monthWideNorway) - } - }) -} - -func BenchmarkTimeFormatter(b *testing.B) { - june06, _ := time.Parse("2006-Jan-02", "2018-Jun-06") - - b.Run("Native", func(b *testing.B) { - for i := 0; i < b.N; i++ { - got := june06.Format("Monday Jan 2 2006") - if got != "Wednesday Jun 6 2018" { - b.Fatalf("invalid format, got %q", got) - } - } - }) - - b.Run("Localized", func(b *testing.B) { - f := NewTimeFormatter(translators.GetTranslator("nn")) - b.ResetTimer() - for i := 0; i < b.N; i++ { - got := f.Format(june06, "Monday Jan 2 2006") - if got != "onsdag juni 6 2018" { - b.Fatalf("invalid format, got %q", got) - } - } - }) - - b.Run("Localized Custom", func(b *testing.B) { - f := NewTimeFormatter(translators.GetTranslator("nn")) - b.ResetTimer() - for i := 0; i < b.N; i++ { - got := f.Format(june06, ":date_medium") - if got != "6. juni 2018" { - b.Fatalf("invalid format, got %q", got) - } - } - }) -} diff --git a/common/hugio/copy.go b/common/hugio/copy.go deleted file mode 100644 index 31d679dfc..000000000 --- a/common/hugio/copy.go +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright 2019 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 hugio - -import ( - "fmt" - "io" - iofs "io/fs" - "path/filepath" - - "github.com/spf13/afero" -) - -// CopyFile copies a file. -func CopyFile(fs afero.Fs, from, to string) error { - sf, err := fs.Open(from) - if err != nil { - return err - } - defer sf.Close() - df, err := fs.Create(to) - if err != nil { - return err - } - defer df.Close() - _, err = io.Copy(df, sf) - if err != nil { - return err - } - si, err := fs.Stat(from) - if err != nil { - err = fs.Chmod(to, si.Mode()) - - if err != nil { - return err - } - } - - return nil -} - -// CopyDir copies a directory. -func CopyDir(fs afero.Fs, from, to string, shouldCopy func(filename string) bool) error { - fi, err := fs.Stat(from) - if err != nil { - return err - } - - if !fi.IsDir() { - return fmt.Errorf("%q is not a directory", from) - } - - err = fs.MkdirAll(to, 0o777) // before umask - if err != nil { - return err - } - - d, err := fs.Open(from) - if err != nil { - return err - } - entries, _ := d.(iofs.ReadDirFile).ReadDir(-1) - for _, entry := range entries { - fromFilename := filepath.Join(from, entry.Name()) - toFilename := filepath.Join(to, entry.Name()) - if entry.IsDir() { - if shouldCopy != nil && !shouldCopy(fromFilename) { - continue - } - if err := CopyDir(fs, fromFilename, toFilename, shouldCopy); err != nil { - return err - } - } else { - if err := CopyFile(fs, fromFilename, toFilename); err != nil { - return err - } - } - - } - - return nil -} diff --git a/common/hugio/hasBytesWriter.go b/common/hugio/hasBytesWriter.go deleted file mode 100644 index d2bcd1bb4..000000000 --- a/common/hugio/hasBytesWriter.go +++ /dev/null @@ -1,80 +0,0 @@ -// 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 hugio - -import ( - "bytes" -) - -// HasBytesWriter is a writer will match against a slice of patterns. -type HasBytesWriter struct { - Patterns []*HasBytesPattern - - i int - done bool - buff []byte -} - -type HasBytesPattern struct { - Match bool - Pattern []byte -} - -func (h *HasBytesWriter) patternLen() int { - l := 0 - for _, p := range h.Patterns { - l += len(p.Pattern) - } - return l -} - -func (h *HasBytesWriter) Write(p []byte) (n int, err error) { - if h.done { - return len(p), nil - } - - if len(h.buff) == 0 { - h.buff = make([]byte, h.patternLen()*2) - } - - for i := range p { - h.buff[h.i] = p[i] - h.i++ - if h.i == len(h.buff) { - // Shift left. - copy(h.buff, h.buff[len(h.buff)/2:]) - h.i = len(h.buff) / 2 - } - - for _, pp := range h.Patterns { - if bytes.Contains(h.buff, pp.Pattern) { - pp.Match = true - done := true - for _, ppp := range h.Patterns { - if !ppp.Match { - done = false - break - } - } - if done { - h.done = true - } - return len(p), nil - } - } - - } - - return len(p), nil -} diff --git a/common/hugio/hasBytesWriter_test.go b/common/hugio/hasBytesWriter_test.go deleted file mode 100644 index 9e689a112..000000000 --- a/common/hugio/hasBytesWriter_test.go +++ /dev/null @@ -1,67 +0,0 @@ -// 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 hugio - -import ( - "bytes" - "fmt" - "io" - "math/rand" - "strings" - "testing" - "time" - - qt "github.com/frankban/quicktest" -) - -func TestHasBytesWriter(t *testing.T) { - r := rand.New(rand.NewSource(time.Now().UnixNano())) - - c := qt.New((t)) - - neww := func() (*HasBytesWriter, io.Writer) { - var b bytes.Buffer - - h := &HasBytesWriter{ - Patterns: []*HasBytesPattern{ - {Pattern: []byte("__foo")}, - }, - } - - return h, io.MultiWriter(&b, h) - } - - rndStr := func() string { - return strings.Repeat("ab cfo", r.Intn(33)) - } - - for range 22 { - h, w := neww() - fmt.Fprint(w, rndStr()+"abc __foobar"+rndStr()) - c.Assert(h.Patterns[0].Match, qt.Equals, true) - - h, w = neww() - fmt.Fprint(w, rndStr()+"abc __f") - fmt.Fprint(w, "oo bar"+rndStr()) - c.Assert(h.Patterns[0].Match, qt.Equals, true) - - h, w = neww() - fmt.Fprint(w, rndStr()+"abc __moo bar") - c.Assert(h.Patterns[0].Match, qt.Equals, false) - } - - h, w := neww() - fmt.Fprintf(w, "__foo") - c.Assert(h.Patterns[0].Match, qt.Equals, true) -} diff --git a/common/hugio/readers.go b/common/hugio/readers.go deleted file mode 100644 index c4304c84e..000000000 --- a/common/hugio/readers.go +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2019 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 hugio - -import ( - "bytes" - "io" - "strings" -) - -// ReadSeeker wraps io.Reader and io.Seeker. -type ReadSeeker interface { - io.Reader - io.Seeker -} - -// ReadSeekCloser is implemented by afero.File. We use this as the common type for -// content in Resource objects, even for strings. -type ReadSeekCloser interface { - ReadSeeker - io.Closer -} - -// ReadSeekCloserProvider provides a ReadSeekCloser. -type ReadSeekCloserProvider interface { - ReadSeekCloser() (ReadSeekCloser, error) -} - -// readSeekerNopCloser implements ReadSeekCloser by doing nothing in Close. -type readSeekerNopCloser struct { - ReadSeeker -} - -// Close does nothing. -func (r readSeekerNopCloser) Close() error { - return nil -} - -// NewReadSeekerNoOpCloser creates a new ReadSeekerNoOpCloser with the given ReadSeeker. -func NewReadSeekerNoOpCloser(r ReadSeeker) ReadSeekCloser { - return readSeekerNopCloser{r} -} - -// NewReadSeekerNoOpCloserFromString uses strings.NewReader to create a new ReadSeekerNoOpCloser -// from the given string. -func NewReadSeekerNoOpCloserFromString(content string) ReadSeekCloser { - return stringReadSeeker{s: content, readSeekerNopCloser: readSeekerNopCloser{strings.NewReader(content)}} -} - -var _ StringReader = (*stringReadSeeker)(nil) - -type stringReadSeeker struct { - s string - readSeekerNopCloser -} - -func (s *stringReadSeeker) ReadString() string { - return s.s -} - -// StringReader provides a way to read a string. -type StringReader interface { - ReadString() string -} - -// NewReadSeekerNoOpCloserFromBytes uses bytes.NewReader to create a new ReadSeekerNoOpCloser -// from the given bytes slice. -func NewReadSeekerNoOpCloserFromBytes(content []byte) readSeekerNopCloser { - return readSeekerNopCloser{bytes.NewReader(content)} -} - -// NewOpenReadSeekCloser creates a new ReadSeekCloser from the given ReadSeeker. -// The ReadSeeker will be seeked to the beginning before returned. -func NewOpenReadSeekCloser(r ReadSeekCloser) OpenReadSeekCloser { - return func() (ReadSeekCloser, error) { - r.Seek(0, io.SeekStart) - return r, nil - } -} - -// OpenReadSeekCloser allows setting some other way (than reading from a filesystem) -// to open or create a ReadSeekCloser. -type OpenReadSeekCloser func() (ReadSeekCloser, error) - -// ReadString reads from the given reader and returns the content as a string. -func ReadString(r io.Reader) (string, error) { - if sr, ok := r.(StringReader); ok { - return sr.ReadString(), nil - } - b, err := io.ReadAll(r) - if err != nil { - return "", err - } - return string(b), nil -} diff --git a/common/hugio/writers.go b/common/hugio/writers.go deleted file mode 100644 index 6f439cc8b..000000000 --- a/common/hugio/writers.go +++ /dev/null @@ -1,113 +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 hugio - -import ( - "io" -) - -// As implemented by strings.Builder. -type FlexiWriter interface { - io.Writer - io.ByteWriter - WriteString(s string) (int, error) - WriteRune(r rune) (int, error) -} - -type multiWriteCloser struct { - io.Writer - closers []io.WriteCloser -} - -func (m multiWriteCloser) Close() error { - var err error - for _, c := range m.closers { - if closeErr := c.Close(); closeErr != nil { - err = closeErr - } - } - return err -} - -// NewMultiWriteCloser creates a new io.WriteCloser that duplicates its writes to all the -// provided writers. -func NewMultiWriteCloser(writeClosers ...io.WriteCloser) io.WriteCloser { - writers := make([]io.Writer, len(writeClosers)) - for i, w := range writeClosers { - writers[i] = w - } - return multiWriteCloser{Writer: io.MultiWriter(writers...), closers: writeClosers} -} - -// ToWriteCloser creates an io.WriteCloser from the given io.Writer. -// If it's not already, one will be created with a Close method that does nothing. -func ToWriteCloser(w io.Writer) io.WriteCloser { - if rw, ok := w.(io.WriteCloser); ok { - return rw - } - - return struct { - io.Writer - io.Closer - }{ - w, - io.NopCloser(nil), - } -} - -// ToReadCloser creates an io.ReadCloser from the given io.Reader. -// If it's not already, one will be created with a Close method that does nothing. -func ToReadCloser(r io.Reader) io.ReadCloser { - if rc, ok := r.(io.ReadCloser); ok { - return rc - } - - return struct { - io.Reader - io.Closer - }{ - r, - io.NopCloser(nil), - } -} - -type ReadWriteCloser interface { - io.Reader - io.Writer - io.Closer -} - -// PipeReadWriteCloser is a convenience type to create a pipe with a ReadCloser and a WriteCloser. -type PipeReadWriteCloser struct { - *io.PipeReader - *io.PipeWriter -} - -// NewPipeReadWriteCloser creates a new PipeReadWriteCloser. -func NewPipeReadWriteCloser() PipeReadWriteCloser { - pr, pw := io.Pipe() - return PipeReadWriteCloser{pr, pw} -} - -func (c PipeReadWriteCloser) Close() (err error) { - if err = c.PipeReader.Close(); err != nil { - return - } - err = c.PipeWriter.Close() - return -} - -func (c PipeReadWriteCloser) WriteString(s string) (int, error) { - return c.PipeWriter.Write([]byte(s)) -} diff --git a/common/hugo/hugo.go b/common/hugo/hugo.go deleted file mode 100644 index 764a86a97..000000000 --- a/common/hugo/hugo.go +++ /dev/null @@ -1,467 +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 hugo - -import ( - "context" - "fmt" - "html/template" - "os" - "path/filepath" - "runtime/debug" - "sort" - "strings" - "sync" - "time" - - "github.com/bep/logg" - - "github.com/bep/godartsass/v2" - "github.com/gohugoio/hugo/common/hcontext" - "github.com/gohugoio/hugo/common/hexec" - "github.com/gohugoio/hugo/common/loggers" - "github.com/gohugoio/hugo/common/maps" - "github.com/gohugoio/hugo/hugofs/files" - - "github.com/spf13/afero" - - iofs "io/fs" - - "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/hugofs" -) - -const ( - EnvironmentDevelopment = "development" - EnvironmentProduction = "production" -) - -var ( - // buildDate allows vendor-specified build date when .git/ is unavailable. - buildDate string - // vendorInfo contains vendor notes about the current build. - vendorInfo string -) - -var _ maps.StoreProvider = (*HugoInfo)(nil) - -// HugoInfo contains information about the current Hugo environment -type HugoInfo struct { - CommitHash string - BuildDate string - - // The build environment. - // Defaults are "production" (hugo) and "development" (hugo server). - // This can also be set by the user. - // It can be any string, but it will be all lower case. - Environment string - - // version of go that the Hugo binary was built with - GoVersion string - - conf ConfigProvider - deps []*Dependency - - store *maps.Scratch - - // Context gives access to some of the context scoped variables. - Context Context -} - -// Version returns the current version as a comparable version string. -func (i HugoInfo) Version() VersionString { - return CurrentVersion.Version() -} - -// Generator a Hugo meta generator HTML tag. -func (i HugoInfo) Generator() template.HTML { - return template.HTML(fmt.Sprintf(``, 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 -} - -// WorkingDir returns the project working directory. -func (i HugoInfo) WorkingDir() string { - return i.conf.WorkingDir() -} - -// Deps gets a list of dependencies for this Hugo build. -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. -func NewInfo(conf ConfigProvider, deps []*Dependency) HugoInfo { - if conf.Environment() == "" { - panic("environment not set") - } - var ( - commitHash string - buildDate string - goVersion string - ) - - bi := getBuildInfo() - if bi != nil { - commitHash = bi.Revision - buildDate = bi.RevisionTime - goVersion = bi.GoVersion - } - - return HugoInfo{ - CommitHash: commitHash, - BuildDate: buildDate, - Environment: conf.Environment(), - conf: conf, - deps: deps, - store: maps.NewScratch(), - GoVersion: goVersion, - } -} - -// GetExecEnviron creates and gets the common os/exec environment used in the -// external programs we interact with via os/exec, e.g. postcss. -func GetExecEnviron(workDir string, cfg config.AllProvider, fs afero.Fs) []string { - var env []string - nodepath := filepath.Join(workDir, "node_modules") - if np := os.Getenv("NODE_PATH"); np != "" { - nodepath = workDir + string(os.PathListSeparator) + np - } - config.SetEnvVars(&env, "NODE_PATH", nodepath) - config.SetEnvVars(&env, "PWD", workDir) - config.SetEnvVars(&env, "HUGO_ENVIRONMENT", cfg.Environment()) - config.SetEnvVars(&env, "HUGO_ENV", cfg.Environment()) - config.SetEnvVars(&env, "HUGO_PUBLISHDIR", filepath.Join(workDir, cfg.BaseConfig().PublishDir)) - - if fs != nil { - 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()), ".", "_")) - value := fi.(hugofs.FileMetaInfo).Meta().Filename - config.SetEnvVars(&env, key, value) - } - } - } - - return env -} - -type buildInfo struct { - VersionControlSystem string - Revision string - RevisionTime string - Modified bool - - GoOS string - GoArch string - - *debug.BuildInfo -} - -var ( - bInfo *buildInfo - bInfoInit sync.Once -) - -func getBuildInfo() *buildInfo { - bInfoInit.Do(func() { - bi, ok := debug.ReadBuildInfo() - if !ok { - return - } - - bInfo = &buildInfo{BuildInfo: bi} - - for _, s := range bInfo.Settings { - switch s.Key { - case "vcs": - bInfo.VersionControlSystem = s.Value - case "vcs.revision": - bInfo.Revision = s.Value - case "vcs.time": - bInfo.RevisionTime = s.Value - case "vcs.modified": - bInfo.Modified = s.Value == "true" - case "GOOS": - bInfo.GoOS = s.Value - case "GOARCH": - bInfo.GoArch = s.Value - } - } - }) - - return bInfo -} - -func formatDep(path, version string) string { - return fmt.Sprintf("%s=%q", path, version) -} - -// GetDependencyList returns a sorted dependency list on the format package="version". -// It includes both Go dependencies and (a manually maintained) list of C(++) dependencies. -func GetDependencyList() []string { - var deps []string - - bi := getBuildInfo() - if bi == nil { - return deps - } - - for _, dep := range bi.Deps { - deps = append(deps, formatDep(dep.Path, dep.Version)) - } - - deps = append(deps, GetDependencyListNonGo()...) - - sort.Strings(deps) - - return deps -} - -// GetDependencyListNonGo returns a list of non-Go dependencies. -func GetDependencyListNonGo() []string { - var deps []string - - if IsExtended { - deps = append( - deps, - formatDep("github.com/sass/libsass", "3.6.6"), - formatDep("github.com/webmproject/libwebp", "v1.3.2"), - ) - } - - if dartSass := dartSassVersion(); dartSass.ProtocolVersion != "" { - 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), - formatDep(dartSassPath+"/implementation", dartSass.ImplementationVersion), - ) - } - return deps -} - -// IsRunningAsTest reports whether we are running as a test. -func IsRunningAsTest() bool { - for _, arg := range os.Args { - if strings.HasPrefix(arg, "-test") { - return true - } - } - return false -} - -// Dependency is a single dependency, which can be either a Hugo Module or a local theme. -type Dependency struct { - // Returns the path to this module. - // This will either be the module path, e.g. "github.com/gohugoio/myshortcodes", - // or the path below your /theme folder, e.g. "mytheme". - Path string - - // The module version. - Version string - - // Whether this dependency is vendored. - Vendor bool - - // Time version was created. - Time time.Time - - // In the dependency tree, this is the first module that defines this module - // as a dependency. - Owner *Dependency - - // Replaced by this dependency. - Replace *Dependency -} - -func dartSassVersion() godartsass.DartSassVersion { - if DartSassBinaryName == "" || !IsDartSassGeV2() { - return godartsass.DartSassVersion{} - } - 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 deleted file mode 100644 index 77dbb5c91..000000000 --- a/common/hugo/hugo_integration_test.go +++ /dev/null @@ -1,77 +0,0 @@ -// 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 deleted file mode 100644 index f938073da..000000000 --- a/common/hugo/hugo_test.go +++ /dev/null @@ -1,111 +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 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", running: false} - hugoInfo := NewInfo(conf, nil) - - c.Assert(hugoInfo.Version(), qt.Equals, CurrentVersion.Version()) - c.Assert(fmt.Sprintf("%T", VersionString("")), qt.Equals, fmt.Sprintf("%T", hugoInfo.Version())) - c.Assert(hugoInfo.WorkingDir(), qt.Equals, "/mywork") - - bi := getBuildInfo() - if bi != nil { - c.Assert(hugoInfo.CommitHash, qt.Equals, bi.Revision) - c.Assert(hugoInfo.BuildDate, qt.Equals, bi.RevisionTime) - c.Assert(hugoInfo.GoVersion, qt.Equals, bi.GoVersion) - } - 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", 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 - 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 deleted file mode 100644 index ab01e2647..000000000 --- a/common/hugo/vars_extended.go +++ /dev/null @@ -1,18 +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. - -//go:build extended - -package hugo - -var IsExtended = true diff --git a/common/hugo/vars_regular.go b/common/hugo/vars_regular.go deleted file mode 100644 index a78aeb0b6..000000000 --- a/common/hugo/vars_regular.go +++ /dev/null @@ -1,18 +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. - -//go:build !extended - -package hugo - -var IsExtended = false diff --git a/common/hugo/vars_withdeploy.go b/common/hugo/vars_withdeploy.go deleted file mode 100644 index 4e0c3efbb..000000000 --- a/common/hugo/vars_withdeploy.go +++ /dev/null @@ -1,18 +0,0 @@ -// 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 deleted file mode 100644 index 36e9bd874..000000000 --- a/common/hugo/vars_withdeploy_off.go +++ /dev/null @@ -1,18 +0,0 @@ -// 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 deleted file mode 100644 index cf5988840..000000000 --- a/common/hugo/version.go +++ /dev/null @@ -1,305 +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 hugo - -import ( - "fmt" - "io" - "math" - "runtime" - "strconv" - "strings" - - "github.com/gohugoio/hugo/compare" - "github.com/spf13/cast" -) - -// Version represents the Hugo build version. -type Version struct { - Major int - - Minor int - - // Increment this for bug releases - PatchLevel int - - // HugoVersionSuffix is the suffix used in the Hugo version string. - // It will be blank for release versions. - Suffix string -} - -var ( - _ compare.Eqer = (*VersionString)(nil) - _ compare.Comparer = (*VersionString)(nil) -) - -func (v Version) String() string { - return version(v.Major, v.Minor, v.PatchLevel, v.Suffix) -} - -// Version returns the Hugo version. -func (v Version) Version() VersionString { - return VersionString(v.String()) -} - -// Compare implements the compare.Comparer interface. -func (h Version) Compare(other any) int { - return compareVersions(h, other) -} - -// VersionString represents a Hugo version string. -type VersionString string - -func (h VersionString) String() string { - return string(h) -} - -// Compare implements the compare.Comparer interface. -func (h VersionString) Compare(other any) int { - return compareVersions(h.Version(), other) -} - -func (h VersionString) Version() Version { - return MustParseVersion(h.String()) -} - -// Eq implements the compare.Eqer interface. -func (h VersionString) Eq(other any) bool { - s, err := cast.ToStringE(other) - if err != nil { - return false - } - return s == h.String() -} - -var versionSuffixes = []string{"-test", "-DEV"} - -// ParseVersion parses a version string. -func ParseVersion(s string) (Version, error) { - var vv Version - for _, suffix := range versionSuffixes { - if strings.HasSuffix(s, suffix) { - vv.Suffix = suffix - s = strings.TrimSuffix(s, suffix) - } - } - - vv.Major, vv.Minor, vv.PatchLevel = parseVersion(s) - - return vv, nil -} - -// MustParseVersion parses a version string -// and panics if any error occurs. -func MustParseVersion(s string) Version { - vv, err := ParseVersion(s) - if err != nil { - panic(err) - } - return vv -} - -// ReleaseVersion represents the release version. -func (v Version) ReleaseVersion() Version { - v.Suffix = "" - return v -} - -// Next returns the next Hugo release version. -func (v Version) Next() Version { - return Version{Major: v.Major, Minor: v.Minor + 1} -} - -// Prev returns the previous Hugo release version. -func (v Version) Prev() Version { - return Version{Major: v.Major, Minor: v.Minor - 1} -} - -// NextPatchLevel returns the next patch/bugfix Hugo version. -// This will be a patch increment on the previous Hugo version. -func (v Version) NextPatchLevel(level int) Version { - prev := v.Prev() - prev.PatchLevel = level - return prev -} - -// BuildVersionString creates a version string. This is what you see when -// running "hugo version". -func BuildVersionString() string { - // program := "Hugo Static Site Generator" - program := "hugo" - - version := "v" + CurrentVersion.String() - - bi := getBuildInfo() - if bi == nil { - return version - } - if bi.Revision != "" { - version += "-" + bi.Revision - } - if IsExtended { - version += "+extended" - } - if IsWithdeploy { - version += "+withdeploy" - } - - osArch := bi.GoOS + "/" + bi.GoArch - - date := bi.RevisionTime - if date == "" { - // Accept vendor-specified build date if .git/ is unavailable. - date = buildDate - } - if date == "" { - date = "unknown" - } - - versionString := fmt.Sprintf("%s %s %s BuildDate=%s", - program, version, osArch, date) - - if vendorInfo != "" { - versionString += " VendorInfo=" + vendorInfo - } - - return versionString -} - -func version(major, minor, patch int, suffix string) string { - if patch > 0 || minor > 53 { - return fmt.Sprintf("%d.%d.%d%s", major, minor, patch, suffix) - } - return fmt.Sprintf("%d.%d%s", major, minor, suffix) -} - -// CompareVersion compares the given version string or number against the -// running Hugo version. -// It returns -1 if the given version is less than, 0 if equal and 1 if greater than -// the running version. -func CompareVersion(version any) int { - return compareVersions(CurrentVersion, version) -} - -func compareVersions(inVersion Version, in any) int { - var c int - switch d := in.(type) { - case float64: - c = compareFloatWithVersion(d, inVersion) - case float32: - c = compareFloatWithVersion(float64(d), inVersion) - case int: - c = compareFloatWithVersion(float64(d), inVersion) - case int32: - c = compareFloatWithVersion(float64(d), inVersion) - case int64: - c = compareFloatWithVersion(float64(d), inVersion) - case Version: - if d.Major == inVersion.Major && d.Minor == inVersion.Minor && d.PatchLevel == inVersion.PatchLevel { - return strings.Compare(inVersion.Suffix, d.Suffix) - } - if d.Major > inVersion.Major { - return 1 - } else if d.Major < inVersion.Major { - return -1 - } - if d.Minor > inVersion.Minor { - return 1 - } else if d.Minor < inVersion.Minor { - return -1 - } - if d.PatchLevel > inVersion.PatchLevel { - return 1 - } else if d.PatchLevel < inVersion.PatchLevel { - return -1 - } - default: - s, err := cast.ToStringE(in) - if err != nil { - return -1 - } - - v, err := ParseVersion(s) - if err != nil { - return -1 - } - return inVersion.Compare(v) - - } - - return c -} - -func parseVersion(s string) (int, int, int) { - var major, minor, patch int - parts := strings.Split(s, ".") - if len(parts) > 0 { - major, _ = strconv.Atoi(parts[0]) - } - if len(parts) > 1 { - minor, _ = strconv.Atoi(parts[1]) - } - if len(parts) > 2 { - patch, _ = strconv.Atoi(parts[2]) - } - - return major, minor, patch -} - -// compareFloatWithVersion compares v1 with v2. -// It returns -1 if v1 is less than v2, 0 if v1 is equal to v2 and 1 if v1 is greater than v2. -func compareFloatWithVersion(v1 float64, v2 Version) int { - mf, minf := math.Modf(v1) - v1maj := int(mf) - v1min := int(minf * 100) - - if v2.Major == v1maj && v2.Minor == v1min { - return 0 - } - - if v1maj > v2.Major { - return 1 - } - - if v1maj < v2.Major { - return -1 - } - - if v1min > v2.Minor { - return 1 - } - - return -1 -} - -func GoMinorVersion() int { - return goMinorVersion(runtime.Version()) -} - -func goMinorVersion(version string) int { - if strings.HasPrefix(version, "devel") { - return 9999 // magic - } - var major, minor int - var trailing string - n, err := fmt.Sscanf(version, "go%d.%d%s", &major, &minor, &trailing) - if n == 2 && err == io.EOF { - // Means there were no trailing characters (i.e., not an alpha/beta) - err = nil - } - if err != nil { - return 0 - } - return minor -} diff --git a/common/hugo/version_current.go b/common/hugo/version_current.go deleted file mode 100644 index ba367ceb5..000000000 --- a/common/hugo/version_current.go +++ /dev/null @@ -1,23 +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 hugo - -// CurrentVersion represents the current build version. -// This should be the only one. -var CurrentVersion = Version{ - Major: 0, - Minor: 148, - PatchLevel: 0, - Suffix: "-DEV", -} diff --git a/common/hugo/version_test.go b/common/hugo/version_test.go deleted file mode 100644 index 33e50ebf5..000000000 --- a/common/hugo/version_test.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2015 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 - -import ( - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestHugoVersion(t *testing.T) { - c := qt.New(t) - - c.Assert(version(0, 15, 0, "-DEV"), qt.Equals, "0.15-DEV") - c.Assert(version(0, 15, 2, "-DEV"), qt.Equals, "0.15.2-DEV") - - v := Version{Minor: 21, Suffix: "-DEV"} - - c.Assert(v.ReleaseVersion().String(), qt.Equals, "0.21") - c.Assert(v.String(), qt.Equals, "0.21-DEV") - c.Assert(v.Next().String(), qt.Equals, "0.22") - nextVersionString := v.Next().Version() - c.Assert(nextVersionString.String(), qt.Equals, "0.22") - c.Assert(nextVersionString.Eq("0.22"), qt.Equals, true) - c.Assert(nextVersionString.Eq("0.21"), qt.Equals, false) - c.Assert(nextVersionString.Eq(nextVersionString), qt.Equals, true) - c.Assert(v.NextPatchLevel(3).String(), qt.Equals, "0.20.3") - - // We started to use full semver versions even for main - // releases in v0.54.0 - v = Version{Minor: 53, PatchLevel: 0} - c.Assert(v.String(), qt.Equals, "0.53") - c.Assert(v.Next().String(), qt.Equals, "0.54.0") - c.Assert(v.Next().Next().String(), qt.Equals, "0.55.0") - v = Version{Minor: 54, PatchLevel: 0, Suffix: "-DEV"} - c.Assert(v.String(), qt.Equals, "0.54.0-DEV") -} - -func TestCompareVersions(t *testing.T) { - c := qt.New(t) - - c.Assert(compareVersions(MustParseVersion("0.20.0"), 0.20), qt.Equals, 0) - c.Assert(compareVersions(MustParseVersion("0.20.0"), float32(0.20)), qt.Equals, 0) - c.Assert(compareVersions(MustParseVersion("0.20.0"), float64(0.20)), qt.Equals, 0) - c.Assert(compareVersions(MustParseVersion("0.19.1"), 0.20), qt.Equals, 1) - c.Assert(compareVersions(MustParseVersion("0.19.3"), "0.20.2"), qt.Equals, 1) - c.Assert(compareVersions(MustParseVersion("0.1"), 3), qt.Equals, 1) - c.Assert(compareVersions(MustParseVersion("0.1"), int32(3)), qt.Equals, 1) - c.Assert(compareVersions(MustParseVersion("0.1"), int64(3)), qt.Equals, 1) - c.Assert(compareVersions(MustParseVersion("0.20"), "0.20"), qt.Equals, 0) - c.Assert(compareVersions(MustParseVersion("0.20.1"), "0.20.1"), qt.Equals, 0) - c.Assert(compareVersions(MustParseVersion("0.20.1"), "0.20"), qt.Equals, -1) - c.Assert(compareVersions(MustParseVersion("0.20.0"), "0.20.1"), qt.Equals, 1) - c.Assert(compareVersions(MustParseVersion("0.20.1"), "0.20.2"), qt.Equals, 1) - c.Assert(compareVersions(MustParseVersion("0.21.1"), "0.22.1"), qt.Equals, 1) - c.Assert(compareVersions(MustParseVersion("0.22.0"), "0.22-DEV"), qt.Equals, -1) - c.Assert(compareVersions(MustParseVersion("0.22.0"), "0.22.1-DEV"), qt.Equals, 1) - c.Assert(compareVersions(MustParseVersion("0.22.0-DEV"), "0.22"), qt.Equals, 1) - c.Assert(compareVersions(MustParseVersion("0.22.1-DEV"), "0.22"), qt.Equals, -1) - c.Assert(compareVersions(MustParseVersion("0.22.1-DEV"), "0.22.1-DEV"), qt.Equals, 0) -} - -func TestParseHugoVersion(t *testing.T) { - c := qt.New(t) - - c.Assert(MustParseVersion("0.25").String(), qt.Equals, "0.25") - c.Assert(MustParseVersion("0.25.2").String(), qt.Equals, "0.25.2") - c.Assert(MustParseVersion("0.25-test").String(), qt.Equals, "0.25-test") - c.Assert(MustParseVersion("0.25-DEV").String(), qt.Equals, "0.25-DEV") -} - -func TestGoMinorVersion(t *testing.T) { - c := qt.New(t) - c.Assert(goMinorVersion("go1.12.5"), qt.Equals, 12) - c.Assert(goMinorVersion("go1.14rc1"), qt.Equals, 14) - c.Assert(GoMinorVersion() >= 11, qt.Equals, true) -} diff --git a/common/loggers/handlerdefault.go b/common/loggers/handlerdefault.go deleted file mode 100644 index bc3c7eec2..000000000 --- a/common/loggers/handlerdefault.go +++ /dev/null @@ -1,106 +0,0 @@ -// 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 deleted file mode 100644 index 2ae6300f7..000000000 --- a/common/loggers/handlersmisc.go +++ /dev/null @@ -1,145 +0,0 @@ -// 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 deleted file mode 100644 index c6a86d3a2..000000000 --- a/common/loggers/handlerterminal.go +++ /dev/null @@ -1,100 +0,0 @@ -// 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 deleted file mode 100644 index f45ce80df..000000000 --- a/common/loggers/handlerterminal_test.go +++ /dev/null @@ -1,40 +0,0 @@ -// 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/logger.go b/common/loggers/logger.go deleted file mode 100644 index a013049f7..000000000 --- a/common/loggers/logger.go +++ /dev/null @@ -1,385 +0,0 @@ -// 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 deleted file mode 100644 index bc8975b06..000000000 --- a/common/loggers/logger_test.go +++ /dev/null @@ -1,154 +0,0 @@ -// 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 deleted file mode 100644 index b8c9a6931..000000000 --- a/common/loggers/loggerglobal.go +++ /dev/null @@ -1,62 +0,0 @@ -// 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/maps/cache.go b/common/maps/cache.go deleted file mode 100644 index de1535994..000000000 --- a/common/maps/cache.go +++ /dev/null @@ -1,195 +0,0 @@ -// 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 deleted file mode 100644 index f9171ebf2..000000000 --- a/common/maps/maps.go +++ /dev/null @@ -1,236 +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 maps - -import ( - "fmt" - "strings" - - "github.com/gohugoio/hugo/common/types" - - "github.com/gobwas/glob" - "github.com/spf13/cast" -) - -// ToStringMapE converts in to map[string]interface{}. -func ToStringMapE(in any) (map[string]any, error) { - switch vv := in.(type) { - case Params: - return vv, nil - case map[string]string: - m := map[string]any{} - for k, v := range vv { - m[k] = v - } - return m, nil - - default: - return cast.ToStringMapE(in) - } -} - -// ToParamsAndPrepare converts in to Params and prepares it for use. -// If in is nil, an empty map is returned. -// See PrepareParams. -func ToParamsAndPrepare(in any) (Params, error) { - if types.IsNil(in) { - return Params{}, nil - } - m, err := ToStringMapE(in) - if err != nil { - return nil, err - } - PrepareParams(m) - return m, nil -} - -// MustToParamsAndPrepare calls ToParamsAndPrepare and panics if it fails. -func MustToParamsAndPrepare(in any) Params { - p, err := ToParamsAndPrepare(in) - if err != nil { - panic(fmt.Sprintf("cannot convert %T to maps.Params: %s", in, err)) - } - return p -} - -// ToStringMap converts in to map[string]interface{}. -func ToStringMap(in any) map[string]any { - m, _ := ToStringMapE(in) - return m -} - -// ToStringMapStringE converts in to map[string]string. -func ToStringMapStringE(in any) (map[string]string, error) { - m, err := ToStringMapE(in) - if err != nil { - return nil, err - } - return cast.ToStringMapStringE(m) -} - -// ToStringMapString converts in to map[string]string. -func ToStringMapString(in any) map[string]string { - m, _ := ToStringMapStringE(in) - return m -} - -// ToStringMapBool converts in to bool. -func ToStringMapBool(in any) map[string]bool { - m, _ := ToStringMapE(in) - return cast.ToStringMapBool(m) -} - -// ToSliceStringMap converts in to []map[string]interface{}. -func ToSliceStringMap(in any) ([]map[string]any, error) { - switch v := in.(type) { - case []map[string]any: - return v, nil - case Params: - return []map[string]any{v}, nil - case []any: - var s []map[string]any - for _, entry := range v { - if vv, ok := entry.(map[string]any); ok { - s = append(s, vv) - } - } - return s, nil - default: - return nil, fmt.Errorf("unable to cast %#v of type %T to []map[string]interface{}", in, in) - } -} - -// LookupEqualFold finds key in m with case insensitive equality checks. -func LookupEqualFold[T any | string](m map[string]T, key string) (T, string, bool) { - if v, found := m[key]; found { - return v, key, true - } - for k, v := range m { - if strings.EqualFold(k, key) { - return v, k, true - } - } - var s T - return s, "", false -} - -// MergeShallow merges src into dst, but only if the key does not already exist in dst. -// The keys are compared case insensitively. -func MergeShallow(dst, src map[string]any) { - for k, v := range src { - found := false - for dk := range dst { - if strings.EqualFold(dk, k) { - found = true - break - } - } - if !found { - dst[k] = v - } - } -} - -type keyRename struct { - pattern glob.Glob - newKey string -} - -// KeyRenamer supports renaming of keys in a map. -type KeyRenamer struct { - renames []keyRename -} - -// NewKeyRenamer creates a new KeyRenamer given a list of pattern and new key -// value pairs. -func NewKeyRenamer(patternKeys ...string) (KeyRenamer, error) { - var renames []keyRename - for i := 0; i < len(patternKeys); i += 2 { - g, err := glob.Compile(strings.ToLower(patternKeys[i]), '/') - if err != nil { - return KeyRenamer{}, err - } - renames = append(renames, keyRename{pattern: g, newKey: patternKeys[i+1]}) - } - - return KeyRenamer{renames: renames}, nil -} - -func (r KeyRenamer) getNewKey(keyPath string) string { - for _, matcher := range r.renames { - if matcher.pattern.Match(keyPath) { - return matcher.newKey - } - } - - return "" -} - -// Rename renames the keys in the given map according -// to the patterns in the current KeyRenamer. -func (r KeyRenamer) Rename(m map[string]any) { - r.renamePath("", m) -} - -func (KeyRenamer) keyPath(k1, k2 string) string { - k1, k2 = strings.ToLower(k1), strings.ToLower(k2) - if k1 == "" { - return k2 - } - return k1 + "/" + k2 -} - -func (r KeyRenamer) renamePath(parentKeyPath string, m map[string]any) { - for k, v := range m { - keyPath := r.keyPath(parentKeyPath, k) - switch vv := v.(type) { - case map[any]any: - r.renamePath(keyPath, cast.ToStringMap(vv)) - case map[string]any: - r.renamePath(keyPath, vv) - } - - newKey := r.getNewKey(keyPath) - - if newKey != "" { - 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 deleted file mode 100644 index 40c8ac824..000000000 --- a/common/maps/maps_test.go +++ /dev/null @@ -1,201 +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 maps - -import ( - "fmt" - "reflect" - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestPrepareParams(t *testing.T) { - tests := []struct { - input Params - expected Params - }{ - { - map[string]any{ - "abC": 32, - }, - Params{ - "abc": 32, - }, - }, - { - map[string]any{ - "abC": 32, - "deF": map[any]any{ - 23: "A value", - 24: map[string]any{ - "AbCDe": "A value", - "eFgHi": "Another value", - }, - }, - "gHi": map[string]any{ - "J": 25, - }, - "jKl": map[string]string{ - "M": "26", - }, - }, - Params{ - "abc": 32, - "def": Params{ - "23": "A value", - "24": Params{ - "abcde": "A value", - "efghi": "Another value", - }, - }, - "ghi": Params{ - "j": 25, - }, - "jkl": Params{ - "m": "26", - }, - }, - }, - } - - 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) - } - }) - } -} - -func TestToSliceStringMap(t *testing.T) { - c := qt.New(t) - - tests := []struct { - input any - expected []map[string]any - }{ - { - input: []map[string]any{ - {"abc": 123}, - }, - expected: []map[string]any{ - {"abc": 123}, - }, - }, { - input: []any{ - map[string]any{ - "def": 456, - }, - }, - expected: []map[string]any{ - {"def": 456}, - }, - }, - } - - for _, test := range tests { - v, err := ToSliceStringMap(test.input) - c.Assert(err, qt.IsNil) - c.Assert(v, qt.DeepEquals, test.expected) - } -} - -func TestToParamsAndPrepare(t *testing.T) { - c := qt.New(t) - _, err := ToParamsAndPrepare(map[string]any{"A": "av"}) - c.Assert(err, qt.IsNil) - - params, err := ToParamsAndPrepare(nil) - c.Assert(err, qt.IsNil) - c.Assert(params, qt.DeepEquals, Params{}) -} - -func TestRenameKeys(t *testing.T) { - c := qt.New(t) - - m := map[string]any{ - "a": 32, - "ren1": "m1", - "ren2": "m1_2", - "sub": map[string]any{ - "subsub": map[string]any{ - "REN1": "m2", - "ren2": "m2_2", - }, - }, - "no": map[string]any{ - "ren1": "m2", - "ren2": "m2_2", - }, - } - - expected := map[string]any{ - "a": 32, - "new1": "m1", - "new2": "m1_2", - "sub": map[string]any{ - "subsub": map[string]any{ - "new1": "m2", - "ren2": "m2_2", - }, - }, - "no": map[string]any{ - "ren1": "m2", - "ren2": "m2_2", - }, - } - - renamer, err := NewKeyRenamer( - "{ren1,sub/*/ren1}", "new1", - "{Ren2,sub/ren2}", "new2", - ) - c.Assert(err, qt.IsNil) - - renamer.Rename(m) - - if !reflect.DeepEqual(expected, m) { - t.Errorf("Expected\n%#v, got\n%#v\n", expected, m) - } -} - -func TestLookupEqualFold(t *testing.T) { - c := qt.New(t) - - m1 := map[string]any{ - "a": "av", - "B": "bv", - } - - 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, 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 deleted file mode 100644 index 0da9d239d..000000000 --- a/common/maps/ordered.go +++ /dev/null @@ -1,144 +0,0 @@ -// 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 deleted file mode 100644 index 65a827810..000000000 --- a/common/maps/ordered_test.go +++ /dev/null @@ -1,99 +0,0 @@ -// 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 deleted file mode 100644 index 819f796e4..000000000 --- a/common/maps/params.go +++ /dev/null @@ -1,384 +0,0 @@ -// Copyright 2019 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 ( - "fmt" - "strings" - - "github.com/spf13/cast" -) - -// Params is a map where all keys are lower case. -type Params map[string]any - -// KeyParams is an utility struct for the WalkParams method. -type KeyParams struct { - Key string - Params Params -} - -// GetNested does a lower case and nested search in this map. -// It will return nil if none found. -// Make all of these methods internal somehow. -func (p Params) GetNested(indices ...string) any { - v, _, _ := getNested(p, indices) - return v -} - -// SetParams overwrites values in dst with values in src for common or new keys. -// This is done recursively. -func SetParams(dst, src Params) { - for k, v := range src { - vv, found := dst[k] - if !found { - dst[k] = v - } else { - switch vvv := vv.(type) { - case Params: - if pv, ok := v.(Params); ok { - SetParams(vvv, pv) - } else { - dst[k] = v - } - default: - dst[k] = v - } - } - } -} - -// IsZero returns true if p is considered empty. -func (p Params) IsZero() bool { - if len(p) == 0 { - return true - } - - if len(p) > 1 { - return false - } - - for k := range p { - return k == MergeStrategyKey - } - - return false -} - -// MergeParamsWithStrategy transfers values from src to dst for new keys using the merge strategy given. -// This is done recursively. -func MergeParamsWithStrategy(strategy string, dst, src Params) { - dst.merge(ParamsMergeStrategy(strategy), src) -} - -// MergeParams transfers values from src to dst for new keys using the merge encoded in dst. -// This is done recursively. -func MergeParams(dst, src Params) { - ms, _ := dst.GetMergeStrategy() - dst.merge(ms, src) -} - -func (p Params) merge(ps ParamsMergeStrategy, pp Params) { - ns, found := p.GetMergeStrategy() - - ms := ns - if !found && ps != "" { - ms = ps - } - - noUpdate := ms == ParamsMergeStrategyNone - noUpdate = noUpdate || (ps != "" && ps == ParamsMergeStrategyShallow) - - for k, v := range pp { - - if k == MergeStrategyKey { - continue - } - vv, found := p[k] - - if found { - // Key matches, if both sides are Params, we try to merge. - if vvv, ok := vv.(Params); ok { - if pv, ok := v.(Params); ok { - vvv.merge(ms, pv) - } - } - } else if !noUpdate { - p[k] = v - } - - } -} - -// For internal use. -func (p Params) GetMergeStrategy() (ParamsMergeStrategy, bool) { - if v, found := p[MergeStrategyKey]; found { - if s, ok := v.(ParamsMergeStrategy); ok { - return s, true - } - } - return ParamsMergeStrategyShallow, false -} - -// For internal use. -func (p Params) DeleteMergeStrategy() bool { - if _, found := p[MergeStrategyKey]; found { - delete(p, MergeStrategyKey) - return true - } - return false -} - -// For internal use. -func (p Params) SetMergeStrategy(s ParamsMergeStrategy) { - switch s { - case ParamsMergeStrategyDeep, ParamsMergeStrategyNone, ParamsMergeStrategyShallow: - default: - panic(fmt.Sprintf("invalid merge strategy %q", s)) - } - p[MergeStrategyKey] = s -} - -func getNested(m map[string]any, indices []string) (any, string, map[string]any) { - if len(indices) == 0 { - return nil, "", nil - } - - first := indices[0] - v, found := m[strings.ToLower(cast.ToString(first))] - if !found { - if len(indices) == 1 { - return nil, first, m - } - return nil, "", nil - - } - - if len(indices) == 1 { - return v, first, m - } - - switch m2 := v.(type) { - case Params: - return getNested(m2, indices[1:]) - case map[string]any: - return getNested(m2, indices[1:]) - default: - return nil, "", nil - } -} - -// GetNestedParam gets the first match of the keyStr in the candidates given. -// It will first try the exact match and then try to find it as a nested map value, -// using the given separator, e.g. "mymap.name". -// It assumes that all the maps given have lower cased keys. -func GetNestedParam(keyStr, separator string, candidates ...Params) (any, error) { - keyStr = strings.ToLower(keyStr) - - // Try exact match first - for _, m := range candidates { - if v, ok := m[keyStr]; ok { - return v, nil - } - } - - keySegments := strings.Split(keyStr, separator) - for _, m := range candidates { - if v := m.GetNested(keySegments...); v != nil { - return v, nil - } - } - - return nil, nil -} - -func GetNestedParamFn(keyStr, separator string, lookupFn func(key string) any) (any, string, map[string]any, error) { - keySegments := strings.Split(keyStr, separator) - if len(keySegments) == 0 { - return nil, "", nil, nil - } - - first := lookupFn(keySegments[0]) - if first == nil { - return nil, "", nil, nil - } - - if len(keySegments) == 1 { - return first, keySegments[0], nil, nil - } - - switch m := first.(type) { - case map[string]any: - v, key, owner := getNested(m, keySegments[1:]) - return v, key, owner, nil - case Params: - v, key, owner := getNested(m, keySegments[1:]) - return v, key, owner, nil - } - - return nil, "", nil, nil -} - -// ParamsMergeStrategy tells what strategy to use in Params.Merge. -type ParamsMergeStrategy string - -const ( - // Do not merge. - ParamsMergeStrategyNone ParamsMergeStrategy = "none" - // Only add new keys. - ParamsMergeStrategyShallow ParamsMergeStrategy = "shallow" - // Add new keys, merge existing. - ParamsMergeStrategyDeep ParamsMergeStrategy = "deep" - - MergeStrategyKey = "_merge" -) - -// CleanConfigStringMapString removes any processing instructions from m, -// m will never be modified. -func CleanConfigStringMapString(m map[string]string) map[string]string { - if len(m) == 0 { - return m - } - if _, found := m[MergeStrategyKey]; !found { - return m - } - // Create a new map and copy all the keys except the merge strategy key. - m2 := make(map[string]string, len(m)-1) - for k, v := range m { - if k != MergeStrategyKey { - m2[k] = v - } - } - return m2 -} - -// CleanConfigStringMap is the same as CleanConfigStringMapString but for -// map[string]any. -func CleanConfigStringMap(m map[string]any) map[string]any { - if len(m) == 0 { - return m - } - if _, found := m[MergeStrategyKey]; !found { - return m - } - // Create a new map and copy all the keys except the merge strategy key. - m2 := make(map[string]any, len(m)-1) - for k, v := range m { - if k != MergeStrategyKey { - m2[k] = v - } - switch v2 := v.(type) { - case map[string]any: - m2[k] = CleanConfigStringMap(v2) - case Params: - var p Params = CleanConfigStringMap(v2) - m2[k] = p - case map[string]string: - m2[k] = CleanConfigStringMapString(v2) - } - - } - return m2 -} - -func toMergeStrategy(v any) ParamsMergeStrategy { - s := ParamsMergeStrategy(cast.ToString(v)) - switch s { - case ParamsMergeStrategyDeep, ParamsMergeStrategyNone, ParamsMergeStrategyShallow: - return s - default: - return ParamsMergeStrategyDeep - } -} - -// PrepareParams -// * 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. -func PrepareParams(m 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 = p - PrepareParams(p) - retyped = true - case map[string]any: - var p Params = v.(map[string]any) - v = p - PrepareParams(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 { - delete(m, k) - m[lKey] = v - } - } -} - -// 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 deleted file mode 100644 index 892c77175..000000000 --- a/common/maps/params_test.go +++ /dev/null @@ -1,169 +0,0 @@ -// Copyright 2019 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 TestGetNestedParam(t *testing.T) { - m := map[string]any{ - "string": "value", - "first": 1, - "with_underscore": 2, - "nested": map[string]any{ - "color": "blue", - "nestednested": map[string]any{ - "color": "green", - }, - }, - } - - c := qt.New(t) - - must := func(keyStr, separator string, candidates ...Params) any { - v, err := GetNestedParam(keyStr, separator, candidates...) - c.Assert(err, qt.IsNil) - return v - } - - c.Assert(must("first", "_", m), qt.Equals, 1) - c.Assert(must("First", "_", m), qt.Equals, 1) - c.Assert(must("with_underscore", "_", m), qt.Equals, 2) - c.Assert(must("nested_color", "_", m), qt.Equals, "blue") - c.Assert(must("nested.nestednested.color", ".", m), qt.Equals, "green") - c.Assert(must("string.name", ".", m), qt.IsNil) - c.Assert(must("nested.foo", ".", m), qt.IsNil) -} - -// https://github.com/gohugoio/hugo/issues/7903 -func TestGetNestedParamFnNestedNewKey(t *testing.T) { - c := qt.New(t) - - nested := map[string]any{ - "color": "blue", - } - m := map[string]any{ - "nested": nested, - } - - existing, nestedKey, owner, err := GetNestedParamFn("nested.new", ".", func(key string) any { - return m[key] - }) - - c.Assert(err, qt.IsNil) - c.Assert(existing, qt.IsNil) - c.Assert(nestedKey, qt.Equals, "new") - c.Assert(owner, qt.DeepEquals, nested) -} - -func TestParamsSetAndMerge(t *testing.T) { - c := qt.New(t) - - createParamsPair := func() (Params, Params) { - p1 := Params{"a": "av", "c": "cv", "nested": Params{"al2": "al2v", "cl2": "cl2v"}} - p2 := Params{"b": "bv", "a": "abv", "nested": Params{"bl2": "bl2v", "al2": "al2bv"}, MergeStrategyKey: ParamsMergeStrategyDeep} - return p1, p2 - } - - p1, p2 := createParamsPair() - - SetParams(p1, p2) - - c.Assert(p1, qt.DeepEquals, Params{ - "a": "abv", - "c": "cv", - "nested": Params{ - "al2": "al2bv", - "cl2": "cl2v", - "bl2": "bl2v", - }, - "b": "bv", - MergeStrategyKey: ParamsMergeStrategyDeep, - }) - - p1, p2 = createParamsPair() - - MergeParamsWithStrategy("", p1, p2) - - // Default is to do a shallow merge. - c.Assert(p1, qt.DeepEquals, Params{ - "c": "cv", - "nested": Params{ - "al2": "al2v", - "cl2": "cl2v", - }, - "b": "bv", - "a": "av", - }) - - p1, p2 = createParamsPair() - p1.SetMergeStrategy(ParamsMergeStrategyNone) - MergeParamsWithStrategy("", p1, p2) - p1.DeleteMergeStrategy() - - c.Assert(p1, qt.DeepEquals, Params{ - "a": "av", - "c": "cv", - "nested": Params{ - "al2": "al2v", - "cl2": "cl2v", - }, - }) - - p1, p2 = createParamsPair() - p1.SetMergeStrategy(ParamsMergeStrategyShallow) - MergeParamsWithStrategy("", p1, p2) - p1.DeleteMergeStrategy() - - c.Assert(p1, qt.DeepEquals, Params{ - "a": "av", - "c": "cv", - "nested": Params{ - "al2": "al2v", - "cl2": "cl2v", - }, - "b": "bv", - }) - - p1, p2 = createParamsPair() - p1.SetMergeStrategy(ParamsMergeStrategyDeep) - MergeParamsWithStrategy("", p1, p2) - p1.DeleteMergeStrategy() - - c.Assert(p1, qt.DeepEquals, Params{ - "nested": Params{ - "al2": "al2v", - "cl2": "cl2v", - "bl2": "bl2v", - }, - "b": "bv", - "a": "av", - "c": "cv", - }) -} - -func TestParamsIsZero(t *testing.T) { - c := qt.New(t) - - var nilParams Params - - c.Assert(Params{}.IsZero(), qt.IsTrue) - c.Assert(nilParams.IsZero(), qt.IsTrue) - c.Assert(Params{"foo": "bar"}.IsZero(), qt.IsFalse) - c.Assert(Params{"_merge": "foo", "foo": "bar"}.IsZero(), qt.IsFalse) - c.Assert(Params{"_merge": "foo"}.IsZero(), qt.IsTrue) -} diff --git a/common/maps/scratch.go b/common/maps/scratch.go deleted file mode 100644 index cf5231783..000000000 --- a/common/maps/scratch.go +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright 2019 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 ( - "reflect" - "sort" - "sync" - - "github.com/gohugoio/hugo/common/collections" - "github.com/gohugoio/hugo/common/math" -) - -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 -} - -// Add will, for single values, add (using the + operator) the addend to the existing addend (if found). -// Supports numeric values and strings. -// -// If the first add for a key is an array or slice, then the next value(s) will be appended. -func (c *Scratch) Add(key string, newAddend any) (string, error) { - var newVal any - c.mu.RLock() - existingAddend, found := c.values[key] - c.mu.RUnlock() - if found { - var err error - - addendV := reflect.TypeOf(existingAddend) - - if addendV.Kind() == reflect.Slice || addendV.Kind() == reflect.Array { - newVal, err = collections.Append(existingAddend, newAddend) - if err != nil { - return "", err - } - } else { - newVal, err = math.DoArithmetic(existingAddend, newAddend, '+') - if err != nil { - return "", err - } - } - } else { - newVal = newAddend - } - c.mu.Lock() - c.values[key] = newVal - c.mu.Unlock() - return "", nil // have to return something to make it work with the Go templates -} - -// Set stores a value with the given key in the Node context. -// This value can later be retrieved with Get. -func (c *Scratch) Set(key string, value any) string { - c.mu.Lock() - c.values[key] = value - c.mu.Unlock() - return "" -} - -// Delete deletes the given key. -func (c *Scratch) Delete(key string) string { - c.mu.Lock() - delete(c.values, key) - c.mu.Unlock() - return "" -} - -// Get returns a value previously set by Add or Set. -func (c *Scratch) Get(key string) any { - c.mu.RLock() - val := c.values[key] - c.mu.RUnlock() - - return val -} - -// Values returns the raw backing map. Note that you should just use -// this method on the locally scoped Scratch instances you obtain via newScratch, not -// .Page.Scratch etc., as that will lead to concurrency issues. -func (c *Scratch) Values() map[string]any { - c.mu.RLock() - defer c.mu.RUnlock() - return c.values -} - -// SetInMap stores a value to a map with the given key in the Node context. -// This map can later be retrieved with GetSortedMapValues. -func (c *Scratch) SetInMap(key string, mapKey string, value any) string { - c.mu.Lock() - _, found := c.values[key] - if !found { - c.values[key] = make(map[string]any) - } - - c.values[key].(map[string]any)[mapKey] = value - c.mu.Unlock() - return "" -} - -// DeleteInMap deletes a value to a map with the given key in the Node context. -func (c *Scratch) DeleteInMap(key string, mapKey string) string { - c.mu.Lock() - _, found := c.values[key] - if found { - delete(c.values[key].(map[string]any), mapKey) - } - c.mu.Unlock() - return "" -} - -// GetSortedMapValues returns a sorted map previously filled with SetInMap. -func (c *Scratch) GetSortedMapValues(key string) any { - c.mu.RLock() - - if c.values[key] == nil { - c.mu.RUnlock() - return nil - } - - unsortedMap := c.values[key].(map[string]any) - c.mu.RUnlock() - var keys []string - for mapKey := range unsortedMap { - keys = append(keys, mapKey) - } - - sort.Strings(keys) - - sortedArray := make([]any, len(unsortedMap)) - for i, mapKey := range keys { - sortedArray[i] = unsortedMap[mapKey] - } - - return sortedArray -} - -// NewScratch returns a new instance of Scratch. -func NewScratch() *Scratch { - return &Scratch{values: make(map[string]any)} -} diff --git a/common/maps/scratch_test.go b/common/maps/scratch_test.go deleted file mode 100644 index f07169e61..000000000 --- a/common/maps/scratch_test.go +++ /dev/null @@ -1,221 +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 maps - -import ( - "reflect" - "sync" - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestScratchAdd(t *testing.T) { - t.Parallel() - c := qt.New(t) - - scratch := NewScratch() - scratch.Add("int1", 10) - scratch.Add("int1", 20) - scratch.Add("int2", 20) - - c.Assert(scratch.Get("int1"), qt.Equals, int64(30)) - c.Assert(scratch.Get("int2"), qt.Equals, 20) - - scratch.Add("float1", float64(10.5)) - scratch.Add("float1", float64(20.1)) - - c.Assert(scratch.Get("float1"), qt.Equals, float64(30.6)) - - scratch.Add("string1", "Hello ") - scratch.Add("string1", "big ") - scratch.Add("string1", "World!") - - c.Assert(scratch.Get("string1"), qt.Equals, "Hello big World!") - - scratch.Add("scratch", scratch) - _, err := scratch.Add("scratch", scratch) - - m := scratch.Values() - c.Assert(m, qt.HasLen, 5) - - if err == nil { - t.Errorf("Expected error from invalid arithmetic") - } -} - -func TestScratchAddSlice(t *testing.T) { - t.Parallel() - c := qt.New(t) - - scratch := NewScratch() - - _, err := scratch.Add("intSlice", []int{1, 2}) - c.Assert(err, qt.IsNil) - _, err = scratch.Add("intSlice", 3) - c.Assert(err, qt.IsNil) - - sl := scratch.Get("intSlice") - expected := []int{1, 2, 3} - - if !reflect.DeepEqual(expected, sl) { - t.Errorf("Slice difference, go %q expected %q", sl, expected) - } - _, err = scratch.Add("intSlice", []int{4, 5}) - - c.Assert(err, qt.IsNil) - - sl = scratch.Get("intSlice") - expected = []int{1, 2, 3, 4, 5} - - if !reflect.DeepEqual(expected, sl) { - t.Errorf("Slice difference, go %q expected %q", sl, expected) - } -} - -// https://github.com/gohugoio/hugo/issues/5275 -func TestScratchAddTypedSliceToInterfaceSlice(t *testing.T) { - t.Parallel() - c := qt.New(t) - - scratch := NewScratch() - scratch.Set("slice", []any{}) - - _, err := scratch.Add("slice", []int{1, 2}) - c.Assert(err, qt.IsNil) - c.Assert(scratch.Get("slice"), qt.DeepEquals, []int{1, 2}) -} - -// https://github.com/gohugoio/hugo/issues/5361 -func TestScratchAddDifferentTypedSliceToInterfaceSlice(t *testing.T) { - t.Parallel() - c := qt.New(t) - - scratch := NewScratch() - scratch.Set("slice", []string{"foo"}) - - _, err := scratch.Add("slice", []int{1, 2}) - c.Assert(err, qt.IsNil) - c.Assert(scratch.Get("slice"), qt.DeepEquals, []any{"foo", 1, 2}) -} - -func TestScratchSet(t *testing.T) { - t.Parallel() - c := qt.New(t) - - scratch := NewScratch() - scratch.Set("key", "val") - c.Assert(scratch.Get("key"), qt.Equals, "val") -} - -func TestScratchDelete(t *testing.T) { - t.Parallel() - c := qt.New(t) - - scratch := NewScratch() - scratch.Set("key", "val") - scratch.Delete("key") - scratch.Add("key", "Lucy Parsons") - c.Assert(scratch.Get("key"), qt.Equals, "Lucy Parsons") -} - -// Issue #2005 -func TestScratchInParallel(t *testing.T) { - var wg sync.WaitGroup - scratch := NewScratch() - - key := "counter" - scratch.Set(key, int64(1)) - for i := 1; i <= 10; i++ { - wg.Add(1) - go func(j int) { - for k := range 10 { - newVal := int64(k + j) - - _, err := scratch.Add(key, newVal) - if err != nil { - t.Errorf("Got err %s", err) - } - - scratch.Set(key, newVal) - - val := scratch.Get(key) - - if counter, ok := val.(int64); ok { - if counter < 1 { - t.Errorf("Got %d", counter) - } - } else { - t.Errorf("Got %T", val) - } - } - wg.Done() - }(i) - } - wg.Wait() -} - -func TestScratchGet(t *testing.T) { - t.Parallel() - scratch := NewScratch() - nothing := scratch.Get("nothing") - if nothing != nil { - t.Errorf("Should not return anything, but got %v", nothing) - } -} - -func TestScratchSetInMap(t *testing.T) { - t.Parallel() - c := qt.New(t) - - scratch := NewScratch() - scratch.SetInMap("key", "lux", "Lux") - scratch.SetInMap("key", "abc", "Abc") - scratch.SetInMap("key", "zyx", "Zyx") - scratch.SetInMap("key", "abc", "Abc (updated)") - scratch.SetInMap("key", "def", "Def") - c.Assert(scratch.GetSortedMapValues("key"), qt.DeepEquals, any([]any{"Abc (updated)", "Def", "Lux", "Zyx"})) -} - -func TestScratchDeleteInMap(t *testing.T) { - t.Parallel() - c := qt.New(t) - - scratch := NewScratch() - scratch.SetInMap("key", "lux", "Lux") - scratch.SetInMap("key", "abc", "Abc") - scratch.SetInMap("key", "zyx", "Zyx") - scratch.DeleteInMap("key", "abc") - scratch.SetInMap("key", "def", "Def") - scratch.DeleteInMap("key", "lmn") // Do nothing - c.Assert(scratch.GetSortedMapValues("key"), qt.DeepEquals, any([]any{"Def", "Lux", "Zyx"})) -} - -func TestScratchGetSortedMapValues(t *testing.T) { - t.Parallel() - scratch := NewScratch() - nothing := scratch.GetSortedMapValues("nothing") - if nothing != nil { - t.Errorf("Should not return anything, but got %v", nothing) - } -} - -func BenchmarkScratchGet(b *testing.B) { - scratch := NewScratch() - scratch.Add("A", 1) - b.ResetTimer() - for i := 0; i < b.N; i++ { - scratch.Get("A") - } -} diff --git a/common/math/math.go b/common/math/math.go deleted file mode 100644 index f88fbcd9c..000000000 --- a/common/math/math.go +++ /dev/null @@ -1,133 +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 math - -import ( - "errors" - "reflect" -) - -// DoArithmetic performs arithmetic operations (+,-,*,/) using reflection to -// determine the type of the two terms. -func DoArithmetic(a, b any, op rune) (any, error) { - av := reflect.ValueOf(a) - bv := reflect.ValueOf(b) - 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 - 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) - } else { - isInt = true - bi = int64(bu) // may overflow - } - 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: - bf = float64(bv.Int()) // may overflow - case reflect.Float32, reflect.Float64: - bf = bv.Float() - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - bf = float64(bv.Uint()) // may overflow - default: - return nil, errors.New("can't apply the operator to the values") - } - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - au = av.Uint() - switch bv.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - bi = bv.Int() - if bi >= 0 { - isUint = true - bu = uint64(bi) - } else { - isInt = true - ai = int64(au) // may overflow - } - case reflect.Float32, reflect.Float64: - isFloat = true - af = float64(au) // may overflow - 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") - } - case reflect.String: - as := av.String() - if bv.Kind() == reflect.String && op == '+' { - bs := bv.String() - return as + bs, nil - } - return nil, errors.New("can't apply the operator to the values") - default: - return nil, errors.New("can't apply the operator to the values") - } - - switch op { - case '+': - if isInt { - return ai + bi, nil - } else if isFloat { - return af + bf, nil - } - return au + bu, nil - case '-': - if isInt { - return ai - bi, nil - } else if isFloat { - return af - bf, nil - } - return au - bu, nil - case '*': - if isInt { - return ai * bi, nil - } else if isFloat { - return af * bf, nil - } - return au * bu, nil - case '/': - if isInt && bi != 0 { - return ai / bi, nil - } else if isFloat && bf != 0 { - return af / bf, nil - } else if isUint && bu != 0 { - return au / bu, nil - } - return nil, errors.New("can't divide the value by 0") - default: - return nil, errors.New("there is no such an operation") - } -} diff --git a/common/math/math_test.go b/common/math/math_test.go deleted file mode 100644 index d75d30a69..000000000 --- a/common/math/math_test.go +++ /dev/null @@ -1,111 +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 math - -import ( - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestDoArithmetic(t *testing.T) { - t.Parallel() - c := qt.New(t) - - for _, test := range []struct { - a any - b any - op rune - 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)}, - {3, 2.0, '+', float64(5)}, - {3, 2.0, '-', float64(1)}, - {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)}, - {uint(3), -2, '+', int64(1)}, - {uint(3), -2, '-', int64(5)}, - {uint(3), -2, '*', int64(-6)}, - {uint(3), -2, '/', int64(-1)}, - {-3, uint(2), '+', int64(-1)}, - {-3, uint(2), '-', int64(-5)}, - {-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)}, - {"foo", "bar", '+', "foobar"}, - {3, 0, '/', false}, - {3.0, 0, '/', false}, - {3, 0.0, '/', false}, - {uint(3), uint(0), '/', false}, - {3, uint(0), '/', false}, - {-3, uint(0), '/', false}, - {uint(3), 0, '/', false}, - {3.0, uint(0), '/', false}, - {uint(3), 0.0, '/', false}, - {3, "foo", '+', false}, - {3.0, "foo", '+', false}, - {uint(3), "foo", '+', false}, - {"foo", 3, '+', false}, - {"foo", "bar", '-', false}, - {3, 2, '%', false}, - } { - result, err := DoArithmetic(test.a, test.b, test.op) - - if b, ok := test.expect.(bool); ok && !b { - c.Assert(err, qt.Not(qt.IsNil)) - continue - } - - c.Assert(err, qt.IsNil) - c.Assert(test.expect, qt.Equals, result) - } -} diff --git a/common/para/para.go b/common/para/para.go deleted file mode 100644 index c323a3073..000000000 --- a/common/para/para.go +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2019 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 para implements parallel execution helpers. -package para - -import ( - "context" - - "golang.org/x/sync/errgroup" -) - -// Workers configures a task executor with the most number of tasks to be executed in parallel. -type Workers struct { - sem chan struct{} -} - -// Runner wraps the lifecycle methods of a new task set. -// -// Run will block until a worker is available or the context is cancelled, -// and then run the given func in a new goroutine. -// Wait will wait for all the running goroutines to finish. -type Runner interface { - Run(func() error) - Wait() error -} - -type errGroupRunner struct { - *errgroup.Group - w *Workers - ctx context.Context -} - -func (g *errGroupRunner) Run(fn func() error) { - select { - case g.w.sem <- struct{}{}: - case <-g.ctx.Done(): - return - } - - g.Go(func() error { - err := fn() - <-g.w.sem - return err - }) -} - -// New creates a new Workers with the given number of workers. -func New(numWorkers int) *Workers { - return &Workers{ - sem: make(chan struct{}, numWorkers), - } -} - -// Start starts a new Runner. -func (w *Workers) Start(ctx context.Context) (Runner, context.Context) { - g, ctx := errgroup.WithContext(ctx) - return &errGroupRunner{ - Group: g, - ctx: ctx, - w: w, - }, ctx -} diff --git a/common/para/para_test.go b/common/para/para_test.go deleted file mode 100644 index cf24a4e37..000000000 --- a/common/para/para_test.go +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2019 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 para - -import ( - "context" - "runtime" - "sort" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/gohugoio/hugo/htesting" - - qt "github.com/frankban/quicktest" -) - -func TestPara(t *testing.T) { - if runtime.NumCPU() < 4 { - t.Skipf("skip para test, CPU count is %d", runtime.NumCPU()) - } - - // TODO(bep) - if htesting.IsCI() { - t.Skip("skip para test when running on CI") - } - - c := qt.New(t) - - c.Run("Order", func(c *qt.C) { - n := 500 - ints := make([]int, n) - for i := range n { - ints[i] = i - } - - p := New(4) - r, _ := p.Start(context.Background()) - - var result []int - var mu sync.Mutex - for i := range n { - i := i - r.Run(func() error { - mu.Lock() - defer mu.Unlock() - result = append(result, i) - return nil - }) - } - - c.Assert(r.Wait(), qt.IsNil) - c.Assert(result, qt.HasLen, len(ints)) - c.Assert(sort.IntsAreSorted(result), qt.Equals, false, qt.Commentf("Para does not seem to be parallel")) - sort.Ints(result) - c.Assert(result, qt.DeepEquals, ints) - }) - - c.Run("Time", func(c *qt.C) { - const n = 100 - - p := New(5) - r, _ := p.Start(context.Background()) - - start := time.Now() - - var counter int64 - - for range n { - r.Run(func() error { - atomic.AddInt64(&counter, 1) - time.Sleep(1 * time.Millisecond) - return nil - }) - } - - c.Assert(r.Wait(), qt.IsNil) - c.Assert(counter, qt.Equals, int64(n)) - - since := time.Since(start) - limit := n / 2 * time.Millisecond - c.Assert(since < limit, qt.Equals, true, qt.Commentf("%s >= %s", since, limit)) - }) -} diff --git a/common/paths/path.go b/common/paths/path.go deleted file mode 100644 index de91d6a2f..000000000 --- a/common/paths/path.go +++ /dev/null @@ -1,430 +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 paths - -import ( - "errors" - "fmt" - "net/url" - "path" - "path/filepath" - "strings" - "unicode" -) - -// FilePathSeparator as defined by os.Separator. -const ( - FilePathSeparator = string(filepath.Separator) - slash = "/" -) - -// filepathPathBridge is a bridge for common functionality in filepath vs path -type filepathPathBridge interface { - Base(in string) string - Clean(in string) string - Dir(in string) string - Ext(in string) string - Join(elem ...string) string - Separator() string -} - -type filepathBridge struct{} - -func (filepathBridge) Base(in string) string { - return filepath.Base(in) -} - -func (filepathBridge) Clean(in string) string { - return filepath.Clean(in) -} - -func (filepathBridge) Dir(in string) string { - return filepath.Dir(in) -} - -func (filepathBridge) Ext(in string) string { - return filepath.Ext(in) -} - -func (filepathBridge) Join(elem ...string) string { - return filepath.Join(elem...) -} - -func (filepathBridge) Separator() string { - return FilePathSeparator -} - -var fpb filepathBridge - -// AbsPathify creates an absolute path if given a working dir and a relative path. -// If already absolute, the path is just cleaned. -func AbsPathify(workingDir, inPath string) string { - if filepath.IsAbs(inPath) { - return filepath.Clean(inPath) - } - 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 { - return strings.Replace(strings.TrimSpace(inpath), "-", " ", -1) -} - -// ReplaceExtension takes a path and an extension, strips the old extension -// and returns the path with the new extension. -func ReplaceExtension(path string, newExt string) string { - f, _ := fileAndExt(path, fpb) - return f + "." + newExt -} - -func makePathRelative(inPath string, possibleDirectories ...string) (string, error) { - for _, currentPath := range possibleDirectories { - if strings.HasPrefix(inPath, currentPath) { - return strings.TrimPrefix(inPath, currentPath), nil - } - } - return inPath, errors.New("can't extract relative path, unknown prefix") -} - -// ExtNoDelimiter takes a path and returns the extension, excluding the delimiter, i.e. "md". -func ExtNoDelimiter(in string) string { - return strings.TrimPrefix(Ext(in), ".") -} - -// Ext takes a path and returns the extension, including the delimiter, i.e. ".md". -func Ext(in string) string { - _, ext := fileAndExt(in, fpb) - return ext -} - -// PathAndExt is the same as FileAndExt, but it uses the path package. -func PathAndExt(in string) (string, string) { - return fileAndExt(in, pb) -} - -// FileAndExt takes a path and returns the file and extension separated, -// the extension including the delimiter, i.e. ".md". -func FileAndExt(in string) (string, string) { - return fileAndExt(in, fpb) -} - -// FileAndExtNoDelimiter takes a path and returns the file and extension separated, -// the extension excluding the delimiter, e.g "md". -func FileAndExtNoDelimiter(in string) (string, string) { - file, ext := fileAndExt(in, fpb) - return file, strings.TrimPrefix(ext, ".") -} - -// Filename takes a file path, strips out the extension, -// and returns the name of the file. -func Filename(in string) (name string) { - name, _ = fileAndExt(in, fpb) - return -} - -// FileAndExt returns the filename and any extension of a file path as -// two separate strings. -// -// If the path, in, contains a directory name ending in a slash, -// then both name and ext will be empty strings. -// -// If the path, in, is either the current directory, the parent -// directory or the root directory, or an empty string, -// then both name and ext will be empty strings. -// -// If the path, in, represents the path of a file without an extension, -// then name will be the name of the file and ext will be an empty string. -// -// If the path, in, represents a filename with an extension, -// then name will be the filename minus any extension - including the dot -// and ext will contain the extension - minus the dot. -func fileAndExt(in string, b filepathPathBridge) (name string, ext string) { - ext = b.Ext(in) - base := b.Base(in) - - return extractFilename(in, ext, base, b.Separator()), ext -} - -func extractFilename(in, ext, base, pathSeparator string) (name string) { - // No file name cases. These are defined as: - // 1. any "in" path that ends in a pathSeparator - // 2. any "base" consisting of just an pathSeparator - // 3. any "base" consisting of just an empty string - // 4. any "base" consisting of just the current directory i.e. "." - // 5. any "base" consisting of just the parent directory i.e. ".." - if (strings.LastIndex(in, pathSeparator) == len(in)-1) || base == "" || base == "." || base == ".." || base == pathSeparator { - name = "" // there is NO filename - } else if ext != "" { // there was an Extension - // return the filename minus the extension (and the ".") - name = base[:strings.LastIndex(base, ".")] - } else { - // no extension case so just return base, which will - // be the filename - name = base - } - return -} - -// GetRelativePath returns the relative path of a given path. -func GetRelativePath(path, base string) (final string, err error) { - if filepath.IsAbs(path) && base == "" { - return "", errors.New("source: missing base directory") - } - name := filepath.Clean(path) - base = filepath.Clean(base) - - name, err = filepath.Rel(base, name) - if err != nil { - return "", err - } - - if strings.HasSuffix(filepath.FromSlash(path), FilePathSeparator) && !strings.HasSuffix(name, FilePathSeparator) { - name += FilePathSeparator - } - return name, nil -} - -func prettifyPath(in string, b filepathPathBridge) string { - if filepath.Ext(in) == "" { - // /section/name/ -> /section/name/index.html - if len(in) < 2 { - return b.Separator() - } - return b.Join(in, "index.html") - } - name, ext := fileAndExt(in, b) - if name == "index" { - // /section/name/index.html -> /section/name/index.html - return b.Clean(in) - } - // /section/name.html -> /section/name/index.html - return b.Join(b.Dir(in), name, "index"+ext) -} - -// 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 -} - -// 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 - } - } - - 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. -type DirFile struct { - Dir string - File string -} - -// Used in test. -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 deleted file mode 100644 index bc27df6c6..000000000 --- a/common/paths/path_test.go +++ /dev/null @@ -1,313 +0,0 @@ -// 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" - - qt "github.com/frankban/quicktest" -) - -func TestGetRelativePath(t *testing.T) { - tests := []struct { - path string - base string - expect any - }{ - {filepath.FromSlash("/a/b"), filepath.FromSlash("/a"), filepath.FromSlash("b")}, - {filepath.FromSlash("/a/b/c/"), filepath.FromSlash("/a"), filepath.FromSlash("b/c/")}, - {filepath.FromSlash("/c"), filepath.FromSlash("/a/b"), filepath.FromSlash("../../c")}, - {filepath.FromSlash("/c"), "", false}, - } - for i, this := range tests { - // ultimately a fancy wrapper around filepath.Rel - result, err := GetRelativePath(this.path, this.base) - - if b, ok := this.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] GetRelativePath didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] GetRelativePath failed: %s", i, err) - continue - } - if result != this.expect { - t.Errorf("[%d] GetRelativePath got %v but expected %v", i, result, this.expect) - } - } - - } -} - -func TestMakePathRelative(t *testing.T) { - type test struct { - inPath, path1, path2, output string - } - - data := []test{ - {"/abc/bcd/ab.css", "/abc/bcd", "/bbc/bcd", "/ab.css"}, - {"/abc/bcd/ab.css", "/abcd/bcd", "/abc/bcd", "/ab.css"}, - } - - for i, d := range data { - output, _ := makePathRelative(d.inPath, d.path1, d.path2) - if d.output != output { - t.Errorf("Test #%d failed. Expected %q got %q", i, d.output, output) - } - } - _, error := makePathRelative("a/b/c.ss", "/a/c", "/d/c", "/e/f") - - if error == nil { - t.Errorf("Test failed, expected error") - } -} - -func TestMakeTitle(t *testing.T) { - type test struct { - input, expected string - } - data := []test{ - {"Make-Title", "Make Title"}, - {"MakeTitle", "MakeTitle"}, - {"make_title", "make_title"}, - } - for i, d := range data { - output := MakeTitle(d.input) - if d.expected != output { - t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output) - } - } -} - -// Replace Extension is probably poorly named, but the intent of the -// function is to accept a path and return only the file name with a -// new extension. It's intentionally designed to strip out the path -// and only provide the name. We should probably rename the function to -// be more explicit at some point. -func TestReplaceExtension(t *testing.T) { - type test struct { - input, newext, expected string - } - data := []test{ - // These work according to the above definition - {"/some/random/path/file.xml", "html", "file.html"}, - {"/banana.html", "xml", "banana.xml"}, - {"./banana.html", "xml", "banana.xml"}, - {"banana/pie/index.html", "xml", "index.xml"}, - {"../pies/fish/index.html", "xml", "index.xml"}, - // but these all fail - {"filename-without-an-ext", "ext", "filename-without-an-ext.ext"}, - {"/filename-without-an-ext", "ext", "filename-without-an-ext.ext"}, - {"/directory/mydir/", "ext", ".ext"}, - {"mydir/", "ext", ".ext"}, - } - - for i, d := range data { - output := ReplaceExtension(filepath.FromSlash(d.input), d.newext) - if d.expected != output { - t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output) - } - } -} - -func TestExtNoDelimiter(t *testing.T) { - c := qt.New(t) - c.Assert(ExtNoDelimiter(filepath.FromSlash("/my/data.json")), qt.Equals, "json") -} - -func TestFilename(t *testing.T) { - type test struct { - input, expected string - } - data := []test{ - {"index.html", "index"}, - {"./index.html", "index"}, - {"/index.html", "index"}, - {"index", "index"}, - {"/tmp/index.html", "index"}, - {"./filename-no-ext", "filename-no-ext"}, - {"/filename-no-ext", "filename-no-ext"}, - {"filename-no-ext", "filename-no-ext"}, - {"directory/", ""}, // no filename case?? - {"directory/.hidden.ext", ".hidden"}, - {"./directory/../~/banana/gold.fish", "gold"}, - {"../directory/banana.man", "banana"}, - {"~/mydir/filename.ext", "filename"}, - {"./directory//tmp/filename.ext", "filename"}, - } - - for i, d := range data { - output := Filename(filepath.FromSlash(d.input)) - if d.expected != output { - t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output) - } - } -} - -func TestFileAndExt(t *testing.T) { - type test struct { - input, expectedFile, expectedExt string - } - data := []test{ - {"index.html", "index", ".html"}, - {"./index.html", "index", ".html"}, - {"/index.html", "index", ".html"}, - {"index", "index", ""}, - {"/tmp/index.html", "index", ".html"}, - {"./filename-no-ext", "filename-no-ext", ""}, - {"/filename-no-ext", "filename-no-ext", ""}, - {"filename-no-ext", "filename-no-ext", ""}, - {"directory/", "", ""}, // no filename case?? - {"directory/.hidden.ext", ".hidden", ".ext"}, - {"./directory/../~/banana/gold.fish", "gold", ".fish"}, - {"../directory/banana.man", "banana", ".man"}, - {"~/mydir/filename.ext", "filename", ".ext"}, - {"./directory//tmp/filename.ext", "filename", ".ext"}, - } - - for i, d := range data { - file, ext := fileAndExt(filepath.FromSlash(d.input), fpb) - if d.expectedFile != file { - t.Errorf("Test %d failed. Expected filename %q got %q.", i, d.expectedFile, file) - } - if d.expectedExt != ext { - t.Errorf("Test %d failed. Expected extension %q got %q.", i, d.expectedExt, ext) - } - } -} - -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 deleted file mode 100644 index 1cae710e8..000000000 --- a/common/paths/pathparser.go +++ /dev/null @@ -1,788 +0,0 @@ -// 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 typ == "" || 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 deleted file mode 100644 index b1734aef2..000000000 --- a/common/paths/pathparser_test.go +++ /dev/null @@ -1,611 +0,0 @@ -// 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 deleted file mode 100644 index f5ea3066a..000000000 --- a/common/paths/paths_integration_test.go +++ /dev/null @@ -1,103 +0,0 @@ -// 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 deleted file mode 100644 index 08fbcc835..000000000 --- a/common/paths/type_string.go +++ /dev/null @@ -1,32 +0,0 @@ -// 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 deleted file mode 100644 index 1d1408b51..000000000 --- a/common/paths/url.go +++ /dev/null @@ -1,273 +0,0 @@ -// 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 ( - "fmt" - "net/url" - "path" - "path/filepath" - "runtime" - "strings" -) - -type pathBridge struct{} - -func (pathBridge) Base(in string) string { - return path.Base(in) -} - -func (pathBridge) Clean(in string) string { - return path.Clean(in) -} - -func (pathBridge) Dir(in string) string { - return path.Dir(in) -} - -func (pathBridge) Ext(in string) string { - return path.Ext(in) -} - -func (pathBridge) Join(elem ...string) string { - return path.Join(elem...) -} - -func (pathBridge) Separator() string { - return "/" -} - -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 -func MakePermalink(host, plink string) *url.URL { - base, err := url.Parse(host) - if err != nil { - panic(err) - } - - p, err := url.Parse(plink) - if err != nil { - panic(err) - } - - if p.Host != "" { - panic(fmt.Errorf("can't make permalink from absolute link %q", plink)) - } - - 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, "/") - if hadTrailingSlash && !strings.HasSuffix(base.Path, "/") { - base.Path = base.Path + "/" - } - - return base -} - -// AddContextRoot adds the context root to an URL if it's not already set. -// For relative URL entries on sites with a base url with a context root set (i.e. http://example.com/mysite), -// relative URLs must not include the context root if canonifyURLs is enabled. But if it's disabled, it must be set. -func AddContextRoot(baseURL, relativePath string) string { - url, err := url.Parse(baseURL) - if err != nil { - panic(err) - } - - newPath := path.Join(url.Path, relativePath) - - // path strips trailing slash, ignore root path. - if newPath != "/" && strings.HasSuffix(relativePath, "/") { - newPath += "/" - } - return newPath -} - -// URLizeAn - -// PrettifyURL takes a URL string and returns a semantic, clean URL. -func PrettifyURL(in string) string { - x := PrettifyURLPath(in) - - if path.Base(x) == "index.html" { - return path.Dir(x) - } - - if in == "" { - return "/" - } - - return x -} - -// 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 -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 -func Uglify(in string) string { - if path.Ext(in) == "" { - if len(in) < 2 { - return "/" - } - // /section/name/ -> /section/name.html - return path.Clean(in) + ".html" - } - - name, ext := fileAndExt(in, pb) - if name == "index" { - // /section/name/index.html -> /section/name.html - d := path.Dir(in) - if len(d) > 1 { - return d + ext - } - return in - } - // /.xml -> /index.xml - if name == "" { - return path.Dir(in) + "index" + ext - } - // /section/name.html -> /section/name.html - return path.Clean(in) -} - -// 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 - } - - p := u.Path - - if p == "" { - p, _ = url.QueryUnescape(u.Opaque) - 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 len(u.Host) == 1 { - // file://c/Users/... - return strings.ToUpper(u.Host) + ":" + 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 deleted file mode 100644 index 5a9233c26..000000000 --- a/common/paths/url_test.go +++ /dev/null @@ -1,100 +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 paths - -import ( - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestMakePermalink(t *testing.T) { - type test struct { - host, link, output string - } - - data := []test{ - {"http://abc.com/foo", "post/bar", "http://abc.com/foo/post/bar"}, - {"http://abc.com/foo/", "post/bar", "http://abc.com/foo/post/bar"}, - {"http://abc.com", "post/bar", "http://abc.com/post/bar"}, - {"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 { - output := MakePermalink(d.host, d.link).String() - if d.output != output { - t.Errorf("Test #%d failed. Expected %q got %q", i, d.output, output) - } - } -} - -func TestAddContextRoot(t *testing.T) { - tests := []struct { - baseURL string - url string - expected string - }{ - {"http://example.com/sub/", "/foo", "/sub/foo"}, - {"http://example.com/sub/", "/foo/index.html", "/sub/foo/index.html"}, - {"http://example.com/sub1/sub2", "/foo", "/sub1/sub2/foo"}, - {"http://example.com", "/foo", "/foo"}, - // cannot guess that the context root is already added int the example below - {"http://example.com/sub/", "/sub/foo", "/sub/sub/foo"}, - {"http://example.com/тря", "/трям/", "/тря/трям/"}, - {"http://example.com", "/", "/"}, - {"http://example.com/bar", "//", "/bar/"}, - } - - for _, test := range tests { - output := AddContextRoot(test.baseURL, test.url) - if output != test.expected { - t.Errorf("Expected %#v, got %#v\n", test.expected, output) - } - } -} - -func TestPretty(t *testing.T) { - c := qt.New(t) - c.Assert("/section/name/index.html", qt.Equals, PrettifyURLPath("/section/name.html")) - c.Assert("/section/sub/name/index.html", qt.Equals, PrettifyURLPath("/section/sub/name.html")) - c.Assert("/section/name/index.html", qt.Equals, PrettifyURLPath("/section/name/")) - c.Assert("/section/name/index.html", qt.Equals, PrettifyURLPath("/section/name/index.html")) - c.Assert("/index.html", qt.Equals, PrettifyURLPath("/index.html")) - c.Assert("/name/index.xml", qt.Equals, PrettifyURLPath("/name.xml")) - c.Assert("/", qt.Equals, PrettifyURLPath("/")) - c.Assert("/", qt.Equals, PrettifyURLPath("")) - c.Assert("/section/name", qt.Equals, PrettifyURL("/section/name.html")) - c.Assert("/section/sub/name", qt.Equals, PrettifyURL("/section/sub/name.html")) - c.Assert("/section/name", qt.Equals, PrettifyURL("/section/name/")) - c.Assert("/section/name", qt.Equals, PrettifyURL("/section/name/index.html")) - c.Assert("/", qt.Equals, PrettifyURL("/index.html")) - c.Assert("/name/index.xml", qt.Equals, PrettifyURL("/name.xml")) - c.Assert("/", qt.Equals, PrettifyURL("/")) - c.Assert("/", qt.Equals, PrettifyURL("")) -} - -func TestUgly(t *testing.T) { - c := qt.New(t) - c.Assert("/section/name.html", qt.Equals, Uglify("/section/name.html")) - c.Assert("/section/sub/name.html", qt.Equals, Uglify("/section/sub/name.html")) - c.Assert("/section/name.html", qt.Equals, Uglify("/section/name/")) - c.Assert("/section/name.html", qt.Equals, Uglify("/section/name/index.html")) - c.Assert("/index.html", qt.Equals, Uglify("/index.html")) - c.Assert("/name.xml", qt.Equals, Uglify("/name.xml")) - c.Assert("/", qt.Equals, Uglify("/")) - c.Assert("/", qt.Equals, Uglify("")) -} diff --git a/common/predicate/predicate.go b/common/predicate/predicate.go deleted file mode 100644 index f71536474..000000000 --- a/common/predicate/predicate.go +++ /dev/null @@ -1,78 +0,0 @@ -// 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 deleted file mode 100644 index 1e1ec004b..000000000 --- a/common/predicate/predicate_test.go +++ /dev/null @@ -1,83 +0,0 @@ -// 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 deleted file mode 100644 index 80a730ca9..000000000 --- a/common/rungroup/rungroup.go +++ /dev/null @@ -1,93 +0,0 @@ -// 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 deleted file mode 100644 index ac902079e..000000000 --- a/common/rungroup/rungroup_test.go +++ /dev/null @@ -1,44 +0,0 @@ -// 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 deleted file mode 100644 index 3f8a754e9..000000000 --- a/common/tasks/tasks.go +++ /dev/null @@ -1,150 +0,0 @@ -// 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 deleted file mode 100644 index fef6efce8..000000000 --- a/common/terminal/colors.go +++ /dev/null @@ -1,74 +0,0 @@ -// 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 terminal contains helper for the terminal, such as coloring output. -package terminal - -import ( - "fmt" - "os" - "strings" - - isatty "github.com/mattn/go-isatty" -) - -const ( - errorColor = "\033[1;31m%s\033[0m" - warningColor = "\033[0;33m%s\033[0m" - noticeColor = "\033[1;36m%s\033[0m" -) - -// PrintANSIColors returns false if NO_COLOR env variable is set, -// else IsTerminal(f). -func PrintANSIColors(f *os.File) bool { - if os.Getenv("NO_COLOR") != "" { - return false - } - return IsTerminal(f) -} - -// 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 { - fd := f.Fd() - return os.Getenv("TERM") != "dumb" && (isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd)) -} - -// Notice colorizes the string in a noticeable color. -func Notice(s string) string { - return colorize(s, noticeColor) -} - -// Error colorizes the string in a colour that grabs attention. -func Error(s string) string { - return colorize(s, errorColor) -} - -// Warning colorizes the string in a colour that warns. -func Warning(s string) string { - return colorize(s, warningColor) -} - -// colorize s in color. -func colorize(s, color string) string { - s = fmt.Sprintf(color, doublePercent(s)) - return singlePercent(s) -} - -func doublePercent(str string) string { - return strings.Replace(str, "%", "%%", -1) -} - -func singlePercent(str string) string { - return strings.Replace(str, "%%", "%", -1) -} diff --git a/common/text/position.go b/common/text/position.go deleted file mode 100644 index eb9de5624..000000000 --- a/common/text/position.go +++ /dev/null @@ -1,100 +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 text - -import ( - "fmt" - "os" - "strings" - - "github.com/gohugoio/hugo/common/terminal" -) - -// Positioner represents a thing that knows its position in a text file or stream, -// typically an error. -type Positioner interface { - // Position returns the current position. - // Useful in error logging, e.g. {{ errorf "error in code block: %s" .Position }}. - Position() Position -} - -// Position holds a source position in a text file or stream. -type Position struct { - Filename string // filename, if any - Offset int // byte offset, starting at 0. It's set to -1 if not provided. - LineNumber int // line number, starting at 1 - ColumnNumber int // column number, starting at 1 (character count per line) -} - -func (pos Position) String() string { - if pos.Filename == "" { - pos.Filename = "" - } - return positionStringFormatfunc(pos) -} - -// IsValid returns true if line number is > 0. -func (pos Position) IsValid() bool { - return pos.LineNumber > 0 -} - -var positionStringFormatfunc func(p Position) string - -func createPositionStringFormatter(formatStr string) func(p Position) string { - if formatStr == "" { - formatStr = "\":file::line::col\"" - } - - identifiers := []string{":file", ":line", ":col"} - var identifiersFound []string - - for i := range formatStr { - for _, id := range identifiers { - if strings.HasPrefix(formatStr[i:], id) { - identifiersFound = append(identifiersFound, id) - } - } - } - - replacer := strings.NewReplacer(":file", "%s", ":line", "%d", ":col", "%d") - format := replacer.Replace(formatStr) - - f := func(pos Position) string { - args := make([]any, len(identifiersFound)) - for i, id := range identifiersFound { - switch id { - case ":file": - args[i] = pos.Filename - case ":line": - args[i] = pos.LineNumber - case ":col": - args[i] = pos.ColumnNumber - } - } - - msg := fmt.Sprintf(format, args...) - - if terminal.PrintANSIColors(os.Stdout) { - return terminal.Notice(msg) - } - - return msg - } - - return f -} - -func init() { - positionStringFormatfunc = createPositionStringFormatter(os.Getenv("HUGO_FILE_LOG_FORMAT")) -} diff --git a/common/text/position_test.go b/common/text/position_test.go deleted file mode 100644 index a1f43c5d4..000000000 --- a/common/text/position_test.go +++ /dev/null @@ -1,32 +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 text - -import ( - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestPositionStringFormatter(t *testing.T) { - c := qt.New(t) - - pos := Position{Filename: "/my/file.txt", LineNumber: 12, ColumnNumber: 13, Offset: 14} - - c.Assert(createPositionStringFormatter(":file|:col|:line")(pos), qt.Equals, "/my/file.txt|13|12") - c.Assert(createPositionStringFormatter(":col|:file|:line")(pos), qt.Equals, "13|/my/file.txt|12") - c.Assert(createPositionStringFormatter("好::col")(pos), qt.Equals, "好:13") - c.Assert(createPositionStringFormatter("")(pos), qt.Equals, "\"/my/file.txt:12:13\"") - c.Assert(pos.String(), qt.Equals, "\"/my/file.txt:12:13\"") -} diff --git a/common/text/transform.go b/common/text/transform.go deleted file mode 100644 index de093af0d..000000000 --- a/common/text/transform.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2019 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 text - -import ( - "strings" - "sync" - "unicode" - - "golang.org/x/text/runes" - "golang.org/x/text/transform" - "golang.org/x/text/unicode/norm" -) - -var accentTransformerPool = &sync.Pool{ - New: func() any { - return transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC) - }, -} - -// RemoveAccents removes all accents from b. -func RemoveAccents(b []byte) []byte { - t := accentTransformerPool.Get().(transform.Transformer) - b, _, _ = transform.Bytes(t, b) - t.Reset() - accentTransformerPool.Put(t) - return b -} - -// RemoveAccentsString removes all accents from s. -func RemoveAccentsString(s string) string { - t := accentTransformerPool.Get().(transform.Transformer) - s, _, _ = transform.String(t, s) - t.Reset() - accentTransformerPool.Put(t) - return s -} - -// Chomp removes trailing newline characters from s. -func Chomp(s string) string { - return strings.TrimRightFunc(s, func(r rune) bool { - return r == '\n' || r == '\r' - }) -} - -// Puts adds a trailing \n none found. -func Puts(s string) string { - if s == "" || s[len(s)-1] == '\n' { - return s - } - return s + "\n" -} - -// VisitLinesAfter calls the given function for each line, including newlines, in the given string. -func VisitLinesAfter(s string, fn func(line string)) { - high := strings.IndexRune(s, '\n') - for high != -1 { - fn(s[:high+1]) - s = s[high+1:] - - high = strings.IndexRune(s, '\n') - } - - if s != "" { - fn(s) - } -} diff --git a/common/text/transform_test.go b/common/text/transform_test.go deleted file mode 100644 index 74bb37783..000000000 --- a/common/text/transform_test.go +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2019 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 text - -import ( - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestRemoveAccents(t *testing.T) { - c := qt.New(t) - - c.Assert(string(RemoveAccents([]byte("Resumé"))), qt.Equals, "Resume") - c.Assert(string(RemoveAccents([]byte("Hugo Rocks!"))), qt.Equals, "Hugo Rocks!") - c.Assert(string(RemoveAccentsString("Resumé")), qt.Equals, "Resume") -} - -func TestChomp(t *testing.T) { - c := qt.New(t) - - c.Assert(Chomp("\nA\n"), qt.Equals, "\nA") - c.Assert(Chomp("A\r\n"), qt.Equals, "A") -} - -func TestPuts(t *testing.T) { - c := qt.New(t) - - c.Assert(Puts("A"), qt.Equals, "A\n") - c.Assert(Puts("\nA\n"), qt.Equals, "\nA\n") - c.Assert(Puts(""), qt.Equals, "") -} - -func TestVisitLinesAfter(t *testing.T) { - const lines = `line 1 -line 2 - -line 3` - - var collected []string - - VisitLinesAfter(lines, func(s string) { - collected = append(collected, s) - }) - - c := qt.New(t) - - c.Assert(collected, qt.DeepEquals, []string{"line 1\n", "line 2\n", "\n", "line 3"}) -} - -func BenchmarkVisitLinesAfter(b *testing.B) { - const lines = `line 1 - line 2 - - line 3` - - for i := 0; i < b.N; i++ { - VisitLinesAfter(lines, func(s string) { - }) - } -} diff --git a/common/types/closer.go b/common/types/closer.go deleted file mode 100644 index 9f8875a8a..000000000 --- a/common/types/closer.go +++ /dev/null @@ -1,54 +0,0 @@ -// 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 deleted file mode 100644 index 6b1750376..000000000 --- a/common/types/convert.go +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright 2019 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 ( - "encoding/json" - "fmt" - "html/template" - "reflect" - "time" - - "github.com/spf13/cast" -) - -// ToDuration converts v to time.Duration. -// See ToDurationE if you need to handle errors. -func ToDuration(v any) time.Duration { - d, _ := ToDurationE(v) - return d -} - -// ToDurationE converts v to time.Duration. -func ToDurationE(v any) (time.Duration, error) { - if n := cast.ToInt(v); n > 0 { - return time.Duration(n) * time.Millisecond, nil - } - d, err := time.ParseDuration(cast.ToString(v)) - if err != nil { - return 0, fmt.Errorf("cannot convert %v to time.Duration", v) - } - return d, nil -} - -// ToStringSlicePreserveString is the same as ToStringSlicePreserveStringE, -// but it never fails. -func ToStringSlicePreserveString(v any) []string { - vv, _ := ToStringSlicePreserveStringE(v) - return vv -} - -// ToStringSlicePreserveStringE converts v to a string slice. -// If v is a string, it will be wrapped in a string slice. -func ToStringSlicePreserveStringE(v any) ([]string, error) { - if v == nil { - return nil, nil - } - if sds, ok := v.(string); ok { - return []string{sds}, nil - } - result, err := cast.ToStringSliceE(v) - if err == nil { - return result, nil - } - - // Probably []int or similar. Fall back to reflect. - vv := reflect.ValueOf(v) - - switch vv.Kind() { - case reflect.Slice, reflect.Array: - result = make([]string, vv.Len()) - for i := range vv.Len() { - s, err := cast.ToStringE(vv.Index(i).Interface()) - if err != nil { - return nil, err - } - result[i] = s - } - return result, nil - 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. -// Note that this will not try to convert numeric values etc., -// use ToString for that. -func TypeToString(v any) (string, bool) { - switch s := v.(type) { - case string: - return s, true - case template.HTML: - return string(s), true - case template.CSS: - return string(s), true - case template.HTMLAttr: - return string(s), true - case template.JS: - return string(s), true - case template.JSStr: - return string(s), true - case template.URL: - return string(s), true - case template.Srcset: - return string(s), true - } - - return "", false -} - -// ToString converts v to a string. -func ToString(v any) string { - s, _ := ToStringE(v) - return s -} - -// ToStringE converts v to a string. -func ToStringE(v any) (string, error) { - if s, ok := TypeToString(v); ok { - return s, nil - } - - switch s := v.(type) { - case json.RawMessage: - return string(s), nil - default: - return cast.ToStringE(v) - } -} diff --git a/common/types/convert_test.go b/common/types/convert_test.go deleted file mode 100644 index 13059285d..000000000 --- a/common/types/convert_test.go +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2019 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 ( - "encoding/json" - "testing" - "time" - - qt "github.com/frankban/quicktest" -) - -func TestToStringSlicePreserveString(t *testing.T) { - c := qt.New(t) - - c.Assert(ToStringSlicePreserveString("Hugo"), qt.DeepEquals, []string{"Hugo"}) - c.Assert(ToStringSlicePreserveString(qt.Commentf("Hugo")), qt.DeepEquals, []string{"Hugo"}) - c.Assert(ToStringSlicePreserveString([]any{"A", "B"}), qt.DeepEquals, []string{"A", "B"}) - c.Assert(ToStringSlicePreserveString([]int{1, 3}), qt.DeepEquals, []string{"1", "3"}) - c.Assert(ToStringSlicePreserveString(nil), qt.IsNil) -} - -func TestToString(t *testing.T) { - c := qt.New(t) - - c.Assert(ToString([]byte("Hugo")), qt.Equals, "Hugo") - c.Assert(ToString(json.RawMessage("Hugo")), qt.Equals, "Hugo") -} - -func TestToDuration(t *testing.T) { - c := qt.New(t) - - c.Assert(ToDuration("200ms"), qt.Equals, 200*time.Millisecond) - 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 deleted file mode 100644 index 061acfe64..000000000 --- a/common/types/css/csstypes.go +++ /dev/null @@ -1,20 +0,0 @@ -// 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 css - -// QuotedString is a string that needs to be quoted in CSS. -type QuotedString string - -// UnquotedString is a string that does not need to be quoted in CSS. -type UnquotedString string diff --git a/common/types/evictingqueue.go b/common/types/evictingqueue.go deleted file mode 100644 index a335be3b2..000000000 --- a/common/types/evictingqueue.go +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright 2017-present 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 contains types shared between packages in Hugo. -package types - -import ( - "slices" - "sync" -) - -// 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. -type EvictingQueue[T comparable] struct { - size int - vals []T - set map[T]bool - mu sync.Mutex - zero T -} - -// 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 *EvictingQueue[T]) Add(v T) *EvictingQueue[T] { - q.mu.Lock() - if q.set[v] { - q.mu.Unlock() - return q - } - - if len(q.set) == q.size { - // Full - delete(q.set, q.vals[0]) - 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 *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 *EvictingQueue[T]) Peek() T { - q.mu.Lock() - l := len(q.vals) - if l == 0 { - q.mu.Unlock() - return q.zero - } - elem := q.vals[l-1] - q.mu.Unlock() - return elem -} - -// PeekAll looks at all the elements in the queue, with the newest first. -func (q *EvictingQueue[T]) PeekAll() []T { - if q == nil { - return nil - } - q.mu.Lock() - 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 { - vals[i], vals[j] = vals[j], vals[i] - } - return vals -} - -// PeekAllSet returns PeekAll as a set. -func (q *EvictingQueue[T]) PeekAllSet() map[T]bool { - all := q.PeekAll() - set := make(map[T]bool) - for _, v := range all { - set[v] = true - } - - return set -} diff --git a/common/types/evictingqueue_test.go b/common/types/evictingqueue_test.go deleted file mode 100644 index b93243f3c..000000000 --- a/common/types/evictingqueue_test.go +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2017-present 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" - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestEvictingStringQueue(t *testing.T) { - c := qt.New(t) - - queue := NewEvictingQueue[string](3) - - c.Assert(queue.Peek(), qt.Equals, "") - queue.Add("a") - queue.Add("b") - queue.Add("a") - c.Assert(queue.Peek(), qt.Equals, "b") - queue.Add("b") - c.Assert(queue.Peek(), qt.Equals, "b") - - queue.Add("a") - queue.Add("b") - - c.Assert(queue.Contains("a"), qt.Equals, true) - c.Assert(queue.Contains("foo"), qt.Equals, false) - - c.Assert(queue.PeekAll(), qt.DeepEquals, []string{"b", "a"}) - c.Assert(queue.Peek(), qt.Equals, "b") - queue.Add("c") - queue.Add("d") - // Overflowed, a should now be removed. - c.Assert(queue.PeekAll(), qt.DeepEquals, []string{"d", "c", "b"}) - c.Assert(len(queue.PeekAllSet()), qt.Equals, 3) - c.Assert(queue.PeekAllSet()["c"], qt.Equals, true) -} - -func TestEvictingStringQueueConcurrent(t *testing.T) { - var wg sync.WaitGroup - val := "someval" - - queue := NewEvictingQueue[string](3) - - for range 100 { - wg.Add(1) - go func() { - defer wg.Done() - queue.Add(val) - v := queue.Peek() - if v != val { - t.Error("wrong val") - } - vals := queue.PeekAll() - if len(vals) != 1 || vals[0] != val { - t.Error("wrong val") - } - }() - } - wg.Wait() -} diff --git a/common/types/hstring/stringtypes.go b/common/types/hstring/stringtypes.go deleted file mode 100644 index 53ce2068f..000000000 --- a/common/types/hstring/stringtypes.go +++ /dev/null @@ -1,36 +0,0 @@ -// 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 hstring - -import ( - "html/template" - - "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 deleted file mode 100644 index 05e2c22b9..000000000 --- a/common/types/hstring/stringtypes_test.go +++ /dev/null @@ -1,30 +0,0 @@ -// 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 hstring - -import ( - "html/template" - "testing" - - qt "github.com/frankban/quicktest" - "github.com/spf13/cast" -) - -func TestRenderedString(t *testing.T) { - c := qt.New(t) - - // Validate that it will behave like a string in Hugo settings. - 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 deleted file mode 100644 index 7e94c1eea..000000000 --- a/common/types/types.go +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright 2019 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 contains types shared between packages in Hugo. -package types - -import ( - "fmt" - "reflect" - "sync/atomic" - - "github.com/spf13/cast" -) - -// RLocker represents the read locks in sync.RWMutex. -type RLocker interface { - RLock() - RUnlock() -} - -type Locker interface { - Lock() - Unlock() -} - -type RWLocker interface { - RLocker - Locker -} - -// KeyValue is a interface{} tuple. -type KeyValue struct { - Key any - Value any -} - -// KeyValueStr is a string tuple. -type KeyValueStr struct { - Key string - Value string -} - -// KeyValues holds an key and a slice of values. -type KeyValues struct { - Key any - Values []any -} - -// KeyString returns the key as a string, an empty string if conversion fails. -func (k KeyValues) KeyString() string { - return cast.ToString(k.Key) -} - -func (k KeyValues) String() string { - return fmt.Sprintf("%v: %v", k.Key, k.Values) -} - -// NewKeyValuesStrings takes a given key and slice of values and returns a new -// KeyValues struct. -func NewKeyValuesStrings(key string, values ...string) KeyValues { - iv := make([]any, len(values)) - for i := range values { - iv[i] = values[i] - } - return KeyValues{Key: key, Values: iv} -} - -// Zeroer, as implemented by time.Time, will be used by the truth template -// funcs in Hugo (if, with, not, and, or). -type Zeroer interface { - IsZero() bool -} - -// IsNil reports whether v is nil. -func IsNil(v any) bool { - if v == nil { - return true - } - - value := reflect.ValueOf(v) - switch value.Kind() { - case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: - return value.IsNil() - } - - return false -} - -// DevMarker is a marker interface for types that should only be used during -// development. -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 deleted file mode 100644 index 795733047..000000000 --- a/common/types/types_test.go +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2017-present 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 ( - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestKeyValues(t *testing.T) { - c := qt.New(t) - - kv := NewKeyValuesStrings("key", "a1", "a2") - - 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 deleted file mode 100644 index 2958a2a04..000000000 --- a/common/urls/baseURL.go +++ /dev/null @@ -1,112 +0,0 @@ -// 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 urls - -import ( - "fmt" - "net/url" - "strconv" - "strings" -) - -// 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 - WithPathNoTrailingSlash string - WithoutPath string - BasePath string - BasePathNoTrailingSlash string -} - -func (b BaseURL) String() string { - return b.WithPath -} - -func (b BaseURL) Path() string { - return b.url.Path -} - -func (b BaseURL) Port() int { - p, _ := strconv.Atoi(b.url.Port()) - return p -} - -// HostURL returns the URL to the host root without any path elements. -func (b BaseURL) HostURL() string { - return strings.TrimSuffix(b.String(), b.Path()) -} - -// WithProtocol returns the BaseURL prefixed with the given protocol. -// The Protocol is normally of the form "scheme://", i.e. "webcal://". -func (b BaseURL) WithProtocol(protocol string) (BaseURL, error) { - u := b.URL() - - scheme := protocol - isFullProtocol := strings.HasSuffix(scheme, "://") - isOpaqueProtocol := strings.HasSuffix(scheme, ":") - - if isFullProtocol { - scheme = strings.TrimSuffix(scheme, "://") - } else if isOpaqueProtocol { - scheme = strings.TrimSuffix(scheme, ":") - } - - u.Scheme = scheme - - if isFullProtocol && u.Opaque != "" { - u.Opaque = "//" + u.Opaque - } else if isOpaqueProtocol && u.Opaque == "" { - return BaseURL{}, fmt.Errorf("cannot determine BaseURL for protocol %q", protocol) - } - - return newBaseURLFromURL(u) -} - -func (b BaseURL) WithPort(port int) (BaseURL, error) { - u := b.URL() - u.Host = u.Hostname() + ":" + strconv.Itoa(port) - return newBaseURLFromURL(u) -} - -// URL returns a copy of the internal URL. -// The copy can be safely used and modified. -func (b BaseURL) URL() *url.URL { - c := *b.url - return &c -} - -func NewBaseURLFromString(b string) (BaseURL, error) { - u, err := url.Parse(b) - if err != nil { - return BaseURL{}, err - } - return newBaseURLFromURL(u) -} - -func newBaseURLFromURL(u *url.URL) (BaseURL, error) { - // 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() - 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 deleted file mode 100644 index ba337aac8..000000000 --- a/common/urls/baseURL_test.go +++ /dev/null @@ -1,81 +0,0 @@ -// 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 urls - -import ( - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestBaseURL(t *testing.T) { - c := qt.New(t) - - b, err := NewBaseURLFromString("http://example.com/") - c.Assert(err, qt.IsNil) - 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/") - - p, err = b.WithProtocol("webcal") - c.Assert(err, qt.IsNil) - c.Assert(p.String(), qt.Equals, "webcal://example.com/") - - _, err = b.WithProtocol("mailto:") - c.Assert(err, qt.Not(qt.IsNil)) - - b, err = NewBaseURLFromString("mailto:hugo@rules.com") - c.Assert(err, qt.IsNil) - c.Assert(b.String(), qt.Equals, "mailto:hugo@rules.com") - - // These are pretty constructed - p, err = b.WithProtocol("webcal") - c.Assert(err, qt.IsNil) - c.Assert(p.String(), qt.Equals, "webcal:hugo@rules.com") - - p, err = b.WithProtocol("webcal://") - c.Assert(err, qt.IsNil) - c.Assert(p.String(), qt.Equals, "webcal://hugo@rules.com") - - // Test with "non-URLs". Some people will try to use these as a way to get - // relative URLs working etc. - b, err = NewBaseURLFromString("/") - c.Assert(err, qt.IsNil) - c.Assert(b.String(), qt.Equals, "/") - - b, err = NewBaseURLFromString("") - c.Assert(err, qt.IsNil) - 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.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/common/urls/ref.go b/common/urls/ref.go deleted file mode 100644 index e5804a279..000000000 --- a/common/urls/ref.go +++ /dev/null @@ -1,22 +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 urls - -// RefLinker is implemented by those who support reference linking. -// args must contain a path, but can also point to the target -// language or output format. -type RefLinker interface { - Ref(args map[string]any) (string, error) - RelRef(args map[string]any) (string, error) -} diff --git a/compare/compare.go b/compare/compare.go deleted file mode 100644 index fd15bd087..000000000 --- a/compare/compare.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2019 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 compare - -// Eqer can be used to determine if this value is equal to the other. -// The semantics of equals is that the two value are interchangeable -// in the Hugo templates. -type Eqer interface { - // Eq returns whether this value is equal to the other. - // This is for internal use. - Eq(other any) bool -} - -// ProbablyEqer is an equal check that may return false positives, but never -// a false negative. -type ProbablyEqer interface { - // For internal use. - ProbablyEq(other any) bool -} - -// Comparer can be used to compare two values. -// This will be used when using the le, ge etc. operators in the templates. -// Compare returns -1 if the given version is less than, 0 if equal and 1 if greater than -// the running version. -type Comparer interface { - Compare(other any) int -} - -// Eq returns whether v1 is equal to v2. -// It will use the Eqer interface if implemented, which -// defines equals when two value are interchangeable -// in the Hugo templates. -func Eq(v1, v2 any) bool { - if v1 == nil || v2 == nil { - return v1 == v2 - } - - if eqer, ok := v1.(Eqer); ok { - return eqer.Eq(v2) - } - - 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.go b/compare/compare_strings.go deleted file mode 100644 index 1fd954081..000000000 --- a/compare/compare_strings.go +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright 2019 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 compare - -import ( - "strings" - "unicode" - "unicode/utf8" -) - -// Strings returns an integer comparing two strings lexicographically. -func Strings(s, t string) int { - c := compareFold(s, t) - - if c == 0 { - // "B" and "b" would be the same so we need a tiebreaker. - return strings.Compare(s, t) - } - - return c -} - -// This function is derived from strings.EqualFold in Go's stdlib. -// https://github.com/golang/go/blob/ad4a58e31501bce5de2aad90a620eaecdc1eecb8/src/strings/strings.go#L893 -func compareFold(s, t string) int { - for s != "" && t != "" { - var sr, tr rune - if s[0] < utf8.RuneSelf { - sr, s = rune(s[0]), s[1:] - } else { - r, size := utf8.DecodeRuneInString(s) - sr, s = r, s[size:] - } - if t[0] < utf8.RuneSelf { - tr, t = rune(t[0]), t[1:] - } else { - r, size := utf8.DecodeRuneInString(t) - tr, t = r, t[size:] - } - - if tr == sr { - continue - } - - c := 1 - if tr < sr { - tr, sr = sr, tr - c = -c - } - - // ASCII only. - if tr < utf8.RuneSelf { - if sr >= 'A' && sr <= 'Z' { - if tr <= 'Z' { - // Same case. - return -c - } - - diff := tr - (sr + 'a' - 'A') - - if diff == 0 { - continue - } - - if diff < 0 { - return c - } - - if diff > 0 { - return -c - } - } - } - - // Unicode. - r := unicode.SimpleFold(sr) - for r != sr && r < tr { - r = unicode.SimpleFold(r) - } - - if r == tr { - continue - } - - return -c - } - - if s == "" && t == "" { - return 0 - } - - if s == "" { - return -1 - } - - return 1 -} - -// LessStrings returns whether s is less than t lexicographically. -func LessStrings(s, t string) bool { - return Strings(s, t) < 0 -} diff --git a/compare/compare_strings_test.go b/compare/compare_strings_test.go deleted file mode 100644 index 1a5bb0b1a..000000000 --- a/compare/compare_strings_test.go +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2019 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 compare - -import ( - "sort" - "strings" - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestCompare(t *testing.T) { - c := qt.New(t) - for _, test := range []struct { - a string - b string - }{ - {"a", "a"}, - {"A", "a"}, - {"Ab", "Ac"}, - {"az", "Za"}, - {"C", "D"}, - {"B", "a"}, - {"C", ""}, - {"", ""}, - {"αβδC", "ΑΒΔD"}, - {"αβδC", "ΑΒΔ"}, - {"αβδ", "ΑΒΔD"}, - {"αβδ", "ΑΒΔ"}, - {"β", "δ"}, - {"好", strings.ToLower("好")}, - } { - - expect := strings.Compare(strings.ToLower(test.a), strings.ToLower(test.b)) - got := compareFold(test.a, test.b) - - c.Assert(got, qt.Equals, expect) - - } -} - -func TestLexicographicSort(t *testing.T) { - c := qt.New(t) - - s := []string{"b", "Bz", "ba", "A", "Ba", "ba"} - - sort.Slice(s, func(i, j int) bool { - return LessStrings(s[i], s[j]) - }) - - c.Assert(s, qt.DeepEquals, []string{"A", "b", "Ba", "ba", "ba", "Bz"}) -} - -func BenchmarkStringSort(b *testing.B) { - prototype := []string{"b", "Bz", "zz", "ba", "αβδ αβδ αβδ", "A", "Ba", "ba", "nnnnasdfnnn", "AAgæåz", "αβδC"} - b.Run("LessStrings", func(b *testing.B) { - ss := make([][]string, b.N) - for i := 0; i < b.N; i++ { - ss[i] = make([]string, len(prototype)) - copy(ss[i], prototype) - } - b.ResetTimer() - for i := 0; i < b.N; i++ { - sss := ss[i] - sort.Slice(sss, func(i, j int) bool { - return LessStrings(sss[i], sss[j]) - }) - } - }) -} diff --git a/config/allconfig/allconfig.go b/config/allconfig/allconfig.go deleted file mode 100644 index 0db0be1d8..000000000 --- a/config/allconfig/allconfig.go +++ /dev/null @@ -1,1182 +0,0 @@ -// 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 contains the full configuration for Hugo. -// { "name": "Configuration", "description": "This section holds all configuration options in Hugo." } -package allconfig - -import ( - "errors" - "fmt" - "reflect" - "regexp" - "sort" - "strconv" - "strings" - "sync" - "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/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" - "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" - "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" - - xmaps "golang.org/x/exp/maps" -) - -// InternalConfig is the internal configuration for Hugo, not read from any user provided config file. -type InternalConfig struct { - // Server mode? - Running bool - - Quiet bool - Verbose bool - Clock string - Watch bool - FastRenderMode bool - LiveReloadPort int -} - -// All non-params config keys for language. -var configLanguageKeys map[string]bool - -func init() { - skip := map[string]bool{ - "internal": true, - "c": true, - "rootconfig": true, - } - configLanguageKeys = make(map[string]bool) - addKeys := func(v reflect.Value) { - for i := range v.NumField() { - name := strings.ToLower(v.Type().Field(i).Name) - if skip[name] { - continue - } - configLanguageKeys[name] = true - } - } - addKeys(reflect.ValueOf(Config{})) - addKeys(reflect.ValueOf(RootConfig{})) - addKeys(reflect.ValueOf(config.CommonDirs{})) - addKeys(reflect.ValueOf(langs.LanguageConfig{})) -} - -type Config struct { - // For internal use only. - Internal InternalConfig `mapstructure:"-" json:"-"` - // For internal use only. - C *ConfigCompiled `mapstructure:"-" json:"-"` - - 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. - // {"identifiers": ["build"] } - Build config.BuildConfig `mapstructure:"-"` - - // The caches configuration section contains cache-related configuration options. - // {"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:"-"` - - Imaging *config.ConfigNamespace[images.ImagingConfig, images.ImagingConfigInternal] `mapstructure:"-"` - - // The outputformats configuration sections maps a format name (a string) to a configuration object for that format. - OutputFormats *config.ConfigNamespace[map[string]output.OutputFormatConfig, output.Formats] `mapstructure:"-"` - - // The outputs configuration section maps a Page Kind (a string) to a slice of output formats. - // This can be overridden in the front matter. - Outputs map[string][]string `mapstructure:"-"` - - // 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, *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 deployconfig. - Deployment deployconfig.DeployConfig `mapstructure:"-"` - - // Module configuration. - Module modules.Config `mapstructure:"-"` - - // Front matter configuration. - Frontmatter pagemeta.FrontmatterConfig `mapstructure:"-"` - - // Minification configuration. - Minify minifiers.MinifyConfig `mapstructure:"-"` - - // Permalink configuration. - Permalinks map[string]map[string]string `mapstructure:"-"` - - // Taxonomy configuration. - Taxonomies map[string]string `mapstructure:"-"` - - // Sitemap configuration. - Sitemap config.SitemapConfig `mapstructure:"-"` - - // Related content configuration. - Related related.Config `mapstructure:"-"` - - // Server configuration. - Server config.Server `mapstructure:"-"` - - // Pagination configuration. - Pagination config.Pagination `mapstructure:"-"` - - // Page configuration. - Page config.PageConfig `mapstructure:"-"` - - // Privacy configuration. - Privacy privacy.Config `mapstructure:"-"` - - // Security configuration. - Security security.Config `mapstructure:"-"` - - // Services configuration. - Services services.Config `mapstructure:"-"` - - // User provided parameters. - // {"refs": ["config:languages:params"] } - Params maps.Params `mapstructure:"-"` - - // The languages configuration sections maps a language code (a string) to a configuration object for that language. - Languages map[string]langs.LanguageConfig `mapstructure:"-"` - - // UglyURLs configuration. Either a boolean or a sections map. - UglyURLs any `mapstructure:"-"` -} - -type configCompiler interface { - CompileConfig(logger loggers.Logger) error -} - -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() - // These will go away soon ... - x.StaticDir0 = nil - x.StaticDir1 = nil - x.StaticDir2 = nil - x.StaticDir3 = nil - x.StaticDir4 = nil - x.StaticDir5 = nil - x.StaticDir6 = nil - x.StaticDir7 = nil - x.StaticDir8 = nil - x.StaticDir9 = nil - x.StaticDir10 = nil - - return &x -} - -func (c *Config) CompileConfig(logger loggers.Logger) error { - var transientErr error - s := c.Timeout - if _, err := strconv.Atoi(s); err == nil { - // A number, assume seconds. - s = s + "s" - } - timeout, err := time.ParseDuration(s) - if err != nil { - return fmt.Errorf("failed to parse timeout: %s", err) - } - disabledKinds := make(map[string]bool) - for _, kind := range c.DisableKinds { - kind = strings.ToLower(kind) - if newKind := kinds.IsDeprecatedAndReplacedWith(kind); newKind != "" { - logger.Deprecatef(false, "Kind %q used in disableKinds is deprecated, use %q instead.", kind, newKind) - // Legacy config. - kind = newKind - } - if kinds.GetKindAny(kind) == "" { - logger.Warnf("Unknown kind %q in disableKinds configuration.", kind) - continue - } - disabledKinds[kind] = true - } - kindOutputFormats := make(map[string]output.Formats) - 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. - continue - } - f, found := outputFormats.GetByName(format) - if !found { - transientErr = fmt.Errorf("unknown output format %q for kind %q", format, kind) - continue - } - kindOutputFormats[kind] = append(kindOutputFormats[kind], f) - } - } - - 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) - } - defaultOutputFormat = f - } else { - c.DefaultOutputFormat = defaultOutputFormat.Name - } - - 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) - if err != nil { - return err - } - - isUglyURL := func(section string) bool { - switch v := c.UglyURLs.(type) { - case bool: - return v - case map[string]bool: - return v[section] - default: - return false - } - } - - ignoreFile := func(s string) bool { - return false - } - if len(c.IgnoreFiles) > 0 { - regexps := make([]*regexp.Regexp, len(c.IgnoreFiles)) - for i, pattern := range c.IgnoreFiles { - var err error - regexps[i], err = regexp.Compile(pattern) - if err != nil { - return fmt.Errorf("failed to compile ignoreFiles pattern %q: %s", pattern, err) - } - } - ignoreFile = func(s string) bool { - for _, r := range regexps { - if r.MatchString(s) { - return true - } - } - return false - } - } - - var clock time.Time - if c.Internal.Clock != "" { - var err error - clock, err = time.Parse(time.RFC3339, c.Internal.Clock) - if err != nil { - return fmt.Errorf("failed to parse clock: %s", err) - } - } - - 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, - 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 { - if getCompiler := s.getCompiler; getCompiler != nil { - if err := getCompiler(c).CompileConfig(logger); err != nil { - return err - } - } - } - - return nil -} - -func (c *Config) IsKindEnabled(kind string) bool { - return !c.C.DisabledKinds[kind] -} - -func (c *Config) IsLangDisabled(lang string) bool { - return c.C.DisabledLanguages[lang] -} - -// ConfigCompiled holds values and functions that are derived from the config. -type ConfigCompiled struct { - 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 - // errors with missing output format definitions may resolve itself. - transientErr error - - mu sync.Mutex -} - -// This may be set after the config is compiled. -func (c *ConfigCompiled) SetMainSections(sections []string) { - c.mu.Lock() - defer c.mu.Unlock() - 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) 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"] } - BaseURL string - - // Whether to build content marked as draft.X - // {"identifiers": ["draft"] } - BuildDrafts bool - - // Whether to build content with expiryDate in the past. - // {"identifiers": ["expiryDate"] } - BuildExpired bool - - // Whether to build content with publishDate in the future. - // {"identifiers": ["publishDate"] } - BuildFuture bool - - // Copyright information. - Copyright string - - // The language to apply to content without any language indicator. - DefaultContentLanguage string - - // 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 - - // Disable lower casing of path segments. - DisablePathToLower bool - - // Disable page kinds from build. - DisableKinds []string - - // 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 - - // THe main section(s) of the site. - // If not set, Hugo will try to guess this from the content. - MainSections []string - - // Enable robots.txt generation. - EnableRobotsTXT bool - - // When enabled, Hugo will apply Git version information to each Page if possible, which - // can be used to keep lastUpdated in synch and to print version information. - // {"identifiers": ["Page"] } - EnableGitInfo bool - - // Enable to track, calculate and print metrics. - TemplateMetrics bool - - // Enable to track, print and calculate metric hints. - TemplateMetricsHints bool - - // Enable to disable the build lock file. - NoBuildLock bool - - // 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. - IgnoreFiles []string - - // Ignore cache. - IgnoreCache bool - - // Enable to print greppable placeholders (on the form "[i18n] TRANSLATIONID") for missing translation strings. - EnableMissingTranslationPlaceholders 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 - - // The default language code. - LanguageCode string - - // Enable if the site content has CJK language (Chinese, Japanese, or Korean). This affects how Hugo counts words. - 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 - - // Enable this to make all relative URLs relative to content root. Note that this does not affect absolute URLs. - RelativeURLs bool - - // Removes non-spacing marks from composite characters in content paths. - RemovePathAccents bool - - // 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 - - // When using ref or relref to resolve page links and a link cannot be resolved, it will be logged with this log level. - // Valid values are ERROR (default) or WARNING. Any ERROR will fail the build (exit -1). - RefLinksErrorLevel string - - // This will create a menu with all the sections as menu items and all the sections’ pages as “shadow-members”. - SectionPagesMenu string - - // The length of text in words to show in a .Summary. - SummaryLength int - - // The site title. - Title string - - // The theme(s) to use. - // See Modules for more a more flexible way to load themes. - Theme []string - - // 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. - TimeZone string - - // Set titleCaseStyle to specify the title style used by the title template function and the automatic section titles in Hugo. - // It defaults to AP Stylebook for title casing, but you can also set it to Chicago or Go (every word starts with a capital letter). - TitleCaseStyle string - - // The editor used for opening up new content. - NewContentEditor string - - // Don't sync modification time of files for the static mounts. - NoTimes bool - - // Don't sync modification time of files for the static mounts. - NoChmod bool - - // Clean the destination folder before a new build. - // This currently only handles static files. - CleanDestinationDir bool - - // A Glob pattern of module paths to ignore in the _vendor folder. - IgnoreVendorPaths string - - config.CommonDirs `mapstructure:",squash"` - - // The odd constructs below are kept for backwards compatibility. - // Deprecated: Use module mount config instead. - StaticDir []string - // Deprecated: Use module mount config instead. - StaticDir0 []string - // Deprecated: Use module mount config instead. - StaticDir1 []string - // Deprecated: Use module mount config instead. - StaticDir2 []string - // Deprecated: Use module mount config instead. - StaticDir3 []string - // Deprecated: Use module mount config instead. - StaticDir4 []string - // Deprecated: Use module mount config instead. - StaticDir5 []string - // Deprecated: Use module mount config instead. - StaticDir6 []string - // Deprecated: Use module mount config instead. - StaticDir7 []string - // Deprecated: Use module mount config instead. - StaticDir8 []string - // Deprecated: Use module mount config instead. - StaticDir9 []string - // Deprecated: Use module mount config instead. - StaticDir10 []string -} - -func (c RootConfig) staticDirs() []string { - var dirs []string - dirs = append(dirs, c.StaticDir...) - dirs = append(dirs, c.StaticDir0...) - dirs = append(dirs, c.StaticDir1...) - dirs = append(dirs, c.StaticDir2...) - dirs = append(dirs, c.StaticDir3...) - dirs = append(dirs, c.StaticDir4...) - dirs = append(dirs, c.StaticDir5...) - dirs = append(dirs, c.StaticDir6...) - dirs = append(dirs, c.StaticDir7...) - dirs = append(dirs, c.StaticDir8...) - dirs = append(dirs, c.StaticDir9...) - dirs = append(dirs, c.StaticDir10...) - return helpers.UniqueStringsReuse(dirs) -} - -type Configs struct { - Base *Config - LoadingInfo config.LoadConfigResult - LanguageConfigMap map[string]*Config - LanguageConfigSlice []*Config - - 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.LanguageConfigMap { - if l.C.transientErr != nil { - return l.C.transientErr - } - } - return nil -} - -func (c *Configs) IsZero() bool { - // A config always has at least one language. - return c == nil || len(c.Languages) == 0 -} - -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{ - m: c, - config: c.LanguageConfigMap[l.Lang], - baseConfig: c.LoadingInfo.BaseConfig, - language: l, - } - } - - if len(c.Modules) == 0 { - return errors.New("no modules loaded (need at least the main module)") - } - - // Apply default project mounts. - if err := modules.ApplyProjectConfigDefaults(c.Modules[0], c.configLangs...); err != nil { - 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 -} - -func (c Configs) ConfigLangs() []config.AllProvider { - return c.configLangs -} - -func (c Configs) GetFirstLanguageConfig() config.AllProvider { - return c.configLangs[0] -} - -func (c Configs) GetByLang(lang string) config.AllProvider { - for _, l := range c.configLangs { - if l.Language().Lang == lang { - return l - } - } - 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 := newDefaultConfig() - - err := decodeConfigFromParams(fs, logger, bcfg, cfg, all, nil) - if err != nil { - return nil, err - } - - langConfigMap := make(map[string]*Config) - - languagesConfig := cfg.GetStringMap("languages") - - var isMultihost bool - - if err := all.CompileConfig(logger); err != nil { - return nil, err - } - - for k, v := range languagesConfig { - mergedConfig := config.New() - var differentRootKeys []string - switch x := v.(type) { - case maps.Params: - _, found := x["params"] - if !found { - x["params"] = maps.Params{ - maps.MergeStrategyKey: maps.ParamsMergeStrategyDeep, - } - } - - for kk, vv := range x { - if kk == "_merge" { - continue - } - if kk == "baseurl" { - // baseURL configure don the language level is a multihost setup. - isMultihost = true - } - mergedConfig.Set(kk, vv) - 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) { - case maps.Params: - differentRootKeys = append(differentRootKeys, kk) - - // Use the language value as base. - mergedConfigEntry := xmaps.Clone(vvv) - // Merge in the root value. - maps.MergeParams(mergedConfigEntry, rootv.(maps.Params)) - - mergedConfig.Set(kk, mergedConfigEntry) - default: - // Apply new values to the root. - differentRootKeys = append(differentRootKeys, "") - } - } - } else { - switch vv.(type) { - case maps.Params: - differentRootKeys = append(differentRootKeys, kk) - default: - // Apply new values to the root. - differentRootKeys = append(differentRootKeys, "") - } - } - } - differentRootKeys = helpers.UniqueStringsSorted(differentRootKeys) - - if len(differentRootKeys) == 0 { - langConfigMap[k] = all - continue - } - - // Create a copy of the complete config and replace the root keys with the language specific ones. - clone := all.cloneForLang() - - 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: - panic(fmt.Sprintf("unknown type in languages config: %T", v)) - - } - } - - 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, - LoadingInfo: res, - IsMultihost: isMultihost, - } - - return cm, nil -} - -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 { - for _, v := range allDecoderSetups { - decoderSetups = append(decoderSetups, v) - } - } else { - for _, key := range keys { - if v, found := allDecoderSetups[key]; found { - decoderSetups = append(decoderSetups, v) - } else { - logger.Warnf("Skip unknown config key %q", key) - } - } - } - - // Sort them to get the dependency order right. - sort.Slice(decoderSetups, func(i, j int) bool { - ki, kj := decoderSetups[i], decoderSetups[j] - if ki.weight == kj.weight { - return ki.key < kj.key - } - return ki.weight < kj.weight - }) - - for _, v := range decoderSetups { - p := decodeConfig{p: p, c: target, fs: fs, bcfg: bcfg} - if err := v.decode(v, p); err != nil { - return fmt.Errorf("failed to decode %q: %w", v.key, err) - } - } - - return nil -} - -func createDefaultOutputFormats(allFormats output.Formats) map[string][]string { - if len(allFormats) == 0 { - panic("no output formats") - } - rssOut, rssFound := allFormats.GetByName(output.RSSFormat.Name) - htmlOut, _ := allFormats.GetByName(output.HTMLFormat.Name) - - defaultListTypes := []string{htmlOut.Name} - if rssFound { - defaultListTypes = append(defaultListTypes, rssOut.Name) - } - - m := map[string][]string{ - kinds.KindPage: {htmlOut.Name}, - kinds.KindHome: defaultListTypes, - kinds.KindSection: defaultListTypes, - kinds.KindTerm: defaultListTypes, - kinds.KindTaxonomy: defaultListTypes, - } - - // May be disabled - if rssFound { - m["rss"] = []string{rssOut.Name} - } - - return m -} diff --git a/config/allconfig/allconfig_integration_test.go b/config/allconfig/allconfig_integration_test.go deleted file mode 100644 index 8f6cacf84..000000000 --- a/config/allconfig/allconfig_integration_test.go +++ /dev/null @@ -1,381 +0,0 @@ -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 deleted file mode 100644 index 035349790..000000000 --- a/config/allconfig/alldecoders.go +++ /dev/null @@ -1,469 +0,0 @@ -// 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 ( - "fmt" - "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/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" - "github.com/gohugoio/hugo/resources/images" - "github.com/gohugoio/hugo/resources/page" - "github.com/gohugoio/hugo/resources/page/pagemeta" - "github.com/mitchellh/mapstructure" - "github.com/spf13/afero" - "github.com/spf13/cast" -) - -type decodeConfig struct { - p config.Provider - c *Config - fs afero.Fs - bcfg config.BaseConfig -} - -type decodeWeight struct { - 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{ - "": { - key: "", - weight: -100, // Always first. - decode: func(d decodeWeight, p decodeConfig) error { - 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": { - key: "imaging", - decode: func(d decodeWeight, p decodeConfig) error { - var err error - p.c.Imaging, err = images.DecodeConfig(p.p.GetStringMap(d.key)) - return err - }, - }, - "caches": { - key: "caches", - decode: func(d decodeWeight, p decodeConfig) error { - var err error - p.c.Caches, err = filecache.DecodeConfig(p.fs, p.bcfg, p.p.GetStringMap(d.key)) - if p.c.IgnoreCache { - // Set MaxAge in all caches to 0. - for k, cache := range p.c.Caches { - cache.MaxAge = 0 - p.c.Caches[k] = cache - } - } - 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 { - p.c.Build = config.DecodeBuildConfig(p.p) - return nil - }, - getCompiler: func(c *Config) configCompiler { - return &c.Build - }, - }, - "frontmatter": { - key: "frontmatter", - decode: func(d decodeWeight, p decodeConfig) error { - var err error - p.c.Frontmatter, err = pagemeta.DecodeFrontMatterConfig(p.p) - return err - }, - }, - "markup": { - key: "markup", - decode: func(d decodeWeight, p decodeConfig) error { - var err error - p.c.Markup, err = markup_config.Decode(p.p) - 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 { - var err error - p.c.Server, err = config.DecodeServer(p.p) - return err - }, - getCompiler: func(c *Config) configCompiler { - return &c.Server - }, - }, - "minify": { - key: "minify", - decode: func(d decodeWeight, p decodeConfig) error { - var err error - p.c.Minify, err = minifiers.DecodeConfig(p.p.Get(d.key)) - return err - }, - }, - "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)) - return err - }, - }, - "outputs": { - key: "outputs", - decode: func(d decodeWeight, p decodeConfig) error { - defaults := createDefaultOutputFormats(p.c.OutputFormats.Config) - m := maps.CleanConfigStringMap(p.p.GetStringMap("outputs")) - p.c.Outputs = make(map[string][]string) - for k, v := range m { - s := types.ToStringSlicePreserveString(v) - for i, v := range s { - s[i] = strings.ToLower(v) - } - p.c.Outputs[k] = s - } - // Apply defaults. - for k, v := range defaults { - if _, found := p.c.Outputs[k]; !found { - p.c.Outputs[k] = v - } - } - return nil - }, - }, - "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)) - return err - }, - }, - "params": { - key: "params", - decode: func(d decodeWeight, p decodeConfig) error { - p.c.Params = maps.CleanConfigStringMap(p.p.GetStringMap("params")) - if p.c.Params == nil { - p.c.Params = make(map[string]any) - } - - // Before Hugo 0.112.0 this was configured via site Params. - if mainSections, found := p.c.Params["mainsections"]; found { - p.c.MainSections = types.ToStringSlicePreserveString(mainSections) - if p.c.MainSections == nil { - p.c.MainSections = []string{} - } - } - - return nil - }, - }, - "module": { - key: "module", - decode: func(d decodeWeight, p decodeConfig) error { - var err error - p.c.Module, err = modules.DecodeConfig(p.p) - return err - }, - }, - "permalinks": { - key: "permalinks", - decode: func(d decodeWeight, p decodeConfig) error { - 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 - 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 { - if p.p.IsSet(d.key) { - p.c.Taxonomies = maps.CleanConfigStringMapString(p.p.GetStringMapString(d.key)) - } - return nil - }, - }, - "related": { - key: "related", - weight: 100, // This needs to be decoded after taxonomies. - decode: func(d decodeWeight, p decodeConfig) error { - if p.p.IsSet(d.key) { - var err error - p.c.Related, err = related.DecodeConfig(p.p.GetParams(d.key)) - if err != nil { - return fmt.Errorf("failed to decode related config: %w", err) - } - } else { - p.c.Related = related.DefaultConfig - if _, found := p.c.Taxonomies["tag"]; found { - p.c.Related.Add(related.IndexConfig{Name: "tags", Weight: 80, Type: related.TypeBasic}) - } - } - return nil - }, - }, - "languages": { - key: "languages", - decode: func(d decodeWeight, p decodeConfig) error { - var err error - m := p.p.GetStringMap(d.key) - 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, ok = v.(maps.Params) - if ok { - break - } - } - if first != nil { - if _, found := first["languagecode"]; !found { - first["languagecode"] = p.p.GetString("languagecode") - } - } - } - p.c.Languages, err = langs.DecodeConfig(m) - if err != nil { - 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 - }, - }, - "cascade": { - key: "cascade", - decode: func(d decodeWeight, p decodeConfig) error { - var err error - p.c.Cascade, err = page.DecodeCascadeConfig(nil, true, p.p.Get(d.key)) - return err - }, - }, - "menus": { - key: "menus", - decode: func(d decodeWeight, p decodeConfig) error { - var err error - p.c.Menus, err = navigation.DecodeConfig(p.p.Get(d.key)) - 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 { - var err error - p.c.Privacy, err = privacy.DecodeConfig(p.p) - return err - }, - }, - "security": { - key: "security", - decode: func(d decodeWeight, p decodeConfig) error { - var err error - p.c.Security, err = security.DecodeConfig(p.p) - return err - }, - }, - "services": { - key: "services", - decode: func(d decodeWeight, p decodeConfig) error { - var err error - p.c.Services, err = services.DecodeConfig(p.p) - return err - }, - }, - "deployment": { - key: "deployment", - decode: func(d decodeWeight, p decodeConfig) error { - var err error - p.c.Deployment, err = deployconfig.DecodeConfig(p.p) - return err - }, - }, - "author": { - key: "author", - decode: func(d decodeWeight, p decodeConfig) error { - 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 = maps.CleanConfigStringMapString(p.p.GetStringMapString(d.key)) - return nil - }, - internalOrDeprecated: true, - }, - "uglyurls": { - key: "uglyurls", - decode: func(d decodeWeight, p decodeConfig) error { - v := p.p.Get(d.key) - switch vv := v.(type) { - case bool: - 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 deleted file mode 100644 index 6990a3590..000000000 --- a/config/allconfig/configlanguage.go +++ /dev/null @@ -1,261 +0,0 @@ -// 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 ( - "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" -) - -type ConfigLanguage struct { - config *Config - baseConfig config.BaseConfig - - m *Configs - language *langs.Language -} - -func (c ConfigLanguage) Language() *langs.Language { - return c.language -} - -func (c ConfigLanguage) Languages() langs.Languages { - return c.m.Languages -} - -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 -} - -func (c ConfigLanguage) BaseURLLiveReload() urls.BaseURL { - return c.config.C.BaseURLLiveReload -} - -func (c ConfigLanguage) Environment() string { - return c.config.Environment -} - -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) FastRenderMode() bool { - return c.config.Internal.FastRenderMode -} - -func (c ConfigLanguage) IsMultilingual() bool { - return len(c.m.Languages) > 1 -} - -func (c ConfigLanguage) TemplateMetrics() bool { - return c.config.TemplateMetrics -} - -func (c ConfigLanguage) TemplateMetricsHints() bool { - return c.config.TemplateMetricsHints -} - -func (c ConfigLanguage) IsLangDisabled(lang string) bool { - return c.config.C.DisabledLanguages[lang] -} - -func (c ConfigLanguage) IgnoredLogs() map[string]bool { - return c.config.C.IgnoredLogs -} - -func (c ConfigLanguage) NoBuildLock() bool { - return c.config.NoBuildLock -} - -func (c ConfigLanguage) NewContentEditor() string { - return c.config.NewContentEditor -} - -func (c ConfigLanguage) Timeout() time.Duration { - return c.config.C.Timeout -} - -func (c ConfigLanguage) BaseConfig() config.BaseConfig { - return c.baseConfig -} - -func (c ConfigLanguage) Dirs() config.CommonDirs { - return c.config.CommonDirs -} - -func (c ConfigLanguage) DirsBase() config.CommonDirs { - return c.m.Base.CommonDirs -} - -func (c ConfigLanguage) WorkingDir() string { - return c.m.Base.WorkingDir -} - -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 { - case "security": - return c.config.Security - case "build": - return c.config.Build - case "frontmatter": - return c.config.Frontmatter - case "caches": - return c.config.Caches - case "markup": - return c.config.Markup - case "mediaTypes": - return c.config.MediaTypes.Config - case "outputFormats": - return c.config.OutputFormats.Config - case "permalinks": - return c.config.Permalinks - case "minify": - return c.config.Minify - case "allModules": - return c.m.Modules - case "deployment": - return c.config.Deployment - case "httpCacheCompiled": - return c.config.C.HTTPCache - default: - panic("not implemented: " + s) - } -} - -func (c ConfigLanguage) GetConfig() any { - return c.config -} - -func (c ConfigLanguage) CanonifyURLs() bool { - return c.config.CanonifyURLs -} - -func (c ConfigLanguage) IsUglyURLs(section string) bool { - return c.config.C.IsUglyURLSection(section) -} - -func (c ConfigLanguage) IgnoreFile(s string) bool { - return c.config.C.IgnoreFile(s) -} - -func (c ConfigLanguage) DisablePathToLower() bool { - return c.config.DisablePathToLower -} - -func (c ConfigLanguage) RemovePathAccents() bool { - return c.config.RemovePathAccents -} - -func (c ConfigLanguage) DefaultContentLanguage() string { - return c.config.DefaultContentLanguage -} - -func (c ConfigLanguage) DefaultContentLanguageInSubdir() bool { - return c.config.DefaultContentLanguageInSubdir -} - -func (c ConfigLanguage) SummaryLength() int { - return c.config.SummaryLength -} - -func (c ConfigLanguage) BuildExpired() bool { - return c.config.BuildExpired -} - -func (c ConfigLanguage) BuildFuture() bool { - return c.config.BuildFuture -} - -func (c ConfigLanguage) BuildDrafts() bool { - return c.config.BuildDrafts -} - -func (c ConfigLanguage) Running() bool { - return c.config.Internal.Running -} - -func (c ConfigLanguage) PrintUnusedTemplates() bool { - return c.config.PrintUnusedTemplates -} - -func (c ConfigLanguage) EnableMissingTranslationPlaceholders() bool { - return c.config.EnableMissingTranslationPlaceholders -} - -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) 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 deleted file mode 100644 index 1a5fb6153..000000000 --- a/config/allconfig/docshelper.go +++ /dev/null @@ -1,48 +0,0 @@ -// 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/load.go b/config/allconfig/load.go deleted file mode 100644 index 4fb8bbaef..000000000 --- a/config/allconfig/load.go +++ /dev/null @@ -1,544 +0,0 @@ -// 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 contains the full configuration for Hugo. -package allconfig - -import ( - "errors" - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/gobwas/glob" - "github.com/gohugoio/hugo/common/herrors" - "github.com/gohugoio/hugo/common/hexec" - "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/config" - "github.com/gohugoio/hugo/helpers" - hglob "github.com/gohugoio/hugo/hugofs/glob" - "github.com/gohugoio/hugo/modules" - "github.com/gohugoio/hugo/parser/metadecoders" - "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) { - if len(d.Environ) == 0 && !hugo.IsRunningAsTest() { - d.Environ = os.Environ() - } - - if d.Logger == nil { - d.Logger = loggers.NewDefault() - } - - l := &configLoader{ConfigSourceDescriptor: d, cfg: config.New()} - // Make sure we always do this, even in error situations, - // as we have commands (e.g. "hugo mod init") that will - // use a partial configuration to do its job. - defer l.deleteMergeStrategies() - res, _, err := l.loadConfigMain(d) - if err != nil { - return nil, fmt.Errorf("failed to load config: %w", err) - } - - configs, err := fromLoadConfigResult(d.Fs, d.Logger, res) - if err != nil { - return nil, fmt.Errorf("failed to create config from result: %w", err) - } - - moduleConfig, modulesClient, err := l.loadModules(configs, d.IgnoreModuleDoesNotExist) - if err != nil { - return nil, fmt.Errorf("failed to load modules: %w", err) - } - - if len(l.ModulesConfigFiles) > 0 { - // Config merged in from modules. - // Re-read the config. - configs, err = fromLoadConfigResult(d.Fs, d.Logger, res) - if err != nil { - return nil, fmt.Errorf("failed to create config from modules config: %w", err) - } - 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.AllModules - configs.ModulesClient = modulesClient - - if err := configs.Init(); err != nil { - return nil, fmt.Errorf("failed to init config: %w", err) - } - - loggers.SetGlobalLogger(d.Logger) - - return configs, nil -} - -// ConfigSourceDescriptor describes where to find the config (e.g. config.toml etc.). -type ConfigSourceDescriptor struct { - Fs afero.Fs - Logger loggers.Logger - - // Config received from the command line. - // These will override any config file settings. - Flags config.Provider - - // Path to the config file to use, e.g. /my/project/config.toml - Filename string - - // The (optional) directory for additional configuration files. - ConfigDir string - - // production, development - Environment string - - // 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 { - if d.Filename == "" { - return nil - } - return strings.Split(d.Filename, ",") -} - -type configLoader struct { - cfg config.Provider - BaseConfig config.BaseConfig - ConfigSourceDescriptor - - // collected - ModulesConfig modules.ModulesConfig - ModulesConfigFiles []string -} - -// Handle some legacy values. -func (l configLoader) applyConfigAliases() error { - 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) { - vv := l.cfg.Get(alias.Key) - l.cfg.Set(alias.Value, vv) - } - } - - return nil -} - -func (l configLoader) applyDefaultConfig() error { - defaultSettings := maps.Params{ - // These dirs are used early/before we build the config struct. - "themesDir": "themes", - "configDir": "config", - } - - l.cfg.SetDefaults(defaultSettings) - - return nil -} - -func (l configLoader) normalizeCfg(cfg config.Provider) error { - if b, ok := cfg.Get("minifyOutput").(bool); ok && b { - cfg.Set("minify.minifyOutput", true) - } else if b, ok := cfg.Get("minify").(bool); ok && b { - cfg.Set("minify", maps.Params{"minifyOutput": true}) - } - - return nil -} - -func (l configLoader) cleanExternalConfig(cfg config.Provider) error { - if cfg.IsSet("internal") { - cfg.Set("internal", nil) - } - return nil -} - -func (l configLoader) applyFlagsOverrides(cfg config.Provider) error { - for _, k := range cfg.Keys() { - l.cfg.Set(k, cfg.Get(k)) - } - return nil -} - -func (l configLoader) applyOsEnvOverrides(environ []string) error { - if len(environ) == 0 { - return nil - } - - const delim = "__env__delim" - - // Extract all that start with the HUGO prefix. - // The delimiter is the following rune, usually "_". - const hugoEnvPrefix = "HUGO" - var hugoEnv []types.KeyValueStr - for _, v := range environ { - key, val := config.SplitEnvVar(v) - if strings.HasPrefix(key, hugoEnvPrefix) { - delimiterAndKey := strings.TrimPrefix(key, hugoEnvPrefix) - if len(delimiterAndKey) < 2 { - continue - } - // Allow delimiters to be case sensitive. - // It turns out there isn't that many allowed special - // chars in environment variables when used in Bash and similar, - // so variables on the form HUGOxPARAMSxFOO=bar is one option. - key := strings.ReplaceAll(delimiterAndKey[1:], delimiterAndKey[:1], delim) - key = strings.ToLower(key) - hugoEnv = append(hugoEnv, types.KeyValueStr{ - Key: key, - Value: val, - }) - - } - } - - for _, env := range hugoEnv { - existing, nestedKey, owner, err := maps.GetNestedParamFn(env.Key, delim, l.cfg.Get) - if err != nil { - return err - } - - if existing != nil { - val, err := metadecoders.Default.UnmarshalStringTo(env.Value, existing) - 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 && nestedKey != "" { - owner[nestedKey] = env.Value - } else { - 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 - - if d.Flags != nil { - if err := l.normalizeCfg(d.Flags); err != nil { - return res, l.ModulesConfig, err - } - } - - if d.Fs == nil { - return res, l.ModulesConfig, errors.New("no filesystem provided") - } - - if d.Flags != nil { - if err := l.applyFlagsOverrides(d.Flags); err != nil { - return res, l.ModulesConfig, err - } - workingDir := filepath.Clean(l.cfg.GetString("workingDir")) - - l.BaseConfig = config.BaseConfig{ - WorkingDir: workingDir, - ThemesDir: paths.AbsPathify(workingDir, l.cfg.GetString("themesDir")), - } - - } - - names := d.configFilenames() - - if names != nil { - for _, name := range names { - var filename string - filename, err := l.loadConfig(name) - if err == nil { - res.ConfigFiles = append(res.ConfigFiles, filename) - } else if err != ErrNoConfigFile { - return res, l.ModulesConfig, l.wrapFileError(err, filename) - } - } - } else { - for _, name := range config.DefaultConfigNames { - var filename string - filename, err := l.loadConfig(name) - if err == nil { - res.ConfigFiles = append(res.ConfigFiles, filename) - break - } else if err != ErrNoConfigFile { - return res, l.ModulesConfig, l.wrapFileError(err, filename) - } - } - } - - if d.ConfigDir != "" { - absConfigDir := paths.AbsPathify(l.BaseConfig.WorkingDir, d.ConfigDir) - dcfg, dirnames, err := config.LoadConfigFromDir(l.Fs, absConfigDir, l.Environment) - if err == nil { - if len(dirnames) > 0 { - if err := l.normalizeCfg(dcfg); err != nil { - return res, l.ModulesConfig, err - } - if err := l.cleanExternalConfig(dcfg); err != nil { - return res, l.ModulesConfig, err - } - l.cfg.Set("", dcfg.Get("")) - res.ConfigFiles = append(res.ConfigFiles, dirnames...) - } - } else if err != ErrNoConfigFile { - if len(dirnames) > 0 { - return res, l.ModulesConfig, l.wrapFileError(err, dirnames[0]) - } - return res, l.ModulesConfig, err - } - } - - res.Cfg = l.cfg - - if err := l.applyDefaultConfig(); err != nil { - return res, l.ModulesConfig, err - } - - // Some settings are used before we're done collecting all settings, - // so apply OS environment both before and after. - if err := l.applyOsEnvOverrides(d.Environ); err != nil { - return res, l.ModulesConfig, err - } - - workingDir := filepath.Clean(l.cfg.GetString("workingDir")) - - l.BaseConfig = config.BaseConfig{ - WorkingDir: workingDir, - CacheDir: l.cfg.GetString("cacheDir"), - ThemesDir: paths.AbsPathify(workingDir, l.cfg.GetString("themesDir")), - } - - var err error - l.BaseConfig.CacheDir, err = helpers.GetCacheDir(l.Fs, l.BaseConfig.CacheDir) - if err != nil { - return res, l.ModulesConfig, err - } - - res.BaseConfig = l.BaseConfig - - l.cfg.SetDefaultMergeStrategy() - - res.ConfigFiles = append(res.ConfigFiles, l.ModulesConfigFiles...) - - if d.Flags != nil { - if err := l.applyFlagsOverrides(d.Flags); err != nil { - return res, l.ModulesConfig, err - } - } - - if err := l.applyOsEnvOverrides(d.Environ); err != nil { - return res, l.ModulesConfig, err - } - - if err = l.applyConfigAliases(); err != nil { - return res, l.ModulesConfig, err - } - - return res, l.ModulesConfig, err -} - -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 - - var ignoreVendor glob.Glob - if s := conf.IgnoreVendorPaths; s != "" { - ignoreVendor, _ = hglob.GetGlob(hglob.NormalizePath(s)) - } - - ex := hexec.New(conf.Security, workingDir, l.Logger) - - hook := func(m *modules.ModulesConfig) error { - for _, tc := range m.AllModules { - if len(tc.ConfigFilenames()) > 0 { - if tc.Watch() { - l.ModulesConfigFiles = append(l.ModulesConfigFiles, tc.ConfigFilenames()...) - } - - // Merge in the theme config using the configured - // merge strategy. - cfg.Merge("", tc.Cfg().Get("")) - - } - } - - return nil - } - - modulesClient := modules.NewClient(modules.ClientConfig{ - 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() - - // We want to watch these for changes and trigger rebuild on version - // changes etc. - if moduleConfig.GoModulesFilename != "" { - l.ModulesConfigFiles = append(l.ModulesConfigFiles, moduleConfig.GoModulesFilename) - } - - if moduleConfig.GoWorkspaceFilename != "" { - l.ModulesConfigFiles = append(l.ModulesConfigFiles, moduleConfig.GoWorkspaceFilename) - } - - return moduleConfig, modulesClient, err -} - -func (l configLoader) loadConfig(configName string) (string, error) { - baseDir := l.BaseConfig.WorkingDir - var baseFilename string - if filepath.IsAbs(configName) { - baseFilename = configName - } else { - baseFilename = filepath.Join(baseDir, configName) - } - - var filename string - if paths.ExtNoDelimiter(configName) != "" { - exists, _ := helpers.Exists(baseFilename, l.Fs) - if exists { - filename = baseFilename - } - } else { - for _, ext := range config.ValidConfigFileExtensions { - filenameToCheck := baseFilename + "." + ext - exists, _ := helpers.Exists(filenameToCheck, l.Fs) - if exists { - filename = filenameToCheck - break - } - } - } - - if filename == "" { - return "", ErrNoConfigFile - } - - m, err := config.FromFileToMap(l.Fs, filename) - if err != nil { - return filename, err - } - - // Set overwrites keys of the same name, recursively. - l.cfg.Set("", m) - - if err := l.normalizeCfg(l.cfg); err != nil { - return filename, err - } - - if err := l.cleanExternalConfig(l.cfg); err != nil { - return filename, err - } - - return filename, nil -} - -func (l configLoader) deleteMergeStrategies() { - l.cfg.WalkParams(func(params ...maps.KeyParams) bool { - params[len(params)-1].Params.DeleteMergeStrategy() - return false - }) -} - -func (l configLoader) wrapFileError(err error, filename string) error { - fe := herrors.UnwrapFileError(err) - if fe != nil { - pos := fe.Position() - pos.Filename = filename - fe.UpdatePosition(pos) - return err - } - return herrors.NewFileErrorFromFile(err, filename, l.Fs, nil) -} diff --git a/config/allconfig/load_test.go b/config/allconfig/load_test.go deleted file mode 100644 index 3c16e71e9..000000000 --- a/config/allconfig/load_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package allconfig - -import ( - "os" - "path/filepath" - "testing" - - "github.com/spf13/afero" -) - -func BenchmarkLoad(b *testing.B) { - tempDir := b.TempDir() - configFilename := filepath.Join(tempDir, "hugo.toml") - config := ` -baseURL = "https://example.com" -defaultContentLanguage = 'en' - -[module] -[[module.mounts]] -source = 'content/en' -target = 'content/en' -lang = 'en' -[[module.mounts]] -source = 'content/nn' -target = 'content/nn' -lang = 'nn' -[[module.mounts]] -source = 'content/no' -target = 'content/no' -lang = 'no' -[[module.mounts]] -source = 'content/sv' -target = 'content/sv' -lang = 'sv' -[[module.mounts]] -source = 'layouts' -target = 'layouts' - -[languages] -[languages.en] -title = "English" -weight = 1 -[languages.nn] -title = "Nynorsk" -weight = 2 -[languages.no] -title = "Norsk" -weight = 3 -[languages.sv] -title = "Svenska" -weight = 4 -` - if err := os.WriteFile(configFilename, []byte(config), 0o666); err != nil { - b.Fatal(err) - } - d := ConfigSourceDescriptor{ - Fs: afero.NewOsFs(), - Filename: configFilename, - } - - for i := 0; i < b.N; i++ { - _, err := LoadConfig(d) - if err != nil { - b.Fatal(err) - } - } -} diff --git a/config/commonConfig.go b/config/commonConfig.go deleted file mode 100644 index 947078672..000000000 --- a/config/commonConfig.go +++ /dev/null @@ -1,511 +0,0 @@ -// Copyright 2019 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 ( - "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" - - "github.com/gohugoio/hugo/common/herrors" - "github.com/mitchellh/mapstructure" - "github.com/spf13/cast" -) - -type BaseConfig struct { - WorkingDir string - CacheDir string - ThemesDir string - PublishDir string -} - -type CommonDirs struct { - // The directory where Hugo will look for themes. - ThemesDir string - - // Where to put the generated files. - PublishDir string - - // The directory to put the generated resources files. This directory should in most situations be considered temporary - // and not be committed to version control. But there may be cached content in here that you want to keep, - // e.g. resources/_gen/images for performance reasons or CSS built from SASS when your CI server doesn't have the full setup. - ResourceDir string - - // The project root directory. - WorkingDir string - - // The root directory for all cache files. - CacheDir string - - // The content source directory. - // Deprecated: Use module mounts. - ContentDir string - // Deprecated: Use module mounts. - // The data source directory. - DataDir string - // Deprecated: Use module mounts. - // The layout source directory. - LayoutDir string - // Deprecated: Use module mounts. - // The i18n source directory. - I18nDir string - // Deprecated: Use module mounts. - // The archetypes source directory. - ArcheTypeDir string - // Deprecated: Use module mounts. - // The assets source directory. - AssetDir string -} - -type LoadConfigResult struct { - Cfg Provider - ConfigFiles []string - BaseConfig BaseConfig -} - -var defaultBuild = BuildConfig{ - UseResourceCacheWhen: "fallback", - BuildStats: BuildStats{}, - - CacheBusters: []CacheBuster{ - { - Source: `(postcss|tailwind)\.config\.js`, - Target: cssTargetCachebusterRe, - }, - }, -} - -// BuildConfig holds some build related configuration. -type BuildConfig struct { - // 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). - // Note that this was a bool <= v0.115.0. - BuildStats BuildStats - - // Can be used to toggle off writing of the IntelliSense /assets/jsconfig.js - // file. - NoJSConfigInAssets bool - - // Can used to control how the resource cache gets evicted on rebuilds. - 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 = slices.Clone(b.CacheBusters) - return b -} - -func (b BuildConfig) UseResourceCache(err error) bool { - if b.UseResourceCacheWhen == "never" { - return false - } - - if b.UseResourceCacheWhen == "fallback" { - return herrors.IsFeatureNotAvailableError(err) - } - - return true -} - -// MatchCacheBuster returns the cache buster for the given path p, nil if none. -func (s BuildConfig) MatchCacheBuster(logger loggers.Logger, p string) (func(string) bool, error) { - var matchers []func(string) bool - for _, cb := range s.CacheBusters { - if matcher := cb.compiledSource(p); matcher != nil { - matchers = append(matchers, matcher) - } - } - if len(matchers) > 0 { - return (func(cacheKey string) bool { - for _, m := range matchers { - if m(cacheKey) { - return true - } - } - return false - }), nil - } - return nil, nil -} - -func (b *BuildConfig) CompileConfig(logger loggers.Logger) error { - for i, cb := range b.CacheBusters { - if err := cb.CompileConfig(logger); err != nil { - return fmt.Errorf("failed to compile cache buster %q: %w", cb.Source, err) - } - b.CacheBusters[i] = cb - } - return nil -} - -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 b - } - - b.UseResourceCacheWhen = strings.ToLower(b.UseResourceCacheWhen) - when := b.UseResourceCacheWhen - if when != "never" && when != "always" && when != "fallback" { - b.UseResourceCacheWhen = "fallback" - } - - return b -} - -// SitemapConfig configures the sitemap to be generated. -type SitemapConfig struct { - // The page change frequency. - ChangeFreq string - // The priority of the page. - Priority float64 - // The sitemap filename. - Filename string - // Whether to disable page inclusion. - Disable bool -} - -func DecodeSitemap(prototype SitemapConfig, input map[string]any) (SitemapConfig, error) { - err := mapstructure.WeakDecode(input, &prototype) - return prototype, err -} - -// Config for the dev server. -type Server struct { - Headers []Headers - Redirects []Redirect - - compiledHeaders []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 { - if s.compiledHeaders != nil { - return nil - } - for _, h := range s.Headers { - 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 { - 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 -} - -func (s *Server) MatchHeaders(pattern string) []types.KeyValueStr { - if s.compiledHeaders == nil { - return nil - } - - var matches []types.KeyValueStr - - for i, g := range s.compiledHeaders { - if g.Match(pattern) { - h := s.Headers[i] - for k, v := range h.Values { - matches = append(matches, types.KeyValueStr{Key: k, Value: cast.ToString(v)}) - } - } - } - - sort.Slice(matches, func(i, j int) bool { - return matches[i].Key < matches[j].Key - }) - - return matches -} - -func (s *Server) MatchRedirect(pattern string, header http.Header) Redirect { - if s.compiledRedirects == nil { - return Redirect{} - } - - pattern = strings.TrimSuffix(pattern, "index.html") - - for i, r := range s.compiledRedirects { - redir := s.Redirects[i] - - 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 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 - } - } - - return Redirect{} -} - -type Headers struct { - For string - Values map[string]any -} - -type Redirect struct { - // From is the Glob pattern to match. - // One of From or FromRe must be set. - From 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. - Status int - - // Forcode redirect, even if original request path exists. - Force bool -} - -// CacheBuster configures cache busting for assets. -type CacheBuster struct { - // Trigger for files matching this regexp. - Source string - - // Cache bust targets matching this regexp. - // This regexp can contain group matches (e.g. $1) from the source regexp. - Target string - - compiledSource func(string) func(string) bool -} - -func (c *CacheBuster) CompileConfig(logger loggers.Logger) error { - if c.compiledSource != nil { - return nil - } - - source := c.Source - 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" - match := m != nil - if match { - matchString = "match!" - } - 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 { - currentTarget = strings.ReplaceAll(target, fmt.Sprintf("$%d", i+1), g) - } - targetRe, err := regexp.Compile(currentTarget) - if err != nil { - compileErr = fmt.Errorf("failed to compile cache buster target %q: %w", currentTarget, err) - return nil - } - return func(ss string) bool { - match = targetRe.MatchString(ss) - matchString := "no match" - if match { - matchString = "match!" - } - logger.Debugf("Matching %q with target %q: %s", ss, currentTarget, matchString) - - return match - } - } - return compileErr -} - -func (r Redirect) IsZero() bool { - return r.From == "" && r.FromRe == "" -} - -const ( - // Keep this a little coarse grained, some false positives are OK. - cssTargetCachebusterRe = `(css|styles|scss|sass)` -) - -func DecodeServer(cfg Provider) (Server, error) { - s := &Server{} - - _ = mapstructure.WeakDecode(cfg.GetStringMap("server"), s) - - for i, redir := range s.Redirects { - redir.To = strings.TrimSuffix(redir.To, "index.html") - s.Redirects[i] = redir - } - - if len(s.Redirects) == 0 { - // Set up a default redirect for 404s. - s.Redirects = []Redirect{ - { - 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 deleted file mode 100644 index 05ba185e3..000000000 --- a/config/commonConfig_test.go +++ /dev/null @@ -1,197 +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 config - -import ( - "errors" - "testing" - - "github.com/gohugoio/hugo/common/herrors" - "github.com/gohugoio/hugo/common/loggers" - "github.com/gohugoio/hugo/common/types" - - qt "github.com/frankban/quicktest" -) - -func TestBuild(t *testing.T) { - c := qt.New(t) - - v := New() - v.Set("build", map[string]any{ - "useResourceCacheWhen": "always", - }) - - b := DecodeBuildConfig(v) - - c.Assert(b.UseResourceCacheWhen, qt.Equals, "always") - - v.Set("build", map[string]any{ - "useResourceCacheWhen": "foo", - }) - - b = DecodeBuildConfig(v) - - c.Assert(b.UseResourceCacheWhen, qt.Equals, "fallback") - - c.Assert(b.UseResourceCache(herrors.ErrFeatureNotAvailable), qt.Equals, true) - c.Assert(b.UseResourceCache(errors.New("err")), qt.Equals, false) - - b.UseResourceCacheWhen = "always" - c.Assert(b.UseResourceCache(herrors.ErrFeatureNotAvailable), qt.Equals, true) - c.Assert(b.UseResourceCache(errors.New("err")), qt.Equals, true) - c.Assert(b.UseResourceCache(nil), qt.Equals, true) - - b.UseResourceCacheWhen = "never" - c.Assert(b.UseResourceCache(herrors.ErrFeatureNotAvailable), qt.Equals, false) - c.Assert(b.UseResourceCache(errors.New("err")), qt.Equals, false) - c.Assert(b.UseResourceCache(nil), qt.Equals, false) -} - -func TestServer(t *testing.T) { - c := qt.New(t) - - cfg, err := FromConfigString(`[[server.headers]] -for = "/*.jpg" - -[server.headers.values] -X-Frame-Options = "DENY" -X-XSS-Protection = "1; mode=block" -X-Content-Type-Options = "nosniff" - -[[server.redirects]] -from = "/foo/**" -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]] -from = "/google/**" -to = "https://google.com/" -status = 301 - - - -`, "toml") - - c.Assert(err, qt.IsNil) - - s, err := DecodeServer(cfg) - c.Assert(err, 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"}, - {Key: "X-Frame-Options", Value: "DENY"}, - {Key: "X-XSS-Protection", Value: "1; mode=block"}, - }) - - c.Assert(s.MatchRedirect("/foo/bar/baz", nil), qt.DeepEquals, Redirect{ - From: "/foo/**", - To: "/baz/", - Status: 200, - }) - - c.Assert(s.MatchRedirect("/foo/bar/", nil), qt.DeepEquals, Redirect{ - From: "/foo/**", - To: "/baz/", - Status: 200, - }) - - 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, - }) -} - -func TestBuildConfigCacheBusters(t *testing.T) { - c := qt.New(t) - cfg := New() - conf := DecodeBuildConfig(cfg) - l := loggers.NewDefault() - c.Assert(conf.CompileConfig(l), qt.IsNil) - - m, _ := conf.MatchCacheBuster(l, "tailwind.config.js") - c.Assert(m, qt.IsNotNil) - c.Assert(m("css"), qt.IsTrue) - c.Assert(m("js"), qt.IsFalse) - - 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 deleted file mode 100644 index dd103f27b..000000000 --- a/config/configLoader.go +++ /dev/null @@ -1,231 +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 config - -import ( - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/gohugoio/hugo/common/herrors" - - "github.com/gohugoio/hugo/common/paths" - - "github.com/gohugoio/hugo/common/maps" - "github.com/gohugoio/hugo/parser/metadecoders" - "github.com/spf13/afero" -) - -var ( - // See issue #8979 for context. - // Hugo has always used config.toml etc. as the default config file name. - // But hugo.toml is a more descriptive name, but we need to check for both. - DefaultConfigNames = []string{"hugo", "config"} - - DefaultConfigNamesSet = make(map[string]bool) - - ValidConfigFileExtensions = []string{"toml", "yaml", "yml", "json"} - validConfigFileExtensionsMap map[string]bool = make(map[string]bool) -) - -func init() { - for _, name := range DefaultConfigNames { - DefaultConfigNamesSet[name] = true - } - - for _, ext := range ValidConfigFileExtensions { - validConfigFileExtensionsMap[ext] = true - } -} - -// IsValidConfigFilename returns whether filename is one of the supported -// config formats in Hugo. -func IsValidConfigFilename(filename string) bool { - ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(filename), ".")) - return validConfigFileExtensionsMap[ext] -} - -func FromTOMLConfigString(config string) Provider { - cfg, err := FromConfigString(config, "toml") - if err != nil { - panic(err) - } - return cfg -} - -// FromConfigString creates a config from the given YAML, JSON or TOML config. This is useful in tests. -func FromConfigString(config, configType string) (Provider, error) { - m, err := readConfig(metadecoders.FormatFromString(configType), []byte(config)) - if err != nil { - return nil, err - } - return NewFrom(m), nil -} - -// FromFile loads the configuration from the given filename. -func FromFile(fs afero.Fs, filename string) (Provider, error) { - m, err := loadConfigFromFile(fs, filename) - if err != nil { - fe := herrors.UnwrapFileError(err) - if fe != nil { - pos := fe.Position() - pos.Filename = filename - fe.UpdatePosition(pos) - return nil, err - } - return nil, herrors.NewFileErrorFromFile(err, filename, fs, nil) - } - return NewFrom(m), nil -} - -// FromFileToMap is the same as FromFile, but it returns the config values -// as a simple map. -func FromFileToMap(fs afero.Fs, filename string) (map[string]any, error) { - return loadConfigFromFile(fs, filename) -} - -func readConfig(format metadecoders.Format, data []byte) (map[string]any, error) { - m, err := metadecoders.Default.UnmarshalToMap(data, format) - if err != nil { - return nil, err - } - - RenameKeys(m) - - return m, nil -} - -func loadConfigFromFile(fs afero.Fs, filename string) (map[string]any, error) { - m, err := metadecoders.Default.UnmarshalFileToMap(fs, filename) - if err != nil { - return nil, err - } - RenameKeys(m) - return m, nil -} - -func LoadConfigFromDir(sourceFs afero.Fs, configDir, environment string) (Provider, []string, error) { - defaultConfigDir := filepath.Join(configDir, "_default") - environmentConfigDir := filepath.Join(configDir, environment) - cfg := New() - - var configDirs []string - // Merge from least to most specific. - for _, dir := range []string{defaultConfigDir, environmentConfigDir} { - if _, err := sourceFs.Stat(dir); err == nil { - configDirs = append(configDirs, dir) - } - } - - if len(configDirs) == 0 { - return nil, nil, nil - } - - // Keep track of these so we can watch them for changes. - var dirnames []string - - for _, configDir := range configDirs { - err := afero.Walk(sourceFs, configDir, func(path string, fi os.FileInfo, err error) error { - if fi == nil || err != nil { - return nil - } - - if fi.IsDir() { - dirnames = append(dirnames, path) - return nil - } - - if !IsValidConfigFilename(path) { - return nil - } - - name := paths.Filename(filepath.Base(path)) - - item, err := metadecoders.Default.UnmarshalFileToMap(sourceFs, path) - if err != nil { - // This will be used in error reporting, use the most specific value. - dirnames = []string{path} - return fmt.Errorf("failed to unmarshal config for path %q: %w", path, err) - } - - var keyPath []string - if !DefaultConfigNamesSet[name] { - // Can be params.jp, menus.en etc. - name, lang := paths.FileAndExtNoDelimiter(name) - - keyPath = []string{name} - - if lang != "" { - keyPath = []string{"languages", lang} - switch name { - case "menu", "menus": - keyPath = append(keyPath, "menus") - case "params": - keyPath = append(keyPath, "params") - } - } - } - - root := item - if len(keyPath) > 0 { - root = make(map[string]any) - m := root - for i, key := range keyPath { - if i >= len(keyPath)-1 { - m[key] = item - } else { - nm := make(map[string]any) - m[key] = nm - m = nm - } - } - } - - // Migrate menu => menus etc. - RenameKeys(root) - - // Set will overwrite keys with the same name, recursively. - cfg.Set("", root) - - return nil - }) - if err != nil { - return nil, dirnames, err - } - - } - - return cfg, dirnames, nil -} - -var keyAliases maps.KeyRenamer - -func init() { - var err error - keyAliases, err = maps.NewKeyRenamer( - // Before 0.53 we used singular for "menu". - "{menu,languages/*/menu}", "menus", - ) - - if err != nil { - panic(err) - } -} - -// RenameKeys renames config keys in m recursively according to a global Hugo -// alias definition. -func RenameKeys(m map[string]any) { - keyAliases.Rename(m) -} diff --git a/config/configLoader_test.go b/config/configLoader_test.go deleted file mode 100644 index 546031334..000000000 --- a/config/configLoader_test.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2019 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 ( - "strings" - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestIsValidConfigFileName(t *testing.T) { - c := qt.New(t) - - for _, ext := range ValidConfigFileExtensions { - filename := "config." + ext - c.Assert(IsValidConfigFilename(filename), qt.Equals, true) - c.Assert(IsValidConfigFilename(strings.ToUpper(filename)), qt.Equals, true) - } - - c.Assert(IsValidConfigFilename(""), qt.Equals, false) - c.Assert(IsValidConfigFilename("config.toml.swp"), qt.Equals, false) -} diff --git a/config/configProvider.go b/config/configProvider.go index c21342dce..870341f7f 100644 --- a/config/configProvider.go +++ b/config/configProvider.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Hugo Authors. All rights reserved. +// Copyright 2017-present 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,100 +13,14 @@ package config -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" -) - -// AllProvider is a sub set of all config settings. -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 - NoBuildLock() bool - BaseConfig() BaseConfig - Dirs() CommonDirs - Quiet() bool - DirsBase() CommonDirs - ContentTypes() ContentTypesProvider - GetConfigSection(string) any - GetConfig() any - CanonifyURLs() bool - DisablePathToLower() bool - RemovePathAccents() bool - IsUglyURLs(section string) bool - DefaultContentLanguage() string - DefaultContentLanguageInSubdir() bool - IsLangDisabled(string) bool - SummaryLength() int - 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 - PrintI18nWarnings() bool - CreateTitle(s string) string - IgnoreFile(s string) bool - NewContentEditor() string - Timeout() time.Duration - StaticDirs() []string - 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. type Provider interface { GetString(key string) string GetInt(key string) int GetBool(key string) bool - GetParams(key string) maps.Params - GetStringMap(key string) map[string]any + GetStringMap(key string) map[string]interface{} GetStringMapString(key string) map[string]string - GetStringSlice(key string) []string - Get(key string) any - Set(key string, value any) - Keys() []string - Merge(key string, value any) - SetDefaults(params maps.Params) - SetDefaultMergeStrategy() - WalkParams(walkFn func(params ...maps.KeyParams) bool) + Get(key string) interface{} + Set(key string, value interface{}) IsSet(key string) bool } - -// GetStringSlicePreserveString returns a string slice from the given config and key. -// It differs from the GetStringSlice method in that if the config value is a string, -// we do not attempt to split it into fields. -func GetStringSlicePreserveString(cfg Provider, key string) []string { - sd := cfg.Get(key) - return types.ToStringSlicePreserveString(sd) -} diff --git a/config/configProvider_test.go b/config/configProvider_test.go deleted file mode 100644 index 0afba1e58..000000000 --- a/config/configProvider_test.go +++ /dev/null @@ -1,35 +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 config - -import ( - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestGetStringSlicePreserveString(t *testing.T) { - c := qt.New(t) - cfg := New() - - s := "This is a string" - sSlice := []string{"This", "is", "a", "slice"} - - cfg.Set("s1", s) - cfg.Set("s2", sSlice) - - c.Assert(GetStringSlicePreserveString(cfg, "s1"), qt.DeepEquals, []string{s}) - c.Assert(GetStringSlicePreserveString(cfg, "s2"), qt.DeepEquals, sSlice) - c.Assert(GetStringSlicePreserveString(cfg, "s3"), qt.IsNil) -} diff --git a/config/defaultConfigProvider.go b/config/defaultConfigProvider.go deleted file mode 100644 index 8c1d63851..000000000 --- a/config/defaultConfigProvider.go +++ /dev/null @@ -1,366 +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 ( - "fmt" - "strings" - "sync" - - xmaps "golang.org/x/exp/maps" - - "github.com/spf13/cast" - - "github.com/gohugoio/hugo/common/maps" -) - -// New creates a Provider backed by an empty maps.Params. -func New() Provider { - return &defaultConfigProvider{ - root: make(maps.Params), - } -} - -// NewFrom creates a Provider backed by params. -func NewFrom(params maps.Params) Provider { - maps.PrepareParams(params) - return &defaultConfigProvider{ - root: params, - } -} - -// defaultConfigProvider is a Provider backed by a map where all keys are lower case. -// All methods are thread safe. -type defaultConfigProvider struct { - mu sync.RWMutex - root maps.Params - - keyCache sync.Map -} - -func (c *defaultConfigProvider) Get(k string) any { - if k == "" { - return c.root - } - c.mu.RLock() - key, m := c.getNestedKeyAndMap(strings.ToLower(k), false) - if m == nil { - c.mu.RUnlock() - return nil - } - v := m[key] - c.mu.RUnlock() - return v -} - -func (c *defaultConfigProvider) GetBool(k string) bool { - v := c.Get(k) - return cast.ToBool(v) -} - -func (c *defaultConfigProvider) GetInt(k string) int { - v := c.Get(k) - return cast.ToInt(v) -} - -func (c *defaultConfigProvider) IsSet(k string) bool { - var found bool - c.mu.RLock() - key, m := c.getNestedKeyAndMap(strings.ToLower(k), false) - if m != nil { - _, found = m[key] - } - c.mu.RUnlock() - return found -} - -func (c *defaultConfigProvider) GetString(k string) string { - v := c.Get(k) - return cast.ToString(v) -} - -func (c *defaultConfigProvider) GetParams(k string) maps.Params { - v := c.Get(k) - if v == nil { - return nil - } - return v.(maps.Params) -} - -func (c *defaultConfigProvider) GetStringMap(k string) map[string]any { - v := c.Get(k) - return maps.ToStringMap(v) -} - -func (c *defaultConfigProvider) GetStringMapString(k string) map[string]string { - v := c.Get(k) - return maps.ToStringMapString(v) -} - -func (c *defaultConfigProvider) GetStringSlice(k string) []string { - v := c.Get(k) - return cast.ToStringSlice(v) -} - -func (c *defaultConfigProvider) Set(k string, v any) { - c.mu.Lock() - defer c.mu.Unlock() - - k = strings.ToLower(k) - - if k == "" { - if p, err := maps.ToParamsAndPrepare(v); err == nil { - // Set the values directly in root. - maps.SetParams(c.root, p) - } else { - c.root[k] = v - } - - return - } - - switch vv := v.(type) { - case map[string]any, map[any]any, map[string]string: - p := maps.MustToParamsAndPrepare(vv) - v = p - } - - key, m := c.getNestedKeyAndMap(k, true) - if m == nil { - return - } - - if existing, found := m[key]; found { - if p1, ok := existing.(maps.Params); ok { - if p2, ok := v.(maps.Params); ok { - maps.SetParams(p1, p2) - return - } - } - } - - m[key] = v -} - -// SetDefaults will set values from params if not already set. -func (c *defaultConfigProvider) SetDefaults(params maps.Params) { - maps.PrepareParams(params) - for k, v := range params { - if _, found := c.root[k]; !found { - c.root[k] = v - } - } -} - -func (c *defaultConfigProvider) Merge(k string, v any) { - c.mu.Lock() - defer c.mu.Unlock() - k = strings.ToLower(k) - - if k == "" { - rs, f := c.root.GetMergeStrategy() - if f && rs == maps.ParamsMergeStrategyNone { - // The user has set a "no merge" strategy on this, - // nothing more to do. - return - } - - if p, err := maps.ToParamsAndPrepare(v); err == nil { - // As there may be keys in p not in root, we need to handle - // those as a special case. - var keysToDelete []string - for kk, vv := range p { - if pp, ok := vv.(maps.Params); ok { - if pppi, ok := c.root[kk]; ok { - ppp := pppi.(maps.Params) - maps.MergeParamsWithStrategy("", ppp, pp) - } else { - // We need to use the default merge strategy for - // this key. - np := make(maps.Params) - strategy := c.determineMergeStrategy(maps.KeyParams{Key: "", Params: c.root}, maps.KeyParams{Key: kk, Params: np}) - np.SetMergeStrategy(strategy) - maps.MergeParamsWithStrategy("", np, pp) - c.root[kk] = np - if np.IsZero() { - // Just keep it until merge is done. - keysToDelete = append(keysToDelete, kk) - } - } - } - } - // Merge the rest. - maps.MergeParams(c.root, p) - for _, k := range keysToDelete { - delete(c.root, k) - } - } else { - panic(fmt.Sprintf("unsupported type %T received in Merge", v)) - } - - return - } - - switch vv := v.(type) { - case map[string]any, map[any]any, map[string]string: - p := maps.MustToParamsAndPrepare(vv) - v = p - } - - key, m := c.getNestedKeyAndMap(k, true) - if m == nil { - return - } - - if existing, found := m[key]; found { - if p1, ok := existing.(maps.Params); ok { - if p2, ok := v.(maps.Params); ok { - maps.MergeParamsWithStrategy("", p1, p2) - } - } - } else { - m[key] = v - } -} - -func (c *defaultConfigProvider) Keys() []string { - c.mu.RLock() - defer c.mu.RUnlock() - return xmaps.Keys(c.root) -} - -func (c *defaultConfigProvider) WalkParams(walkFn func(params ...maps.KeyParams) bool) { - var walk func(params ...maps.KeyParams) - walk = func(params ...maps.KeyParams) { - if walkFn(params...) { - return - } - p1 := params[len(params)-1] - i := len(params) - for k, v := range p1.Params { - if p2, ok := v.(maps.Params); ok { - paramsplus1 := make([]maps.KeyParams, i+1) - copy(paramsplus1, params) - paramsplus1[i] = maps.KeyParams{Key: k, Params: p2} - walk(paramsplus1...) - } - } - } - walk(maps.KeyParams{Key: "", Params: c.root}) -} - -func (c *defaultConfigProvider) determineMergeStrategy(params ...maps.KeyParams) maps.ParamsMergeStrategy { - if len(params) == 0 { - return maps.ParamsMergeStrategyNone - } - - var ( - strategy maps.ParamsMergeStrategy - prevIsRoot bool - curr = params[len(params)-1] - ) - - if len(params) > 1 { - prev := params[len(params)-2] - prevIsRoot = prev.Key == "" - - // Inherit from parent (but not from the root unless it's set by user). - s, found := prev.Params.GetMergeStrategy() - if !prevIsRoot && !found { - panic("invalid state, merge strategy not set on parent") - } - if found || !prevIsRoot { - strategy = s - } - } - - switch curr.Key { - case "": - // Don't set a merge strategy on the root unless set by user. - // This will be handled as a special case. - case "params": - strategy = maps.ParamsMergeStrategyDeep - case "outputformats", "mediatypes": - if prevIsRoot { - strategy = maps.ParamsMergeStrategyShallow - } - case "menus": - isMenuKey := prevIsRoot - if !isMenuKey { - // Can also be set below languages. - // root > languages > en > menus - if len(params) == 4 && params[1].Key == "languages" { - isMenuKey = true - } - } - if isMenuKey { - strategy = maps.ParamsMergeStrategyShallow - } - default: - if strategy == "" { - strategy = maps.ParamsMergeStrategyNone - } - } - - return strategy -} - -func (c *defaultConfigProvider) SetDefaultMergeStrategy() { - c.WalkParams(func(params ...maps.KeyParams) bool { - if len(params) == 0 { - return false - } - p := params[len(params)-1].Params - var found bool - if _, found = p.GetMergeStrategy(); found { - // Set by user. - return false - } - strategy := c.determineMergeStrategy(params...) - if strategy != "" { - p.SetMergeStrategy(strategy) - } - return false - }) -} - -func (c *defaultConfigProvider) getNestedKeyAndMap(key string, create bool) (string, maps.Params) { - var parts []string - v, ok := c.keyCache.Load(key) - if ok { - parts = v.([]string) - } else { - parts = strings.Split(key, ".") - c.keyCache.Store(key, parts) - } - current := c.root - for i := range len(parts) - 1 { - next, found := current[parts[i]] - if !found { - if create { - next = make(maps.Params) - current[parts[i]] = next - } else { - return "", nil - } - } - var ok bool - current, ok = next.(maps.Params) - if !ok { - // E.g. a string, not a map that we can store values in. - return "", nil - } - } - return parts[len(parts)-1], current -} diff --git a/config/defaultConfigProvider_test.go b/config/defaultConfigProvider_test.go deleted file mode 100644 index cd6247e60..000000000 --- a/config/defaultConfigProvider_test.go +++ /dev/null @@ -1,400 +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 ( - "context" - "errors" - "fmt" - "strconv" - "strings" - "testing" - - "github.com/gohugoio/hugo/common/para" - - "github.com/gohugoio/hugo/common/maps" - - qt "github.com/frankban/quicktest" -) - -func TestDefaultConfigProvider(t *testing.T) { - c := qt.New(t) - - c.Run("Set and get", func(c *qt.C) { - cfg := New() - var k string - var v any - - k, v = "foo", "bar" - cfg.Set(k, v) - c.Assert(cfg.Get(k), qt.Equals, v) - c.Assert(cfg.Get(strings.ToUpper(k)), qt.Equals, v) - c.Assert(cfg.GetString(k), qt.Equals, v) - - k, v = "foo", 42 - cfg.Set(k, v) - c.Assert(cfg.Get(k), qt.Equals, v) - c.Assert(cfg.GetInt(k), qt.Equals, v) - - c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{ - "foo": 42, - }) - }) - - c.Run("Set and get map", func(c *qt.C) { - cfg := New() - - cfg.Set("foo", map[string]any{ - "bar": "baz", - }) - - c.Assert(cfg.Get("foo"), qt.DeepEquals, maps.Params{ - "bar": "baz", - }) - - c.Assert(cfg.GetStringMap("foo"), qt.DeepEquals, map[string]any{"bar": string("baz")}) - c.Assert(cfg.GetStringMapString("foo"), qt.DeepEquals, map[string]string{"bar": string("baz")}) - }) - - c.Run("Set and get nested", func(c *qt.C) { - cfg := New() - - cfg.Set("a", map[string]any{ - "B": "bv", - }) - cfg.Set("a.c", "cv") - - c.Assert(cfg.Get("a"), qt.DeepEquals, maps.Params{ - "b": "bv", - "c": "cv", - }) - c.Assert(cfg.Get("a.c"), qt.Equals, "cv") - - cfg.Set("b.a", "av") - c.Assert(cfg.Get("b"), qt.DeepEquals, maps.Params{ - "a": "av", - }) - - cfg.Set("b", map[string]any{ - "b": "bv", - }) - - c.Assert(cfg.Get("b"), qt.DeepEquals, maps.Params{ - "a": "av", - "b": "bv", - }) - - cfg = New() - - cfg.Set("a", "av") - - cfg.Set("", map[string]any{ - "a": "av2", - "b": "bv2", - }) - - c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{ - "a": "av2", - "b": "bv2", - }) - - cfg = New() - - cfg.Set("a", "av") - - cfg.Set("", map[string]any{ - "b": "bv2", - }) - - c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{ - "a": "av", - "b": "bv2", - }) - - cfg = New() - - cfg.Set("", map[string]any{ - "foo": map[string]any{ - "a": "av", - }, - }) - - cfg.Set("", map[string]any{ - "foo": map[string]any{ - "b": "bv2", - }, - }) - - c.Assert(cfg.Get("foo"), qt.DeepEquals, maps.Params{ - "a": "av", - "b": "bv2", - }) - }) - - c.Run("Merge default strategy", func(c *qt.C) { - cfg := New() - - cfg.Set("a", map[string]any{ - "B": "bv", - }) - - cfg.Merge("a", map[string]any{ - "B": "bv2", - "c": "cv2", - }) - - c.Assert(cfg.Get("a"), qt.DeepEquals, maps.Params{ - "b": "bv", - "c": "cv2", - }) - - cfg = New() - - cfg.Set("a", "av") - - cfg.Merge("", map[string]any{ - "a": "av2", - "b": "bv2", - }) - - c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{ - "a": "av", - }) - }) - - c.Run("Merge shallow", func(c *qt.C) { - cfg := New() - - cfg.Set("a", map[string]any{ - "_merge": "shallow", - "B": "bv", - "c": map[string]any{ - "b": "bv", - }, - }) - - cfg.Merge("a", map[string]any{ - "c": map[string]any{ - "d": "dv2", - }, - "e": "ev2", - }) - - c.Assert(cfg.Get("a"), qt.DeepEquals, maps.Params{ - "e": "ev2", - "_merge": maps.ParamsMergeStrategyShallow, - "b": "bv", - "c": maps.Params{ - "b": "bv", - }, - }) - }) - - // Issue #8679 - c.Run("Merge typed maps", func(c *qt.C) { - for _, left := range []any{ - map[string]string{ - "c": "cv1", - }, - map[string]any{ - "c": "cv1", - }, - map[any]any{ - "c": "cv1", - }, - } { - cfg := New() - - cfg.Set("", map[string]any{ - "b": left, - }) - - cfg.Merge("", maps.Params{ - "b": maps.Params{ - "c": "cv2", - "d": "dv2", - }, - }) - - c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{ - "b": maps.Params{ - "c": "cv1", - "d": "dv2", - }, - }) - } - - for _, left := range []any{ - map[string]string{ - "b": "bv1", - }, - map[string]any{ - "b": "bv1", - }, - map[any]any{ - "b": "bv1", - }, - } { - for _, right := range []any{ - map[string]string{ - "b": "bv2", - "c": "cv2", - }, - map[string]any{ - "b": "bv2", - "c": "cv2", - }, - map[any]any{ - "b": "bv2", - "c": "cv2", - }, - } { - cfg := New() - - cfg.Set("a", left) - - cfg.Merge("a", right) - - c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{ - "a": maps.Params{ - "b": "bv1", - "c": "cv2", - }, - }) - } - } - }) - - // Issue #8701 - c.Run("Prevent _merge only maps", func(c *qt.C) { - cfg := New() - - cfg.Set("", map[string]any{ - "B": "bv", - }) - - cfg.Merge("", map[string]any{ - "c": map[string]any{ - "_merge": "shallow", - "d": "dv2", - }, - }) - - c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{ - "b": "bv", - }) - }) - - c.Run("IsSet", func(c *qt.C) { - cfg := New() - - cfg.Set("a", map[string]any{ - "B": "bv", - }) - - c.Assert(cfg.IsSet("A"), qt.IsTrue) - c.Assert(cfg.IsSet("a.b"), qt.IsTrue) - c.Assert(cfg.IsSet("z"), qt.IsFalse) - }) - - c.Run("Para", func(c *qt.C) { - cfg := New() - p := para.New(4) - r, _ := p.Start(context.Background()) - - setAndGet := func(k string, v int) error { - vs := strconv.Itoa(v) - cfg.Set(k, v) - err := errors.New("get failed") - if cfg.Get(k) != v { - return err - } - if cfg.GetInt(k) != v { - return err - } - if cfg.GetString(k) != vs { - return err - } - if !cfg.IsSet(k) { - return err - } - return nil - } - - for i := range 20 { - i := i - r.Run(func() error { - const v = 42 - k := fmt.Sprintf("k%d", i) - if err := setAndGet(k, v); err != nil { - return err - } - - m := maps.Params{ - "new": 42, - } - - cfg.Merge("", m) - - return nil - }) - } - - c.Assert(r.Wait(), qt.IsNil) - }) -} - -func BenchmarkDefaultConfigProvider(b *testing.B) { - type cfger interface { - Get(key string) any - Set(key string, value any) - IsSet(key string) bool - } - - newMap := func() map[string]any { - return map[string]any{ - "a": map[string]any{ - "b": map[string]any{ - "c": 32, - "d": 43, - }, - }, - "b": 62, - } - } - - runMethods := func(b *testing.B, cfg cfger) { - m := newMap() - cfg.Set("mymap", m) - cfg.Set("num", 32) - if !(cfg.IsSet("mymap") && cfg.IsSet("mymap.a") && cfg.IsSet("mymap.a.b") && cfg.IsSet("mymap.a.b.c")) { - b.Fatal("IsSet failed") - } - - if cfg.Get("num") != 32 { - b.Fatal("Get failed") - } - - if cfg.Get("mymap.a.b.c") != 32 { - b.Fatal("Get failed") - } - } - - b.Run("Custom", func(b *testing.B) { - cfg := New() - for i := 0; i < b.N; i++ { - runMethods(b, cfg) - } - }) -} diff --git a/config/env.go b/config/env.go deleted file mode 100644 index 4dcd63653..000000000 --- a/config/env.go +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright 2019 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 ( - "os" - "runtime" - "strconv" - "strings" - - "github.com/pbnjay/memory" -) - -const ( - gigabyte = 1 << 30 -) - -// GetNumWorkerMultiplier returns the base value used to calculate the number -// of workers to use for Hugo's parallel execution. -// It returns the value in HUGO_NUMWORKERMULTIPLIER OS env variable if set to a -// positive integer, else the number of logical CPUs. -func GetNumWorkerMultiplier() int { - if gmp := os.Getenv("HUGO_NUMWORKERMULTIPLIER"); gmp != "" { - if p, err := strconv.Atoi(gmp); err == nil && p > 0 { - return p - } - } - 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 { - setEnvVar(oldVars, keyValues[i], keyValues[i+1]) - } -} - -func SplitEnvVar(v string) (string, string) { - name, value, _ := strings.Cut(v, "=") - return name, value -} - -func setEnvVar(vars *[]string, key, value string) { - for i := range *vars { - if strings.HasPrefix((*vars)[i], key+"=") { - (*vars)[i] = key + "=" + value - return - } - } - // New var. - *vars = append(*vars, key+"="+value) -} diff --git a/config/env_test.go b/config/env_test.go deleted file mode 100644 index 3c402b9ef..000000000 --- a/config/env_test.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2019 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 ( - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestSetEnvVars(t *testing.T) { - t.Parallel() - c := qt.New(t) - vars := []string{"FOO=bar", "HUGO=cool", "BAR=foo"} - SetEnvVars(&vars, "HUGO", "rocking!", "NEW", "bar") - c.Assert(vars, qt.DeepEquals, []string{"FOO=bar", "HUGO=rocking!", "BAR=foo", "NEW=bar"}) - - key, val := SplitEnvVar("HUGO=rocks") - c.Assert(key, qt.Equals, "HUGO") - c.Assert(val, qt.Equals, "rocks") -} diff --git a/config/namespace.go b/config/namespace.go deleted file mode 100644 index e41b56e2d..000000000 --- a/config/namespace.go +++ /dev/null @@ -1,75 +0,0 @@ -// 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 config - -import ( - "encoding/json" - - "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 := hashing.HashStringHex(configSource) - - // Build the config - c, ext, err := buildConfig(configSource) - if err != nil { - return nil, err - } - - if ext == nil { - ext = configSource - } - - if ext == nil { - panic("ext is nil") - } - - ns := &ConfigNamespace[S, C]{ - SourceStructure: ext, - SourceHash: h, - Config: c, - } - - return ns, nil -} - -// ConfigNamespace holds a Hugo configuration namespace. -// The construct looks a little odd, but it's built to make the configuration elements -// both self-documenting and contained in a common structure. -type ConfigNamespace[S, C any] struct { - // SourceStructure represents the source configuration with any defaults applied. - // This is used for documentation and printing of the configuration setup to the user. - SourceStructure any - - // SourceHash is a hash of the source configuration before any defaults gets applied. - SourceHash string - - // Config is the final configuration as used by Hugo. - Config C -} - -// MarshalJSON marshals the source structure. -func (ns *ConfigNamespace[S, C]) MarshalJSON() ([]byte, error) { - return json.Marshal(ns.SourceStructure) -} - -// Signature returns the signature of the source structure. -// Note that this is for documentation purposes only and SourceStructure may not always be cast to S (it's usually just a map). -func (ns *ConfigNamespace[S, C]) Signature() S { - var s S - return s -} diff --git a/config/namespace_test.go b/config/namespace_test.go deleted file mode 100644 index f443523a4..000000000 --- a/config/namespace_test.go +++ /dev/null @@ -1,60 +0,0 @@ -// 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 config - -import ( - "strings" - "testing" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/common/maps" - "github.com/mitchellh/mapstructure" -) - -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 := DecodeNamespace[[]*tstNsExt]( - map[string]any{"foo": "bar"}, - func(v any) (*tstNsExt, any, error) { - t := &tstNsExt{} - m, err := maps.ToStringMapE(v) - if err != nil { - return nil, nil, err - } - return t, nil, mapstructure.WeakDecode(m, t) - }, - ) - - c.Assert(err, qt.IsNil) - c.Assert(ns, qt.Not(qt.IsNil)) - 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 - } -) - -func (t *tstNsExt) Init() error { - t.Foo = strings.ToUpper(t.Foo) - return nil -} diff --git a/config/privacy/privacyConfig.go b/config/privacy/privacyConfig.go deleted file mode 100644 index 900f73540..000000000 --- a/config/privacy/privacyConfig.go +++ /dev/null @@ -1,124 +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 privacy - -import ( - "github.com/gohugoio/hugo/config" - "github.com/mitchellh/mapstructure" -) - -const privacyConfigKey = "privacy" - -// Service is the common values for a service in a policy definition. -type Service struct { - Disable bool -} - -// Config is a privacy configuration for all the relevant services in Hugo. -type Config struct { - Disqus Disqus - GoogleAnalytics GoogleAnalytics - Instagram Instagram - 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. -type Disqus struct { - Service `mapstructure:",squash"` -} - -// GoogleAnalytics holds the privacy configuration settings related to the Google Analytics template. -type GoogleAnalytics struct { - Service `mapstructure:",squash"` - - // Enabling this will make the GA templates respect the - // "Do Not Track" HTTP header. See https://www.paulfurley.com/google-analytics-dnt/. - RespectDoNotTrack bool -} - -// Instagram holds the privacy configuration settings related to the Instagram shortcode. -type Instagram struct { - Service `mapstructure:",squash"` - - // If simple mode is enabled, a static and no-JS version of the Instagram - // image card will be built. - Simple bool -} - -// 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"` - - // When set to true, the Tweet 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 Tweet will be built. - Simple bool -} - -// Vimeo holds the privacy configuration settings related to the Vimeo shortcode. -type Vimeo struct { - Service `mapstructure:",squash"` - - // When set to true, the Vimeo player will be blocked from tracking any session data, - // including all cookies and stats. - EnableDNT bool - - // If simple mode is enabled, only a thumbnail is fetched from i.vimeocdn.com and - // shown with a play button overlaid. If a user clicks the button, he/she will - // be taken to the video page on vimeo.com in a new browser tab. - Simple bool -} - -// YouTube holds the privacy configuration settings related to the YouTube shortcode. -type YouTube struct { - Service `mapstructure:",squash"` - - // When you turn on privacy-enhanced mode, - // YouTube won’t store information about visitors on your website - // unless the user plays the embedded video. - 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) { - return - } - - m := cfg.GetStringMap(privacyConfigKey) - - err = mapstructure.WeakDecode(m, &pc) - - return -} diff --git a/config/privacy/privacyConfig_test.go b/config/privacy/privacyConfig_test.go deleted file mode 100644 index 1dd20215b..000000000 --- a/config/privacy/privacyConfig_test.go +++ /dev/null @@ -1,98 +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 privacy - -import ( - "testing" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/config" -) - -func TestDecodeConfigFromTOML(t *testing.T) { - c := qt.New(t) - - tomlConfig := ` - -someOtherValue = "foo" - -[privacy] -[privacy.disqus] -disable = true -[privacy.googleAnalytics] -disable = true -respectDoNotTrack = true -[privacy.instagram] -disable = true -simple = true -[privacy.x] -disable = true -enableDNT = true -simple = true -[privacy.vimeo] -disable = true -enableDNT = true -simple = true -[privacy.youtube] -disable = true -privacyEnhanced = true -simple = true -` - cfg, err := config.FromConfigString(tomlConfig, "toml") - c.Assert(err, qt.IsNil) - - pc, err := DecodeConfig(cfg) - c.Assert(err, qt.IsNil) - c.Assert(pc, qt.Not(qt.IsNil)) - - got := []bool{ - pc.Disqus.Disable, pc.GoogleAnalytics.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) -} - -func TestDecodeConfigFromTOMLCaseInsensitive(t *testing.T) { - c := qt.New(t) - - tomlConfig := ` - -someOtherValue = "foo" - -[Privacy] -[Privacy.YouTube] -PrivacyENhanced = true -` - cfg, err := config.FromConfigString(tomlConfig, "toml") - c.Assert(err, qt.IsNil) - - pc, err := DecodeConfig(cfg) - c.Assert(err, qt.IsNil) - c.Assert(pc, qt.Not(qt.IsNil)) - c.Assert(pc.YouTube.PrivacyEnhanced, qt.Equals, true) -} - -func TestDecodeConfigDefault(t *testing.T) { - c := qt.New(t) - - pc, err := DecodeConfig(config.New()) - c.Assert(err, qt.IsNil) - c.Assert(pc, qt.Not(qt.IsNil)) - c.Assert(pc.YouTube.PrivacyEnhanced, qt.Equals, false) -} diff --git a/config/security/securityConfig.go b/config/security/securityConfig.go deleted file mode 100644 index a3ec5197d..000000000 --- a/config/security/securityConfig.go +++ /dev/null @@ -1,230 +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 security - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "reflect" - "strings" - - "github.com/gohugoio/hugo/common/herrors" - "github.com/gohugoio/hugo/common/types" - "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/parser" - "github.com/gohugoio/hugo/parser/metadecoders" - "github.com/mitchellh/mapstructure" -) - -const securityConfigKey = "security" - -// DefaultConfig holds the default security policy. -var DefaultConfig = Config{ - Exec: Exec{ - 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: 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: MustNewWhitelist("^HUGO_", "^CI$"), - }, - HTTP: HTTP{ - URLs: MustNewWhitelist(".*"), - Methods: MustNewWhitelist("(?i)GET|POST"), - }, -} - -// Config is the top level security config. -// {"name": "security", "description": "This section holds the top level security config.", "newIn": "0.91.0" } -type Config struct { - // Restricts access to os.Exec.... - // { "newIn": "0.91.0" } - Exec Exec `json:"exec"` - - // Restricts access to certain template funcs. - Funcs Funcs `json:"funcs"` - - // Restricts access to resources.GetRemote, getJSON, getCSV. - HTTP HTTP `json:"http"` - - // Allow inline shortcodes - EnableInlineShortcodes bool `json:"enableInlineShortcodes"` -} - -// Exec holds os/exec policies. -type Exec struct { - Allow Whitelist `json:"allow"` - OsEnv Whitelist `json:"osEnv"` -} - -// Funcs holds template funcs policies. -type Funcs struct { - // OS env keys allowed to query in os.Getenv. - Getenv Whitelist `json:"getenv"` -} - -type HTTP struct { - // URLs to allow in remote HTTP (resources.Get, getJSON, getCSV). - URLs Whitelist `json:"urls"` - - // HTTP methods to allow. - Methods Whitelist `json:"methods"` - - // Media types where the Content-Type in the response is used instead of resolving from the file content. - MediaTypes Whitelist `json:"mediaTypes"` -} - -// ToTOML converts c to TOML with [security] as the root. -func (c Config) ToTOML() string { - sec := c.ToSecurityMap() - - var b bytes.Buffer - - if err := parser.InterfaceToConfig(sec, metadecoders.TOML, &b); err != nil { - panic(err) - } - - return strings.TrimSpace(b.String()) -} - -func (c Config) CheckAllowedExec(name string) error { - if !c.Exec.Allow.Accept(name) { - return &AccessDeniedError{ - name: name, - path: "security.exec.allow", - policies: c.ToTOML(), - } - } - return nil -} - -func (c Config) CheckAllowedGetEnv(name string) error { - if !c.Funcs.Getenv.Accept(name) { - return &AccessDeniedError{ - name: name, - path: "security.funcs.getenv", - policies: c.ToTOML(), - } - } - return nil -} - -func (c Config) CheckAllowedHTTPURL(url string) error { - if !c.HTTP.URLs.Accept(url) { - return &AccessDeniedError{ - name: url, - path: "security.http.urls", - policies: c.ToTOML(), - } - } - return nil -} - -func (c Config) CheckAllowedHTTPMethod(method string) error { - if !c.HTTP.Methods.Accept(method) { - return &AccessDeniedError{ - name: method, - path: "security.http.method", - policies: c.ToTOML(), - } - } - return nil -} - -// ToSecurityMap converts c to a map with 'security' as the root key. -func (c Config) ToSecurityMap() map[string]any { - // Take it to JSON and back to get proper casing etc. - asJson, err := json.Marshal(c) - herrors.Must(err) - m := make(map[string]any) - herrors.Must(json.Unmarshal(asJson, &m)) - - // Add the root - sec := map[string]any{ - "security": m, - } - return sec -} - -// DecodeConfig creates a privacy Config from a given Hugo configuration. -func DecodeConfig(cfg config.Provider) (Config, error) { - sc := DefaultConfig - if cfg.IsSet(securityConfigKey) { - m := cfg.GetStringMap(securityConfigKey) - dec, err := mapstructure.NewDecoder( - &mapstructure.DecoderConfig{ - WeaklyTypedInput: true, - Result: &sc, - DecodeHook: stringSliceToWhitelistHook(), - }, - ) - if err != nil { - return sc, err - } - - if err = dec.Decode(m); err != nil { - return sc, err - } - } - - if !sc.EnableInlineShortcodes { - // Legacy - sc.EnableInlineShortcodes = cfg.GetBool("enableInlineShortcodes") - } - - return sc, nil -} - -func stringSliceToWhitelistHook() mapstructure.DecodeHookFuncType { - return func( - f reflect.Type, - t reflect.Type, - data any, - ) (any, error) { - if t != reflect.TypeOf(Whitelist{}) { - return data, nil - } - - wl := types.ToStringSlicePreserveString(data) - - return NewWhitelist(wl...) - } -} - -// AccessDeniedError represents a security policy conflict. -type AccessDeniedError struct { - path string - name string - policies string -} - -func (e *AccessDeniedError) Error() string { - return fmt.Sprintf("access denied: %q is not whitelisted in policy %q; the current security configuration is:\n\n%s\n\n", e.name, e.path, e.policies) -} - -// IsAccessDenied reports whether err is an AccessDeniedError -func IsAccessDenied(err error) bool { - var notFoundErr *AccessDeniedError - return errors.As(err, ¬FoundErr) -} diff --git a/config/security/securityConfig_test.go b/config/security/securityConfig_test.go deleted file mode 100644 index faa05a97f..000000000 --- a/config/security/securityConfig_test.go +++ /dev/null @@ -1,167 +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 security - -import ( - "testing" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/config" -) - -func TestDecodeConfigFromTOML(t *testing.T) { - c := qt.New(t) - - c.Run("Slice whitelist", func(c *qt.C) { - c.Parallel() - tomlConfig := ` - - -someOtherValue = "bar" - -[security] -enableInlineShortcodes=true -[security.exec] -allow=["a", "b"] -osEnv=["a", "b", "c"] -[security.funcs] -getEnv=["a", "b"] - -` - - cfg, err := config.FromConfigString(tomlConfig, "toml") - c.Assert(err, qt.IsNil) - - pc, err := DecodeConfig(cfg) - c.Assert(err, qt.IsNil) - c.Assert(pc, qt.Not(qt.IsNil)) - c.Assert(pc.EnableInlineShortcodes, qt.IsTrue) - c.Assert(pc.Exec.Allow.Accept("a"), qt.IsTrue) - c.Assert(pc.Exec.Allow.Accept("d"), qt.IsFalse) - c.Assert(pc.Exec.OsEnv.Accept("a"), qt.IsTrue) - 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) { - c.Parallel() - tomlConfig := ` - - -someOtherValue = "bar" - -[security] -[security.exec] -allow="a" -osEnv="b" - -` - - cfg, err := config.FromConfigString(tomlConfig, "toml") - c.Assert(err, qt.IsNil) - - pc, err := DecodeConfig(cfg) - c.Assert(err, qt.IsNil) - c.Assert(pc, qt.Not(qt.IsNil)) - c.Assert(pc.Exec.Allow.Accept("a"), qt.IsTrue) - 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) { - c.Parallel() - tomlConfig := ` - - -someOtherValue = "bar" - -[security] -[security.exec] -allow="a" - -` - - cfg, err := config.FromConfigString(tomlConfig, "toml") - c.Assert(err, qt.IsNil) - - pc, err := DecodeConfig(cfg) - c.Assert(err, qt.IsNil) - c.Assert(pc, qt.Not(qt.IsNil)) - 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) { - c.Parallel() - tomlConfig := ` - - -someOtherValue = "bar" -enableInlineShortcodes=true - -[security] -[security.exec] -allow="a" -osEnv="b" - -` - - cfg, err := config.FromConfigString(tomlConfig, "toml") - c.Assert(err, qt.IsNil) - - pc, err := DecodeConfig(cfg) - c.Assert(err, qt.IsNil) - c.Assert(pc.EnableInlineShortcodes, qt.IsTrue) - }) -} - -func TestToTOML(t *testing.T) { - c := qt.New(t) - - got := DefaultConfig.ToTOML() - - c.Assert(got, qt.Equals, - "[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 = ['.*']", - ) -} - -func TestDecodeConfigDefault(t *testing.T) { - t.Parallel() - c := qt.New(t) - - pc, err := DecodeConfig(config.New()) - c.Assert(err, qt.IsNil) - c.Assert(pc, qt.Not(qt.IsNil)) - 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.HTTP.URLs.Accept("https://example.org"), qt.IsTrue) - c.Assert(pc.HTTP.Methods.Accept("POST"), qt.IsTrue) - c.Assert(pc.HTTP.Methods.Accept("GET"), qt.IsTrue) - c.Assert(pc.HTTP.Methods.Accept("get"), qt.IsTrue) - c.Assert(pc.HTTP.Methods.Accept("DELETE"), qt.IsFalse) - c.Assert(pc.HTTP.MediaTypes.Accept("application/msword"), qt.IsFalse) - - 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 deleted file mode 100644 index 5ce369a1f..000000000 --- a/config/security/whitelist.go +++ /dev/null @@ -1,116 +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 ( - "encoding/json" - "fmt" - "regexp" - "strings" -) - -const ( - acceptNoneKeyword = "none" -) - -// Whitelist holds a whitelist. -type Whitelist struct { - acceptNone bool - patterns []*regexp.Regexp - - // Store this for debugging/error reporting - patternsStrings []string -} - -// MarshalJSON is for internal use only. -func (w Whitelist) MarshalJSON() ([]byte, error) { - if w.acceptNone { - return json.Marshal(acceptNoneKeyword) - } - - return json.Marshal(w.patternsStrings) -} - -// 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, error) { - if len(patterns) == 0 { - return Whitelist{acceptNone: true}, nil - } - - var acceptSome bool - var patternsStrings []string - - for _, p := range patterns { - if p == acceptNoneKeyword { - acceptSome = false - break - } - - if ps := strings.TrimSpace(p); ps != "" { - acceptSome = true - patternsStrings = append(patternsStrings, ps) - } - } - - if !acceptSome { - return Whitelist{ - acceptNone: true, - }, nil - } - - var patternsr []*regexp.Regexp - - for i := range patterns { - p := strings.TrimSpace(patterns[i]) - if p == "" { - continue - } - 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}, 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. -func (w Whitelist) Accept(name string) bool { - if w.acceptNone { - return false - } - - for _, p := range w.patterns { - if p.MatchString(name) { - return true - } - } - return false -} - -func (w Whitelist) String() string { - return fmt.Sprint(w.patternsStrings) -} diff --git a/config/security/whitelist_test.go b/config/security/whitelist_test.go deleted file mode 100644 index add3345a8..000000000 --- a/config/security/whitelist_test.go +++ /dev/null @@ -1,46 +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 ( - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestWhitelist(t *testing.T) { - t.Parallel() - c := qt.New(t) - - c.Run("none", func(c *qt.C) { - 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 := MustNewWhitelist("^foo.*") - c.Assert(w.Accept("foo"), qt.IsTrue) - c.Assert(w.Accept("mfoo"), qt.IsFalse) - }) - - c.Run("Multiple", func(c *qt.C) { - 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 deleted file mode 100644 index f9d5e1a6e..000000000 --- a/config/services/servicesConfig.go +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright 2019 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 services - -import ( - "github.com/gohugoio/hugo/config" - "github.com/mitchellh/mapstructure" -) - -const ( - servicesConfigKey = "services" - - disqusShortnameKey = "disqusshortname" - googleAnalyticsKey = "googleanalytics" - rssLimitKey = "rssLimit" -) - -// Config is a privacy configuration for all the relevant services in Hugo. -type Config struct { - Disqus Disqus - GoogleAnalytics GoogleAnalytics - Instagram Instagram - Twitter Twitter // deprecated in favor of X in v0.141.0 - X X - RSS RSS -} - -// Disqus holds the functional configuration settings related to the Disqus template. -type Disqus struct { - // A Shortname is the unique identifier assigned to a Disqus site. - Shortname string -} - -// GoogleAnalytics holds the functional configuration settings related to the Google Analytics template. -type GoogleAnalytics struct { - // The GA tracking ID. - ID string -} - -// Instagram holds the functional configuration settings related to the Instagram shortcodes. -type Instagram struct { - // The Simple variant of the Instagram is decorated with Bootstrap 4 card classes. - // This means that if you use Bootstrap 4 or want to provide your own CSS, you want - // to disable the inline CSS provided by Hugo. - DisableInlineCSS bool - - // App or Client Access Token. - // If you are using a Client Access Token, remember that you must combine it with your App ID - // using a pipe symbol (|) otherwise the request will fail. - AccessToken string -} - -// 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 - // to disable the inline CSS provided by Hugo. - 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. - Limit int -} - -// DecodeConfig creates a services Config from a given Hugo configuration. -func DecodeConfig(cfg config.Provider) (c Config, err error) { - m := cfg.GetStringMap(servicesConfigKey) - - err = mapstructure.WeakDecode(m, &c) - - // Keep backwards compatibility. - if c.GoogleAnalytics.ID == "" { - // Try the global config - c.GoogleAnalytics.ID = cfg.GetString(googleAnalyticsKey) - } - if c.Disqus.Shortname == "" { - c.Disqus.Shortname = cfg.GetString(disqusShortnameKey) - } - - 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 deleted file mode 100644 index 952a7fe1c..000000000 --- a/config/services/servicesConfig_test.go +++ /dev/null @@ -1,69 +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 services - -import ( - "testing" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/config" -) - -func TestDecodeConfigFromTOML(t *testing.T) { - c := qt.New(t) - - tomlConfig := ` - -someOtherValue = "foo" - -[services] -[services.disqus] -shortname = "DS" -[services.googleAnalytics] -id = "ga_id" -[services.instagram] -disableInlineCSS = true -[services.twitter] -disableInlineCSS = true -[services.x] -disableInlineCSS = true -` - cfg, err := config.FromConfigString(tomlConfig, "toml") - c.Assert(err, qt.IsNil) - - config, err := DecodeConfig(cfg) - c.Assert(err, qt.IsNil) - c.Assert(config, qt.Not(qt.IsNil)) - - c.Assert(config.Disqus.Shortname, qt.Equals, "DS") - c.Assert(config.GoogleAnalytics.ID, qt.Equals, "ga_id") - - c.Assert(config.Instagram.DisableInlineCSS, qt.Equals, true) -} - -// Support old root-level GA settings etc. -func TestUseSettingsFromRootIfSet(t *testing.T) { - c := qt.New(t) - - cfg := config.New() - cfg.Set("disqusShortname", "root_short") - cfg.Set("googleAnalytics", "ga_root") - - config, err := DecodeConfig(cfg) - c.Assert(err, qt.IsNil) - c.Assert(config, qt.Not(qt.IsNil)) - - c.Assert(config.Disqus.Shortname, qt.Equals, "root_short") - c.Assert(config.GoogleAnalytics.ID, qt.Equals, "ga_root") -} diff --git a/config/testconfig/testconfig.go b/config/testconfig/testconfig.go deleted file mode 100644 index 8f70e6cb7..000000000 --- a/config/testconfig/testconfig.go +++ /dev/null @@ -1,83 +0,0 @@ -// 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. - -// This package should only be used for testing. -package testconfig - -import ( - _ "unsafe" - - "github.com/gohugoio/hugo/common/maps" - "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/config/allconfig" - "github.com/gohugoio/hugo/deps" - "github.com/gohugoio/hugo/hugofs" - toml "github.com/pelletier/go-toml/v2" - "github.com/spf13/afero" -) - -func GetTestConfigs(fs afero.Fs, cfg config.Provider) *allconfig.Configs { - if fs == nil { - fs = afero.NewMemMapFs() - } - if cfg == nil { - cfg = config.New() - } - // Make sure that the workingDir exists. - workingDir := cfg.GetString("workingDir") - if workingDir != "" { - if err := fs.MkdirAll(workingDir, 0o777); err != nil { - panic(err) - } - } - - 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 { - return GetTestConfigs(fs, cfg).GetFirstLanguageConfig() -} - -func GetTestDeps(fs afero.Fs, cfg config.Provider, beforeInit ...func(*deps.Deps)) *deps.Deps { - if fs == nil { - fs = afero.NewMemMapFs() - } - conf := GetTestConfig(fs, cfg) - d := &deps.Deps{ - Conf: conf, - Fs: hugofs.NewFrom(fs, conf.BaseConfig()), - } - for _, f := range beforeInit { - f(d) - } - if err := d.Init(); err != nil { - panic(err) - } - return d -} - -func GetTestConfigSectionFromStruct(section string, v any) config.AllProvider { - data, err := toml.Marshal(v) - if err != nil { - panic(err) - } - p := maps.Params{ - section: config.FromTOMLConfigString(string(data)).Get(""), - } - cfg := config.NewFrom(p) - return GetTestConfig(nil, cfg) -} diff --git a/create/content.go b/create/content.go index a4661c1ba..8af417294 100644 --- a/create/content.go +++ b/create/content.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Hugo Authors. All rights reserved. +// Copyright 2016 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,387 +16,114 @@ package create import ( "bytes" - "errors" - "fmt" - "io" "os" + "os/exec" "path/filepath" - "strings" - - "github.com/gohugoio/hugo/hugofs/glob" - - "github.com/gohugoio/hugo/common/hexec" - "github.com/gohugoio/hugo/common/hstrings" - "github.com/gohugoio/hugo/common/paths" - - "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/hugolib" - "github.com/spf13/afero" + jww "github.com/spf13/jwalterweatherman" ) -const ( - // DefaultArchetypeTemplateTemplate is the template used in 'hugo new site' - // and the template we use as a fall back. - DefaultArchetypeTemplateTemplate = `--- -title: "{{ replace .File.ContentBaseName "-" " " | title }}" -date: {{ .Date }} -draft: true ---- +// NewContent creates a new content file in the content directory based upon the +// given kind, which is used to lookup an archetype. +func NewContent( + ps *helpers.PathSpec, + siteFactory func(filename string, siteUsed bool) (*hugolib.Site, error), kind, targetPath string) error { + ext := helpers.Ext(targetPath) -` -) + jww.INFO.Printf("attempting to create %q of %q of ext %q", targetPath, kind, ext) -// 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 _, err := h.BaseFs.Content.Fs.Stat(""); err != nil { - return errors.New("no existing content directory configured for this project") - } + archetypeFilename := findArchetype(ps, kind, ext) - cf := hugolib.NewContentFactory(h) + // Building the sites can be expensive, so only do it if really needed. + siteUsed := false - if kind == "" { - var err error - kind, err = cf.SectionFromFilename(targetPath) + if archetypeFilename != "" { + f, err := ps.Fs.Source.Open(archetypeFilename) if err != nil { return err } + defer f.Close() + + if helpers.ReaderContains(f, []byte(".Site")) { + siteUsed = true + } } - b := &contentBuilder{ - archeTypeFs: h.PathSpec.BaseFs.Archetypes.Fs, - sourceFs: h.PathSpec.Fs.Source, - ps: h.PathSpec, - h: h, - cf: cf, - - kind: kind, - targetPath: targetPath, - force: force, - } - - ext := paths.Ext(targetPath) - - b.setArcheTypeFilenameToUse(ext) - - withBuildLock := func() (string, error) { - 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() - } - - if b.isDir { - return "", b.buildDir() - } - - if ext == "" { - return "", fmt.Errorf("failed to resolve %q to an archetype template", 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() + s, err := siteFactory(targetPath, siteUsed) if err != nil { return err } - if filename != "" { - return b.openInEditorIfConfigured(filename) + var content []byte + + content, err = executeArcheTypeAsTemplate(s, kind, targetPath, archetypeFilename) + if err != nil { + return err + } + + contentPath := s.PathSpec.AbsPathify(filepath.Join(s.Cfg.GetString("contentDir"), targetPath)) + + if err := helpers.SafeWriteToDisk(contentPath, bytes.NewReader(content), s.Fs.Source); err != nil { + return err + } + + jww.FEEDBACK.Println(contentPath, "created") + + editor := s.Cfg.GetString("newContentEditor") + if editor != "" { + jww.FEEDBACK.Printf("Editing %s with %q ...\n", targetPath, editor) + + cmd := exec.Command(editor, contentPath) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return cmd.Run() } return nil } -type contentBuilder struct { - archeTypeFs afero.Fs - sourceFs afero.Fs +// FindArchetype takes a given kind/archetype of content and returns an output +// path for that archetype. If no archetype is found, an empty string is +// returned. +func findArchetype(ps *helpers.PathSpec, kind, ext string) (outpath string) { + search := []string{ps.AbsPathify(ps.Cfg.GetString("archetypeDir"))} - ps *helpers.PathSpec - h *hugolib.HugoSites - cf hugolib.ContentFactory - - // Builder state - archetypeFi hugofs.FileMetaInfo - targetPath string - kind string - isDir bool - dirMap archetypeMap - force bool -} - -func (b *contentBuilder) buildDir() error { - // Split the dir into content files and the rest. - if err := b.mapArcheTypeDir(); err != nil { - return err + if ps.Cfg.GetString("theme") != "" { + themeDir := filepath.Join(ps.AbsPathify(ps.Cfg.GetString("themesDir")+"/"+ps.Cfg.GetString("theme")), "/archetypes/") + if _, err := ps.Fs.Source.Stat(themeDir); os.IsNotExist(err) { + jww.ERROR.Printf("Unable to find archetypes directory for theme %q at %q", ps.Cfg.GetString("theme"), themeDir) + } else { + search = append(search, themeDir) + } } - var contentTargetFilenames []string - var baseDir string + for _, x := range search { + // If the new content isn't in a subdirectory, kind == "". + // Therefore it should be excluded otherwise `is a directory` + // error will occur. github.com/gohugoio/hugo/issues/411 + var pathsToCheck = []string{"default"} - for _, fi := range b.dirMap.contentFiles { - - 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 - } - if baseDir == "" { - baseDir = strings.TrimSuffix(abs, targetFilename) - } - - contentTargetFilenames = append(contentTargetFilenames, abs) - } - - var contentInclusionFilter *glob.FilenameFilter - if !b.dirMap.siteUsed { - // We don't need to build everything. - contentInclusionFilter = glob.NewFilenameFilterForInclusionFunc(func(filename string) bool { - filename = strings.TrimPrefix(filename, string(os.PathSeparator)) - for _, cn := range contentTargetFilenames { - if strings.Contains(cn, filename) { - return true - } + if ext != "" { + if kind != "" { + pathsToCheck = append([]string{kind + ext, "default" + ext}, pathsToCheck...) + } else { + pathsToCheck = append([]string{"default" + ext}, pathsToCheck...) } - return false - }) - } - - if err := b.h.Build(hugolib.BuildCfg{NoBuildLock: true, SkipRender: true, ContentInclusionFilter: contentInclusionFilter}); err != nil { - return err - } - - for i, filename := range contentTargetFilenames { - if err := b.applyArcheType(filename, b.dirMap.contentFiles[i]); err != nil { - return err - } - } - - // Copy the rest as is. - 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(fi.Meta().Filename, b.archetypeFi.Meta().Filename)) - targetDir := filepath.Dir(targetFilename) - - if err := b.sourceFs.MkdirAll(targetDir, 0o777); err != nil && !os.IsExist(err) { - return fmt.Errorf("failed to create target directory for %q: %w", targetDir, err) } - out, err := b.sourceFs.Create(targetFilename) - if err != nil { - return err - } - - _, err = io.Copy(out, in) - if err != nil { - return err - } - - in.Close() - out.Close() - } - - b.h.Log.Printf("Content dir %q created", filepath.Join(baseDir, b.targetPath)) - - return nil -} - -func (b *contentBuilder) buildFile() (string, error) { - contentPlaceholderAbsFilename, err := b.cf.CreateContentPlaceHolder(b.targetPath, b.force) - if err != nil { - return "", err - } - - usesSite, err := b.usesSiteVar(b.archetypeFi) - if err != nil { - return "", err - } - - var contentInclusionFilter *glob.FilenameFilter - if !usesSite { - // We don't need to build everything. - contentInclusionFilter = glob.NewFilenameFilterForInclusionFunc(func(filename string) bool { - filename = strings.TrimPrefix(filename, string(os.PathSeparator)) - return strings.Contains(contentPlaceholderAbsFilename, filename) - }) - } - - if err := b.h.Build(hugolib.BuildCfg{NoBuildLock: true, SkipRender: true, ContentInclusionFilter: contentInclusionFilter}); err != nil { - return "", err - } - - if err := b.applyArcheType(contentPlaceholderAbsFilename, b.archetypeFi); err != nil { - return "", err - } - - b.h.Log.Printf("Content %q created", contentPlaceholderAbsFilename) - - return contentPlaceholderAbsFilename, nil -} - -func (b *contentBuilder) setArcheTypeFilenameToUse(ext string) { - var pathsToCheck []string - - if b.kind != "" { - pathsToCheck = append(pathsToCheck, b.kind+ext) - } - - pathsToCheck = append(pathsToCheck, "default"+ext) - - for _, p := range pathsToCheck { - fi, err := b.archeTypeFs.Stat(p) - if err == nil { - b.archetypeFi = fi.(hugofs.FileMetaInfo) - b.isDir = fi.IsDir() - return - } - } -} - -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)) - } - - f, err := b.sourceFs.Create(contentFilename) - if err != nil { - return err - } - defer f.Close() - - if archetypeFi == nil { - return b.cf.ApplyArchetypeTemplate(f, p, b.kind, DefaultArchetypeTemplateTemplate) - } - - return b.cf.ApplyArchetypeFi(f, p, b.kind, archetypeFi) -} - -func (b *contentBuilder) mapArcheTypeDir() error { - var m archetypeMap - - seen := map[hstrings.Strings2]bool{} - - walkFn := func(path string, fim hugofs.FileMetaInfo) error { - if fim.IsDir() { - return nil - } - - pi := fim.Meta().PathInfo - - 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()) + for _, p := range pathsToCheck { + curpath := filepath.Join(x, p) + jww.DEBUG.Println("checking", curpath, "for archetypes") + if exists, _ := helpers.Exists(curpath, ps.Fs.Source); exists { + jww.INFO.Println("curpath: " + curpath) + return curpath } - seen[pathLang] = true - m.contentFiles = append(m.contentFiles, fim) - if !m.siteUsed { - var err error - m.siteUsed, err = b.usesSiteVar(fim) - if err != nil { - return err - } - } - return nil } - - m.otherFiles = append(m.otherFiles, fim) - - return nil } - walkCfg := hugofs.WalkwayConfig{ - WalkFn: walkFn, - Fs: b.archeTypeFs, - 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.archetypeFi.Meta().Filename, err) - } - - b.dirMap = m - - return nil -} - -func (b *contentBuilder) openInEditorIfConfigured(filename string) error { - editor := b.h.Conf.NewContentEditor() - if editor == "" { - return nil - } - - editorExec := strings.Fields(editor)[0] - editorFlags := strings.Fields(editor)[1:] - - var args []any - for _, editorFlag := range editorFlags { - args = append(args, editorFlag) - } - args = append( - args, - filename, - hexec.WithStdin(os.Stdin), - hexec.WithStderr(os.Stderr), - hexec.WithStdout(os.Stdout), - ) - - b.h.Log.Printf("Editing %q with %q ...\n", filename, editorExec) - - cmd, err := b.h.Deps.ExecHelper.New(editorExec, args...) - if err != nil { - return err - } - - return cmd.Run() -} - -func (b *contentBuilder) usesSiteVar(fi hugofs.FileMetaInfo) (bool, error) { - if fi == nil { - return false, nil - } - f, err := fi.Meta().Open() - if err != nil { - 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 { - // These needs to be parsed and executed as Go templates. - contentFiles []hugofs.FileMetaInfo - // These are just copied to destination. - otherFiles []hugofs.FileMetaInfo - // If the templates needs a fully built site. This can potentially be - // expensive, so only do when needed. - siteUsed bool + return "" } diff --git a/create/content_template_handler.go b/create/content_template_handler.go new file mode 100644 index 000000000..0be495d15 --- /dev/null +++ b/create/content_template_handler.go @@ -0,0 +1,135 @@ +// Copyright 2017 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 create + +import ( + "bytes" + "fmt" + "strings" + "time" + + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/source" + + "github.com/gohugoio/hugo/hugolib" + "github.com/gohugoio/hugo/tpl" + "github.com/spf13/afero" +) + +// ArchetypeFileData represents the data available to an archetype template. +type ArchetypeFileData struct { + // The archetype content type, either given as --kind option or extracted + // from the target path's section, i.e. "blog/mypost.md" will resolve to + // "blog". + Type string + + // The current date and time as a RFC3339 formatted string, suitable for use in front matter. + Date string + + // The Site, fully equipped with all the pages etc. Note: This will only be set if it is actually + // used in the archetype template. Also, if this is a multilingual setup, + // this site is the site that best matches the target content file, based + // on the presence of language code in the filename. + Site *hugolib.Site + + // The target content file. Note that the .Content will be empty, as that + // has not been created yet. + *source.File +} + +const ( + ArchetypeTemplateTemplate = `--- +title: "{{ replace .TranslationBaseName "-" " " | title }}" +date: {{ .Date }} +draft: true +--- + +` +) + +var ( + archetypeShortcodeReplacementsPre = strings.NewReplacer( + "{{<", "{x{<", + "{{%", "{x{%", + ">}}", ">}x}", + "%}}", "%}x}") + + archetypeShortcodeReplacementsPost = strings.NewReplacer( + "{x{<", "{{<", + "{x{%", "{{%", + ">}x}", ">}}", + "%}x}", "%}}") +) + +func executeArcheTypeAsTemplate(s *hugolib.Site, kind, targetPath, archetypeFilename string) ([]byte, error) { + + var ( + archetypeContent []byte + archetypeTemplate []byte + err error + ) + + sp := source.NewSourceSpec(s.Deps.Cfg, s.Deps.Fs) + f := sp.NewFile(targetPath) + + data := ArchetypeFileData{ + Type: kind, + Date: time.Now().Format(time.RFC3339), + File: f, + Site: s, + } + + if archetypeFilename == "" { + // TODO(bep) archetype revive the issue about wrong tpl funcs arg order + archetypeTemplate = []byte(ArchetypeTemplateTemplate) + } else { + archetypeTemplate, err = afero.ReadFile(s.Fs.Source, archetypeFilename) + if err != nil { + return nil, fmt.Errorf("Failed to read archetype file %q: %s", archetypeFilename, err) + } + + } + + // The archetype template may contain shortcodes, and these does not play well + // with the Go templates. Need to set some temporary delimiters. + archetypeTemplate = []byte(archetypeShortcodeReplacementsPre.Replace(string(archetypeTemplate))) + + // Reuse the Hugo template setup to get the template funcs properly set up. + templateHandler := s.Deps.Tmpl.(tpl.TemplateHandler) + templateName := "_text/" + helpers.Filename(archetypeFilename) + if err := templateHandler.AddTemplate(templateName, string(archetypeTemplate)); err != nil { + return nil, fmt.Errorf("Failed to parse archetype file %q: %s", archetypeFilename, err) + } + + templ := templateHandler.Lookup(templateName) + + var buff bytes.Buffer + if err := templ.Execute(&buff, data); err != nil { + return nil, fmt.Errorf("Failed to process archetype file %q: %s", archetypeFilename, err) + } + + archetypeContent = []byte(archetypeShortcodeReplacementsPost.Replace(buff.String())) + + if !bytes.Contains(archetypeContent, []byte("date")) || !bytes.Contains(archetypeContent, []byte("title")) { + // TODO(bep) remove some time in the future. + s.Log.FEEDBACK.Println(fmt.Sprintf(`WARNING: date and/or title missing from archetype file %q. +From Hugo 0.24 this must be provided in the archetype file itself, if needed. Example: +%s +`, archetypeFilename, ArchetypeTemplateTemplate)) + + } + + return archetypeContent, nil + +} diff --git a/create/content_test.go b/create/content_test.go index 429edfc26..914759164 100644 --- a/create/content_test.go +++ b/create/content_test.go @@ -14,150 +14,85 @@ package create_test import ( - "fmt" "os" "path/filepath" "strings" "testing" - "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/config/allconfig" - "github.com/gohugoio/hugo/config/testconfig" - "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/hugolib" + "fmt" + "github.com/gohugoio/hugo/hugofs" - qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/create" "github.com/gohugoio/hugo/helpers" "github.com/spf13/afero" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" ) -// TODO(bep) clean this up. Export the test site builder in Hugolib or something. -func TestNewContentFromFile(t *testing.T) { +func TestNewContent(t *testing.T) { + v := viper.New() + initViper(v) + cases := []struct { - name string kind string path string - expected any + expected []string }{ - {"Post", "post", "post/sample-1.md", []string{`title = "Post Arch title"`, `test = "test1"`, "date = \"2015-01-12T19:20:04-07:00\""}}, - {"Post org-mode", "post", "post/org-1.org", []string{`#+title: ORG-1`}}, - {"Post, unknown content filetype", "post", "post/sample-1.pdoc", false}, - {"Empty date", "emptydate", "post/sample-ed.md", []string{`title = "Empty Date Arch title"`, `test = "test1"`}}, - {"Archetype file not found", "stump", "stump/sample-2.md", []string{`title: "Sample 2"`}}, // no archetype file - {"No archetype", "", "sample-3.md", []string{`title: "Sample 3"`}}, // no archetype - {"Empty archetype", "product", "product/sample-4.md", []string{`title = "SAMPLE-4"`}}, // empty archetype front matter - {"Filenames", "filenames", "content/mypage/index.md", []string{"title = \"INDEX\"\n+++\n\n\nContentBaseName: mypage"}}, - {"Branch Name", "name", "content/tags/tag-a/_index.md", []string{"+++\ntitle = 'Tag A'\n+++"}}, - - {"Lang 1", "lang", "post/lang-1.md", []string{`Site Lang: en|Name: Lang 1|i18n: Hugo Rocks!`}}, - {"Lang 2", "lang", "post/lang-2.en.md", []string{`Site Lang: en|Name: Lang 2|i18n: Hugo Rocks!`}}, - {"Lang nn file", "lang", "content/post/lang-3.nn.md", []string{`Site Lang: nn|Name: Lang 3|i18n: Hugo Rokkar!`}}, - {"Lang nn dir", "lang", "content_nn/post/lang-4.md", []string{`Site Lang: nn|Name: Lang 4|i18n: Hugo Rokkar!`}}, - {"Lang en in nn dir", "lang", "content_nn/post/lang-5.en.md", []string{`Site Lang: en|Name: Lang 5|i18n: Hugo Rocks!`}}, - {"Lang en default", "lang", "post/my-bundle/index.md", []string{`Site Lang: en|Name: My Bundle|i18n: Hugo Rocks!`}}, - {"Lang en file", "lang", "post/my-bundle/index.en.md", []string{`Site Lang: en|Name: My Bundle|i18n: Hugo Rocks!`}}, - {"Lang nn bundle", "lang", "content/post/my-bundle/index.nn.md", []string{`Site Lang: nn|Name: My Bundle|i18n: Hugo Rokkar!`}}, - {"Site", "site", "content/mypage/index.md", []string{"RegularPages .Site: 10", "RegularPages site: 10"}}, - {"Shortcodes", "shortcodes", "shortcodes/go.md", []string{ + {"post", "post/sample-1.md", []string{`title = "Post Arch title"`, `test = "test1"`, "date = \"2015-01-12T19:20:04-07:00\""}}, + {"post", "post/org-1.org", []string{`#+title: ORG-1`}}, + {"emptydate", "post/sample-ed.md", []string{`title = "Empty Date Arch title"`, `test = "test1"`}}, + {"stump", "stump/sample-2.md", []string{`title: "Sample 2"`}}, // no archetype file + {"", "sample-3.md", []string{`title: "Sample 3"`}}, // no archetype + {"product", "product/sample-4.md", []string{`title = "SAMPLE-4"`}}, // empty archetype front matter + {"shortcodes", "shortcodes/go.md", []string{ `title = "GO"`, "{{< myshortcode >}}", "{{% myshortcode %}}", - "{{}}\n{{%/* comment */%}}", - }}, // shortcodes + "{{}}\n{{%/* comment */%}}"}}, // shortcodes } - c := qt.New(t) + for _, c := range cases { + cfg, fs := newTestCfg() + ps, err := helpers.NewPathSpec(fs, cfg) + require.NoError(t, err) + h, err := hugolib.NewHugoSites(deps.DepsCfg{Cfg: cfg, Fs: fs}) + require.NoError(t, err) + require.NoError(t, initFs(fs)) - for i, cas := range cases { - cas := cas + siteFactory := func(filename string, siteUsed bool) (*hugolib.Site, error) { + return h.Sites[0], nil + } - c.Run(cas.name, func(c *qt.C) { - c.Parallel() + require.NoError(t, create.NewContent(ps, siteFactory, c.kind, c.path)) - mm := afero.NewMemMapFs() - 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) - err = create.NewContent(h, cas.kind, cas.path, false) - - if b, ok := cas.expected.(bool); ok && !b { - if !b { - c.Assert(err, qt.Not(qt.IsNil)) - } - return + fname := filepath.Join("content", filepath.FromSlash(c.path)) + content := readFileFromFs(t, fs.Source, fname) + for i, v := range c.expected { + found := strings.Contains(content, v) + if !found { + t.Errorf("[%d] %q missing from output:\n%q", i, v, content) } - - c.Assert(err, qt.IsNil) - - fname := filepath.FromSlash(cas.path) - if !strings.HasPrefix(fname, "content") { - fname = filepath.Join("content", fname) - } - - content := readFileFromFs(c, fs.Source, fname) - - for _, v := range cas.expected.([]string) { - found := strings.Contains(content, v) - if !found { - c.Fatalf("[%d] %q missing from output:\n%q", i, v, content) - } - } - }) - + } } } -func TestNewContentFromDirSiteFunction(t *testing.T) { - mm := afero.NewMemMapFs() - c := qt.New(t) - - archetypeDir := filepath.Join("archetypes", "my-bundle") - defaultArchetypeDir := filepath.Join("archetypes", "default") - c.Assert(mm.MkdirAll(archetypeDir, 0o755), qt.IsNil) - c.Assert(mm.MkdirAll(defaultArchetypeDir, 0o755), qt.IsNil) - - contentFile := ` -File: %s -site RegularPages: {{ len site.RegularPages }} - -` - - 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) - 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/index.md")), `site RegularPages: 10`) - - // Default bundle archetype - c.Assert(create.NewContent(h, "", "post/my-post2", false), qt.IsNil) - cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post2/index.md")), `default archetype index.md`) - - // Regular file with bundle kind. - c.Assert(create.NewContent(h, "my-bundle", "post/foo.md", false), qt.IsNil) - cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/foo.md")), `draft: true`) - - // 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 initViper(v *viper.Viper) { + v.Set("metaDataFormat", "toml") + v.Set("archetypeDir", "archetypes") + v.Set("contentDir", "content") + v.Set("themesDir", "themes") + v.Set("layoutDir", "layouts") + v.Set("i18nDir", "i18n") + v.Set("theme", "sample") } -func initFs(fs afero.Fs) error { - perm := os.FileMode(0o755) +func initFs(fs *hugofs.Fs) error { + perm := os.FileMode(0755) var err error // create directories @@ -167,22 +102,13 @@ func initFs(fs afero.Fs) error { filepath.Join("themes", "sample", "archetypes"), } for _, dir := range dirs { - err = fs.Mkdir(dir, perm) - if err != nil && !os.IsExist(err) { + err = fs.Source.Mkdir(dir, perm) + if err != nil { return err } } - // create some dummy content - for i := 1; i <= 10; i++ { - filename := filepath.Join("content", fmt.Sprintf("page%d.md", i)) - afero.WriteFile(fs, filename, []byte(`--- -title: Test ---- -`), 0o666) - } - - // create archetype files + // create files for _, v := range []struct { path string content string @@ -195,49 +121,16 @@ title: Test path: filepath.Join("archetypes", "post.org"), content: "#+title: {{ .BaseFileName | upper }}", }, - { - path: filepath.Join("archetypes", "name.md"), - content: `+++ -title = '{{ replace .Name "-" " " | title }}' -+++`, - }, { path: filepath.Join("archetypes", "product.md"), content: `+++ title = "{{ .BaseFileName | upper }}" +++`, - }, - { - path: filepath.Join("archetypes", "filenames.md"), - content: `... -title = "{{ .BaseFileName | upper }}" -+++ - - -ContentBaseName: {{ .File.ContentBaseName }} - -`, - }, - { - path: filepath.Join("archetypes", "site.md"), - content: `... -title = "{{ .BaseFileName | upper }}" -+++ - -Len RegularPages .Site: {{ len .Site.RegularPages }} -Len RegularPages site: {{ len site.RegularPages }} - - -`, }, { path: filepath.Join("archetypes", "emptydate.md"), content: "+++\ndate =\"\"\ntitle = \"Empty Date Arch title\"\ntest = \"test1\"\n+++\n", }, - { - path: filepath.Join("archetypes", "lang.md"), - content: `Site Lang: {{ site.Language.Lang }}|Name: {{ replace .Name "-" " " | title }}|i18n: {{ T "hugo" }}`, - }, // #3623x { path: filepath.Join("archetypes", "shortcodes.md"), @@ -257,7 +150,7 @@ Some text. `, }, } { - f, err := fs.Create(v.path) + f, err := fs.Source.Create(v.path) if err != nil { return err } @@ -272,15 +165,8 @@ Some text. return nil } -func cContains(c *qt.C, v any, matches ...string) { - for _, m := range matches { - c.Assert(v, qt.Contains, m) - } -} - // TODO(bep) extract common testing package with this and some others -func readFileFromFs(t testing.TB, fs afero.Fs, filename string) string { - t.Helper() +func readFileFromFs(t *testing.T, fs afero.Fs, filename string) string { filename = filepath.FromSlash(filename) b, err := afero.ReadFile(fs, filename) if err != nil { @@ -298,48 +184,15 @@ func readFileFromFs(t testing.TB, fs afero.Fs, filename string) string { return string(b) } -func newTestCfg(c *qt.C, mm afero.Fs) (config.Provider, *hugofs.Fs) { - cfg := ` +func newTestCfg() (*viper.Viper, *hugofs.Fs) { -theme = "mytheme" -[languages] -[languages.en] -weight = 1 -languageName = "English" -[languages.nn] -weight = 2 -languageName = "Nynorsk" + v := viper.New() + fs := hugofs.NewMem(v) -[module] -[[module.mounts]] - source = 'archetypes' - target = 'archetypes' -[[module.mounts]] - source = 'content' - target = 'content' - lang = 'en' -[[module.mounts]] - source = 'content_nn' - target = 'content' - lang = 'nn' -` - if mm == nil { - mm = afero.NewMemMapFs() - } + v.SetFs(fs.Source) - mm.MkdirAll(filepath.FromSlash("content_nn"), 0o777) + initViper(v) - mm.MkdirAll(filepath.FromSlash("themes/mytheme"), 0o777) + return v, fs - c.Assert(afero.WriteFile(mm, filepath.Join("i18n", "en.toml"), []byte(`[hugo] -other = "Hugo Rocks!"`), 0o755), qt.IsNil) - c.Assert(afero.WriteFile(mm, filepath.Join("i18n", "nn.toml"), []byte(`[hugo] -other = "Hugo Rokkar!"`), 0o755), qt.IsNil) - - c.Assert(afero.WriteFile(mm, "config.toml", []byte(cfg), 0o755), qt.IsNil) - - res, err := allconfig.LoadConfig(allconfig.ConfigSourceDescriptor{Fs: mm, Filename: "config.toml"}) - c.Assert(err, qt.IsNil) - - return res.LoadingInfo.Cfg, hugofs.NewFrom(mm, res.LoadingInfo.BaseConfig) } diff --git a/create/skeletons/site/assets/.gitkeep b/create/skeletons/site/assets/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/create/skeletons/site/content/.gitkeep b/create/skeletons/site/content/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/create/skeletons/site/data/.gitkeep b/create/skeletons/site/data/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/create/skeletons/site/i18n/.gitkeep b/create/skeletons/site/i18n/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/create/skeletons/site/layouts/.gitkeep b/create/skeletons/site/layouts/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/create/skeletons/site/static/.gitkeep b/create/skeletons/site/static/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/create/skeletons/site/themes/.gitkeep b/create/skeletons/site/themes/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/create/skeletons/skeletons.go b/create/skeletons/skeletons.go deleted file mode 100644 index a6241ef92..000000000 --- a/create/skeletons/skeletons.go +++ /dev/null @@ -1,182 +0,0 @@ -// 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 deleted file mode 100644 index 166ade924..000000000 --- a/create/skeletons/theme/assets/css/main.css +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index e2aac5275..000000000 --- a/create/skeletons/theme/assets/js/main.js +++ /dev/null @@ -1 +0,0 @@ -console.log('This site was generated by Hugo.'); diff --git a/create/skeletons/theme/content/_index.md b/create/skeletons/theme/content/_index.md deleted file mode 100644 index 652623b57..000000000 --- a/create/skeletons/theme/content/_index.md +++ /dev/null @@ -1,9 +0,0 @@ -+++ -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 deleted file mode 100644 index e7066c092..000000000 --- a/create/skeletons/theme/content/posts/_index.md +++ /dev/null @@ -1,7 +0,0 @@ -+++ -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 deleted file mode 100644 index 3e3fc6b25..000000000 --- a/create/skeletons/theme/content/posts/post-1.md +++ /dev/null @@ -1,10 +0,0 @@ -+++ -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 deleted file mode 100644 index 22b828769..000000000 --- a/create/skeletons/theme/content/posts/post-2.md +++ /dev/null @@ -1,10 +0,0 @@ -+++ -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 deleted file mode 100644 index 9a923bea0..000000000 Binary files a/create/skeletons/theme/content/posts/post-3/bryce-canyon.jpg and /dev/null differ diff --git a/create/skeletons/theme/content/posts/post-3/index.md b/create/skeletons/theme/content/posts/post-3/index.md deleted file mode 100644 index ca42a664b..000000000 --- a/create/skeletons/theme/content/posts/post-3/index.md +++ /dev/null @@ -1,12 +0,0 @@ -+++ -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 deleted file mode 100644 index e69de29bb..000000000 diff --git a/create/skeletons/theme/i18n/.gitkeep b/create/skeletons/theme/i18n/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/create/skeletons/theme/layouts/_partials/footer.html b/create/skeletons/theme/layouts/_partials/footer.html deleted file mode 100644 index a7cd916d0..000000000 --- a/create/skeletons/theme/layouts/_partials/footer.html +++ /dev/null @@ -1 +0,0 @@ -

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

diff --git a/create/skeletons/theme/layouts/_partials/head.html b/create/skeletons/theme/layouts/_partials/head.html deleted file mode 100644 index 02c224018..000000000 --- a/create/skeletons/theme/layouts/_partials/head.html +++ /dev/null @@ -1,5 +0,0 @@ - - -{{ 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 deleted file mode 100644 index d76d23a16..000000000 --- a/create/skeletons/theme/layouts/_partials/head/css.html +++ /dev/null @@ -1,9 +0,0 @@ -{{- 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 deleted file mode 100644 index 16ffbedfe..000000000 --- a/create/skeletons/theme/layouts/_partials/head/js.html +++ /dev/null @@ -1,16 +0,0 @@ -{{- 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 deleted file mode 100644 index 7980a00e1..000000000 --- a/create/skeletons/theme/layouts/_partials/header.html +++ /dev/null @@ -1,2 +0,0 @@ -

{{ 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 deleted file mode 100644 index 14245b55d..000000000 --- a/create/skeletons/theme/layouts/_partials/menu.html +++ /dev/null @@ -1,51 +0,0 @@ -{{- /* -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 deleted file mode 100644 index 8a6ebec2a..000000000 --- a/create/skeletons/theme/layouts/_partials/terms.html +++ /dev/null @@ -1,23 +0,0 @@ -{{- /* -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 deleted file mode 100644 index 39dcbec61..000000000 --- a/create/skeletons/theme/layouts/baseof.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - {{ 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 deleted file mode 100644 index 0df659742..000000000 --- a/create/skeletons/theme/layouts/home.html +++ /dev/null @@ -1,7 +0,0 @@ -{{ 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 deleted file mode 100644 index 7e286c802..000000000 --- a/create/skeletons/theme/layouts/page.html +++ /dev/null @@ -1,10 +0,0 @@ -{{ 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 deleted file mode 100644 index 50fc92d40..000000000 --- a/create/skeletons/theme/layouts/section.html +++ /dev/null @@ -1,8 +0,0 @@ -{{ 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 deleted file mode 100644 index c2e787519..000000000 --- a/create/skeletons/theme/layouts/taxonomy.html +++ /dev/null @@ -1,7 +0,0 @@ -{{ 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 deleted file mode 100644 index c2e787519..000000000 --- a/create/skeletons/theme/layouts/term.html +++ /dev/null @@ -1,7 +0,0 @@ -{{ 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 deleted file mode 100644 index 67f8b7778..000000000 Binary files a/create/skeletons/theme/static/favicon.ico and /dev/null differ diff --git a/deploy/cloudfront.go b/deploy/cloudfront.go deleted file mode 100644 index 3202a73ea..000000000 --- a/deploy/cloudfront.go +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2019 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 deploy - -import ( - "context" - "net/url" - "time" - - "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. -// 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(target.CloudFrontDistributionID), - InvalidationBatch: &types.InvalidationBatch{ - CallerReference: aws.String(time.Now().Format("20060102150405")), - Paths: &types.Paths{ - Items: []string{"/*"}, - Quantity: aws.Int32(1), - }, - }, - } - _, err = cf.CreateInvalidation(ctx, req) - return err -} diff --git a/deploy/deploy.go b/deploy/deploy.go deleted file mode 100644 index 57e1f41a2..000000000 --- a/deploy/deploy.go +++ /dev/null @@ -1,763 +0,0 @@ -// Copyright 2019 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 deploy - -import ( - "bytes" - "compress/gzip" - "context" - "crypto/md5" - "encoding/hex" - "errors" - "fmt" - "io" - "mime" - "os" - "path/filepath" - "regexp" - "runtime" - "sort" - "strings" - "sync" - - "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" - "golang.org/x/text/unicode/norm" - - "gocloud.dev/blob" - _ "gocloud.dev/blob/fileblob" // import - _ "gocloud.dev/blob/gcsblob" // import - _ "gocloud.dev/blob/s3blob" // import - "gocloud.dev/gcerrors" -) - -// Deployer supports deploying the site to target cloud providers. -type Deployer struct { - localFs afero.Fs - bucket *blob.Bucket - - mediaTypes media.Types // Hugo's MediaType to guess ContentType - quiet bool // true reduces STDOUT // TODO(bep) remove, this is a global feature. - - cfg deployconfig.DeployConfig - logger loggers.Logger - - target *deployconfig.Target // the target to deploy to - - // For tests... - summary deploySummary // summary of latest Deploy results -} - -type deploySummary struct { - NumLocal, NumRemote, NumUploads, NumDeletes int -} - -const metaMD5Hash = "md5chksum" // the meta key to store md5hash in - -// New constructs a new *Deployer. -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 { - return nil, errors.New("no deployment targets found") - } - mediaTypes := cfg.GetConfigSection("mediaTypes").(media.Types) - - // Find the target to deploy to. - var tgt *deployconfig.Target - if targetName == "" { - // Default to the first target. - tgt = dcfg.Targets[0] - } else { - for _, t := range dcfg.Targets { - if t.Name == targetName { - tgt = t - } - } - if tgt == nil { - return nil, fmt.Errorf("deployment target %q not found", targetName) - } - } - - return &Deployer{ - localFs: localFs, - target: tgt, - quiet: cfg.BuildExpired(), - mediaTypes: mediaTypes, - cfg: dcfg, - }, nil -} - -func (d *Deployer) openBucket(ctx context.Context) (*blob.Bucket, error) { - if d.bucket != nil { - return d.bucket, nil - } - 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 - } - - if d.cfg.Workers <= 0 { - d.cfg.Workers = 10 - } - - // 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 - if d.target.StripIndexHTML { - mappath = stripIndexHTML - } - } - local, err := d.walkLocal(d.localFs, d.cfg.Matchers, include, exclude, d.mediaTypes, mappath) - if err != nil { - return err - } - d.logger.Infof("Found %d local files.\n", len(local)) - d.summary.NumLocal = len(local) - - // Load remote files from the target. - remote, err := d.walkRemote(ctx, bucket, include, exclude) - if err != nil { - return err - } - 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 := 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 { - d.logger.Println("No changes required.") - } - return nil - } - if !d.quiet { - d.logger.Println(summarizeChanges(uploads, deletes)) - } - - // Ask for confirmation before proceeding. - if d.cfg.Confirm && !d.cfg.DryRun { - fmt.Printf("Continue? (Y/n) ") - var confirm string - if _, err := fmt.Scanln(&confirm); err != nil { - return err - } - if confirm != "" && confirm[0] != 'y' && confirm[0] != 'Y' { - return errors.New("aborted") - } - } - - // 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) - - nParallel := d.cfg.Workers - var errs []error - var errMu sync.Mutex // protects errs - - for _, uploads := range uploadGroups { - // Short-circuit for an empty group. - if len(uploads) == 0 { - continue - } - - // Within the group, apply uploads in parallel. - sem := make(chan struct{}, nParallel) - for _, upload := range uploads { - if d.cfg.DryRun { - if !d.quiet { - d.logger.Printf("[DRY RUN] Would upload: %v\n", upload) - } - continue - } - - sem <- struct{}{} - go func(upload *fileToUpload) { - if err := d.doSingleUpload(ctx, bucket, upload); err != nil { - errMu.Lock() - defer errMu.Unlock() - errs = append(errs, err) - } - <-sem - }(upload) - } - // Wait for all uploads in the group to finish. - for n := nParallel; n > 0; n-- { - sem <- struct{}{} - } - } - - if d.cfg.MaxDeletes != -1 && 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. - sort.Slice(deletes, func(i, j int) bool { return deletes[i] < deletes[j] }) - sem := make(chan struct{}, nParallel) - for _, del := range deletes { - if d.cfg.DryRun { - if !d.quiet { - d.logger.Printf("[DRY RUN] Would delete %s\n", del) - } - continue - } - sem <- struct{}{} - go func(del string) { - d.logger.Infof("Deleting %s...\n", del) - if err := bucket.Delete(ctx, del); err != nil { - if gcerrors.Code(err) == gcerrors.NotFound { - d.logger.Warnf("Failed to delete %q because it wasn't found: %v", del, err) - } else { - errMu.Lock() - defer errMu.Unlock() - errs = append(errs, err) - } - } - <-sem - }(del) - } - // Wait for all deletes to finish. - for n := nParallel; n > 0; n-- { - sem <- struct{}{} - } - } - - if len(errs) > 0 { - if !d.quiet { - d.logger.Printf("Encountered %d errors.\n", len(errs)) - } - return errs[0] - } - if !d.quiet { - d.logger.Println("Success!") - } - - if d.cfg.InvalidateCDN { - if d.target.CloudFrontDistributionID != "" { - if d.cfg.DryRun { - if !d.quiet { - d.logger.Printf("[DRY RUN] Would invalidate CloudFront CDN with ID %s\n", d.target.CloudFrontDistributionID) - } - } else { - 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 - } - } - } - if d.target.GoogleCloudCDNOrigin != "" { - if d.cfg.DryRun { - if !d.quiet { - d.logger.Printf("[DRY RUN] Would invalidate Google Cloud CDN with origin %s\n", d.target.GoogleCloudCDNOrigin) - } - } else { - d.logger.Println("Invalidating Google Cloud CDN...") - if err := InvalidateGoogleCloudCDN(ctx, d.target.GoogleCloudCDNOrigin); err != nil { - d.logger.Printf("Failed to invalidate Google Cloud CDN: %v\n", err) - return err - } - } - } - d.logger.Println("Success!") - } - return nil -} - -// summarizeChanges creates a text description of the proposed changes. -func summarizeChanges(uploads []*fileToUpload, deletes []string) string { - uploadSize := int64(0) - for _, u := range uploads { - uploadSize += u.Local.UploadSize - } - return fmt.Sprintf("Identified %d file(s) to upload, totaling %s, and %d file(s) to delete.", len(uploads), humanize.Bytes(uint64(uploadSize)), len(deletes)) -} - -// doSingleUpload executes a single file 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(), - ContentType: upload.Local.ContentType(), - Metadata: map[string]string{metaMD5Hash: hex.EncodeToString(upload.Local.MD5())}, - } - w, err := bucket.NewWriter(ctx, upload.Local.SlashPath, opts) - if err != nil { - return err - } - r, err := upload.Local.Reader() - if err != nil { - return err - } - defer r.Close() - _, err = io.Copy(w, r) - if err != nil { - return err - } - if err := w.Close(); err != nil { - return err - } - return nil -} - -// localFile represents a local file from the source. Use newLocalFile to -// construct one. -type localFile struct { - // NativePath is the native path to the file (using file.Separator). - NativePath string - // SlashPath is NativePath converted to use /. - SlashPath string - // UploadSize is the size of the content to be uploaded. It may not - // be the same as the local file size if the content will be - // gzipped before upload. - UploadSize int64 - - fs afero.Fs - 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 *deployconfig.Matcher, mt media.Types) (*localFile, error) { - f, err := fs.Open(nativePath) - if err != nil { - return nil, err - } - defer f.Close() - lf := &localFile{ - NativePath: nativePath, - SlashPath: slashpath, - fs: fs, - matcher: m, - mediaTypes: mt, - } - if m != nil && m.Gzip { - // We're going to gzip the content. Do it once now, and cache the result - // in gzipped. The UploadSize is the size of the gzipped content. - gz := gzip.NewWriter(&lf.gzipped) - if _, err := io.Copy(gz, f); err != nil { - return nil, err - } - if err := gz.Close(); err != nil { - return nil, err - } - lf.UploadSize = int64(lf.gzipped.Len()) - } else { - // Raw content. Just get the UploadSize. - info, err := f.Stat() - if err != nil { - return nil, err - } - lf.UploadSize = info.Size() - } - return lf, nil -} - -// Reader returns an io.ReadCloser for reading the content to be uploaded. -// The caller must call Close on the returned ReaderCloser. -// The reader content may not be the same as the local file content due to -// gzipping. -func (lf *localFile) Reader() (io.ReadCloser, error) { - if lf.matcher != nil && lf.matcher.Gzip { - // We've got the gzipped contents cached in gzipped. - // Note: we can't use lf.gzipped directly as a Reader, since we it discards - // data after it is read, and we may read it more than once. - return io.NopCloser(bytes.NewReader(lf.gzipped.Bytes())), nil - } - // Not expected to fail since we did it successfully earlier in newLocalFile, - // but could happen due to changes in the underlying filesystem. - return lf.fs.Open(lf.NativePath) -} - -// CacheControl returns the Cache-Control header to use for lf, based on the -// first matching matcher (if any). -func (lf *localFile) CacheControl() string { - if lf.matcher == nil { - return "" - } - return lf.matcher.CacheControl -} - -// ContentEncoding returns the Content-Encoding header to use for lf, based -// on the matcher's Content-Encoding and Gzip fields. -func (lf *localFile) ContentEncoding() string { - if lf.matcher == nil { - return "" - } - if lf.matcher.Gzip { - return "gzip" - } - return lf.matcher.ContentEncoding -} - -// ContentType returns the Content-Type header to use for lf. -// It first checks if there's a Content-Type header configured via a matching -// matcher; if not, it tries to generate one based on the filename extension. -// If this fails, the Content-Type will be the empty string. In this case, Go -// Cloud will automatically try to infer a Content-Type based on the file -// content. -func (lf *localFile) ContentType() string { - if lf.matcher != nil && lf.matcher.ContentType != "" { - return lf.matcher.ContentType - } - - ext := filepath.Ext(lf.NativePath) - if mimeType, _, found := lf.mediaTypes.GetFirstBySuffix(strings.TrimPrefix(ext, ".")); found { - return mimeType.Type - } - - return mime.TypeByExtension(ext) -} - -// Force returns true if the file should be forced to re-upload based on the -// matching matcher. -func (lf *localFile) Force() bool { - return lf.matcher != nil && lf.matcher.Force -} - -// MD5 returns an MD5 hash of the content to be uploaded. -func (lf *localFile) MD5() []byte { - if len(lf.md5) > 0 { - return lf.md5 - } - h := md5.New() - r, err := lf.Reader() - if err != nil { - return nil - } - defer r.Close() - if _, err := io.Copy(h, r); err != nil { - return nil - } - lf.md5 = h.Sum(nil) - return lf.md5 -} - -// knownHiddenDirectory checks if the specified name is a well known -// hidden directory. -func knownHiddenDirectory(name string) bool { - knownDirectories := []string{ - ".well-known", - } - - for _, dir := range knownDirectories { - if name == dir { - return true - } - } - return false -} - -// walkLocal walks the source directory and returns a flat list of files, -// using localFile.SlashPath as the map keys. -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 - } - if info.IsDir() { - // Skip hidden directories. - if path != "" && strings.HasPrefix(info.Name(), ".") { - // Except for specific hidden directories - if !knownHiddenDirectory(info.Name()) { - return filepath.SkipDir - } - } - return nil - } - - // .DS_Store is an internal MacOS attribute file; skip it. - if info.Name() == ".DS_Store" { - return nil - } - - // 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) - } - - // 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 (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 { - obj, err := iter.Next(ctx) - if err == io.EOF { - break - } - if err != nil { - return nil, err - } - // Check include/exclude matchers. - if include != nil && !include.Match(obj.Key) { - d.logger.Infof(" remote dropping %q due to include\n", obj.Key) - continue - } - if exclude != nil && exclude.Match(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. - // This can happen for some providers (e.g., fileblob, which uses the - // local filesystem), but not for the most common Cloud providers - // (S3, GCS, Azure). Although, it can happen for S3 if the blob was uploaded - // via a multi-part upload. - // Although it's unfortunate to have to read the file, it's likely better - // than assuming a delta and re-uploading it. - if len(obj.MD5) == 0 { - var attrMD5 []byte - attrs, err := bucket.Attributes(ctx, obj.Key) - if err == nil { - md5String, exists := attrs.Metadata[metaMD5Hash] - if exists { - attrMD5, _ = hex.DecodeString(md5String) - } - } - if len(attrMD5) == 0 { - r, err := bucket.NewReader(ctx, obj.Key, nil) - if err == nil { - h := md5.New() - if _, err := io.Copy(h, r); err == nil { - obj.MD5 = h.Sum(nil) - } - r.Close() - } - } else { - obj.MD5 = attrMD5 - } - } - retval[obj.Key] = obj - } - return retval, nil -} - -// uploadReason is an enum of reasons why a file must be uploaded. -type uploadReason string - -const ( - reasonUnknown uploadReason = "unknown" - reasonNotFound uploadReason = "not found at target" - reasonForce uploadReason = "--force" - reasonSize uploadReason = "size differs" - reasonMD5Differs uploadReason = "md5 differs" - reasonMD5Missing uploadReason = "remote md5 missing" -) - -// fileToUpload represents a single local file that should be uploaded to -// the target. -type fileToUpload struct { - Local *localFile - Reason uploadReason -} - -func (u *fileToUpload) String() string { - details := []string{humanize.Bytes(uint64(u.Local.UploadSize))} - if s := u.Local.CacheControl(); s != "" { - details = append(details, fmt.Sprintf("Cache-Control: %q", s)) - } - if s := u.Local.ContentEncoding(); s != "" { - details = append(details, fmt.Sprintf("Content-Encoding: %q", s)) - } - if s := u.Local.ContentType(); s != "" { - details = append(details, fmt.Sprintf("Content-Type: %q", s)) - } - return fmt.Sprintf("%s (%s): %v", u.Local.SlashPath, strings.Join(details, ", "), u.Reason) -} - -// 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 (d *Deployer) findDiffs(localFiles map[string]*localFile, remoteFiles map[string]*blob.ListObject, force bool) ([]*fileToUpload, []string) { - var uploads []*fileToUpload - var deletes []string - - found := map[string]bool{} - for path, lf := range localFiles { - upload := false - reason := reasonUnknown - - if remoteFile, ok := remoteFiles[path]; ok { - // The file exists in remote. Let's see if we need to upload it anyway. - - // TODO: We don't register a diff if the metadata (e.g., Content-Type - // header) has changed. This would be difficult/expensive to detect; some - // providers return metadata along with their "List" result, but others - // (notably AWS S3) do not, so gocloud.dev's blob.Bucket doesn't expose - // it in the list result. It would require a separate request per blob - // to fetch. At least for now, we work around this by documenting it and - // providing a "force" flag (to re-upload everything) and a "force" bool - // per matcher (to re-upload all files in a matcher whose headers may have - // changed). - // Idea: extract a sample set of 1 file per extension + 1 file per matcher - // and check those files? - if force { - upload = true - reason = reasonForce - } else if lf.Force() { - upload = true - reason = reasonForce - } else if lf.UploadSize != remoteFile.Size { - upload = true - reason = reasonSize - } else if len(remoteFile.MD5) == 0 { - // This shouldn't happen unless the remote didn't give us an MD5 hash - // from List, AND we failed to compute one by reading the remote file. - // Default to considering the files different. - upload = true - reason = reasonMD5Missing - } else if !bytes.Equal(lf.MD5(), remoteFile.MD5) { - upload = true - reason = reasonMD5Differs - } - found[path] = true - } else { - // The file doesn't exist in remote. - upload = true - reason = reasonNotFound - } - if upload { - d.logger.Debugf("%s needs to be uploaded: %v\n", path, reason) - uploads = append(uploads, &fileToUpload{lf, reason}) - } else { - d.logger.Debugf("%s exists at target and does not need to be uploaded", path) - } - } - - // Remote files that weren't found locally should be deleted. - for path := range remoteFiles { - if !found[path] { - deletes = append(deletes, path) - } - } - return uploads, deletes -} - -// applyOrdering returns an ordered slice of slices of uploads. -// -// The returned slice will have length len(ordering)+1. -// -// The subslice at index i, for i = 0 ... len(ordering)-1, will have all of the -// uploads whose Local.SlashPath matched the regex at ordering[i] (but not any -// previous ordering regex). -// The subslice at index len(ordering) will have the remaining uploads that -// didn't match any ordering regex. -// -// The subslices are sorted by Local.SlashPath. -func applyOrdering(ordering []*regexp.Regexp, uploads []*fileToUpload) [][]*fileToUpload { - // Sort the whole slice by Local.SlashPath first. - sort.Slice(uploads, func(i, j int) bool { return uploads[i].Local.SlashPath < uploads[j].Local.SlashPath }) - - retval := make([][]*fileToUpload, len(ordering)+1) - for _, u := range uploads { - matched := false - for i, re := range ordering { - if re.MatchString(u.Local.SlashPath) { - retval[i] = append(retval[i], u) - matched = true - break - } - } - if !matched { - retval[len(ordering)] = append(retval[len(ordering)], u) - } - } - return retval -} diff --git a/deploy/deploy_azure.go b/deploy/deploy_azure.go deleted file mode 100644 index b1ce7358c..000000000 --- a/deploy/deploy_azure.go +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2019 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 !solaris && withdeploy - -package deploy - -import ( - _ "gocloud.dev/blob" - _ "gocloud.dev/blob/azureblob" // import -) diff --git a/deploy/deploy_test.go b/deploy/deploy_test.go deleted file mode 100644 index bdc8299a0..000000000 --- a/deploy/deploy_test.go +++ /dev/null @@ -1,1102 +0,0 @@ -// Copyright 2019 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 deploy - -import ( - "bytes" - "compress/gzip" - "context" - "crypto/md5" - "fmt" - "io" - "os" - "path" - "path/filepath" - "regexp" - "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" - "github.com/spf13/afero" - "gocloud.dev/blob" - "gocloud.dev/blob/fileblob" - "gocloud.dev/blob/memblob" -) - -func TestFindDiffs(t *testing.T) { - hash1 := []byte("hash 1") - hash2 := []byte("hash 2") - makeLocal := func(path string, size int64, hash []byte) *localFile { - return &localFile{NativePath: path, SlashPath: filepath.ToSlash(path), UploadSize: size, md5: hash} - } - makeRemote := func(path string, size int64, hash []byte) *blob.ListObject { - return &blob.ListObject{Key: path, Size: size, MD5: hash} - } - - tests := []struct { - Description string - Local []*localFile - Remote []*blob.ListObject - Force bool - WantUpdates []*fileToUpload - WantDeletes []string - }{ - { - Description: "empty -> no diffs", - }, - { - Description: "local == remote -> no diffs", - Local: []*localFile{ - makeLocal("aaa", 1, hash1), - makeLocal("bbb", 2, hash1), - makeLocal("ccc", 3, hash2), - }, - Remote: []*blob.ListObject{ - makeRemote("aaa", 1, hash1), - makeRemote("bbb", 2, hash1), - makeRemote("ccc", 3, hash2), - }, - }, - { - Description: "local w/ separators == remote -> no diffs", - Local: []*localFile{ - makeLocal(filepath.Join("aaa", "aaa"), 1, hash1), - makeLocal(filepath.Join("bbb", "bbb"), 2, hash1), - makeLocal(filepath.Join("ccc", "ccc"), 3, hash2), - }, - Remote: []*blob.ListObject{ - makeRemote("aaa/aaa", 1, hash1), - makeRemote("bbb/bbb", 2, hash1), - makeRemote("ccc/ccc", 3, hash2), - }, - }, - { - Description: "local == remote with force flag true -> diffs", - Local: []*localFile{ - makeLocal("aaa", 1, hash1), - makeLocal("bbb", 2, hash1), - makeLocal("ccc", 3, hash2), - }, - Remote: []*blob.ListObject{ - makeRemote("aaa", 1, hash1), - makeRemote("bbb", 2, hash1), - makeRemote("ccc", 3, hash2), - }, - Force: true, - WantUpdates: []*fileToUpload{ - {makeLocal("aaa", 1, nil), reasonForce}, - {makeLocal("bbb", 2, nil), reasonForce}, - {makeLocal("ccc", 3, nil), reasonForce}, - }, - }, - { - Description: "local == remote with route.Force true -> diffs", - Local: []*localFile{ - {NativePath: "aaa", SlashPath: "aaa", UploadSize: 1, matcher: &deployconfig.Matcher{Force: true}, md5: hash1}, - makeLocal("bbb", 2, hash1), - }, - Remote: []*blob.ListObject{ - makeRemote("aaa", 1, hash1), - makeRemote("bbb", 2, hash1), - }, - WantUpdates: []*fileToUpload{ - {makeLocal("aaa", 1, nil), reasonForce}, - }, - }, - { - Description: "extra local file -> upload", - Local: []*localFile{ - makeLocal("aaa", 1, hash1), - makeLocal("bbb", 2, hash2), - }, - Remote: []*blob.ListObject{ - makeRemote("aaa", 1, hash1), - }, - WantUpdates: []*fileToUpload{ - {makeLocal("bbb", 2, nil), reasonNotFound}, - }, - }, - { - Description: "extra remote file -> delete", - Local: []*localFile{ - makeLocal("aaa", 1, hash1), - }, - Remote: []*blob.ListObject{ - makeRemote("aaa", 1, hash1), - makeRemote("bbb", 2, hash2), - }, - WantDeletes: []string{"bbb"}, - }, - { - Description: "diffs in size or md5 -> upload", - Local: []*localFile{ - makeLocal("aaa", 1, hash1), - makeLocal("bbb", 2, hash1), - makeLocal("ccc", 1, hash2), - }, - Remote: []*blob.ListObject{ - makeRemote("aaa", 1, nil), - makeRemote("bbb", 1, hash1), - makeRemote("ccc", 1, hash1), - }, - WantUpdates: []*fileToUpload{ - {makeLocal("aaa", 1, nil), reasonMD5Missing}, - {makeLocal("bbb", 2, nil), reasonSize}, - {makeLocal("ccc", 1, nil), reasonMD5Differs}, - }, - }, - { - Description: "mix of updates and deletes", - Local: []*localFile{ - makeLocal("same", 1, hash1), - makeLocal("updated", 2, hash1), - makeLocal("updated2", 1, hash2), - makeLocal("new", 1, hash1), - makeLocal("new2", 2, hash2), - }, - Remote: []*blob.ListObject{ - makeRemote("same", 1, hash1), - makeRemote("updated", 1, hash1), - makeRemote("updated2", 1, hash1), - makeRemote("stale", 1, hash1), - makeRemote("stale2", 1, hash1), - }, - WantUpdates: []*fileToUpload{ - {makeLocal("new", 1, nil), reasonNotFound}, - {makeLocal("new2", 2, nil), reasonNotFound}, - {makeLocal("updated", 2, nil), reasonSize}, - {makeLocal("updated2", 1, nil), reasonMD5Differs}, - }, - WantDeletes: []string{"stale", "stale2"}, - }, - } - - for _, tc := range tests { - t.Run(tc.Description, func(t *testing.T) { - local := map[string]*localFile{} - for _, l := range tc.Local { - local[l.SlashPath] = l - } - remote := map[string]*blob.ListObject{} - for _, r := range tc.Remote { - remote[r.Key] = r - } - 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 != "" { - t.Errorf("updates differ:\n%s", diff) - } - if diff := cmp.Diff(gotDeletes, tc.WantDeletes); diff != "" { - t.Errorf("deletes differ:\n%s", diff) - } - }) - } -} - -func TestWalkLocal(t *testing.T) { - tests := map[string]struct { - Given []string - Expect []string - MapPath func(string) string - }{ - "Empty": { - Given: []string{}, - Expect: []string{}, - }, - "Normal": { - Given: []string{"file.txt", "normal_dir/file.txt"}, - Expect: []string{"file.txt", "normal_dir/file.txt"}, - }, - "Hidden": { - Given: []string{"file.txt", ".hidden_dir/file.txt", "normal_dir/file.txt"}, - Expect: []string{"file.txt", "normal_dir/file.txt"}, - }, - "Well Known": { - 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 { - t.Run(desc, func(t *testing.T) { - fs := afero.NewMemMapFs() - for _, name := range tc.Given { - dir, _ := path.Split(name) - if dir != "" { - if err := fs.MkdirAll(dir, 0o755); err != nil { - t.Fatal(err) - } - } - if fd, err := fs.Create(name); err != nil { - t.Fatal(err) - } else { - fd.Close() - } - } - 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{} - for _, path := range tc.Expect { - if _, ok := got[path]; !ok { - t.Errorf("expected %q in results, but was not found", path) - } - expect[path] = nil - } - for path := range got { - if _, ok := expect[path]; !ok { - t.Errorf("got %q in results unexpectedly", path) - } - } - } - }) - } -} - -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!" - ) - contentBytes := []byte(content) - contentLen := int64(len(contentBytes)) - contentMD5 := md5.Sum(contentBytes) - var buf bytes.Buffer - gz := gzip.NewWriter(&buf) - if _, err := gz.Write(contentBytes); err != nil { - t.Fatal(err) - } - gz.Close() - gzBytes := buf.Bytes() - gzLen := int64(len(gzBytes)) - gzMD5 := md5.Sum(gzBytes) - - tests := []struct { - Description string - Path string - Matcher *deployconfig.Matcher - MediaTypesConfig map[string]any - WantContent []byte - WantSize int64 - WantMD5 []byte - WantContentType string // empty string is always OK, since content type detection is OS-specific - WantCacheControl string - WantContentEncoding string - }{ - { - Description: "file with no suffix", - Path: "foo", - WantContent: contentBytes, - WantSize: contentLen, - WantMD5: contentMD5[:], - }, - { - Description: "file with .txt suffix", - Path: "foo.txt", - WantContent: contentBytes, - WantSize: contentLen, - WantMD5: contentMD5[:], - }, - { - Description: "CacheControl from matcher", - Path: "foo.txt", - Matcher: &deployconfig.Matcher{CacheControl: "max-age=630720000"}, - WantContent: contentBytes, - WantSize: contentLen, - WantMD5: contentMD5[:], - WantCacheControl: "max-age=630720000", - }, - { - Description: "ContentEncoding from matcher", - Path: "foo.txt", - Matcher: &deployconfig.Matcher{ContentEncoding: "foobar"}, - WantContent: contentBytes, - WantSize: contentLen, - WantMD5: contentMD5[:], - WantContentEncoding: "foobar", - }, - { - Description: "ContentType from matcher", - Path: "foo.txt", - Matcher: &deployconfig.Matcher{ContentType: "foo/bar"}, - WantContent: contentBytes, - WantSize: contentLen, - WantMD5: contentMD5[:], - WantContentType: "foo/bar", - }, - { - Description: "gzipped content", - Path: "foo.txt", - Matcher: &deployconfig.Matcher{Gzip: true}, - WantContent: gzBytes, - WantSize: gzLen, - WantMD5: gzMD5[:], - WantContentEncoding: "gzip", - }, - { - Description: "Custom MediaType", - Path: "foo.hugo", - MediaTypesConfig: map[string]any{ - "hugo/custom": map[string]any{ - "suffixes": []string{"hugo"}, - }, - }, - WantContent: contentBytes, - WantSize: contentLen, - WantMD5: contentMD5[:], - WantContentType: "hugo/custom", - }, - } - - for _, tc := range tests { - t.Run(tc.Description, func(t *testing.T) { - fs := new(afero.MemMapFs) - if err := afero.WriteFile(fs, tc.Path, []byte(content), os.ModePerm); err != nil { - t.Fatal(err) - } - mediaTypes := media.DefaultTypes - if len(tc.MediaTypesConfig) > 0 { - mt, err := media.DecodeTypes(tc.MediaTypesConfig) - if err != nil { - t.Fatal(err) - } - mediaTypes = mt.Config - } - lf, err := newLocalFile(fs, tc.Path, filepath.ToSlash(tc.Path), tc.Matcher, mediaTypes) - if err != nil { - t.Fatal(err) - } - if got := lf.UploadSize; got != tc.WantSize { - t.Errorf("got size %d want %d", got, tc.WantSize) - } - if got := lf.MD5(); !bytes.Equal(got, tc.WantMD5) { - t.Errorf("got MD5 %x want %x", got, tc.WantMD5) - } - if got := lf.CacheControl(); got != tc.WantCacheControl { - t.Errorf("got CacheControl %q want %q", got, tc.WantCacheControl) - } - if got := lf.ContentEncoding(); got != tc.WantContentEncoding { - t.Errorf("got ContentEncoding %q want %q", got, tc.WantContentEncoding) - } - if tc.WantContentType != "" { - if got := lf.ContentType(); got != tc.WantContentType { - t.Errorf("got ContentType %q want %q", got, tc.WantContentType) - } - } - // Verify the reader last to ensure the previous operations don't - // interfere with it. - r, err := lf.Reader() - if err != nil { - t.Fatal(err) - } - gotContent, err := io.ReadAll(r) - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(gotContent, tc.WantContent) { - t.Errorf("got content %q want %q", string(gotContent), string(tc.WantContent)) - } - r.Close() - // Verify we can read again. - r, err = lf.Reader() - if err != nil { - t.Fatal(err) - } - gotContent, err = io.ReadAll(r) - if err != nil { - t.Fatal(err) - } - r.Close() - if !bytes.Equal(gotContent, tc.WantContent) { - t.Errorf("got content %q want %q", string(gotContent), string(tc.WantContent)) - } - }) - } -} - -func TestOrdering(t *testing.T) { - tests := []struct { - Description string - Uploads []string - Ordering []*regexp.Regexp - Want [][]string - }{ - { - Description: "empty", - Want: [][]string{nil}, - }, - { - Description: "no ordering", - Uploads: []string{"c", "b", "a", "d"}, - Want: [][]string{{"a", "b", "c", "d"}}, - }, - { - Description: "one ordering", - Uploads: []string{"db", "c", "b", "a", "da"}, - Ordering: []*regexp.Regexp{regexp.MustCompile("^d")}, - Want: [][]string{{"da", "db"}, {"a", "b", "c"}}, - }, - { - Description: "two orderings", - Uploads: []string{"db", "c", "b", "a", "da"}, - Ordering: []*regexp.Regexp{ - regexp.MustCompile("^d"), - regexp.MustCompile("^b"), - }, - Want: [][]string{{"da", "db"}, {"b"}, {"a", "c"}}, - }, - } - - for _, tc := range tests { - t.Run(tc.Description, func(t *testing.T) { - uploads := make([]*fileToUpload, len(tc.Uploads)) - for i, u := range tc.Uploads { - uploads[i] = &fileToUpload{Local: &localFile{SlashPath: u}} - } - gotUploads := applyOrdering(tc.Ordering, uploads) - var got [][]string - for _, subslice := range gotUploads { - var gotsubslice []string - for _, u := range subslice { - gotsubslice = append(gotsubslice, u.Local.SlashPath) - } - got = append(got, gotsubslice) - } - if diff := cmp.Diff(got, tc.Want); diff != "" { - t.Error(diff) - } - }) - } -} - -type fileData struct { - Name string // name of the file - Contents string // contents of the file -} - -// initLocalFs initializes fs with some test files. -func initLocalFs(ctx context.Context, fs afero.Fs) ([]*fileData, error) { - // The initial local filesystem. - local := []*fileData{ - {"aaa", "aaa"}, - {"bbb", "bbb"}, - {"subdir/aaa", "subdir-aaa"}, - {"subdir/nested/aaa", "subdir-nested-aaa"}, - {"subdir2/bbb", "subdir2-bbb"}, - } - if err := writeFiles(fs, local); err != nil { - return nil, err - } - return local, nil -} - -// fsTest represents an (afero.FS, Go CDK blob.Bucket) against which end-to-end -// tests can be run. -type fsTest struct { - name string - fs afero.Fs - bucket *blob.Bucket -} - -// initFsTests initializes a pair of tests for end-to-end test: -// 1. An in-memory afero.Fs paired with an in-memory Go CDK bucket. -// 2. A filesystem-based afero.Fs paired with an filesystem-based Go CDK bucket. -// It returns the pair of tests and a cleanup function. -func initFsTests(t *testing.T) []*fsTest { - t.Helper() - - tmpfsdir := t.TempDir() - tmpbucketdir := t.TempDir() - - memfs := afero.NewMemMapFs() - membucket := memblob.OpenBucket(nil) - t.Cleanup(func() { membucket.Close() }) - - filefs := hugofs.NewBasePathFs(afero.NewOsFs(), tmpfsdir) - filebucket, err := fileblob.OpenBucket(tmpbucketdir, nil) - if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { filebucket.Close() }) - - tests := []*fsTest{ - {"mem", memfs, membucket}, - {"file", filefs, filebucket}, - } - return tests -} - -// TestEndToEndSync verifies that basic adds, updates, and deletes are working -// correctly. -func TestEndToEndSync(t *testing.T) { - ctx := context.Background() - tests := initFsTests(t) - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - local, err := initLocalFs(ctx, test.fs) - if err != nil { - t.Fatal(err) - } - deployer := &Deployer{ - localFs: test.fs, - bucket: test.bucket, - mediaTypes: media.DefaultTypes, - cfg: deployconfig.DeployConfig{Workers: 2, MaxDeletes: -1}, - } - - // Initial deployment should sync remote with local. - if err := deployer.Deploy(ctx); err != nil { - t.Errorf("initial deploy: failed: %v", err) - } - wantSummary := deploySummary{NumLocal: 5, NumRemote: 0, NumUploads: 5, NumDeletes: 0} - if !cmp.Equal(deployer.summary, wantSummary) { - t.Errorf("initial deploy: got %v, want %v", deployer.summary, wantSummary) - } - if diff, err := verifyRemote(ctx, deployer.bucket, local); err != nil { - t.Errorf("initial deploy: failed to verify remote: %v", err) - } else if diff != "" { - t.Errorf("initial deploy: remote snapshot doesn't match expected:\n%v", diff) - } - - // A repeat deployment shouldn't change anything. - if err := deployer.Deploy(ctx); err != nil { - t.Errorf("no-op deploy: %v", err) - } - wantSummary = deploySummary{NumLocal: 5, NumRemote: 5, NumUploads: 0, NumDeletes: 0} - if !cmp.Equal(deployer.summary, wantSummary) { - t.Errorf("no-op deploy: got %v, want %v", deployer.summary, wantSummary) - } - - // Make some changes to the local filesystem: - // 1. Modify file [0]. - // 2. Delete file [1]. - // 3. Add a new file (sorted last). - updatefd := local[0] - updatefd.Contents = "new contents" - deletefd := local[1] - local = append(local[:1], local[2:]...) // removing deleted [1] - newfd := &fileData{"zzz", "zzz"} - local = append(local, newfd) - if err := writeFiles(test.fs, []*fileData{updatefd, newfd}); err != nil { - t.Fatal(err) - } - if err := test.fs.Remove(deletefd.Name); err != nil { - t.Fatal(err) - } - - // A deployment should apply those 3 changes. - if err := deployer.Deploy(ctx); err != nil { - t.Errorf("deploy after changes: failed: %v", err) - } - wantSummary = deploySummary{NumLocal: 5, NumRemote: 5, NumUploads: 2, NumDeletes: 1} - if !cmp.Equal(deployer.summary, wantSummary) { - t.Errorf("deploy after changes: got %v, want %v", deployer.summary, wantSummary) - } - if diff, err := verifyRemote(ctx, deployer.bucket, local); err != nil { - t.Errorf("deploy after changes: failed to verify remote: %v", err) - } else if diff != "" { - t.Errorf("deploy after changes: remote snapshot doesn't match expected:\n%v", diff) - } - - // Again, a repeat deployment shouldn't change anything. - if err := deployer.Deploy(ctx); err != nil { - t.Errorf("no-op deploy: %v", err) - } - wantSummary = deploySummary{NumLocal: 5, NumRemote: 5, NumUploads: 0, NumDeletes: 0} - if !cmp.Equal(deployer.summary, wantSummary) { - t.Errorf("no-op deploy: got %v, want %v", deployer.summary, wantSummary) - } - }) - } -} - -// TestMaxDeletes verifies that the "maxDeletes" flag is working correctly. -func TestMaxDeletes(t *testing.T) { - ctx := context.Background() - tests := initFsTests(t) - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - local, err := initLocalFs(ctx, test.fs) - if err != nil { - t.Fatal(err) - } - deployer := &Deployer{ - localFs: test.fs, - bucket: test.bucket, - mediaTypes: media.DefaultTypes, - cfg: deployconfig.DeployConfig{Workers: 2, MaxDeletes: -1}, - } - - // Sync remote with local. - if err := deployer.Deploy(ctx); err != nil { - t.Errorf("initial deploy: failed: %v", err) - } - wantSummary := deploySummary{NumLocal: 5, NumRemote: 0, NumUploads: 5, NumDeletes: 0} - if !cmp.Equal(deployer.summary, wantSummary) { - t.Errorf("initial deploy: got %v, want %v", deployer.summary, wantSummary) - } - - // Delete two files, [1] and [2]. - if err := test.fs.Remove(local[1].Name); err != nil { - t.Fatal(err) - } - if err := test.fs.Remove(local[2].Name); err != nil { - t.Fatal(err) - } - - // A deployment with maxDeletes=0 shouldn't change anything. - deployer.cfg.MaxDeletes = 0 - if err := deployer.Deploy(ctx); err != nil { - t.Errorf("deploy failed: %v", err) - } - wantSummary = deploySummary{NumLocal: 3, NumRemote: 5, NumUploads: 0, NumDeletes: 0} - if !cmp.Equal(deployer.summary, wantSummary) { - t.Errorf("deploy: got %v, want %v", deployer.summary, wantSummary) - } - - // A deployment with maxDeletes=1 shouldn't change anything either. - deployer.cfg.MaxDeletes = 1 - if err := deployer.Deploy(ctx); err != nil { - t.Errorf("deploy failed: %v", err) - } - wantSummary = deploySummary{NumLocal: 3, NumRemote: 5, NumUploads: 0, NumDeletes: 0} - if !cmp.Equal(deployer.summary, wantSummary) { - t.Errorf("deploy: got %v, want %v", deployer.summary, wantSummary) - } - - // A deployment with maxDeletes=2 should make the changes. - deployer.cfg.MaxDeletes = 2 - if err := deployer.Deploy(ctx); err != nil { - t.Errorf("deploy failed: %v", err) - } - wantSummary = deploySummary{NumLocal: 3, NumRemote: 5, NumUploads: 0, NumDeletes: 2} - if !cmp.Equal(deployer.summary, wantSummary) { - t.Errorf("deploy: got %v, want %v", deployer.summary, wantSummary) - } - - // Delete two more files, [0] and [3]. - if err := test.fs.Remove(local[0].Name); err != nil { - t.Fatal(err) - } - if err := test.fs.Remove(local[3].Name); err != nil { - t.Fatal(err) - } - - // A deployment with maxDeletes=-1 should make the changes. - deployer.cfg.MaxDeletes = -1 - if err := deployer.Deploy(ctx); err != nil { - t.Errorf("deploy failed: %v", err) - } - wantSummary = deploySummary{NumLocal: 1, NumRemote: 3, NumUploads: 0, NumDeletes: 2} - if !cmp.Equal(deployer.summary, wantSummary) { - t.Errorf("deploy: got %v, want %v", deployer.summary, wantSummary) - } - }) - } -} - -// TestIncludeExclude verifies that the include/exclude options for targets work. -func TestIncludeExclude(t *testing.T) { - ctx := context.Background() - tests := []struct { - Include string - Exclude string - Want deploySummary - }{ - { - Want: deploySummary{NumLocal: 5, NumUploads: 5}, - }, - { - Include: "**aaa", - Want: deploySummary{NumLocal: 3, NumUploads: 3}, - }, - { - Include: "**bbb", - Want: deploySummary{NumLocal: 2, NumUploads: 2}, - }, - { - Include: "aaa", - Want: deploySummary{NumLocal: 1, NumUploads: 1}, - }, - { - Exclude: "**aaa", - Want: deploySummary{NumLocal: 2, NumUploads: 2}, - }, - { - Exclude: "**bbb", - Want: deploySummary{NumLocal: 3, NumUploads: 3}, - }, - { - Exclude: "aaa", - Want: deploySummary{NumLocal: 4, NumUploads: 4}, - }, - { - Include: "**aaa", - Exclude: "**nested**", - Want: deploySummary{NumLocal: 2, NumUploads: 2}, - }, - } - for _, test := range tests { - t.Run(fmt.Sprintf("include %q exclude %q", test.Include, test.Exclude), func(t *testing.T) { - fsTests := initFsTests(t) - fsTest := fsTests[1] // just do file-based test - - _, err := initLocalFs(ctx, fsTest.fs) - if err != nil { - t.Fatal(err) - } - tgt := &deployconfig.Target{ - Include: test.Include, - Exclude: test.Exclude, - } - if err := tgt.ParseIncludeExclude(); err != nil { - t.Error(err) - } - deployer := &Deployer{ - localFs: fsTest.fs, - cfg: deployconfig.DeployConfig{Workers: 2, MaxDeletes: -1}, bucket: fsTest.bucket, - target: tgt, - mediaTypes: media.DefaultTypes, - } - - // Sync remote with local. - if err := deployer.Deploy(ctx); err != nil { - t.Errorf("deploy: failed: %v", err) - } - if !cmp.Equal(deployer.summary, test.Want) { - t.Errorf("deploy: got %v, want %v", deployer.summary, test.Want) - } - }) - } -} - -// TestIncludeExcludeRemoteDelete verifies deleted local files that don't match include/exclude patterns -// are not deleted on the remote. -func TestIncludeExcludeRemoteDelete(t *testing.T) { - ctx := context.Background() - - tests := []struct { - Include string - Exclude string - Want deploySummary - }{ - { - Want: deploySummary{NumLocal: 3, NumRemote: 5, NumUploads: 0, NumDeletes: 2}, - }, - { - Include: "**aaa", - Want: deploySummary{NumLocal: 2, NumRemote: 3, NumUploads: 0, NumDeletes: 1}, - }, - { - Include: "subdir/**", - Want: deploySummary{NumLocal: 1, NumRemote: 2, NumUploads: 0, NumDeletes: 1}, - }, - { - Exclude: "**bbb", - Want: deploySummary{NumLocal: 2, NumRemote: 3, NumUploads: 0, NumDeletes: 1}, - }, - { - Exclude: "bbb", - Want: deploySummary{NumLocal: 3, NumRemote: 4, NumUploads: 0, NumDeletes: 1}, - }, - } - for _, test := range tests { - t.Run(fmt.Sprintf("include %q exclude %q", test.Include, test.Exclude), func(t *testing.T) { - fsTests := initFsTests(t) - fsTest := fsTests[1] // just do file-based test - - local, err := initLocalFs(ctx, fsTest.fs) - if err != nil { - t.Fatal(err) - } - deployer := &Deployer{ - localFs: fsTest.fs, - cfg: deployconfig.DeployConfig{Workers: 2, MaxDeletes: -1}, bucket: fsTest.bucket, - mediaTypes: media.DefaultTypes, - } - - // Initial sync to get the files on the remote - if err := deployer.Deploy(ctx); err != nil { - t.Errorf("deploy: failed: %v", err) - } - - // Delete two files, [1] and [2]. - if err := fsTest.fs.Remove(local[1].Name); err != nil { - t.Fatal(err) - } - if err := fsTest.fs.Remove(local[2].Name); err != nil { - t.Fatal(err) - } - - // Second sync - tgt := &deployconfig.Target{ - Include: test.Include, - Exclude: test.Exclude, - } - if err := tgt.ParseIncludeExclude(); err != nil { - t.Error(err) - } - deployer.target = tgt - if err := deployer.Deploy(ctx); err != nil { - t.Errorf("deploy: failed: %v", err) - } - - if !cmp.Equal(deployer.summary, test.Want) { - t.Errorf("deploy: got %v, want %v", deployer.summary, test.Want) - } - }) - } -} - -// TestCompression verifies that gzip compression works correctly. -// In particular, MD5 hashes must be of the compressed content. -func TestCompression(t *testing.T) { - ctx := context.Background() - - tests := initFsTests(t) - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - local, err := initLocalFs(ctx, test.fs) - if err != nil { - t.Fatal(err) - } - deployer := &Deployer{ - localFs: test.fs, - bucket: test.bucket, - cfg: deployconfig.DeployConfig{Workers: 2, MaxDeletes: -1, Matchers: []*deployconfig.Matcher{{Pattern: ".*", Gzip: true, Re: regexp.MustCompile(".*")}}}, - mediaTypes: media.DefaultTypes, - } - - // Initial deployment should sync remote with local. - if err := deployer.Deploy(ctx); err != nil { - t.Errorf("initial deploy: failed: %v", err) - } - wantSummary := deploySummary{NumLocal: 5, NumRemote: 0, NumUploads: 5, NumDeletes: 0} - if !cmp.Equal(deployer.summary, wantSummary) { - t.Errorf("initial deploy: got %v, want %v", deployer.summary, wantSummary) - } - - // A repeat deployment shouldn't change anything. - if err := deployer.Deploy(ctx); err != nil { - t.Errorf("no-op deploy: %v", err) - } - wantSummary = deploySummary{NumLocal: 5, NumRemote: 5, NumUploads: 0, NumDeletes: 0} - if !cmp.Equal(deployer.summary, wantSummary) { - t.Errorf("no-op deploy: got %v, want %v", deployer.summary, wantSummary) - } - - // Make an update to the local filesystem, on [1]. - updatefd := local[1] - updatefd.Contents = "new contents" - if err := writeFiles(test.fs, []*fileData{updatefd}); err != nil { - t.Fatal(err) - } - - // A deployment should apply the changes. - if err := deployer.Deploy(ctx); err != nil { - t.Errorf("deploy after changes: failed: %v", err) - } - wantSummary = deploySummary{NumLocal: 5, NumRemote: 5, NumUploads: 1, NumDeletes: 0} - if !cmp.Equal(deployer.summary, wantSummary) { - t.Errorf("deploy after changes: got %v, want %v", deployer.summary, wantSummary) - } - }) - } -} - -// TestMatching verifies that matchers match correctly, and that the Force -// attribute for matcher works. -func TestMatching(t *testing.T) { - ctx := context.Background() - tests := initFsTests(t) - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - _, err := initLocalFs(ctx, test.fs) - if err != nil { - t.Fatal(err) - } - deployer := &Deployer{ - localFs: test.fs, - bucket: test.bucket, - cfg: deployconfig.DeployConfig{Workers: 2, MaxDeletes: -1, Matchers: []*deployconfig.Matcher{{Pattern: "^subdir/aaa$", Force: true, Re: regexp.MustCompile("^subdir/aaa$")}}}, - mediaTypes: media.DefaultTypes, - } - - // Initial deployment to sync remote with local. - if err := deployer.Deploy(ctx); err != nil { - t.Errorf("initial deploy: failed: %v", err) - } - wantSummary := deploySummary{NumLocal: 5, NumRemote: 0, NumUploads: 5, NumDeletes: 0} - if !cmp.Equal(deployer.summary, wantSummary) { - t.Errorf("initial deploy: got %v, want %v", deployer.summary, wantSummary) - } - - // A repeat deployment should upload a single file, the one that matched the Force matcher. - // Note that matching happens based on the ToSlash form, so this matches - // even on Windows. - if err := deployer.Deploy(ctx); err != nil { - t.Errorf("no-op deploy with single force matcher: %v", err) - } - wantSummary = deploySummary{NumLocal: 5, NumRemote: 5, NumUploads: 1, NumDeletes: 0} - if !cmp.Equal(deployer.summary, wantSummary) { - t.Errorf("no-op deploy with single force matcher: got %v, want %v", deployer.summary, wantSummary) - } - - // Repeat with a matcher that should now match 3 files. - 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) - } - wantSummary = deploySummary{NumLocal: 5, NumRemote: 5, NumUploads: 3, NumDeletes: 0} - if !cmp.Equal(deployer.summary, wantSummary) { - t.Errorf("no-op deploy with triple force matcher: got %v, want %v", deployer.summary, wantSummary) - } - }) - } -} - -// writeFiles writes the files in fds to fd. -func writeFiles(fs afero.Fs, fds []*fileData) error { - for _, fd := range fds { - dir := path.Dir(fd.Name) - if dir != "." { - err := fs.MkdirAll(dir, os.ModePerm) - if err != nil { - return err - } - } - f, err := fs.Create(fd.Name) - if err != nil { - return err - } - defer f.Close() - _, err = f.WriteString(fd.Contents) - if err != nil { - return err - } - } - return nil -} - -// verifyRemote that the current contents of bucket matches local. -// It returns an empty string if the contents matched, and a non-empty string -// capturing the diff if they didn't. -func verifyRemote(ctx context.Context, bucket *blob.Bucket, local []*fileData) (string, error) { - var cur []*fileData - iter := bucket.List(nil) - for { - obj, err := iter.Next(ctx) - if err == io.EOF { - break - } - if err != nil { - return "", err - } - contents, err := bucket.ReadAll(ctx, obj.Key) - if err != nil { - return "", err - } - cur = append(cur, &fileData{obj.Key, string(contents)}) - } - if cmp.Equal(cur, local) { - return "", nil - } - diff := "got: \n" - for _, f := range cur { - diff += fmt.Sprintf(" %s: %s\n", f.Name, f.Contents) - } - diff += "want: \n" - for _, f := range local { - diff += fmt.Sprintf(" %s: %s\n", f.Name, f.Contents) - } - return diff, nil -} - -func newDeployer() *Deployer { - return &Deployer{ - logger: loggers.NewDefault(), - cfg: deployconfig.DeployConfig{Workers: 2}, - } -} diff --git a/deploy/deployconfig/deployConfig.go b/deploy/deployconfig/deployConfig.go deleted file mode 100644 index b16b7c627..000000000 --- a/deploy/deployconfig/deployConfig.go +++ /dev/null @@ -1,179 +0,0 @@ -// 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 deployconfig - -import ( - "errors" - "fmt" - "regexp" - - "github.com/gobwas/glob" - "github.com/gohugoio/hugo/config" - hglob "github.com/gohugoio/hugo/hugofs/glob" - "github.com/mitchellh/mapstructure" -) - -const DeploymentConfigKey = "deployment" - -// DeployConfig is the complete configuration for deployment. -type DeployConfig struct { - Targets []*Target - Matchers []*Matcher - Order []string - - // Usually set via flags. - // Target deployment Name; defaults to the first one. - Target string - // Show a confirm prompt before deploying. - Confirm bool - // DryRun will try the deployment without any remote changes. - DryRun bool - // Force will re-upload all files. - Force bool - // Invalidate the CDN cache listed in the deployment target. - InvalidateCDN bool - // MaxDeletes is the maximum number of files to delete. - MaxDeletes int - // Number of concurrent workers to use when uploading files. - Workers int - - Ordering []*regexp.Regexp `json:"-"` // compiled Order -} - -type Target struct { - Name string - URL string - - CloudFrontDistributionID string - - // GoogleCloudCDNOrigin specifies the Google Cloud project and CDN origin to - // invalidate when deploying this target. It is specified as /. - GoogleCloudCDNOrigin string - - // Optional patterns of files to include/exclude for this target. - // Parsed using github.com/gobwas/glob. - Include string - Exclude string - - // Parsed versions of Include/Exclude. - 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 { - var err error - if 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) - if err != nil { - return fmt.Errorf("invalid deployment.target.exclude %q: %v", tgt.Exclude, err) - } - } - return nil -} - -// Matcher represents configuration to be applied to files whose paths match -// a specified pattern. -type Matcher struct { - // Pattern is the string pattern to match against paths. - // Matching is done against paths converted to use / as the path separator. - Pattern string - - // CacheControl specifies caching attributes to use when serving the blob. - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control - CacheControl string - - // ContentEncoding specifies the encoding used for the blob's content, if any. - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding - ContentEncoding string - - // ContentType specifies the MIME type of the blob being written. - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type - ContentType string - - // Gzip determines whether the file should be gzipped before upload. - // If so, the ContentEncoding field will automatically be set to "gzip". - Gzip bool - - // Force indicates that matching files should be re-uploaded. Useful when - // other route-determined metadata (e.g., ContentType) has changed. - Force bool - - // Re is Pattern compiled. - Re *regexp.Regexp `json:"-"` -} - -func (m *Matcher) Matches(path string) bool { - 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) { - dcfg := DefaultConfig - - if !cfg.IsSet(DeploymentConfigKey) { - return dcfg, nil - } - if err := mapstructure.WeakDecode(cfg.GetStringMap(DeploymentConfigKey), &dcfg); err != nil { - return dcfg, err - } - - if dcfg.Workers <= 0 { - dcfg.Workers = 10 - } - - for _, tgt := range dcfg.Targets { - if *tgt == (Target{}) { - return dcfg, errors.New("empty deployment target") - } - if err := tgt.ParseIncludeExclude(); err != nil { - return dcfg, err - } - } - var err error - for _, m := range dcfg.Matchers { - if *m == (Matcher{}) { - return dcfg, errors.New("empty deployment matcher") - } - m.Re, err = regexp.Compile(m.Pattern) - if err != nil { - return dcfg, fmt.Errorf("invalid deployment.matchers.pattern: %v", err) - } - } - for _, o := range dcfg.Order { - re, err := regexp.Compile(o) - if err != nil { - return dcfg, fmt.Errorf("invalid deployment.orderings.pattern: %v", err) - } - dcfg.Ordering = append(dcfg.Ordering, re) - } - - return dcfg, nil -} diff --git a/deploy/deployconfig/deployConfig_test.go b/deploy/deployconfig/deployConfig_test.go deleted file mode 100644 index 38d0aadd6..000000000 --- a/deploy/deployconfig/deployConfig_test.go +++ /dev/null @@ -1,198 +0,0 @@ -// 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 deployconfig - -import ( - "fmt" - "testing" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/config" -) - -func TestDecodeConfigFromTOML(t *testing.T) { - c := qt.New(t) - - tomlConfig := ` - -someOtherValue = "foo" - -[deployment] - -order = ["o1", "o2"] - -# All lowercase. -[[deployment.targets]] -name = "name0" -url = "url0" -cloudfrontdistributionid = "cdn0" -include = "*.html" - -# All uppercase. -[[deployment.targets]] -NAME = "name1" -URL = "url1" -CLOUDFRONTDISTRIBUTIONID = "cdn1" -INCLUDE = "*.jpg" - -# Camelcase. -[[deployment.targets]] -name = "name2" -url = "url2" -cloudFrontDistributionID = "cdn2" -exclude = "*.png" - -# All lowercase. -[[deployment.matchers]] -pattern = "^pattern0$" -cachecontrol = "cachecontrol0" -contentencoding = "contentencoding0" -contenttype = "contenttype0" - -# All uppercase. -[[deployment.matchers]] -PATTERN = "^pattern1$" -CACHECONTROL = "cachecontrol1" -CONTENTENCODING = "contentencoding1" -CONTENTTYPE = "contenttype1" -GZIP = true -FORCE = true - -# Camelcase. -[[deployment.matchers]] -pattern = "^pattern2$" -cacheControl = "cachecontrol2" -contentEncoding = "contentencoding2" -contentType = "contenttype2" -gzip = true -force = true -` - cfg, err := config.FromConfigString(tomlConfig, "toml") - c.Assert(err, qt.IsNil) - - dcfg, err := DecodeConfig(cfg) - c.Assert(err, qt.IsNil) - - // Order. - 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) - - // Targets. - c.Assert(len(dcfg.Targets), qt.Equals, 3) - wantInclude := []string{"*.html", "*.jpg", ""} - wantExclude := []string{"", "", "*.png"} - for i := 0; i < 3; i++ { - tgt := dcfg.Targets[i] - c.Assert(tgt.Name, qt.Equals, fmt.Sprintf("name%d", i)) - c.Assert(tgt.URL, qt.Equals, fmt.Sprintf("url%d", i)) - 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.Exclude, qt.Equals, wantExclude[i]) - if wantExclude[i] != "" { - c.Assert(tgt.ExcludeGlob, qt.Not(qt.IsNil)) - } - } - - // Matchers. - c.Assert(len(dcfg.Matchers), qt.Equals, 3) - 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.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)) - c.Assert(m.Gzip, qt.Equals, i != 0) - c.Assert(m.Force, qt.Equals, i != 0) - } -} - -func TestInvalidOrderingPattern(t *testing.T) { - c := qt.New(t) - - tomlConfig := ` - -someOtherValue = "foo" - -[deployment] -order = ["["] # invalid regular expression -` - cfg, err := config.FromConfigString(tomlConfig, "toml") - c.Assert(err, qt.IsNil) - - _, err = DecodeConfig(cfg) - c.Assert(err, qt.Not(qt.IsNil)) -} - -func TestInvalidMatcherPattern(t *testing.T) { - c := qt.New(t) - - tomlConfig := ` - -someOtherValue = "foo" - -[deployment] -[[deployment.matchers]] -Pattern = "[" # invalid regular expression -` - cfg, err := config.FromConfigString(tomlConfig, "toml") - c.Assert(err, qt.IsNil) - - _, err = DecodeConfig(cfg) - c.Assert(err, qt.Not(qt.IsNil)) -} - -func TestDecodeConfigDefault(t *testing.T) { - c := qt.New(t) - - dcfg, err := DecodeConfig(config.New()) - c.Assert(err, qt.IsNil) - c.Assert(len(dcfg.Targets), qt.Equals, 0) - c.Assert(len(dcfg.Matchers), qt.Equals, 0) -} - -func TestEmptyTarget(t *testing.T) { - c := qt.New(t) - - tomlConfig := ` -[deployment] -[[deployment.targets]] -` - cfg, err := config.FromConfigString(tomlConfig, "toml") - c.Assert(err, qt.IsNil) - - _, err = DecodeConfig(cfg) - c.Assert(err, qt.Not(qt.IsNil)) -} - -func TestEmptyMatcher(t *testing.T) { - c := qt.New(t) - - tomlConfig := ` -[deployment] -[[deployment.matchers]] -` - cfg, err := config.FromConfigString(tomlConfig, "toml") - c.Assert(err, qt.IsNil) - - _, err = DecodeConfig(cfg) - c.Assert(err, qt.Not(qt.IsNil)) -} diff --git a/deploy/google.go b/deploy/google.go deleted file mode 100644 index 5b302e95b..000000000 --- a/deploy/google.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2019 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 deploy - -import ( - "context" - "fmt" - "strings" - - "google.golang.org/api/compute/v1" -) - -// Invalidate all of the content in a Google Cloud CDN distribution. -func InvalidateGoogleCloudCDN(ctx context.Context, origin string) error { - parts := strings.Split(origin, "/") - if len(parts) != 2 { - return fmt.Errorf("origin must be /") - } - service, err := compute.NewService(ctx) - if err != nil { - return err - } - rule := &compute.CacheInvalidationRule{Path: "/*"} - _, err = service.UrlMaps.InvalidateCache(parts[0], parts[1], rule).Context(ctx).Do() - return err -} diff --git a/deps/deps.go b/deps/deps.go index d0d6d95fc..b5f935c09 100644 --- a/deps/deps.go +++ b/deps/deps.go @@ -1,51 +1,27 @@ package deps import ( - "context" - "fmt" - "io" + "io/ioutil" + "log" "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/output" "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 +// There will be normally be 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:"-"` + Log *jww.Notepad `json:"-"` - ExecHelper *hexec.Exec + // The templates to use. This will usually implement the full tpl.TemplateHandler. + Tmpl tpl.TemplateFinder `json:"-"` // The file systems to use. Fs *hugofs.Fs `json:"-"` @@ -56,443 +32,145 @@ type Deps struct { // The ContentSpec to use *helpers.ContentSpec `json:"-"` - // The SourceSpec to use - SourceSpec *source.SourceSpec `json:"-"` - - // The Resource Spec to use - ResourceSpec *resources.Spec - // The configuration to use - Conf config.AllProvider `json:"-"` - - // The memory cache to use. - MemCache *dynacache.Cache + Cfg config.Provider `json:"-"` // The translation func to use - Translate func(ctx context.Context, translationID string, templateData any) string `json:"-"` + Translate func(translationID string, args ...interface{}) string `json:"-"` - // The site building. - Site page.Site + Language *helpers.Language - TemplateStore *tplimpl.TemplateStore + // All the output formats available for the current site. + OutputFormatsConfig output.Formats - // Used in tests - OverloadedTemplateFuncs map[string]any + templateProvider ResourceProvider + WithTemplate func(templ tpl.TemplateHandler) error `json:"-"` - TranslationProvider ResourceProvider - - Metrics metrics.Provider - - // BuildStartListeners will be notified before a build starts. - 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 *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 -} - -func (d Deps) Clone(s page.Site, conf config.AllProvider) (*Deps, error) { - d.Conf = conf - d.Site = s - d.ExecHelper = nil - d.ContentSpec = nil - - if err := d.Init(); err != nil { - return nil, err - } - - return &d, nil -} - -func (d *Deps) GetTemplateStore() *tplimpl.TemplateStore { - return d.TemplateStore -} - -func (d *Deps) Init() error { - if d.Conf == nil { - panic("conf is nil") - } - - if d.Fs == nil { - // For tests. - d.Fs = hugofs.NewFrom(afero.NewMemMapFs(), d.Conf.BaseConfig()) - } - - if d.Log == nil { - d.Log = loggers.NewDefault() - } - - if d.globalErrHandler == nil { - 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[any]{} - } - - if d.BuildEndListeners == nil { - d.BuildEndListeners = &Listeners[any]{} - } - - if d.BuildClosers == nil { - d.BuildClosers = &types.Closers{} - } - - if d.OnChangeListeners == nil { - d.OnChangeListeners = &Listeners[identity.Identity]{} - } - - if d.Metrics == nil && d.Conf.TemplateMetrics() { - d.Metrics = metrics.NewProvider(d.Conf.TemplateMetricsHints()) - } - - if d.ExecHelper == nil { - 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 []byte) { - s := string(match) - switch s { - case postpub.PostProcessPrefix: - d.BuildState.AddFilenameWithPostPrefix(name) - case tpl.HugoDeferredTemplatePrefix: - d.BuildState.DeferredExecutions.FilenamesWithPostPrefix.Set(name, true) - } - } - - // Skip binary files. - mediaTypes := d.Conf.GetConfigSection("mediaTypes").(media.Types) - 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(tpl.HugoDeferredTemplatePrefix), - []byte(postpub.PostProcessPrefix)) - - pathSpec, err := helpers.NewPathSpec(d.Fs, d.Conf, d.Log) - if err != nil { - return err - } - d.PathSpec = pathSpec - } else { - var err error - d.PathSpec, err = helpers.NewPathSpecWithBaseBaseFsProvided(d.Fs, d.Conf, d.Log, d.PathSpec.BaseFs) - if err != nil { - return err - } - } - - if d.ContentSpec == nil { - contentSpec, err := helpers.NewContentSpec(d.Conf, d.Log, d.Content.Fs, d.ExecHelper) - if err != nil { - return err - } - d.ContentSpec = contentSpec - } - - if d.SourceSpec == nil { - d.SourceSpec = source.NewSourceSpec(d.PathSpec, nil, d.Fs.Source) - } - - var common *resources.SpecCommon - if d.ResourceSpec != nil { - common = d.ResourceSpec.SpecCommon - } - - 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) - } - d.ResourceSpec = resourceSpec - - 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.TranslationProvider.NewResource(d); err != nil { - return err - } - return nil - } - - if err = d.TranslationProvider.CloneResource(d, prototype); err != nil { - return err - } - - 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. - quit chan struct{} -} - -// SendError sends the error on a channel to be handled later. -// This can be used in situations where returning and aborting the current -// operation isn't practical. -func (e *globalErrHandler) SendError(err error) { - if e.buildErrors != nil { - select { - case <-e.quit: - case e.buildErrors <- err: - default: - } - return - } - e.logger.Errorln(err) -} - -func (e *globalErrHandler) StartErrorCollector() chan error { - e.quit = make(chan struct{}) - e.buildErrors = make(chan error, 10) - return e.buildErrors -} - -func (e *globalErrHandler) StopErrorCollector() { - if e.buildErrors != nil { - close(e.quit) - close(e.buildErrors) - } -} - -// Listeners represents an event listener. -type Listeners[T any] struct { - sync.Mutex - - // A list of funcs to be notified about an event. - // 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[T]) Add(f func(...T) bool) { - if b == nil { - return - } - b.Lock() - defer b.Unlock() - b.listeners = append(b.listeners, f) -} - -// Notify executes all listener functions. -func (b *Listeners[T]) Notify(vs ...T) { - b.Lock() - defer b.Unlock() - temp := b.listeners[:0] - for _, notify := range b.listeners { - if !notify(vs...) { - temp = append(temp, notify) - } - } - b.listeners = temp + translationProvider ResourceProvider } // ResourceProvider is used to create and refresh, and clone resources needed. type ResourceProvider interface { - NewResource(dst *Deps) error - CloneResource(dst, src *Deps) error + Update(deps *Deps) error + Clone(deps *Deps) error } -func (d *Deps) Close() error { - if d.isClosed { - return nil - } - d.isClosed = true +func (d *Deps) TemplateHandler() tpl.TemplateHandler { + return d.Tmpl.(tpl.TemplateHandler) +} - if d.MemCache != nil { - d.MemCache.Stop() +func (d *Deps) LoadResources() error { + // Note that the translations need to be loaded before the templates. + if err := d.translationProvider.Update(d); err != nil { + return err } - if d.WasmDispatchers != nil { - d.WasmDispatchers.Close() + + if err := d.templateProvider.Update(d); err != nil { + return err } - return d.BuildClosers.Close() + + if th, ok := d.Tmpl.(tpl.TemplateHandler); ok { + th.PrintErrors() + } + + return nil +} + +func New(cfg DepsCfg) (*Deps, error) { + var ( + logger = cfg.Logger + fs = cfg.Fs + ) + + if cfg.TemplateProvider == nil { + panic("Must have a TemplateProvider") + } + + if cfg.TranslationProvider == nil { + panic("Must have a TranslationProvider") + } + + if cfg.Language == nil { + panic("Must have a Language") + } + + if logger == nil { + logger = jww.NewNotepad(jww.LevelError, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime) + } + + if fs == nil { + // Default to the production file system. + fs = hugofs.NewDefault(cfg.Language) + } + + ps, err := helpers.NewPathSpec(fs, cfg.Language) + + if err != nil { + return nil, err + } + + d := &Deps{ + Fs: fs, + Log: logger, + templateProvider: cfg.TemplateProvider, + translationProvider: cfg.TranslationProvider, + WithTemplate: cfg.WithTemplate, + PathSpec: ps, + ContentSpec: helpers.NewContentSpec(cfg.Language), + Cfg: cfg.Language, + Language: cfg.Language, + } + + return d, nil +} + +// ForLanguage creates a copy of the Deps with the language dependent +// parts switched out. +func (d Deps) ForLanguage(l *helpers.Language) (*Deps, error) { + var err error + + d.PathSpec, err = helpers.NewPathSpec(d.Fs, l) + if err != nil { + return nil, err + } + + d.ContentSpec = helpers.NewContentSpec(l) + d.Cfg = l + d.Language = l + + if err := d.translationProvider.Clone(&d); err != nil { + return nil, err + } + + if err := d.templateProvider.Clone(&d); err != nil { + return nil, err + } + + return &d, nil + } // DepsCfg contains configuration options that can be used to configure Hugo // 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 logging level to use. - LogLevel logg.Level - - // Logging output. - StdErr io.Writer - - // The console output. - StdOut io.Writer + // The Logger to use. + Logger *jww.Notepad // The file systems to use Fs *hugofs.Fs - // The Site in use - Site page.Site + // The language to use. + Language *helpers.Language - Configs *allconfig.Configs + // The configuration to use. + Cfg config.Provider // Template handling. TemplateProvider ResourceProvider + WithTemplate func(templ tpl.TemplateHandler) error // 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. -type BuildState struct { - counter uint64 - - mu sync.Mutex // protects state below. - - 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) { - b.mu.Lock() - defer b.mu.Unlock() - if b.filenamesWithPostPrefix == nil { - b.filenamesWithPostPrefix = make(map[string]bool) - } - b.filenamesWithPostPrefix[filename] = true -} - -func (b *BuildState) GetFilenamesWithPostPrefix() []string { - b.mu.Lock() - defer b.mu.Unlock() - var filenames []string - for filename := range b.filenamesWithPostPrefix { - filenames = append(filenames, filename) - } - sort.Strings(filenames) - return filenames -} - -func (b *BuildState) Incr() int { - return int(atomic.AddUint64(&b.counter, uint64(1))) } diff --git a/deps/deps_test.go b/deps/deps_test.go deleted file mode 100644 index e92ed2327..000000000 --- a/deps/deps_test.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2019 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 deps_test - -import ( - "testing" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/deps" -) - -func TestBuildFlags(t *testing.T) { - c := qt.New(t) - var bf deps.BuildState - bf.Incr() - bf.Incr() - bf.Incr() - - c.Assert(bf.Incr(), qt.Equals, 4) -} diff --git a/docs/.codespellrc b/docs/.codespellrc deleted file mode 100644 index 564fc77c0..000000000 --- a/docs/.codespellrc +++ /dev/null @@ -1,13 +0,0 @@ -# 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 deleted file mode 100644 index bf61489da..000000000 --- a/docs/.cspell.json +++ /dev/null @@ -1,185 +0,0 @@ -{ - "version": "0.2", - "allowCompoundWords": true, - "files": [ - "**/*.md" - ], - "flagWords": [ - "alot", - "hte", - "langauge", - "reccommend", - "seperate", - "teh" - ], - "ignorePaths": [ - "**/emojis.md", - "**/commands/*", - "**/showcase/*", - "**/tools/*" - ], - "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/.editorconfig b/docs/.editorconfig deleted file mode 100644 index dd2a0096f..000000000 --- a/docs/.editorconfig +++ /dev/null @@ -1,20 +0,0 @@ -# https://editorconfig.org - -root = true - -[*] -charset = utf-8 -end_of_line = lf -indent_size = 2 -indent_style = space -trim_trailing_whitespace = true - -[*.go] -indent_size = 8 -indent_style = tab - -[*.js] -insert_final_newline = true - -[*.md] -trim_trailing_whitespace = false diff --git a/docs/.github/ISSUE_TEMPLATE/config.yml b/docs/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 3ba13e0ce..000000000 --- a/docs/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1 +0,0 @@ -blank_issues_enabled: false diff --git a/docs/.github/ISSUE_TEMPLATE/default.md b/docs/.github/ISSUE_TEMPLATE/default.md deleted file mode 100644 index ada35b3a5..000000000 --- a/docs/.github/ISSUE_TEMPLATE/default.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -name: Default -about: This is the default issue template. -labels: - - NeedsTriage ---- diff --git a/docs/.github/SUPPORT.md b/docs/.github/SUPPORT.md deleted file mode 100644 index 96a4400c3..000000000 --- a/docs/.github/SUPPORT.md +++ /dev/null @@ -1,3 +0,0 @@ -### 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/stale.yml b/docs/.github/stale.yml deleted file mode 100644 index 1e72eb329..000000000 --- a/docs/.github/stale.yml +++ /dev/null @@ -1,22 +0,0 @@ -# Number of days of inactivity before an issue becomes stale -daysUntilStale: 120 -# Number of days of inactivity before a stale issue is closed -daysUntilClose: 30 -# Issues with these labels will never be considered stale -exemptLabels: - - Keep - - Security - - UndocumentedFeature -# Label to use when marking an issue as stale -staleLabel: Stale -# Comment to post when marking an issue as stale. Set to `false` to disable -markComment: > - This issue has been automatically marked as stale because it has not had - recent activity. The resources of the Hugo team are limited, and so we are asking for your help. - - If you still think this is important, please tell us why. - - This issue will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions. - -# Comment to post when closing a stale issue. Set to `false` to disable -closeComment: false diff --git a/docs/.github/workflows/codeql-analysis.yml b/docs/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 86441b845..000000000 --- a/docs/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: "CodeQL" - -on: - schedule: - - cron: "0 0 1 * *" - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: "javascript" - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 diff --git a/docs/.github/workflows/spellcheck.yml b/docs/.github/workflows/spellcheck.yml deleted file mode 100644 index e01ab1764..000000000 --- a/docs/.github/workflows/spellcheck.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: "Check spelling" -on: - push: - pull_request: - branches-ignore: - - "dependabot/**" - -permissions: - contents: read - -jobs: - spellcheck: - runs-on: ubuntu-latest - steps: - - 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 deleted file mode 100644 index d8e408ee2..000000000 --- a/docs/.github/workflows/super-linter.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Super Linter - -on: - workflow_dispatch: - -permissions: - contents: read # to fetch code (actions/checkout) - -jobs: - build: - permissions: - contents: read # to fetch code (actions/checkout) - statuses: write # to mark status of each linter run (github/super-linter/slim) - - name: Lint Code Base - runs-on: ubuntu-latest - if: ${{ github.actor != 'dependabot[bot]' }} - - steps: - - name: Checkout Code - uses: actions/checkout@v4 - - - name: Lint Code Base - uses: super-linter/super-linter/slim@v6 - env: - DEFAULT_BRANCH: master - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - IGNORE_GITIGNORED_FILES: true - LINTER_RULES_PATH: / - LOG_LEVEL: NOTICE - MARKDOWN_CONFIG_FILE: .markdownlint.yaml - SUPPRESS_POSSUM: true - VALIDATE_CSS: false - VALIDATE_EDITORCONFIG: false - VALIDATE_GITLEAKS: false - VALIDATE_HTML: false - VALIDATE_JAVASCRIPT_STANDARD: false - VALIDATE_JSCPD: false - VALIDATE_NATURAL_LANGUAGE: false - VALIDATE_SHELL_SHFMT: false - VALIDATE_XML: false diff --git a/docs/.gitignore b/docs/.gitignore index 5208c5c3a..665360d49 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1,12 +1,2 @@ -.DS_Store -.hugo_build.lock /.idea -/.vscode -/dist /public -/resources -hugo_stats.json -node_modules/ -nohup.out -package-lock.json -trace.out diff --git a/docs/.markdownlint.yaml b/docs/.markdownlint.yaml deleted file mode 100644 index dbb5b2ee8..000000000 --- a/docs/.markdownlint.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md - -MD001: false -MD002: false -MD003: false -MD004: false -MD007: false -MD012: - maximum: 2 -MD013: false -MD014: false -MD022: false -MD024: false -MD031: false -MD032: false -MD033: false -MD034: false -MD036: false -MD037: false -MD038: false -MD041: false -MD046: false -MD049: false -MD050: false -MD051: false -MD053: false -MD055: false diff --git a/docs/.markdownlintignore b/docs/.markdownlintignore deleted file mode 100644 index 4ac45b395..000000000 --- a/docs/.markdownlintignore +++ /dev/null @@ -1,6 +0,0 @@ -**/commands/** -**/functions/** -**/news/** -**/showcase/** -**/zh/** -**/license.md diff --git a/docs/.prettierignore b/docs/.prettierignore deleted file mode 100644 index f24bbcef0..000000000 --- a/docs/.prettierignore +++ /dev/null @@ -1,18 +0,0 @@ -# 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 deleted file mode 100644 index 395ae39af..000000000 --- a/docs/.prettierrc +++ /dev/null @@ -1,24 +0,0 @@ -{ - "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/.textlintignore b/docs/.textlintignore deleted file mode 100644 index 97a18e37c..000000000 --- a/docs/.textlintignore +++ /dev/null @@ -1,3 +0,0 @@ -**/news/** -**/showcase/** -**/zh/** \ No newline at end of file diff --git a/docs/.vscode/extensions.json b/docs/.vscode/extensions.json deleted file mode 100644 index 76c6afe3f..000000000 --- a/docs/.vscode/extensions.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "recommendations": [ - "DavidAnson.vscode-markdownlint", - "EditorConfig.EditorConfig", - "streetsidesoftware.code-spell-checker" - ] -} diff --git a/docs/LICENSE.md b/docs/LICENSE.md deleted file mode 100644 index d4facbf8a..000000000 --- a/docs/LICENSE.md +++ /dev/null @@ -1,3 +0,0 @@ -See [content/LICENSE.md](content/LICENSE.md) for the license of the content of this repository. - -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 58d0e748c..60a554e54 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,27 +1,13 @@ -Hugo +# Hugo Docs -A fast and flexible static site generator built with love by [bep], [spf13], and [friends] in [Go]. +Documentation site for [Hugo](https://github.com/gohugoio/hugo), the very fast and flexible static site generator built with love in GoLang. ---- +## Branches -[![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/) +* The `master` branch is the **current Hugo version** and will be auto-deployed to [gohugo.io/](https://gohugo.io/). +* Anything not relevant to the current Hugo version goes into the `dev` branch. +* Changes in [hugo/docs](https://github.com/gohugoio/hugo/tree/master/docs) will, in general, be merged once every release, but can be manually merged/cherry picked if needed. This goes both ways. +* All contributions that is not tightly coupled with code changes, should be made directly to `hugoDocs`. +* But we also merge PRs into [hugo/docs](https://github.com/gohugoio/hugo/tree/master/docs), but preferably changes that is related to the code in the PR itself -This is the repository for the [Hugo](https://github.com/gohugoio/hugo) documentation site. - -Please see the [contributing] section for guidelines, examples, and process. - -[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 - -# Install - -```sh -npm i -hugo server -``` - -**Note:** We're working on removing the need to run `npm i` for local development. Stay tuned. +To summarize, we have two branches in this repository: `master` (current Hugo) and `dev` (next Hugo). diff --git a/docs/archetypes/default.md b/docs/archetypes/default.md index 58a60edc4..6d6497c4d 100644 --- a/docs/archetypes/default.md +++ b/docs/archetypes/default.md @@ -1,6 +1,6 @@ ---- -title: {{ replace .File.ContentBaseName "-" " " | strings.FirstUpper }} -description: -categories: [] -keywords: [] ---- ++++ +weight = 5 +[menu] + [menu.main] + parent = "x" ++++ diff --git a/docs/archetypes/functions.md b/docs/archetypes/functions.md deleted file mode 100644 index de2d72060..000000000 --- a/docs/archetypes/functions.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: {{ replace .File.ContentBaseName "-" " " | title }} -description: -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: - signatures: [] ---- diff --git a/docs/archetypes/glossary.md b/docs/archetypes/glossary.md deleted file mode 100644 index 1eeb4ef4b..000000000 --- a/docs/archetypes/glossary.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: {{ replace .File.ContentBaseName "-" " " }} -params: - reference: ---- - - diff --git a/docs/archetypes/methods.md b/docs/archetypes/methods.md deleted file mode 100644 index 944fe527c..000000000 --- a/docs/archetypes/methods.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: {{ replace .File.ContentBaseName "-" " " | title }} -description: -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: - signatures: [] ---- diff --git a/docs/archetypes/news.md b/docs/archetypes/news.md deleted file mode 100644 index 04792a152..000000000 --- a/docs/archetypes/news.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: {{ replace .File.ContentBaseName "-" " " | strings.FirstUpper }} -description: -categories: [] -keywords: [] -publishDate: {{ .Date }} ---- diff --git a/docs/archetypes/showcase.md b/docs/archetypes/showcase.md new file mode 100644 index 000000000..ebe87035a --- /dev/null +++ b/docs/archetypes/showcase.md @@ -0,0 +1,14 @@ +--- +date: 2013-07-01T07:32:00Z +description: "" +license: "" +licenseLink: "" +sitelink: http://spf13.com/ +sourceLink: https://github.com/spf13/spf13.com +tags: +- personal +- blog +thumbnail: /img/spf13-tn.jpg +title: spf13.com +--- + diff --git a/docs/assets/css/components/all.css b/docs/assets/css/components/all.css deleted file mode 100644 index f5002fd50..000000000 --- a/docs/assets/css/components/all.css +++ /dev/null @@ -1,7 +0,0 @@ -/* The order of these does not matter. */ -@import "./content.css"; -@import "./fonts.css"; -@import "./helpers.css"; -@import "./shortcodes.css"; -@import "./tableofcontents.css"; -@import "./view-transitions.css"; diff --git a/docs/assets/css/components/chroma.css b/docs/assets/css/components/chroma.css deleted file mode 100644 index 9d4c91f7b..000000000 --- a/docs/assets/css/components/chroma.css +++ /dev/null @@ -1,85 +0,0 @@ -/* Background */ .bg { background-color: var(--color-light); } -/* PreWrapper */ .chroma { background-color: var(--color-light); } -/* Other */ .chroma .x { } -/* Error */ .chroma .err { color: #a61717; background-color: #e3d2d2 } -/* CodeLine */ .chroma .cl { } -/* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } -/* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; } -/* LineHighlight */ .chroma .hl { background-color: #ffffcc } -/* LineNumbersTable */ .chroma .lnt { white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } -/* LineNumbers */ .chroma .ln { white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } -/* Line */ .chroma .line { display: flex; } -/* 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 } -/* Name */ .chroma .n { } -/* NameAttribute */ .chroma .na { color: #008080 } -/* NameBuiltin */ .chroma .nb { color: #999999 } -/* NameBuiltinPseudo */ .chroma .bp { } -/* NameClass */ .chroma .nc { color: #445588; font-weight: bold } -/* NameConstant */ .chroma .no { color: #008080 } -/* NameDecorator */ .chroma .nd { } -/* NameEntity */ .chroma .ni { color: #800080 } -/* NameException */ .chroma .ne { color: #990000; font-weight: bold } -/* NameFunction */ .chroma .nf { color: #990000; font-weight: bold } -/* NameFunctionMagic */ .chroma .fm { } -/* NameLabel */ .chroma .nl { } -/* NameNamespace */ .chroma .nn { color: #555555 } -/* NameOther */ .chroma .nx { } -/* NameProperty */ .chroma .py { } -/* NameTag */ .chroma .nt { color: #000080 } -/* NameVariable */ .chroma .nv { color: #008080 } -/* NameVariableClass */ .chroma .vc { } -/* NameVariableGlobal */ .chroma .vg { } -/* NameVariableInstance */ .chroma .vi { } -/* NameVariableMagic */ .chroma .vm { } -/* Literal */ .chroma .l { } -/* LiteralDate */ .chroma .ld { } -/* 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 } -/* Punctuation */ .chroma .p { } -/* 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 } -/* Generic */ .chroma .g { } -/* 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 } -/* GenericUnderline */ .chroma .gl { text-decoration: underline } -/* TextWhitespace */ .chroma .w { color: #bbbbbb } diff --git a/docs/assets/css/components/chroma_dark.css b/docs/assets/css/components/chroma_dark.css deleted file mode 100644 index 0b0ae3000..000000000 --- a/docs/assets/css/components/chroma_dark.css +++ /dev/null @@ -1,85 +0,0 @@ -/* Background */.dark .bg { background-color: var(--color-dark); } -/* PreWrapper */ .dark .chroma { background-color: var(--color-dark); } -/* Other */ .dark .chroma .x { } -/* Error */ .dark .chroma .err { color: #ef6155 } -/* CodeLine */ .dark .chroma .cl { } -/* LineTableTD */ .dark .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } -/* LineTable */ .dark .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; } -/* LineHighlight */ .dark .chroma .hl { background-color: rgb(0,19,28) } -/* LineNumbersTable */ .dark .chroma .lnt { white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } -/* LineNumbers */ .dark .chroma .ln { white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } -/* Line */ .dark .chroma .line { display: flex; } -/* Keyword */ .dark .chroma .k { color: #815ba4 } -/* KeywordConstant */ .dark .chroma .kc { color: #815ba4 } -/* KeywordDeclaration */ .dark .chroma .kd { color: #815ba4 } -/* KeywordNamespace */ .dark .chroma .kn { color: #5bc4bf } -/* KeywordPseudo */ .dark .chroma .kp { color: #815ba4 } -/* KeywordReserved */ .dark .chroma .kr { color: #815ba4 } -/* KeywordType */ .dark .chroma .kt { color: #fec418 } -/* Name */ .dark .chroma .n { } -/* NameAttribute */ .dark .chroma .na { color: #06b6ef } -/* NameBuiltin */ .dark .chroma .nb { } -/* NameBuiltinPseudo */ .dark .chroma .bp { } -/* NameClass */ .dark .chroma .nc { color: #fec418 } -/* NameConstant */ .dark .chroma .no { color: #ef6155 } -/* NameDecorator */ .dark .chroma .nd { color: #5bc4bf } -/* NameEntity */ .dark .chroma .ni { } -/* NameException */ .dark .chroma .ne { color: #ef6155 } -/* NameFunction */ .dark .chroma .nf { color: #06b6ef } -/* NameFunctionMagic */ .dark .chroma .fm { } -/* NameLabel */ .dark .chroma .nl { } -/* NameNamespace */ .dark .chroma .nn { color: #fec418 } -/* NameOther */ .dark .chroma .nx { color: #06b6ef } -/* NameProperty */ .dark .chroma .py { } -/* NameTag */ .dark .chroma .nt { color: #5bc4bf } -/* NameVariable */ .dark .chroma .nv { color: #ef6155 } -/* NameVariableClass */ .dark .chroma .vc { } -/* NameVariableGlobal */ .dark .chroma .vg { } -/* NameVariableInstance */ .dark .chroma .vi { } -/* NameVariableMagic */ .dark .chroma .vm { } -/* Literal */ .dark .chroma .l { color: #f99b15 } -/* LiteralDate */ .dark .chroma .ld { color: #48b685 } -/* LiteralString */ .dark .chroma .s { color: #48b685 } -/* LiteralStringAffix */ .dark .chroma .sa { color: #48b685 } -/* LiteralStringBacktick */ .dark .chroma .sb { color: #48b685 } -/* LiteralStringChar */ .dark .chroma .sc { } -/* LiteralStringDelimiter */ .dark .chroma .dl { color: #48b685 } -/* LiteralStringDoc */ .dark .chroma .sd { color: #776e71 } -/* LiteralStringDouble */ .dark .chroma .s2 { color: #48b685 } -/* LiteralStringEscape */ .dark .chroma .se { color: #f99b15 } -/* LiteralStringHeredoc */ .dark .chroma .sh { color: #48b685 } -/* LiteralStringInterpol */ .dark .chroma .si { color: #f99b15 } -/* LiteralStringOther */ .dark .chroma .sx { color: #48b685 } -/* LiteralStringRegex */ .dark .chroma .sr { color: #48b685 } -/* LiteralStringSingle */ .dark .chroma .s1 { color: #48b685 } -/* LiteralStringSymbol */ .dark .chroma .ss { color: #48b685 } -/* LiteralNumber */ .dark .chroma .m { color: #f99b15 } -/* LiteralNumberBin */ .dark .chroma .mb { color: #f99b15 } -/* LiteralNumberFloat */ .dark .chroma .mf { color: #f99b15 } -/* LiteralNumberHex */ .dark .chroma .mh { color: #f99b15 } -/* LiteralNumberInteger */ .dark .chroma .mi { color: #f99b15 } -/* LiteralNumberIntegerLong */ .dark .chroma .il { color: #f99b15 } -/* LiteralNumberOct */ .dark .chroma .mo { color: #f99b15 } -/* Operator */ .dark .chroma .o { color: #5bc4bf } -/* OperatorWord */ .dark .chroma .ow { color: #5bc4bf } -/* Punctuation */ .dark .chroma .p { } -/* Comment */ .dark .chroma .c { color: #776e71 } -/* CommentHashbang */ .dark .chroma .ch { color: #776e71 } -/* CommentMultiline */ .dark .chroma .cm { color: #776e71 } -/* CommentSingle */ .dark .chroma .c1 { color: #776e71 } -/* CommentSpecial */ .dark .chroma .cs { color: #776e71 } -/* CommentPreproc */ .dark .chroma .cp { color: #776e71 } -/* CommentPreprocFile */ .dark .chroma .cpf { color: #776e71 } -/* Generic */ .dark .chroma .g { } -/* GenericDeleted */ .dark .chroma .gd { color: #ef6155 } -/* GenericEmph */ .dark .chroma .ge { font-style: italic } -/* GenericError */ .dark .chroma .gr { } -/* GenericHeading */ .dark .chroma .gh { font-weight: bold } -/* GenericInserted */ .dark .chroma .gi { color: #48b685 } -/* GenericOutput */ .dark .chroma .go { } -/* GenericPrompt */ .dark .chroma .gp { color: #776e71; font-weight: bold } -/* GenericStrong */ .dark .chroma .gs { font-weight: bold } -/* GenericSubheading */ .dark .chroma .gu { color: #5bc4bf; font-weight: bold } -/* GenericTraceback */ .dark .chroma .gt { } -/* GenericUnderline */ .dark .chroma .gl { } -/* TextWhitespace */ .dark .chroma .w { } diff --git a/docs/assets/css/components/content.css b/docs/assets/css/components/content.css deleted file mode 100644 index e9064f439..000000000 --- a/docs/assets/css/components/content.css +++ /dev/null @@ -1,49 +0,0 @@ -@import "./chroma_dark.css"; -@import "./chroma.css"; -@import "./highlight.css"; - -/* Some contrast ratio fixes as reported by Google Page Speed. */ -.chroma .c1 { - @apply text-gray-500; -} - -.dark .chroma .c1 { - @apply text-gray-400; -} - -.highlight code { - @apply text-sm/6; -} - -.content { - @apply prose prose-sm sm:prose-base prose-stone max-w-none dark:prose-invert dark:text-slate-200; - /* headings */ - @apply prose-headings:font-semibold; - /* lead */ - @apply prose-lead:text-slate-500 prose-lead:text-xl prose-lead:mt-2 sm:prose-lead:mt-4 prose-lead:leading-relaxed dark:prose-lead:text-slate-400; - /* links */ - @apply prose-a:text-primary dark:prose-a:text-blue-500 prose-a:hover:text-blue-500 dark:prose-a:hover:text-blue-400 prose-a:underline; - @apply prose-a:prose-code:underline prose-a:prose-code:hover:text-blue-500 prose-a:prose-code:hover:underline; - /* pre */ - @apply prose-pre:text-gray-800 prose-pre:border-1 prose-pre:border-gray-100 prose-pre:bg-light dark:prose-pre:bg-dark dark:prose-pre:ring-1 dark:prose-pre:ring-slate-300/10; - /* code */ - @apply prose-code:px-0.5 prose-code:text-gray-500 prose-code:dark:text-gray-300 border-none; - @apply prose-code:before:hidden prose-code:after:hidden prose-code:font-mono; - @apply prose-table:prose-th:prose-code:text-white; - /* tables */ - @apply prose-table:w-auto prose-table:border-2 prose-table:border-gray-100 prose-table:dark:border-gray-800 prose-table:prose-th:font-bold prose-table:prose-th:bg-blue-500 dark:prose-table:prose-th:bg-blue-500/50 prose-table:prose-th:p-2 prose-table:prose-td:p-2 prose-table:prose-th:text-white; - /* hr */ - @apply dark:prose-hr:border-slate-800; - /* ol */ - @apply prose-ol:marker:prose dark:prose-ol:marker:text-gray-300; - /* ul */ - @apply prose-ul:marker:text-gray-500 dark:prose-ul:marker:text-gray-300; -} - -/* This will not match highlighting inside e.g. the code-toggle shortcode. */ -/* For more fine grained control of this, see components/shortcodes.css. */ -.content > .highlight, -.content dd > .highlight, -.content li > .highlight { - @apply border-1 border-gray-200 dark:border-slate-600 mt-6 mb-8; -} diff --git a/docs/assets/css/components/fonts.css b/docs/assets/css/components/fonts.css deleted file mode 100644 index 06f40b4bf..000000000 --- a/docs/assets/css/components/fonts.css +++ /dev/null @@ -1,15 +0,0 @@ -@font-face { - font-family: "Mulish"; - font-style: normal; - src: url("../fonts/Mulish-VariableFont_wght.ttf") format("truetype"); - font-weight: 1 999; - font-display: swap; -} - -@font-face { - font-family: "Mulish"; - font-style: italic; - src: url("../fonts/Mulish-Italic-VariableFont_wght.ttf") format("truetype"); - font-weight: 1 999; - font-display: swap; -} diff --git a/docs/assets/css/components/helpers.css b/docs/assets/css/components/helpers.css deleted file mode 100644 index 8eb6930b8..000000000 --- a/docs/assets/css/components/helpers.css +++ /dev/null @@ -1,19 +0,0 @@ -/* Helper class to limit a text block to two lines. */ -.two-lines-ellipsis { - display: block; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - text-overflow: ellipsis; -} - -/* Helper class to limit a text block to three lines. */ -.three-lines-ellipsis { - display: block; - display: -webkit-box; - -webkit-line-clamp: 3; - -webkit-box-orient: vertical; - overflow: hidden; - text-overflow: ellipsis; -} diff --git a/docs/assets/css/components/highlight.css b/docs/assets/css/components/highlight.css deleted file mode 100644 index 5f25fe368..000000000 --- a/docs/assets/css/components/highlight.css +++ /dev/null @@ -1,11 +0,0 @@ -.highlight { - @apply bg-light dark:bg-dark rounded-none; -} - -.highlight pre { - @apply m-0 p-3 w-full h-full overflow-x-auto dark:border-black rounded-none; -} - -.highlight pre code { - @apply m-0 p-0 w-full h-full; -} diff --git a/docs/assets/css/components/shortcodes.css b/docs/assets/css/components/shortcodes.css deleted file mode 100644 index 7314d5b20..000000000 --- a/docs/assets/css/components/shortcodes.css +++ /dev/null @@ -1,4 +0,0 @@ -.shortcode-code { - .highlight { - } -} diff --git a/docs/assets/css/components/tableofcontents.css b/docs/assets/css/components/tableofcontents.css deleted file mode 100644 index 3640adf6d..000000000 --- a/docs/assets/css/components/tableofcontents.css +++ /dev/null @@ -1,14 +0,0 @@ -.tableofcontents { - ul { - @apply list-none; - li { - @apply mb-2; - a { - @apply text-primary; - &:hover { - @apply text-primary/60; - } - } - } - } -} diff --git a/docs/assets/css/components/view-transitions.css b/docs/assets/css/components/view-transitions.css deleted file mode 100644 index cf68ed3d7..000000000 --- a/docs/assets/css/components/view-transitions.css +++ /dev/null @@ -1,20 +0,0 @@ -/* Global slight fade */ -::view-transition-old(root), -::view-transition-new(root) { - animation-duration: 200ms; -} - -::view-transition-old(qr), -::view-transition-new(qr) { - animation-duration: 800ms; - animation-delay: 250ms; -} - -.view-transition-qr { - view-transition-name: qr; -} - -/* Turbo styles */ -.turbo-progress-bar { - visibility: hidden; -} diff --git a/docs/assets/css/styles.css b/docs/assets/css/styles.css deleted file mode 100644 index 6665a7e2b..000000000 --- a/docs/assets/css/styles.css +++ /dev/null @@ -1,131 +0,0 @@ -@import "tailwindcss"; -@plugin "@tailwindcss/typography"; -@variant dark (&:where(.dark, .dark *)); - -@import "components/all.css"; - -/* TailwindCSS ignores files in .gitignore, so make it explicit. */ -@source "hugo_stats.json"; - -@theme { - /* Breakpoints. */ - --breakpoint-sm: 40rem; - --breakpoint-md: 48rem; - --breakpoint-lg: 68rem; /* Default 64rem; */ - --breakpoint-xl: 80rem; - --breakpoint-2xl: 96rem; - - /* Colors. */ - --color-primary: var(--color-blue-600); - --color-dark: #000; - --color-light: var(--color-gray-50); - --color-accent: var(--color-orange-500); - --color-accent-light: var(--color-pink-500); - --color-accent-dark: var(--color-green-500); - - /* https://www.tints.dev/blue/0594CB */ - --color-blue-50: #e1f6fe; - --color-blue-100: #c3edfe; - --color-blue-200: #88dbfc; - --color-blue-300: #4cc9fb; - --color-blue-400: #15b9f9; - --color-blue-500: #0594cb; - --color-blue-600: #0477a4; - --color-blue-700: #035677; - --color-blue-800: #023a50; - --color-blue-900: #011d28; - --color-blue-950: #000e14; - - /* https://www.tints.dev/orange/EBB951 */ - --color-orange-50: #fdf8ed; - --color-orange-100: #fbf1da; - --color-orange-200: #f7e4ba; - --color-orange-300: #f3d596; - --color-orange-400: #efc976; - --color-orange-500: #ebb951; - --color-orange-600: #e5a51a; - --color-orange-700: #a97a13; - --color-orange-800: #72520d; - --color-orange-900: #372806; - --color-orange-950: #1b1403; - - /* https://www.tints.dev/pink/FF4088 */ - --color-pink-50: #ffebf2; - --color-pink-100: #ffdbe9; - --color-pink-200: #ffb3d0; - --color-pink-300: #ff8fba; - --color-pink-400: #ff66a1; - --color-pink-500: #ff4088; - --color-pink-600: #ff0062; - --color-pink-700: #c2004a; - --color-pink-800: #800031; - --color-pink-900: #420019; - --color-pink-950: #1f000c; - - /* https://www.tints.dev/green/33BA91 */ - --color-green-50: #ebfaf5; - --color-green-100: #d3f3e9; - --color-green-200: #abe8d6; - --color-green-300: #7fdcc0; - --color-green-400: #53d0aa; - --color-green-500: #33ba91; - --color-green-600: #299474; - --color-green-700: #1f7058; - --color-green-800: #154c3b; - --color-green-900: #0a241c; - --color-green-950: #051410; - - /* Fonts. */ - --font-sans: - "Mulish", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", - "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; -} - -html { - scroll-padding-top: 100px; -} - -body { - @apply antialiased font-sans text-black dark:text-gray-100; -} - -.p-safe-area-x { - padding-left: env(safe-area-inset-left); - padding-right: env(safe-area-inset-right); -} - -.p-safe-area-y { - padding-top: env(safe-area-inset-top); - padding-bottom: env(safe-area-inset-bottom); -} - -.px-main { - padding-left: max(env(safe-area-inset-left), 1rem); - padding-right: max(env(safe-area-inset-right), 1rem); -} - -@media screen(md) { - .px-main { - padding-left: max(env(safe-area-inset-left), 2rem); - padding-right: max(env(safe-area-inset-right), 2rem); - } -} - -@media screen(lg) { - .px-main { - padding-left: max(env(safe-area-inset-left), 3rem); - padding-right: max(env(safe-area-inset-right), 3rem); - } -} - -/* Algolia DocSearch */ -.algolia-docsearch-suggestion--highlight { - color: var(--color-primary); -} - -/* Footnotes */ -.footnote-backref, -.footnote-ref { - text-decoration: none; - padding-left: .0625em; -} diff --git a/docs/assets/images/examples/landscape-exif-orientation-5.jpg b/docs/assets/images/examples/landscape-exif-orientation-5.jpg deleted file mode 100644 index ad64835eb..000000000 Binary files a/docs/assets/images/examples/landscape-exif-orientation-5.jpg and /dev/null differ diff --git a/docs/assets/images/examples/mask.png b/docs/assets/images/examples/mask.png deleted file mode 100644 index c3005a669..000000000 Binary files a/docs/assets/images/examples/mask.png and /dev/null differ diff --git a/docs/assets/images/examples/zion-national-park.jpg b/docs/assets/images/examples/zion-national-park.jpg deleted file mode 100644 index 7980abccb..000000000 Binary files a/docs/assets/images/examples/zion-national-park.jpg and /dev/null differ diff --git a/docs/assets/images/hugo-github-screenshot.png b/docs/assets/images/hugo-github-screenshot.png deleted file mode 100644 index 275b6969d..000000000 Binary files a/docs/assets/images/hugo-github-screenshot.png and /dev/null differ diff --git a/docs/assets/images/logos/logo-128x128.png b/docs/assets/images/logos/logo-128x128.png deleted file mode 100644 index ec1a2d6e1..000000000 Binary files a/docs/assets/images/logos/logo-128x128.png and /dev/null differ diff --git a/docs/assets/images/logos/logo-256x256.png b/docs/assets/images/logos/logo-256x256.png deleted file mode 100644 index d9fdb888a..000000000 Binary files a/docs/assets/images/logos/logo-256x256.png and /dev/null differ diff --git a/docs/assets/images/logos/logo-512x512.png b/docs/assets/images/logos/logo-512x512.png deleted file mode 100644 index 76d463600..000000000 Binary files a/docs/assets/images/logos/logo-512x512.png and /dev/null differ diff --git a/docs/assets/images/logos/logo-64x64.png b/docs/assets/images/logos/logo-64x64.png deleted file mode 100644 index 9857bcea1..000000000 Binary files a/docs/assets/images/logos/logo-64x64.png and /dev/null differ diff --git a/docs/assets/images/logos/logo-96x96.png b/docs/assets/images/logos/logo-96x96.png deleted file mode 100644 index 48d0cb98e..000000000 Binary files a/docs/assets/images/logos/logo-96x96.png and /dev/null differ diff --git a/docs/assets/images/sponsors/Route4MeLogoBlueOnWhite.svg b/docs/assets/images/sponsors/Route4MeLogoBlueOnWhite.svg deleted file mode 100644 index d4334e8d8..000000000 --- a/docs/assets/images/sponsors/Route4MeLogoBlueOnWhite.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/docs/assets/images/sponsors/bep-consulting.svg b/docs/assets/images/sponsors/bep-consulting.svg deleted file mode 100644 index 598a1eb71..000000000 --- a/docs/assets/images/sponsors/bep-consulting.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/docs/assets/images/sponsors/butter-dark.svg b/docs/assets/images/sponsors/butter-dark.svg deleted file mode 100644 index 657b75c50..000000000 --- a/docs/assets/images/sponsors/butter-dark.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/assets/images/sponsors/butter-light.svg b/docs/assets/images/sponsors/butter-light.svg deleted file mode 100644 index a0697df08..000000000 --- a/docs/assets/images/sponsors/butter-light.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/assets/images/sponsors/cloudcannon-blue.svg b/docs/assets/images/sponsors/cloudcannon-blue.svg deleted file mode 100644 index 79b13f431..000000000 --- a/docs/assets/images/sponsors/cloudcannon-blue.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/docs/assets/images/sponsors/cloudcannon-white.svg b/docs/assets/images/sponsors/cloudcannon-white.svg deleted file mode 100644 index 83e319a6d..000000000 --- a/docs/assets/images/sponsors/cloudcannon-white.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/docs/assets/images/sponsors/esolia-logo.svg b/docs/assets/images/sponsors/esolia-logo.svg deleted file mode 100644 index 3f5344c61..000000000 --- a/docs/assets/images/sponsors/esolia-logo.svg +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/assets/images/sponsors/goland.svg b/docs/assets/images/sponsors/goland.svg deleted file mode 100644 index c32f25d7f..000000000 --- a/docs/assets/images/sponsors/goland.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/docs/assets/images/sponsors/graitykit-dark.svg b/docs/assets/images/sponsors/graitykit-dark.svg deleted file mode 100644 index fd7d12f5c..000000000 --- a/docs/assets/images/sponsors/graitykit-dark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/docs/assets/images/sponsors/linode-logo.svg b/docs/assets/images/sponsors/linode-logo.svg deleted file mode 100644 index 873678398..000000000 --- a/docs/assets/images/sponsors/linode-logo.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docs/assets/images/sponsors/linode-logo_standard_light_medium.png b/docs/assets/images/sponsors/linode-logo_standard_light_medium.png deleted file mode 100644 index 269e6af84..000000000 Binary files a/docs/assets/images/sponsors/linode-logo_standard_light_medium.png and /dev/null differ diff --git a/docs/assets/images/sponsors/your-company-dark.svg b/docs/assets/images/sponsors/your-company-dark.svg deleted file mode 100644 index 58fd601f5..000000000 --- a/docs/assets/images/sponsors/your-company-dark.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/docs/assets/images/sponsors/your-company.svg b/docs/assets/images/sponsors/your-company.svg deleted file mode 100644 index 3b85ece5c..000000000 --- a/docs/assets/images/sponsors/your-company.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/docs/assets/js/alpinejs/data/explorer.js b/docs/assets/js/alpinejs/data/explorer.js deleted file mode 100644 index 783db58f4..000000000 --- a/docs/assets/js/alpinejs/data/explorer.js +++ /dev/null @@ -1,123 +0,0 @@ -var debug = 0 ? console.log.bind(console, '[explorer]') : function () {}; - -// This is currently not used, but kept in case I change my mind. -export const explorer = (Alpine) => ({ - uiState: { - containerScrollTop: -1, - lastActiveRef: '', - }, - treeState: { - // The href of the current page. - currentNode: '', - // The state of each node in the tree. - nodes: {}, - - // We currently only list the sections, not regular pages, in the side bar. - // This strikes me as the right balance. The pages gets listed on the section pages. - // This array is sorted by length, so we can find the longest prefix of the current page - // without having to iterate over all the keys. - nodeRefsByLength: [], - }, - async init() { - let keys = Reflect.ownKeys(this.$refs); - for (let key of keys) { - let n = { - open: false, - active: false, - }; - this.treeState.nodes[key] = n; - this.treeState.nodeRefsByLength.push(key); - } - - this.treeState.nodeRefsByLength.sort((a, b) => b.length - a.length); - - this.setCurrentActive(); - }, - - longestPrefix(ref) { - let longestPrefix = ''; - for (let key of this.treeState.nodeRefsByLength) { - if (ref.startsWith(key)) { - longestPrefix = key; - break; - } - } - return longestPrefix; - }, - - setCurrentActive() { - let ref = this.longestPrefix(window.location.pathname); - let activeChanged = this.uiState.lastActiveRef !== ref; - debug('setCurrentActive', this.uiState.lastActiveRef, window.location.pathname, '=>', ref, activeChanged); - this.uiState.lastActiveRef = ref; - if (this.uiState.containerScrollTop === -1 && activeChanged) { - // Navigation outside of the explorer menu. - let el = document.querySelector(`[x-ref="${ref}"]`); - if (el) { - this.$nextTick(() => { - debug('scrolling to', ref); - el.scrollIntoView({ behavior: 'smooth', block: 'center' }); - }); - } - } - this.treeState.currentNode = ref; - for (let key in this.treeState.nodes) { - let n = this.treeState.nodes[key]; - n.active = false; - n.open = ref == key || ref.startsWith(key); - if (n.open) { - debug('open', key); - } - } - - let n = this.treeState.nodes[this.longestPrefix(ref)]; - if (n) { - n.active = true; - } - }, - - getScrollingContainer() { - return document.getElementById('leftsidebar'); - }, - - onLoad() { - debug('onLoad', this.uiState.containerScrollTop); - if (this.uiState.containerScrollTop >= 0) { - debug('onLoad: scrolling to', this.uiState.containerScrollTop); - this.getScrollingContainer().scrollTo(0, this.uiState.containerScrollTop); - } - this.uiState.containerScrollTop = -1; - }, - - onBeforeRender() { - debug('onBeforeRender', this.uiState.containerScrollTop); - this.setCurrentActive(); - }, - - toggleNode(ref) { - this.uiState.containerScrollTop = this.getScrollingContainer().scrollTop; - this.uiState.lastActiveRef = ''; - debug('toggleNode', ref, this.uiState.containerScrollTop); - - let node = this.treeState.nodes[ref]; - if (!node) { - debug('node not found', ref); - return; - } - let wasOpen = node.open; - }, - - isCurrent(ref) { - let n = this.treeState.nodes[ref]; - return n && n.active; - }, - - isOpen(ref) { - let node = this.treeState.nodes[ref]; - if (!node) return false; - if (node.open) { - debug('isOpen', ref); - } - return node.open; - }, -}); diff --git a/docs/assets/js/alpinejs/data/index.js b/docs/assets/js/alpinejs/data/index.js deleted file mode 100644 index 7bf0532e3..000000000 --- a/docs/assets/js/alpinejs/data/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export * from './navbar'; -export * from './search'; -export * from './toc'; diff --git a/docs/assets/js/alpinejs/data/navbar.js b/docs/assets/js/alpinejs/data/navbar.js deleted file mode 100644 index 1075f3f29..000000000 --- a/docs/assets/js/alpinejs/data/navbar.js +++ /dev/null @@ -1,28 +0,0 @@ -export const navbar = (Alpine) => ({ - init: function () { - Alpine.bind(this.$root, this.root); - - return this.$nextTick(() => { - let contentEl = document.querySelector('.content:not(.content--ready)'); - if (contentEl) { - contentEl.classList.add('content--ready'); - let anchorTemplate = document.getElementById('anchor-heading'); - if (anchorTemplate) { - let els = contentEl.querySelectorAll('h2[id], h3[id], h4[id], h5[id], h6[id], dt[id]'); - for (let i = 0; i < els.length; i++) { - let el = els[i]; - el.classList.add('group'); - let a = anchorTemplate.content.cloneNode(true).firstElementChild; - a.href = '#' + el.id; - el.appendChild(a); - } - } - } - }); - }, - root: { - ['@scroll.window.debounce.10ms'](event) { - this.$store.nav.scroll.atTop = window.scrollY < 40 ? true : false; - }, - }, -}); diff --git a/docs/assets/js/alpinejs/data/search.js b/docs/assets/js/alpinejs/data/search.js deleted file mode 100644 index c633799a1..000000000 --- a/docs/assets/js/alpinejs/data/search.js +++ /dev/null @@ -1,119 +0,0 @@ -import { LRUCache } from '../../helpers'; - -const designMode = false; - -const groupByLvl0 = (array) => { - if (!array) return []; - return array.reduce((result, currentValue) => { - (result[currentValue.hierarchy.lvl0] = result[currentValue.hierarchy.lvl0] || []).push(currentValue); - return result; - }, {}); -}; - -const applyHelperFuncs = (array) => { - if (!array) return []; - return array.map((item) => { - item.getHeadingHTML = function () { - let lvl2 = this._highlightResult.hierarchy.lvl2; - let lvl3 = this._highlightResult.hierarchy.lvl3; - - if (!lvl3) { - if (lvl2) { - return lvl2.value; - } - return ''; - } - - if (!lvl2) { - return lvl3.value; - } - - return `${lvl2.value}  >  ${lvl3.value}`; - }; - return item; - }); -}; - -export const search = (Alpine, cfg) => ({ - query: designMode ? 'apac' : '', - open: designMode, - result: {}, - cache: new LRUCache(10), // Small cache, avoids network requests on e.g. backspace. - init() { - Alpine.bind(this.$root, this.root); - - this.checkOpen(); - return this.$nextTick(() => { - this.$watch('query', () => { - this.search(); - }); - }); - }, - toggleOpen: function () { - this.open = !this.open; - this.checkOpen(); - }, - checkOpen: function () { - if (!this.open) { - return; - } - this.search(); - this.$nextTick(() => { - this.$refs.input.focus(); - }); - }, - - search: function () { - if (!this.query) { - this.result = {}; - return; - } - - // Check cache first. - const cached = this.cache.get(this.query); - if (cached) { - this.result = cached; - return; - } - var queries = { - requests: [ - { - indexName: cfg.index, - params: `query=${encodeURIComponent(this.query)}`, - attributesToHighlight: ['hierarchy', 'content'], - attributesToRetrieve: ['hierarchy', 'url', 'content'], - }, - ], - }; - - const host = `https://${cfg.app_id}-dsn.algolia.net`; - const url = `${host}/1/indexes/*/queries`; - - fetch(url, { - method: 'POST', - headers: { - 'X-Algolia-Application-Id': cfg.app_id, - 'X-Algolia-API-Key': cfg.api_key, - }, - body: JSON.stringify(queries), - }) - .then((response) => response.json()) - .then((data) => { - this.result = groupByLvl0(applyHelperFuncs(data.results[0].hits)); - this.cache.put(this.query, this.result); - }); - }, - root: { - ['@click']() { - if (!this.open) { - this.toggleOpen(); - } - }, - ['@search-toggle.window']() { - this.toggleOpen(); - }, - ['@keydown.slash.window.prevent']() { - this.toggleOpen(); - }, - }, -}); diff --git a/docs/assets/js/alpinejs/data/toc.js b/docs/assets/js/alpinejs/data/toc.js deleted file mode 100644 index 233f8777f..000000000 --- a/docs/assets/js/alpinejs/data/toc.js +++ /dev/null @@ -1,71 +0,0 @@ -var debug = 0 ? console.log.bind(console, '[toc]') : function () {}; - -export const toc = (Alpine) => ({ - contentScrollSpy: null, - activeHeading: '', - justClicked: false, - - setActive(id) { - debug('setActive', id); - this.activeHeading = id; - // Prevent the intersection observer from changing the active heading right away. - this.justClicked = true; - setTimeout(() => { - this.justClicked = false; - }, 200); - }, - - init() { - this.$watch('$store.nav.scroll.atTop', (value) => { - if (!value) return; - this.activeHeading = ''; - this.$root.scrollTop = 0; - }); - - return this.$nextTick(() => { - let contentEl = document.getElementById('article'); - if (contentEl) { - const handleIntersect = (entries) => { - if (this.justClicked) { - return; - } - for (let entry of entries) { - if (entry.isIntersecting) { - let id = entry.target.id; - this.activeHeading = id; - let liEl = this.$refs[id]; - if (liEl) { - // If liEl is not in the viewport, scroll it into view. - let bounding = liEl.getBoundingClientRect(); - if (bounding.top < 0 || bounding.bottom > window.innerHeight) { - this.$root.scrollTop = liEl.offsetTop - 100; - } - } - debug('intersecting', id); - break; - } - } - }; - - let opts = { - rootMargin: '0px 0px -75%', - threshold: 0.75, - }; - - this.contentScrollSpy = new IntersectionObserver(handleIntersect, opts); - // Observe all headings. - let headings = contentEl.querySelectorAll('h2, h3, h4, h5, h6'); - for (let heading of headings) { - this.contentScrollSpy.observe(heading); - } - } - }); - }, - - destroy() { - if (this.contentScrollSpy) { - debug('disconnecting'); - this.contentScrollSpy.disconnect(); - } - }, -}); diff --git a/docs/assets/js/alpinejs/magics/helpers.js b/docs/assets/js/alpinejs/magics/helpers.js deleted file mode 100644 index de9fa24e9..000000000 --- a/docs/assets/js/alpinejs/magics/helpers.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict'; - -export function registerMagics(Alpine) { - Alpine.magic('copy', (currentEl) => { - return function (el) { - if (!el) { - el = currentEl; - } - - // Select the element to copy. - let range = document.createRange(); - range.selectNode(el); - window.getSelection().removeAllRanges(); - window.getSelection().addRange(range); - - // Remove the selection after some time. - setTimeout(() => { - window.getSelection().removeAllRanges(); - }, 500); - - // Trim whitespace. - let text = el.textContent.trim(); - - navigator.clipboard.writeText(text); - }; - }); - - Alpine.magic('isScrollX', (currentEl) => { - return function (el) { - if (!el) { - el = currentEl; - } - return el.clientWidth < el.scrollWidth; - }; - }); -} diff --git a/docs/assets/js/alpinejs/magics/index.js b/docs/assets/js/alpinejs/magics/index.js deleted file mode 100644 index c5f595cf9..000000000 --- a/docs/assets/js/alpinejs/magics/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from './helpers'; diff --git a/docs/assets/js/alpinejs/stores/index.js b/docs/assets/js/alpinejs/stores/index.js deleted file mode 100644 index 17e2a347b..000000000 --- a/docs/assets/js/alpinejs/stores/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from './nav.js'; diff --git a/docs/assets/js/alpinejs/stores/nav.js b/docs/assets/js/alpinejs/stores/nav.js deleted file mode 100644 index 6409cd86c..000000000 --- a/docs/assets/js/alpinejs/stores/nav.js +++ /dev/null @@ -1,94 +0,0 @@ -var debug = 1 ? console.log.bind(console, '[navStore]') : function () {}; - -var ColorScheme = { - System: 1, - Light: 2, - Dark: 3, -}; - -const localStorageUserSettingsKey = 'hugoDocsUserSettings'; - -export const navStore = (Alpine) => ({ - init() { - // There is no $watch available in Alpine stores, - // but this has the same effect. - this.userSettings.onColorSchemeChanged = Alpine.effect(() => { - if (this.userSettings.settings.colorScheme) { - this.userSettings.isDark = isDark(this.userSettings.settings.colorScheme); - toggleDarkMode(this.userSettings.isDark); - } - }); - - // Also react to changes in system settings. - window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { - this.userSettings.setColorScheme(ColorScheme.System); - }); - }, - - destroy() {}, - - scroll: { - atTop: true, - }, - - userSettings: { - // settings gets persisted between page navigations. - settings: Alpine.$persist({ - // light, dark or system mode. - // If not set, we use the OS setting. - colorScheme: ColorScheme.System, - // Used to show the most relevant tab in config listings etc. - configFileType: 'toml', - }).as(localStorageUserSettingsKey), - - isDark: false, - - setColorScheme(colorScheme) { - this.settings.colorScheme = colorScheme; - this.isDark = isDark(colorScheme); - }, - - toggleColorScheme() { - let next = this.settings.colorScheme + 1; - if (next > ColorScheme.Dark) { - next = ColorScheme.System; - } - this.setColorScheme(next); - }, - colorScheme() { - return this.settings.colorScheme ? this.settings.colorScheme : ColorScheme.System; - }, - }, -}); - -function isMediaDark() { - return window.matchMedia('(prefers-color-scheme: dark)').matches; -} - -function isDark(colorScheme) { - if (!colorScheme || colorScheme == ColorScheme.System) { - return isMediaDark(); - } - - return colorScheme == ColorScheme.Dark; -} - -export function initColorScheme() { - // The AlpineJS store has not have been initialized yet, so access the - // localStorage directly. - let settingsJSON = localStorage[localStorageUserSettingsKey]; - if (settingsJSON) { - let settings = JSON.parse(settingsJSON); - toggleDarkMode(isDark(settings.colorScheme)); - return; - } - toggleDarkMode(isDark(null)); -} - -const toggleDarkMode = function (dark) { - if (dark) { - document.documentElement.classList.add('dark'); - } else { - document.documentElement.classList.remove('dark'); - } -}; diff --git a/docs/assets/js/body-start.js b/docs/assets/js/body-start.js deleted file mode 100644 index f9b596671..000000000 --- a/docs/assets/js/body-start.js +++ /dev/null @@ -1,6 +0,0 @@ -import { initColorScheme } from './alpinejs/stores/index'; - -(function () { - // This allows us to initialize the color scheme before AlpineJS etc. is loaded. - initColorScheme(); -})(); diff --git a/docs/assets/js/head-early.js b/docs/assets/js/head-early.js deleted file mode 100644 index 250bdd6cb..000000000 --- a/docs/assets/js/head-early.js +++ /dev/null @@ -1,16 +0,0 @@ -import { scrollToActive } from 'js/helpers/index'; - -(function () { - // Now we know that the browser has JS enabled. - document.documentElement.classList.remove('no-js'); - - // Add os-macos class to body if user is using macOS. - if (navigator.userAgent.indexOf('Mac') > -1) { - document.documentElement.classList.add('os-macos'); - } - - // Wait for the DOM to be ready. - document.addEventListener('DOMContentLoaded', function () { - scrollToActive('DOMContentLoaded'); - }); -})(); diff --git a/docs/assets/js/helpers/bridgeTurboAndAlpine.js b/docs/assets/js/helpers/bridgeTurboAndAlpine.js deleted file mode 100644 index 0494d02f2..000000000 --- a/docs/assets/js/helpers/bridgeTurboAndAlpine.js +++ /dev/null @@ -1,67 +0,0 @@ -export function bridgeTurboAndAlpine(Alpine) { - document.addEventListener('turbo:before-render', (event) => { - event.detail.newBody.querySelectorAll('[data-alpine-generated]').forEach((el) => { - if (el.hasAttribute('data-alpine-generated')) { - el.removeAttribute('data-alpine-generated'); - el.remove(); - } - }); - }); - - document.addEventListener('turbo:render', () => { - if (document.documentElement.hasAttribute('data-turbo-preview')) { - return; - } - - document.querySelectorAll('[data-alpine-ignored]').forEach((el) => { - el.removeAttribute('x-ignore'); - el.removeAttribute('data-alpine-ignored'); - }); - - document.body.querySelectorAll('[x-data]').forEach((el) => { - if (el.hasAttribute('data-turbo-permanent')) { - return; - } - Alpine.initTree(el); - }); - - Alpine.startObservingMutations(); - }); - - // Cleanup Alpine state on navigation. - document.addEventListener('turbo:before-cache', () => { - // This will be restarted in turbo:render. - Alpine.stopObservingMutations(); - - document.body.querySelectorAll('[data-turbo-permanent]').forEach((el) => { - if (!el.hasAttribute('x-ignore')) { - el.setAttribute('x-ignore', true); - el.setAttribute('data-alpine-ignored', true); - } - }); - - document.body.querySelectorAll('[x-for],[x-if],[x-teleport]').forEach((el) => { - if (el.hasAttribute('x-for') && el._x_lookup) { - Object.values(el._x_lookup).forEach((el) => el.setAttribute('data-alpine-generated', true)); - } - - if (el.hasAttribute('x-if') && el._x_currentIfEl) { - el._x_currentIfEl.setAttribute('data-alpine-generated', true); - } - - if (el.hasAttribute('x-teleport') && el._x_teleport) { - el._x_teleport.setAttribute('data-alpine-generated', true); - } - }); - - document.body.querySelectorAll('[x-data]').forEach((el) => { - if (!el.hasAttribute('data-turbo-permanent')) { - Alpine.destroyTree(el); - // Turbo leaks DOM elements via their data-turbo-permanent handling. - // That needs to be fixed upstream, but until then. - let clone = el.cloneNode(true); - el.replaceWith(clone); - } - }); - }); -} diff --git a/docs/assets/js/helpers/helpers.js b/docs/assets/js/helpers/helpers.js deleted file mode 100644 index 818eac40c..000000000 --- a/docs/assets/js/helpers/helpers.js +++ /dev/null @@ -1,17 +0,0 @@ -export const scrollToActive = (when) => { - let els = document.querySelectorAll('.scroll-active'); - if (!els.length) { - return; - } - els.forEach((el) => { - // Find scrolling container. - let container = el.closest('[data-turbo-preserve-scroll-container]'); - if (container) { - // Avoid scrolling if el is already in view. - if (el.offsetTop >= container.scrollTop && el.offsetTop <= container.scrollTop + container.clientHeight) { - return; - } - container.scrollTop = el.offsetTop - container.offsetTop; - } - }); -}; diff --git a/docs/assets/js/helpers/index.js b/docs/assets/js/helpers/index.js deleted file mode 100644 index 41ffa3c39..000000000 --- a/docs/assets/js/helpers/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export * from './bridgeTurboAndAlpine'; -export * from './helpers'; -export * from './lrucache'; diff --git a/docs/assets/js/helpers/lrucache.js b/docs/assets/js/helpers/lrucache.js deleted file mode 100644 index 258848c95..000000000 --- a/docs/assets/js/helpers/lrucache.js +++ /dev/null @@ -1,19 +0,0 @@ -// A simple LRU cache implementation backed by a map. -export class LRUCache { - constructor(maxSize) { - this.maxSize = maxSize; - this.cache = new Map(); - } - - get(key) { - return this.cache.get(key); - } - - put(key, value) { - if (this.cache.size >= this.maxSize) { - const firstKey = this.cache.keys().next().value; - this.cache.delete(firstKey); - } - this.cache.set(key, value); - } -} diff --git a/docs/assets/js/main.js b/docs/assets/js/main.js deleted file mode 100644 index 14440044b..000000000 --- a/docs/assets/js/main.js +++ /dev/null @@ -1,89 +0,0 @@ -import Alpine from 'alpinejs'; -import { registerMagics } from './alpinejs/magics/index'; -import { navbar, search, toc } from './alpinejs/data/index'; -import { navStore, initColorScheme } from './alpinejs/stores/index'; -import { bridgeTurboAndAlpine } from './helpers/index'; -import persist from '@alpinejs/persist'; -import focus from '@alpinejs/focus'; - -var debug = 0 ? console.log.bind(console, '[index]') : function () {}; - -// Turbolinks init. -(function () { - document.addEventListener('turbo:render', function (e) { - // This is also called right after the body start. This is added to prevent flicker on navigation. - initColorScheme(); - }); -})(); - -// Set up and start Alpine. -(function () { - // Register AlpineJS plugins. - { - Alpine.plugin(focus); - Alpine.plugin(persist); - } - // Register AlpineJS magics and directives. - { - // Handles copy to clipboard etc. - registerMagics(Alpine); - } - - // Register AlpineJS controllers. - { - // Register AlpineJS data controllers. - let searchConfig = { - index: 'hugodocs', - app_id: 'D1BPLZHGYQ', - api_key: '6df94e1e5d55d258c56f60d974d10314', - }; - - Alpine.data('navbar', () => navbar(Alpine)); - Alpine.data('search', () => search(Alpine, searchConfig)); - Alpine.data('toc', () => toc(Alpine)); - } - - // Register AlpineJS stores. - { - Alpine.store('nav', navStore(Alpine)); - } - - // Start AlpineJS. - Alpine.start(); - - // Start the Turbo-Alpine bridge. - bridgeTurboAndAlpine(Alpine); - - { - let containerScrollTops = {}; - - // To preserve scroll position in scrolling elements on navigation add data-turbo-preserve-scroll-container="somename" to the scrolling container. - addEventListener('turbo:click', () => { - document.querySelectorAll('[data-turbo-preserve-scroll-container]').forEach((el2) => { - containerScrollTops[el2.dataset.turboPreserveScrollContainer] = el2.scrollTop; - }); - }); - - addEventListener('turbo:render', () => { - document.querySelectorAll('[data-turbo-preserve-scroll-container]').forEach((ele) => { - const containerScrollTop = containerScrollTops[ele.dataset.turboPreserveScrollContainer]; - if (containerScrollTop) { - ele.scrollTop = containerScrollTop; - } else { - let els = ele.querySelectorAll('.scroll-active'); - if (els.length) { - els.forEach((el) => { - // Avoid scrolling if el is already in view. - if (el.offsetTop >= ele.scrollTop && el.offsetTop <= ele.scrollTop + ele.clientHeight) { - return; - } - ele.scrollTop = el.offsetTop - ele.offsetTop; - }); - } - } - }); - - containerScrollTops = {}; - }); - } -})(); diff --git a/docs/assets/js/turbo.js b/docs/assets/js/turbo.js deleted file mode 100644 index c007896f6..000000000 --- a/docs/assets/js/turbo.js +++ /dev/null @@ -1 +0,0 @@ -import * as Turbo from '@hotwired/turbo'; diff --git a/docs/assets/jsconfig.json b/docs/assets/jsconfig.json deleted file mode 100644 index 377218ccb..000000000 --- a/docs/assets/jsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "baseUrl": ".", - "paths": { - "*": [ - "*" - ] - } - } -} \ No newline at end of file diff --git a/docs/assets/opengraph/gohugoio-card-base-1.png b/docs/assets/opengraph/gohugoio-card-base-1.png deleted file mode 100644 index 65555845b..000000000 Binary files a/docs/assets/opengraph/gohugoio-card-base-1.png and /dev/null differ diff --git a/docs/assets/opengraph/mulish-black.ttf b/docs/assets/opengraph/mulish-black.ttf deleted file mode 100644 index db680a088..000000000 Binary files a/docs/assets/opengraph/mulish-black.ttf and /dev/null differ diff --git a/docs/config.toml b/docs/config.toml new file mode 100644 index 000000000..4115e601a --- /dev/null +++ b/docs/config.toml @@ -0,0 +1,135 @@ +title = "Hugo: A Fast and Flexible Website Generator" +baseurl = "http://gohugo.io/" +MetaDataFormat = "yaml" +pluralizeListTitles = false +# We do redirects via Netlify's _redirects file, generated by Hugo (see "outputs" below). +disableAliases = true + +[blackfriday] + plainIDAnchors = true + +[outputs] +home = [ "HTML", "RSS", "REDIR" ] + +[mediaTypes] +[mediaTypes."text/netlify"] +suffix = "" +delimiter = "" + +[outputFormats] +[outputFormats.REDIR] +mediatype = "text/netlify" +baseName = "_redirects" +isPlainText = true +notAlternative = true + +[params] + description = "Documentation of Hugo, a fast and flexible static site generator built with love by spf13, bep and friends in Go" + author = "Steve Francia (spf13) and friends" + release = "0.25" + +[taxonomies] + tag = "tags" + group = "groups" + +[[menu.main]] + name = "Download Hugo" + pre = "" + url = "https://github.com/gohugoio/hugo/releases" + weight = -200 +[[menu.main]] + name = "Site Showcase" + pre = "" + url = "/showcase/" + weight = -180 +[[menu.main]] + name = "Theme Showcase" + pre = "" + url = "http://themes.gohugo.io" + weight = -170 +[[menu.main]] + name = "Press & Articles" + pre = "" + url = "/community/press/" + weight = -160 +[[menu.main]] + name = "Discuss Hugo" + pre = "" + url = "https://discourse.gohugo.io/" + weight = -150 +[[menu.main]] + name = "About Hugo" + identifier = "about" + pre = "" + weight = -110 +[[menu.main]] + name = "Release Notes" + url = "/release-notes/" + pre = "" + weight = -111 +[[menu.main]] + name = "Getting Started" + identifier = "getting started" + pre = "" + weight = -100 +[[menu.main]] + name = "Content" + identifier = "content" + pre = "" + weight = -90 +[[menu.main]] + name = "Themes" + identifier = "themes" + pre = "" + weight = -85 +[[menu.main]] + parent = "themes" + name = "Theme Showcase" + url = "http://themes.gohugo.io" + weight = -170 +[[menu.main]] + name = "Templates" + identifier = "layout" + pre = "" + weight = -80 +[[menu.main]] + name = "Taxonomies" + identifier = "taxonomy" + pre = "" + weight = -70 +[[menu.main]] + name = "Extras" + identifier = "extras" + pre = "" + weight = -60 +[[menu.main]] + name = "Community" + identifier = "community" + pre = "" + weight = -50 +[[menu.main]] + parent = "community" + name = "Discussion Forum" + url = "https://discourse.gohugo.io/" + weight = 150 +[[menu.main]] + name = "Tutorials" + identifier = "tutorials" + pre = "" + weight = -40 +[[menu.main]] + name = "Troubleshooting" + identifier = "troubleshooting" + pre = "" + weight = -30 +[[menu.main]] + name = "Tools" + url = "/tools/" + pre = "" + weight = -25 +[[menu.main]] + name = "Hugo Cmd Reference" + identifier = "commands" + pre = "" + weight = -20 + url = "/commands/" diff --git a/docs/content/LICENSE.md b/docs/content/LICENSE.md deleted file mode 100644 index b09cd7856..000000000 --- a/docs/content/LICENSE.md +++ /dev/null @@ -1,201 +0,0 @@ -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - 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. diff --git a/docs/content/commands/hugo.md b/docs/content/commands/hugo.md new file mode 100644 index 000000000..a88861e5e --- /dev/null +++ b/docs/content/commands/hugo.md @@ -0,0 +1,80 @@ +--- +date: 2017-07-06T10:34:39+02:00 +title: "hugo" +slug: hugo +url: /commands/hugo/ +--- +## hugo + +hugo builds your site + +### Synopsis + + +hugo 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 http://gohugo.io/. + +``` +hugo [flags] +``` + +### Options + +``` + -b, --baseURL string hostname (and path) to the root, e.g. http://spf13.com/ + -D, --buildDrafts include content marked as draft + -E, --buildExpired include expired content + -F, --buildFuture include content with publishdate in the future + --cacheDir string filesystem path to cache directory. Defaults: $TMPDIR/hugo_cache/ + --canonifyURLs if true, all relative URLs will be canonicalized using baseURL + --cleanDestinationDir remove files from destination not found in static directories + --config string config file (default is path/config.yaml|json|toml) + -c, --contentDir string filesystem path to content directory + -d, --destination string filesystem path to write files to + --disable404 do not render 404 page + --disableKinds stringSlice disable different kind of pages (home, RSS etc.) + --disableRSS do not build RSS files + --disableSitemap do not build Sitemap file + --enableGitInfo add Git revision, date and author info to the pages + --forceSyncStatic copy all files when static is changed. + -h, --help help for hugo + --i18n-warnings print missing translations + --ignoreCache ignores the cache directory + -l, --layoutDir string filesystem path to layout directory + --log enable Logging + --logFile string log File path (if set, logging enabled automatically) + --noChmod don't sync permission mode of files + --noTimes don't sync modification time of files + --pluralizeListTitles pluralize titles in lists using inflect (default true) + --preserveTaxonomyNames preserve taxonomy names as written ("Gérard Depardieu" vs "gerard-depardieu") + --quiet build in quiet mode + --renderToMemory render to memory (only useful for benchmark testing) + -s, --source string filesystem path to read files relative from + --stepAnalysis display memory and timing of different steps of the program + -t, --theme string theme to use (located in /themes/THEMENAME/) + --themesDir string filesystem path to themes directory + --uglyURLs if true, use /filename.html instead of /filename/ + -v, --verbose verbose output + --verboseLog verbose logging + -w, --watch watch filesystem for changes and recreate as needed +``` + +### SEE ALSO +* [hugo benchmark](/commands/hugo_benchmark/) - Benchmark Hugo by building a site a number of times. +* [hugo check](/commands/hugo_check/) - Contains some verification checks +* [hugo config](/commands/hugo_config/) - Print the site configuration +* [hugo convert](/commands/hugo_convert/) - Convert your content to different formats +* [hugo env](/commands/hugo_env/) - Print Hugo version and environment info +* [hugo gen](/commands/hugo_gen/) - A collection of several useful generators. +* [hugo import](/commands/hugo_import/) - Import your site from others. +* [hugo list](/commands/hugo_list/) - Listing out various types of content +* [hugo new](/commands/hugo_new/) - Create new content for your site +* [hugo server](/commands/hugo_server/) - A high performance webserver +* [hugo undraft](/commands/hugo_undraft/) - Undraft resets the content's draft status +* [hugo version](/commands/hugo_version/) - Print the version number of Hugo + +###### Auto generated by spf13/cobra on 6-Jul-2017 diff --git a/docs/content/commands/hugo_benchmark.md b/docs/content/commands/hugo_benchmark.md new file mode 100644 index 000000000..a3e4dcc68 --- /dev/null +++ b/docs/content/commands/hugo_benchmark.md @@ -0,0 +1,72 @@ +--- +date: 2017-07-06T10:34:39+02:00 +title: "hugo benchmark" +slug: hugo_benchmark +url: /commands/hugo_benchmark/ +--- +## hugo benchmark + +Benchmark Hugo by building a site a number of times. + +### Synopsis + + +Hugo can build a site many times over and analyze the running process +creating a benchmark. + +``` +hugo benchmark [flags] +``` + +### Options + +``` + -b, --baseURL string hostname (and path) to the root, e.g. http://spf13.com/ + -D, --buildDrafts include content marked as draft + -E, --buildExpired include expired content + -F, --buildFuture include content with publishdate in the future + --cacheDir string filesystem path to cache directory. Defaults: $TMPDIR/hugo_cache/ + --canonifyURLs if true, all relative URLs will be canonicalized using baseURL + --cleanDestinationDir remove files from destination not found in static directories + -c, --contentDir string filesystem path to content directory + -n, --count int number of times to build the site (default 13) + --cpuprofile string path/filename for the CPU profile file + -d, --destination string filesystem path to write files to + --disable404 do not render 404 page + --disableKinds stringSlice disable different kind of pages (home, RSS etc.) + --disableRSS do not build RSS files + --disableSitemap do not build Sitemap file + --enableGitInfo add Git revision, date and author info to the pages + --forceSyncStatic copy all files when static is changed. + -h, --help help for benchmark + --i18n-warnings print missing translations + --ignoreCache ignores the cache directory + -l, --layoutDir string filesystem path to layout directory + --memprofile string path/filename for the memory profile file + --noChmod don't sync permission mode of files + --noTimes don't sync modification time of files + --pluralizeListTitles pluralize titles in lists using inflect (default true) + --preserveTaxonomyNames preserve taxonomy names as written ("Gérard Depardieu" vs "gerard-depardieu") + --renderToMemory render to memory (only useful for benchmark testing) + -s, --source string filesystem path to read files relative from + --stepAnalysis display memory and timing of different steps of the program + -t, --theme string theme to use (located in /themes/THEMENAME/) + --themesDir string filesystem path to themes directory + --uglyURLs if true, use /filename.html instead of /filename/ +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --log enable Logging + --logFile string log File path (if set, logging enabled automatically) + --quiet build in quiet mode + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO +* [hugo](/commands/hugo/) - hugo builds your site + +###### Auto generated by spf13/cobra on 6-Jul-2017 diff --git a/docs/content/commands/hugo_check.md b/docs/content/commands/hugo_check.md new file mode 100644 index 000000000..17b57c1c7 --- /dev/null +++ b/docs/content/commands/hugo_check.md @@ -0,0 +1,37 @@ +--- +date: 2017-07-06T10:34:39+02:00 +title: "hugo check" +slug: hugo_check +url: /commands/hugo_check/ +--- +## hugo check + +Contains some verification checks + +### Synopsis + + +Contains some verification checks + +### Options + +``` + -h, --help help for check +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --log enable Logging + --logFile string log File path (if set, logging enabled automatically) + --quiet build in quiet mode + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO +* [hugo](/commands/hugo/) - hugo builds your site +* [hugo check ulimit](/commands/hugo_check_ulimit/) - Check system ulimit settings + +###### Auto generated by spf13/cobra on 6-Jul-2017 diff --git a/docs/content/commands/hugo_check_ulimit.md b/docs/content/commands/hugo_check_ulimit.md new file mode 100644 index 000000000..f9cfadaca --- /dev/null +++ b/docs/content/commands/hugo_check_ulimit.md @@ -0,0 +1,41 @@ +--- +date: 2017-07-06T10:34:39+02:00 +title: "hugo check ulimit" +slug: hugo_check_ulimit +url: /commands/hugo_check_ulimit/ +--- +## hugo check ulimit + +Check system ulimit settings + +### Synopsis + + +Hugo will inspect the current ulimit settings on the system. +This is primarily to ensure that Hugo can watch enough files on some OSs + +``` +hugo check ulimit [flags] +``` + +### Options + +``` + -h, --help help for ulimit +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --log enable Logging + --logFile string log File path (if set, logging enabled automatically) + --quiet build in quiet mode + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO +* [hugo check](/commands/hugo_check/) - Contains some verification checks + +###### Auto generated by spf13/cobra on 6-Jul-2017 diff --git a/docs/content/commands/hugo_config.md b/docs/content/commands/hugo_config.md new file mode 100644 index 000000000..c5abf9f9a --- /dev/null +++ b/docs/content/commands/hugo_config.md @@ -0,0 +1,40 @@ +--- +date: 2017-07-06T10:34:39+02:00 +title: "hugo config" +slug: hugo_config +url: /commands/hugo_config/ +--- +## hugo config + +Print the site configuration + +### Synopsis + + +Print the site configuration, both default and custom settings. + +``` +hugo config [flags] +``` + +### Options + +``` + -h, --help help for config +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --log enable Logging + --logFile string log File path (if set, logging enabled automatically) + --quiet build in quiet mode + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO +* [hugo](/commands/hugo/) - hugo builds your site + +###### Auto generated by spf13/cobra on 6-Jul-2017 diff --git a/docs/content/commands/hugo_convert.md b/docs/content/commands/hugo_convert.md new file mode 100644 index 000000000..0996888f6 --- /dev/null +++ b/docs/content/commands/hugo_convert.md @@ -0,0 +1,44 @@ +--- +date: 2017-07-06T10:34:39+02:00 +title: "hugo convert" +slug: hugo_convert +url: /commands/hugo_convert/ +--- +## hugo convert + +Convert your content to different formats + +### Synopsis + + +Convert your content (e.g. front matter) to different formats. + +See convert's subcommands toJSON, toTOML and toYAML for more information. + +### Options + +``` + -h, --help help for convert + -o, --output string filesystem path to write files to + -s, --source string filesystem path to read files relative from + --unsafe enable less safe operations, please backup first +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --log enable Logging + --logFile string log File path (if set, logging enabled automatically) + --quiet build in quiet mode + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO +* [hugo](/commands/hugo/) - hugo builds your site +* [hugo convert toJSON](/commands/hugo_convert_tojson/) - Convert front matter to JSON +* [hugo convert toTOML](/commands/hugo_convert_totoml/) - Convert front matter to TOML +* [hugo convert toYAML](/commands/hugo_convert_toyaml/) - Convert front matter to YAML + +###### Auto generated by spf13/cobra on 6-Jul-2017 diff --git a/docs/content/commands/hugo_convert_toJSON.md b/docs/content/commands/hugo_convert_toJSON.md new file mode 100644 index 000000000..7d3937540 --- /dev/null +++ b/docs/content/commands/hugo_convert_toJSON.md @@ -0,0 +1,44 @@ +--- +date: 2017-07-06T10:34:39+02:00 +title: "hugo convert toJSON" +slug: hugo_convert_toJSON +url: /commands/hugo_convert_tojson/ +--- +## hugo convert toJSON + +Convert front matter to JSON + +### Synopsis + + +toJSON converts all front matter in the content directory +to use JSON for the front matter. + +``` +hugo convert toJSON [flags] +``` + +### Options + +``` + -h, --help help for toJSON +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --log enable Logging + --logFile string log File path (if set, logging enabled automatically) + -o, --output string filesystem path to write files to + --quiet build in quiet mode + -s, --source string filesystem path to read files relative from + --unsafe enable less safe operations, please backup first + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO +* [hugo convert](/commands/hugo_convert/) - Convert your content to different formats + +###### Auto generated by spf13/cobra on 6-Jul-2017 diff --git a/docs/content/commands/hugo_convert_toTOML.md b/docs/content/commands/hugo_convert_toTOML.md new file mode 100644 index 000000000..a18595780 --- /dev/null +++ b/docs/content/commands/hugo_convert_toTOML.md @@ -0,0 +1,44 @@ +--- +date: 2017-07-06T10:34:39+02:00 +title: "hugo convert toTOML" +slug: hugo_convert_toTOML +url: /commands/hugo_convert_totoml/ +--- +## hugo convert toTOML + +Convert front matter to TOML + +### Synopsis + + +toTOML converts all front matter in the content directory +to use TOML for the front matter. + +``` +hugo convert toTOML [flags] +``` + +### Options + +``` + -h, --help help for toTOML +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --log enable Logging + --logFile string log File path (if set, logging enabled automatically) + -o, --output string filesystem path to write files to + --quiet build in quiet mode + -s, --source string filesystem path to read files relative from + --unsafe enable less safe operations, please backup first + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO +* [hugo convert](/commands/hugo_convert/) - Convert your content to different formats + +###### Auto generated by spf13/cobra on 6-Jul-2017 diff --git a/docs/content/commands/hugo_convert_toYAML.md b/docs/content/commands/hugo_convert_toYAML.md new file mode 100644 index 000000000..d49964f37 --- /dev/null +++ b/docs/content/commands/hugo_convert_toYAML.md @@ -0,0 +1,44 @@ +--- +date: 2017-07-06T10:34:39+02:00 +title: "hugo convert toYAML" +slug: hugo_convert_toYAML +url: /commands/hugo_convert_toyaml/ +--- +## hugo convert toYAML + +Convert front matter to YAML + +### Synopsis + + +toYAML converts all front matter in the content directory +to use YAML for the front matter. + +``` +hugo convert toYAML [flags] +``` + +### Options + +``` + -h, --help help for toYAML +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --log enable Logging + --logFile string log File path (if set, logging enabled automatically) + -o, --output string filesystem path to write files to + --quiet build in quiet mode + -s, --source string filesystem path to read files relative from + --unsafe enable less safe operations, please backup first + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO +* [hugo convert](/commands/hugo_convert/) - Convert your content to different formats + +###### Auto generated by spf13/cobra on 6-Jul-2017 diff --git a/docs/content/commands/hugo_env.md b/docs/content/commands/hugo_env.md new file mode 100644 index 000000000..c6bc7c7e1 --- /dev/null +++ b/docs/content/commands/hugo_env.md @@ -0,0 +1,40 @@ +--- +date: 2017-07-06T10:34:39+02:00 +title: "hugo env" +slug: hugo_env +url: /commands/hugo_env/ +--- +## hugo env + +Print Hugo version and environment info + +### Synopsis + + +Print Hugo version and environment info. This is useful in Hugo bug reports. + +``` +hugo env [flags] +``` + +### Options + +``` + -h, --help help for env +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --log enable Logging + --logFile string log File path (if set, logging enabled automatically) + --quiet build in quiet mode + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO +* [hugo](/commands/hugo/) - hugo builds your site + +###### Auto generated by spf13/cobra on 6-Jul-2017 diff --git a/docs/content/commands/hugo_gen.md b/docs/content/commands/hugo_gen.md new file mode 100644 index 000000000..7fec7a597 --- /dev/null +++ b/docs/content/commands/hugo_gen.md @@ -0,0 +1,39 @@ +--- +date: 2017-07-06T10:34:39+02:00 +title: "hugo gen" +slug: hugo_gen +url: /commands/hugo_gen/ +--- +## hugo gen + +A collection of several useful generators. + +### Synopsis + + +A collection of several useful generators. + +### Options + +``` + -h, --help help for gen +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --log enable Logging + --logFile string log File path (if set, logging enabled automatically) + --quiet build in quiet mode + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO +* [hugo](/commands/hugo/) - hugo builds your site +* [hugo gen autocomplete](/commands/hugo_gen_autocomplete/) - Generate shell autocompletion script for Hugo +* [hugo gen doc](/commands/hugo_gen_doc/) - Generate Markdown documentation for the Hugo CLI. +* [hugo gen man](/commands/hugo_gen_man/) - Generate man pages for the Hugo CLI + +###### Auto generated by spf13/cobra on 6-Jul-2017 diff --git a/docs/content/commands/hugo_gen_autocomplete.md b/docs/content/commands/hugo_gen_autocomplete.md new file mode 100644 index 000000000..e1bb01270 --- /dev/null +++ b/docs/content/commands/hugo_gen_autocomplete.md @@ -0,0 +1,58 @@ +--- +date: 2017-07-06T10:34:39+02:00 +title: "hugo gen autocomplete" +slug: hugo_gen_autocomplete +url: /commands/hugo_gen_autocomplete/ +--- +## hugo gen autocomplete + +Generate shell autocompletion script for Hugo + +### Synopsis + + +Generates a shell autocompletion script for Hugo. + +NOTE: The current version supports Bash only. + This should work for *nix systems with Bash installed. + +By default, the file is written directly to /etc/bash_completion.d +for convenience, and the command may need superuser rights, e.g.: + + $ sudo hugo gen autocomplete + +Add `--completionfile=/path/to/file` flag to set alternative +file-path and name. + +Logout and in again to reload the completion scripts, +or just source them in directly: + + $ . /etc/bash_completion + +``` +hugo gen autocomplete [flags] +``` + +### Options + +``` + --completionfile string autocompletion file (default "/etc/bash_completion.d/hugo.sh") + -h, --help help for autocomplete + --type string autocompletion type (currently only bash supported) (default "bash") +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --log enable Logging + --logFile string log File path (if set, logging enabled automatically) + --quiet build in quiet mode + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO +* [hugo gen](/commands/hugo_gen/) - A collection of several useful generators. + +###### Auto generated by spf13/cobra on 6-Jul-2017 diff --git a/docs/content/commands/hugo_gen_doc.md b/docs/content/commands/hugo_gen_doc.md new file mode 100644 index 000000000..c4442de0b --- /dev/null +++ b/docs/content/commands/hugo_gen_doc.md @@ -0,0 +1,47 @@ +--- +date: 2017-07-06T10:34:39+02:00 +title: "hugo gen doc" +slug: hugo_gen_doc +url: /commands/hugo_gen_doc/ +--- +## hugo gen doc + +Generate Markdown documentation for the Hugo CLI. + +### Synopsis + + +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 http://gohugo.io/. + +It creates one Markdown file per command with front matter suitable +for rendering in Hugo. + +``` +hugo gen doc [flags] +``` + +### Options + +``` + --dir string the directory to write the doc. (default "/tmp/hugodoc/") + -h, --help help for doc +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --log enable Logging + --logFile string log File path (if set, logging enabled automatically) + --quiet build in quiet mode + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO +* [hugo gen](/commands/hugo_gen/) - A collection of several useful generators. + +###### Auto generated by spf13/cobra on 6-Jul-2017 diff --git a/docs/content/commands/hugo_gen_man.md b/docs/content/commands/hugo_gen_man.md new file mode 100644 index 000000000..e4e6af0f7 --- /dev/null +++ b/docs/content/commands/hugo_gen_man.md @@ -0,0 +1,43 @@ +--- +date: 2017-07-06T10:34:39+02:00 +title: "hugo gen man" +slug: hugo_gen_man +url: /commands/hugo_gen_man/ +--- +## hugo gen man + +Generate man pages for the Hugo CLI + +### Synopsis + + +This command automatically generates up-to-date man pages of Hugo's +command-line interface. By default, it creates the man page files +in the "man" directory under the current directory. + +``` +hugo gen man [flags] +``` + +### Options + +``` + --dir string the directory to write the man pages. (default "man/") + -h, --help help for man +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --log enable Logging + --logFile string log File path (if set, logging enabled automatically) + --quiet build in quiet mode + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO +* [hugo gen](/commands/hugo_gen/) - A collection of several useful generators. + +###### Auto generated by spf13/cobra on 6-Jul-2017 diff --git a/docs/content/commands/hugo_import.md b/docs/content/commands/hugo_import.md new file mode 100644 index 000000000..b5140760a --- /dev/null +++ b/docs/content/commands/hugo_import.md @@ -0,0 +1,39 @@ +--- +date: 2017-07-06T10:34:39+02:00 +title: "hugo import" +slug: hugo_import +url: /commands/hugo_import/ +--- +## hugo import + +Import your site from others. + +### Synopsis + + +Import your site from other web site generators like Jekyll. + +Import requires a subcommand, e.g. `hugo import jekyll jekyll_root_path target_path`. + +### Options + +``` + -h, --help help for import +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --log enable Logging + --logFile string log File path (if set, logging enabled automatically) + --quiet build in quiet mode + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO +* [hugo](/commands/hugo/) - hugo builds your site +* [hugo import jekyll](/commands/hugo_import_jekyll/) - hugo import from Jekyll + +###### Auto generated by spf13/cobra on 6-Jul-2017 diff --git a/docs/content/commands/hugo_import_jekyll.md b/docs/content/commands/hugo_import_jekyll.md new file mode 100644 index 000000000..cbdd01824 --- /dev/null +++ b/docs/content/commands/hugo_import_jekyll.md @@ -0,0 +1,43 @@ +--- +date: 2017-07-06T10:34:39+02:00 +title: "hugo import jekyll" +slug: hugo_import_jekyll +url: /commands/hugo_import_jekyll/ +--- +## hugo import jekyll + +hugo import from Jekyll + +### Synopsis + + +hugo import from Jekyll. + +Import from Jekyll requires two paths, e.g. `hugo import jekyll jekyll_root_path target_path`. + +``` +hugo import jekyll [flags] +``` + +### Options + +``` + --force allow import into non-empty target directory + -h, --help help for jekyll +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --log enable Logging + --logFile string log File path (if set, logging enabled automatically) + --quiet build in quiet mode + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO +* [hugo import](/commands/hugo_import/) - Import your site from others. + +###### Auto generated by spf13/cobra on 6-Jul-2017 diff --git a/docs/content/commands/hugo_list.md b/docs/content/commands/hugo_list.md new file mode 100644 index 000000000..902cfc38b --- /dev/null +++ b/docs/content/commands/hugo_list.md @@ -0,0 +1,42 @@ +--- +date: 2017-07-06T10:34:39+02:00 +title: "hugo list" +slug: hugo_list +url: /commands/hugo_list/ +--- +## hugo list + +Listing out various types of content + +### Synopsis + + +Listing out various types of content. + +List requires a subcommand, e.g. `hugo list drafts`. + +### Options + +``` + -h, --help help for list + -s, --source string filesystem path to read files relative from +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --log enable Logging + --logFile string log File path (if set, logging enabled automatically) + --quiet build in quiet mode + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO +* [hugo](/commands/hugo/) - hugo builds your site +* [hugo list drafts](/commands/hugo_list_drafts/) - List all drafts +* [hugo list expired](/commands/hugo_list_expired/) - List all posts already expired +* [hugo list future](/commands/hugo_list_future/) - List all posts dated in the future + +###### Auto generated by spf13/cobra on 6-Jul-2017 diff --git a/docs/content/commands/hugo_list_drafts.md b/docs/content/commands/hugo_list_drafts.md new file mode 100644 index 000000000..4e1de8460 --- /dev/null +++ b/docs/content/commands/hugo_list_drafts.md @@ -0,0 +1,41 @@ +--- +date: 2017-07-06T10:34:39+02:00 +title: "hugo list drafts" +slug: hugo_list_drafts +url: /commands/hugo_list_drafts/ +--- +## hugo list drafts + +List all drafts + +### Synopsis + + +List all of the drafts in your content directory. + +``` +hugo list drafts [flags] +``` + +### Options + +``` + -h, --help help for drafts +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --log enable Logging + --logFile string log File path (if set, logging enabled automatically) + --quiet build in quiet mode + -s, --source string filesystem path to read files relative from + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO +* [hugo list](/commands/hugo_list/) - Listing out various types of content + +###### Auto generated by spf13/cobra on 6-Jul-2017 diff --git a/docs/content/commands/hugo_list_expired.md b/docs/content/commands/hugo_list_expired.md new file mode 100644 index 000000000..7e48d61b4 --- /dev/null +++ b/docs/content/commands/hugo_list_expired.md @@ -0,0 +1,42 @@ +--- +date: 2017-07-06T10:34:39+02:00 +title: "hugo list expired" +slug: hugo_list_expired +url: /commands/hugo_list_expired/ +--- +## hugo list expired + +List all posts already expired + +### Synopsis + + +List all of the posts in your content directory which has already +expired. + +``` +hugo list expired [flags] +``` + +### Options + +``` + -h, --help help for expired +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --log enable Logging + --logFile string log File path (if set, logging enabled automatically) + --quiet build in quiet mode + -s, --source string filesystem path to read files relative from + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO +* [hugo list](/commands/hugo_list/) - Listing out various types of content + +###### Auto generated by spf13/cobra on 6-Jul-2017 diff --git a/docs/content/commands/hugo_list_future.md b/docs/content/commands/hugo_list_future.md new file mode 100644 index 000000000..e1358b0fc --- /dev/null +++ b/docs/content/commands/hugo_list_future.md @@ -0,0 +1,42 @@ +--- +date: 2017-07-06T10:34:39+02:00 +title: "hugo list future" +slug: hugo_list_future +url: /commands/hugo_list_future/ +--- +## hugo list future + +List all posts dated in the future + +### Synopsis + + +List all of the posts in your content directory which will be +posted in the future. + +``` +hugo list future [flags] +``` + +### Options + +``` + -h, --help help for future +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --log enable Logging + --logFile string log File path (if set, logging enabled automatically) + --quiet build in quiet mode + -s, --source string filesystem path to read files relative from + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO +* [hugo list](/commands/hugo_list/) - Listing out various types of content + +###### Auto generated by spf13/cobra on 6-Jul-2017 diff --git a/docs/content/commands/hugo_new.md b/docs/content/commands/hugo_new.md new file mode 100644 index 000000000..8e6668eca --- /dev/null +++ b/docs/content/commands/hugo_new.md @@ -0,0 +1,50 @@ +--- +date: 2017-07-06T10:34:39+02:00 +title: "hugo new" +slug: hugo_new +url: /commands/hugo_new/ +--- +## hugo new + +Create new content for your site + +### Synopsis + + +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. + +``` +hugo new [path] [flags] +``` + +### Options + +``` + --editor string edit new content with this editor, if provided + -h, --help help for new + -k, --kind string content type to create + -s, --source string filesystem path to read files relative from +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --log enable Logging + --logFile string log File path (if set, logging enabled automatically) + --quiet build in quiet mode + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO +* [hugo](/commands/hugo/) - hugo builds your site +* [hugo new site](/commands/hugo_new_site/) - Create a new site (skeleton) +* [hugo new theme](/commands/hugo_new_theme/) - Create a new theme + +###### Auto generated by spf13/cobra on 6-Jul-2017 diff --git a/docs/content/commands/hugo_new_site.md b/docs/content/commands/hugo_new_site.md new file mode 100644 index 000000000..af055195e --- /dev/null +++ b/docs/content/commands/hugo_new_site.md @@ -0,0 +1,45 @@ +--- +date: 2017-07-06T10:34:39+02:00 +title: "hugo new site" +slug: hugo_new_site +url: /commands/hugo_new_site/ +--- +## hugo new site + +Create a new site (skeleton) + +### Synopsis + + +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. + +``` +hugo new site [path] [flags] +``` + +### Options + +``` + --force init inside non-empty directory + -f, --format string config & frontmatter format (default "toml") + -h, --help help for site +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --log enable Logging + --logFile string log File path (if set, logging enabled automatically) + --quiet build in quiet mode + -s, --source string filesystem path to read files relative from + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO +* [hugo new](/commands/hugo_new/) - Create new content for your site + +###### Auto generated by spf13/cobra on 6-Jul-2017 diff --git a/docs/content/commands/hugo_new_theme.md b/docs/content/commands/hugo_new_theme.md new file mode 100644 index 000000000..94a595176 --- /dev/null +++ b/docs/content/commands/hugo_new_theme.md @@ -0,0 +1,44 @@ +--- +date: 2017-07-06T10:34:39+02:00 +title: "hugo new theme" +slug: hugo_new_theme +url: /commands/hugo_new_theme/ +--- +## hugo new theme + +Create a new theme + +### Synopsis + + +Create a new theme (skeleton) called [name] in the current directory. +New theme is a skeleton. Please add content to the touched files. Add your +name to the copyright line in the license and adjust the theme.toml file +as you see fit. + +``` +hugo new theme [name] [flags] +``` + +### Options + +``` + -h, --help help for theme +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --log enable Logging + --logFile string log File path (if set, logging enabled automatically) + --quiet build in quiet mode + -s, --source string filesystem path to read files relative from + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO +* [hugo new](/commands/hugo_new/) - Create new content for your site + +###### Auto generated by spf13/cobra on 6-Jul-2017 diff --git a/docs/content/commands/hugo_server.md b/docs/content/commands/hugo_server.md new file mode 100644 index 000000000..71e8ff6d5 --- /dev/null +++ b/docs/content/commands/hugo_server.md @@ -0,0 +1,87 @@ +--- +date: 2017-07-06T10:34:39+02:00 +title: "hugo server" +slug: hugo_server +url: /commands/hugo_server/ +--- +## hugo server + +A high performance webserver + +### Synopsis + + +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. + +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 +and push the latest content to them. As most Hugo sites are built in a fraction +of a second, you will be able to save and see your changes nearly instantly. + +``` +hugo server [flags] +``` + +### Options + +``` + --appendPort append port to baseURL (default true) + -b, --baseURL string hostname (and path) to the root, e.g. http://spf13.com/ + --bind string interface to which the server will bind (default "127.0.0.1") + -D, --buildDrafts include content marked as draft + -E, --buildExpired include expired content + -F, --buildFuture include content with publishdate in the future + --cacheDir string filesystem path to cache directory. Defaults: $TMPDIR/hugo_cache/ + --canonifyURLs if true, all relative URLs will be canonicalized using baseURL + --cleanDestinationDir remove files from destination not found in static directories + -c, --contentDir string filesystem path to content directory + -d, --destination string filesystem path to write files to + --disable404 do not render 404 page + --disableKinds stringSlice disable different kind of pages (home, RSS etc.) + --disableLiveReload watch without enabling live browser reload on rebuild + --disableRSS do not build RSS files + --disableSitemap do not build Sitemap file + --enableGitInfo add Git revision, date and author info to the pages + --forceSyncStatic copy all files when static is changed. + -h, --help help for server + --i18n-warnings print missing translations + --ignoreCache ignores the cache directory + -l, --layoutDir string filesystem path to layout directory + --meminterval string interval to poll memory usage (requires --memstats), valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". (default "100ms") + --memstats string log memory usage to this file + --navigateToChanged navigate to changed content file on live browser reload + --noChmod don't sync permission mode of files + --noTimes don't sync modification time of files + --pluralizeListTitles pluralize titles in lists using inflect (default true) + -p, --port int port on which the server will listen (default 1313) + --preserveTaxonomyNames preserve taxonomy names as written ("Gérard Depardieu" vs "gerard-depardieu") + --renderToDisk render to Destination path (default is render to memory & serve from there) + -s, --source string filesystem path to read files relative from + --stepAnalysis display memory and timing of different steps of the program + -t, --theme string theme to use (located in /themes/THEMENAME/) + --themesDir string filesystem path to themes directory + --uglyURLs if true, use /filename.html instead of /filename/ + -w, --watch watch filesystem for changes and recreate as needed (default true) +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --log enable Logging + --logFile string log File path (if set, logging enabled automatically) + --quiet build in quiet mode + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO +* [hugo](/commands/hugo/) - hugo builds your site + +###### Auto generated by spf13/cobra on 6-Jul-2017 diff --git a/docs/content/commands/hugo_undraft.md b/docs/content/commands/hugo_undraft.md new file mode 100644 index 000000000..df986a2d2 --- /dev/null +++ b/docs/content/commands/hugo_undraft.md @@ -0,0 +1,42 @@ +--- +date: 2017-07-06T10:34:39+02:00 +title: "hugo undraft" +slug: hugo_undraft +url: /commands/hugo_undraft/ +--- +## hugo undraft + +Undraft resets the content's draft status + +### Synopsis + + +Undraft resets the content's draft status +and updates the date to the current date and time. +If the content's draft status is 'False', nothing is done. + +``` +hugo undraft path/to/content [flags] +``` + +### Options + +``` + -h, --help help for undraft +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --log enable Logging + --logFile string log File path (if set, logging enabled automatically) + --quiet build in quiet mode + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO +* [hugo](/commands/hugo/) - hugo builds your site + +###### Auto generated by spf13/cobra on 6-Jul-2017 diff --git a/docs/content/commands/hugo_version.md b/docs/content/commands/hugo_version.md new file mode 100644 index 000000000..053946df1 --- /dev/null +++ b/docs/content/commands/hugo_version.md @@ -0,0 +1,40 @@ +--- +date: 2017-07-06T10:34:39+02:00 +title: "hugo version" +slug: hugo_version +url: /commands/hugo_version/ +--- +## hugo version + +Print the version number of Hugo + +### Synopsis + + +All software has versions. This is Hugo's. + +``` +hugo version [flags] +``` + +### Options + +``` + -h, --help help for version +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --log enable Logging + --logFile string log File path (if set, logging enabled automatically) + --quiet build in quiet mode + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO +* [hugo](/commands/hugo/) - hugo builds your site + +###### Auto generated by spf13/cobra on 6-Jul-2017 diff --git a/docs/content/community/contributing.md b/docs/content/community/contributing.md new file mode 100644 index 000000000..052311911 --- /dev/null +++ b/docs/content/community/contributing.md @@ -0,0 +1,111 @@ +--- +aliases: +- /doc/contributing/ +- /meta/contributing/ +lastmod: 2015-02-12 +date: 2013-07-01 +menu: + main: + parent: community +next: /tutorials/automated-deployments +prev: /community/mailing-list +title: Contributing to Hugo +weight: 30 +--- + +All contributions to Hugo are welcome. Whether you want to scratch an itch or simply contribute to the project, feel free to pick something from the [roadmap]({{< relref "meta/roadmap.md" >}}) or contact the dev team via the [Forums](https://discourse.gohugo.io/) or [Gitter](https://gitter.im/gohugoio/hugo) about what may make sense to do next. + +You should fork the project and make your changes. *We encourage pull requests to discuss code changes.* + + +When you're ready to create a pull request, be sure to: + + * Have test cases for the new code. If you have questions about how to do it, please ask in your pull request. + * Run `go fmt`. + * Squash your commits into a single commit. `git rebase -i`. It's okay to force update your pull request. + * Run `make check` and ensure it succeeds. [Travis CI](https://travis-ci.org/gohugoio/hugo) and [Appveyor](https://ci.appveyor.com/project/gohugoio/hugo) will runs these checks and fail the build if `make check` fails. + +## Contribution Overview + +We wrote a [detailed guide]({{< relref "tutorials/how-to-contribute-to-hugo.md" >}}) for newcomers that guides you step by step to your first contribution. If you are more experienced, follow the guide below. + + +# Building from source + +## Vendored Dependencies + +Hugo uses [govendor][] to vendor dependencies, but we don't commit the vendored packages themselves to the Hugo git repository. +Therefore, a simple `go get` is not supported since `go get` is not vendor-aware. +You **must use govendor** to fetch Hugo's dependencies. + +## Fetch the Sources + + go get github.com/kardianos/govendor + govendor get github.com/gohugoio/hugo + +## Running Hugo + + cd $HOME/go/src/github.com/gohugoio/hugo + go run main.go + +## Building Hugo + + cd $HOME/go/src/github.com/gohugoio/hugo + make build + # or to install to $HOME/go/bin: + make install + + +# Showcase additions + +You got your new website running and it's powered by Hugo? Great. You can add your website with a few steps to the [showcase](/showcase/). + +First, make sure that you created a [fork](https://help.github.com/articles/fork-a-repo/) of the [`hugoDocs`](https://github.com/gohugoio/hugodocs) repository on GitHub and cloned your fork on your local computer. **Next, create a separate branch for your additions**: + +``` +# You can choose a different descriptive branch name if you like +git checkout -b showcase-addition +``` + +Let's create a new document that contains some metadata of your homepage. Replace `example` in the following examples with something unique like the name of your website. Inside the terminal enter the following commands: + +``` +cd docs +hugo new showcase/example.md +``` + +You should find the new file at `content/showcase/example.md`. Open it in an editor. The file should contain a frontmatter with predefined variables like below: + +``` +--- +date: 2016-02-12T21:01:18+01:00 +description: "" +license: "" +licenseLink: "" +sitelink: http://spf13.com/ +sourceLink: https://github.com/spf13/spf13.com +tags: +- personal +- blog +thumbnail: /img/spf13-tn.jpg +title: example +--- +``` + +Add at least values for `sitelink`, `title`, `description` and a path for `thumbnail`. + +Furthermore, we need to create the thumbnail of your website. **It's important that the thumbnail has the required dimensions of 600px by 400px.** Give your thumbnail a name like `example-tn.png`. Save it under `static/img/`. + +Check a last time that everything works as expected. Start Hugo's built-in server in order to inspect your local copy of the showcase in the browser: + + hugo server + +If everything looks fine, we are ready to commit your additions. For the sake of best practices, please make sure that your commit follows our [code contribution guideline](https://github.com/gohugoio/hugo#code-contribution-guideline). + + git commit -m"Add example.com to the showcase" + +Last but not least, we're ready to create a [pull request](https://github.com/gohugoio/hugoDocs/compare). + +Don't forget to accept the contributor license agreement. Click on the yellow badge in the automatically added comment in the pull request. + +[govendor]: https://github.com/kardianos/govendor diff --git a/docs/content/community/mailing-list.md b/docs/content/community/mailing-list.md new file mode 100644 index 000000000..3bd9f58bf --- /dev/null +++ b/docs/content/community/mailing-list.md @@ -0,0 +1,51 @@ +--- +lastmod: 2015-05-25 +date: 2013-07-01 +menu: + main: + parent: community +next: /community/contributing +prev: /extras/urls +title: Mailing List +weight: 10 +--- + +## Discussion Forum + +Hugo has its own [discussion forum](https://discourse.gohugo.io/) powered by [Discourse](http://www.discourse.org/). + +Please use this for all discussions, questions, etc. + +### Twitter + +Get the latest bite-sized news and themes from the Hugo community on Twitter by following [@gohugoio](http://twitter.com/gohugoio). + +## Mailing List + +Hugo has two mailing lists: + +### Announcements +Very low traffic. Only releases will be emailed here. + +https://groups.google.com/forum/#!forum/hugo-announce + +### Discussion (Archive) + +**This has been replaced with the [Hugo discussion forum](https://discourse.gohugo.io/).** + +It is available for archival purposes. + +https://groups.google.com/forum/#!forum/hugo-discuss + + +## Other Resources + +### GoNuts + +For general Go questions or discussion please refer to the Go mailing list. + +https://groups.google.com/forum/#!forum/golang-nuts + +### GitHub Issues + +https://github.com/gohugoio/hugo/issues diff --git a/docs/content/community/press.md b/docs/content/community/press.md new file mode 100644 index 000000000..c5523d66e --- /dev/null +++ b/docs/content/community/press.md @@ -0,0 +1,141 @@ +--- +lastmod: 2017-03-02 +date: 2014-03-24T20:00:00Z +linktitle: Press +notoc: true +title: Press, Blogs and Media Coverage +weight: 20 +--- + +### Help keep this list up to date + +Know of a post, article or tutorial on Hugo? [Add it to this list](https://github.com/gohugoio/hugo/edit/master/docs/content/community/press.md). + +## Press and Articles + +Hugo has been featured in the following Blog Posts, Press and Media. + + +| Title | Author | Date | +| ------ | ------ | -----: | +| [Build, Test, And Deploy Statically Generated Websites With Hugo & CircleCI](https://circleci.com/blog/build-test-deploy-hugo-sites/)| Ricardo N Feliciano | 2017-05-31 | +| [Hugo Easy Gallery - Automagical PhotoSwipe image gallery with a one-line shortcode](https://www.liwen.id.au/heg/)| Li-Wen Yip | 2017-03-25 | +| [Hugo Tutorial: How to Build & Host a (Very Fast) Static E-Commerce Site](https://snipcart.com/blog/hugo-tutorial-static-site-ecommerce) | Snipcart | 2017-03-12 | +| [Automagical image gallery in Hugo with PhotoSwipe and jQuery](https://www.liwen.id.au/photoswipe/)| Li-Wen Yip | 2017-03-04 | +| [Adding Isso Comments to Hugo](https://stiobhart.net/2017-02-24-isso-comments/) | Stíobhart Matulevicz | 2017-02-24 | +| [Zero to HTTP/2 with AWS and Hugo](https://habd.as/zero-to-http-2-aws-hugo/) | Josh Habdas | 2017-02-16 | +| [How to Password Protect a Hugo Site](https://www.aerobatic.com/blog/password-protect-a-hugo-site/) | Aerobatic | 2017-02-19 | +| [Switching from Wordpress to Hugo](http://schnuddelhuddel.de/switching-from-wordpress-to-hugo/) | Mario Martelli | 2017-02-19 | ] +| [Deploy a Hugo site to Aerobatic with CircleCI ](https://www.aerobatic.com/blog/hugo-github-circleci/) | Aerobatic | 2017-02-14 | +| [NPM scripts for building and deploying Hugo site](https://www.aerobatic.com/blog/hugo-npm-buildtool-setup/) | Aerobatic | 2017-02-12 | +| [Getting started with Hugo and the plain-blog theme, on NearlyFreeSpeech.Net](https://www.penwatch.net/cms/get_started_plain_blog/) | Li-aung “Lewis” Yip | 2017-02-12 | +| [Build a Hugo site using Cloud9 IDE and host on App Engine](https://loyall.ch/lab/2017/01/build-a-static-website-with-cloud9-hugo-and-app-engine/)| Pascal Aubort | 2017-02-05 | +| [Hugo Continuous Deployment with Bitbucket Pipelines and Aerobatic](https://www.aerobatic.com/blog/hugo-bitbucket-pipelines/) | Aerobatic | 2017-02-04 | +| [How to use Firebase to host a Hugo site](https://www.m0d3rnc0ad.com/post/static-site-firebase/) | Andrew Cuga | 2017-02-04 | +| [A publishing workflow for teams using static site generators](https://www.keybits.net/post/publishing-workflow-for-teams-using-static-site-generators/) | Tom Atkins | 2017-01-02 | +| [How To Dynamically Use Google Fonts In A Hugo Website](https://stoned.io/web-development/hugo/How-To-Dynamically-Use-Google-Fonts-In-A-Hugo-Website/) | Hash Borgir | 2016-10-27 | +| [Embedding Facebook In A Hugo Template](https://stoned.io/web-development/hugo/Embedding-Facebook-In-A-Hugo-Template/) | Hash Borgir | 2016-10-22 | +| [通过 Gitlab-cl 将 Hugo blog 自动部署至 GitHub](https://zetaoyang.github.io/post/2016/10/17/gitlab-cl.html) (Chinese, Continious integration) | Zetao Yang | 2016-10-17 | +| [A Step-by-Step Guide: Hugo on Netlify](https://www.netlify.com/blog/2016/09/21/a-step-by-step-guide-hugo-on-netlify/) | Eli Williamson | 2016-09-21 | +| [Building our site: From Django & Wordpress to a static generator (Part I)](https://tryolabs.com/blog/2016/09/20/building-our-site-django-wordpress-to-static-part-i/) | Alan Descoins | 2016-09-20 | +| [Webseitenmaschine - Statische Websites mit Hugo erzeugen](http://www.heise.de/ct/ausgabe/2016-12-Statische-Websites-mit-Hugo-erzeugen-3211704.html) (German, $) | Christian Helmbold | 2016-05-27 | +| [Cómo hacer sitios web estáticos con Hugo y Go - Platzi](https://www.youtube.com/watch?v=qaXXpdiCHXE) (Video tutorial) | Verónica López | 2016-04-06 | +| [CDNOverview: A CDN comparison site made with Hugo](https://www.cloakfusion.com/cdnoverview-cdn-comparison-site-made-hugo/) | Thijs de Zoete | 2016-02-23 | +| [Hugo: A Modern WebSite Engine That Just Works](https://github.com/shekhargulati/52-technologies-in-2016/blob/master/07-hugo/README.md) | Shekhar Gulati | 2016-02-14 | +| [Minify Hugo Generated HTML](http://ratson.name/blog/minify-hugo-generated-html/) | Ratson | 2016-02-02 | +| [HugoのデプロイをWerckerからCircle CIに変更した - log](http://log.deprode.net/logs/2016-01-17/) | Deprode | 2016-01-17 | +| [Static site generators: el futuro de las webs estáticas
    (Hugo, Jekyll, Flask y otros)](http://sitelabs.es/static-site-generators-futuro-las-webs-estaticas/) | Eneko Sarasola | 2016-01-09 | +| [Writing a Lambda Function for Hugo](https://blog.jolexa.net/post/writing-a-lambda-function-for-hugo/) | Jeremy Olexa | 2016-01-01 | +| [Ein Blog mit Hugo erstellen - Tutorial](http://privat.albicker.org/tags/hugo.html) (Deutsch/German) | Bernhard Albicker | 2015-12-30 | +| [How to host Hugo static website generator on AWS Lambda](http://bezdelev.com/post/hugo-aws-lambda-static-website/) | Ilya Bezdelev | 2015-12-15 | +| [Migrating from Pelican to Hugo](http://www.softinio.com/post/migrating-from-pelican-to-hugo/) | Salar Rahmanian | 2015-11-29 | +| [Static Website Generators Reviewed: Jekyll, Middleman, Roots, Hugo](http://www.smashingmagazine.com/2015/11/static-website-generators-jekyll-middleman-roots-hugo-review/) | Mathias Biilmann Christensen | 2015-11-16 | +| [How To Deploy a Hugo Site to Production with Git Hooks on Ubuntu 14.04](https://www.digitalocean.com/community/tutorials/how-to-deploy-a-hugo-site-to-production-with-git-hooks-on-ubuntu-14-04) | Justin Ellingwood | 2015-11-12 | +| [How To Install and Use Hugo, a Static Site Generator, on Ubuntu 14.04](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-hugo-a-static-site-generator-on-ubuntu-14-04) | Justin Ellingwood | 2015-11-09 | +| [Switching from Wordpress to Hugo](http://justinfx.com/2015/11/08/switching-from-wordpress-to-hugo/) | Justin Israel | 2015-11-08 | +| [Hands-on Experience with Hugo as a Static Site Generator](http://usersnap.com/blog/hands-on-experience-with-hugo-static-site-generator/) | Thomas Peham | 2015 -10-15 | +| [Statische Webseites mit Hugo erstellen/Vortrag mit Foliensatz (deutsch)](http://sfd.koelnerlinuxtreffen.de/2015/HaraldWeidner/) | Harald Weidner | 2015-09-19 | +| [Moving from WordPress to Hugo](http://abhipandey.com/2015/09/moving-to-hugo/) | Abhishek Pandey | 2015-09-15 | +| [通过webhook将Hugo自动部署至GitHub Pages和GitCafe Pages (Automated deployment)](http://blog.coderzh.com/2015/09/13/use-webhook-automated-deploy-hugo/) | CoderZh | 2015-09-13 | +| [使用hugo搭建个人博客站点 (Using Hugo to build a personal blog site)](http://blog.coderzh.com/2015/08/29/hugo/) | CoderZh | 2015-08-29 | +| [Good-Bye Wordpress, Hello Hugo!](http://blog.arminhanisch.de/2015/08/blog-migration-zu-hugo/) (German) | Armin Hanisch | 2015-08-18 | +| [Générer votre site web statique avec Hugo (Generate your static site with Hugo)](http://www.linux-pratique.com/?p=191) | Benoît Benedetti | 2015-06-26 | +| [Hugo向けの新しいテーマを作った (I created a new theme for Hugo)](https://yet.unresolved.xyz/blog/2016/10/03/how-to-make-of-hugo-theme/) | Daisuke Tsuji | 2015-06-20 | +| [Hugo - Gerando um site com conteúdo estático. (Portuguese Brazil)](http://blog.ffrizzo.com/posts/hugo/) | Fabiano Frizzo | 2015-06-02 | +| [An Introduction to Static Site Generators](http://davidwalsh.name/introduction-static-site-generators) | Eduardo Bouças | 2015-05-20 | +| [Hugo Still Rules](http://cheekycoder.com/2015/05/hugo-still-rules/) | Cheeky Coder | 2015-05-18 | +| [hugo - Static Site Generator](http://gscacco.github.io/post/hugo/) | G Scaccoio | 2015-05-04 | +| [WindowsでHugoを使う](http://ureta.net/2015/05/hugo-on-windows/) | うれ太郎 | 2015-05-01 | +| [Hugoのshortcodesを用いてサイトにスライドなどを埋め込む](http://blog.yucchiy.com/2015/04/29/hugo-shortcode/) | Yucchiy | 2015-04-29 | +| [HugoとCircleCIでGitHub PagesにBlogを公開してみたら超簡単だった](http://hori-ryota.github.io/blog/create-blog-with-hugo-and-circleci/) | Hori Ryota | 2015-04-17 | +| [10 Best Static Site Generators](http://beebom.com/2015/04/best-static-site-generators) | Aniruddha Mysore | 2015-04-06 | +| [Goodbye WordPress; Hello Hugo](http://willwarren.com/2015/04/05/goodbye-wordpress-hello-hugo/) | Will Warren | 2015-04-05 | +| [Static Websites with Hugo on Google Cloud Storage](http://www.moxie.io/post/static-websites-with-hugo-on-google-cloud-storage/) | Moxie Input/Output | 2015-04-02 | +| [De nuevo iniciando un blog](https://alvarolizama.net/) | Alvaro Lizama | 2015-03-29 | +| [We moved our blog from Posthaven to Hugo after only three posts. Why?](http://blog.hypriot.com/post/moved-from-posthaven-to-hugo/) | Hypriot | 2015-03-27 | +| [Top Static Site Generators in 2015](http://superdevresources.com/static-site-generators-2015/) | Kanishk Kunal | 2015-03-12 | +| [Moving to Hugo](http://abiosoft.com/moving-to-hugo/) | Abiola Ibrahim | 2015-03-08 | +| [Migrating a blog (yes, this one!) from Wordpress to Hugo](http://justindunham.net/migrating-from-wordpress-to-hugo/) | Justin Dunham | 2015-02-13 | +| [blogをoctopressからHugoに乗り換えたメモ](http://blog.jigyakkuma.org/2015/02/11/hugo/) | jigyakkuma | 2015-02-11 | +| [Hugoでブログをつくった](http://porgy13.github.io/post/new-hugo-blog/) | porgy13 | 2015-02-07 | +| [Hugoにブログを移行した](http://keichi.net/post/first/) | Keichi Takahashi | 2015-02-04 | +| [Hugo静态网站生成器中文教程](http://nanshu.wang/post/2015-01-31/) | Nanshu Wang | 2015-01-31 | +| [Hugo + GitHub Pages + Wercker CI = ¥0(無料)
    でコマンド 1 発(自動化)でサイト
    ・ブログを公開・運営・分析・収益化
    ](http://qiita.com/yoheimuta/items/8a619cac356bed89a4c9) | Yohei Yoshimuta | 2015-01-31 | +| [Running Hugo websites on anynines](http://blog.anynines.com/running-hugo-websites-on-anynines/) | Julian Weber | 2015-01-30 | +| [MiddlemanからHugoへ移行した](http://re-dzine.net/2015/01/hugo/) | Haruki Konishi | 2015-01-21 | +| [WordPress から Hugo に乗り換えました](http://rakuishi.com/archives/wordpress-to-hugo/) | rakuishi | 2015-01-20 | +| [HUGOを使ってサイトを立ち上げる方法](http://qiita.com/syui/items/869538099551f24acbbf) | Syui | 2015-01-17 | +| [Jekyllが許されるのは小学生までだよね](http://t32k.me/mol/log/hugo/) | Ishimoto Koji | 2015-01-16 | +| [Getting started with Hugo](http://anthonyfok.org/post/getting-started-with-hugo/) | Anthony Fok | 2015-01-12 | +| [把这个博客静态化了 (Migrate to Hugo)](http://lich-eng.com/2015/01/03/migrate-to-hugo/)| Li Cheng | 2015-01-03 | +| [Porting my blog with Hugo](http://blog.srackham.com/posts/porting-my-blog-with-hugo/) | Stuart Rackham | 2014-12-30 | +| [Hugoを使ってみたときのメモ](http://machortz.github.io/posts/usinghugo/) | Machortz | 2014-12-29 | +| [OctopressからHugoへ移行した](http://deeeet.com/writing/2014/12/25/hugo/) | Taichi Nakashima | 2014-12-25 | +| [Migrating to Hugo From Octopress](http://nathanleclaire.com/blog/2014/12/22/migrating-to-hugo-from-octopress/) | Nathan LeClaire | 2014-12-22 | +| [Dynamic Pages with GoHugo.io](http://cyrillschumacher.com/2014/12/21/dynamic-pages-with-gohugo.io/) | Cyrill Schumacher | 2014-12-21 | +| [6 Static Blog Generators That Aren’t Jekyll](http://www.sitepoint.com/6-static-blog-generators-arent-jekyll/) | David Turnbull | 2014-12-08 | +| [Travel Blogging Setup](http://www.stou.dk/2014/11/travel-blogging-setup/) | Rasmus Stougaard | 2014-11-23 | +| [Hosting A Hugo Website Behind Nginx](http://www.bigbeeconsultants.co.uk/blog/hosting-hugo-website-behind-nginx) | Rick Beton | 2014-11-20 | +| [使用Hugo搭建免费个人Blog (How to use Hugo)](http://ulricqin.com/post/how-to-use-hugo/) | Ulric Qin 秦晓辉 | 2014-11-11 | +| [Built in Speed and Built for Speed by Hugo](http://cheekycoder.com/2014/10/built-for-speed-by-hugo/) | Cheeky Coder | 2014-10-30 | +| [Hugo para crear sitios web estáticos](http://www.webbizarro.com/noticias/1076/hugo-para-crear-sitios-web-estaticos/) | Web Bizarro | 2014-08-19 | +| [Going with hugo](http://www.markuseliasson.se/article/going-with-hugo/) | Markus Eliasson | 2014-08-18 | +| [Benchmarking Jekyll, Hugo and Wintersmith](http://fredrikloch.me/post/2014-08-12-Jekyll-and-its-alternatives-from-a-site-generation-point-of-view/) | Fredrik Loch | 2014-08-12 | +| [Goodbye Octopress, Hello Hugo!](http://andreimihu.com/blog/2014/08/11/goodbye-octopress-hello-hugo/) | Andrei Mihu | 2014-08-11 | +| [Beautiful sites for Open Source projects](http://beautifulopen.com/2014/08/09/hugo/) | Beautiful Open | 2014-08-09 | +| [Hugo: Beyond the Defaults](http://npf.io/2014/08/hugo-beyond-the-defaults/) | Nate Finch | 2014-08-08 | +| [First Impressions of Hugo](https://peteraba.com/blog/first-impressions-of-hugo/) | Peter Aba | 2014-06-06 | +| [New Site Workflow](http://vurt.co.uk/post/new_website/) | Giles Paterson | 2014-08-05 | +| [How I Learned to Stop Worrying and Love the (Static) Web](http://cognition.ca/post/about-hugo/) | Joshua McKenty | 2014-08-04 | +| [Hugo - Static Site Generator](http://kenwoo.io/blog/hugo---static-site-generator/) | Kenny Woo | 2014-08-03 | +| [Hugo Is Friggin' Awesome](http://npf.io/2014/08/hugo-is-awesome/) | Nate Finch | 2014-08-01 | +| [再次搬家 (Move from WordPress to Hugo)](http://www.chingli.com/misc/move-from-wordpress-to-hugo/) | 青砾 (chingli) | 2014-07-12 | +| [Embedding Gists in Hugo](http://danmux.com/posts/embedded_gists/) | Dan Mull | 2014-07-05 | +| [An Introduction To Hugo](http://www.cirrushosting.com/web-hosting-blog/an-introduction-to-hugo/) | Dan Silber | 2014-07-01 | +| [Moving to Hugo](http://danmux.com/posts/hugo_based_blog/) | Dan Mull | 2014-05-29 | +| [开源之静态站点生成器排行榜
    (Leaderboard of open-source static website generators)](http://code.csdn.net/news/2819909) | CSDN.net | 2014-05-23 | +| [Finally, a satisfying and effective blog setup](http://michaelwhatcott.com/now-powered-by-hugo/) | Michael Whatcott | 2014-05-20 | +| [Hugo from scratch](http://zackofalltrades.com/notes/2014/05/hugo-from-scratch/) | Zack Williams | 2014-05-18 | +| [Why I switched away from Jekyll](http://www.jakejanuzelli.com/why-I-switched-away-from-jekyll/) | Jake Januzelli | 2014-05-10 | +| [Welcome our new blog](http://blog.ninya.io/posts/welcome-our-new-blog/) | Ninya.io | 2014-04-11 | +| [Mission Not Accomplished](http://johnsto.co.uk/blog/mission-not-accomplished/) | Dave Johnston | 2014-04-03 | +| [Hugo - A Static Site Builder in Go](http://deepfriedcode.com/post/hugo/) | Deep Fried Code | 2014-03-30 | +| [Adventures in Angular Podcast](http://devchat.tv/adventures-in-angular/003-aia-gdes) | Matias Niemela | 2014-03-28 | +| [Hugo](http://bra.am/post/hugo/) | bra.am | 2014-03-23 | +| [Converting Blogger To Markdown](http://trishagee.github.io/project/atom-to-hugo/) | Trisha Gee | 2014-03-20 | +| [Moving to Hugo Static Web Pages](http://tepid.org/tech/hugo-web/) | Tobias Weingartner | 2014-03-16 | +| [New Blog Engine: Hugo](https://blog.afoolishmanifesto.com/posts/hugo/) | fREW Schmidt | 2014-03-15 | +| [Hugo + gulp.js = Huggle](http://ktmud.github.io/huggle/en/intro/) ([English](http://ktmud.github.io/huggle/en/intro/), [中文](http://ktmud.github.io/huggle/zh/intro/)) | Jesse Yang 杨建超 | 2014-03-08 | +| [Powered by Hugo](http://kieranhealy.org/blog/archives/2014/02/24/powered-by-hugo/) | Kieran Healy | 2014-02-24 | +| [静的サイトを素早く構築するために
    GoLangで作られたジェネレータHugo
    ](http://hamasyou.com/blog/2014/02/21/hugo/)|
    Shogo Hamada
    濱田章吾
    | 2014-02-21 | +| [Latest Roundup of Useful Tools For Developers](http://codegeekz.com/latest-roundup-of-useful-tools-for-developers/) | CodeGeekz | 2014-02-13 | +| [Hugo: Static Site Generator written in Go](http://www.braveterry.com/2014/02/06/hugo-static-site-generator-written-in-go/) | Brave Terry | 2014-02-06 | +| [10 Useful HTML5 Tools for Web Designers and Developers](http://designdizzy.com/10-useful-html5-tools-for-web-designers-and-developers/) | Design Dizzy | 2014-02-04 | +| [Hugo – Fast, Flexible Static Site Generator](http://cube3x.com/hugo-fast-flexible-static-site-generator/) | Joby Joseph | 2014-01-18 | +| [Hugo: A new way to build static website](http://www.w3update.com/opensource/hugo-a-new-way-to-build-static-website.html) | w3update | 2014-01-17 | +| [Xaprb now uses Hugo](http://xaprb.com/blog/2014/01/15/using-hugo/) | Baron Schwartz | 2014-01-15 | +| [New jQuery Plugins And Resources That Web Designers Need](http://www.designyourway.net/blog/resources/new-jquery-plugins-and-resources-that-web-designers-need/) | Design Your Way | 2014-01-01 | +| [On Blog Construction](http://alexla.sh/post/on-blog-construction/) | Alexander Lash | 2013-12-27 | +| [Hugo](http://onethingwell.org/post/69070926608/hugo) | One Thing Well | 2013-12-05 | +| [In Praise Of Hugo](http://sound-guru.com/blog/post/hello-world/) | sound-guru.com | 2013-10-19 | +| [Hosting a blog on S3 and Cloudfront](http://www.danesparza.net/2013/07/hosting-a-blog-on-s3-and-cloudfront/) | Dan Esparza | 2013-07-24 | diff --git a/docs/content/content/archetypes.md b/docs/content/content/archetypes.md new file mode 100644 index 000000000..88efdbd0f --- /dev/null +++ b/docs/content/content/archetypes.md @@ -0,0 +1,330 @@ +--- +lastmod: 2016-10-01 +date: 2014-05-14T02:13:50Z +menu: + main: + parent: content +next: /content/ordering +prev: /content/types +title: Archetypes +weight: 50 +toc: true +--- + +Typically, each piece of content you create within a Hugo project will have [front matter](/content/front-matter/) that follows a consistent structure. If you write blog posts, for instance, you might use the following front matter for the vast majority of those posts: + +```toml ++++ +title = "" +date = "" +slug = "" +tags = [ + "" +] +categories = [ + "" +] +draft = true ++++ +``` + +You can always add non-typical front matter to any piece of content, but since it takes extra work to develop a theme that handles unique metadata, consistency is simpler. + +With this in mind, Hugo has a convenient feature known as *archetypes* that allows users to define default front matter for new pieces of content. + +By using archetypes, we can: + +1. **Save time**. Stop writing the same front matter over and over again. +2. **Avoid errors**. Reduce the odds of typos, improperly formatted syntax, and other simple mistakes. +3. **Focus on more important things**. Avoid having to remember all of the fields that need to be associated with each piece of content. (This is particularly important for larger projects with complex front matter and a variety of content types.) + +Let's explore how they work. + +## Built-in Archetypes + +If you've been using Hugo for a while, there's a decent chance you've come across archetypes without even realizing it. This is because Hugo includes a basic, built-in archetype that is used by default whenever it generates a content file. + +To see this in action, open the command line, navigate into your project's directory, and run the following command: + +```bash +hugo new hello-world.md +``` + +This `hugo new` command creates a new content file inside the project's `content` directory — in this case, a file named `hello-world.md` — and if you open this file, you'll notice it contains the following front matter: + +```toml ++++ +date = "2017-05-31T15:18:11+10:00" +draft = true +title = "hello world" ++++ +``` + +Here, we can see that three fields have been added to the document: a `title` field that is based on the file name we defined, a `draft` field that ensures this content won't be published by default, and a `date` field that is auto-populated with the current date and time in the [RFC 3339](https://stackoverflow.com/questions/522251/whats-the-difference-between-iso-8601-and-rfc-3339-date-formats) format. + +This, in its most basic form, is an example of an archetype. To understand how useful they can be though, it's best if we create our own. + +## Creating Archetypes + +In this section, we're going to create an archetype that will override the built-in archetype, allowing us to define custom front matter that will be included in any content files that we generate with the `hugo new` command. + +To achieve this, create a file named `default.md` inside the `archetypes` folder of a Hugo project. (If the folder doesn't exist, create it.) + +Then, inside this file, define the following front matter: + +```toml ++++ +slug = "" +tags = [] +categories = [] +draft = true ++++ +``` + +You'll notice that we haven't defined a `title` or `date` field. This is because Hugo will automatically add these fields to the beginning of the front matter. We do, however, need to define the `draft` field if we want it to exist in our front matter. + +You'll also notice that we're writing the front matter in the TOML format. It's possible to define archetype front matter in other formats, but a setting needs to be changed in the configuration file for this to be possible. See the "[Archetype Formats](#archetype-formats)" section of this article for more details. + +Next, run the following command: + +```bash +hugo new my-archetype-example.md +``` + +This command will generate a file named `my-archetype-example.md` inside the `content` directory, and this file will contain the following output: + +```toml ++++ +categories = [] +date = "2017-05-31T15:21:13+10:00" +draft = true +slug = "" +tags = [] +title = "my archetype example" ++++ +``` + +As we can see, the file contains the `title` and `date` property that Hugo created for us, along with the front matter that we defined in the `archetypes/default.md` file. + +You'll also notice that the fields have been sorted into alphabetical order. This is an unintentional side-effect that stems from the underlying code libraries that Hugo relies upon. It is, however, [a known issue that is actively being discussed](https://github.com/gohugoio/hugo/issues/452). + +## Section Archetypes + +By creating the `archetypes/default.md` file, we've created a default archetype that is more useful than the built-in archetype, but since Hugo encourages us to [organize our content into sections](/content/sections/), each of which will likely have different front matter requirements, a "one-size-fits-all" archetype isn't necessarily the best approach. + +To accommodate for this, Hugo allows us to create archetypes for each section of our project. This means, whenever we generate content for a certain section, the appropriate front matter for that section will be automatically included in the generated file. + +To see this in action, create a "photo" section by creating a directory named "photo" inside the `content` directory. + +Then create a file named `photo.md` inside the `archetypes` directory and include the following front matter inside this file: + +```toml ++++ +image_url = "" +camera = "" +lens = "" +aperture = "" +iso = "" +draft = true ++++ +``` + +Here, the critical detail is that the `photo.md` file in the `archetypes` directory is named after the `photo` section that we just created. By sharing a name, Hugo can understand that there's a relationship between them. + +Next, run the following command: + +```bash +hugo new photo/my-pretty-cat.md +``` + +This command will generate a file named `my-pretty-cat.md` inside the `content/photo` directory, and this file will contain the following output: + +```toml ++++ +aperture = "" +camera = "" +date = "2017-05-31T15:25:18+10:00" +draft = true +image_url = "" +iso = "" +lens = "" +title = "my pretty cat" ++++ +``` + +As we can see, the `title` and `date` fields are still included by Hugo, but the rest of the front matter is being generated from the `photo.md` archetype instead of the `default.md` archetype. + +### Tip: Default Values + +To make archetypes more useful, define default values for any fields that will always be set to a range of limited options. In the case of the `photo.md` archetype, for instance, you could include lists of the various cameras and lenses that you own: + +```toml ++++ +image_url = "" +camera = [ + "Sony RX100 Mark IV", + "Canon 5D Mark III", + "iPhone 6S" +] +lens = [ + "Canon EF 50mm f/1.8", + "Rokinon 14mm f/2.8" +] +aperture = "" +iso = "" +draft = true ++++ +``` + +Then, after generating a content file, simply remove the values that aren't relevant. This saves you from typing out the same options over and over again while ensuring consistency in how they're written. + +## Scaffolding Content + +Archetypes aren't limited to defining default front matter. They can also be used to define a default structure for the body of Markdown documents. + +For example, imagine creating a `review.md` archetype for the purpose of writing camera reviews. This is what the front matter for such an archetype might look like: + +```toml ++++ +manufacturer = "" +model = "" +price = "" +releaseDate = "" +rating = "" ++++ +``` + +But reviews tend to follow strict formats and need to answer specific questions, and it's with these expectations of precise structure that archetypes can prove to be even more useful. + +For the sake of writing reviews, for instance, we could define the structure of a review beneath the front matter of the `review.md` file: + +```markdown ++++ +manufacturer = "" +model = "" +price = "" +releaseDate = "" +rating = "" ++++ + +## Introduction + +## Sample Photos + +## Conclusion +``` + +Then, whenever we use the `hugo new` command to create a new review, not only will the default front matter be copied into the newly created Markdown document, but the body of the `review.md` archetype will also be copied. + +To take this further though — and to ensure authors on multi-author websites are on the same page about how content should be written — we could include notes and reminders within the archetype: + +```markdown ++++ +manufacturer = "" +model = "" +price = "" +releaseDate = "" +rating = "" ++++ + +## Introduction + + + +## Sample Photos + + + +## Conclusion + + + +``` + +That way, each time we generate a new content file, we have a series of handy notes to push us closer to a piece of writing that's suitable for publishing. + +(If you're wondering why the notes are wrapped in the HTML comment syntax, it's to ensure they won't appear inside the preview window of whatever Markdown editor the author happens to be using. They're not strictly necessary though.) + +This is still a fairly simple example, but if your content usually contains a variety of components — headings, bullet-points, images, [short-codes](/extras/shortcodes/), etc — it's not hard to see the time-saving benefits of placing these components in the body of an archetype file. + +## Theme Archetypes + +Whenever you generate a content file with the `hugo new` command, Hugo will start by searching for archetypes in the `archetypes` directory, initially looking for an archetype that matches the content's section and falling-back on the `default.md` archetype (if one is present). If no archetypes are found in this directory, Hugo will continue its search in the `archetypes` directory of the currently active theme. In other words, it's possible for themes to come packaged with their own archetypes, ensuring that users of that theme format their content files with correctly structured front matter. + +To allow Hugo to use archetypes from a theme, [that theme must be activated via the project's configuration file](/themes/usage/): + +```toml +theme = "ThemeNameGoesHere" +``` + +If an archetype doesn't exist in the `archetypes` directory at the top-level of a project or inside the `archetypes` directory of an active theme, the built-in archetype will be used. + +{{< figure src="/img/content/archetypes/archetype-hierarchy.png" alt="How Hugo Decides Which Archetype To Use" >}} + +## Archetype Formats + +By default, the `hugo new` command will generate front matter in the TOML format. This means, even if we define the front matter in our archetype files as YAML or JSON, it will be converted to the TOML format before it ends up in our content files. + +Fortunately, this functionality can be overwritten. + +Inside the project's configuration file, simply define a `metaDataFormat` property: + +```toml +metaDataFormat = "" +``` + +Then set this property to any of the following values: + +* toml +* yaml +* json + +By defining this option, any front matter will be generated in your preferred format. + +It's worth noting, however, that when generating front matter in the TOML format, you might encounter the following error: + +```bash +Error: cannot convert type to TomlTree +``` + +This is because, to generate TOML, all of the fields in the front matter need to have a default value, even if that default value is just an empty string. + +For example, this YAML would *not* successfully compile into the TOML format: + +```yaml +--- +slug: +tags: +categories: +draft: +--- +``` + +But this YAML *would* successfully compile: + +```yaml +--- +slug: "" +tags: + - +categories: + - +draft: true +--- +``` + +It's a subtle yet important detail to remember. + +## Notes + +* Prior to Hugo v0.13, some users received [an "EOF" error when using archetypes](https://github.com/gohugoio/hugo/issues/776), related to what text editor they used to create the archetype. As of Hugo v0.13, this error has been [resolved](https://github.com/gohugoio/hugo/pull/785). diff --git a/docs/content/content/example.md b/docs/content/content/example.md new file mode 100644 index 000000000..022764994 --- /dev/null +++ b/docs/content/content/example.md @@ -0,0 +1,91 @@ +--- +aliases: +- /doc/example/ +lastmod: 2015-12-23 +date: 2013-07-01 +linktitle: Example +menu: + main: + parent: content +prev: /content/multilingual +next: /content/using-index-md +notoc: true +title: Example Content File +weight: 70 +--- + +Some things are better shown than explained. The following is a very basic example of a content file written in [Markdown](https://help.github.com/articles/github-flavored-markdown/): + +**mysite/content/project/nitro.md → http://mysite.com/project/nitro.html** + +With TOML front matter: + +
    +++
    +date        = "2013-06-21T11:27:27-04:00"
    +title       = "Nitro: A quick and simple profiler for Go"
    +description = "Nitro is a simple profiler for your Golang applications"
    +tags        = [ "Development", "Go", "profiling" ]
    +topics      = [ "Development", "Go" ]
    +slug        = "nitro"
    +project_url = "https://github.com/spf13/nitro"
    ++++
    +# Nitro
    +
    +Quick and easy performance analyzer library for [Go](http://golang.org/).
    +
    +## Overview
    +
    +Nitro is a quick and easy performance analyzer library for Go.
    +It is useful for comparing A/B against different drafts of functions
    +or different functions.
    +
    +## Implementing Nitro
    +
    +Using Nitro is simple. First, use `go get` to install the latest version
    +of the library.
    +
    +    $ go get github.com/spf13/nitro
    +
    +Next, include nitro in your application.
    +
    + +You may also use the equivalent YAML front matter: + +```yaml +--- +lastmod: 2015-12-23 +date: "2013-06-21T11:27:27-04:00" +title: "Nitro: A quick and simple profiler for Go" +description: "Nitro is a simple profiler for your Go lang applications" +tags: [ "Development", "Go", "profiling" ] +topics: [ "Development", "Go" ] +slug: "nitro" +project_url: "https://github.com/spf13/nitro" +--- +``` + +`nitro.md` would be rendered as follows: + +> # Nitro +> +> Quick and easy performance analyzer library for [Go](http://golang.org/). +> +> ## Overview +> +> Nitro is a quick and easy performance analyzer library for Go. +> It is useful for comparing A/B against different drafts of functions +> or different functions. +> +> ## Implementing Nitro +> +> Using Nitro is simple. First, use `go get` to install the latest version +> of the library. +> +> $ go get github.com/spf13/nitro +> +> Next, include nitro in your application. + +The source `nitro.md` file is converted to HTML by the excellent +[Blackfriday](https://github.com/russross/blackfriday) Markdown processor, +which supports extended features found in the popular +[GitHub Flavored Markdown](https://help.github.com/articles/github-flavored-markdown/). diff --git a/docs/content/content/front-matter.md b/docs/content/content/front-matter.md new file mode 100644 index 000000000..e61a48f55 --- /dev/null +++ b/docs/content/content/front-matter.md @@ -0,0 +1,119 @@ +--- +aliases: +- /doc/front-matter/ +lastmod: 2015-12-23 +date: 2013-07-01 +menu: + main: + parent: content +next: /content/sections +prev: /content/organization +title: Front Matter +weight: 20 +toc: true +--- + +The **front matter** is one of the features that gives Hugo its strength. It enables +you to include the meta data of the content right with it. Hugo supports a few +different formats, each with their own identifying tokens. + +Supported formats: + + * **[TOML][]**, identified by '`+++`'. + * **[YAML][]**, identified by '`---`'. + * **[JSON][]**, a single JSON object which is surrounded by '`{`' and '`}`', followed by a newline. + +[TOML]: https://github.com/toml-lang/toml "Tom's Obvious, Minimal Language" +[YAML]: http://www.yaml.org/ "YAML Ain't Markup Language" +[JSON]: http://www.json.org/ "JavaScript Object Notation" + +## TOML Example + +
    +++
    +title = "spf13-vim 3.0 release and new website"
    +description = "spf13-vim is a cross platform distribution of vim plugins and resources for Vim."
    +tags = [ ".vimrc", "plugins", "spf13-vim", "vim" ]
    +date = "2012-04-06"
    +categories = [
    +  "Development",
    +  "VIM"
    +]
    +slug = "spf13-vim-3-0-release-and-new-website"
    ++++
    +Content of the file goes Here
    +
    + +## YAML Example + +```yaml +--- +title: "spf13-vim 3.0 release and new website" +description: "spf13-vim is a cross platform distribution of vim plugins and resources for Vim." +tags: [ ".vimrc", "plugins", "spf13-vim", "vim" ] +lastmod: 2015-12-23 +date: "2012-04-06" +categories: + - "Development" + - "VIM" +slug: "spf13-vim-3-0-release-and-new-website" +--- + +Content of the file goes Here +``` + +## JSON Example + +```json +{ + "title": "spf13-vim 3.0 release and new website", + "description": "spf13-vim is a cross platform distribution of vim plugins and resources for Vim.", + "tags": [ ".vimrc", "plugins", "spf13-vim", "vim" ], + "date": "2012-04-06", + "categories": [ + "Development", + "VIM" + ], + "slug": "spf13-vim-3-0-release-and-new-website" +} + +Content of the file goes Here +``` + +## Variables + +There are a few predefined variables that Hugo is aware of and utilizes. The user can also create +any variable they want. These will be placed into the `.Params` variable available to the templates. +Field names are always normalized to lowercase (e.g. `camelCase: true` is available as `.Params.camelcase`). + +### Required variables + +* **title** The title for the content +* **description** The description for the content +* **date** The date the content will be sorted by +* **taxonomies** These will use the field name of the plural form of the index (see tags and categories above) + +### Optional variables + +* **aliases** An array of one or more aliases + (e.g. old published path of a renamed content) + that would be created to redirect to this content. + See [Aliases]({{< relref "extras/aliases.md" >}}) for details. +* **draft** If true, the content will not be rendered unless `hugo` is called with `--buildDrafts` +* **publishdate** If in the future, content will not be rendered unless `hugo` is called with `--buildFuture` +* **expirydate** Content already expired will not be rendered unless `hugo` is called with `--buildExpired` +* **type** The type of the content (will be derived from the directory automatically if unset) +* **isCJKLanguage** If true, explicitly treat the content as CJKLanguage (`.Summary` and `.WordCount` can work properly in CJKLanguage) +* **weight** Used for sorting +* **markup** *(Experimental)* Specify `"rst"` for reStructuredText (requires + `rst2html`) or `"md"` (default) for Markdown +* **slug** appears as tail of the url. It can be used to change the part of the url that is based on the filename. +* **url** The full path to the content from the web root. It makes no assumptions about the path of the content file. It also ignores any language prefixes of the multilingual feature. + +*If neither `slug` or `url` is present, the filename will be used.* + +## Configure Blackfriday rendering + +It's possible to set some options for Markdown rendering in the page's front matter as an override to the site wide configuration. + +See [Configuration]({{< ref "overview/configuration.md#configure-blackfriday-rendering" >}}) for more. + diff --git a/docs/content/content/markdown-extras.md b/docs/content/content/markdown-extras.md new file mode 100644 index 000000000..7673c53da --- /dev/null +++ b/docs/content/content/markdown-extras.md @@ -0,0 +1,49 @@ +--- +aliases: +- /doc/supported-formats/ +lastmod: 2016-07-22 +date: 2016-07-22 +menu: + main: + parent: content +prev: /content/summaries +next: /content/multilingual +title: Markdown Extras +weight: 66 +toc: false +--- + +Hugo provides some convenient markdown extensions. + +## Task lists + +Hugo supports GitHub styled task lists (TODO lists) for the Blackfriday renderer (md-files). See [Blackfriday config](/overview/configuration/#configure-blackfriday-rendering) for how to turn it off. + +Example: + +```markdown +- [ ] a task list item +- [ ] list syntax required +- [ ] incomplete +- [x] completed +``` + +Renders as: + +- [ ] a task list item +- [ ] list syntax required +- [ ] incomplete +- [x] completed + + +And produces this HTML: + +```html + +
      +
    • a task list item
    • +
    • list syntax required
    • +
    • incomplete
    • +
    • completed
    • +
    +``` diff --git a/docs/content/content/multilingual.md b/docs/content/content/multilingual.md new file mode 100644 index 000000000..5e09bc539 --- /dev/null +++ b/docs/content/content/multilingual.md @@ -0,0 +1,232 @@ +--- +date: 2016-01-02T21:21:00Z +menu: + main: + parent: content +prev: /content/markdown-extras +next: /content/example +title: Multilingual Mode +weight: 68 +toc: true +--- +Hugo supports multiple languages side-by-side (added in `Hugo 0.17`). Define the available languages in a `Languages` section in your top-level `config.toml` (or equivalent). + +Example: + +``` +DefaultContentLanguage = "en" +copyright = "Everything is mine" + +[params.navigation] +help = "Help" + +[Languages] +[Languages.en] +title = "My blog" +weight = 1 +[Languages.en.params] +linkedin = "english-link" + +[Languages.fr] +copyright = "Tout est à moi" +title = "Mon blog" +weight = 2 +[Languages.fr.params] +linkedin = "lien-francais" +[Languages.fr.navigation] +help = "Aide" + +``` + +Anything not defined in a `[Languages]` block will fall back to the global +value for that key (like `copyright` for the English (`en`) language in this example). + +With the config above, all content, sitemap, RSS feeds, paginations +and taxonomy pages will be rendered below `/` in English (your default content language), and below `/fr` in French. + +When working with params in frontmatter pages, omit the `params` in the key for the translation. + +If you want all of the languages to be put below their respective language code, enable `defaultContentLanguageInSubdir: true` in your configuration. + +Only the obvious non-global options can be overridden per language. Examples of global options are `BaseURL`, `BuildDrafts`, etc. + +Taxonomies and Blackfriday configuration can also be set per language, example: + +``` +[Taxonomies] +tag = "tags" + +[blackfriday] +angledQuotes = true +hrefTargetBlank = true + +[Languages] +[Languages.en] +weight = 1 +title = "English" +[Languages.en.blackfriday] +angledQuotes = false + +[Languages.fr] +weight = 2 +title = "Français" +[Languages.fr.Taxonomies] +plaque = "plaques" +``` + + +### Translating your content + +Translated articles are identified by the name of the content file. + +Example of translated articles: + +1. `/content/about.en.md` +2. `/content/about.fr.md` + +You can also have: + +1. `/content/about.md` +2. `/content/about.fr.md` + +In which case the config variable `defaultContentLanguage` will be used to affect the default language `about.md`. This way, you can +slowly start to translate your current content without having to rename everything. + +If left unspecified, the value for `defaultContentLanguage` defaults to `en`. + +By having the same _base file name_, the content pieces are linked together as translated pieces. + +If you need distinct URLs per language you can set the slug in the non-default language file. Just define the custom slug for the french translation in your `/content/about.fr.md` file: + +``` +--- +slug: "a-propos" +--- +``` + +You will get both `/about/` and `/a-propos/` URLs in your build, properly linked as translated pieces. + +### Link to translated content + +To create a list of links to translated content, use a template similar to this: + +``` +{{ if .IsTranslated }} +

    {{ i18n "translations" }}

    + +{{ end }} +``` +The above can be put in a `partial` and included in any template, be it for a content page or the home page. It will not print anything if there are no translations for a given page, or if it is -- in the case of the home page, section listing etc. -- a site with only one language. + +The above also uses the `i18n` func, see [Translation of strings](#translation-of-strings). + +### Translation of strings + +Hugo uses [go-i18n](https://github.com/nicksnyder/go-i18n) to support string translations. Follow the link to find tools to manage your translation workflows. + +Translations are collected from the `themes/[name]/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. + +From within your templates, use the `i18n` function like this: + +``` +{{ i18n "home" }} +``` + +This uses a definition like this one in `i18n/en-US.toml`: + +``` +[home] +other = "Home" +``` + +Often you will want to use to the page variables in the translations strings. To do that, pass on the "." context when calling `i18n`: + +``` +{{ i18n "wordCount" . }} +``` + +This uses a definition like this one in `i18n/en-US.toml`: + +``` +[wordCount] +other = "This article has {{ .WordCount }} words." +``` +An example of singular and plural form: + +``` +[readingTime] +one = "One minute read" +other = "{{.Count}} minutes read" +``` +And then in the template: + +``` +{{ i18n "readingTime" .ReadingTime }} +``` +To track down missing translation strings, run Hugo with the `--i18n-warnings` flag: + +```bash + hugo --i18n-warnings | grep i18n +i18n|MISSING_TRANSLATION|en|wordCount +``` + +### Menus + +You can define your menus for each language independently. The [creation of a menu]({{< relref "extras/menus.md" >}}) works analogous to earlier versions of Hugo, except that they have to be defined in their language-specific block in the configuration file: + +```toml +defaultContentLanguage = "en" + +[languages.en] +weight = 0 +languageName = "English" + +[[languages.en.menu.main]] +url = "/" +name = "Home" +weight = 0 + + +[languages.de] +weight = 10 +languageName = "Deutsch" + +[[languages.de.menu.main]] +url = "/" +name = "Startseite" +weight = 0 +``` + +The rendering of the main navigation works as usual. `.Site.Menus` will just contain the menu of the current language. Pay attention to the generation of the menu links. `absLangURL` takes care that you link to the correct locale of your website. Otherwise, both menu entries would link to the English version because it's the default content language that resides in the root directory. + +```html +
      + {{- $currentPage := . -}} + {{ range .Site.Menus.main -}} +
    • + {{ .Name }} +
    • + {{- end }} +
    + +``` + +### 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 site, it can be handy to have a visual indicator of missing translations. The `EnableMissingTranslationPlaceholders` config option will flag all untranslated strings with the placeholder `[i18n] identifier`, where `identifier` is the id of the missing translation. + +**Remember: Hugo will generate your website with these placeholders. It might not be suited for production environments.** + +### Multilingual Themes support + +To support Multilingual mode in your themes, some considerations must be taken for the URLs in the templates. If there are more than one language, URLs must either come from the built-in `.Permalink` or `.URL`, be constructed with `relLangURL` or `absLangURL` template funcs -- or prefixed with `{{.LanguagePrefix }}`. + +If there are 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, so it is harmless for single-language sites. diff --git a/docs/content/content/ordering.md b/docs/content/content/ordering.md new file mode 100644 index 000000000..d0933a608 --- /dev/null +++ b/docs/content/content/ordering.md @@ -0,0 +1,41 @@ +--- +lastmod: 2015-12-23 +date: 2014-03-06 +linktitle: Ordering +menu: + main: + parent: content +next: /content/summaries +prev: /content/archetypes +title: Ordering Content +weight: 60 +--- + +Hugo provides you with all the flexibility you need to organize how your content is ordered. + +By default, content is ordered by weight, then by date with the most +recent date first, but alternative sorting (by `title` and `linktitle`) is +also available. The order the content would appear is specified in +the [list template](/templates/list/). + +_Both the `date` and `weight` fields are optional._ + +Unweighted pages appear at the end of the list. If no weights are provided (or +if weights are the same), `date` will be used to sort. If neither is provided, +content will be ordered based on how it's read off the disk, and no order is +guaranteed. + +## Assigning weight to content + +```toml ++++ +weight = 4 +title = "Three" +date = "2012-04-06" ++++ +Front Matter with Ordered Pages 3 +``` + +## Ordering Content Within Taxonomies + +Please see the [Taxonomy Ordering Documentation](/taxonomies/ordering/). diff --git a/docs/content/content/organization.md b/docs/content/content/organization.md new file mode 100644 index 000000000..bd83fbf04 --- /dev/null +++ b/docs/content/content/organization.md @@ -0,0 +1,175 @@ +--- +aliases: +- /doc/organization/ +lastmod: 2015-09-27 +date: 2013-07-01 +linktitle: Organization +menu: + main: + parent: content +next: /content/supported-formats +prev: /overview/source-directory +title: Content Organization +weight: 10 +toc: true +--- + +Hugo uses files (see [supported formats](/content/supported-formats/)) with headers commonly called the *front matter*. Hugo +respects the organization that you provide for your content to minimize any +extra configuration, though this can be overridden by additional configuration +in the front matter. + +## Organization + +In Hugo, the content should be arranged in the same way they are intended for +the rendered website. Without any additional configuration, the following will +just work. Hugo supports content nested at any level. The top level is special +in Hugo and is used as the [section](/content/sections/). + + . + └── content + └── about + | └── _index.md // <- http://1.com/about/ + ├── post + | ├── firstpost.md // <- http://1.com/post/firstpost/ + | ├── happy + | | └── ness.md // <- http://1.com/post/happy/ness/ + | └── secondpost.md // <- http://1.com/post/secondpost/ + └── quote + ├── first.md // <- http://1.com/quote/first/ + └── second.md // <- http://1.com/quote/second/ + +**Here's the same organization run with `hugo --uglyURLs`** + + . + └── content + └── about + | └── _index.md // <- http://1.com/about/ + ├── post + | ├── firstpost.md // <- http://1.com/post/firstpost.html + | ├── happy + | | └── ness.md // <- http://1.com/post/happy/ness.html + | └── secondpost.md // <- http://1.com/post/secondpost.html + └── quote + ├── first.md // <- http://1.com/quote/first.html + └── second.md // <- http://1.com/quote/second.html + +## Destinations + +Hugo believes that you organize your content with a purpose. The same structure +that works to organize your source content is used to organize the rendered +site. As displayed above, the organization of the source content will be +mirrored in the destination. + +Notice that the first level `about/` page URL was created using a directory +named "about" with a single `_index.md` file inside. Find out more about `_index.md` specifically in [content for the homepage and other list pages](https://gohugo.io/overview/source-directory#content-for-home-page-and-other-list-pages). + +There are times when one would need more control over their content. In these +cases, there are a variety of things that can be specified in the front matter +to determine the destination of a specific piece of content. + +The following items are defined in order; latter items in the list will override +earlier settings. + +### filename +This isn't in the front matter, but is the actual name of the file minus the +extension. This will be the name of the file in the destination. + +### slug +Defined in the front matter, the `slug` can take the place of the filename for the +destination. + +### filepath +The actual path to the file on disk. Destination will create the destination +with the same path. Includes [section](/content/sections/). + +### section +`section` is determined by its location on disk and *cannot* be specified in the front matter. See [section](/content/sections/). + +### type +`type` is also determined by its location on disk but, unlike `section`, it *can* be specified in the front matter. See [type](/content/types/). + +### path +`path` can be provided in the front matter. This will replace the actual +path to the file on disk. Destination will create the destination with the same +path. Includes [section](/content/sections/). + +### url +A complete URL can be provided. This will override all the above as it pertains +to the end destination. This must be the path from the baseURL (starting with a "/"). +When a `url` is provided, it will be used exactly. Using `url` will ignore the +`--uglyURLs` setting. + + +## Path breakdown in Hugo + +### Content + + . path slug + . ⊢-------^----⊣ ⊢------^-------⊣ + content/extras/indexes/category-example/index.html + + + . section slug + . ⊢--^--⊣ ⊢------^-------⊣ + content/extras/indexes/category-example/index.html + + + . section slug + . ⊢--^--⊣⊢--^--⊣ + content/extras/indexes/index.html + +### Destination + + + permalink + ⊢--------------^-------------⊣ + http://spf13.com/projects/hugo + + + baseURL section slug + ⊢-----^--------⊣ ⊢--^---⊣ ⊢-^⊣ + http://spf13.com/projects/hugo + + + baseURL section slug + ⊢-----^--------⊣ ⊢--^--⊣ ⊢--^--⊣ + http://spf13.com/extras/indexes/example + + + baseURL path slug + ⊢-----^--------⊣ ⊢------^-----⊣ ⊢--^--⊣ + http://spf13.com/extras/indexes/example + + + baseURL url + ⊢-----^--------⊣ ⊢-----^-----⊣ + http://spf13.com/projects/hugo + + + baseURL url + ⊢-----^--------⊣ ⊢--------^-----------⊣ + http://spf13.com/extras/indexes/example + + + +**section** = which type the content is by default + +* based on content location +* front matter overrides + +**slug** = name.ext or name/ + +* based on content-name.md +* front matter overrides + +**path** = section + path to file excluding slug + +* based on path to content location + + +**url** = relative URL + +* defined in front matter +* overrides all the above + diff --git a/docs/content/content/sections.md b/docs/content/content/sections.md new file mode 100644 index 000000000..c603c0872 --- /dev/null +++ b/docs/content/content/sections.md @@ -0,0 +1,54 @@ +--- +lastmod: 2015-12-23 +date: 2013-07-01 +menu: + main: + parent: content +next: /content/types +notoc: true +prev: /content/front-matter +title: Sections +weight: 30 +--- + +Hugo believes that you organize your content with a purpose. The same structure +that works to organize your source content is used to organize the rendered +site (see [Organization](/content/organization/)). Following this pattern Hugo +uses the top level of your content organization as **the Section**. + +The following example site uses two sections, "post" and "quote". + +{{< nohighlight >}}. +└── content + ├── post + | ├── firstpost.md // <- http://1.com/post/firstpost/ + | ├── happy + | | └── ness.md // <- http://1.com/post/happy/ness/ + | └── secondpost.md // <- http://1.com/post/secondpost/ + └── quote + ├── first.md // <- http://1.com/quote/first/ + └── second.md // <- http://1.com/quote/second/ +{{< /nohighlight >}} + +## Section Lists + +Hugo will automatically create pages for each section root that list all +of the content in that section. See [List Templates](/templates/list/) +for details on customizing the way they appear. + +Section pages can also have a content file and frontmatter, see [Source Organization]({{< relref "overview/source-directory.md#content-for-home-page-and-other-list-pages" >}}). + +## Sections and Types + +By default everything created within a section will use the content type +that matches the section name. + +Section defined in the front matter have the same impact. + +To change the type of a given piece of content, simply define the type +in the front matter. + +If a layout for a given type hasn't been provided, a default type template will +be used instead provided it exists. + + diff --git a/docs/content/content/summaries.md b/docs/content/content/summaries.md new file mode 100644 index 000000000..1f65d431d --- /dev/null +++ b/docs/content/content/summaries.md @@ -0,0 +1,54 @@ +--- +lastmod: 2015-01-27 +date: 2013-07-01 +menu: + main: + parent: content +notoc: true +prev: /content/ordering +next: /content/markdown-extras +title: Summaries +weight: 65 +--- + +With the use of the `.Summary` [page variable](/templates/variables/), Hugo can generate summaries of content to show snippets in summary views. The summary view snippets are automatically generated by Hugo. Where a piece of content is split for the content summary depends on whether the split is Hugo-defined or user-defined. + +Content summaries may also provide links to the original content, usually in the form of a "Read More..." link, with the help of the `.RelPermalink` or `.Permalink` variable, as well as the `.Truncated` boolean variable to determine whether such "Read More..." link is necessary. + +## Hugo-defined: automatic summary split + +By default, Hugo automatically takes the first 70 words of your content as its summary and stores it into the `.Summary` variable, which you may use in your templates. + +* Pros: Automatic, no additional work on your part. +* Cons: All HTML tags are stripped from the summary, and the first 70 words, whether they belong to a heading or to different paragraphs, are all lumped into one paragraph. Some people like it, but some people don't. + +## User-defined: manual summary split: + +Alternatively, you may add the <!--more--> summary divider[^1] (for org content, use # more) where you want to split the article. Content prior to the summary divider will be used as that content's summary, and stored into the `.Summary` variable with all HTML formatting intact. + +[^1]: The **summary divider** is also called "more tag", "excerpt separator", etc. in other literature. + +* Pros: Freedom, precision, and improved rendering. All formatting is preserved. +* Cons: Need to remember to type <!--more--> (or # more for org content) in your content file. :-) + +Be careful to enter <!--more--> (or # more for org content) exactly, i.e. all lowercase with no whitespace, otherwise it would be treated as regular comment and ignored. + +If there is nothing but spaces and newlines after the summary divider then `.Truncated` will be false. + +## Showing Summaries + +You can show content summaries with the following code. You could do this, for example, on a [list](/templates/list/) page. + + {{ range first 10 .Data.Pages }} +
    +

    {{ .Title }}

    + {{ .Summary }} +
    + {{ if .Truncated }} + + {{ end }} + {{ end }} + +Note how the `.Truncated` boolean valuable may be used to hide the "Read More..." link when the content is not truncated, i.e. when the summary contains the entire article. diff --git a/docs/content/content/supported-formats.md b/docs/content/content/supported-formats.md new file mode 100644 index 000000000..3fc905d6d --- /dev/null +++ b/docs/content/content/supported-formats.md @@ -0,0 +1,27 @@ +--- +aliases: +- /doc/supported-formats/ +lastmod: 2015-08-01 +date: 2015-08-01 +menu: + main: + parent: content +next: /content/front-matter +prev: /content/organization +title: Supported Formats +weight: 15 +toc: true +--- + + Since 0.14, Hugo has defined a new concept called _external helpers_. It means that you can write your content using Asciidoc[tor], reStructuredText or Org-Mode. If you have files with associated extensions ([details](https://github.com/gohugoio/hugo/blob/77c60a3440806067109347d04eb5368b65ea0fe8/helpers/general.go#L65)), then Hugo will call external commands to generate the content (the exception being Org-Mode content, which is parsed natively). + + This means that you will have to install the associated tool on your machine to be able to use those formats. + + For example, for Asciidoc files, Hugo will try to call __asciidoctor__ or __asciidoc__ command. + + To use those formats, just use the standard extension and the front matter exactly as you would do with natively supported _.md_ files. + + Notes: + + * as these are external commands, generation performance for that content will heavily depend on the performance of those external tools. + * this feature is still in early stage, hence feedback is even more welcome. diff --git a/docs/content/content/types.md b/docs/content/content/types.md new file mode 100644 index 000000000..277294881 --- /dev/null +++ b/docs/content/content/types.md @@ -0,0 +1,80 @@ +--- +lastmod: 2015-09-28 +date: 2013-07-01 +linktitle: Types +menu: + main: + parent: content +next: /content/archetypes +prev: /content/sections +title: Content Types +weight: 40 +toc: true +--- + +Hugo has full support for different types of content. A content type can have a +unique set of meta data, template and can be automatically created by the `hugo new` +command through using content [archetypes](/content/archetypes/). + +A good example of when multiple types are needed is to look at [Tumblr](https://www.tumblr.com/). A piece +of content could be a photo, quote or post, each with different meta data and +rendered differently. + +## Assigning a content type + +Hugo assumes that your site will be organized into [sections](/content/sections/) +and each section will use the corresponding type. If you are taking advantage of +this, then each new piece of content you place into a section will automatically +inherit the type. + +Alternatively, you can set the type in the meta data under the key "`type`". + + +## Creating new content of a specific type + +Hugo has the ability to create a new content file and populate the front matter +with the data set corresponding to that type. Hugo does this by utilizing +[archetypes](/content/archetypes/). + +To create a new piece of content, use: + + hugo new relative/path/to/content.md + +For example, if I wanted to create a new post inside the post section, I would type: + + hugo new post/my-newest-post.md + + +## Defining a content type + +Creating a new content type is easy in Hugo. You simply provide the templates and archetype +that the new type will use. You only need to define the templates, archetypes and/or views +unique to that content type. Hugo will fall back to using the general templates and default archetype +whenever a specific file is not present. + +*Remember, all of the following are optional:* + +### Create Type Directory +Create a directory with the name of the type in `/layouts`. Type is always singular. *E.g. `/layouts/post`*. + +### Create single template +Create a file called `single.html` inside your directory. *E.g. `/layouts/post/single.html`*. + +### Create list template +Create a file called `post.html` inside the section lists template directory, `/layouts/section`. *E.g. `/layouts/section/post.html`*. + +### Create views +Many sites support rendering content in a few different ways, for +instance, a single page view and a summary view to be used when +displaying a [list of contents on a single page](/templates/list). +Hugo makes no assumptions here about how you want to display your +content, and will support as many different views of a content type +as your site requires. All that is required for these additional +views is that a template exists in each `/layouts/TYPE` directory +with the same name. + +### Create a corresponding archetype + +Create a file called type.md in the `/archetypes` directory. *E.g. `/archetypes/post.md`*. + +More details about archetypes can be found at the [archetypes docs](/content/archetypes/). diff --git a/docs/content/content/using-index-md.md b/docs/content/content/using-index-md.md new file mode 100644 index 000000000..1f1298f67 --- /dev/null +++ b/docs/content/content/using-index-md.md @@ -0,0 +1,118 @@ +--- +aliases: +- /doc/using-index-md/ +lastmod: 2017-02-22 +date: 2017-02-22 +linktitle: Using _index.md +menu: + main: + parent: content +prev: /content/example +next: /themes/overview +notoc: true +title: Using _index.md +weight: 70 +--- +# \_index.md and 'Everything is a Page' + +As of version v0.18 Hugo now treats '[everything as a page](http://bepsays.com/en/2016/12/19/hugo-018/)'. This allows you to add content and frontmatter to any page - including List pages like [Sections](/content/sections/), [Taxonomies](/taxonomies/overview/), [Taxonomy Terms pages](/templates/terms/) and even to potential 'special case' pages like the [Home page](/templates/homepage/). + +In order to take advantage of this behaviour you need to do a few things. + +1. Create an \_index.md file that contains the frontmatter and content you would like to apply. + +2. Place the \_index.md file in the correct place in the directory structure. + +3. Ensure that the respective template is configured to display `{{ .Content }}` if you wish for the content of the \_index.md file to be rendered on the respective page. + +## How \_index.md pages work + +Before continuing it's important to know that this page must reference certain templates to describe how the \_index.md page will be rendered. Hugo has a multitude of possible templates that can be used and placed in various places (think theme templates for instance). For simplicity/brevity the default/top level template location will be used to refer to the entire range of places the template can be placed. + +If this is confusing or you are unfamiliar with Hugo's template hierarchy, visit the various template pages listed below. You may need to find the 'active' template responsible for any particular page on your own site by going through the template hierarchy and matching it to your particular setup/theme you are using. + +- [Home page template](/templates/homepage/) +- [Content List templates](/templates/list/) +- [Single Content templates](/templates/content/) +- [Taxonomy Terms templates](/templates/terms/) + +Now that you've got a handle on templates lets recap some Hugo basics to understand how to use an \_index.md file with a List page. + +1. Sections and Taxonomies are 'List' pages, NOT single pages. +2. List pages are rendered using the template hierarchy found in the [Content - List Template](/templates/list/) docs. +3. The Home page, though technically a List page, can have [its own template](/templates/homepage/) at layouts/index.html rather than \_default/list.html. Many themes exploit this behaviour so you are likely to encounter this specific use case. +4. Taxonomy terms pages are 'lists of metadata' not lists of content, so [have their own templates](/templates/terms/). + +Let's put all this information together: + +> **\_index.md files used in List pages, Terms pages or the Home page are NOT rendered as single pages or with Single Content templates.** + +> **All pages, including List pages, can have frontmatter and frontmatter can have markdown content - meaning \_index.md files are the way to _provide_ frontmatter and content to the respective List/Terms/Home page.** + +Here are a couple of examples to make it clearer... + +| \_index.md location | Page affected | Rendered by | +| ------------------- | ------------ | ----------- | +| /content/post/\_index.md | site.com/post/ | /layouts/section/post.html | +| /content/categories/hugo/\_index.md | site.com/categories/hugo/ | /layouts/taxonomy/hugo.html | + +## Why \_index.md files are used + +With a Single page such as a post it's possible to add the frontmatter and content directly into the .md page itself. With List/Terms/Home pages this is not possible so \_index.md files can be used to provide that frontmatter/content to them. + +## How to display content from \_index.md files + +From the information above it should follow that content within an \_index.md file won't be rendered in its own Single Page, instead it'll be made available to the respective List/Terms/Home page. + +To **_actually render that content_** you need to ensure that the relevant template responsible for rendering the List/Terms/Home page contains (at least) `{{ .Content }}`. + +This is the way to actually display the content within the \_index.md file on the List/Terms/Home page. + +A very simple/naive example of this would be: + +```html +{{ partial "header.html" . }} +
    + {{ .Content }} + {{ range .Paginator.Pages }} + {{ partial "summary.html" . }} + {{ end }} + {{ partial "pagination.html" . }} +
    +{{ partial "sidebar.html" . }} +{{ partial "footer.html" . }} +``` + +You can see `{{ .Content }}` just after the `
    ` element. For this particular example, the content of the \_index.md file will show before the main list of summaries. + +## Where to organise an \_index.md file + +To add content and frontmatter to the home page, a section, a taxonomy or a taxonomy terms listing, add a markdown file with the base name \_index on the relevant place on the file system. + +```bash +└── content + ├── _index.md + ├── categories + │   ├── _index.md + │   └── photo + │   └── _index.md + ├── post + │   ├── _index.md + │   └── firstpost.md + └── tags + ├── _index.md + └── hugo + └── _index.md +``` + +In the above example \_index.md pages have been added to each section/taxonomy. + +An \_index.md file has also been added in the top level 'content' directory. + +### Where to place \_index.md for the Home page + +Hugo themes are designed to use the 'content' directory as the root of the website, so adding an \_index.md file here (like has been done in the example above) is how you would add frontmatter/content to the home page. + + + + diff --git a/docs/content/en/_common/_index.md b/docs/content/en/_common/_index.md deleted file mode 100644 index 612165e5c..000000000 --- a/docs/content/en/_common/_index.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -cascade: - build: - list: never - publishResources: false - render: never ---- - - diff --git a/docs/content/en/_common/content-format-table.md b/docs/content/en/_common/content-format-table.md deleted file mode 100644 index c0a66a146..000000000 --- a/docs/content/en/_common/content-format-table.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -Content format|Media type|Identifier|File extensions -:--|:--|:--|:-- -Markdown|`text/markdown`|`markdown`|`markdown`,`md`, `mdown` -HTML|`text/html`|`html`|`htm`, `html` -Emacs Org Mode|`text/org`|`org`|`org` -AsciiDoc|`text/asciidoc`|`asciidoc`|`ad`, `adoc`, `asciidoc` -Pandoc|`text/pandoc`|`pandoc`|`pandoc`, `pdc` -reStructuredText|`text/rst`|`rst`|`rst` - diff --git a/docs/content/en/_common/filter-sort-group.md b/docs/content/en/_common/filter-sort-group.md deleted file mode 100644 index ac73766da..000000000 --- a/docs/content/en/_common/filter-sort-group.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -> [!note] -> The [page collections quick reference guide] describes methods and functions to filter, sort, and group page collections. - -[page collections quick reference guide]: /quick-reference/page-collections/ diff --git a/docs/content/en/_common/functions/fmt/format-string.md b/docs/content/en/_common/functions/fmt/format-string.md deleted file mode 100644 index 09a9ee867..000000000 --- a/docs/content/en/_common/functions/fmt/format-string.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -The documentation for Go's [fmt] package describes the structure and content of the format string. - -[fmt]: https://pkg.go.dev/fmt diff --git a/docs/content/en/_common/functions/go-html-template-package.md b/docs/content/en/_common/functions/go-html-template-package.md deleted file mode 100644 index 57992ea66..000000000 --- a/docs/content/en/_common/functions/go-html-template-package.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -Hugo uses Go's [text/template] and [html/template] packages. - -The text/template package implements data-driven templates for generating textual output, while the html/template package implements data-driven templates for generating HTML output safe against code injection. - -By default, Hugo uses the html/template package when rendering HTML files. - -To generate HTML output that is safe against code injection, the html/template package escapes strings in certain contexts. - -[text/template]: https://pkg.go.dev/text/template -[html/template]: https://pkg.go.dev/html/template diff --git a/docs/content/en/_common/functions/go-template/text-template.md b/docs/content/en/_common/functions/go-template/text-template.md deleted file mode 100644 index 4b934c1e9..000000000 --- a/docs/content/en/_common/functions/go-template/text-template.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -See Go's [text/template] documentation for more information. - -[text/template]: https://pkg.go.dev/text/template diff --git a/docs/content/en/_common/functions/images/apply-image-filter.md b/docs/content/en/_common/functions/images/apply-image-filter.md deleted file mode 100644 index 08e08238f..000000000 --- a/docs/content/en/_common/functions/images/apply-image-filter.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -Apply the filter using the [`images.Filter`] function: - -[`images.Filter`]: /functions/images/filter/ - -```go-html-template -{{ with resources.Get "images/original.jpg" }} - {{ with . | images.Filter $filter }} - - {{ end }} -{{ end }} -``` - -You can also apply the filter using the [`Filter`] method on a `Resource` object: - -[`Filter`]: /methods/resource/filter/ - -```go-html-template -{{ with resources.Get "images/original.jpg" }} - {{ with .Filter $filter }} - - {{ end }} -{{ end }} -``` diff --git a/docs/content/en/_common/functions/js/options.md b/docs/content/en/_common/functions/js/options.md deleted file mode 100644 index 475429d05..000000000 --- a/docs/content/en/_common/functions/js/options.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -params -: (`map` or `slice`) Params that can be imported as JSON in your JS files, e.g. - - ```go-html-template - {{ $js := resources.Get "js/main.js" | js.Build (dict "params" (dict "api" "https://example.org/api")) }} - ``` - And then in your JS file: - - ```js - import * as params from '@params'; - ``` - - Note that this is meant for small data sets, e.g., configuration settings. For larger data sets, please put/mount the files into `assets` and import them directly. - -minify -: (`bool`) Whether to let `js.Build` handle the minification. - -loaders -: {{< new-in 0.140.0 />}} -: (`map`) Configuring a loader for a given file type lets you load that file type with an `import` statement or a `require` call. For example, configuring the `.png` file extension to use the data URL loader means importing a `.png` file gives you a data URL containing the contents of that image. Loaders available are `none`, `base64`, `binary`, `copy`, `css`, `dataurl`, `default`, `empty`, `file`, `global-css`, `js`, `json`, `jsx`, `local-css`, `text`, `ts`, `tsx`. See https://esbuild.github.io/api/#loader. - -inject -: (`slice`) This option allows you to automatically replace a global variable with an import from another file. The path names must be relative to `assets`. See https://esbuild.github.io/api/#inject. - -shims -: (`map`) This option allows swapping out a component with another. A common use case is to load dependencies like React from a CDN (with _shims_) when in production, but running with the full bundled `node_modules` dependency during development: - - ```go-html-template - {{ $shims := dict "react" "js/shims/react.js" "react-dom" "js/shims/react-dom.js" }} - {{ $js = $js | js.Build dict "shims" $shims }} - ``` - - The _shim_ files may look like these: - - ```js - // js/shims/react.js - module.exports = window.React; - ``` - - ```js - // js/shims/react-dom.js - module.exports = window.ReactDOM; - ``` - - With the above, these imports should work in both scenarios: - - ```js - import * as React from 'react'; - import * as ReactDOM from 'react-dom/client'; - ``` - -target -: (`string`) The language target. One of: `es5`, `es2015`, `es2016`, `es2017`, `es2018`, `es2019`, `es2020`, `es2021`, `es2022`, `es2023`, `es2024`, or `esnext`. Default is `esnext`. - -platform -: {{< new-in 0.140.0 />}} -: (`string`) One of `browser`, `node`, `neutral`. Default is `browser`. See https://esbuild.github.io/api/#platform. - -externals -: (`slice`) External dependencies. Use this to trim dependencies you know will never be executed. See https://esbuild.github.io/api/#external. - -defines -: (`map`) This option allows you to define a set of string replacements to be performed when building. It must be a map where each key will be replaced by its value. - - ```go-html-template - {{ $defines := dict "process.env.NODE_ENV" `"development"` }} - ``` - -drop -: {{< new-in 0.144.0 />}} -: (`string`) Edit your source code before building to drop certain constructs: One of `debugger` or `console`. -: See https://esbuild.github.io/api/#drop - -sourceMap -: (`string`) Whether to generate `inline`, `linked`, or `external` source maps from esbuild. Linked and external source maps will be written to the target with the output file name + ".map". When `linked` a `sourceMappingURL` will also be written to the output file. By default, source maps are not created. Note that the `linked` option was added in Hugo 0.140.0. - -sourcesContent -: {{< new-in 0.140.0 />}} -: (`bool`) Whether to include the content of the source files in the source map. By default, this is `true`. - -JSX -: {{< new-in 0.124.0 />}} -: (`string`) How to handle/transform JSX syntax. One of: `transform`, `preserve`, `automatic`. Default is `transform`. Notably, the `automatic` transform was introduced in React 17+ and will cause the necessary JSX helper functions to be imported automatically. See https://esbuild.github.io/api/#jsx. - -JSXImportSource -: {{< new-in 0.124.0 />}} -: (`string`) Which library to use to automatically import its JSX helper functions from. This only works if `JSX` is set to `automatic`. The specified library needs to be installed through npm and expose certain exports. See https://esbuild.github.io/api/#jsx-import-source. - - The combination of `JSX` and `JSXImportSource` is helpful if you want to use a non-React JSX library like Preact, e.g.: - - ```go-html-template - {{ $js := resources.Get "js/main.jsx" | js.Build (dict "JSX" "automatic" "JSXImportSource" "preact") }} - ``` - - With the above, you can use Preact components and JSX without having to manually import `h` and `Fragment` every time: - - ```jsx - import { render } from 'preact'; - - const App = () => <>Hello world!; - - const container = document.getElementById('app'); - if (container) render(, container); - ``` diff --git a/docs/content/en/_common/functions/locales.md b/docs/content/en/_common/functions/locales.md deleted file mode 100644 index 1cfd7a1e6..000000000 --- a/docs/content/en/_common/functions/locales.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -> [!note] -> Localization of dates, currencies, numbers, and percentages is performed by the [gohugoio/locales] package. The language tag of the current site must match one of the listed locales. - -[gohugoio/locales]: https://github.com/gohugoio/locales diff --git a/docs/content/en/_common/functions/regular-expressions.md b/docs/content/en/_common/functions/regular-expressions.md deleted file mode 100644 index 58f81a2ee..000000000 --- a/docs/content/en/_common/functions/regular-expressions.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -When specifying the regular expression, use a raw [string literal] (backticks) instead of an interpreted string literal (double quotes) to simplify the syntax. With an interpreted string literal you must escape backslashes. - -Go's regular expression package implements the [RE2 syntax]. The RE2 syntax is a subset of that accepted by [PCRE], roughly speaking, and with various [caveats]. Note that the RE2 `\C` escape sequence is not supported. - -[caveats]: https://swtch.com/~rsc/regexp/regexp3.html#caveats -[PCRE]: https://www.pcre.org/ -[RE2 syntax]: https://github.com/google/re2/wiki/Syntax/ -[string literal]: https://go.dev/ref/spec#String_literals diff --git a/docs/content/en/_common/functions/truthy-falsy.md b/docs/content/en/_common/functions/truthy-falsy.md deleted file mode 100644 index e15e58d61..000000000 --- a/docs/content/en/_common/functions/truthy-falsy.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -The falsy values are `false`, `0`, any `nil` pointer or interface value, any array, slice, map, or string of length zero, and zero `time.Time` values. - -Everything else is truthy. diff --git a/docs/content/en/_common/functions/urls/anchorize-vs-urlize.md b/docs/content/en/_common/functions/urls/anchorize-vs-urlize.md deleted file mode 100644 index e00c181b8..000000000 --- a/docs/content/en/_common/functions/urls/anchorize-vs-urlize.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -The [`anchorize`] and [`urlize`] functions are similar: - -[`anchorize`]: /functions/urls/anchorize/ -[`urlize`]: /functions/urls/urlize/ - -- Use the `anchorize` function to generate an HTML `id` attribute value -- Use the `urlize` function to sanitize a string for usage in a URL - -For example: - -```go-html-template -{{ $s := "A B C" }} -{{ $s | anchorize }} → a-b-c -{{ $s | urlize }} → a-b-c - -{{ $s := "a b c" }} -{{ $s | anchorize }} → a-b---c -{{ $s | urlize }} → a-b-c - -{{ $s := "< a, b, & c >" }} -{{ $s | anchorize }} → -a-b--c- -{{ $s | urlize }} → a-b-c - -{{ $s := "main.go" }} -{{ $s | anchorize }} → maingo -{{ $s | urlize }} → main.go - -{{ $s := "Hugö" }} -{{ $s | anchorize }} → hugö -{{ $s | urlize }} → hug%C3%B6 -``` diff --git a/docs/content/en/_common/glob-patterns.md b/docs/content/en/_common/glob-patterns.md deleted file mode 100644 index d3092dece..000000000 --- a/docs/content/en/_common/glob-patterns.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -Path|Pattern|Match -:--|:--|:-- -`images/foo/a.jpg`|`images/foo/*.jpg`|`true` -`images/foo/a.jpg`|`images/foo/*.*`|`true` -`images/foo/a.jpg`|`images/foo/*`|`true` -`images/foo/a.jpg`|`images/*/*.jpg`|`true` -`images/foo/a.jpg`|`images/*/*.*`|`true` -`images/foo/a.jpg`|`images/*/*`|`true` -`images/foo/a.jpg`|`*/*/*.jpg`|`true` -`images/foo/a.jpg`|`*/*/*.*`|`true` -`images/foo/a.jpg`|`*/*/*`|`true` -`images/foo/a.jpg`|`**/*.jpg`|`true` -`images/foo/a.jpg`|`**/*.*`|`true` -`images/foo/a.jpg`|`**/*`|`true` -`images/foo/a.jpg`|`**`|`true` -`images/foo/a.jpg`|`*/*.jpg`|`false` -`images/foo/a.jpg`|`*.jpg`|`false` -`images/foo/a.jpg`|`*.*`|`false` -`images/foo/a.jpg`|`*`|`false` diff --git a/docs/content/en/_common/gomodules-info.md b/docs/content/en/_common/gomodules-info.md deleted file mode 100644 index 5d88a6f9d..000000000 --- a/docs/content/en/_common/gomodules-info.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -> [!note] Hugo Modules are Go Modules -> You need [Go] version 1.18 or later and [Git] to use Hugo Modules. For older sites hosted on Netlify, please ensure the `GO_VERSION` environment variable is set to `1.18` or higher. -> -> Go Modules resources: -> - [go.dev/wiki/Modules](https://go.dev/wiki/Modules) -> - [blog.golang.org/using-go-modules](https://go.dev/blog/using-go-modules) - -[Git]: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git -[Go]: https://go.dev/doc/install diff --git a/docs/content/en/_common/installation/01-editions.md b/docs/content/en/_common/installation/01-editions.md deleted file mode 100644 index 634002822..000000000 --- a/docs/content/en/_common/installation/01-editions.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -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. - -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: - -[dart sass]: /functions/css/sass/#dart-sass -[processing images]: /content-management/image-processing/ -[transpile sass to css]: /functions/css/sass/ -[details]: /host-and-deploy/deploy-with-hugo-deploy/ diff --git a/docs/content/en/_common/installation/02-prerequisites.md b/docs/content/en/_common/installation/02-prerequisites.md deleted file mode 100644 index f27d9d56b..000000000 --- a/docs/content/en/_common/installation/02-prerequisites.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -## Prerequisites - -Although not required in all cases, [Git], [Go], and [Dart Sass] are commonly used when working with Hugo. - -Git is required to: - -- Build Hugo from source -- Use the [Hugo Modules] feature -- Install a theme as a Git submodule -- Access [commit information] from a local Git repository -- Host your site with services such as [CloudCannon], [Cloudflare Pages], [GitHub Pages], [GitLab Pages], and [Netlify] - -Go is required to: - -- Build Hugo from source -- Use the Hugo Modules feature - -Dart Sass is required to transpile Sass to CSS when using the latest features of the Sass language. - -Please refer to the relevant documentation for installation instructions: - -- [Git][git install] -- [Go][go install] -- [Dart Sass][dart sass install] - -[cloudcannon]: https://cloudcannon.com/ -[cloudflare pages]: https://pages.cloudflare.com/ -[dart sass install]: /functions/css/sass/#dart-sass -[dart sass]: https://sass-lang.com/dart-sass -[git install]: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git -[git]: https://git-scm.com/ -[github pages]: https://pages.github.com/ -[gitlab pages]: https://docs.gitlab.com/ee/user/project/pages/ -[go install]: https://go.dev/doc/install -[go]: https://go.dev/ -[netlify]: https://www.netlify.com/ diff --git a/docs/content/en/_common/installation/03-prebuilt-binaries.md b/docs/content/en/_common/installation/03-prebuilt-binaries.md deleted file mode 100644 index 34411cddd..000000000 --- a/docs/content/en/_common/installation/03-prebuilt-binaries.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -## Prebuilt binaries - -Prebuilt binaries are available for a variety of operating systems and architectures. Visit the [latest release] page, and scroll down to the Assets section. - -1. Download the archive for the desired edition, operating system, and architecture -1. Extract the archive -1. Move the executable to the desired directory -1. Add this directory to the PATH environment variable -1. Verify that you have _execute_ permission on the file - -Please consult your operating system documentation if you need help setting file permissions or modifying your PATH environment variable. - -If you do not see a prebuilt binary for the desired edition, operating system, and architecture, install Hugo using one of the methods described below. - -[commit information]: /methods/page/gitinfo/ -[Git]: https://git-scm.com/ -[Go]: https://go.dev/ -[Hugo Modules]: /hugo-modules/ -[latest release]: https://github.com/gohugoio/hugo/releases/latest diff --git a/docs/content/en/_common/installation/04-build-from-source.md b/docs/content/en/_common/installation/04-build-from-source.md deleted file mode 100644 index 3ce245f4a..000000000 --- a/docs/content/en/_common/installation/04-build-from-source.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -## Build from source - -To build the extended or extended/deploy edition from source you must: - -1. Install [Git] -1. Install [Go] version 1.23.0 or later -1. Install a C compiler, either [GCC] or [Clang] -1. Update your `PATH` environment variable as described in the [Go documentation] - -> The install directory is controlled by the `GOPATH` and `GOBIN` environment variables. If `GOBIN` is set, binaries are installed to that directory. If `GOPATH` is set, binaries are installed to the bin subdirectory of the first directory in the `GOPATH` list. Otherwise, binaries are installed to the bin subdirectory of the default `GOPATH` (`$HOME/go` or `%USERPROFILE%\go`). - -To build the standard edition: - -```sh -go install github.com/gohugoio/hugo@latest -``` - -To build the extended edition: - -```sh -CGO_ENABLED=1 go install -tags extended github.com/gohugoio/hugo@latest -``` - -To build the extended/deploy edition: - -```sh -CGO_ENABLED=1 go install -tags extended,withdeploy github.com/gohugoio/hugo@latest -``` - -[Clang]: https://clang.llvm.org/ -[GCC]: https://gcc.gnu.org/ -[Git]: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git -[Go documentation]: https://go.dev/doc/code#Command -[Go]: https://go.dev/doc/install diff --git a/docs/content/en/_common/installation/homebrew.md b/docs/content/en/_common/installation/homebrew.md deleted file mode 100644 index 14f48174e..000000000 --- a/docs/content/en/_common/installation/homebrew.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -### Homebrew - -[Homebrew] is a free and open-source package manager for macOS and Linux. To install the extended edition of Hugo: - -```sh -brew install hugo -``` - -[Homebrew]: https://brew.sh/ diff --git a/docs/content/en/_common/menu-entries/pre-and-post.md b/docs/content/en/_common/menu-entries/pre-and-post.md deleted file mode 100644 index da3d584d1..000000000 --- a/docs/content/en/_common/menu-entries/pre-and-post.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -In this site configuration we enable rendering of [emoji shortcodes], and add emoji shortcodes before (pre) and after (post) each menu entry: - -{{< code-toggle file=hugo >}} -enableEmoji = true - -[[menus.main]] -name = 'About' -pageRef = '/about' -post = ':point_left:' -pre = ':point_right:' -weight = 10 - -[[menus.main]] -name = 'Contact' -pageRef = '/contact' -post = ':arrow_left:' -pre = ':arrow_right:' -weight = 20 -{{< /code-toggle >}} - -To render the menu: - -```go-html-template -
      - {{ range .Site.Menus.main }} -
    • - {{ .Pre | markdownify }} - {{ .Name }} - {{ .Post | markdownify }} -
    • - {{ end }} -
    -``` - -[emoji shortcodes]: /quick-reference/emojis/ diff --git a/docs/content/en/_common/menu-entry-properties.md b/docs/content/en/_common/menu-entry-properties.md deleted file mode 100644 index daeadd79d..000000000 --- a/docs/content/en/_common/menu-entry-properties.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -_comment: Do not remove 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. - -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. diff --git a/docs/content/en/_common/methods/page/next-and-prev.md b/docs/content/en/_common/methods/page/next-and-prev.md deleted file mode 100644 index f859961a4..000000000 --- a/docs/content/en/_common/methods/page/next-and-prev.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -Hugo determines the _next_ and _previous_ page by sorting the site's collection of regular pages according to this sorting hierarchy: - -Field|Precedence|Sort direction -:--|:--|:-- -[`weight`]|1|descending -[`date`]|2|descending -[`linkTitle`]|3|descending -[`path`]|4|descending - -[`date`]: /methods/page/date/ -[`weight`]: /methods/page/weight/ -[`linkTitle`]: /methods/page/linktitle/ -[`path`]: /methods/page/path/ - -The sorted page collection used to determine the _next_ and _previous_ page is independent of other page collections, which may lead to unexpected behavior. - -For example, with this content structure: - -```text -content/ -├── pages/ -│ ├── _index.md -│ ├── page-1.md <-- front matter: weight = 10 -│ ├── page-2.md <-- front matter: weight = 20 -│ └── page-3.md <-- front matter: weight = 30 -└── _index.md -``` - -And these templates: - -```go-html-template {file="layouts/_default/list.html"} -{{ range .Pages.ByWeight }} -

    {{ .LinkTitle }}

    -{{ end }} -``` - -```go-html-template {file="layouts/_default/single.html"} -{{ with .Prev }} - Previous -{{ end }} - -{{ with .Next }} - Next -{{ end }} -``` - -When you visit page-2: - -- The `Prev` method points to page-3 -- The `Next` method points to page-1 - -To reverse the meaning of _next_ and _previous_ you can change the sort direction in your [site configuration], or use the [`Next`] and [`Prev`] methods on a `Pages` object for more flexibility. - -[site configuration]: /configuration/page/ -[`Next`]: /methods/pages/prev -[`Prev`]: /methods/pages/prev diff --git a/docs/content/en/_common/methods/page/nextinsection-and-previnsection.md b/docs/content/en/_common/methods/page/nextinsection-and-previnsection.md deleted file mode 100644 index 54d240eb4..000000000 --- a/docs/content/en/_common/methods/page/nextinsection-and-previnsection.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -Hugo determines the _next_ and _previous_ page by sorting the current section's regular pages according to this sorting hierarchy: - -Field|Precedence|Sort direction -:--|:--|:-- -[`weight`]|1|descending -[`date`]|2|descending -[`linkTitle`]|3|descending -[`path`]|4|descending - -[`date`]: /methods/page/date/ -[`weight`]: /methods/page/weight/ -[`linkTitle`]: /methods/page/linktitle/ -[`path`]: /methods/page/path/ - -The sorted page collection used to determine the _next_ and _previous_ page is independent of other page collections, which may lead to unexpected behavior. - -For example, with this content structure: - -```text -content/ -├── pages/ -│ ├── _index.md -│ ├── page-1.md <-- front matter: weight = 10 -│ ├── page-2.md <-- front matter: weight = 20 -│ └── page-3.md <-- front matter: weight = 30 -└── _index.md -``` - -And these templates: - -```go-html-template {file="layouts/_default/list.html"} -{{ range .Pages.ByWeight }} -

    {{ .LinkTitle }}

    -{{ end }} -``` - -```go-html-template {file="layouts/_default/single.html"} -{{ with .PrevInSection }} - Previous -{{ end }} - -{{ with .NextInSection }} - Next -{{ end }} -``` - -When you visit page-2: - -- The `PrevInSection` method points to page-3 -- The `NextInSection` method points to page-1 - -To reverse the meaning of _next_ and _previous_ you can change the sort direction in your [site configuration], or use the [`Next`] and [`Prev`] methods on a `Pages` object for more flexibility. - -[site configuration]: /configuration/page/ -[`Next`]: /methods/pages/prev -[`Prev`]: /methods/pages/prev - -## Example - -Code defensively by checking for page existence: - -```go-html-template -{{ with .PrevInSection }} - Previous -{{ end }} - -{{ with .NextInSection }} - Next -{{ end }} -``` - -## Alternative - -Use the [`Next`] and [`Prev`] methods on a `Pages` object for more flexibility. diff --git a/docs/content/en/_common/methods/page/output-format-methods.md b/docs/content/en/_common/methods/page/output-format-methods.md deleted file mode 100644 index 1e914db03..000000000 --- a/docs/content/en/_common/methods/page/output-format-methods.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -### Get IDENTIFIER - -(`any`) Returns the `OutputFormat` object with the given identifier. - -### MediaType - -(`media.Type`) Returns the media type of the output format. - -### MediaType.MainType - -(`string`) Returns the main type of the output format's media type. - -### MediaType.SubType - -(`string`) Returns the subtype of the current format's media type. - -### Name - -(`string`) Returns the output identifier of the output format. - -### Permalink - -(`string`) Returns the permalink of the page generated by the current output format. - -### Rel - -(`string`) Returns the `rel` value of the output format, either the default or as defined in the site configuration. - -### RelPermalink - -(`string`) Returns the relative permalink of the page generated by the current output format. diff --git a/docs/content/en/_common/methods/pages/group-sort-order.md b/docs/content/en/_common/methods/pages/group-sort-order.md deleted file mode 100644 index e2997a1bd..000000000 --- a/docs/content/en/_common/methods/pages/group-sort-order.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -For the optional sort order, specify either `asc` for ascending order, or `desc` for descending order. diff --git a/docs/content/en/_common/methods/pages/next-and-prev.md b/docs/content/en/_common/methods/pages/next-and-prev.md deleted file mode 100644 index 462545c3f..000000000 --- a/docs/content/en/_common/methods/pages/next-and-prev.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -Hugo determines the _next_ and _previous_ page by sorting the page collection according to this sorting hierarchy: - -Field|Precedence|Sort direction -:--|:--|:-- -[`weight`]|1|descending -[`date`]|2|descending -[`linkTitle`]|3|descending -[`path`]|4|descending - -[`date`]: /methods/page/date/ -[`weight`]: /methods/page/weight/ -[`linkTitle`]: /methods/page/linktitle/ -[`path`]: /methods/page/path/ - -The sorted page collection used to determine the _next_ and _previous_ page is independent of other page collections, which may lead to unexpected behavior. - -For example, with this content structure: - -```text -content/ -├── pages/ -│ ├── _index.md -│ ├── page-1.md <-- front matter: weight = 10 -│ ├── page-2.md <-- front matter: weight = 20 -│ └── page-3.md <-- front matter: weight = 30 -└── _index.md -``` - -And these templates: - -```go-html-template {file="layouts/_default/list.html"} -{{ range .Pages.ByWeight }} -

    {{ .LinkTitle }}

    -{{ end }} -``` - -```go-html-template {file="layouts/_default/single.html"} -{{ $pages := .CurrentSection.Pages.ByWeight }} - -{{ with $pages.Prev . }} - Previous -{{ end }} - -{{ with $pages.Next . }} - Next -{{ end }} -``` - -When you visit page-2: - -- The `Prev` method points to page-3 -- The `Next` method points to page-1 - -To reverse the meaning of _next_ and _previous_ you can chain the [`Reverse`] method to the page collection definition: - -```go-html-template {file="layouts/_default/single.html"} -{{ $pages := .CurrentSection.Pages.ByWeight.Reverse }} - -{{ with $pages.Prev . }} - Previous -{{ end }} - -{{ with $pages.Next . }} - Next -{{ end }} -``` - -[`Reverse`]: /methods/pages/reverse/ diff --git a/docs/content/en/_common/methods/resource/global-page-remote-resources.md b/docs/content/en/_common/methods/resource/global-page-remote-resources.md deleted file mode 100644 index 49146aed4..000000000 --- a/docs/content/en/_common/methods/resource/global-page-remote-resources.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -> [!note] -> Use this method with [global resources](g), [page resources](g), or [remote resources](g). diff --git a/docs/content/en/_common/methods/resource/processing-spec.md b/docs/content/en/_common/methods/resource/processing-spec.md deleted file mode 100644 index 395217328..000000000 --- a/docs/content/en/_common/methods/resource/processing-spec.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -## Process specification - -The process specification is a space-delimited, case-insensitive list of one or more of the following in any sequence: - -action -: Applicable to the [`Process`](/methods/resource/process) method only. Specify zero or one of `crop`, `fill`, `fit`, or `resize`. If you specify an action you must also provide dimensions. - -dimensions -: Provide width _or_ height when using the [`Resize`](/methods/resource/resize) method, else provide both width _and_ height. See [details](/content-management/image-processing/#dimensions). - -anchor -: Use with the [`Crop`](/methods/resource/crop) and [`Fill`](/methods/resource/fill) methods. Specify zero or one of `TopLeft`, `Top`, `TopRight`, `Left`, `Center`, `Right`, `BottomLeft`, `Bottom`, `BottomRight`, or `Smart`. Default is `Smart`. See [details](/content-management/image-processing/#anchor). - -rotation -: Typically specify zero or one of `r90`, `r180`, or `r270`. Also supports arbitrary rotation angles. See [details](/content-management/image-processing/#rotation). - -target format -: Specify zero or one of `gif`, `jpeg`, `png`, `tiff`, or `webp`. See [details](/content-management/image-processing/#target-format). - -quality -: Applicable to JPEG and WebP images. Optionally specify `qN` where `N` is an integer in the range [0, 100]. Default is `75`. See [details](/content-management/image-processing/#quality). - -hint -: Applicable to WebP images and equivalent to the `-preset` flag for the [`cwebp`] encoder. Specify zero or one of `drawing`, `icon`, `photo`, `picture`, or `text`. Default is `photo`. See [details](/content-management/image-processing/#hint). - -[`cwebp`]: https://developers.google.com/speed/webp/docs/cwebp - -background color -: When converting a PNG or WebP with transparency to a format that does not support transparency, optionally specify a background color using a 3-digit or a 6-digit hexadecimal color code. Default is `#ffffff` (white). See [details](/content-management/image-processing/#background-color). - -resampling filter -: Typically specify zero or one of `Box`, `Lanczos`, `CatmullRom`, `MitchellNetravali`, `Linear`, or `NearestNeighbor`. Other resampling filters are available. See [details](/content-management/image-processing/#resampling-filter). diff --git a/docs/content/en/_common/methods/taxonomy/get-a-taxonomy-object.md b/docs/content/en/_common/methods/taxonomy/get-a-taxonomy-object.md deleted file mode 100644 index 6fb729c17..000000000 --- a/docs/content/en/_common/methods/taxonomy/get-a-taxonomy-object.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -Before we can use a `Taxonomy` method, we need to capture a `Taxonomy` object. - -## Capture a Taxonomy object - -Consider this site configuration: - -{{< code-toggle file=hugo >}} -[taxonomies] -genre = 'genres' -author = 'authors' -{{< /code-toggle >}} - -And this content structure: - -```text -content/ -├── books/ -│ ├── and-then-there-were-none.md --> genres: suspense -│ ├── death-on-the-nile.md --> genres: suspense -│ └── jamaica-inn.md --> genres: suspense, romance -│ └── pride-and-prejudice.md --> genres: romance -└── _index.md -``` - -To capture the "genres" `Taxonomy` object from within any template, use the [`Taxonomies`] method on a `Site` object. - -```go-html-template -{{ $taxonomyObject := .Site.Taxonomies.genres }} -``` - -To capture the "genres" `Taxonomy` object when rendering its page with a taxonomy template, use the [`Terms`] method on the page's [`Data`] object: - -```go-html-template {file="layouts/_default/taxonomy.html"} -{{ $taxonomyObject := .Data.Terms }} -``` - -To inspect the data structure: - -```go-html-template -
    {{ debug.Dump $taxonomyObject }}
    -``` - -Although the [`Alphabetical`] and [`ByCount`] methods provide a better data structure for ranging through the taxonomy, you can render the weighted pages by term directly from the `Taxonomy` object: - -```go-html-template -{{ range $term, $weightedPages := $taxonomyObject }} -

    {{ .Page.LinkTitle }}

    - -{{ end }} -``` - -In the example above, the first anchor element is a link to the term page. - -[`Alphabetical`]: /methods/taxonomy/alphabetical/ -[`ByCount`]: /methods/taxonomy/bycount/ - -[`data`]: /methods/page/data/ -[`terms`]: /methods/page/data/#in-a-taxonomy-template -[`taxonomies`]: /methods/site/taxonomies/ diff --git a/docs/content/en/_common/methods/taxonomy/ordered-taxonomy-element-methods.md b/docs/content/en/_common/methods/taxonomy/ordered-taxonomy-element-methods.md deleted file mode 100644 index ec5f8e406..000000000 --- a/docs/content/en/_common/methods/taxonomy/ordered-taxonomy-element-methods.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -An ordered taxonomy is a slice, where each element is an object that contains the term and a slice of its weighted pages. - -Each element of the slice provides these methods: - -Count -: (`int`) Returns the number of pages to which the term is assigned. - -Page -: (`page.Page`) Returns the term's `Page` object, useful for linking to the term page. - -Pages -: (`page.Pages`) Returns a `Pages` object containing the `Page` objects to which the term is assigned, sorted by [taxonomic weight](g). To sort or group, use any of the [methods] available to the `Pages` object. For example, sort by the last modification date. - -Term -: (`string`) Returns the term name. - -WeightedPages -: (`page.WeightedPages`) Returns a slice of weighted pages to which the term is assigned, sorted by taxonomic weight. The `Pages` method above is more flexible, allowing you to sort and group. - -[methods]: /methods/pages/ diff --git a/docs/content/en/_common/parsable-date-time-strings.md b/docs/content/en/_common/parsable-date-time-strings.md deleted file mode 100644 index 92842767e..000000000 --- a/docs/content/en/_common/parsable-date-time-strings.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -Format|Time zone -:--|:-- -`2023-10-15T13:18:50-07:00`|`America/Los_Angeles` -`2023-10-15T13:18:50-0700`|`America/Los_Angeles` -`2023-10-15T13:18:50Z`|`Etc/UTC` -`2023-10-15T13:18:50`|Default is `Etc/UTC` -`2023-10-15`|Default is `Etc/UTC` -`15 Oct 2023`|Default is `Etc/UTC` - -The last three examples are not fully qualified, and default to the `Etc/UTC` time zone. diff --git a/docs/content/en/_common/permalink-tokens.md b/docs/content/en/_common/permalink-tokens.md deleted file mode 100644 index 4aec68fb8..000000000 --- a/docs/content/en/_common/permalink-tokens.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -`:year` -: The 4-digit year as defined in the front matter `date` field. - -`:month` -: The 2-digit month as defined in the front matter `date` field. - -`:monthname` -: The name of the month as defined in the front matter `date` field. - -`:day` -: The 2-digit day as defined in the front matter `date` field. - -`:weekday` -: The 1-digit day of the week as defined in the front matter `date` field (Sunday = `0`). - -`:weekdayname` -: The name of the day of the week as defined in the front matter `date` field. - -`:yearday` -: The 1- to 3-digit day of the year as defined in the front matter `date` field. - -`:section` -: The content's section. - -`:sections` -: The content's sections hierarchy. You can use a selection of the sections using _slice syntax_: `:sections[1:]` includes all but the first, `:sections[:last]` includes all but the last, `:sections[last]` includes only the last, `:sections[1:2]` includes section 2 and 3. Note that this slice access will not throw any out-of-bounds errors, so you don't have to be exact. - -`:title` -: The `title` as defined in front matter, else the automatic title. Hugo generates titles automatically for section, taxonomy, and term pages that are not backed by a file. - -`:slug` -: The `slug` as defined in front matter, else the `title` as defined in front matter, else the automatic title. Hugo generates titles automatically for section, taxonomy, and term pages that are not backed by a file. - -`:filename` -: The content's file name without extension, applicable to the `page` page kind. - - {{< deprecated-in v0.144.0 >}} - The `:filename` token has been deprecated. Use `:contentbasename` instead. - {{< /deprecated-in >}} - -`:slugorfilename` -: The `slug` as defined in front matter, else the content's file name without extension, applicable to the `page` page kind. - - {{< deprecated-in v0.144.0 >}} - The `:slugorfilename` token has been deprecated. Use `:slugorcontentbasename` instead. - {{< /deprecated-in >}} - -`:contentbasename` -: {{< new-in 0.144.0 />}} -: The [content base name]. - -[content base name]: /methods/page/file/#contentbasename - -`:slugorcontentbasename` -: {{< new-in 0.144.0 />}} -: The `slug` as defined in front matter, else the [content base name]. - -For time-related values, you can also use the layout string components defined in Go's [time package]. For example: - -[time package]: https://pkg.go.dev/time#pkg-constants - -{{< code-toggle file=hugo >}} -permalinks: - posts: /:06/:1/:2/:title/ -{{< /code-toggle >}} - -[content base name]: /methods/page/file/#contentbasename diff --git a/docs/content/en/_common/ref-and-relref-error-handling.md b/docs/content/en/_common/ref-and-relref-error-handling.md deleted file mode 100644 index 1d67bbc1f..000000000 --- a/docs/content/en/_common/ref-and-relref-error-handling.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -By default, Hugo will throw an error and fail the build if it cannot resolve the path. You can change this to a warning in your site configuration, and specify a URL to return when the path cannot be resolved. - -{{< code-toggle file=hugo >}} -refLinksErrorLevel = 'warning' -refLinksNotFoundURL = '/some/other/url' -{{< /code-toggle >}} diff --git a/docs/content/en/_common/ref-and-relref-options.md b/docs/content/en/_common/ref-and-relref-options.md deleted file mode 100644 index ed0dd14c6..000000000 --- a/docs/content/en/_common/ref-and-relref-options.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -path -: (`string`) The path to the target page. Paths without a leading slash (`/`) are resolved first relative to the current page, and then relative to the rest of the site. - -lang -: (`string`) The language of the target page. Default is the current language. Optional. - -outputFormat -: (`string`) The output format of the target page. Default is the current output format. Optional. diff --git a/docs/content/en/_common/render-hooks/pageinner.md b/docs/content/en/_common/render-hooks/pageinner.md deleted file mode 100644 index a598b880a..000000000 --- a/docs/content/en/_common/render-hooks/pageinner.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -## PageInner details - -{{< new-in 0.125.0 />}} - -The primary use case for `PageInner` is to resolve links and [page resources](g) relative to an included `Page`. For example, create an "include" shortcode to compose a page from multiple content files, while preserving a global context for footnotes and the table of contents: - -```go-html-template {file="layouts/shortcodes/include.html" copy=true} -{{ with .Get 0 }} - {{ with $.Page.GetPage . }} - {{- .RenderShortcodes }} - {{ else }} - {{ errorf "The %q shortcode was unable to find %q. See %s" $.Name . $.Position }} - {{ end }} -{{ else }} - {{ errorf "The %q shortcode requires a positional parameter indicating the logical path of the file to include. See %s" .Name .Position }} -{{ end }} -``` - -Then call the shortcode in your Markdown: - -```text {file="content/posts/p1.md"} -{{%/* include "/posts/p2" */%}} -``` - -Any render hook triggered while rendering `/posts/p2` will get: - -- `/posts/p1` when calling `Page` -- `/posts/p2` when calling `PageInner` - -`PageInner` falls back to the value of `Page` if not relevant, and always returns a value. - -> [!note] -> The `PageInner` method is only relevant for shortcodes that invoke the [`RenderShortcodes`] method, and you must call the shortcode using [Markdown notation]. - -As a practical example, Hugo's embedded link and image render hooks use the `PageInner` method to resolve markdown link and image destinations. See the source code for each: - -- [Embedded link render hook] -- [Embedded image render hook] - -[`RenderShortcodes`]: /methods/page/rendershortcodes/ -[Markdown notation]: /content-management/shortcodes/#notation -[Embedded link render hook]: {{% eturl render-link %}} -[Embedded image render hook]: {{% eturl render-image %}} diff --git a/docs/content/en/_common/scratch-pad-scope.md b/docs/content/en/_common/scratch-pad-scope.md deleted file mode 100644 index b659497d8..000000000 --- a/docs/content/en/_common/scratch-pad-scope.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -## Scope - -The method or function used to create a scratch pad determines its scope. For example, use the `Store` method on a `Page` object to create a scratch pad scoped to the page. - -Scope|Method or function -:--|:-- -page|[`PAGE.Store`] -site|[`SITE.Store`] -global|[`hugo.Store`] -local|[`collections.NewScratch`] -shortcode|[`SHORTCODE.Store`] - -[`page.store`]: /methods/page/store -[`site.store`]: /methods/site/store -[`hugo.store`]: /functions/hugo/store -[`collections.newscratch`]: functions/collections/newscratch -[`shortcode.store`]: /methods/shortcode/store diff --git a/docs/content/en/_common/store-methods.md b/docs/content/en/_common/store-methods.md deleted file mode 100644 index 1dd776130..000000000 --- a/docs/content/en/_common/store-methods.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -# Do not remove front matter. ---- - -## Methods - -### Set - -Sets the value of the given key. - -```go-html-template -{{ .Store.Set "greeting" "Hello" }} -``` - -### Get - -Gets the value of the given key. - -```go-html-template -{{ .Store.Set "greeting" "Hello" }} -{{ .Store.Get "greeting" }} → Hello -``` - -### Add - -Adds the given value to the existing value(s) of the given key. - -For single values, `Add` accepts values that support Go's `+` operator. If the first `Add` for a key is an array or slice, the following adds will be appended to that list. - -```go-html-template -{{ .Store.Set "greeting" "Hello" }} -{{ .Store.Add "greeting" "Welcome" }} -{{ .Store.Get "greeting" }} → HelloWelcome -``` - -```go-html-template -{{ .Store.Set "total" 3 }} -{{ .Store.Add "total" 7 }} -{{ .Store.Get "total" }} → 10 -``` - -```go-html-template -{{ .Store.Set "greetings" (slice "Hello") }} -{{ .Store.Add "greetings" (slice "Welcome" "Cheers") }} -{{ .Store.Get "greetings" }} → [Hello Welcome Cheers] -``` - -### SetInMap - -Takes a `key`, `mapKey` and `value` and adds a map of `mapKey` and `value` to the given `key`. - -```go-html-template -{{ .Store.SetInMap "greetings" "english" "Hello" }} -{{ .Store.SetInMap "greetings" "french" "Bonjour" }} -{{ .Store.Get "greetings" }} → map[english:Hello french:Bonjour] - ``` - -### DeleteInMap - -Takes a `key` and `mapKey` and removes the map of `mapKey` from the given `key`. - -```go-html-template -{{ .Store.SetInMap "greetings" "english" "Hello" }} -{{ .Store.SetInMap "greetings" "french" "Bonjour" }} -{{ .Store.DeleteInMap "greetings" "english" }} -{{ .Store.Get "greetings" }} → map[french:Bonjour] -``` - -### GetSortedMapValues - -Returns an array of values from `key` sorted by `mapKey`. - -```go-html-template -{{ .Store.SetInMap "greetings" "english" "Hello" }} -{{ .Store.SetInMap "greetings" "french" "Bonjour" }} -{{ .Store.GetSortedMapValues "greetings" }} → [Hello Bonjour] -``` - -### Delete - -Removes the given key. - -```go-html-template -{{ .Store.Set "greeting" "Hello" }} -{{ .Store.Delete "greeting" }} -``` diff --git a/docs/content/en/_common/syntax-highlighting-options.md b/docs/content/en/_common/syntax-highlighting-options.md deleted file mode 100644 index 36144e090..000000000 --- a/docs/content/en/_common/syntax-highlighting-options.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -anchorLineNos -: (`bool`) Whether to render each line number as an HTML anchor element, setting the `id` attribute of the surrounding `span` element to the line number. Irrelevant if `lineNos` is `false`. Default is `false`. - -codeFences -: (`bool`) Whether to highlight fenced code blocks. Default is `true`. - -guessSyntax -: (`bool`) Whether to automatically detect the language if the `LANG` argument is blank or set to a language for which there is no corresponding [lexer](g). Falls back to a plain text lexer if unable to automatically detect the language. Default is `false`. - - > [!note] - > The Chroma syntax highlighter includes lexers for approximately 250 languages, but only 5 of these have implemented automatic language detection. - -hl_Lines -: (`string`) A space-delimited list of lines to emphasize within the highlighted code. To emphasize lines 2, 3, 4, and 7, set this value to `2-4 7`. This option is independent of the `lineNoStart` option. - -hl_inline -: (`bool`) Whether to render the highlighted code without a wrapping container. Default is `false`. - -lineAnchors -: (`string`) When rendering a line number as an HTML anchor element, prepend this value to the `id` attribute of the surrounding `span` element. This provides unique `id` attributes when a page contains two or more code blocks. Irrelevant if `lineNos` or `anchorLineNos` is `false`. - -lineNoStart -: (`int`) The number to display at the beginning of the first line. Irrelevant if `lineNos` is `false`. Default is `1`. - -lineNos -: (`any`) Controls line number display. Default is `false`. - - `true`: Enable line numbers, controlled by `lineNumbersInTable`. - - `false`: Disable line numbers. - - `inline`: Enable inline line numbers (sets `lineNumbersInTable` to `false`). - - `table`: Enable table-based line numbers (sets `lineNumbersInTable` to `true`). - -lineNumbersInTable -: (`bool`) Whether to render the highlighted code in an HTML table with two cells. The left table cell contains the line numbers, while the right table cell contains the code. Irrelevant if `lineNos` is `false`. Default is `true`. - -noClasses -: (`bool`) Whether to use inline CSS styles instead of an external CSS file. Default is `true`. To use an external CSS file, set this value to `false` and generate the CSS file from the command line: - - ```text - hugo gen chromastyles --style=monokai > syntax.css - ``` - -style -: (`string`) The CSS styles to apply to the highlighted code. Case-sensitive. Default is `monokai`. See [syntax highlighting styles]. - -tabWidth -: (`int`) Substitute this number of spaces for each tab character in your highlighted code. Irrelevant if `noClasses` is `false`. Default is `4`. - -wrapperClass -: {{< new-in 0.140.2 />}} -: (`string`) The class or classes to use for the outermost element of the highlighted code. Default is `highlight`. - -[syntax highlighting styles]: /quick-reference/syntax-highlighting-styles/ diff --git a/docs/content/en/_common/time-layout-string.md b/docs/content/en/_common/time-layout-string.md deleted file mode 100644 index 3664eaef2..000000000 --- a/docs/content/en/_common/time-layout-string.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -_comment: Do not remove front matter. ---- - -Format a `time.Time` value based on [Go's reference time]: - -[Go's reference time]: https://pkg.go.dev/time#pkg-constants - -```text -Mon Jan 2 15:04:05 MST 2006 -``` - -Create a layout string using these components: - -Description|Valid components -:--|:-- -Year|`"2006" "06"` -Month|`"Jan" "January" "01" "1"` -Day of the week|`"Mon" "Monday"` -Day of the month|`"2" "_2" "02"` -Day of the year|`"__2" "002"` -Hour|`"15" "3" "03"` -Minute|`"4" "04"` -Second|`"5" "05"` -AM/PM mark|`"PM"` -Time zone offsets|`"-0700" "-07:00" "-07" "-070000" "-07:00:00"` - -Replace the sign in the layout string with a Z to print Z instead of an offset for the UTC zone. - -Description|Valid components -:--|:-- -Time zone offsets|`"Z0700" "Z07:00" "Z07" "Z070000" "Z07:00:00"` - -```go-html-template -{{ $t := "2023-01-27T23:44:58-08:00" }} -{{ $t = time.AsTime $t }} -{{ $t = $t.Format "Jan 02, 2006 3:04 PM Z07:00" }} - -{{ $t }} → Jan 27, 2023 11:44 PM -08:00 -``` - -Strings such as `PST` and `CET` are not time zones. They are time zone _abbreviations_. - -Strings such as `-07:00` and `+01:00` are not time zones. They are time zone _offsets_. - -A time zone is a geographic area with the same local time. For example, the time zone abbreviated by `PST` and `PDT` (depending on Daylight Savings Time) is `America/Los_Angeles`. diff --git a/docs/content/en/_index.md b/docs/content/en/_index.md deleted file mode 100644 index 358f6a4d9..000000000 --- a/docs/content/en/_index.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: The world's fastest framework for building websites -description: Hugo is one of the most popular open-source static site generators. With its amazing speed and flexibility, Hugo makes building websites fun again. ---- diff --git a/docs/content/en/about/_index.md b/docs/content/en/about/_index.md deleted file mode 100644 index e55800959..000000000 --- a/docs/content/en/about/_index.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: About Hugo -linkTitle: About -description: Learn about Hugo and its features, privacy protections, and security model. -categories: [] -keywords: [] -weight: 10 -aliases: [/about-hugo/,/docs/] ---- diff --git a/docs/content/en/about/features.md b/docs/content/en/about/features.md deleted file mode 100644 index ff1a6b8eb..000000000 --- a/docs/content/en/about/features.md +++ /dev/null @@ -1,136 +0,0 @@ ---- -title: Features -description: Hugo's rich and powerful feature set provides the framework and tools to create static sites that build in seconds, often less. -categories: [] -keywords: [] -weight: 20 ---- - -## Framework - -[Multiplatform] -: Install Hugo's single executable on Linux, macOS, Windows, and more. - -[Multilingual] -: 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. - -[Output formats] -: Render each page of your site to one or more output formats, with granular control by page kind, section, and path. While HTML is the default output format, you can add JSON, RSS, CSV, and more. For example, create a REST API to access content. - -[Templates] -: Create templates using variables, functions, and methods to transform your content, resources, and data into a published page. While HTML templates are the most common, you can create templates for any output format. - -[Themes] -: Reduce development time and cost by using one of the hundreds of themes contributed by the Hugo community. Themes are available for corporate sites, documentation projects, image portfolios, landing pages, personal and professional blogs, resumes, CVs, and more. - -[Modules] -: Reduce development time and cost by creating or importing packaged combinations of archetypes, assets, content, data, templates, translation tables, static files, or configuration settings. A module may serve as the basis for a new site, or to augment an existing site. - -[Privacy] -: Configure your site to help comply with regional privacy regulations. - -[Security] -: Hugo's security model is based on the premise that template and configuration authors are trusted, but content authors are not. This model enables generation of HTML output safe against code injection. Other protections prevent "shelling out" to arbitrary applications, limit access to specific environment variables, prevent connections to arbitrary remote data sources, and more. - -## Content authoring - -[Content formats] -: Create your content using Markdown, HTML, AsciiDoc, Emacs Org Mode, Pandoc, or reStructuredText. Markdown is the default content format, conforming to the [CommonMark] and [GitHub Flavored Markdown] specifications. - -[Markdown attributes] -: Apply HTML attributes such as `class` and `id` to Markdown images and block elements including blockquotes, fenced code blocks, headings, horizontal rules, lists, paragraphs, and tables. - -[Markdown extensions] -: Leverage the embedded Markdown extensions to create tables, definition lists, footnotes, task lists, inserted text, mark text, subscripts, superscripts, and more. - -[Markdown render hooks] -: Override the conversion of Markdown to HTML when rendering blockquotes, fenced code blocks, headings, images, links, and tables. For example, render every standalone image as an HTML `figure` element. - -[Diagrams] -: Use fenced code blocks and Markdown render hooks to include diagrams in your content. - -[Mathematics] -: Include mathematical equations and expressions in Markdown using LaTeX markup. - -[Syntax highlighting] -: Syntactically highlight code examples using Hugo's embedded syntax highlighter, enabled by default for fenced code blocks in Markdown. The syntax highlighter supports hundreds of code languages and dozens of styles. - -[Shortcodes] -: Use Hugo's embedded shortcodes, or create your own, to insert complex content. For example, use shortcodes to include `audio` and `video` elements, render tables from local or remote data sources, insert snippets from other pages, and more. - -## Content management - -[Content adapters] -: Create content adapters to dynamically add content when building your site. For example, use a content adapter to create pages from a remote data source such as JSON, TOML, YAML, or XML. - -[Taxonomies] -: Classify content to establish simple or complex logical relationships between pages. For example, create an authors taxonomy, and assign one or more authors to each page. Among other uses, the taxonomy system provides an inverted, weighted index to render a list of related pages, ordered by relevance. - -[Data] -: Augment your content using local or remote data sources including CSV, JSON, TOML, YAML, and XML. For example, create a shortcode to render an HTML table from a remote CSV file. - -[Menus] -: Provide rapid access to content via Hugo's menu system, configured automatically, globally, or on a page-by-page basis. The menu system is a key component of Hugo's multilingual architecture. - -[URL management] -: Serve any page from any path via global configuration or on a page-by-page basis. - -## Asset pipelines - -[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. - -## Performance - -[Caching] -: Reduce build time and cost by rendering a partial template once then cache the result, either globally or within a given context. For example, cache the result of an asset pipeline to prevent reprocessing on every rendered page. - -[Segmentation] -: Reduce build time and cost by partitioning your sites into segments. For example, render the home page and the "news section" every hour, and render the entire site once a week. - -[Minification] -: Minify HTML, CSS, and JavaScript to reduce file size, bandwidth consumption, and loading times. - -[Multilingual]: /content-management/multilingual/ -[Multiplatform]: /installation/ -[Output formats]: /configuration/output-formats/ -[Templates]: /templates/introduction/ -[Themes]: https://themes.gohugo.io/ -[Modules]: /hugo-modules/ -[Privacy]: /configuration/privacy/ -[Security]: /about/security/ - -[Content formats]: /content-management/formats/ -[CommonMark]: https://spec.commonmark.org/current/ -[GitHub Flavored Markdown]: https://github.github.com/gfm/ -[Markdown attributes]: /content-management/markdown-attributes/ -[Markdown extensions]: /configuration/markup/#extensions -[Markdown render hooks]: /render-hooks/introduction/ -[Diagrams]: /content-management/diagrams/ -[Mathematics]: /content-management/mathematics/ -[Syntax highlighting]: /content-management/syntax-highlighting/ -[Shortcodes]: /content-management/shortcodes/ - -[Content adapters]: /content-management/content-adapters/ -[Taxonomies]: /content-management/taxonomies/ -[Data]: /content-management/data-sources/ -[Menus]: /content-management/menus/ -[URL management]: /content-management/urls/ - -[Image processing]: /content-management/image-processing/ -[JavaScript bundling]: /functions/js/build/ -[Sass processing]: /functions/css/Sass/ -[Tailwind CSS processing]: /functions/css/tailwindcss/ - -[Caching]: /functions/partials/includecached/ -[Segmentation]: /configuration/segments/ -[Minification]: /configuration/minify/ diff --git a/docs/content/en/about/introduction.md b/docs/content/en/about/introduction.md deleted file mode 100644 index 9586d08f8..000000000 --- a/docs/content/en/about/introduction.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: Introduction -description: Hugo is a static site generator written in Go, optimized for speed and designed for flexibility. -categories: [] -keywords: [] -weight: 10 -aliases: [/about/what-is-hugo/,/about/benefits/] ---- - -Hugo is a [static site generator] written in [Go], optimized for speed and designed for flexibility. With its advanced templating system and fast asset pipelines, Hugo renders a complete site in seconds, often less. - -Due to its flexible framework, multilingual support, and powerful taxonomy system, Hugo is widely used to create: - -- Corporate, government, nonprofit, education, news, event, and project sites -- Documentation sites -- Image portfolios -- Landing pages -- Business, professional, and personal blogs -- Resumes and CVs - -Use Hugo's embedded web server during development to instantly see changes to content, structure, behavior, and presentation. Then deploy the site to your host, or push changes to your Git provider for automated builds and deployment. - -And with [Hugo Modules], you can share content, assets, data, translations, themes, templates, and configuration with other projects via public or private Git repositories. - -Learn more about Hugo's [features], [privacy protections], and [security model]. - -[Go]: https://go.dev -[Hugo Modules]: /hugo-modules/ -[static site generator]: https://en.wikipedia.org/wiki/Static_site_generator -[features]: /about/features/ -[security model]: about/security/ -[privacy protections]: /configuration/privacy - -{{< youtube 0RKpf3rK57I >}} diff --git a/docs/content/en/about/license.md b/docs/content/en/about/license.md deleted file mode 100644 index 06a3a695d..000000000 --- a/docs/content/en/about/license.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -title: License -description: Hugo is released under the Apache 2.0 license. -categories: [] -keywords: [] -weight: 40 ---- - -## Apache License - -_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. diff --git a/docs/content/en/about/security.md b/docs/content/en/about/security.md deleted file mode 100644 index 509ca6a75..000000000 --- a/docs/content/en/about/security.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -title: Security model -linkTitle: Security -description: A summary of Hugo's security model. -categories: [] -keywords: [] -weight: 30 -aliases: [/about/security-model/] ---- - -## Runtime security - -Hugo generates static websites, meaning the final output runs directly in the browser and interacts with any integrated APIs. However, during development and site building, the `hugo` executable itself is the runtime environment. - -Securing a runtime is a complex task. Hugo addresses this through a robust sandboxing approach and a strict security policy with default protections. Key features include: - -- Virtual file system: Hugo employs a virtual file system, limiting file access. Only the main project, not external components, can access files or directories outside the project root. -- Read-Only access: User-defined components have read-only access to the file system, preventing unintended modifications. -- Controlled external binaries: While Hugo utilizes external binaries for features like Asciidoctor support, these are strictly predefined with specific flags and are disabled by default. The [security policy] details these limitations. -- No arbitrary commands: To mitigate risks, Hugo intentionally avoids implementing general functions that would allow users to execute arbitrary operating system commands. - -This combination of sandboxing and strict defaults effectively minimizes potential security vulnerabilities during the Hugo build process. - -[security policy]: /configuration/security/ - -## Dependency security - -Hugo utilizes [Go Modules] to manage its dependencies, compiling as a static binary. Go Modules create a `go.sum` file, a critical security feature. This file acts as a database, storing the expected cryptographic checksums of all dependencies, including those required indirectly (transitive dependencies). - -[Hugo Modules], which extend Go Modules' functionality, also produce a `go.sum` file. To ensure dependency integrity, commit this `go.sum` file to your version control. If Hugo detects a checksum mismatch during the build process, it will fail, indicating a possible attempt to [tamper with your project's dependencies]. - -[Go Modules]: https://go.dev/wiki/Modules#modules -[Hugo Modules]: /hugo-modules/ -[tamper with your project's dependencies]: https://julienrenaux.fr/2019/12/20/github-actions-security-risk/ - -## Web application security - -Hugo's security philosophy is rooted in established security standards, primarily aligning with the threats defined by [OWASP]. For HTML output, Hugo operates under a clear trust model. This model assumes that template and configuration authors, the developers, are trustworthy. However, the data supplied to these templates is inherently considered untrusted. This distinction is crucial for understanding how Hugo handles potential security risks. - -[OWASP]: https://en.wikipedia.org/wiki/OWASP - -To prevent unintended escaping of data that developers know is safe, Hugo provides [`safe`] functions, such as [`safeHTML`]. These functions allow developers to explicitly mark data as trusted, bypassing the default escaping mechanisms. This is essential for scenarios where data is generated or sourced from reliable sources. However, an exception exists: enabling [inline shortcodes]. By activating this feature, you are implicitly trusting the logic within the shortcodes and the data contained within your content files. - -[`safeHTML`]: /functions/safe/html/ -[inline shortcodes]: /content-management/shortcodes/#inline - -It's vital to remember that Hugo is a static site generator. This architectural choice significantly reduces the attack surface by eliminating the complexities and vulnerabilities associated with dynamic user input. Unlike dynamic websites, Hugo generates static HTML files, minimizing the risk of real-time attacks. Regarding content, Hugo's default Markdown renderer is [configured to sanitize] potentially unsafe content. This default behavior ensures that potentially malicious code or scripts are removed or escaped. However, this setting can be reconfigured if you have a high degree of confidence in the safety of your content sources. - -[configured to sanitize]: /configuration/markup/#rendererunsafe - -In essence, Hugo prioritizes secure output by establishing a clear trust boundary between developers and data. By default, it errs on the side of caution, sanitizing potentially unsafe content and escaping data. Developers have the flexibility to adjust these defaults through [`safe`] functions and [configuration options], but they must do so with a clear understanding of the security implications. Hugo's static site generation model further strengthens its security posture by minimizing dynamic vulnerabilities. - -[`safe`]: /functions/safe -[configuration options]: /configuration/security - -## Configuration - -See [configure security](/configuration/security/). diff --git a/docs/content/en/commands/_index.md b/docs/content/en/commands/_index.md deleted file mode 100644 index 5869bfd9d..000000000 --- a/docs/content/en/commands/_index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Command line interface -linkTitle: CLI -description: Use the command line interface (CLI) to manage your site. -categories: [] -keywords: [] -weight: 10 ---- diff --git a/docs/content/en/commands/hugo.md b/docs/content/en/commands/hugo.md deleted file mode 100644 index ef0bca9a5..000000000 --- a/docs/content/en/commands/hugo.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -title: "hugo" -slug: hugo -url: /commands/hugo/ ---- -## hugo - -Build your site - -### Synopsis - -hugo 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/. - -``` -hugo [flags] -``` - -### Options - -``` - -b, --baseURL string hostname (and path) to the root, e.g. https://spf13.com/ - -D, --buildDrafts include content marked as draft - -E, --buildExpired include expired content - -F, --buildFuture include content with publishdate in the future - --cacheDir string filesystem path to cache directory - --cleanDestinationDir remove files from destination not found in static directories - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -c, --contentDir string filesystem path to content directory - -d, --destination string filesystem path to write files to - --disableKinds strings disable different kind of pages (home, RSS etc.) - --enableGitInfo add Git revision, date, author, and CODEOWNERS info to the pages - -e, --environment string build environment - --forceSyncStatic copy all files when static is changed. - --gc enable to run some cleanup tasks (remove unused cache files) after the build - -h, --help help for hugo - --ignoreCache ignores the cache directory - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - -l, --layoutDir string filesystem path to layout directory - --logLevel string log level (debug|info|warn|error) - --minify minify any supported output format (HTML, XML etc.) - --noBuildLock don't create .hugo_build.lock file - --noChmod don't sync permission mode of files - --noTimes don't sync modification time of files - --panicOnWarning panic on first WARNING log - --poll string set this to a poll interval, e.g --poll 700ms, to use a poll based approach to watch for file system changes - --printI18nWarnings print missing translations - --printMemoryUsage print memory usage to screen at intervals - --printPathWarnings print warnings on duplicate target paths etc. - --printUnusedTemplates print warnings on unused templates. - --quiet build in quiet mode - --renderSegments strings named segments to render (configured in the segments config) - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --templateMetrics display metrics about template executions - --templateMetricsHints calculate some improvement hints when combined with --templateMetrics - -t, --theme strings themes to use (located in /themes/THEMENAME/) - --themesDir string filesystem path to themes directory - --trace file write trace to file (not useful in general) - -w, --watch watch filesystem for changes and recreate as needed -``` - -### SEE ALSO - -* [hugo build](/commands/hugo_build/) - Build your site -* [hugo completion](/commands/hugo_completion/) - Generate the autocompletion script for the specified shell -* [hugo config](/commands/hugo_config/) - Display site configuration -* [hugo convert](/commands/hugo_convert/) - Convert front matter to another format -* [hugo deploy](/commands/hugo_deploy/) - Deploy your site to a cloud provider -* [hugo env](/commands/hugo_env/) - Display version and environment info -* [hugo gen](/commands/hugo_gen/) - Generate documentation and syntax highlighting styles -* [hugo import](/commands/hugo_import/) - Import a site from another system -* [hugo list](/commands/hugo_list/) - List content -* [hugo mod](/commands/hugo_mod/) - Manage modules -* [hugo new](/commands/hugo_new/) - Create new content -* [hugo server](/commands/hugo_server/) - Start the embedded web server -* [hugo version](/commands/hugo_version/) - Display version - diff --git a/docs/content/en/commands/hugo_build.md b/docs/content/en/commands/hugo_build.md deleted file mode 100644 index 582cbe511..000000000 --- a/docs/content/en/commands/hugo_build.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: "hugo build" -slug: hugo_build -url: /commands/hugo_build/ ---- -## hugo build - -Build your site - -### Synopsis - -build 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/. - -``` -hugo build [flags] -``` - -### Options - -``` - -b, --baseURL string hostname (and path) to the root, e.g. https://spf13.com/ - -D, --buildDrafts include content marked as draft - -E, --buildExpired include expired content - -F, --buildFuture include content with publishdate in the future - --cacheDir string filesystem path to cache directory - --cleanDestinationDir remove files from destination not found in static directories - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -c, --contentDir string filesystem path to content directory - -d, --destination string filesystem path to write files to - --disableKinds strings disable different kind of pages (home, RSS etc.) - --enableGitInfo add Git revision, date, author, and CODEOWNERS info to the pages - -e, --environment string build environment - --forceSyncStatic copy all files when static is changed. - --gc enable to run some cleanup tasks (remove unused cache files) after the build - -h, --help help for build - --ignoreCache ignores the cache directory - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - -l, --layoutDir string filesystem path to layout directory - --logLevel string log level (debug|info|warn|error) - --minify minify any supported output format (HTML, XML etc.) - --noBuildLock don't create .hugo_build.lock file - --noChmod don't sync permission mode of files - --noTimes don't sync modification time of files - --panicOnWarning panic on first WARNING log - --poll string set this to a poll interval, e.g --poll 700ms, to use a poll based approach to watch for file system changes - --printI18nWarnings print missing translations - --printMemoryUsage print memory usage to screen at intervals - --printPathWarnings print warnings on duplicate target paths etc. - --printUnusedTemplates print warnings on unused templates. - --quiet build in quiet mode - --renderSegments strings named segments to render (configured in the segments config) - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --templateMetrics display metrics about template executions - --templateMetricsHints calculate some improvement hints when combined with --templateMetrics - -t, --theme strings themes to use (located in /themes/THEMENAME/) - --themesDir string filesystem path to themes directory - --trace file write trace to file (not useful in general) - -w, --watch watch filesystem for changes and recreate as needed -``` - -### SEE ALSO - -* [hugo](/commands/hugo/) - Build your site - diff --git a/docs/content/en/commands/hugo_completion.md b/docs/content/en/commands/hugo_completion.md deleted file mode 100644 index ac60dc148..000000000 --- a/docs/content/en/commands/hugo_completion.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: "hugo completion" -slug: hugo_completion -url: /commands/hugo_completion/ ---- -## hugo completion - -Generate the autocompletion script for the specified shell - -### Synopsis - -Generate the autocompletion script for hugo for the specified shell. -See each sub-command's help for details on how to use the generated script. - - -### Options - -``` - -h, --help help for completion -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo](/commands/hugo/) - Build your site -* [hugo completion bash](/commands/hugo_completion_bash/) - Generate the autocompletion script for bash -* [hugo completion fish](/commands/hugo_completion_fish/) - Generate the autocompletion script for fish -* [hugo completion powershell](/commands/hugo_completion_powershell/) - Generate the autocompletion script for powershell -* [hugo completion zsh](/commands/hugo_completion_zsh/) - Generate the autocompletion script for zsh - diff --git a/docs/content/en/commands/hugo_completion_bash.md b/docs/content/en/commands/hugo_completion_bash.md deleted file mode 100644 index 41fb47c0c..000000000 --- a/docs/content/en/commands/hugo_completion_bash.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: "hugo completion bash" -slug: hugo_completion_bash -url: /commands/hugo_completion_bash/ ---- -## hugo completion bash - -Generate the autocompletion script for bash - -### Synopsis - -Generate the autocompletion script for the bash shell. - -This script depends on the 'bash-completion' package. -If it is not installed already, you can install it via your OS's package manager. - -To load completions in your current shell session: - - source <(hugo completion bash) - -To load completions for every new session, execute once: - -#### Linux: - - hugo completion bash > /etc/bash_completion.d/hugo - -#### macOS: - - hugo completion bash > $(brew --prefix)/etc/bash_completion.d/hugo - -You will need to start a new shell for this setup to take effect. - - -``` -hugo completion bash -``` - -### Options - -``` - -h, --help help for bash - --no-descriptions disable completion descriptions -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo completion](/commands/hugo_completion/) - Generate the autocompletion script for the specified shell - diff --git a/docs/content/en/commands/hugo_completion_fish.md b/docs/content/en/commands/hugo_completion_fish.md deleted file mode 100644 index 7f971c3ca..000000000 --- a/docs/content/en/commands/hugo_completion_fish.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -title: "hugo completion fish" -slug: hugo_completion_fish -url: /commands/hugo_completion_fish/ ---- -## hugo completion fish - -Generate the autocompletion script for fish - -### Synopsis - -Generate the autocompletion script for the fish shell. - -To load completions in your current shell session: - - hugo completion fish | source - -To load completions for every new session, execute once: - - hugo completion fish > ~/.config/fish/completions/hugo.fish - -You will need to start a new shell for this setup to take effect. - - -``` -hugo completion fish [flags] -``` - -### Options - -``` - -h, --help help for fish - --no-descriptions disable completion descriptions -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo completion](/commands/hugo_completion/) - Generate the autocompletion script for the specified shell - diff --git a/docs/content/en/commands/hugo_completion_powershell.md b/docs/content/en/commands/hugo_completion_powershell.md deleted file mode 100644 index 6ea17892b..000000000 --- a/docs/content/en/commands/hugo_completion_powershell.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: "hugo completion powershell" -slug: hugo_completion_powershell -url: /commands/hugo_completion_powershell/ ---- -## hugo completion powershell - -Generate the autocompletion script for powershell - -### Synopsis - -Generate the autocompletion script for powershell. - -To load completions in your current shell session: - - hugo completion powershell | Out-String | Invoke-Expression - -To load completions for every new session, add the output of the above command -to your powershell profile. - - -``` -hugo completion powershell [flags] -``` - -### Options - -``` - -h, --help help for powershell - --no-descriptions disable completion descriptions -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo completion](/commands/hugo_completion/) - Generate the autocompletion script for the specified shell - diff --git a/docs/content/en/commands/hugo_completion_zsh.md b/docs/content/en/commands/hugo_completion_zsh.md deleted file mode 100644 index b9e79f9f3..000000000 --- a/docs/content/en/commands/hugo_completion_zsh.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: "hugo completion zsh" -slug: hugo_completion_zsh -url: /commands/hugo_completion_zsh/ ---- -## hugo completion zsh - -Generate the autocompletion script for zsh - -### Synopsis - -Generate the autocompletion script for the zsh shell. - -If shell completion is not already enabled in your environment you will need -to enable it. You can execute the following once: - - echo "autoload -U compinit; compinit" >> ~/.zshrc - -To load completions in your current shell session: - - source <(hugo completion zsh) - -To load completions for every new session, execute once: - -#### Linux: - - hugo completion zsh > "${fpath[1]}/_hugo" - -#### macOS: - - hugo completion zsh > $(brew --prefix)/share/zsh/site-functions/_hugo - -You will need to start a new shell for this setup to take effect. - - -``` -hugo completion zsh [flags] -``` - -### Options - -``` - -h, --help help for zsh - --no-descriptions disable completion descriptions -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo completion](/commands/hugo_completion/) - Generate the autocompletion script for the specified shell - diff --git a/docs/content/en/commands/hugo_config.md b/docs/content/en/commands/hugo_config.md deleted file mode 100644 index 2b4eaafa1..000000000 --- a/docs/content/en/commands/hugo_config.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: "hugo config" -slug: hugo_config -url: /commands/hugo_config/ ---- -## hugo config - -Display site configuration - -### Synopsis - -Display site configuration, both default and custom settings. - -``` -hugo config [command] [flags] -``` - -### Options - -``` - -b, --baseURL string hostname (and path) to the root, e.g. https://spf13.com/ - --cacheDir string filesystem path to cache directory - -c, --contentDir string filesystem path to content directory - --format string preferred file format (toml, yaml or json) (default "toml") - -h, --help help for config - --lang string the language to display config for. Defaults to the first language defined. - --printZero include config options with zero values (e.g. false, 0, "") in the output - --renderSegments strings named segments to render (configured in the segments config) - -t, --theme strings themes to use (located in /themes/THEMENAME/) -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo](/commands/hugo/) - Build your site -* [hugo config mounts](/commands/hugo_config_mounts/) - Print the configured file mounts - diff --git a/docs/content/en/commands/hugo_config_mounts.md b/docs/content/en/commands/hugo_config_mounts.md deleted file mode 100644 index 06a781220..000000000 --- a/docs/content/en/commands/hugo_config_mounts.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: "hugo config mounts" -slug: hugo_config_mounts -url: /commands/hugo_config_mounts/ ---- -## hugo config mounts - -Print the configured file mounts - -``` -hugo config mounts [flags] [args] -``` - -### Options - -``` - -b, --baseURL string hostname (and path) to the root, e.g. https://spf13.com/ - --cacheDir string filesystem path to cache directory - -c, --contentDir string filesystem path to content directory - -h, --help help for mounts - --renderSegments strings named segments to render (configured in the segments config) - -t, --theme strings themes to use (located in /themes/THEMENAME/) -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo config](/commands/hugo_config/) - Display site configuration - diff --git a/docs/content/en/commands/hugo_convert.md b/docs/content/en/commands/hugo_convert.md deleted file mode 100644 index a8d0b6a38..000000000 --- a/docs/content/en/commands/hugo_convert.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: "hugo convert" -slug: hugo_convert -url: /commands/hugo_convert/ ---- -## hugo convert - -Convert front matter to another format - -### Synopsis - -Convert front matter to another format. - -See convert's subcommands toJSON, toTOML and toYAML for more information. - -### Options - -``` - -h, --help help for convert - -o, --output string filesystem path to write files to - --unsafe enable less safe operations, please backup first -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo](/commands/hugo/) - Build your site -* [hugo convert toJSON](/commands/hugo_convert_tojson/) - Convert front matter to JSON -* [hugo convert toTOML](/commands/hugo_convert_totoml/) - Convert front matter to TOML -* [hugo convert toYAML](/commands/hugo_convert_toyaml/) - Convert front matter to YAML - diff --git a/docs/content/en/commands/hugo_convert_toJSON.md b/docs/content/en/commands/hugo_convert_toJSON.md deleted file mode 100644 index fe81146f9..000000000 --- a/docs/content/en/commands/hugo_convert_toJSON.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: "hugo convert toJSON" -slug: hugo_convert_toJSON -url: /commands/hugo_convert_tojson/ ---- -## hugo convert toJSON - -Convert front matter to JSON - -### Synopsis - -toJSON converts all front matter in the content directory -to use JSON for the front matter. - -``` -hugo convert toJSON [flags] [args] -``` - -### Options - -``` - -h, --help help for toJSON -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - -o, --output string filesystem path to write files to - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory - --unsafe enable less safe operations, please backup first -``` - -### SEE ALSO - -* [hugo convert](/commands/hugo_convert/) - Convert front matter to another format - diff --git a/docs/content/en/commands/hugo_convert_toTOML.md b/docs/content/en/commands/hugo_convert_toTOML.md deleted file mode 100644 index 490b15ee6..000000000 --- a/docs/content/en/commands/hugo_convert_toTOML.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: "hugo convert toTOML" -slug: hugo_convert_toTOML -url: /commands/hugo_convert_totoml/ ---- -## hugo convert toTOML - -Convert front matter to TOML - -### Synopsis - -toTOML converts all front matter in the content directory -to use TOML for the front matter. - -``` -hugo convert toTOML [flags] [args] -``` - -### Options - -``` - -h, --help help for toTOML -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - -o, --output string filesystem path to write files to - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory - --unsafe enable less safe operations, please backup first -``` - -### SEE ALSO - -* [hugo convert](/commands/hugo_convert/) - Convert front matter to another format - diff --git a/docs/content/en/commands/hugo_convert_toYAML.md b/docs/content/en/commands/hugo_convert_toYAML.md deleted file mode 100644 index 9b00ce247..000000000 --- a/docs/content/en/commands/hugo_convert_toYAML.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: "hugo convert toYAML" -slug: hugo_convert_toYAML -url: /commands/hugo_convert_toyaml/ ---- -## hugo convert toYAML - -Convert front matter to YAML - -### Synopsis - -toYAML converts all front matter in the content directory -to use YAML for the front matter. - -``` -hugo convert toYAML [flags] [args] -``` - -### Options - -``` - -h, --help help for toYAML -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - -o, --output string filesystem path to write files to - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory - --unsafe enable less safe operations, please backup first -``` - -### SEE ALSO - -* [hugo convert](/commands/hugo_convert/) - Convert front matter to another format - diff --git a/docs/content/en/commands/hugo_deploy.md b/docs/content/en/commands/hugo_deploy.md deleted file mode 100644 index 696acf51f..000000000 --- a/docs/content/en/commands/hugo_deploy.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: "hugo deploy" -slug: hugo_deploy -url: /commands/hugo_deploy/ ---- -## hugo deploy - -Deploy your site to a cloud provider - -### Synopsis - -Deploy your site to a cloud provider - -See https://gohugo.io/hosting-and-deployment/hugo-deploy/ for detailed -documentation. - - -``` -hugo deploy [flags] [args] -``` - -### Options - -``` - --confirm ask for confirmation before making changes to the target - --dryRun dry run - --force force upload of all files - -h, --help help for deploy - --invalidateCDN invalidate the CDN cache listed in the deployment target (default true) - --maxDeletes int maximum # of files to delete, or -1 to disable (default 256) - --target string target deployment from deployments section in config file; defaults to the first one - --workers int number of workers to transfer files. defaults to 10 (default 10) -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo](/commands/hugo/) - Build your site - diff --git a/docs/content/en/commands/hugo_env.md b/docs/content/en/commands/hugo_env.md deleted file mode 100644 index 7e21733a4..000000000 --- a/docs/content/en/commands/hugo_env.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -title: "hugo env" -slug: hugo_env -url: /commands/hugo_env/ ---- -## hugo env - -Display version and environment info - -### Synopsis - -Display version and environment info. This is useful in Hugo bug reports - -``` -hugo env [flags] [args] -``` - -### Options - -``` - -h, --help help for env -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo](/commands/hugo/) - Build your site - diff --git a/docs/content/en/commands/hugo_gen.md b/docs/content/en/commands/hugo_gen.md deleted file mode 100644 index ae11a0321..000000000 --- a/docs/content/en/commands/hugo_gen.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: "hugo gen" -slug: hugo_gen -url: /commands/hugo_gen/ ---- -## hugo gen - -Generate documentation and syntax highlighting styles - -### Synopsis - -Generate documentation for your project using Hugo's documentation engine, including syntax highlighting for various programming languages. - -### Options - -``` - -h, --help help for gen -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo](/commands/hugo/) - Build your site -* [hugo gen chromastyles](/commands/hugo_gen_chromastyles/) - Generate CSS stylesheet for the Chroma code highlighter -* [hugo gen doc](/commands/hugo_gen_doc/) - Generate Markdown documentation for the Hugo CLI -* [hugo gen man](/commands/hugo_gen_man/) - Generate man pages for the Hugo CLI - diff --git a/docs/content/en/commands/hugo_gen_chromastyles.md b/docs/content/en/commands/hugo_gen_chromastyles.md deleted file mode 100644 index 2863e46b4..000000000 --- a/docs/content/en/commands/hugo_gen_chromastyles.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: "hugo gen chromastyles" -slug: hugo_gen_chromastyles -url: /commands/hugo_gen_chromastyles/ ---- -## hugo gen chromastyles - -Generate CSS stylesheet for the Chroma code highlighter - -### Synopsis - -Generate CSS stylesheet for the Chroma code highlighter for a given style. This stylesheet is needed if markup.highlight.noClasses is disabled in config. - -See https://xyproto.github.io/splash/docs/all.html for a preview of the available styles - -``` -hugo gen chromastyles [flags] [args] -``` - -### Options - -``` - -h, --help help for chromastyles - --highlightStyle string foreground and background colors for highlighted lines, e.g. --highlightStyle "#fff000 bg:#000fff" - --lineNumbersInlineStyle string foreground and background colors for inline line numbers, e.g. --lineNumbersInlineStyle "#fff000 bg:#000fff" - --lineNumbersTableStyle string foreground and background colors for table line numbers, e.g. --lineNumbersTableStyle "#fff000 bg:#000fff" - --style string highlighter style (see https://xyproto.github.io/splash/docs/) (default "friendly") -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo gen](/commands/hugo_gen/) - Generate documentation and syntax highlighting styles - diff --git a/docs/content/en/commands/hugo_gen_doc.md b/docs/content/en/commands/hugo_gen_doc.md deleted file mode 100644 index 3d808e75c..000000000 --- a/docs/content/en/commands/hugo_gen_doc.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: "hugo gen doc" -slug: hugo_gen_doc -url: /commands/hugo_gen_doc/ ---- -## hugo gen doc - -Generate Markdown documentation for the Hugo CLI - -### Synopsis - -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. - -``` -hugo gen doc [flags] [args] -``` - -### Options - -``` - --dir string the directory to write the doc. (default "/tmp/hugodoc/") - -h, --help help for doc -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo gen](/commands/hugo_gen/) - Generate documentation and syntax highlighting styles - diff --git a/docs/content/en/commands/hugo_gen_man.md b/docs/content/en/commands/hugo_gen_man.md deleted file mode 100644 index 14fe859e3..000000000 --- a/docs/content/en/commands/hugo_gen_man.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: "hugo gen man" -slug: hugo_gen_man -url: /commands/hugo_gen_man/ ---- -## hugo gen man - -Generate man pages for the Hugo CLI - -### Synopsis - -This command automatically generates up-to-date man pages of Hugo's - command-line interface. By default, it creates the man page files - in the "man" directory under the current directory. - -``` -hugo gen man [flags] [args] -``` - -### Options - -``` - --dir string the directory to write the man pages. (default "man/") - -h, --help help for man -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo gen](/commands/hugo_gen/) - Generate documentation and syntax highlighting styles - diff --git a/docs/content/en/commands/hugo_import.md b/docs/content/en/commands/hugo_import.md deleted file mode 100644 index 2b8e62951..000000000 --- a/docs/content/en/commands/hugo_import.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: "hugo import" -slug: hugo_import -url: /commands/hugo_import/ ---- -## hugo import - -Import a site from another system - -### Synopsis - -Import a site from another system. - -Import requires a subcommand, e.g. `hugo import jekyll jekyll_root_path target_path`. - -### Options - -``` - -h, --help help for import -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo](/commands/hugo/) - Build your site -* [hugo import jekyll](/commands/hugo_import_jekyll/) - hugo import from Jekyll - diff --git a/docs/content/en/commands/hugo_import_jekyll.md b/docs/content/en/commands/hugo_import_jekyll.md deleted file mode 100644 index 8746c156e..000000000 --- a/docs/content/en/commands/hugo_import_jekyll.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: "hugo import jekyll" -slug: hugo_import_jekyll -url: /commands/hugo_import_jekyll/ ---- -## hugo import jekyll - -hugo import from Jekyll - -### Synopsis - -hugo import from Jekyll. - -Import from Jekyll requires two paths, e.g. `hugo import jekyll jekyll_root_path target_path`. - -``` -hugo import jekyll [flags] [args] -``` - -### Options - -``` - --force allow import into non-empty target directory - -h, --help help for jekyll -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo import](/commands/hugo_import/) - Import a site from another system - diff --git a/docs/content/en/commands/hugo_list.md b/docs/content/en/commands/hugo_list.md deleted file mode 100644 index 741ca1d68..000000000 --- a/docs/content/en/commands/hugo_list.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: "hugo list" -slug: hugo_list -url: /commands/hugo_list/ ---- -## hugo list - -List content - -### Synopsis - -List content. - -List requires a subcommand, e.g. hugo list drafts - -### Options - -``` - -h, --help help for list -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo](/commands/hugo/) - Build your site -* [hugo list all](/commands/hugo_list_all/) - List all content -* [hugo list drafts](/commands/hugo_list_drafts/) - List draft content -* [hugo list expired](/commands/hugo_list_expired/) - List expired content -* [hugo list future](/commands/hugo_list_future/) - List future content -* [hugo list published](/commands/hugo_list_published/) - List published content - diff --git a/docs/content/en/commands/hugo_list_all.md b/docs/content/en/commands/hugo_list_all.md deleted file mode 100644 index e0f1efdcb..000000000 --- a/docs/content/en/commands/hugo_list_all.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -title: "hugo list all" -slug: hugo_list_all -url: /commands/hugo_list_all/ ---- -## hugo list all - -List all content - -### Synopsis - -List all content including draft, future, and expired. - -``` -hugo list all [flags] [args] -``` - -### Options - -``` - -h, --help help for all -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo list](/commands/hugo_list/) - List content - diff --git a/docs/content/en/commands/hugo_list_drafts.md b/docs/content/en/commands/hugo_list_drafts.md deleted file mode 100644 index 25ddc78d3..000000000 --- a/docs/content/en/commands/hugo_list_drafts.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -title: "hugo list drafts" -slug: hugo_list_drafts -url: /commands/hugo_list_drafts/ ---- -## hugo list drafts - -List draft content - -### Synopsis - -List draft content. - -``` -hugo list drafts [flags] [args] -``` - -### Options - -``` - -h, --help help for drafts -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo list](/commands/hugo_list/) - List content - diff --git a/docs/content/en/commands/hugo_list_expired.md b/docs/content/en/commands/hugo_list_expired.md deleted file mode 100644 index 1936b9920..000000000 --- a/docs/content/en/commands/hugo_list_expired.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -title: "hugo list expired" -slug: hugo_list_expired -url: /commands/hugo_list_expired/ ---- -## hugo list expired - -List expired content - -### Synopsis - -List content with a past expiration date. - -``` -hugo list expired [flags] [args] -``` - -### Options - -``` - -h, --help help for expired -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo list](/commands/hugo_list/) - List content - diff --git a/docs/content/en/commands/hugo_list_future.md b/docs/content/en/commands/hugo_list_future.md deleted file mode 100644 index 3152639c2..000000000 --- a/docs/content/en/commands/hugo_list_future.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -title: "hugo list future" -slug: hugo_list_future -url: /commands/hugo_list_future/ ---- -## hugo list future - -List future content - -### Synopsis - -List content with a future publication date. - -``` -hugo list future [flags] [args] -``` - -### Options - -``` - -h, --help help for future -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo list](/commands/hugo_list/) - List content - diff --git a/docs/content/en/commands/hugo_list_published.md b/docs/content/en/commands/hugo_list_published.md deleted file mode 100644 index a7a08c7b4..000000000 --- a/docs/content/en/commands/hugo_list_published.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -title: "hugo list published" -slug: hugo_list_published -url: /commands/hugo_list_published/ ---- -## hugo list published - -List published content - -### Synopsis - -List content that is not draft, future, or expired. - -``` -hugo list published [flags] [args] -``` - -### Options - -``` - -h, --help help for published -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo list](/commands/hugo_list/) - List content - diff --git a/docs/content/en/commands/hugo_mod.md b/docs/content/en/commands/hugo_mod.md deleted file mode 100644 index 25a27185d..000000000 --- a/docs/content/en/commands/hugo_mod.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -title: "hugo mod" -slug: hugo_mod -url: /commands/hugo_mod/ ---- -## hugo mod - -Manage modules - -### Synopsis - -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". - - -Note that Hugo will always start out by resolving the components defined in the site -configuration, provided by a _vendor directory (if no --ignoreVendorPaths flag provided), -Go Modules, or a folder inside the themes directory, in that order. - -See https://gohugo.io/hugo-modules/ for more information. - - - -### Options - -``` - -h, --help help for mod -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo](/commands/hugo/) - Build your site -* [hugo mod clean](/commands/hugo_mod_clean/) - Delete the Hugo Module cache for the current project -* [hugo mod get](/commands/hugo_mod_get/) - Resolves dependencies in your current Hugo project -* [hugo mod graph](/commands/hugo_mod_graph/) - Print a module dependency graph -* [hugo mod init](/commands/hugo_mod_init/) - Initialize this project as a Hugo Module -* [hugo mod npm](/commands/hugo_mod_npm/) - Various npm helpers -* [hugo mod tidy](/commands/hugo_mod_tidy/) - Remove unused entries in go.mod and go.sum -* [hugo mod vendor](/commands/hugo_mod_vendor/) - Vendor all module dependencies into the _vendor directory -* [hugo mod verify](/commands/hugo_mod_verify/) - Verify dependencies - diff --git a/docs/content/en/commands/hugo_mod_clean.md b/docs/content/en/commands/hugo_mod_clean.md deleted file mode 100644 index ff2255e53..000000000 --- a/docs/content/en/commands/hugo_mod_clean.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -title: "hugo mod clean" -slug: hugo_mod_clean -url: /commands/hugo_mod_clean/ ---- -## hugo mod clean - -Delete the Hugo Module cache for the current project - -### Synopsis - -Delete the Hugo Module cache for the current project. - -``` -hugo mod clean [flags] [args] -``` - -### Options - -``` - --all clean entire module cache - -b, --baseURL string hostname (and path) to the root, e.g. https://spf13.com/ - --cacheDir string filesystem path to cache directory - -c, --contentDir string filesystem path to content directory - -h, --help help for clean - --pattern string pattern matching module paths to clean (all if not set), e.g. "**hugo*" - --renderSegments strings named segments to render (configured in the segments config) - -t, --theme strings themes to use (located in /themes/THEMENAME/) -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo mod](/commands/hugo_mod/) - Manage modules - diff --git a/docs/content/en/commands/hugo_mod_get.md b/docs/content/en/commands/hugo_mod_get.md deleted file mode 100644 index a5c9a9ea9..000000000 --- a/docs/content/en/commands/hugo_mod_get.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -title: "hugo mod get" -slug: hugo_mod_get -url: /commands/hugo_mod_get/ ---- -## hugo mod get - -Resolves dependencies in your current Hugo project - -### Synopsis - - -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 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) - -Run "go help get" for more information. All flags available for "go get" is also relevant here. - -Note that Hugo will always start out by resolving the components defined in the site -configuration, provided by a _vendor directory (if no --ignoreVendorPaths flag provided), -Go Modules, or a folder inside the themes directory, in that order. - -See https://gohugo.io/hugo-modules/ for more information. - - - -``` -hugo mod get [flags] [args] -``` - -### Options - -``` - -h, --help help for get -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo mod](/commands/hugo_mod/) - Manage modules - diff --git a/docs/content/en/commands/hugo_mod_graph.md b/docs/content/en/commands/hugo_mod_graph.md deleted file mode 100644 index cb2bdfb5a..000000000 --- a/docs/content/en/commands/hugo_mod_graph.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -title: "hugo mod graph" -slug: hugo_mod_graph -url: /commands/hugo_mod_graph/ ---- -## hugo mod graph - -Print a module dependency graph - -### Synopsis - -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. - - -``` -hugo mod graph [flags] [args] -``` - -### Options - -``` - -b, --baseURL string hostname (and path) to the root, e.g. https://spf13.com/ - --cacheDir string filesystem path to cache directory - --clean delete module cache for dependencies that fail verification - -c, --contentDir string filesystem path to content directory - -h, --help help for graph - --renderSegments strings named segments to render (configured in the segments config) - -t, --theme strings themes to use (located in /themes/THEMENAME/) -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo mod](/commands/hugo_mod/) - Manage modules - diff --git a/docs/content/en/commands/hugo_mod_init.md b/docs/content/en/commands/hugo_mod_init.md deleted file mode 100644 index 3315e97d6..000000000 --- a/docs/content/en/commands/hugo_mod_init.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -title: "hugo mod init" -slug: hugo_mod_init -url: /commands/hugo_mod_init/ ---- -## hugo mod init - -Initialize this project as a Hugo Module - -### Synopsis - -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. - - -``` -hugo mod init [flags] [args] -``` - -### Options - -``` - -b, --baseURL string hostname (and path) to the root, e.g. https://spf13.com/ - --cacheDir string filesystem path to cache directory - -c, --contentDir string filesystem path to content directory - -h, --help help for init - --renderSegments strings named segments to render (configured in the segments config) - -t, --theme strings themes to use (located in /themes/THEMENAME/) -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo mod](/commands/hugo_mod/) - Manage modules - diff --git a/docs/content/en/commands/hugo_mod_npm.md b/docs/content/en/commands/hugo_mod_npm.md deleted file mode 100644 index 39a559e0f..000000000 --- a/docs/content/en/commands/hugo_mod_npm.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: "hugo mod npm" -slug: hugo_mod_npm -url: /commands/hugo_mod_npm/ ---- -## hugo mod npm - -Various npm helpers - -### Synopsis - -Various npm (Node package manager) helpers. - -``` -hugo mod npm [command] [flags] -``` - -### Options - -``` - -h, --help help for npm -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo mod](/commands/hugo_mod/) - Manage modules -* [hugo mod npm pack](/commands/hugo_mod_npm_pack/) - Experimental: Prepares and writes a composite package.json file for your project - diff --git a/docs/content/en/commands/hugo_mod_npm_pack.md b/docs/content/en/commands/hugo_mod_npm_pack.md deleted file mode 100644 index 5ece05769..000000000 --- a/docs/content/en/commands/hugo_mod_npm_pack.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -title: "hugo mod npm pack" -slug: hugo_mod_npm_pack -url: /commands/hugo_mod_npm_pack/ ---- -## hugo mod npm pack - -Experimental: Prepares and writes a composite package.json file for your project - -### Synopsis - -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. - -This set will be merged with all "package.hugo.json" files found in the dependency tree, picking the version closest to the project. - -This command is marked as 'Experimental'. We think it's a great idea, so it's not likely to be -removed from Hugo, but we need to test this out in "real life" to get a feel of it, -so this may/will change in future versions of Hugo. - - -``` -hugo mod npm pack [flags] [args] -``` - -### Options - -``` - -b, --baseURL string hostname (and path) to the root, e.g. https://spf13.com/ - --cacheDir string filesystem path to cache directory - -c, --contentDir string filesystem path to content directory - -h, --help help for pack - --renderSegments strings named segments to render (configured in the segments config) - -t, --theme strings themes to use (located in /themes/THEMENAME/) -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo mod npm](/commands/hugo_mod_npm/) - Various npm helpers - diff --git a/docs/content/en/commands/hugo_mod_tidy.md b/docs/content/en/commands/hugo_mod_tidy.md deleted file mode 100644 index c7ae40625..000000000 --- a/docs/content/en/commands/hugo_mod_tidy.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: "hugo mod tidy" -slug: hugo_mod_tidy -url: /commands/hugo_mod_tidy/ ---- -## hugo mod tidy - -Remove unused entries in go.mod and go.sum - -``` -hugo mod tidy [flags] [args] -``` - -### Options - -``` - -b, --baseURL string hostname (and path) to the root, e.g. https://spf13.com/ - --cacheDir string filesystem path to cache directory - -c, --contentDir string filesystem path to content directory - -h, --help help for tidy - --renderSegments strings named segments to render (configured in the segments config) - -t, --theme strings themes to use (located in /themes/THEMENAME/) -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo mod](/commands/hugo_mod/) - Manage modules - diff --git a/docs/content/en/commands/hugo_mod_vendor.md b/docs/content/en/commands/hugo_mod_vendor.md deleted file mode 100644 index dc403affe..000000000 --- a/docs/content/en/commands/hugo_mod_vendor.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -title: "hugo mod vendor" -slug: hugo_mod_vendor -url: /commands/hugo_mod_vendor/ ---- -## hugo mod vendor - -Vendor all module dependencies into the _vendor directory - -### Synopsis - -Vendor all module dependencies into the _vendor directory. - If a module is vendored, that is where Hugo will look for it's dependencies. - - -``` -hugo mod vendor [flags] [args] -``` - -### Options - -``` - -b, --baseURL string hostname (and path) to the root, e.g. https://spf13.com/ - --cacheDir string filesystem path to cache directory - -c, --contentDir string filesystem path to content directory - -h, --help help for vendor - --renderSegments strings named segments to render (configured in the segments config) - -t, --theme strings themes to use (located in /themes/THEMENAME/) -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo mod](/commands/hugo_mod/) - Manage modules - diff --git a/docs/content/en/commands/hugo_mod_verify.md b/docs/content/en/commands/hugo_mod_verify.md deleted file mode 100644 index 2f22a2e49..000000000 --- a/docs/content/en/commands/hugo_mod_verify.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: "hugo mod verify" -slug: hugo_mod_verify -url: /commands/hugo_mod_verify/ ---- -## hugo mod verify - -Verify dependencies - -### Synopsis - -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. - -``` -hugo mod verify [flags] [args] -``` - -### Options - -``` - -b, --baseURL string hostname (and path) to the root, e.g. https://spf13.com/ - --cacheDir string filesystem path to cache directory - --clean delete module cache for dependencies that fail verification - -c, --contentDir string filesystem path to content directory - -h, --help help for verify - --renderSegments strings named segments to render (configured in the segments config) - -t, --theme strings themes to use (located in /themes/THEMENAME/) -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo mod](/commands/hugo_mod/) - Manage modules - diff --git a/docs/content/en/commands/hugo_new.md b/docs/content/en/commands/hugo_new.md deleted file mode 100644 index 2788ef168..000000000 --- a/docs/content/en/commands/hugo_new.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: "hugo new" -slug: hugo_new -url: /commands/hugo_new/ ---- -## hugo new - -Create new content - -### Synopsis - -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. - -### Options - -``` - -h, --help help for new -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo](/commands/hugo/) - Build your site -* [hugo new content](/commands/hugo_new_content/) - Create new content -* [hugo new site](/commands/hugo_new_site/) - Create a new site (skeleton) -* [hugo new theme](/commands/hugo_new_theme/) - Create a new theme (skeleton) - diff --git a/docs/content/en/commands/hugo_new_content.md b/docs/content/en/commands/hugo_new_content.md deleted file mode 100644 index 9624e9a61..000000000 --- a/docs/content/en/commands/hugo_new_content.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -title: "hugo new content" -slug: hugo_new_content -url: /commands/hugo_new_content/ ---- -## hugo new content - -Create new content - -### Synopsis - -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. - -``` -hugo new content [path] [flags] -``` - -### Options - -``` - -b, --baseURL string hostname (and path) to the root, e.g. https://spf13.com/ - --cacheDir string filesystem path to cache directory - -c, --contentDir string filesystem path to content directory - --editor string edit new content with this editor, if provided - -f, --force overwrite file if it already exists - -h, --help help for content - -k, --kind string content type to create - --renderSegments strings named segments to render (configured in the segments config) - -t, --theme strings themes to use (located in /themes/THEMENAME/) -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo new](/commands/hugo_new/) - Create new content - diff --git a/docs/content/en/commands/hugo_new_site.md b/docs/content/en/commands/hugo_new_site.md deleted file mode 100644 index 0f0096ae4..000000000 --- a/docs/content/en/commands/hugo_new_site.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: "hugo new site" -slug: hugo_new_site -url: /commands/hugo_new_site/ ---- -## hugo new site - -Create a new site (skeleton) - -### Synopsis - -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. - -``` -hugo new site [path] [flags] -``` - -### Options - -``` - -f, --force init inside non-empty directory - --format string preferred file format (toml, yaml or json) (default "toml") - -h, --help help for site -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo new](/commands/hugo_new/) - Create new content - diff --git a/docs/content/en/commands/hugo_new_theme.md b/docs/content/en/commands/hugo_new_theme.md deleted file mode 100644 index b1c937bae..000000000 --- a/docs/content/en/commands/hugo_new_theme.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: "hugo new theme" -slug: hugo_new_theme -url: /commands/hugo_new_theme/ ---- -## hugo new theme - -Create a new theme (skeleton) - -### Synopsis - -Create a new theme (skeleton) called [name] in ./themes. -New theme is a skeleton. Please add content to the touched files. Add your -name to the copyright line in the license and adjust the theme.toml file -according to your needs. - -``` -hugo new theme [name] [flags] -``` - -### Options - -``` - -h, --help help for theme -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo new](/commands/hugo_new/) - Create new content - diff --git a/docs/content/en/commands/hugo_server.md b/docs/content/en/commands/hugo_server.md deleted file mode 100644 index d735f449a..000000000 --- a/docs/content/en/commands/hugo_server.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -title: "hugo server" -slug: hugo_server -url: /commands/hugo_server/ ---- -## hugo server - -Start the embedded web server - -### Synopsis - -Hugo provides its own webserver which builds and serves the site. -While hugo server is high performance, it is a webserver with limited options. - -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 -and push the latest content to them. As most Hugo sites are built in a fraction -of a second, you will be able to save and see your changes nearly instantly. - -``` -hugo server [command] [flags] -``` - -### Options - -``` - --appendPort append port to baseURL (default true) - -b, --baseURL string hostname (and path) to the root, e.g. https://spf13.com/ - --bind string interface to which the server will bind (default "127.0.0.1") - -D, --buildDrafts include content marked as draft - -E, --buildExpired include expired content - -F, --buildFuture include content with publishdate in the future - --cacheDir string filesystem path to cache directory - --cleanDestinationDir remove files from destination not found in static directories - -c, --contentDir string filesystem path to content directory - --disableBrowserError do not show build errors in the browser - --disableFastRender enables full re-renders on changes - --disableKinds strings disable different kind of pages (home, RSS etc.) - --disableLiveReload watch without enabling live browser reload on rebuild - --enableGitInfo add Git revision, date, author, and CODEOWNERS info to the pages - --forceSyncStatic copy all files when static is changed. - --gc enable to run some cleanup tasks (remove unused cache files) after the build - -h, --help help for server - --ignoreCache ignores the cache directory - -l, --layoutDir string filesystem path to layout directory - --liveReloadPort int port for live reloading (i.e. 443 in HTTPS proxy situations) (default -1) - --minify minify any supported output format (HTML, XML etc.) - -N, --navigateToChanged navigate to changed content file on live browser reload - --noChmod don't sync permission mode of files - --noHTTPCache prevent HTTP caching - --noTimes don't sync modification time of files - -O, --openBrowser open the site in a browser after server startup - --panicOnWarning panic on first WARNING log - --poll string set this to a poll interval, e.g --poll 700ms, to use a poll based approach to watch for file system changes - -p, --port int port on which the server will listen (default 1313) - --pprof enable the pprof server (port 8080) - --printI18nWarnings print missing translations - --printMemoryUsage print memory usage to screen at intervals - --printPathWarnings print warnings on duplicate target paths etc. - --printUnusedTemplates print warnings on unused templates. - --renderSegments strings named segments to render (configured in the segments config) - --renderStaticToDisk serve static files from disk and dynamic files from memory - --templateMetrics display metrics about template executions - --templateMetricsHints calculate some improvement hints when combined with --templateMetrics - -t, --theme strings themes to use (located in /themes/THEMENAME/) - --tlsAuto generate and use locally-trusted certificates. - --tlsCertFile string path to TLS certificate file - --tlsKeyFile string path to TLS key file - --trace file write trace to file (not useful in general) - -w, --watch watch filesystem for changes and recreate as needed (default true) -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo](/commands/hugo/) - Build your site -* [hugo server trust](/commands/hugo_server_trust/) - Install the local CA in the system trust store - diff --git a/docs/content/en/commands/hugo_server_trust.md b/docs/content/en/commands/hugo_server_trust.md deleted file mode 100644 index 22ca2491e..000000000 --- a/docs/content/en/commands/hugo_server_trust.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: "hugo server trust" -slug: hugo_server_trust -url: /commands/hugo_server_trust/ ---- -## hugo server trust - -Install the local CA in the system trust store - -``` -hugo server trust [flags] [args] -``` - -### Options - -``` - -h, --help help for trust - --uninstall Uninstall the local CA (but do not delete it). -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo server](/commands/hugo_server/) - Start the embedded web server - diff --git a/docs/content/en/commands/hugo_version.md b/docs/content/en/commands/hugo_version.md deleted file mode 100644 index 14cc92a00..000000000 --- a/docs/content/en/commands/hugo_version.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -title: "hugo version" -slug: hugo_version -url: /commands/hugo_version/ ---- -## hugo version - -Display version - -### Synopsis - -Display version and environment info. This is useful in Hugo bug reports. - -``` -hugo version [flags] [args] -``` - -### Options - -``` - -h, --help help for version -``` - -### Options inherited from parent commands - -``` - --clock string set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00 - --config string config file (default is hugo.yaml|json|toml) - --configDir string config dir (default "config") - -d, --destination string filesystem path to write files to - -e, --environment string build environment - --ignoreVendorPaths string ignores any _vendor for module paths matching the given Glob pattern - --logLevel string log level (debug|info|warn|error) - --noBuildLock don't create .hugo_build.lock file - --quiet build in quiet mode - -M, --renderToMemory render to memory (mostly useful when running the server) - -s, --source string filesystem path to read files relative from - --themesDir string filesystem path to themes directory -``` - -### SEE ALSO - -* [hugo](/commands/hugo/) - Build your site - diff --git a/docs/content/en/configuration/_index.md b/docs/content/en/configuration/_index.md deleted file mode 100644 index 7cb08cc73..000000000 --- a/docs/content/en/configuration/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Configuration -description: Configure your site. -categories: [] -keywords: [] -weight: 10 ---- diff --git a/docs/content/en/configuration/all.md b/docs/content/en/configuration/all.md deleted file mode 100644 index 9bc05057f..000000000 --- a/docs/content/en/configuration/all.md +++ /dev/null @@ -1,362 +0,0 @@ ---- -title: All settings -description: The complete list of Hugo configuration settings. -categories: [] -keywords: [] -weight: 20 -aliases: [/getting-started/configuration/] ---- - -## Settings - -archetypeDir -: (`string`) The designated directory for [archetypes](g). Default is `archetypes`. {{% module-mounts-note %}} - -assetDir -: (`string`) The designated directory for [global resources](g). Default is `assets`. {{% module-mounts-note %}} - -baseURL -: (`string`) The absolute URL of your published site including the protocol, host, path, and a trailing slash. - -build -: See [configure build](/configuration/build/). - -buildDrafts -: (`bool`) Whether to include draft content when building a site. Default is `false`. - -buildExpired -: (`bool`) Whether to include expired content when building a site. Default is `false`. - -buildFuture -: (`bool`) Whether to include future content when building a site. Default is `false`. - -cacheDir -: (`string`) The designated cache directory. See [details](#cache-directory). - -caches -: See [configure file caches](/configuration/caches/). - -canonifyURLs -: (`bool`) See [details](/content-management/urls/#canonical-urls) before enabling this feature. Default is `false`. - -capitalizeListTitles -: {{< new-in 0.123.3 />}} -: (`bool`) Whether to capitalize automatic list titles. Applicable to section, taxonomy, and term pages. Default is `true`. Use the [`titleCaseStyle`](#titlecasestyle) setting to configure capitalization rules. - -cascade -: See [configure cascade](/configuration/cascade/). - -cleanDestinationDir -: (`bool`) Whether to remove files from the site's destination directory that do not have corresponding files in the `static` directory during the build. Default is `false`. - -contentDir -: (`string`) The designated directory for content files. Default is `content`. {{% module-mounts-note %}} - -copyright -: (`string`) The copyright notice for a site, typically displayed in the footer. - -dataDir -: (`string`) The designated directory for data files. Default is `data`. {{% module-mounts-note %}} - -defaultContentLanguage -: (`string`) The project's default language key, conforming to the syntax described in [RFC 5646]. This value must match one of the defined language keys. Default is `en`. - -defaultContentLanguageInSubdir -: (`bool`) Whether to publish the default language site to a subdirectory matching the `defaultContentLanguage`. Default is `false`. - -defaultOutputFormat -: (`string`) The default output format for the site. If unspecified, the first available format in the defined order (by weight, then alphabetically) will be used. - -deployment -: See [configure deployment](/configuration/deployment/). - -disableAliases -: (`bool`) Whether to disable generation of alias redirects. Even if this option is enabled, the defined aliases will still be present on the page. This allows you to manage redirects separately, for example, by generating 301 redirects in an `.htaccess` file or a Netlify `_redirects` file using a custom output format. Default is `false`. - -disableDefaultLanguageRedirect -: {{< new-in 0.140.0 />}} -: (`bool`) Whether to disable generation of the alias redirect to the default language when `DefaultContentLanguageInSubdir` is `true`. Default is `false`. - -disableHugoGeneratorInject -: (`bool`) Whether to disable injection of a `` tag into the home page. Default is `false`. - -disableKinds -: (`[]string`) A slice of page [kinds](g) to disable during the build process, any of `404`, `home`, `page`, `robotstxt`, `rss`, `section`, `sitemap`, `taxonomy`, or `term`. - -disableLanguages -: (`[]string]`) A slice of language keys representing the languages to disable during the build process. Although this is functional, consider using the [`disabled`] key under each language instead. - -disableLiveReload -: (`bool`) Whether to disable automatic live reloading of the browser window. Default is `false`. - -disablePathToLower -: (`bool`) Whether to disable transformation of page URLs to lower case. - -enableEmoji -: (`bool`) Whether to allow emoji in Markdown. Default is `false`. - -enableGitInfo -: (`bool`) For sites under Git version control, whether to enable the [`GitInfo`] object for each page. With the [default front matter configuration], the `Lastmod` method on a `Page` object will return the Git author date. Default is `false`. - -enableMissingTranslationPlaceholders -: (`bool`) Whether to show a placeholder instead of the default value or an empty string if a translation is missing. Default is `false`. - -enableRobotsTXT -: (`bool`) Whether to enable generation of a `robots.txt` file. Default is `false`. - -environment -: (`string`) The build environment. Default is `production` when running `hugo` and `development` when running `hugo server`. - -frontmatter -: See [configure front matter](/configuration/front-matter/). - -hasCJKLanguage -: (`bool`) Whether to automatically detect [CJK](g) languages in content. Affects the values returned by the [`WordCount`] and [`FuzzyWordCount`] methods. Default is `false`. - -HTTPCache -: See [configure HTTP cache](/configuration/http-cache/). - -i18nDir -: (`string`) The designated directory for translation tables. Default is `i18n`. {{% module-mounts-note %}} - -ignoreCache -: (`bool`) Whether to ignore the cache directory. Default is `false`. - -ignoreFiles -: (`[]string]`) A slice of [regular expressions](g) used to exclude specific files from a build. These expressions are matched against the absolute file path and apply to files within the `content`, `data`, and `i18n` directories. For more advanced file exclusion options, see the section on [module mounts]. - -ignoreLogs -: (`[]string`) A slice of message identifiers corresponding to warnings and errors you wish to suppress. See [`erroridf`] and [`warnidf`]. - -ignoreVendorPaths -: (`string`) A [glob](g) pattern matching the module paths to exclude from the `_vendor` directory. - -imaging -: See [configure imaging](/configuration/imaging/). - -languageCode -: (`string`) The site's language tag, conforming to the syntax described in [RFC 5646]. This value does not affect translations or localization. Hugo uses this value to populate: - - - The `language` element in the [embedded RSS template] - - The `lang` attribute of the `html` element in the [embedded alias template] - - The `og:locale` `meta` element in the [embedded Open Graph template] - - When present in the root of the configuration, this value is ignored if one or more language keys exists. Please specify this value independently for each language key. - -languages -: See [configure languages](/configuration/languages/). - -layoutDir -: (`string`) The designated directory for templates. Default is `layouts`. {{% module-mounts-note %}} - -mainSections -: (`string` or `[]string`) The main sections of a site. If set, the [`MainSections`] method on the `Site` object returns the given sections, otherwise it returns the section with the most pages. - -markup -: See [configure markup](/configuration/markup/). - -mediaTypes -: See [configure media types](/configuration/media-types/). - -menus -: See [configure menus](/configuration/menus/). - -minify -: See [configure minify](/configuration/minify/). - -module -: See [configure modules](/configuration/module/). - -newContentEditor -: (`string`) The editor to use when creating new content. - -noBuildLock -: (`bool`) Whether to disable creation of the `.hugo_build.lock` file. Default is `false`. - -noChmod -: (`bool`) Whether to disable synchronization of file permission modes. Default is `false`. - -noTimes -: (`bool`) Whether to disable synchronization of file modification times. Default is `false`. - -outputFormats -: See [configure output formats](/configuration/output-formats/). - -outputs -: See [configure outputs](/configuration/outputs/). - -page -: See [configure page](/configuration/page/). - -pagination -: See [configure pagination](/configuration/pagination/). - -panicOnWarning -: (`bool`) Whether to panic on the first WARNING. Default is `false`. - -params -: See [configure params](/configuration/params/). - -permalinks -: See [configure permalinks](/configuration/permalinks/). - -pluralizeListTitles -: (`bool`) Whether to pluralize automatic list titles. Applicable to section pages. Default is `true`. - -printI18nWarnings -: (`bool`) Whether to log WARNINGs for each missing translation. Default is `false`. - -printPathWarnings -: (`bool`) Whether to log WARNINGs when Hugo publishes two or more files to the same path. Default is `false`. - -printUnusedTemplates -: (`bool`) Whether to log WARNINGs for each unused template. Default is `false`. - -privacy -: See [configure privacy](/configuration/privacy/). - -publishDir -: (`string`) The designated directory for publishing the site. Default is `public`. - -refLinksErrorLevel -: (`string`) The logging error level to use when the `ref` and `relref` functions, methods, and shortcodes are unable to resolve a reference to a page. Either `ERROR` or `WARNING`. Any `ERROR` will fail the build. Default is `ERROR`. - -refLinksNotFoundURL -: (`string`) The URL to return when the `ref` and `relref` functions, methods, and shortcodes are unable to resolve a reference to a page. - -related -: See [configure related content](/configuration/related-content/). - -relativeURLs -: (`bool`) See [details](/content-management/urls/#relative-urls) before enabling this feature. Default is `false`. - -removePathAccents -: (`bool`) Whether to remove [non-spacing marks](https://www.compart.com/en/unicode/category/Mn) from [composite characters](https://en.wikipedia.org/wiki/Precomposed_character) in content paths. Default is `false`. - -renderSegments -: {{< new-in 0.124.0 />}} -: (`[]string`) A slice of [segments](g) to render. If omitted, all segments are rendered. This option is typically set via a command-line flag, such as `hugo --renderSegments segment1,segment2`. The provided segment names must correspond to those defined in the [`segments`] configuration. - -resourceDir -: (`string`) The designated directory for caching output from [asset pipelines](g). Default is `resources`. - -security -: See [configure security](/configuration/security/). - -sectionPagesMenu -: (`string`) When set, each top-level section will be added to the menu identified by the provided value. See [details](/content-management/menus/#define-automatically). - -segments -: See [configure segments](/configuration/segments/). - -server -: See [configure server](/configuration/server/). - -services -: See [configure services](/configuration/services/). - -sitemap -: See [configure sitemap](/configuration/sitemap/). - -staticDir -: (`string`) The designated directory for static files. Default is `static`. {{% module-mounts-note %}} - -summaryLength -: (`int`) Applicable to [automatic summaries], the minimum number of words returned by the [`Summary`] method on a `Page` object. The `Summary` method will return content truncated at the paragraph boundary closest to the specified `summaryLength`, but at least this minimum number of words. - -taxonomies -: See [configure taxonomies](/configuration/taxonomies/). - -templateMetrics -: (`bool`) Whether to print template execution metrics to the console. Default is `false`. See [details](/troubleshooting/performance/#template-metrics). - -templateMetricsHints -: (`bool`) Whether to print template execution improvement hints to the console. Applicable when `templateMetrics` is `true`. Default is `false`. See [details](/troubleshooting/performance/#template-metrics). - -theme -: (`string` or `[]string`) The [theme](g) to use. Multiple themes can be listed, with precedence given from left to right. See [details](/hugo-modules/theme-components/). - -themesDir -: (`string`) The designated directory for themes. Default is `themes`. - -timeout -: (`string`) The timeout for generating page content, either as a [duration] or in seconds. This timeout is used to prevent infinite recursion during content generation. You may need to increase this value if your pages take a long time to generate, for example, due to extensive image processing or reliance on remote content. Default is `30s`. - -timeZone -: (`string`) The time zone used to parse dates without time zone offsets, including front matter date fields and values passed to the [`time.AsTime`] and [`time.Format`] template functions. The list of valid values may be system dependent, but should include `UTC`, `Local`, and any location in the [IANA Time Zone Database]. For example, `America/Los_Angeles` and `Europe/Oslo` are valid time zones. - -title -: (`string`) The site title. - -titleCaseStyle -: (`string`) The capitalization rules to follow when Hugo automatically generates a section title, or when using the [`strings.Title`] function. One of `ap`, `chicago`, `go`, `firstupper`, or `none`. Default is `ap`. See [details](#title-case-style). - -uglyurls -: See [configure ugly URLs](/configuration/ugly-urls/). - -## Cache directory - -Hugo's file cache directory is configurable via the [`cacheDir`] configuration option or the `HUGO_CACHEDIR` environment variable. If neither is set, Hugo will use, in order of preference: - -1. If running on Netlify: `/opt/build/cache/hugo_cache/`. This means that if you run your builds on Netlify, all caches configured with `:cacheDir` will be saved and restored on the next build. For other [CI/CD](g) vendors, please read their documentation. For an CircleCI example, see [this configuration]. -1. In a `hugo_cache` directory below the OS user cache directory as defined by Go's [os.UserCacheDir] function. On Unix systems, per the [XDG base directory specification], this is `$XDG_CACHE_HOME` if non-empty, else `$HOME/.cache`. On MacOS, this is `$HOME/Library/Caches`. On Windows, this is`%LocalAppData%`. On Plan 9, this is `$home/lib/cache`. -1. In a `hugo_cache_$USER` directory below the OS temp dir. - -To determine the current `cacheDir`: - -```sh -hugo config | grep cachedir -``` - -## Title case style - -Hugo's [`titleCaseStyle`] setting governs capitalization for automatically generated section titles and the [`strings.Title`] function. By default, it follows the capitalization rules published in the Associated Press Stylebook. Change this setting to use other capitalization rules. - -ap -: Use the capitalization rules published in the [Associated Press Stylebook]. This is the default. - -chicago -: Use the capitalization rules published in the [Chicago Manual of Style]. - -go -: Capitalize the first letter of every word. - -firstupper -: Capitalize the first letter of the first word. - -none -: Disable transformation of automatic section titles, and disable the transformation performed by the `strings.Title` function. This is useful if you would prefer to manually capitalize section titles as needed, and to bypass opinionated theme usage of the `strings.Title` function. - -## Localized settings - -Some configuration settings, such as menus and custom parameters, can be defined separately for each language. See [configure languages](/configuration/languages/#localized-settings). - -[`cacheDir`]: #cachedir -[`disabled`]: /configuration/languages/#disabled -[`erroridf`]: /functions/fmt/erroridf/ -[`FuzzyWordCount`]: /methods/page/fuzzywordcount/ -[`GitInfo`]: /methods/page/gitinfo/ -[`MainSections`]: /methods/site/mainsections/ -[`segments`]: /configuration/segments/ -[`strings.Title`]: /functions/strings/title/ -[`strings.Title`]: /functions/strings/title -[`Summary`]: /methods/page/summary/ -[`time.AsTime`]: /functions/time/astime/ -[`time.Format`]: /functions/time/format/ -[`titleCaseStyle`]: #titlecasestyle -[`warnidf`]: /functions/fmt/warnidf/ -[`WordCount`]: /methods/page/wordcount/ -[Associated Press Stylebook]: https://www.apstylebook.com/ -[automatic summaries]: /content-management/summaries/#automatic-summary -[Chicago Manual of Style]: https://www.chicagomanualofstyle.org/home.html -[default front matter configuration]: /configuration/front-matter/ -[duration]: https://pkg.go.dev/time#Duration -[embedded alias template]: {{% eturl alias %}} -[embedded Open Graph template]: {{% eturl opengraph %}} -[embedded RSS template]: {{% eturl rss %}} -[IANA Time Zone Database]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones -[module mounts]: /configuration/module/#mounts -[os.UserCacheDir]: https://pkg.go.dev/os#UserCacheDir -[RFC 5646]: https://datatracker.ietf.org/doc/html/rfc5646#section-2.1 -[this configuration]: https://github.com/bep/hugo-sass-test/blob/6c3960a8f4b90e8938228688bc49bdcdd6b2d99e/.circleci/config.yml -[XDG base directory specification]: https://specifications.freedesktop.org/basedir-spec/latest/ diff --git a/docs/content/en/configuration/build.md b/docs/content/en/configuration/build.md deleted file mode 100644 index 116294f05..000000000 --- a/docs/content/en/configuration/build.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -title: Configure build -linkTitle: Build -description: Configure global build options. -categories: [] -keywords: [] -aliases: [/getting-started/configuration-build/] ---- - -The `build` configuration section contains global build-related configuration options. - -{{< code-toggle config=build />}} - -buildStats -: See the [build stats](#build-stats) section below. - -cachebusters -: See the [cache busters](#cache-busters) section below. - -noJSConfigInAssets -: (`bool`) Whether to disable writing a `jsconfig.json` in your `assets` directory with mapping of imports from running [js.Build](/hugo-pipes/js). This file is intended to help with intellisense/navigation inside code editors such as [VS Code](https://code.visualstudio.com/). Note that if you do not use `js.Build`, no file will be written. - -useResourceCacheWhen -: (`string`) When to use the resource file cache, one of `never`, `fallback`, or `always`. Applicable when transpiling Sass to CSS. Default is `fallback`. - -## Cache busters - -The `build.cachebusters` configuration option was added to support development using Tailwind 3.x's JIT compiler where a `build` configuration may look like this: - -{{< code-toggle file=hugo >}} -[build] - [build.buildStats] - enable = true - [[build.cachebusters]] - source = "assets/watching/hugo_stats\\.json" - target = "styles\\.css" - [[build.cachebusters]] - source = "(postcss|tailwind)\\.config\\.js" - target = "css" - [[build.cachebusters]] - source = "assets/.*\\.(js|ts|jsx|tsx)" - target = "js" - [[build.cachebusters]] - source = "assets/.*\\.(.*)$" - target = "$1" -{{< /code-toggle >}} - -When `buildStats` is enabled, Hugo writes a `hugo_stats.json` file on each build with HTML classes etc. that's used in the rendered output. Changes to this file will trigger a rebuild of the `styles.css` file. You also need to add `hugo_stats.json` to Hugo's server watcher. See [Hugo Starter Tailwind Basic](https://github.com/bep/hugo-starter-tailwind-basic) for a running example. - -source -: (`string`) A [regular expression](g) matching file(s) relative to one of the virtual component directories in Hugo, typically `assets/...`. - -target -: (`string`) A [regular expression](g) matching the keys in the resource cache that should be expired when `source` changes. You can use the matching regexp groups from `source` in the expression, e.g. `$1`. - -## Build stats - -{{< code-toggle config=build.buildStats />}} - -enable -: (`bool`) Whether to create a `hugo_stats.json` file in the root of your project. This file contains arrays of the `class` attributes, `id` attributes, and tags of every HTML element within your published site. Use this file as data source when [removing unused CSS] from your site. This process is also known as pruning, purging, or tree shaking. Default is `false`. - -[removing unused CSS]: /functions/resources/postprocess/ - -disableIDs -: (`bool`) Whether to exclude `id` attributes. Default is `false`. - -disableTags -: (`bool`) Whether to exclude element tags. Default is `false`. - -disableClasses -: (`bool`) Whether to exclude `class` attributes. Default is `false`. - -> [!note] -> Given that CSS purging is typically limited to production builds, place the `buildStats` object below [`config/production`]. -> -> Built for speed, there may be "false positive" detections (e.g., HTML elements that are not HTML elements) while parsing the published site. These "false positives" are infrequent and inconsequential. - -Due to the nature of partial server builds, new HTML entities are added while the server is running, but old values will not be removed until you restart the server or run a regular `hugo` build. - -[`config/production`]: /configuration/introduction/#configuration-directory diff --git a/docs/content/en/configuration/caches.md b/docs/content/en/configuration/caches.md deleted file mode 100644 index 03b499dcb..000000000 --- a/docs/content/en/configuration/caches.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: Configure file caches -linkTitle: Caches -description: Configure file caches. -categories: [] -keywords: [] ---- - -This is the default configuration: - -{{< code-toggle config=caches />}} - -## Keys - -dir -: (`string`) The absolute file system path where the cached files will be stored. You can begin the path with the `:cacheDir` or `:resourceDir` token. These tokens will be replaced with the actual configured cache directory and resource directory paths, respectively. - -maxAge -: (`string`) The [duration](g) a cached entry remains valid before being evicted. A value of `0` disables the cache. A value of `-1` means the cache entry never expires (the default). - -## Tokens - -`:cacheDir` -: (`string`) The designated cache directory. See [details](/configuration/all/#cachedir). - -`:project` -: (`string`) The base directory name of the current Hugo project. By default, this ensures each project has isolated file caches, so running `hugo --gc` will only affect the current project's cache and not those of other Hugo projects on the same machine. - -`:resourceDir` -: (`string`) The designated directory for caching output from [asset pipelines](g). See [details](/configuration/all/#resourcedir). diff --git a/docs/content/en/configuration/cascade.md b/docs/content/en/configuration/cascade.md deleted file mode 100644 index d91996301..000000000 --- a/docs/content/en/configuration/cascade.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -title: Configure cascade -linkTitle: Cascade -description: Configure cascade. -categories: [] -keywords: [] ---- - -You can configure your site to cascade front matter values to the home page and any of its descendants. However, this cascading will be prevented if the descendant already defines the field, or if a closer ancestor [node](g) has already cascaded a value for the same field through its front matter's `cascade` key. - -> [!note] -> You can also configure cascading behavior within a page's front matter. See [details]. - -For example, to cascade a "color" parameter to the home page and all its descendants: - -{{< code-toggle file=hugo >}} -title = 'Home' -[cascade.params] -color = 'red' -{{< /code-toggle >}} - -## Target - - - -The `target`[^1] keyword allows you to target specific pages or [environments](g). For example, to cascade a "color" parameter to pages within the "articles" section, including the "articles" section page itself: - -[^1]: The `_target` alias for `target` is deprecated and will be removed in a future release. - -{{< code-toggle file=hugo >}} -[cascade.params] -color = 'red' -[cascade.target] -path = '{/articles,/articles/**}' -{{< /code-toggle >}} - -Use any combination of these keywords to target pages and/or environments: - -environment -: (`string`) A [glob](g) pattern matching the build [environment](g). For example: `{staging,production}`. - -kind -: (`string`) A [glob](g) pattern matching the [page kind](g). For example: ` {taxonomy,term}`. - -lang -: (`string`) A [glob](g) pattern matching the [page language]. For example: `{en,de}`. - -path -: (`string`) A [glob](g) pattern matching the page's [logical path](g). For example: `{/books,/books/**}`. - -## Array - -Define an array of cascade parameters to apply different values to different targets. For example: - -{{< code-toggle file=hugo >}} -[[cascade]] -[cascade.params] -color = 'red' -[cascade.target] -path = '{/books/**}' -kind = 'page' -lang = '{en,de}' -[[cascade]] -[cascade.params] -color = 'blue' -[cascade.target] -path = '{/films/**}' -kind = 'page' -environment = 'production' -{{< /code-toggle >}} - -[details]: /content-management/front-matter/#cascade-1 -[page language]: /methods/page/language/ diff --git a/docs/content/en/configuration/content-types.md b/docs/content/en/configuration/content-types.md deleted file mode 100644 index 4c5b5a23b..000000000 --- a/docs/content/en/configuration/content-types.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -title: Configure content types -linkTitle: Content types -description: Configure content types. -categories: [] -keywords: [] ---- - -{{< new-in 0.144.0 />}} - -Hugo supports six [content formats](g): - -{{% include "/_common/content-format-table.md" %}} - -These can be used as either page content or [page resources](g). When used as page resources, their [resource type](g) is `page`. - -Consider this example of a [page bundle](g): - -```text -content/ -└── example/ - ├── index.md <-- content - ├── a.adoc <-- resource (resource type: page) - ├── b.html <-- resource (resource type: page) - ├── c.md <-- resource (resource type: page) - ├── d.org <-- resource (resource type: page) - ├── e.pdc <-- resource (resource type: page) - ├── f.rst <-- resource (resource type: page) - ├── g.jpg <-- resource (resource type: image) - └── h.png <-- resource (resource type: image) -``` - -The `index.md` file is the page's content, while the other files are page resources. Files `a` through `f` are of resource type `page`, while `g` and `h` are of resource type `image`. - -When you build a site, Hugo does not publish page resources having a resource type of `page`. For example, this is the result of building the site above: - -```text -public/ -├── example/ -│ ├── g.jpg -│ ├── h.png -│ └── index.html -└── index.html -``` - -The default behavior is appropriate in most cases. Given that page resources containing markup are typically intended for inclusion in the main content, publishing them independently is generally undesirable. - -The default behavior is determined by the `contentTypes` configuration: - -{{< code-toggle config=contentTypes />}} - -In this default configuration, page resources with those media types will have a resource type of `page`, and will not be automatically published. To change the resource type assignment from `page` to `text` for a given media type, remove the corresponding entry from the list. - -For example, to set the resource type of `text/html` files to `text`, thereby enabling automatic publication, remove the `text/html` entry: - -{{< code-toggle file=hugo >}} -contentTypes: - text/asciidoc: {} - text/markdown: {} - text/org: {} - text/pandoc: {} - text/rst: {} -{{< /code-toggle >}} diff --git a/docs/content/en/configuration/deployment.md b/docs/content/en/configuration/deployment.md deleted file mode 100644 index fad50da63..000000000 --- a/docs/content/en/configuration/deployment.md +++ /dev/null @@ -1,159 +0,0 @@ ---- -title: Configure deployment -linkTitle: Deployment -description: Configure deployments to Amazon S3, Azure Blob Storage, or Google Cloud Storage. -categories: [] -keywords: [] ---- - -> [!note] -> This configuration is only relevant when running `hugo deploy`. See [details](/host-and-deploy/deploy-with-hugo-deploy/). - -## Top-level options - -These settings control the overall behavior of the deployment process. This is the default configuration: - -{{< code-toggle file=hugo config=deployment />}} - -confirm -: (`bool`) Whether to prompt for confirmation before deploying. Default is `false`. - -dryRun -: (`bool`) Whether to simulate the deployment without any remote changes. Default is `false`. - -force -: (`bool`) Whether to re-upload all files. Default is `false`. - -invalidateCDN -: (`bool`) Whether to invalidate the CDN cache listed in the deployment target. Default is `true`. - -maxDeletes -: (`int`) The maximum number of files to delete, or `-1` to disable. Default is `256`. - -matchers -: (`[]*Matcher`) A slice of [matchers](#matchers-1). - -order -: (`[]string`) An ordered slice of [regular expressions](g) that determines upload priority (left to right). Files not matching any expression are uploaded last in an arbitrary order. - -target -: (`string`) The target deployment [`name`](#name). Defaults to the first target. - -targets -: (`[]*Target`) A slice of [targets](#targets-1). - -workers -: (`int`) The number of concurrent workers to use when uploading files. Default is `10`. - -## Targets - -A target represents a deployment target such as "staging" or "production". - -cloudFrontDistributionID -: (`string`) The CloudFront Distribution ID, applicable if you are using the Amazon Web Services CloudFront CDN. Hugo will invalidate the CDN when deploying this target. - -exclude -: (`string`) A [glob](g) pattern matching files to exclude when deploying to this target. Local files failing the include/exclude filters are not uploaded, and remote files failing these filters are not deleted. - -googleCloudCDNOrigin -: (`string`) The Google Cloud project and CDN origin to invalidate when deploying this target, specified as `/`. - -include -: (`string`) A [glob](g) pattern matching files to include when deploying to this target. Local files failing the include/exclude filters are not uploaded, and remote files failing these filters are not deleted. - -name -: (`string`) An arbitrary name for this target. - -stripIndexHTML -: (`bool`) Whether to map files named `/index.html` to `` on the remote (except for the root `index.html`). This is useful for key-value cloud storage (e.g., Amazon S3, Google Cloud Storage, Azure Blob Storage) to align canonical URLs with object keys. Default is `false`. - -url -: (`string`) The [destination URL](#destination-urls) for deployment. - -## Matchers - -A Matcher represents a configuration to be applied to files whose paths match -the specified pattern. - -cacheControl -: (`string`) The caching attributes to use when serving the blob. See [details][cacheControl]. - -contentEncoding -: (`string`) The encoding used for the blob's content, if any. See [details][contentEncoding]. - -contentType -: (`string`) The media type of the blob being written. See [details][contentType]. - -force -: (`bool`) Whether matching files should be re-uploaded. Useful when other route-determined metadata (e.g., `contentType`) has changed. Default is `false`. - -gzip -: (`bool`) Whether the file should be gzipped before upload. If so, the `ContentEncoding` field will automatically be set to `gzip`. Default is `false`. - -pattern -: (`string`) A [regular expression](g) used to match paths. Paths are converted to use forward slashes (`/`) before matching. - -[cacheControl]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control -[contentEncoding]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding -[contentType]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type - -## Destination URLs - -Service|URL example -:--|:-- -Amazon Simple Storage Service (S3)|`s3://my-bucket?region=us-west-1` -Azure Blob Storage|`azblob://my-container` -Google Cloud Storage (GCS)|`gs://my-bucket` - -With Google Cloud Storage you can target a subdirectory: - -```text -gs://my-bucket?prefix=a/subdirectory -``` - -You can also to deploy to storage servers compatible with Amazon S3 such as: - -- [Ceph] -- [MinIO] -- [SeaweedFS] - -[Ceph]: https://ceph.com/ -[Minio]: https://www.minio.io/ -[SeaweedFS]: https://github.com/chrislusf/seaweedfs - -For example, the `url` for a MinIO deployment target might resemble this: - -```text -s3://my-bucket?endpoint=https://my.minio.instance&awssdk=v2&use_path_style=true&disable_https=false -``` - -## Example - -{{< code-toggle file=hugo >}} -[deployment] - order = ['.jpg$', '.gif$'] - [[deployment.matchers]] - cacheControl = 'max-age=31536000, no-transform, public' - gzip = true - pattern = '^.+\.(js|css|svg|ttf)$' - [[deployment.matchers]] - cacheControl = 'max-age=31536000, no-transform, public' - gzip = false - pattern = '^.+\.(png|jpg)$' - [[deployment.matchers]] - contentType = 'application/xml' - gzip = true - pattern = '^sitemap\.xml$' - [[deployment.matchers]] - gzip = true - pattern = '^.+\.(html|xml|json)$' - [[deployment.targets]] - url = 's3://my_production_bucket?region=us-west-1' - cloudFrontDistributionID = 'E1234567890ABCDEF0' - exclude = '**.{heic,psd}' - name = 'production' - [[deployment.targets]] - url = 's3://my_staging_bucket?region=us-west-1' - exclude = '**.{heic,psd}' - name = 'staging' -{{< /code-toggle >}} diff --git a/docs/content/en/configuration/front-matter.md b/docs/content/en/configuration/front-matter.md deleted file mode 100644 index 9f51b8a5a..000000000 --- a/docs/content/en/configuration/front-matter.md +++ /dev/null @@ -1,91 +0,0 @@ ---- -title: Configure front matter -linkTitle: Front matter -description: Configure front matter. -categories: [] -keywords: [] ---- - -## Dates - -There are four methods on a `Page` object that return a date. - -Method|Description -:--|:-- -[`Date`]|Returns the date of the given page. -[`ExpiryDate`]|Returns the expiry date of the given page. -[`Lastmod`]|Returns the last modification date of the given page. -[`PublishDate`]|Returns the publish date of the given page. - -[`Date`]: /methods/page/date -[`ExpiryDate`]: /methods/page/expirydate -[`Lastmod`]: /methods/page/lastmod -[`PublishDate`]: /methods/page/publishdate - -Hugo determines the values to return based on this configuration: - -{{< code-toggle config=frontmatter />}} - -The `ExpiryDate` method, for example, returns the `expirydate` value if it exists, otherwise it returns `unpublishdate`. - -You can also use custom date parameters: - -{{< code-toggle file=hugo >}} -[frontmatter] -date = ["myDate", "date"] -{{< /code-toggle >}} - -In the example above, the `Date` method returns the `myDate` value if it exists, otherwise it returns `date`. - -To fall back to the default sequence of dates, use the `:default` token: - -{{< code-toggle file=hugo >}} -[frontmatter] -date = ["myDate", ":default"] -{{< /code-toggle >}} - -In the example above, the `Date` method returns the `myDate` value if it exists, otherwise it returns the first valid date from `date`, `publishdate`, `pubdate`, `published`, `lastmod`, and `modified`. - -## Aliases - -Some of the front matter fields have aliases. - -Front matter field|Aliases -:--|:-- -`expiryDate`|`unpublishdate` -`lastmod`|`modified` -`publishDate`|`pubdate`, `published` - -The default front matter configuration includes these aliases. - -## Tokens - -Hugo provides several [tokens](g) to assist with front matter configuration. - -Token|Description -:--|:-- -`:default`|The default ordered sequence of date fields. -`:fileModTime`|The file's last modification timestamp. -`:filename`|The date from the file name, if present. -`:git`|The Git author date for the file's last revision. - -When Hugo extracts a date from a file name, it uses the rest of the file name to generate the page's [`slug`], but only if a slug isn't already specified in the page's front matter. For example, given the name `2025-02-01-article.md`, Hugo will set the `date` to `2025-02-01` and the `slug` to `article`. - -[`slug`]: /content-management/front-matter/#slug - -To enable access to the Git author date, set [`enableGitInfo`] to `true`, or use\ -the `--enableGitInfo` flag when building your site. - -[`enableGitInfo`]: /configuration/all/#enablegitinfo - -Consider this example: - -{{< code-toggle file=hugo >}} -[frontmatter] -date = [':filename', ':default'] -lastmod = ['lastmod', ':fileModTime'] -{{< /code-toggle >}} - -To determine `date`, Hugo tries to extract the date from the file name, falling back to the default ordered sequence of date fields. - -To determine `lastmod`, Hugo looks for a `lastmod` field in front matter, falling back to the file's last modification timestamp. diff --git a/docs/content/en/configuration/http-cache.md b/docs/content/en/configuration/http-cache.md deleted file mode 100644 index 788d22a08..000000000 --- a/docs/content/en/configuration/http-cache.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -title: Configure the HTTP cache -linkTitle: HTTP cache -description: Configure the HTTP cache. -categories: [] -keywords: [] ---- - -> [!note] -> This configuration is only relevant when using the [`resources.GetRemote`] function. - -## Layered caching - -Hugo employs a layered caching system. - -```goat {.w-40} - .-----------. -| dynacache | - '-----+-----' - | - v - .----------. -| HTTP cache | - '-----+----' - | - v - .----------. -| file cache | - '-----+----' -``` - -Dynacache -: An in-memory cache employing a Least Recently Used (LRU) eviction policy. Entries are removed from the cache when changes occur, when they match [cache-busting] patterns, or under low-memory conditions. - -HTTP Cache -: An HTTP cache for remote resources as specified in [RFC 9111]. Optimal performance is achieved when resources include appropriate HTTP cache headers. The HTTP cache utilizes the file cache for storage and retrieval of cached resources. - -File cache -: See [configure file caches]. - -The HTTP cache involves two key aspects: determining which content to cache (the caching process itself) and defining the frequency with which to check for updates (the polling strategy). - -## HTTP caching - -The HTTP cache behavior is defined for a configured set of resources. Stale resources will be refreshed from the file cache, even if their configured Time-To-Live (TTL) has not expired. If HTTP caching is disabled for a resource, Hugo will bypass the cache and access the file directly. - -The default configuration disables everything: - -{{< code-toggle file=hugo >}} -[HTTPCache.cache.for] -excludes = ['**'] -includes = [] -{{< /code-toggle >}} - -cache.for.excludes -: (`string`) A list of [glob](g) patterns to exclude from caching. - -cache.for.includes -: (`string`) A list of [glob](g) patterns to cache. - -## HTTP polling - -Polling is used in watch mode (e.g., `hugo server`) to detect changes in remote resources. Polling can be enabled even if HTTP caching is disabled. Detected changes trigger a rebuild of pages using the affected resource. Polling can be disabled for specific resources, typically those known to be static. - -The default configuration disables everything: - -{{< code-toggle file=hugo >}} -[[HTTPCache.polls]] -disable = true -high = '0s' -low = '0s' -[HTTPCache.polls.for] -includes = ['**'] -excludes = [] -{{< /code-toggle >}} - -polls -: A slice of polling configurations. - -polls.disable -: (`bool`) Whether to disable polling for this configuration. - -polls.low -: (`string`) The minimum polling interval expressed as a [duration](g). This is used after a recent change and gradually increases towards `polls.high`. - -polls.high -: (`string`) The maximum polling interval expressed as a [duration](g). This is used when the resource is considered stable. - -polls.for.excludes -: (`string`) A list of [glob](g) patterns to exclude from polling for this configuration. - -polls.for.includes -: (`string`) A list of [glob](g) patterns to include in polling for this configuration. - -## Behavior - -Polling and HTTP caching interact as follows: - -- With polling enabled, rebuilds are triggered only by actual changes, detected via `eTag` changes (Hugo generates an MD5 hash if the server doesn't provide one). -- If polling is enabled but HTTP caching is disabled, the remote is checked for changes only after the file cache's TTL expires (e.g., a `maxAge` of `10h` with a `1s` polling interval is inefficient). -- If both polling and HTTP caching are enabled, changes are checked for even before the file cache's TTL expires. Cached `eTag` and `last-modified` values are sent in `if-none-match` and `if-modified-since` headers, respectively, and a cached response is returned on HTTP [304]. - -[`resources.GetRemote`]: /functions/resources/getremote/ -[304]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304 -[cache-busting]: /configuration/build/#cache-busters -[configure file caches]: /configuration/caches/ -[RFC 9111]: https://datatracker.ietf.org/doc/html/rfc9111 diff --git a/docs/content/en/configuration/imaging.md b/docs/content/en/configuration/imaging.md deleted file mode 100644 index 13ecf9c26..000000000 --- a/docs/content/en/configuration/imaging.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: Configure imaging -linkTitle: Imaging -description: Configure imaging. -categories: [] -keywords: [] ---- - -## Processing options - -These are the default settings for processing images: - -{{< code-toggle file=hugo >}} -[imaging] -anchor = 'Smart' -bgColor = '#ffffff' -hint = 'photo' -quality = 75 -resampleFilter = 'box' -{{< /code-toggle >}} - -anchor -: (`string`) When using the [`Crop`] or [`Fill`] method, the anchor determines the placement of the crop box. One of `TopLeft`, `Top`, `TopRight`, `Left`, `Center`, `Right`, `BottomLeft`, `Bottom`, `BottomRight`, or `Smart`. Default is `Smart`. - -bgColor -: (`string`) The background color of the resulting image. Applicable when converting from a format that supports transparency to a format that does not support transparency, for example, when converting from PNG to JPEG. Expressed as an RGB [hexadecimal] value. Default is `#ffffff`. - -[hexadecimal]: https://developer.mozilla.org/en-US/docs/Web/CSS/hex-color - -hint -: (`string`) Applicable to WebP images, this option corresponds to a set of predefined encoding parameters. One of `drawing`, `icon`, `photo`, `picture`, or `text`. Default is `photo`. See [details](/content-management/image-processing/#hint). - -quality -: (`int`) Applicable to JPEG and WebP images, this value determines the quality of the converted image. Higher values produce better quality images, while lower values produce smaller files. Set this value to a whole number between `1` and `100`, inclusive. Default is `75`. - -resampleFilter -: (`string`) The resampling filter used when resizing an image. Default is `box`. See [details](/content-management/image-processing/#resampling-filter) - -## EXIF data - -These are the default settings for extracting EXIF data from images: - -{{< code-toggle file=hugo >}} -[imaging.exif] -includeFields = "" -excludeFields = "" -disableDate = false -disableLatLong = false -{{< /code-toggle >}} - -disableDate -: (`bool`) Whether to disable extraction of the image creation date/time. Default is `false`. - -disableLatLong -: (`bool`) Whether to disable extraction of the GPS latitude and longitude. Default is `false`. - -excludeFields -: (`string`) A [regular expression](g) matching the tags to exclude when extracting EXIF data. - -includeFields -: (`string`) A [regular expression](g) matching the tags to include when extracting EXIF data. To include all available tags, set this value to `".*"`. - -> [!note] -> To improve performance and decrease cache size, Hugo excludes the following tags: `ColorSpace`, `Contrast`, `Exif`, `Exposure[M|P|B]`, `Flash`, `GPS`, `JPEG`, `Metering`, `Resolution`, `Saturation`, `Sensing`, `Sharp`, and `WhiteBalance`. -> -> To control tag availability, change the `excludeFields` or `includeFields` settings as described above. - -[`Crop`]: /methods/resource/crop/ -[`Fill`]: /methods/resource/fill/ diff --git a/docs/content/en/configuration/introduction.md b/docs/content/en/configuration/introduction.md deleted file mode 100644 index 8f8ad4c1e..000000000 --- a/docs/content/en/configuration/introduction.md +++ /dev/null @@ -1,284 +0,0 @@ ---- -title: Introduction -description: Configure your site using files, directories, and environment variables. -categories: [] -keywords: [] -weight: 10 ---- - -## Sensible defaults - -Hugo offers many configuration options, but its defaults are often sufficient. A new site requires only these settings: - -{{< code-toggle file=hugo >}} -baseURL = 'https://example.org/' -languageCode = 'en-us' -title = 'My New Hugo Site' -{{< /code-toggle >}} - -Only define settings that deviate from the defaults. A smaller configuration file is easier to read, understand, and debug. Keep your configuration concise. - -> [!note] -> The best configuration file is a short configuration file. - -## Configuration file - -Create a site configuration file in the root of your project directory, naming it `hugo.toml`, `hugo.yaml`, or `hugo.json`, with that order of precedence. - -```text -my-project/ -└── hugo.toml -``` - -> [!note] -> For versions v0.109.0 and earlier, the site configuration file was named `config`. While you can still use this name, it's recommended to switch to the newer naming convention, `hugo`. - -A simple example: - -{{< code-toggle file=hugo >}} -baseURL = 'https://example.org/' -languageCode = 'en-us' -title = 'ABC Widgets, Inc.' -[params] -subtitle = 'The Best Widgets on Earth' -[params.contact] -email = 'info@example.org' -phone = '+1 202-555-1212' -{{< /code-toggle >}} - -To use a different configuration file when building your site, use the `--config` flag: - -```sh -hugo --config other.toml -``` - -Combine two or more configuration files, with left-to-right precedence: - -```sh -hugo --config a.toml,b.yaml,c.json -``` - -> [!note] -> See the specifications for each file format: [TOML], [YAML], and [JSON]. - -## Configuration directory - -Instead of a single site configuration file, split your configuration by [environment](g), root configuration key, and language. For example: - -```text -my-project/ -└── config/ - ├── _default/ - │ ├── hugo.toml - │ ├── menus.en.toml - │ ├── menus.de.toml - │ └── params.toml - └── production/ - └── params.toml -``` - -The root configuration keys are {{< root-configuration-keys >}}. - -### Omit the root key - -When splitting the configuration by root key, omit the root key in the component file. For example, these are equivalent: - -{{< code-toggle file=config/_default/hugo >}} -[params] -foo = 'bar' -{{< /code-toggle >}} - -{{< code-toggle file=config/_default/params >}} -foo = 'bar' -{{< /code-toggle >}} - -### Recursive parsing - -Hugo parses the `config` directory recursively, allowing you to organize the files into subdirectories. For example: - -```text -my-project/ -└── config/ - └── _default/ - ├── navigation/ - │ ├── menus.de.toml - │ └── menus.en.toml - └── hugo.toml -``` - -### Example - -```text -my-project/ -└── config/ - ├── _default/ - │ ├── hugo.toml - │ ├── menus.en.toml - │ ├── menus.de.toml - │ └── params.toml - ├── production/ - │ ├── hugo.toml - │ └── params.toml - └── staging/ - ├── hugo.toml - └── params.toml -``` - -Considering the structure above, when running `hugo --environment staging`, Hugo will use every setting from `config/_default` and merge `staging`'s on top of those. - -Let's take an example to understand this better. Let's say you are using Google Analytics for your website. This requires you to specify a [Google tag ID] in your site configuration: - -{{< code-toggle file=hugo >}} -[services.googleAnalytics] -ID = 'G-XXXXXXXXX' -{{< /code-toggle >}} - -Now consider the following scenario: - -1. You don't want to load the analytics code when running `hugo server`. -1. You want to use different Google tag IDs for your production and staging environments. For example: - - `G-PPPPPPPPP` for production - - `G-SSSSSSSSS` for staging - -To satisfy these requirements, configure your site as follows: - -1. `config/_default/hugo.toml` - - Exclude the `services.googleAnalytics` section. This will prevent loading of the analytics code when you run `hugo server`. - - By default, Hugo sets its `environment` to `development` when running `hugo server`. In the absence of a `config/development` directory, Hugo uses the `config/_default` directory. -1. `config/production/hugo.toml` - - Include this section only: - - {{< code-toggle file=hugo >}} - [services.googleAnalytics] - ID = 'G-PPPPPPPPP' - {{< /code-toggle >}} - - - You do not need to include other parameters in this file. Include only those parameters that are specific to your production environment. Hugo will merge these parameters with the default configuration. - - By default, Hugo sets its `environment` to `production` when running `hugo`. The analytics code will use the `G-PPPPPPPPP` tag ID. - -1. `config/staging/hugo.toml` - - - Include this section only: - - {{< code-toggle file=hugo >}} - [services.googleAnalytics] - ID = 'G-SSSSSSSSS' - {{< /code-toggle >}} - - - You do not need to include other parameters in this file. Include only those parameters that are specific to your staging environment. Hugo will merge these parameters with the default configuration. - - To build your staging site, run `hugo --environment staging`. The analytics code will use the `G-SSSSSSSSS` tag ID. - -## Merge configuration settings - -Hugo merges configuration settings from themes and modules, prioritizing the project's own settings. Given this simplified project structure with two themes: - -```text -project/ -├── themes/ -│ ├── theme-a/ -│ │ └── hugo.toml -│ └── theme-b/ -│ └── hugo.toml -└── hugo.toml -``` - -and this project-level configuration: - -{{< code-toggle file=hugo >}} -baseURL = 'https://example.org/' -languageCode = 'en-us' -title = 'My New Hugo Site' -theme = ['theme-a','theme-b'] -{{< /code-toggle >}} - -Hugo merges settings in this order: - -1. Project configuration (`hugo.toml` in the project root) -1. `theme-a` configuration -1. `theme-b` configuration - -The `_merge` setting within each top-level configuration key controls _which_ settings are merged and _how_ they are merged. - -The value for `_merge` can be one of: - -none -: No merge. - -shallow -: Only add values for new keys. - -deep -: Add values for new keys, merge existing. - -Note that you don't need to be so verbose as in the default setup below; a `_merge` value higher up will be inherited if not set. - -{{< code-toggle file=hugo dataKey="config_helpers.mergeStrategy" skipHeader=true />}} - -## Environment variables - -You can also configure settings using operating system environment variables: - -```sh -export HUGO_BASEURL=https://example.org/ -export HUGO_ENABLEGITINFO=true -export HUGO_ENVIRONMENT=staging -hugo -``` - -The above sets the [`baseURL`], [`enableGitInfo`], and [`environment`] configuration options and then builds your site. - -> [!note] -> An environment variable takes precedence over the values set in the configuration file. This means that if you set a configuration value with both an environment variable and in the configuration file, the value in the environment variable will be used. - -Environment variables simplify configuration for [CI/CD](g) deployments like GitHub Pages, GitLab Pages, and Netlify by allowing you to set values directly within their respective configuration and workflow files. - -> [!note] -> Environment variable names must be prefixed with `HUGO_`. -> -> To set custom site parameters, prefix the name with `HUGO_PARAMS_`. - -For snake_case variable names, the standard `HUGO_` prefix won't work. Hugo infers the delimiter from the first character following `HUGO`. This allows for variations like `HUGOxPARAMSxAPI_KEY=abcdefgh` using any [permitted delimiter]. - -In addition to configuring standard settings, environment variables may be used to override default values for certain internal settings: - -DART_SASS_BINARY -: (`string`) The absolute path to the Dart Sass executable. By default, Hugo searches for the executable in each of the paths in the `PATH` environment variable. - -HUGO_FILE_LOG_FORMAT -: (`string`) A format string for the file path, line number, and column number displayed when reporting errors, or when calling the `Position` method from a shortcode or Markdown render hook. Valid tokens are `:file`, `:line`, and `:col`. Default is `:file::line::col`. - -HUGO_MEMORYLIMIT -: {{< new-in 0.123.0 />}} -: (`int`) The maximum amount of system memory, in gigabytes, that Hugo can use while rendering your site. Default is 25% of total system memory. Note that `HUGO_MEMORYLIMIT` is a "best effort" setting. Don't expect Hugo to build a million pages with only 1 GB of memory. You can get more information about how this behaves during the build by building with `hugo --logLevel info` and look for the `dynacache` label. - -HUGO_NUMWORKERMULTIPLIER -: (`int`) The number of workers used in parallel processing. Default is the number of logical CPUs. - -## Current configuration - -Display the complete site configuration with: - -```sh -hugo config -``` - -Display a specific configuration setting with: - -```sh -hugo config | grep [key] -``` - -Display the configured file mounts with: - -```sh -hugo config mounts -``` - -[`baseURL`]: /configuration/all#baseurl -[`enableGitInfo`]: /configuration/all#enablegitinfo -[`environment`]: /configuration/all#environment -[Google tag ID]: https://support.google.com/tagmanager/answer/12326985?hl=en -[JSON]: https://datatracker.ietf.org/doc/html/rfc7159 -[permitted delimiter]: https://pubs.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap08.html -[TOML]: https://toml.io/en/latest -[YAML]: https://yaml.org/spec/ diff --git a/docs/content/en/configuration/languages.md b/docs/content/en/configuration/languages.md deleted file mode 100644 index 540cfd34f..000000000 --- a/docs/content/en/configuration/languages.md +++ /dev/null @@ -1,193 +0,0 @@ ---- -title: Configure languages -linkTitle: Languages -description: Configure the languages in your multilingual site. -categories: [] -keywords: [] ---- - -## Base settings - -Configure the following base settings within the site's root configuration: - -{{< code-toggle file=hugo >}} -defaultContentLanguage = 'en' -defaultContentLanguageInSubdir = false -disableDefaultLanguageRedirect = false -disableLanguages = [] -{{< /code-toggle >}} - -defaultContentLanguage -: (`string`) The project's default language key, conforming to the syntax described in [RFC 5646]. This value must match one of the defined [language keys](#language-keys). Default is `en`. - -defaultContentLanguageInSubdir -: (`bool`) Whether to publish the default language site to a subdirectory matching the `defaultContentLanguage`. Default is `false`. - -disableDefaultLanguageRedirect -: {{< new-in 0.140.0 />}} -: (`bool`) Whether to disable generation of the alias redirect to the default language when `DefaultContentLanguageInSubdir` is `true`. Default is `false`. - -disableLanguages -: (`[]string]`) A slice of language keys representing the languages to disable during the build process. Although this is functional, consider using the [`disabled`](#disabled) key under each language instead. - -## Language settings - -Configure each language under the `languages` key: - -{{< code-toggle config=languages />}} - -In the above, `en` is the [language key](#language-keys). - -disabled -: (`bool`) Whether to disable this language when building the site. Default is `false`. - -languageCode -: (`string`) The language tag as described in [RFC 5646]. This value does not affect localization or URLs. Hugo uses this value to populate: - - - The `lang` attribute of the `html` element in the [embedded alias template] - - The `language` element in the [embedded RSS template] - - The `locale` property in the [embedded OpenGraph template] - - Access this value from a template using the [`Language.LanguageCode`] method on a `Site` or `Page` object. - -languageDirection -: (`string`) The language direction, either left-to-right (`ltr`) or right-to-left (`rtl`). Use this value in your templates with the global [`dir`] HTML attribute. Access this value from a template using the [`Language.LanguageDirection`] method on a `Site` or `Page` object. - -languageName -: (`string`) The language name, typically used when rendering a language switcher. Access this value from a template using the [`Language.LanguageName`] method on a `Site` or `Page` object. - -title -: (`string`) The site title for this language. Access this value from a template using the [`Title`] method on a `Site` object. - -weight -: (`int`) The language [weight](g). When set to a non-zero value, this is the primary sort criteria for this language. Access this value from a template using the [`Language.Weight`] method on a `Site` or `Page` object. - -## Localized settings - -Some configuration settings can be defined separately for each language. For example: - -{{< code-toggle file=hugo >}} -[languages.en] -languageCode = 'en-US' -languageName = 'English' -weight = 1 -title = 'Project Documentation' -timeZone = 'America/New_York' -[languages.en.pagination] -path = 'page' -[languages.en.params] -subtitle = 'Reference, Tutorials, and Explanations' -{{< /code-toggle >}} - -The following configuration keys can be defined separately for each language: - -{{< per-lang-config-keys >}} - -Any key not defined in a `languages` object will fall back to the global value in the root of the site configuration. - -## Language keys - -Language keys must conform to the syntax described in [RFC 5646]. For example: - -{{< code-toggle file=hugo >}} -defaultContentLanguage = 'de' -[languages.de] - weight = 1 -[languages.en-US] - weight = 2 -[languages.pt-BR] - weight = 3 -{{< /code-toggle >}} - -Artificial languages with private use subtags as defined in [RFC 5646 § 2.2.7] are also supported. Omit the `art-x-` prefix from the language key. For example: - -{{< code-toggle file=hugo >}} -defaultContentLanguage = 'en' -[languages.en] -weight = 1 -[languages.hugolang] -weight = 2 -{{< /code-toggle >}} - -> [!note] -> Private use subtags must not exceed 8 alphanumeric characters. - -## Example - -{{< code-toggle file=hugo >}} -defaultContentLanguage = 'de' -defaultContentLanguageInSubdir = true -disableDefaultLanguageRedirect = false - -[languages.de] -contentDir = 'content/de' -disabled = false -languageCode = 'de-DE' -languageDirection = 'ltr' -languageName = 'Deutsch' -title = 'Projekt Dokumentation' -weight = 1 - -[languages.de.params] -subtitle = 'Referenz, Tutorials und Erklärungen' - -[languages.en] -contentDir = 'content/en' -disabled = false -languageCode = 'en-US' -languageDirection = 'ltr' -languageName = 'English' -title = 'Project Documentation' -weight = 2 - -[languages.en.params] -subtitle = 'Reference, Tutorials, and Explanations' -{{< /code-toggle >}} - -> [!note] -> In the example above, omit `contentDir` if [translating by file name]. - -## Multihost - -Hugo supports multiple languages in a multihost configuration. This means you can configure a `baseURL` per `language`. - -> [!note] -> If you define a `baseURL` for one language, you must define a unique `baseURL` for all languages. - -For example: - -{{< code-toggle file=hugo >}} -defaultContentLanguage = 'fr' -[languages] - [languages.en] - baseURL = 'https://en.example.org/' - languageName = 'English' - title = 'In English' - weight = 2 - [languages.fr] - baseURL = 'https://fr.example.org' - languageName = 'Français' - title = 'En Français' - weight = 1 -{{}} - -With the above, Hugo publishes two sites, each with their own root: - -```text -public -├── en -└── fr -``` - -[`dir`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/dir -[`Language.LanguageCode`]: /methods/site/language/#languagecode -[`Language.LanguageDirection`]: /methods/site/language/#languagedirection -[`Language.LanguageName`]: /methods/site/language/#languagename -[`Language.Weight`]: /methods/site/language/#weight -[`Title`]: /methods/site/title/ -[embedded alias template]: {{% eturl alias %}} -[embedded OpenGraph template]: {{% eturl opengraph %}} -[embedded RSS template]: {{% eturl rss %}} -[RFC 5646]: https://datatracker.ietf.org/doc/html/rfc5646#section-2.1 -[RFC 5646 § 2.2.7]: https://datatracker.ietf.org/doc/html/rfc5646#section-2.2.7 -[translating by file name]: /content-management/multilingual/#translation-by-file-name diff --git a/docs/content/en/configuration/markup.md b/docs/content/en/configuration/markup.md deleted file mode 100644 index b6135cee5..000000000 --- a/docs/content/en/configuration/markup.md +++ /dev/null @@ -1,341 +0,0 @@ ---- -title: Configure markup -linkTitle: Markup -description: Configure markup. -categories: [] -keywords: [] -aliases: [/getting-started/configuration-markup/] ---- - -## Default handler - -In its default configuration, Hugo uses [Goldmark] to render Markdown to HTML. - -{{< code-toggle file=hugo >}} -[markup] -defaultMarkdownHandler = 'goldmark' -{{< /code-toggle >}} - -Files with ending with `.md`, `.mdown`, or `.markdown` are processed as Markdown, unless you've explicitly set a different format using the `markup` field in your front matter. - -To use a different renderer for Markdown files, specify one of `asciidocext`, `org`, `pandoc`, or `rst` in your site configuration. - -`defaultMarkdownHandler`|Renderer -:--|:-- -`asciidocext`|[AsciiDoc] -`goldmark`|[Goldmark] -`org`|[Emacs Org Mode] -`pandoc`|[Pandoc] -`rst`|[reStructuredText] - -To use AsciiDoc, Pandoc, or reStructuredText you must install the relevant renderer and update your [security policy]. - -> [!note] -> Unless you need a unique capability provided by one of the alternative Markdown handlers, we strongly recommend that you use the default setting. Goldmark is fast, well maintained, conforms to the [CommonMark] specification, and is compatible with [GitHub Flavored Markdown] (GFM). - -## Goldmark - -This is the default configuration for the Goldmark Markdown renderer: - -{{< code-toggle config=markup.goldmark />}} - -### Extensions - -The extensions below, excluding Extras and Passthrough, are enabled by default. - -Extension|Documentation|Enabled -:--|:--|:-: -`cjk`|[Goldmark Extensions: CJK]|:heavy_check_mark: -`definitionList`|[PHP Markdown Extra: Definition lists]|:heavy_check_mark: -`extras`|[Hugo Goldmark Extensions: Extras]|| -`footnote`|[PHP Markdown Extra: Footnotes]|:heavy_check_mark: -`linkify`|[GitHub Flavored Markdown: Autolinks]|:heavy_check_mark: -`passthrough`|[Hugo Goldmark Extensions: Passthrough]|| -`strikethrough`|[GitHub Flavored Markdown: Strikethrough]|:heavy_check_mark: -`table`|[GitHub Flavored Markdown: Tables]|:heavy_check_mark: -`taskList`|[GitHub Flavored Markdown: Task list items]|:heavy_check_mark: -`typographer`|[Goldmark Extensions: Typographer]|:heavy_check_mark: - -#### Extras - -{{< new-in 0.126.0 />}} - -Enable [deleted text], [inserted text], [mark text], [subscript], and [superscript] elements in Markdown. - -Element|Markdown|Rendered -:--|:--|:-- -Deleted text|`~~foo~~`|`foo` -Inserted text|`++bar++`|`bar` -Mark text|`==baz==`|`baz` -Subscript|`H~2~O`|`H2O` -Superscript|`1^st^`|`1st` - -To avoid a conflict when enabling the "subscript" feature of the Extras extension, if you want to render subscript and strikethrough text concurrently you must: - -1. Disable the Strikethrough extension -1. Enable the "deleted text" feature of the Extras extension - -For example: - -{{< code-toggle file=hugo >}} -[markup.goldmark.extensions] -strikethrough = false - -[markup.goldmark.extensions.extras.delete] -enable = true - -[markup.goldmark.extensions.extras.subscript] -enable = true -{{< /code-toggle >}} - -#### Passthrough - -{{< new-in 0.122.0 />}} - -Enable the Passthrough extension to include mathematical equations and expressions in Markdown using LaTeX markup. See [mathematics in Markdown] for details. - -#### Typographer - -The Typographer extension replaces certain character combinations with HTML entities as specified below: - -Markdown|Replaced by|Description -:--|:--|:-- -`...`|`…`|horizontal ellipsis -`'`|`’`|apostrophe -`--`|`–`|en dash -`---`|`—`|em dash -`«`|`«`|left angle quote -`“`|`“`|left double quote -`‘`|`‘`|left single quote -`»`|`»`|right angle quote -`”`|`”`|right double quote -`’`|`’`|right single quote - -### Settings explained - -Most of the Goldmark settings above are self-explanatory, but some require explanation. - -duplicateResourceFiles -: {{< new-in 0.123.0 />}} -: (`bool`) Whether to duplicate shared page resources for each language on multilingual single-host sites. See [multilingual page resources] for details. Default is `false`. - - > [!note] - > With multilingual single-host sites, setting this parameter to `false` will enable Hugo's [embedded link render hook] and [embedded image render hook]. This is the default configuration for multilingual single-host sites. - -parser.wrapStandAloneImageWithinParagraph -: (`bool`) Whether to wrap image elements without adjacent content within a `p` element when rendered. This is the default Markdown behavior. Set to `false` when using an [image render hook] to render standalone images as `figure` elements. Default is `true`. - -parser.autoDefinitionTermID -: {{< new-in 0.144.0 />}} -: (`bool`) Whether to automatically add `id` attributes to description list terms (i.e., `dt` elements). When `true`, the `id` attribute of each `dt` element is accessible through the [`Fragments.Identifiers`] method on a `Page` object. - -parser.autoHeadingID -: (`bool`) Whether to automatically add `id` attributes to headings (i.e., `h1`, `h2`, `h3`, `h4`, `h5`, and `h6` elements). - -parser.autoIDType -: (`string`) The strategy used to automatically generate `id` attributes, one of `github`, `github-ascii` or `blackfriday`. - - - `github` produces GitHub-compatible `id` attributes - - `github-ascii` drops any non-ASCII characters after accent normalization - - `blackfriday` produces `id` attributes compatible with the Blackfriday Markdown renderer - - This is also the strategy used by the [anchorize](/functions/urls/anchorize) template function. Default is `github`. - -parser.attribute.block -: (`bool`) Whether to enable [Markdown attributes] for block elements. Default is `false`. - -parser.attribute.title -: (`bool`) Whether to enable [Markdown attributes] for headings. Default is `true`. - -renderHooks.image.enableDefault -: {{< new-in 0.123.0 />}} -: (`bool`) Whether to enable the [embedded image render hook]. Default is `false`. - - > [!note] - > The embedded image render hook is automatically enabled for multilingual single-host sites if [duplication of shared page resources] is disabled. This is the default configuration for multilingual single-host sites. - -renderHooks.link.enableDefault -: {{< new-in 0.123.0 />}} -: (`bool`) Whether to enable the [embedded link render hook]. Default is `false`. - - > [!note] - > The embedded link render hook is automatically enabled for multilingual single-host sites if [duplication of shared page resources] is disabled. This is the default configuration for multilingual single-host sites. - -renderer.hardWraps -: (`bool`) Whether to replace newline characters within a paragraph with `br` elements. Default is `false`. - -renderer.unsafe -: (`bool`) Whether to render raw HTML mixed within Markdown. This is unsafe unless the content is under your control. Default is `false`. - -## AsciiDoc - -This is the default configuration for the AsciiDoc renderer: - -{{< code-toggle config=markup.asciidocExt />}} - -### Settings explained - -attributes -: (`map`) A map of key-value pairs, each a document attribute. See Asciidoctor's [attributes]. - -backend -: (`string`) The backend output file format. Default is `html5`. - -extensions -: (`string array`) An array of enabled extensions, one or more of `asciidoctor-html5s`, `asciidoctor-bibtex`, `asciidoctor-diagram`, `asciidoctor-interdoc-reftext`, `asciidoctor-katex`, `asciidoctor-latex`, `asciidoctor-mathematical`, or `asciidoctor-question`. - - > [!note] - > To mitigate security risks, entries in the extension array may not contain forward slashes (`/`), backslashes (`\`), or periods. Due to this restriction, extensions must be in Ruby's `$LOAD_PATH`. - -failureLevel -: (`string`) The minimum logging level that triggers a non-zero exit code (failure). Default is `fatal`. - -noHeaderOrFooter -: (`bool`) Whether to output an embeddable document, which excludes the header, the footer, and everything outside the body of the document. Default is `true`. - -preserveTOC -: (`bool`) Whether to preserve the table of contents (TOC) rendered by Asciidoctor. By default, to make the TOC compatible with existing themes, Hugo removes the TOC rendered by Asciidoctor. To render the TOC, use the [`TableOfContents`] method on a `Page` object in your templates. Default is `false`. - -safeMode -: (`string`) The safe mode level, one of `unsafe`, `safe`, `server`, or `secure`. Default is `unsafe`. - -sectionNumbers -: (`bool`) Whether to number each section title. Default is `false`. - -trace -: (`bool`) Whether to include backtrace information on errors. Default is `false`. - -verbose -: (`bool`) Whether to verbosely print processing information and configuration file checks to stderr. Default is `false`. - -workingFolderCurrent -: (`bool`) Whether to set the working directory to be the same as that of the AsciiDoc file being processed, allowing [includes] to work with relative paths. Set to `true` to render diagrams with the [asciidoctor-diagram] extension. Default is `false`. - -### Configuration example - -{{< code-toggle file=hugo >}} -[markup.asciidocExt] - extensions = ["asciidoctor-html5s", "asciidoctor-diagram"] - workingFolderCurrent = true - [markup.asciidocExt.attributes] - my-base-url = "https://example.com/" - my-attribute-name = "my value" -{{< /code-toggle >}} - -### Syntax highlighting - -Follow the steps below to enable syntax highlighting. - -#### Step 1 - -Set the `source-highlighter` attribute in your site configuration. For example: - -{{< code-toggle file=hugo >}} -[markup.asciidocExt.attributes] -source-highlighter = 'rouge' -{{< /code-toggle >}} - -#### Step 2 - -Generate the highlighter CSS. For example: - -```text -rougify style monokai.sublime > assets/css/syntax.css -``` - -#### Step 3 - -In your base template add a link to the CSS file: - -```go-html-template {file="layouts/_default/baseof.html"} - - ... - {{ with resources.Get "css/syntax.css" }} - - {{ end }} - ... - -``` - -Then add the code to be highlighted to your markup: - -```text -[#hello,ruby] ----- -require 'sinatra' - -get '/hi' do - "Hello World!" -end ----- -``` - -### Troubleshooting - -Run `hugo --logLevel debug` to examine Hugo's call to the Asciidoctor executable: - -```txt -INFO 2019/12/22 09:08:48 Rendering book-as-pdf.adoc with C:\Ruby26-x64\bin\asciidoctor.bat using asciidoc args [--no-header-footer -r asciidoctor-html5s -b html5s -r asciidoctor-diagram --base-dir D:\prototypes\hugo_asciidoc_ddd\docs -a outdir=D:\prototypes\hugo_asciidoc_ddd\build -] ... -``` - -## Highlight - -This is the default configuration. - -{{< code-toggle config=markup.highlight />}} - -{{% include "/_common/syntax-highlighting-options.md" %}} - -## Table of contents - -This is the default configuration for the table of contents, applicable to Goldmark and Asciidoctor: - -{{< code-toggle config=markup.tableOfContents />}} - -startLevel -: (`int`) Heading levels less than this value will be excluded from the table of contents. For example, to exclude `h1` elements from the table of contents, set this value to `2`. Default is `2`. - -endLevel -: (`int`) Heading levels greater than this value will be excluded from the table of contents. For example, to exclude `h4`, `h5`, and `h6` elements from the table of contents, set this value to `3`. Default is `3`. - -ordered -: (`bool`) Whether to generates an ordered list instead of an unordered list. Default is `false`. - -[`Fragments.Identifiers`]: /methods/page/fragments/#identifiers -[`TableOfContents`]: /methods/page/tableofcontents/ -[asciidoctor-diagram]: https://asciidoctor.org/docs/asciidoctor-diagram/ -[attributes]: https://asciidoctor.org/docs/asciidoc-syntax-quick-reference/#attributes-and-substitutions -[CommonMark]: https://spec.commonmark.org/current/ -[deleted text]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/del -[duplication of shared page resources]: /configuration/markup/#duplicateresourcefiles -[duplication of shared page resources]: /configuration/markup/#duplicateresourcefiles -[embedded image render hook]: /render-hooks/images/#default -[embedded image render hook]: /render-hooks/images/#default -[embedded link render hook]: /render-hooks/links/#default -[embedded link render hook]: /render-hooks/links/#default -[GitHub Flavored Markdown]: https://github.github.com/gfm/ -[GitHub Flavored Markdown: Autolinks]: https://github.github.com/gfm/#autolinks-extension- -[GitHub Flavored Markdown: Strikethrough]: https://github.github.com/gfm/#strikethrough-extension- -[GitHub Flavored Markdown: Tables]: https://github.github.com/gfm/#tables-extension- -[GitHub Flavored Markdown: Task list items]: https://github.github.com/gfm/#task-list-items-extension- -[Goldmark]: https://github.com/yuin/goldmark/ -[Goldmark Extensions: CJK]: https://github.com/yuin/goldmark?tab=readme-ov-file#cjk-extension -[Goldmark Extensions: Typographer]: https://github.com/yuin/goldmark?tab=readme-ov-file#typographer-extension -[Hugo Goldmark Extensions: Extras]: https://github.com/gohugoio/hugo-goldmark-extensions?tab=readme-ov-file#extras-extension -[Hugo Goldmark Extensions: Passthrough]: https://github.com/gohugoio/hugo-goldmark-extensions?tab=readme-ov-file#passthrough-extension -[image render hook]: /render-hooks/images/ -[includes]: https://docs.asciidoctor.org/asciidoc/latest/syntax-quick-reference/#includes -[inserted text]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ins -[mark text]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/mark -[Markdown attributes]: /content-management/markdown-attributes/ -[mathematics in Markdown]: content-management/mathematics/ -[multilingual page resources]: /content-management/page-resources/#multilingual -[PHP Markdown Extra: Definition lists]: https://michelf.ca/projects/php-markdown/extra/#def-list -[PHP Markdown Extra: Footnotes]: https://michelf.ca/projects/php-markdown/extra/#footnotes -[security policy]: /configuration/security/ -[subscript]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/sub -[superscript]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/sup -[AsciiDoc]: https://asciidoc.org/ -[Emacs Org Mode]: https://orgmode.org/ -[Pandoc]: https://www.pandoc.org/ -[reStructuredText]: https://docutils.sourceforge.io/rst.html diff --git a/docs/content/en/configuration/media-types.md b/docs/content/en/configuration/media-types.md deleted file mode 100644 index ea89ee04a..000000000 --- a/docs/content/en/configuration/media-types.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -title: Configure media types -linkTitle: Media types -description: Configure media types. -categories: [] -keywords: [] ---- - -{{% glossary-term "media type" %}} - -Configured media types serve multiple purposes in Hugo, including the definition of [output formats](g). This is the default media type configuration in tabular form: - -{{< datatable "config" "mediaTypes" "_key" "suffixes" >}} - -The `suffixes` column in the table above shows the suffixes associated with each media type. For example, Hugo associates `.html` and `.htm` files with the `text/html` media type. - -> [!note] -> The first suffix is the primary suffix. Use the primary suffix when naming template files. For example, when creating a template for an RSS feed, use the `xml` suffix. - -## Default configuration - -The following is the default configuration that matches the table above: - -{{< code-toggle file=hugo config=mediaTypes />}} - -delimiter -: (`string`) The delimiter between the file name and the suffix. The delimiter, in conjunction with the suffix, forms the file extension. Default is `"."`. - -suffixes -: (`[]string`) The suffixes associated with this media type. The first suffix is the primary suffix. - -## Modify a media type - -You can modify any of the default media types. For example, to switch the primary suffix for `text/html` from `html` to `htm`: - -{{< code-toggle file=hugo >}} -[mediaTypes.'text/html'] -suffixes = ['htm','html'] -{{< /code-toggle >}} - -If you alter a default media type, you must also explicitly redefine all output formats that utilize that media type. For example, to ensure the changes above affect the `html` output format, redefine the `html` output format: - -{{< code-toggle file=hugo >}} -[outputFormats.html] -mediaType = 'text/html' -{{< /code-toggle >}} - -## Create a media type - -You can create new media types as needed. For example, to create a media type for an Atom feed: - -{{< code-toggle file=hugo >}} -[mediaTypes.'application/atom+xml'] -suffixes = ['atom'] -{{< /code-toggle >}} - -## Media types without suffixes - -Occasionally, you may need to create a media type without a suffix or delimiter. For example, [Netlify] recognizes configuration files named `_redirects` and `_headers`, which Hugo can generate using custom [output formats](g). - -To support these custom output formats, register a custom media type with no suffix or delimiter: - -{{< code-toggle file=hugo >}} -[mediaTypes."text/netlify"] -delimiter = "" -{{< /code-toggle >}} - -The custom output format definitions would look something like this: - -{{< code-toggle file=hugo >}} -[outputFormats.redir] -baseName = "_redirects" -isPlainText = true -mediatype = "text/netlify" -[outputFormats.headers] -baseName = "_headers" -isPlainText = true -mediatype = "text/netlify" -notAlternative = true -{{< /code-toggle >}} - -[Netlify]: https://www.netlify.com/ diff --git a/docs/content/en/configuration/menus.md b/docs/content/en/configuration/menus.md deleted file mode 100644 index 759f53ff3..000000000 --- a/docs/content/en/configuration/menus.md +++ /dev/null @@ -1,135 +0,0 @@ ---- -title: Configure menus -linkTitle: Menus -description: Centrally define menu entries for one or more menus. -categories: [] -keywords: [] ---- - -> [!note] -> To understand Hugo's menu system, please refer to the [menus] page. - -There are three ways to define menu entries: - -1. [Automatically] -1. [In front matter] -1. In site configuration - -This page covers the site configuration method. - -## Example - -To define entries for a "main" menu: - -{{< code-toggle file=hugo >}} -[[menus.main]] -name = 'Home' -pageRef = '/' -weight = 10 - -[[menus.main]] -name = 'Products' -pageRef = '/products' -weight = 20 - -[[menus.main]] -name = 'Services' -pageRef = '/services' -weight = 30 -{{< /code-toggle >}} - -This creates a menu structure that you can access with [`Menus`] method on a `Site` object: - -```go-html-template -{{ range .Site.Menus.main }} - ... -{{ end }} -``` - -See [menu templates] for a detailed example. - -To define entries for a "footer" menu: - -{{< code-toggle file=hugo >}} -[[menus.footer]] -name = 'Terms' -pageRef = '/terms' -weight = 10 - -[[menus.footer]] -name = 'Privacy' -pageRef = '/privacy' -weight = 20 -{{< /code-toggle >}} - -Access this menu structure in the same way: - -```go-html-template -{{ range .Site.Menus.footer }} - ... -{{ end }} -``` - -## Properties - -Menu entries usually include at least three properties: `name`, `weight`, and either `pageRef` or `url`. Use `pageRef` for internal page destinations and `url` for external destinations. - -These are the available menu entry properties: - -{{% include "/_common/menu-entry-properties.md" %}} - -pageRef -: (`string`) The [logical path](g) of the target page. For example: - - page kind|pageRef - :--|:-- - home|`/` - page|`/books/book-1` - section|`/books` - taxonomy|`/tags` - term|`/tags/foo` - -url -: (`string`) The destination URL. Use this for external destinations only. - -## Nested menu - -This nested menu demonstrates some of the available properties: - -{{< code-toggle file=hugo >}} -[[menus.main]] -name = 'Products' -pageRef = '/products' -weight = 10 - -[[menus.main]] -name = 'Hardware' -pageRef = '/products/hardware' -parent = 'Products' -weight = 1 - -[[menus.main]] -name = 'Software' -pageRef = '/products/software' -parent = 'Products' -weight = 2 - -[[menus.main]] -name = 'Services' -pageRef = '/services' -weight = 20 - -[[menus.main]] -name = 'Hugo' -pre = '' -url = 'https://gohugo.io/' -weight = 30 -[menus.main.params] -rel = 'external' -{{< /code-toggle >}} - -[`Menus`]: /methods/site/menus/ -[Automatically]: /content-management/menus/#define-automatically -[In front matter]: /content-management/menus/#define-in-front-matter -[menu templates]: /templates/menu/ -[menus]: /content-management/menus/ diff --git a/docs/content/en/configuration/minify.md b/docs/content/en/configuration/minify.md deleted file mode 100644 index a530cb73d..000000000 --- a/docs/content/en/configuration/minify.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Configure minify -linkTitle: Minify -description: Configure minify. -categories: [] -keywords: [] ---- - -This is the default configuration: - -{{< code-toggle config=minify />}} - -See the [tdewolff/minify] project page for details. - -[tdewolff/minify]: https://github.com/tdewolff/minify diff --git a/docs/content/en/configuration/module.md b/docs/content/en/configuration/module.md deleted file mode 100644 index d736b7c6f..000000000 --- a/docs/content/en/configuration/module.md +++ /dev/null @@ -1,179 +0,0 @@ ---- -title: Configure modules -linkTitle: Modules -description: Configure modules. -categories: [] -keywords: [] -aliases: [/hugo-modules/configuration/] ---- - -## Top-level options - -This is the default configuration: - -{{< code-toggle file=hugo >}} -[module] -noProxy = 'none' -noVendor = '' -private = '*.*' -proxy = 'direct' -vendorClosest = false -workspace = 'off' -{{< /code-toggle >}} - -auth -: {{< new-in 0.144.0 />}} -: (`string`) Configures `GOAUTH` when running the Go command for module operations. This is a semicolon-separated list of authentication commands for go-import and HTTPS module mirror interactions. This is useful for private repositories. See `go help goauth` for more information. - -noProxy -: (`string`) A comma-separated list of [glob](g) patterns matching paths that should not use the [configured proxy server](#proxy). - -noVendor -: (`string`) A [glob](g) pattern matching module paths to skip when vendoring. - -private -: (`string`) A comma-separated list of [glob](g) patterns matching paths that should be treated as private. - -proxy -: (`string`) The proxy server to use to download remote modules. Default is `direct`, which means `git clone` and similar. - -replacements -: (`string`) Primarily useful for local module development, a comma-separated list of mappings from module paths to directories. Paths may be absolute or relative to the [`themesDir`]. - - {{< code-toggle file=hugo >}} - [module] - replacements = 'github.com/bep/my-theme -> ../..,github.com/bep/shortcodes -> /some/path' - {{< /code-toggle >}} - -vendorClosest -: (`bool`) Whether to pick the vendored module closest to the module using it. The default behavior is to pick the first. Note that there can still be only one dependency of a given module path, so once it is in use it cannot be redefined. Default is `false`. - -workspace -: (`string`) The Go workspace file to use, either as an absolute path or a path relative to the current working directory. Enabling this activates Go workspace mode and requires Go 1.18 or later. The default is `off`. - -You may also use environment variables to set any of the above. For example: - -```sh -export HUGO_MODULE_PROXY="https://proxy.example.org" -export HUGO_MODULE_REPLACEMENTS="github.com/bep/my-theme -> ../.." -export HUGO_MODULE_WORKSPACE="/my/hugo.work" -``` - -{{% include "/_common/gomodules-info.md" %}} - -## Hugo version - -You can specify a required Hugo version for your module in the `module` section. Users will then receive a warning if their Hugo version is incompatible. - -This is the default configuration: - -{{< code-toggle config=module.hugoVersion />}} - -You can omit any of the settings above. - -extended -: (`bool`) Whether the extended edition of Hugo is required, satisfied by installing either the extended or extended/deploy edition. - -max -: (`string`) The maximum Hugo version supported, for example `0.143.0`. - -min -: (`string`) The minimum Hugo version supported, for example `0.123.0`. - -[`themesDir`]: /configuration/all/#themesdir - -## Imports - -{{< code-toggle file=hugo >}} -[[module.imports]] -disable = false -ignoreConfig = false -ignoreImports = false -path = "github.com/gohugoio/hugoTestModules1_linux/modh1_2_1v" -[[module.imports]] -path = "my-shortcodes" -{{< /code-toggle >}} - -disable -: (`bool`) Whether to disable the module but keep version information in the `go.*` files. Default is `false`. - -ignoreConfig -: (`bool`) Whether to ignore module configuration files, for example, `hugo.toml`. This will also prevent loading of any transitive module dependencies. Default is `false`. - -ignoreImports -: (`bool`) Whether to ignore module imports. Default is `false`. - -noMounts -: (`bool`) Whether to disable directory mounting for this import. Default is `false`. - -noVendor -: (`bool`) Whether to disable vendoring for this import. This setting is restricted to the main project. Default is `false`. - -path -: (`string`) The module path, either a valid Go module path (e.g., `github.com/gohugoio/myShortcodes`) or the directory name if stored in the [`themesDir`]. - -[`themesDir`]: /configuration/all#themesDir - -{{% include "/_common/gomodules-info.md" %}} - -## Mounts - -Before Hugo v0.56.0, custom component paths could only be configured by setting [`archetypeDir`], [`assetDir`], [`contentDir`], [`dataDir`], [`i18nDir`], [`layoutDi`], or [`staticDir`] in the site configuration. Module mounts offer greater flexibility than these legacy settings, but -you cannot use both. - -[`archetypeDir`]: /configuration/all/ -[`assetDir`]: /configuration/all/ -[`contentDir`]: /configuration/all/ -[`dataDir`]: /configuration/all/ -[`i18nDir`]: /configuration/all/ -[`layoutDi`]: /configuration/all/ -[`staticDir`]: /configuration/all/ - -> [!note] -> If you use module mounts do not use the legacy settings. - -### Default mounts - -> [!note] -> Adding a new mount to a target root will cause the existing default mount for that root to be ignored. If you still need the default mount, you must explicitly add it along with the new mount. - -The are the default mounts: - -{{< code-toggle config=module.mounts />}} - -source -: (`string`) The source directory of the mount. For the main project, this can be either project-relative or absolute. For other modules it must be project-relative. - -target -: (`string`) Where the mount will reside within Hugo's virtual file system. It must begin with one of Hugo's component directories: `archetypes`, `assets`, `content`, `data`, `i18n`, `layouts`, or `static`. For example, `content/blog`. - -disableWatch -: {{< new-in 0.128.0 />}} -: (`bool`) Whether to disable watching in watch mode for this mount. Default is `false`. - -lang -: (`string`) The language code, e.g. "en". Relevant for `content` mounts, and `static` mounts when in multihost mode. - -includeFiles -: (`string` or `[]string`) One or more [glob](g) patterns matching files or directories to include. If `excludeFiles` is not set, the files matching `includeFiles` will be the files mounted. - - The glob patterns are matched against file names relative to the source root. Use Unix-style forward slashes (`/`), even on Windows. A single forward slash (`/`) matches the mount root, and double asterisks (`**`) act as a recursive wildcard, matching all directories and files beneath a given point (e.g., `/posts/**.jpg`). The search is case-insensitive. - -excludeFiles -: (`string` or `[]string`) One or more [glob](g) patterns matching files to exclude. - -### Example - -{{< code-toggle file=hugo >}} -[module] -[[module.mounts]] - source="content" - target="content" - excludeFiles="docs/*" -[[module.mounts]] - source="node_modules" - target="assets" -[[module.mounts]] - source="assets" - target="assets" -{{< /code-toggle >}} diff --git a/docs/content/en/configuration/output-formats.md b/docs/content/en/configuration/output-formats.md deleted file mode 100644 index 2627c6df4..000000000 --- a/docs/content/en/configuration/output-formats.md +++ /dev/null @@ -1,209 +0,0 @@ ---- -title: Configure output formats -linkTitle: Output formats -description: Configure output formats. -categories: [] -keywords: [] ---- - -{{% glossary-term "output format" %}} - -You can output a page in as many formats as you want. Define an infinite number of output formats, provided they each resolve to a unique file system path. - -This is the default output format configuration in tabular form: - -{{< datatable - "config" - "outputFormats" - "_key" - "mediaType" - "weight" - "baseName" - "isHTML" - "isPlainText" - "noUgly" - "notAlternative" - "path" - "permalinkable" - "protocol" - "rel" - "root" - "ugly" ->}} - -## Default configuration - -The following is the default configuration that matches the table above: - -{{< code-toggle config=outputFormats />}} - -baseName -: (`string`) The base name of the published file. Default is `index`. - -isHTML -: (`bool`) Whether to classify the output format as HTML. Hugo uses this value to determine when to create alias redirects and when to inject the LiveReload script. Default is `false`. - -isPlainText -: (`bool`) Whether to parse templates for this output format with Go's [text/template] package instead of the [html/template] package. Default is `false`. - -mediaType -: (`string`) The [media type](g) of the published file. This must match one of the [configured media types]. - -notAlternative -: (`bool`) Whether to exclude this output format from the values returned by the [`AlternativeOutputFormats`] method on a `Page` object. Default is `false`. - -noUgly -: (`bool`) Whether to disable ugly URLs for this output format when [`uglyURLs`] are enabled in your site configuration. Default is `false`. - -path -: (`string`) The published file's directory path, relative to the root of the publish directory. If not specified, the file will be published using its content path. - -permalinkable -: (`bool`) Whether to return the rendering output format rather than main output format when invoking the [`Permalink`] and [`RelPermalink`] methods on a `Page` object. See [details](#link-to-output-formats). Enabled by default for the `html` and `amp` output formats. Default is `false`. - -protocol -: (`string`) The protocol (scheme) of the URL for this output format. For example, `https://` or `webcal://`. Default is the scheme of the [`baseURL`] parameter in your site configuration, typically `https://`. - -rel -: (`string`) If provided, you can assign this value to `rel` attributes in `link` elements when iterating over output formats in your templates. Default is `alternate`. - -root -: (`bool`) Whether to publish files to the root of the publish directory. Default is `false`. - -ugly -: (`bool`) Whether to enable uglyURLs for this output format when `uglyURLs` is `false` in your site configuration. Default is `false`. - -weight -: (`int`) When set to a non-zero value, Hugo uses the `weight` as the first criteria when sorting output formats, falling back to the name of the output format. Lighter items float to the top, while heavier items sink to the bottom. Hugo renders output formats sequentially based on the sort order. Default is `0`, except for the `html` output format, which has a default weight of `10`. - -## Modify an output format - -You can modify any of the default output formats. For example, to prioritize `json` rendering over `html` rendering, when both are generated, adjust the [`weight`](#weight): - -{{< code-toggle file=hugo >}} -[outputFormats.json] -weight = 1 -[outputFormats.html] -weight = 2 -{{< /code-toggle >}} - -The example above shows that when you modify a default content format, you only need to define the properties that differ from their default values. - -## Create an output format - -You can create new output formats as needed. For example, you may wish to create an output format to support Atom feeds. - -### Step 1 - -Output formats require a specified media type. Because Atom feeds use `application/atom+xml`, which is not one of the [default media types], you must create it first. - -{{< code-toggle file=hugo >}} -[mediaTypes.'application/atom+xml'] -suffixes = ['atom'] -{{< /code-toggle >}} - -See [configure media types] for more information. - -### Step 2 - -Create a new output format: - -{{< code-toggle file=hugo >}} -[outputFormats.atom] -mediaType = 'application/atom+xml' -noUgly = true -{{< /code-toggle >}} - -Note that we use the default settings for all other output format properties. - -### Step 3 - -Specify the page [kinds](g) for which to render this output format: - -{{< code-toggle file=hugo >}} -[outputs] -home = ['html', 'rss', 'atom'] -section = ['html', 'rss', 'atom'] -taxonomy = ['html', 'rss', 'atom'] -term = ['html', 'rss', 'atom'] -{{< /code-toggle >}} - -See [configure outputs] for more information. - -### Step 4 - -Create a template to render the output format. Since Atom feeds are lists, you need to create a list template. Consult the [template lookup order] to find the correct template path: - -```text -layouts/_default/list.atom.atom -``` - -We leave writing the template code as an exercise for you. Aim for a result similar to the [embedded RSS template]. - -## List output formats - -To access output formats, each `Page` object provides two methods: [`OutputFormats`] (for all formats, including the current one) and [`AlternativeOutputFormats`]. Use `AlternativeOutputFormats` to create a link `rel` list within your site's `head` element, as shown below: - -```go-html-template -{{ range .AlternativeOutputFormats }} - -{{ end }} -``` - -## Link to output formats - -By default, a `Page` object's [`Permalink`] and [`RelPermalink`] methods return the URL of the [primary output format](g), typically `html`. This behavior remains consistent regardless of the template used. - -For example, in `single.json.json`, you'll see: - -```go-html-template -{{ .RelPermalink }} → /that-page/ -{{ with .OutputFormats.Get "json" }} - {{ .RelPermalink }} → /that-page/index.json -{{ end }} -``` - -To make these methods return the URL of the _current_ template's output format, you must set the [`permalinkable`] setting to `true` for that format. - -With `permalinkable` set to true for `json` in the same `single.json.json` template: - -```go-html-template -{{ .RelPermalink }} → /that-page/index.json -{{ with .OutputFormats.Get "html" }} - {{ .RelPermalink }} → /that-page/ -{{ end }} -``` - -## Template lookup order - -Each output format requires a template conforming to the [template lookup order]. - -For the highest specificity in the template lookup order, include the page kind, output format, and suffix in the file name: - -```text -[page kind].[output format].[suffix] -``` - -For example, for section pages: - -Output format|Template path -:--|:-- -`html`|`layouts/_default/section.html.html` -`json`|`layouts/_default/section.json.json` -`rss`|`layouts/_default/section.rss.xml` - -[`AlternativeOutputFormats`]: /methods/page/alternativeoutputformats/ -[`OutputFormats`]: /methods/page/outputformats/ -[`Permalink`]: /methods/page/permalink/ -[`RelPermalink`]: /methods/page/relpermalink/ -[`baseURL`]: /configuration/all/#baseurl -[`permalinkable`]: #permalinkable -[`uglyURLs`]: /configuration/ugly-urls/ -[configure media types]: /configuration/media-types/ -[configure outputs]: /configuration/outputs/ -[configured media types]: /configuration/media-types/ -[default media types]: /configuration/media-types/ -[embedded RSS template]: {{% eturl rss %}} -[html/template]: https://pkg.go.dev/html/template -[template lookup order]: /templates/lookup-order/ -[text/template]: https://pkg.go.dev/text/template diff --git a/docs/content/en/configuration/outputs.md b/docs/content/en/configuration/outputs.md deleted file mode 100644 index 9a83cb6e9..000000000 --- a/docs/content/en/configuration/outputs.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: Configure outputs -linkTitle: Outputs -description: Configure which output formats to render for each page kind. -categories: [] -keywords: [] ---- - -{{% glossary-term "output format" %}} - -Learn more about creating and configuring output formats in the [configure output formats] section. - -## Outputs per page kind - -The following default configuration determines the output formats generated for each page kind: - -{{< code-toggle config=outputs />}} - -To render the built-in `json` output format for the `home` page kind, assuming you've already created the necessary template, add the following to your configuration: - -{{< code-toggle file=hugo >}} -[outputs] -home = ['html','rss','json'] -{{< /code-toggle >}} - -Notice in this example that we only specified the `home` page kind. You don't need to include entries for other page kinds unless you intend to modify their default output formats. - -> [!note] -> The order of the output formats in the arrays above is important. The first element will be the _primary output format_ for that page kind, and in most cases that should be `html` as shown in the default configuration. -> -> The primary output format for a given page kind determines the value returned by the [`Permalink`] and [`RelPermalink`] methods on a `Page` object. -> -> See the [link to output formats] section for details. - -## Outputs per page - -Add output formats to a page's rendering using the `outputs` field in its front matter. For example, to include `json` in the output formats rendered for a specific page: - -{{< code-toggle file=content/example.md fm=true >}} -title = 'Example' -outputs = ['json'] -{{< /code-toggle >}} - -In its default configuration, Hugo will render both the `html` and `json` output formats for this page. The `outputs` field appends to, rather than replaces, the site's configured outputs. - -[`Permalink`]: /methods/page/permalink/ -[`RelPermalink`]: /methods/page/relpermalink/ -[configure output formats]: /configuration/output-formats/ -[link to output formats]: configuration/output-formats/#link-to-output-formats diff --git a/docs/content/en/configuration/page.md b/docs/content/en/configuration/page.md deleted file mode 100644 index 81169e546..000000000 --- a/docs/content/en/configuration/page.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: Configure page -linkTitle: Page -description: Configure page behavior. -categories: [] -keywords: [] ---- - -{{< new-in 0.133.0 />}} - -{{% glossary-term "default sort order" %}} - -Hugo uses the default sort order to determine the _next_ and _previous_ page relative to the current page when calling these methods on a `Page` object: - -- [`Next`](/methods/page/next/) and [`Prev`](/methods/page/prev/) -- [`NextInSection`](/methods/page/nextinsection/) and [`PrevInSection`](/methods/page/previnsection/) - -This is based on this default site configuration: - -{{< code-toggle config=page />}} - -To reverse the meaning of _next_ and _previous_: - -{{< code-toggle file=hugo >}} -[page] - nextPrevInSectionSortOrder = 'asc' - nextPrevSortOrder = 'asc' -{{< /code-toggle >}} - -> [!note] -> These settings do not apply to the [`Next`] or [`Prev`] methods on a `Pages` object. - -[`Next`]: /methods/pages/next -[`Prev`]: /methods/pages/next diff --git a/docs/content/en/configuration/pagination.md b/docs/content/en/configuration/pagination.md deleted file mode 100644 index 66b3b8cf4..000000000 --- a/docs/content/en/configuration/pagination.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: Configure pagination -linkTitle: Pagination -description: Configure pagination. -categories: [] -keywords: [] ---- - -This is the default configuration: - -{{< code-toggle config=pagination />}} - -disableAliases -: (`bool`) Whether to disable alias generation for the first pager. Default is `false`. - -pagerSize -: (`int`) The number of pages per pager. Default is `10`. - -path -: (`string`) The segment of each pager URL indicating that the target page is a pager. Default is `page`. - -With multilingual sites you can define the pagination behavior for each language: - -{{< code-toggle file=hugo >}} -[languages.en] -contentDir = 'content/en' -languageCode = 'en-US' -languageDirection = 'ltr' -languageName = 'English' -weight = 1 -[languages.en.pagination] -disableAliases = true -pagerSize = 10 -path = 'page' -[languages.de] -contentDir = 'content/de' -languageCode = 'de-DE' -languageDirection = 'ltr' -languageName = 'Deutsch' -weight = 2 -[languages.de.pagination] -disableAliases = true -pagerSize = 20 -path = 'blatt' -{{< /code-toggle >}} diff --git a/docs/content/en/configuration/params.md b/docs/content/en/configuration/params.md deleted file mode 100644 index 239b0c2da..000000000 --- a/docs/content/en/configuration/params.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -title: Configure params -linkTitle: Params -description: Create custom site parameters. -categories: [] -keywords: [] ---- - -Use the `params` key for custom parameters: - -{{< code-toggle file=hugo >}} -baseURL = 'https://example.org/' -title = 'Project Documentation' -languageCode = 'en-US' -[params] -subtitle = 'Reference, Tutorials, and Explanations' -[params.contact] -email = 'info@example.org' -phone = '+1 206-555-1212' -{{< /code-toggle >}} - -Access the custom parameters from your templates using the [`Params`] method on a `Site` object: - -[`Params`]: /methods/site/params/ - -```go-html-template -{{ .Site.Params.subtitle }} → Reference, Tutorials, and Explanations -{{ .Site.Params.contact.email }} → info@example.org -``` - -Key names should use camelCase or snake_case. While TOML, YAML, and JSON allow kebab-case keys, they are not valid [identifiers](g) and cannot be used when [chaining](g) identifiers. - -For example, you can do either of these: - -```go-html-template -{{ .Site.params.camelCase.foo }} -{{ .Site.params.snake_case.foo }} -``` - -But you cannot do this: - -```go-html-template -{{ .Site.params.kebab-case.foo }} -``` - -## Multilingual sites - -For multilingual sites, create a `params` key under each language: - -{{< code-toggle file=hugo >}} -baseURL = 'https://example.org/' -defaultContentLanguage = 'en' - -[languages.de] -languageCode = 'de-DE' -languageDirection = 'ltr' -languageName = 'Deutsch' -title = 'Projekt Dokumentation' -weight = 1 - -[languages.de.params] -subtitle = 'Referenz, Tutorials und Erklärungen' - -[languages.de.params.contact] -email = 'info@de.example.org' -phone = '+49 30 1234567' - -[languages.en] -languageCode = 'en-US' -languageDirection = 'ltr' -languageName = 'English' -title = 'Project Documentation' -weight = 2 - -[languages.en.params] -subtitle = 'Reference, Tutorials, and Explanations' - -[languages.en.params.contact] -email = 'info@example.org' -phone = '+1 206-555-1212' -{{< /code-toggle >}} - -## Namespacing - -To prevent naming conflicts, module and theme developers should namespace any custom parameters specific to their module or theme. - -{{< code-toggle file=hugo >}} -[params.modules.myModule.colors] -background = '#efefef' -font = '#222222' -{{< /code-toggle >}} - -To access the module/theme settings: - -```go-html-template -{{ $cfg := .Site.Params.module.mymodule }} - -{{ $cfg.colors.background }} → #efefef -{{ $cfg.colors.font }} → #222222 -``` diff --git a/docs/content/en/configuration/permalinks.md b/docs/content/en/configuration/permalinks.md deleted file mode 100644 index 0810624a6..000000000 --- a/docs/content/en/configuration/permalinks.md +++ /dev/null @@ -1,162 +0,0 @@ ---- -title: Configure permalinks -linkTitle: Permalinks -description: Configure permalinks. -categories: [] -keywords: [] ---- - -This is the default configuration: - -{{< code-toggle config=permalinks />}} - -Define a URL pattern for each top-level section. Each URL pattern can target a given language and/or page kind. - -> [!note] -> The [`url`] front matter field overrides any matching permalink pattern. - -## Monolingual example - -With this content structure: - -```text -content/ -├── posts/ -│ ├── bash-in-slow-motion.md -│ └── tls-in-a-nutshell.md -├── tutorials/ -│ ├── git-for-beginners.md -│ └── javascript-bundling-with-hugo.md -└── _index.md -``` - -Render tutorials under "training", and render the posts under "articles" with a date-base hierarchy: - -{{< code-toggle file=hugo >}} -[permalinks.page] -posts = '/articles/:year/:month/:slug/' -tutorials = '/training/:slug/' -[permalinks.section] -posts = '/articles/' -tutorials = '/training/' -{{< /code-toggle >}} - -The structure of the published site will be: - -```text -public/ -├── articles/ -│ ├── 2023/ -│ │ ├── 04/ -│ │ │ └── bash-in-slow-motion/ -│ │ │ └── index.html -│ │ └── 06/ -│ │ └── tls-in-a-nutshell/ -│ │ └── index.html -│ └── index.html -├── training/ -│ ├── git-for-beginners/ -│ │ └── index.html -│ ├── javascript-bundling-with-hugo/ -│ │ └── index.html -│ └── index.html -└── index.html -``` - -To create a date-based hierarchy for regular pages in the content root: - -{{< code-toggle file=hugo >}} -[permalinks.page] -"/" = "/:year/:month/:slug/" -{{< /code-toggle >}} - -Use the same approach with taxonomy terms. For example, to omit the taxonomy segment of the URL: - -{{< code-toggle file=hugo >}} -[permalinks.term] -'tags' = '/:slug/' -{{< /code-toggle >}} - -## Multilingual example - -Use the `permalinks` configuration as a component of your localization strategy. - -With this content structure: - -```text -content/ -├── en/ -│ ├── books/ -│ │ ├── les-miserables.md -│ │ └── the-hunchback-of-notre-dame.md -│ └── _index.md -└── es/ - ├── books/ - │ ├── les-miserables.md - │ └── the-hunchback-of-notre-dame.md - └── _index.md -``` - -And this site configuration: - -{{< code-toggle file=hugo >}} -defaultContentLanguage = 'en' -defaultContentLanguageInSubdir = true - -[languages.en] -contentDir = 'content/en' -languageCode = 'en-US' -languageDirection = 'ltr' -languageName = 'English' -weight = 1 - -[languages.en.permalinks.page] -books = "/books/:slug/" - -[languages.en.permalinks.section] -books = "/books/" - -[languages.es] -contentDir = 'content/es' -languageCode = 'es-ES' -languageDirection = 'ltr' -languageName = 'Español' -weight = 2 - -[languages.es.permalinks.page] -books = "/libros/:slug/" - -[languages.es.permalinks.section] -books = "/libros/" -{{< /code-toggle >}} - -The structure of the published site will be: - -```text -public/ -├── en/ -│ ├── books/ -│ │ ├── les-miserables/ -│ │ │ └── index.html -│ │ ├── the-hunchback-of-notre-dame/ -│ │ │ └── index.html -│ │ └── index.html -│ └── index.html -├── es/ -│ ├── libros/ -│ │ ├── les-miserables/ -│ │ │ └── index.html -│ │ ├── the-hunchback-of-notre-dame/ -│ │ │ └── index.html -│ │ └── index.html -│ └── index.html -└── index.html -``` - -## Tokens - -Use these tokens when defining a URL pattern. - -{{% include "/_common/permalink-tokens.md" %}} - -[`url`]: /content-management/front-matter/#url diff --git a/docs/content/en/configuration/privacy.md b/docs/content/en/configuration/privacy.md deleted file mode 100644 index c94f2c1c3..000000000 --- a/docs/content/en/configuration/privacy.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: Configure privacy -linkTitle: Privacy -description: Configure your site to help comply with regional privacy regulations. -categories: [] -keywords: [] -aliases: [/about/privacy/] ---- - -## Responsibility - -Site authors are responsible for ensuring compliance with regional privacy regulations, including but not limited to: - -- GDPR (General Data Protection Regulation): Applies to individuals within the European Union and the European Economic Area. -- CCPA (California Consumer Privacy Act): Applies to California residents. -- CPRA (California Privacy Rights Act): Expands upon the CCPA with stronger consumer privacy protections. -- Virginia Consumer Data Protection Act (CDPA): Applies to businesses that collect, process, or sell the personal data of Virginia residents. - -Hugo's privacy settings can assist in compliance efforts. - -## Embedded templates - -Hugo provides [embedded templates](g) to simplify site and content creation. Some of these templates interact with external services. For example, the `youtube` shortcode connects with YouTube's servers to embed videos on your site. - -Some of these templates include settings to enhance privacy. - -## Configuration - -> [!note] -> These settings affect the behavior of some of Hugo's embedded templates. These settings may or may not affect the behavior of templates provided by third parties in their modules or themes. - -These are the default privacy settings for Hugo's embedded templates: - -{{< code-toggle config=privacy />}} - -See each template's documentation for a description of its privacy settings: - -- [Disqus partial](/templates/embedded/#privacy-disqus) -- [Google Analytics partial](/templates/embedded/#privacy-google-analytics) -- [Instagram shortcode](/shortcodes/instagram/#privacy) -- [Vimeo shortcode](/shortcodes/vimeo/#privacy) -- [X shortcode](/shortcodes/x/#privacy) -- [YouTube shortcode](/shortcodes/youtube/#privacy) diff --git a/docs/content/en/configuration/related-content.md b/docs/content/en/configuration/related-content.md deleted file mode 100644 index c6e182fae..000000000 --- a/docs/content/en/configuration/related-content.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -title: Configure related content -linkTitle: Related content -description: Configure related content. -categories: [] -keywords: [] ---- - -> [!note] -> To understand Hugo's related content identification, please refer to the [related content] page. - -Hugo provides a sensible default configuration for identifying related content, but you can customize it in your site configuration, either globally or per language. - -## Default configuration - -This is the default configuration: - -{{< code-toggle config=related />}} - -> [!note] -> Adding a `related` section to your site configuration requires you to provide a full configuration. You cannot override individual default values without specifying all related settings. - -## Top-level options - -threshold -: (`int`) A value between 0-100, inclusive. A lower value will return more, but maybe not so relevant, matches. - -includeNewer -: (`bool`) Whether 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. Default is `false`. - -toLower -: (`bool`) Whether to transform keywords in both the indexes and the queries to lower case. This may give more accurate results at a slight performance penalty. Default is `false`. - -## Per-index options - -name -: (`string`) The index name. This value maps directly to a page parameter. Hugo supports string values (`author` in the example) and lists (`tags`, `keywords` etc.) and time and date objects. - -type -: (`string`) One of `basic` or `fragments`. Default is `basic`. - -applyFilter -: (`string`) Apply a `type` specific filter to the result of a search. This is currently only used for the `fragments` type. - -weight -: (`int`) 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. Default is `0`. - -cardinalityThreshold -: (`int`) If between 1 and 100, this is a percentage. All keywords that are used in more than this percentage of documents are removed. For example, setting this to `60` will remove all keywords that are used in more than 60% of the documents in the index. If `0`, no keyword is removed from the index. Default is `0`. - -pattern -: (`string`) 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 -: (`bool`) Whether to transform keywords in both the indexes and the queries to lower case. This may give more accurate results at a slight performance penalty. Default is `false`. - -## Example - -Imagine we're building a book review site. Our main content will be book reviews, and we'll use genres and authors as taxonomies. When someone views a book review, we want to show a short list of related reviews based on shared authors and genres. - -Create the content: - -```text -content/ -└── book-reviews/ - ├── book-review-1.md - ├── book-review-2.md - ├── book-review-3.md - ├── book-review-4.md - └── book-review-5.md -``` - -Configure the taxonomies: - -{{< code-toggle file=hugo >}} -[taxonomies] -author = 'authors' -genre = 'genres' -{{< /code-toggle >}} - -Configure the related content identification: - -{{< code-toggle file=hugo >}} -[related] -includeNewer = true -threshold = 80 -toLower = true -[[related.indices]] -name = 'authors' -weight = 2 -[[related.indices]] -name = 'genres' -weight = 1 -{{< /code-toggle >}} - -We've configured the `authors` index with a weight of `2` and the `genres` index with a weight of `1`. This means Hugo prioritizes shared `authors` as twice as significant as shared `genres`. - -Then render a list of 5 related reviews with a partial template like this: - -```go-html-template {file="layouts/partials/related.html" copy=true} -{{ with site.RegularPages.Related . | first 5 }} -

    Related content:

    - -{{ end }} -``` - -[related content]: /content-management/related-content/ diff --git a/docs/content/en/configuration/security.md b/docs/content/en/configuration/security.md deleted file mode 100644 index f950dd233..000000000 --- a/docs/content/en/configuration/security.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: Configure security -linkTitle: Security -description: Configure security. -categories: [] -keywords: [] ---- - -Hugo's built-in security policy, which restricts access to `os/exec`, remote communication, and similar operations, is configured via allow lists. By default, access is restricted. If a build attempts to use a feature not included in the allow list, it will fail, providing a detailed message. - -This is the default security configuration: - -{{< code-toggle config=security />}} - -enableInlineShortcodes -: (`bool`) Whether to enable [inline shortcodes]. Default is `false`. - -exec.allow -: (`[]string`) A slice of [regular expressions](g) matching the names of external executables that Hugo is allowed to run. - -exec.osEnv -: (`[]string`) A slice of [regular expressions](g) matching the names of operating system environment variables that Hugo is allowed to access. - -funcs.getenv -: (`[]string`) A slice of [regular expressions](g) matching the names of operating system environment variables that Hugo is allowed to access with the [`os.Getenv`] function. - -http.methods -: (`[]string`) A slice of [regular expressions](g) matching the HTTP methods that the [`resources.GetRemote`] function is allowed to use. - -http.mediaTypes -: (`[]string`) Applicable to the `resources.GetRemote` function, a slice of [regular expressions](g) matching the `Content-Type` in HTTP responses that Hugo trusts, bypassing file content analysis for media type detection. - -http.urls -: (`[]string`) A slice of [regular expressions](g) matching the URLs that the `resources.GetRemote` function is allowed to access. - -> [!note] -> Setting an allow list to the string `none` will completely disable the associated feature. - -You can also override the site configuration with environment variables. For example, to block `resources.GetRemote` from accessing any URL: - -```txt -export HUGO_SECURITY_HTTP_URLS=none -``` - -Learn more about [using environment variables] to configure your site. - -[`os.Getenv`]: /functions/os/getenv -[`resources.GetRemote`]: /functions/resources/getremote -[inline shortcodes]: /content-management/shortcodes/#inline -[using environment variables]: /configuration/introduction/#environment-variables diff --git a/docs/content/en/configuration/segments.md b/docs/content/en/configuration/segments.md deleted file mode 100644 index 0c4098770..000000000 --- a/docs/content/en/configuration/segments.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -title: Configure segments -linkTitle: Segments -description: Configure your site for segmented rendering. -categories: [] -keywords: [] ---- - -{{< new-in 0.124.0 />}} - -> [!note] -> The `segments` configuration applies only to segmented rendering. While it controls when content is rendered, it doesn't restrict access to Hugo's complete object graph (sites and pages), which remains fully available. - -Segmented rendering offers several advantages: - -- Faster builds: Process large sites more efficiently. -- Rapid development: Render only a subset of your site for quicker iteration. -- Scheduled rebuilds: Rebuild specific sections at different frequencies (e.g., home page and news hourly, full site weekly). -- Targeted output: Generate specific output formats (like JSON for search indexes). - -## Segment definition - -Each segment is defined by include and exclude filters: - -- Filters: Each segment has zero or more exclude filters and zero or more include filters. -- Matchers: Each filter contains one or more field [glob](g) matchers. -- Logic: Matchers within a filter use AND logic. Filters within a section (include or exclude) use OR logic. - -## Filter fields - -Available fields for filtering: - -kind -: (`string`) A [glob](g) pattern matching the [page kind](g). For example: ` {taxonomy,term}`. - -lang -: (`string`) A [glob](g) pattern matching the [page language]. For example: `{en,de}`. - -output -: (`string`) A [glob](g) pattern matching the [output format](g) of the page. For example: `{html,json}`. - -path -: (`string`) A [glob](g) pattern matching the page's [logical path](g). For example: `{/books,/books/**}`. - -## Example - -Place broad filters, such as those for language or output format, in the excludes section. For example: - -{{< code-toggle file=hugo >}} -[segments.segment1] - [[segments.segment1.excludes]] - lang = "n*" - [[segments.segment1.excludes]] - lang = "en" - output = "rss" - [[segments.segment1.includes]] - kind = "{home,term,taxonomy}" - [[segments.segment1.includes]] - path = "{/docs,/docs/**}" -{{< /code-toggle >}} - -## Rendering segments - -Render specific segments using the [`renderSegments`] configuration or the `--renderSegments` flag: - -```bash -hugo --renderSegments segment1 -``` - -You can configure multiple segments and use a comma-separated list with `--renderSegments` to render them all. - -```bash -hugo --renderSegments segment1,segment2 -``` - -[`renderSegments`]: /configuration/all/#rendersegments -[page language]: /methods/page/language/ diff --git a/docs/content/en/configuration/server.md b/docs/content/en/configuration/server.md deleted file mode 100644 index 92f0f0cfa..000000000 --- a/docs/content/en/configuration/server.md +++ /dev/null @@ -1,128 +0,0 @@ ---- -title: Configure server -linkTitle: Server -description: Configure the development server. -categories: [] -keywords: [] ---- - -These settings are exclusive to Hugo's development server, so a dedicated [configuration directory] for development, where the server is configured accordingly, is the recommended approach. - -[configuration directory]: /configuration/introduction/#configuration-directory - -```text -project/ -└── config/ - ├── _default/ - │ └── hugo.toml - └── development/ - └── server.toml -``` - -## Default settings - -The development server defaults to redirecting to `/404.html` for any requests to URLs that don't exist. See the [404 errors](#404-errors) section below for details. - -{{< code-toggle config=server />}} - -force -: (`bool`) Whether to force a redirect even if there is existing content in the path. - -from -: (`string`) A [glob](g) pattern matching the requested URL. Either `from` or `fromRE` must be set. If both `from` and `fromRe` are specified, the URL must match both patterns. - -fromHeaders -: {{< new-in 0.144.0 />}} -: (`map[string][string]`) Headers to match for the redirect. This maps the HTTP header name to a [glob](g) pattern with values to match. If the map is empty, the redirect will always be triggered. - -fromRe -: {{< new-in 0.144.0 />}} -: (`string`) A [regular expression](g) used to match the requested URL. Either `from` or `fromRE` must be set. If both `from` and `fromRe` are specified, the URL must match both patterns. Capture groups from the regular expression are accessible in the `to` field as `$1`, `$2`, and so on. - -status -: (`string`) The HTTP status code to use for the redirect. A status code of 200 will trigger a URL rewrite. - -to -: (`string`) The URL to forward the request to. - -## Headers - -Include headers in every server response to facilitate testing, particularly for features like Content Security Policies. - -[Content Security Policies]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP - -{{< code-toggle file=config/development/server >}} -[[headers]] -for = "/**" - -[headers.values] -X-Frame-Options = "DENY" -X-XSS-Protection = "1; mode=block" -X-Content-Type-Options = "nosniff" -Referrer-Policy = "strict-origin-when-cross-origin" -Content-Security-Policy = "script-src localhost:1313" -{{< /code-toggle >}} - -## Redirects - -You can define simple redirect rules. - -{{< code-toggle file=config/development/server >}} -[[redirects]] -from = "/myspa/**" -to = "/myspa/" -status = 200 -force = false -{{< /code-toggle >}} - -The `200` status code in this example triggers a URL rewrite, which is typically the desired behavior for [single-page applications]. - -[single-page applications]: https://en.wikipedia.org/wiki/Single-page_application - -## 404 errors - -The development server defaults to redirecting to /404.html for any requests to URLs that don't exist. - -{{< code-toggle config=server />}} - -If you've already defined other redirects, you must explicitly add the 404 redirect. - -{{< code-toggle file=config/development/server >}} -[[redirects]] -force = false -from = "/**" -to = "/404.html" -status = 404 -{{< /code-toggle >}} - -For multilingual sites, ensure the default language 404 redirect is defined last: - -{{< code-toggle file=config/development/server >}} -defaultContentLanguage = 'en' -defaultContentLanguageInSubdir = false -[[redirects]] -from = '/fr/**' -to = '/fr/404.html' -status = 404 - -[[redirects]] # Default language must be last. -from = '/**' -to = '/404.html' -status = 404 -{{< /code-toggle >}} - -When the default language is served from a subdirectory: - -{{< code-toggle file=config/development/server >}} -defaultContentLanguage = 'en' -defaultContentLanguageInSubdir = true -[[redirects]] -from = '/fr/**' -to = '/fr/404.html' -status = 404 - -[[redirects]] # Default language must be last. -from = '/**' -to = '/en/404.html' -status = 404 -{{< /code-toggle >}} diff --git a/docs/content/en/configuration/services.md b/docs/content/en/configuration/services.md deleted file mode 100644 index dbe3893a7..000000000 --- a/docs/content/en/configuration/services.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -title: Configure services -linkTitle: Services -description: Configure embedded templates. -categories: [] -keywords: [] ---- - -Hugo provides [embedded templates](g) to simplify site and content creation. Some of these templates are configurable. For example, the embedded Google Analytics template requires a Google tag ID. - -This is the default configuration: - -{{< code-toggle config=services />}} - -disqus.shortname -: (`string`) The `shortname` used with the Disqus commenting system. See [details](/templates/embedded/#disqus). To access this value from a template: - - ```go-html-template - {{ .Site.Config.Services.Disqus.Shortname }} - ``` - -googleAnalytics.id -: (`string`) The Google tag ID for Google Analytics 4 properties. See [details](/templates/embedded/#google-analytics). To access this value from a template: - - ```go-html-template - {{ .Site.Config.Services.GoogleAnalytics.ID }} - ``` - -instagram.accessToken -: (`string`) Do not use. Deprecated in [v0.123.0]. The embedded `instagram` shortcode no longer uses this setting. - -instagram.disableInlineCSS -: (`bool`) Do not use. Deprecated in [v0.123.0]. The embedded `instagram` shortcode no longer uses this setting. - -rss.limit -: (`int`) The maximum number of items to include in an RSS feed. Set to `-1` for no limit. Default is `-1`. See [details](/templates/rss/). To access this value from a template: - - ```go-html-template - {{ .Site.Config.Services.RSS.Limit }} - ``` - -twitter.disableInlineCSS -: (`bool`) Do not use. Deprecated in [v0.141.0]. Use the `x` shortcode instead. - -x.disableInlineCSS -: (`bool`) Whether to disable the inline CSS rendered by the embedded `x` shortode. See [details](/shortcodes/x/#privacy). Default is `false`. To access this value from a template: - - ```go-html-template - {{ .Site.Config.Services.X.DisableInlineCSS }} - -[v0.141.0]: https://github.com/gohugoio/hugo/releases/tag/v0.141.0 -[v0.123.0]: https://github.com/gohugoio/hugo/releases/tag/v0.123.0 diff --git a/docs/content/en/configuration/sitemap.md b/docs/content/en/configuration/sitemap.md deleted file mode 100644 index bc972994c..000000000 --- a/docs/content/en/configuration/sitemap.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: Configure sitemap -linkTitle: Sitemap -description: Configure the sitemap. -categories: [] -keywords: [] ---- - -These are the default sitemap configuration values. They apply to all pages unless overridden in front matter. - -{{< code-toggle config=sitemap />}} - -changefreq -: (`string`) How frequently a page is likely to change. Valid values are `always`, `hourly`, `daily`, `weekly`, `monthly`, `yearly`, and `never`. With the default value of `""` Hugo will omit this field from the sitemap. See [details](https://www.sitemaps.org/protocol.html#changefreqdef). - -disable -: {{< new-in 0.125.0 />}} -: (`bool`) Whether to disable page inclusion. Default is `false`. Set to `true` in front matter to exclude the page. - -filename -: (`string`) The name of the generated file. Default is `sitemap.xml`. - -priority -: (`float`) The priority of a page relative to any other page on the site. Valid values range from 0.0 to 1.0. With the default value of `-1` Hugo will omit this field from the sitemap. See [details](https://www.sitemaps.org/protocol.html#prioritydef). diff --git a/docs/content/en/configuration/taxonomies.md b/docs/content/en/configuration/taxonomies.md deleted file mode 100644 index 4b5ba97a5..000000000 --- a/docs/content/en/configuration/taxonomies.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: Configure taxonomies -linkTitle: Taxonomies -description: Configure taxonomies. -categories: [] -keywords: [] ---- - -The default configuration defines two [taxonomies](g), `categories` and `tags`. - -{{< code-toggle config=taxonomies />}} - -When creating a taxonomy: - -- Use the singular form for the key (e.g., `category`). -- Use the plural form for the value (e.g., `categories`). - -Then use the value as the key in front matter: - -{{< code-toggle file=content/example.md fm=true >}} ---- -title: Example -categories: - - vegetarian - - gluten-free -tags: - - appetizer - - main course -{{< /code-toggle >}} - -If you do not expect to assign more than one [term](g) from a given taxonomy to a content page, you may use the singular form for both key and value: - -{{< code-toggle file=hugo >}} -taxonomies: - author: author -{{< /code-toggle >}} - -Then in front matter: - -{{< code-toggle file=content/example.md fm=true >}} ---- -title: Example -author: - - Robert Smith -{{< /code-toggle >}} - -The example above illustrates that even with a single term, the value is still provided as an array. - -You must explicitly define the default taxonomies to maintain them when adding a new one: - -{{< code-toggle file=hugo >}} -taxonomies: - author: author - category: categories - tag: tags -{{< /code-toggle >}} - -To disable the taxonomy system, use the [`disableKinds`] setting in the root of your site configuration to disable the `taxonomy` and `term` page [kinds](g). - -{{< code-toggle file=hugo >}} -disableKinds = ['categories','tags'] -{{< /code-toggle >}} - -[`disableKinds`]: /configuration/all/#disablekinds - -See the [taxonomies] section for more information. - -[taxonomies]: /content-management/taxonomies/ diff --git a/docs/content/en/configuration/ugly-urls.md b/docs/content/en/configuration/ugly-urls.md deleted file mode 100644 index ec1dd8a49..000000000 --- a/docs/content/en/configuration/ugly-urls.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: Configure ugly URLs -linkTitle: Ugly URLs -description: Configure ugly URLs. -categories: [] -keywords: [] ---- - -{{% glossary-term "ugly url" %}} For example: - -```text -https://example.org/section/article.html -``` - -In its default configuration, Hugo generates [pretty URLs](g). For example: -```text -https://example.org/section/article/ -``` - -This is the default configuration: - -{{< code-toggle config=uglyURLs />}} - -To generate ugly URLs for the entire site: - -{{< code-toggle file=hugo >}} -uglyURLs = true -{{< /code-toggle >}} - -To generate ugly URLs for specific sections of your site: - -{{< code-toggle file=hugo >}} -[uglyURLs] -books = true -films = false -{{< /code-toggle >}} diff --git a/docs/content/en/content-management/_index.md b/docs/content/en/content-management/_index.md deleted file mode 100644 index 4e2060756..000000000 --- a/docs/content/en/content-management/_index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Content management -description: Hugo makes managing large static sites easy with support for archetypes, content types, menus, cross references, summaries, and more. -categories: [] -keywords: [] -weight: 10 -aliases: [/content/,/content/organization] ---- diff --git a/docs/content/en/content-management/archetypes.md b/docs/content/en/content-management/archetypes.md deleted file mode 100644 index db0838504..000000000 --- a/docs/content/en/content-management/archetypes.md +++ /dev/null @@ -1,186 +0,0 @@ ---- -title: Archetypes -description: An archetype is a template for new content. -categories: [] -keywords: [] -aliases: [/content/archetypes/] ---- - -## Overview - -A content file consists of [front matter](g) and markup. The markup is typically Markdown, but Hugo also supports other [content formats](g). Front matter can be TOML, YAML, or JSON. - -The `hugo new content` command creates a new file in the `content` directory, using an archetype as a template. This is the default archetype: - -{{< code-toggle file=archetypes/default.md fm=true >}} -title = '{{ replace .File.ContentBaseName `-` ` ` | title }}' -date = '{{ .Date }}' -draft = true -{{< /code-toggle >}} - -When you create new content, Hugo evaluates the [template actions](g) within the archetype. For example: - -```sh -hugo new content posts/my-first-post.md -``` - -With the default archetype shown above, Hugo creates this content file: - -{{< code-toggle file=content/posts/my-first-post.md fm=true >}} -title = 'My First Post' -date = '2023-08-24T11:49:46-07:00' -draft = true -{{< /code-toggle >}} - -You can create an archetype for one or more [content types](g). For example, use one archetype for posts, and use the default archetype for everything else: - -```text -archetypes/ -├── default.md -└── posts.md -``` - -## Lookup order - -Hugo looks for archetypes in the `archetypes` directory in the root of your project, falling back to the `archetypes` directory in themes or installed modules. An archetype for a specific content type takes precedence over the default archetype. - -For example, with this command: - -```sh -hugo new content posts/my-first-post.md -``` - -The archetype lookup order is: - -1. `archetypes/posts.md` -1. `archetypes/default.md` -1. `themes/my-theme/archetypes/posts.md` -1. `themes/my-theme/archetypes/default.md` - -If none of these exists, Hugo uses a built-in default archetype. - -## Functions and context - -You can use any template [function](g) within an archetype. As shown above, the default archetype uses the [`replace`](/functions/strings/replace) function to replace hyphens with spaces when populating the title in front matter. - -Archetypes receive the following [context](g): - -Date -: (`string`) The current date and time, formatted in compliance with RFC3339. - -File -: (`hugolib.fileInfo`) Returns file information for the current page. See [details](/methods/page/file). - -Type -: (`string`) The [content type](g) inferred from the top-level directory name, or as specified by the `--kind` flag passed to the `hugo new content` command. - -Site -: (`page.Site`) The current site object. See [details](/methods/site/). - -## Date format - -To insert date and time with a different format, use the [`time.Now`] function: - -[`time.Now`]: /functions/time/now/ - -{{< code-toggle file=archetypes/default.md fm=true >}} -title = '{{ replace .File.ContentBaseName `-` ` ` | title }}' -date = '{{ time.Now.Format "2006-01-02" }}' -draft = true -{{< /code-toggle >}} - -## Include content - -Although typically used as a front matter template, you can also use an archetype to populate content. - -For example, in a documentation site you might have a section (content type) for functions. Every page within this section should follow the same format: a brief description, the function signature, examples, and notes. We can pre-populate the page to remind content authors of the standard format. - -````text {file="archetypes/functions.md"} ---- -date: '{{ .Date }}' -draft: true -title: '{{ replace .File.ContentBaseName `-` ` ` | title }}' ---- - -A brief description of what the function does, using simple present tense in the third person singular form. For example: - -`someFunction` returns the string `s` repeated `n` times. - -## Signature - -```text -func someFunction(s string, n int) string -``` - -## Examples - -One or more practical examples, each within a fenced code block. - -## Notes - -Additional information to clarify as needed. -```` - -Although you can include [template actions](g) within the content body, remember that Hugo evaluates these once---at the time of content creation. In most cases, place template actions in a [template](g) where Hugo evaluates the actions every time you [build](g) the site. - -## Leaf bundles - -You can also create archetypes for [leaf bundles](g). - -For example, in a photography site you might have a section (content type) for galleries. Each gallery is leaf bundle with content and images. - -Create an archetype for galleries: - -```text -archetypes/ -├── galleries/ -│ ├── images/ -│ │ └── .gitkeep -│ └── index.md <-- same format as default.md -└── default.md -``` - -Subdirectories within an archetype must contain at least one file. Without a file, Hugo will not create the subdirectory when you create new content. The name and size of the file are irrelevant. The example above includes a `.gitkeep` file, an empty file commonly used to preserve otherwise empty directories in a Git repository. - -To create a new gallery: - -```sh -hugo new galleries/bryce-canyon -``` - -This produces: - -```text -content/ -├── galleries/ -│ └── bryce-canyon/ -│ ├── images/ -│ │ └── .gitkeep -│ └── index.md -└── _index.md -``` - -## Specify archetype - -Use the `--kind` command line flag to specify an archetype when creating content. - -For example, let's say your site has two sections: articles and tutorials. Create an archetype for each content type: - -```text -archetypes/ -├── articles.md -├── default.md -└── tutorials.md -``` - -To create an article using the articles archetype: - -```sh -hugo new content articles/something.md -``` - -To create an article using the tutorials archetype: - -```sh -hugo new content --kind tutorials articles/something.md -``` diff --git a/docs/content/en/content-management/build-options.md b/docs/content/en/content-management/build-options.md deleted file mode 100644 index 8c29a19b9..000000000 --- a/docs/content/en/content-management/build-options.md +++ /dev/null @@ -1,303 +0,0 @@ ---- -title: Build options -description: Build options help define how Hugo must treat a given page when building the site. -categories: [] -keywords: [] -aliases: [/content/build-options/] ---- - - - -Build options are stored in a reserved front matter object named `build`[^1] with these defaults: - -[^1]: The `_build` alias for `build` is deprecated and will be removed in a future release. - -{{< code-toggle file=content/example/index.md fm=true >}} -[build] -list = 'always' -publishResources = true -render = 'always' -{{< /code-toggle >}} - -list -: When to include the page within page collections. Specify one of: - - - `always`: Include the page in _all_ page collections. For example, `site.RegularPages`, `.Pages`, etc. This is the default value. - - `local`: Include the page in _local_ page collections. For example, `.RegularPages`, `.Pages`, etc. Use this option to create fully navigable but headless content sections. - - `never`: Do not include the page in _any_ page collection. - -publishResources -: Applicable to [page bundles], determines whether to publish the associated [page resources]. Specify one of: - - - `true`: Always publish resources. This is the default value. - - `false`: Only publish a resource when invoking its [`Permalink`], [`RelPermalink`], or [`Publish`] method within a template. - -render -: When to render the page. Specify one of: - - - `always`: Always render the page to disk. This is the default value. - - `link`: Do not render the page to disk, but assign `Permalink` and `RelPermalink` values. - - `never`: Never render the page to disk, and exclude it from all page collections. - -> [!note] -> Any page, regardless of its build options, will always be available by using the [`.Page.GetPage`] or [`.Site.GetPage`] method. - -## Example -- headless page - -Create a unpublished page whose content and resources can be included in other pages. - -```text -content/ -├── headless/ -│ ├── a.jpg -│ ├── b.jpg -│ └── index.md <-- leaf bundle -└── _index.md <-- home page -``` - -Set the build options in front matter: - -{{< code-toggle file=content/headless/index.md fm=true >}} -title = 'Headless page' -[build] - list = 'never' - publishResources = false - render = 'never' -{{< /code-toggle >}} - -To include the content and images on the home page: - -```go-html-template {file="layouts/_default/home.html"} -{{ with .Site.GetPage "/headless" }} - {{ .Content }} - {{ range .Resources.ByType "image" }} - - {{ end }} -{{ end }} -``` - -The published site will have this structure: - -```text -public/ -├── headless/ -│ ├── a.jpg -│ └── b.jpg -└── index.html -``` - -In the example above, note that: - -1. Hugo did not publish an HTML file for the page. -1. Despite setting `publishResources` to `false` in front matter, Hugo published the [page resources] because we invoked the [`RelPermalink`] method on each resource. This is the expected behavior. - -## Example -- headless section - -Create a unpublished section whose content and resources can be included in other pages. - -```text -content/ -├── headless/ -│ ├── note-1/ -│ │ ├── a.jpg -│ │ ├── b.jpg -│ │ └── index.md <-- leaf bundle -│ ├── note-2/ -│ │ ├── c.jpg -│ │ ├── d.jpg -│ │ └── index.md <-- leaf bundle -│ └── _index.md <-- branch bundle -└── _index.md <-- home page -``` - -Set the build options in front matter, using the `cascade` keyword to "cascade" the values down to descendant pages. - -{{< code-toggle file=content/headless/_index.md fm=true >}} -title = 'Headless section' -[[cascade]] -[cascade.build] - list = 'local' - publishResources = false - render = 'never' -{{< /code-toggle >}} - -In the front matter above, note that we have set `list` to `local` to include the descendant pages in local page collections. - -To include the content and images on the home page: - -```go-html-template {file="layouts/_default/home.html"} -{{ with .Site.GetPage "/headless" }} - {{ range .Pages }} - {{ .Content }} - {{ range .Resources.ByType "image" }} - - {{ end }} - {{ end }} -{{ end }} -``` - -The published site will have this structure: - -```text -public/ -├── headless/ -│ ├── note-1/ -│ │ ├── a.jpg -│ │ └── b.jpg -│ └── note-2/ -│ ├── c.jpg -│ └── d.jpg -└── index.html -``` - -In the example above, note that: - -1. Hugo did not publish an HTML file for the page. -1. Despite setting `publishResources` to `false` in front matter, Hugo correctly published the [page resources] because we invoked the [`RelPermalink`] method on each resource. This is the expected behavior. - -## Example -- list without publishing - -Publish a section page without publishing the descendant pages. For example, to create a glossary: - -```text -content/ -├── glossary/ -│ ├── _index.md -│ ├── bar.md -│ ├── baz.md -│ └── foo.md -└── _index.md -``` - -Set the build options in front matter, using the `cascade` keyword to "cascade" the values down to descendant pages. - -{{< code-toggle file=content/glossary/_index.md fm=true >}} -title = 'Glossary' -[build] -render = 'always' -[[cascade]] -[cascade.build] - list = 'local' - publishResources = false - render = 'never' -{{< /code-toggle >}} - -To render the glossary: - -```go-html-template {file="layouts/glossary/list.html"} -
    - {{ range .Pages }} -
    {{ .Title }}
    -
    {{ .Content }}
    - {{ end }} -
    -``` - -The published site will have this structure: - -```text -public/ -├── glossary/ -│ └── index.html -└── index.html -``` - -## Example -- publish without listing - -Publish a section's descendant pages without publishing the section page itself. - -```text -content/ -├── books/ -│ ├── _index.md -│ ├── book-1.md -│ └── book-2.md -└── _index.md -``` - -Set the build options in front matter: - -{{< code-toggle file=content/books/_index.md fm=true >}} -title = 'Books' -[build] -render = 'never' -list = 'never' -{{< /code-toggle >}} - -The published site will have this structure: - -```text -public/ -├── books/ -│ ├── book-1/ -│ │ └── index.html -│ └── book-2/ -│ └── index.html -└── index.html -``` - -## Example -- conditionally hide section - -Consider this example. A documentation site has a team of contributors with access to 20 custom shortcodes. Each shortcode takes several arguments, and requires documentation for the contributors to reference when using them. - -Instead of external documentation for the shortcodes, include an "internal" section that is hidden when building the production site. - -```text -content/ -├── internal/ -│ ├── shortcodes/ -│ │ ├── _index.md -│ │ ├── shortcode-1.md -│ │ └── shortcode-2.md -│ └── _index.md -├── reference/ -│ ├── _index.md -│ ├── reference-1.md -│ └── reference-2.md -├── tutorials/ -│ ├── _index.md -│ ├── tutorial-1.md -│ └── tutorial-2.md -└── _index.md -``` - -Set the build options in front matter, using the `cascade` keyword to "cascade" the values down to descendant pages, and use the `target` keyword to target the production environment. - -{{< code-toggle file=content/internal/_index.md >}} -title = 'Internal' -[[cascade]] -[cascade.build] -render = 'never' -list = 'never' -[cascade.target] -environment = 'production' -{{< /code-toggle >}} - -The production site will have this structure: - -```text -public/ -├── reference/ -│ ├── reference-1/ -│ │ └── index.html -│ ├── reference-2/ -│ │ └── index.html -│ └── index.html -├── tutorials/ -│ ├── tutorial-1/ -│ │ └── index.html -│ ├── tutorial-2/ -│ │ └── index.html -│ └── index.html -└── index.html -``` - -[`.Page.GetPage`]: /methods/page/getpage/ -[`.Site.GetPage`]: /methods/site/getpage/ -[`Permalink`]: /methods/resource/permalink/ -[`Publish`]: /methods/resource/publish/ -[`RelPermalink`]: /methods/resource/relpermalink/ -[page bundles]: /content-management/page-bundles/ -[page resources]: /content-management/page-resources/ diff --git a/docs/content/en/content-management/comments.md b/docs/content/en/content-management/comments.md deleted file mode 100644 index fee4fb372..000000000 --- a/docs/content/en/content-management/comments.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: Comments -description: Hugo ships with an internal Disqus template, but this isn't the only commenting system that will work with your new Hugo website. -categories: [] -keywords: [] -aliases: [/extras/comments/] ---- - -Hugo ships with support for [Disqus](https://disqus.com/), a third-party service that provides comment and community capabilities to websites via JavaScript. - -Your theme may already support Disqus, but if not, it is easy to add to your templates via [Hugo's built-in Disqus partial][disquspartial]. - -## Add Disqus - -Hugo comes with all the code you need to load Disqus into your templates. Before adding Disqus to your site, you'll need to [set up an account][disqussetup]. - -### Configure Disqus - -Disqus comments require you set a single value in your [site's configuration file][configuration] like so: - -{{< code-toggle file=hugo >}} -[services.disqus] -shortname = 'your-disqus-shortname' -{{}} - -For many websites, this is enough configuration. However, you also have the option to set the following in the [front matter] of a single content file: - -- `disqus_identifier` -- `disqus_title` -- `disqus_url` - -### Render Hugo's built-in Disqus partial template - -Disqus has its own [internal template](/templates/embedded/#disqus) available, to render it add the following code where you want comments to appear: - -```go-html-template -{{ template "_internal/disqus.html" . }} -``` - -## Alternatives - -Commercial commenting systems: - -- [Emote](https://emote.com/) -- [Graph Comment](https://graphcomment.com/) -- [Hyvor Talk](https://talk.hyvor.com/) -- [IntenseDebate](https://intensedebate.com/) -- [ReplyBox](https://getreplybox.com/) - -Open-source commenting systems: - -- [Cactus Comments](https://cactus.chat/docs/integrations/hugo/) -- [Comentario](https://gitlab.com/comentario/comentario/) -- [Comma](https://github.com/Dieterbe/comma/) -- [Commento](https://commento.io/) -- [Discourse](https://meta.discourse.org/t/embed-discourse-comments-on-another-website-via-javascript/31963) -- [Giscus](https://giscus.app/) -- [Isso](https://isso-comments.de/) -- [Remark42](https://remark42.com/) -- [Staticman](https://staticman.net/) -- [Talkyard](https://blog-comments.talkyard.io/) -- [Utterances](https://utteranc.es/) - -[configuration]: /configuration/ -[disquspartial]: /templates/embedded/#disqus -[disqussetup]: https://disqus.com/profile/signup/ -[forum]: https://discourse.gohugo.io -[front matter]: /content-management/front-matter/ -[kaijuissue]: https://github.com/spf13/kaiju/issues/new -[issotutorial]: https://stiobhart.net/2017-02-24-isso-comments/ -[partials]: /templates/partial/ -[MongoDB]: https://www.mongodb.com/ diff --git a/docs/content/en/content-management/content-adapters.md b/docs/content/en/content-management/content-adapters.md deleted file mode 100644 index 3468bb728..000000000 --- a/docs/content/en/content-management/content-adapters.md +++ /dev/null @@ -1,349 +0,0 @@ ---- -title: Content adapters -description: Create content adapters to dynamically add content when building your site. -categories: [] -keywords: [] ---- - -{{< new-in 0.126.0 />}} - -## Overview - -A content adapter is a template that dynamically creates pages when building a site. For example, use a content adapter to create pages from a remote data source such as JSON, TOML, YAML, or XML. - -Unlike templates that reside in the `layouts` directory, content adapters reside in the `content` directory, no more than one per directory per language. When a content adapter creates a page, the page's [logical path](g) will be relative to the content adapter. - -```text -content/ -├── articles/ -│ ├── _index.md -│ ├── article-1.md -│ └── article-2.md -├── books/ -│ ├── _content.gotmpl <-- content adapter -│ └── _index.md -└── films/ - ├── _content.gotmpl <-- content adapter - └── _index.md -``` - -Each content adapter is named _content.gotmpl and uses the same [syntax] as templates in the `layouts` directory. You can use any of the [template functions] within a content adapter, as well as the methods described below. - -## Methods - -Use these methods within a content adapter. - -### AddPage - -Adds a page to the site. - -```go-html-template {file="content/books/_content.gotmpl"} -{{ $content := dict - "mediaType" "text/markdown" - "value" "The _Hunchback of Notre Dame_ was written by Victor Hugo." -}} -{{ $page := dict - "content" $content - "kind" "page" - "path" "the-hunchback-of-notre-dame" - "title" "The Hunchback of Notre Dame" -}} -{{ .AddPage $page }} -``` - -### AddResource - -Adds a page resource to the site. - -```go-html-template {file="content/books/_content.gotmpl"} -{{ with resources.Get "images/a.jpg" }} - {{ $content := dict - "mediaType" .MediaType.Type - "value" . - }} - {{ $resource := dict - "content" $content - "path" "the-hunchback-of-notre-dame/cover.jpg" - }} - {{ $.AddResource $resource }} -{{ end }} -``` - -Then retrieve the new page resource with something like: - -```go-html-template {file="layouts/_default/single.html"} -{{ with .Resources.Get "cover.jpg" }} - -{{ end }} -``` - -### Site - -Returns the `Site` to which the pages will be added. - -```go-html-template {file="content/books/_content.gotmpl"} -{{ .Site.Title }} -``` - -> [!note] -> Note that the `Site` returned isn't fully built when invoked from the content adapters; if you try to call methods that depends on pages, e.g. `.Site.Pages`, you will get an error saying "this method cannot be called before the site is fully initialized". - -### Store - -Returns a persistent “scratch pad” to store and manipulate data. The main use case for this is to transfer values between executions when [EnableAllLanguages](#enablealllanguages) is set. See [examples](/methods/page/store/). - -```go-html-template {file="content/books/_content.gotmpl"} -{{ .Store.Set "key" "value" }} -{{ .Store.Get "key" }} -``` - -### EnableAllLanguages - -By default, Hugo executes the content adapter for the language defined by the _content.gotmpl file. Use this method to activate the content adapter for all languages. - -```go-html-template {file="content/books/_content.gotmpl"} -{{ .EnableAllLanguages }} -{{ $content := dict - "mediaType" "text/markdown" - "value" "The _Hunchback of Notre Dame_ was written by Victor Hugo." -}} -{{ $page := dict - "content" $content - "kind" "page" - "path" "the-hunchback-of-notre-dame" - "title" "The Hunchback of Notre Dame" -}} -{{ .AddPage $page }} -``` - -## Page map - -Set any [front matter field] in the map passed to the [`AddPage`](#addpage) method, excluding `markup`. Instead of setting the `markup` field, specify the `content.mediaType` as described below. - -This table describes the fields most commonly passed to the `AddPage` method. - -Key|Description|Required -:--|:--|:-: -`content.mediaType`|The content [media type]. Default is `text/markdown`. See [content formats] for examples.|  -`content.value`|The content value as a string.|  -`dates.date`|The page creation date as a `time.Time` value.|  -`dates.expiryDate`|The page expiry date as a `time.Time` value.|  -`dates.lastmod`|The page last modification date as a `time.Time` value.|  -`dates.publishDate`|The page publication date as a `time.Time` value.|  -`params`|A map of page parameters.|  -`path`|The page's [logical path](g) relative to the content adapter. Do not include a leading slash or file extension.|:heavy_check_mark: -`title`|The page title.|  - -> [!note] -> While `path` is the only required field, we recommend setting `title` as well. -> -> When setting the `path`, Hugo transforms the given string to a logical path. For example, setting `path` to `A B C` produces a logical path of `/section/a-b-c`. - -## Resource map - -Construct the map passed to the [`AddResource`](#addresource) method using the fields below. - -Key|Description|Required -:--|:--|:-: -`content.mediaType`|The content [media type].|:heavy_check_mark: -`content.value`|The content value as a string or resource.|:heavy_check_mark: -`name`|The resource name.|  -`params`|A map of resource parameters.|  -`path`|The resources's [logical path](g) relative to the content adapter. Do not include a leading slash.|:heavy_check_mark: -`title`|The resource title.|  - -> [!note] -> If the `content.value` is a string Hugo creates a new resource. If the `content.value` is a resource, Hugo obtains the value from the existing resource. -> -> When setting the `path`, Hugo transforms the given string to a logical path. For example, setting `path` to `A B C/cover.jpg` produces a logical path of `/section/a-b-c/cover.jpg`. - -## Example - -Create pages from remote data, where each page represents a book review. - -### Step 1 - -Create the content structure. - -```text -content/ -└── books/ - ├── _content.gotmpl <-- content adapter - └── _index.md -``` - -### Step 2 -Inspect the remote data to determine how to map key-value pairs to front matter fields.\ - - -### Step 3 - -Create the content adapter. - -```go-html-template {file="content/books/_content.gotmpl" copy=true} -{{/* Get remote data. */}} -{{ $data := dict }} -{{ $url := "https://gohugo.io/shared/examples/data/books.json" }} -{{ with try (resources.GetRemote $url) }} - {{ with .Err }} - {{ errorf "Unable to get remote resource %s: %s" $url . }} - {{ else with .Value }} - {{ $data = . | transform.Unmarshal }} - {{ else }} - {{ errorf "Unable to get remote resource %s" $url }} - {{ end }} -{{ end }} - -{{/* Add pages and page resources. */}} -{{ range $data }} - - {{/* Add page. */}} - {{ $content := dict "mediaType" "text/markdown" "value" .summary }} - {{ $dates := dict "date" (time.AsTime .date) }} - {{ $params := dict "author" .author "isbn" .isbn "rating" .rating "tags" .tags }} - {{ $page := dict - "content" $content - "dates" $dates - "kind" "page" - "params" $params - "path" .title - "title" .title - }} - {{ $.AddPage $page }} - - {{/* Add page resource. */}} - {{ $item := . }} - {{ with $url := $item.cover }} - {{ with try (resources.GetRemote $url) }} - {{ with .Err }} - {{ errorf "Unable to get remote resource %s: %s" $url . }} - {{ else with .Value }} - {{ $content := dict "mediaType" .MediaType.Type "value" .Content }} - {{ $params := dict "alt" $item.title }} - {{ $resource := dict - "content" $content - "params" $params - "path" (printf "%s/cover.%s" $item.title .MediaType.SubType) - }} - {{ $.AddResource $resource }} - {{ else }} - {{ errorf "Unable to get remote resource %s" $url }} - {{ end }} - {{ end }} - {{ end }} - -{{ end }} -``` - -### Step 4 - -Create a single template to render each book review. - -```go-html-template {file="layouts/books/single.html" copy=true} -{{ define "main" }} -

    {{ .Title }}

    - - {{ with .Resources.GetMatch "cover.*" }} - {{ .Params.alt }} - {{ end }} - -

    Author: {{ .Params.author }}

    - -

    - ISBN: {{ .Params.isbn }}
    - Rating: {{ .Params.rating }}
    - Review date: {{ .Date | time.Format ":date_long" }} -

    - - {{ with .GetTerms "tags" }} -

    Tags:

    - - {{ end }} - - {{ .Content }} -{{ end }} -``` - -## Multilingual sites - -With multilingual sites you can: - -1. Create one content adapter for all languages using the [`EnableAllLanguages`](#enablealllanguages) method as described above. -1. Create content adapters unique to each language. See the examples below. - -### Translations by file name - -With this site configuration: - -{{< code-toggle file=hugo >}} -[languages.en] -weight = 1 - -[languages.de] -weight = 2 -{{< /code-toggle >}} - -Include a language designator in the content adapter's file name. - -```text -content/ -└── books/ - ├── _content.de.gotmpl - ├── _content.en.gotmpl - ├── _index.de.md - └── _index.en.md -``` - -### Translations by content directory - -With this site configuration: - -{{< code-toggle file=hugo >}} -[languages.en] -contentDir = 'content/en' -weight = 1 - -[languages.de] -contentDir = 'content/de' -weight = 2 -{{< /code-toggle >}} - -Create a single content adapter in each directory: - -```text -content/ -├── de/ -│ └── books/ -│ ├── _content.gotmpl -│ └── _index.md -└── en/ - └── books/ - ├── _content.gotmpl - └── _index.md -``` - -## Page collisions - -Two or more pages collide when they have the same publication path. Due to concurrency, the content of the published page is indeterminate. Consider this example: - -```text -content/ -└── books/ - ├── _content.gotmpl <-- content adapter - ├── _index.md - └── the-hunchback-of-notre-dame.md -``` - -If the content adapter also creates books/the-hunchback-of-notre-dame, the content of the published page is indeterminate. You can not define the processing order. - -To detect page collisions, use the `--printPathWarnings` flag when building your site. - -[content formats]: /content-management/formats/#classification -[front matter field]: /content-management/front-matter/#fields -[media type]: https://en.wikipedia.org/wiki/Media_type -[syntax]: /templates/introduction/ -[template functions]: /functions/ diff --git a/docs/content/en/content-management/data-sources.md b/docs/content/en/content-management/data-sources.md deleted file mode 100644 index 3fc98b36a..000000000 --- a/docs/content/en/content-management/data-sources.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -title: Data sources -description: Use local and remote data sources to augment or create content. -categories: [] -keywords: [] -aliases: [/extras/datafiles/,/extras/datadrivencontent/,/doc/datafiles/,/templates/data-templates/] ---- - -Hugo can access and [unmarshal](g) local and remote data sources including CSV, JSON, TOML, YAML, and XML. Use this data to augment existing content or to create new content. - -A data source might be a file in the `data` directory, a [global resource](g), a [page resource](g), or a [remote resource](g). - -## Data directory - -The `data` directory in the root of your project may contain one or more data files, in either a flat or nested tree. Hugo merges the data files to create a single data structure, accessible with the `Data` method on a `Site` object. - -Hugo also merges data directories from themes and modules into this single data structure, where the `data` directory in the root of your project takes precedence. - -> [!note] -> Hugo reads the combined data structure into memory and keeps it there for the entire build. For data that is infrequently accessed, use global or page resources instead. - -Theme and module authors may wish to namespace their data files to prevent collisions. For example: - -```text -project/ -└── data/ - └── mytheme/ - └── foo.json -``` - -> [!note] -> Do not place CSV files in the `data` directory. Access CSV files as page, global, or remote resources. - -See the documentation for the [`Data`] method on a `Site` object for details and examples. - -## Global resources - -Use the `resources.Get` and `transform.Unmarshal` functions to access data files that exist as global resources. - -See the [`transform.Unmarshal`](/functions/transform/unmarshal/#global-resource) documentation for details and examples. - -## Page resources - -Use the `Resources.Get` method on a `Page` object combined with the `transform.Unmarshal` function to access data files that exist as page resources. - -See the [`transform.Unmarshal`](/functions/transform/unmarshal/#page-resource) documentation for details and examples. - -## Remote resources - -Use the `resources.GetRemote` and `transform.Unmarshal` functions to access remote data. - -See the [`transform.Unmarshal`](/functions/transform/unmarshal/#remote-resource) documentation for details and examples. - -## Augment existing content - -Use data sources to augment existing content. For example, create a shortcode to render an HTML table from a global CSV resource. - -```csv {file="assets/pets.csv"} -"name","type","breed","age" -"Spot","dog","Collie","3" -"Felix","cat","Malicious","7" -``` - -```text {file="content/example.md"} -{{}} -``` - -```go-html-template {file="layouts/shortcodes/csv-to-table.html"} -{{ with $file := .Get 0 }} - {{ with resources.Get $file }} - {{ with . | transform.Unmarshal }} - - - - {{ range index . 0 }} - - {{ end }} - - - - {{ range after 1 . }} - - {{ range . }} - - {{ end }} - - {{ end }} - -
    {{ . }}
    {{ . }}
    - {{ end }} - {{ else }} - {{ errorf "The %q shortcode was unable to find %s. See %s" $.Name $file $.Position }} - {{ end }} -{{ else }} - {{ errorf "The %q shortcode requires one positional argument, the path to the CSV file relative to the assets directory. See %s" .Name .Position }} -{{ end }} -``` - -Hugo renders this to: - -name|type|breed|age -:--|:--|:--|:-- -Spot|dog|Collie|3 -Felix|cat|Malicious|7 - -## Create new content - -Use [content adapters] to create new content. - -[`Data`]: /methods/site/data/ -[content adapters]: /content-management/content-adapters/ diff --git a/docs/content/en/content-management/diagrams.md b/docs/content/en/content-management/diagrams.md deleted file mode 100644 index 0070ced59..000000000 --- a/docs/content/en/content-management/diagrams.md +++ /dev/null @@ -1,260 +0,0 @@ ---- -title: Diagrams -description: Use fenced code blocks and Markdown render hooks to include diagrams in your content. -categories: [] -keywords: [] ---- - -## GoAT diagrams (ASCII) - -Hugo natively supports [GoAT] diagrams with an [embedded code block render hook]. This means that this code block: - -````txt -```goat - . . . .--- 1 .-- 1 / 1 - / \ | | .---+ .-+ + - / \ .---+---. .--+--. | '--- 2 | '-- 2 / \ 2 - + + | | | | ---+ ---+ + - / \ / \ .-+-. .-+-. .+. .+. | .--- 3 | .-- 3 \ / 3 - / \ / \ | | | | | | | | '---+ '-+ + - 1 2 3 4 1 2 3 4 1 2 3 4 '--- 4 '-- 4 \ 4 - -``` -```` - -Will be rendered as: - -```goat - - . . . .--- 1 .-- 1 / 1 - / \ | | .---+ .-+ + - / \ .---+---. .--+--. | '--- 2 | '-- 2 / \ 2 - + + | | | | ---+ ---+ + - / \ / \ .-+-. .-+-. .+. .+. | .--- 3 | .-- 3 \ / 3 - / \ / \ | | | | | | | | '---+ '-+ + - 1 2 3 4 1 2 3 4 1 2 3 4 '--- 4 '-- 4 \ 4 -``` - -## Mermaid diagrams - -Hugo does not provide a built-in template for Mermaid diagrams. Create your own using a [code block render hook]: - -```go-html-template {file="layouts/_default/_markup/render-codeblock-mermaid.html" copy=true} -
    -  {{ .Inner | htmlEscape | safeHTML }}
    -
    -{{ .Page.Store.Set "hasMermaid" true }} -``` - -Then include this snippet at the _bottom_ of your base template, before the closing `body` tag: - -```go-html-template {file="layouts/_default/baseof.html" copy=true} -{{ if .Store.Get "hasMermaid" }} - -{{ end }} -``` - -With that you can use the `mermaid` language in Markdown code blocks: - -````text {copy=true} -```mermaid -sequenceDiagram - participant Alice - participant Bob - Alice->>John: Hello John, how are you? - loop Healthcheck - John->>John: Fight against hypochondria - end - Note right of John: Rational thoughts
    prevail! - John-->>Alice: Great! - John->>Bob: How about you? - Bob-->>John: Jolly good! -``` -```` - -## Goat ASCII diagram examples - -### Graphics - -```goat - . - 0 3 P * Eye / ^ / - *-------* +y \ +) \ / Reflection - 1 /| 2 /| ^ \ \ \ v - *-------* | | v0 \ v3 --------*-------- - | |4 | |7 | *----\-----* - | *-----|-* +-----> +x / v X \ .-.<-------- o - |/ |/ / / o \ | / | Refraction / \ - *-------* v / \ +-' / \ - 5 6 +z v1 *------------------* v2 | o-----o - v - -``` - -### Complex - -```goat -+-------------------+ ^ .---. -| A Box |__.--.__ __.--> | .-. | | -| | '--' v | * |<--- | | -+-------------------+ '-' | | - Round *---(-. | - .-----------------. .-------. .----------. .-------. | | | - | Mixed Rounded | | | / Diagonals \ | | | | | | - | & Square Corners | '--. .--' / \ |---+---| '-)-' .--------. - '--+------------+-' .--. | '-------+--------' | | | | / Search / - | | | | '---. | '-------' | '-+------' - |<---------->| | | | v Interior | ^ - ' <---' '----' .-----------. ---. .--- v | - .------------------. Diag line | .-------. +---. \ / . | - | if (a > b) +---. .--->| | | | | Curved line \ / / \ | - | obj->fcn() | \ / | '-------' |<--' + / \ | - '------------------' '--' '--+--------' .--. .--. | .-. +Done?+-' - .---+-----. | ^ |\ | | /| .--+ | | \ / - | | | Join \|/ | | Curved | \| |/ | | \ | \ / - | | +----> o --o-- '-' Vertical '--' '--' '-- '--' + .---. - <--+---+-----' | /|\ | | 3 | - v not:line 'quotes' .-' '---' - .-. .---+--------. / A || B *bold* | ^ - | | | Not a dot | <---+---<-- A dash--is not a line v | - '-' '---------+--' / Nor/is this. --- - -``` - -### Process - -```goat - . - .---------. / \ - | START | / \ .-+-------+-. ___________ - '----+----' .-------. A / \ B | |COMPLEX| | / \ .-. - | | END |<-----+CHOICE +----->| | | +--->+ PREPARATION +--->| X | - v '-------' \ / | |PROCESS| | \___________/ '-' - .---------. \ / '-+---+---+-' - / INPUT / \ / - '-----+---' ' - | ^ - v | - .-----------. .-----+-----. .-. - | PROCESS +---------------->| PROCESS |<------+ X | - '-----------' '-----------' '-' -``` - -### File tree - -Created from - -```goat {width=300 color="orange"} -───Linux─┬─Android - ├─Debian─┬─Ubuntu─┬─Lubuntu - │ │ ├─Kubuntu - │ │ ├─Xubuntu - │ │ └─Xubuntu - │ └─Mint - ├─Centos - └─Fedora -``` - -### Sequence diagram - - - -```goat {class="w-40"} -┌─────┐ ┌───┐ -│Alice│ │Bob│ -└──┬──┘ └─┬─┘ - │ │ - │ Hello Bob! │ - │───────────>│ - │ │ - │Hello Alice!│ - │<───────────│ -┌──┴──┐ ┌─┴─┐ -│Alice│ │Bob│ -└─────┘ └───┘ - -``` - -### Flowchart - - - -```goat - _________________ - ╱ ╲ ┌─────┐ - ╱ DO YOU UNDERSTAND ╲____________________________________________________│GOOD!│ - ╲ FLOW CHARTS? ╱yes └──┬──┘ - ╲_________________╱ │ - │no │ - _________▽_________ ______________________ │ - ╱ ╲ ╱ ╲ ┌────┐ │ -╱ OKAY, YOU SEE THE ╲________________╱ ... AND YOU CAN SEE ╲___│GOOD│ │ -╲ LINE LABELED 'YES'? ╱yes ╲ THE ONES LABELED 'NO'? ╱yes└──┬─┘ │ - ╲___________________╱ ╲______________________╱ │ │ - │no │no │ │ - ________▽_________ _________▽__________ │ │ - ╱ ╲ ┌───────────┐ ╱ ╲ │ │ - ╱ BUT YOU SEE THE ╲___│WAIT, WHAT?│ ╱ BUT YOU JUST ╲___ │ │ - ╲ ONES LABELED 'NO'? ╱yes└───────────┘ ╲ FOLLOWED THEM TWICE? ╱yes│ │ │ - ╲__________________╱ ╲____________________╱ │ │ │ - │no │no │ │ │ - ┌───▽───┐ │ │ │ │ - │LISTEN.│ └───────┬───────┘ │ │ - └───┬───┘ ┌──────▽─────┐ │ │ - ┌─────▽────┐ │(THAT WASN'T│ │ │ - │I HATE YOU│ │A QUESTION) │ │ │ - └──────────┘ └──────┬─────┘ │ │ - ┌────▽───┐ │ │ - │SCREW IT│ │ │ - └────┬───┘ │ │ - └─────┬─────┘ │ - │ │ - └─────┬─────┘ - ┌───────▽──────┐ - │LET'S GO DRING│ - └───────┬──────┘ - ┌─────────▽─────────┐ - │HEY, I SHOULD TRY │ - │INSTALLING FREEBSD!│ - └───────────────────┘ - -``` - -### Table - - - -```goat {class="w-80 dark-blue"} -┌────────────────────────────────────────────────┐ -│ │ -├────────────────────────────────────────────────┤ -│SYNTAX = { PRODUCTION } . │ -├────────────────────────────────────────────────┤ -│PRODUCTION = IDENTIFIER "=" EXPRESSION "." . │ -├────────────────────────────────────────────────┤ -│EXPRESSION = TERM { "|" TERM } . │ -├────────────────────────────────────────────────┤ -│TERM = FACTOR { FACTOR } . │ -├────────────────────────────────────────────────┤ -│FACTOR = IDENTIFIER │ -├────────────────────────────────────────────────┤ -│ | LITERAL │ -├────────────────────────────────────────────────┤ -│ | "[" EXPRESSION "]" │ -├────────────────────────────────────────────────┤ -│ | "(" EXPRESSION ")" │ -├────────────────────────────────────────────────┤ -│ | "{" EXPRESSION "}" . │ -├────────────────────────────────────────────────┤ -│IDENTIFIER = letter { letter } . │ -├────────────────────────────────────────────────┤ -│LITERAL = """" character { character } """" .│ -└────────────────────────────────────────────────┘ -``` - -[code block render hook]: /render-hooks/code-blocks/ -[embedded code block render hook]: {{% eturl render-codeblock-goat %}} -[GoAT]: https://github.com/bep/goat diff --git a/docs/content/en/content-management/formats.md b/docs/content/en/content-management/formats.md deleted file mode 100644 index 1acaae063..000000000 --- a/docs/content/en/content-management/formats.md +++ /dev/null @@ -1,132 +0,0 @@ ---- -title: Content formats -description: Create your content using Markdown, HTML, Emacs Org Mode, AsciiDoc, Pandoc, or reStructuredText. -categories: [] -keywords: [] -aliases: [/content/markdown-extras/,/content/supported-formats/,/doc/supported-formats/] ---- - -## Introduction - -You may mix content formats throughout your site. For example: - -```text -content/ -└── posts/ - ├── post-1.md - ├── post-2.adoc - ├── post-3.org - ├── post-4.pandoc - ├── post-5.rst - └── post-6.html -``` - -Regardless of content format, all content must have [front matter], preferably including both `title` and `date`. - -Hugo selects the content renderer based on the `markup` identifier in front matter, falling back to the file extension. See the [classification] table below for a list of markup identifiers and recognized file extensions. - -[classification]: #classification -[front matter]: /content-management/front-matter/ - -## Formats - -### Markdown - -Create your content in [Markdown] preceded by front matter. - -Markdown is Hugo's default content format. Hugo natively renders Markdown to HTML using [Goldmark]. Goldmark is fast and conforms to the [CommonMark] and [GitHub Flavored Markdown] specifications. You can configure Goldmark in your [site configuration][configure goldmark]. - -Hugo provides custom Markdown features including: - -[Attributes] -: Apply HTML attributes such as `class` and `id` to Markdown images and block elements including blockquotes, fenced code blocks, headings, horizontal rules, lists, paragraphs, and tables. - -[Extensions] -: Leverage the embedded Markdown extensions to create tables, definition lists, footnotes, task lists, inserted text, mark text, subscripts, superscripts, and more. - -[Mathematics] -: Include mathematical equations and expressions in Markdown using LaTeX markup. - -[Render hooks] -: Override the conversion of Markdown to HTML when rendering fenced code blocks, headings, images, and links. For example, render every standalone image as an HTML `figure` element. - -[Attributes]: /content-management/markdown-attributes/ -[CommonMark]: https://spec.commonmark.org/current/ -[Extensions]: /configuration/markup/#extensions -[GitHub Flavored Markdown]: https://github.github.com/gfm/ -[Goldmark]: https://github.com/yuin/goldmark -[Markdown]: https://daringfireball.net/projects/markdown/ -[Mathematics]: /content-management/mathematics/ -[Render hooks]: /render-hooks/introduction/ -[configure goldmark]: /configuration/markup/#goldmark - -### HTML - -Create your content in [HTML] preceded by front matter. The content is typically what you would place within an HTML document's `body` or `main` element. - -[HTML]: https://developer.mozilla.org/en-US/docs/Learn_web_development/Getting_started/Your_first_website/Creating_the_content - -### Emacs Org Mode - -Create your content in the [Emacs Org Mode] format preceded by front matter. You can use Org Mode keywords for front matter. See [details]. - -[details]: /content-management/front-matter/#emacs-org-mode -[Emacs Org Mode]: https://orgmode.org/ - -### AsciiDoc - -Create your content in the [AsciiDoc] format preceded by front matter. Hugo renders AsciiDoc content to HTML using the Asciidoctor executable. You must install Asciidoctor and its dependencies (Ruby) to use the AsciiDoc content format. - -You can configure the AsciiDoc renderer in your [site configuration][configure asciidoc]. - -In its default configuration, Hugo passes these CLI flags when calling the Asciidoctor executable: - -```text ---no-header-footer -``` - -The CLI flags passed to the Asciidoctor executable depend on configuration. You may inspect the flags when building your site: - -```text -hugo --logLevel info -``` - -[AsciiDoc]: https://asciidoc.org/ -[configure the AsciiDoc renderer]: /configuration/markup/#asciidoc -[configure asciidoc]: /configuration/markup/#asciidoc - -### Pandoc - -Create your content in the [Pandoc] format preceded by front matter. Hugo renders Pandoc content to HTML using the Pandoc executable. You must install Pandoc to use the Pandoc content format. - -Hugo passes these CLI flags when calling the Pandoc executable: - -```text ---mathjax -``` - -[Pandoc]: https://pandoc.org/ - -### reStructuredText - -Create your content in the [reStructuredText] format preceded by front matter. Hugo renders reStructuredText content to HTML using [Docutils], specifically rst2html. You must install Docutils and its dependencies (Python) to use the reStructuredText content format. - -Hugo passes these CLI flags when calling the rst2html executable: - -```text ---leave-comments --initial-header-level=2 -``` - -[Docutils]: https://docutils.sourceforge.io/ -[reStructuredText]: https://docutils.sourceforge.io/rst.html - -## Classification - -{{% include "/_common/content-format-table.md" %}} - -When converting content to HTML, Hugo uses: - -- Native renderers for Markdown, HTML, and Emacs Org mode -- External renderers for AsciiDoc, Pandoc, and reStructuredText - -Native renderers are faster than external renderers. diff --git a/docs/content/en/content-management/front-matter.md b/docs/content/en/content-management/front-matter.md deleted file mode 100644 index 8bfbd1acc..000000000 --- a/docs/content/en/content-management/front-matter.md +++ /dev/null @@ -1,362 +0,0 @@ ---- -title: Front matter -description: Use front matter to add metadata to your content. -categories: [] -keywords: [] -aliases: [/content/front-matter/] ---- - -## Overview - -The front matter at the top of each content file is metadata that: - -- Describes the content -- Augments the content -- Establishes relationships with other content -- Controls the published structure of your site -- Determines template selection - -Provide front matter using a serialization format, one of [JSON], [TOML], or [YAML]. Hugo determines the front matter format by examining the delimiters that separate the front matter from the page content. - -[json]: https://www.json.org/ -[toml]: https://toml.io/ -[yaml]: https://yaml.org/ - -See examples of front matter delimiters by toggling between the serialization formats below. - -{{< code-toggle file=content/example.md fm=true >}} -title = 'Example' -date = 2024-02-02T04:14:54-08:00 -draft = false -weight = 10 -[params] -author = 'John Smith' -{{< /code-toggle >}} - -Front matter fields may be [boolean](g), [integer](g), [float](g), [string](g), [arrays](g), or [maps](g). Note that the TOML format also supports unquoted date/time values. - -## Fields - -The most common front matter fields are `date`, `draft`, `title`, and `weight`, but you can specify metadata using any of fields below. - -> [!note] -> The field names below are reserved. For example, you cannot create a custom field named `type`. Create custom fields under the `params` key. See the [parameters] section for details. - -[parameters]: #parameters - -aliases -: (`string array`) An array of one or more aliases, where each alias is a relative URL that will redirect the browser to the current location. Access these values from a template using the [`Aliases`] method on a `Page` object. See the [aliases] section for details. - -build -: (`map`) A map of [build options]. - -cascade -: (`map`) A map of front matter keys whose values are passed down to the page's descendants unless overwritten by self or a closer ancestor's cascade. See the [cascade] section for details. - -date -: (`string`) The date associated with the page, typically the creation date. Note that the TOML format also supports unquoted date/time values. See the [dates](#dates) section for examples. Access this value from a template using the [`Date`] method on a `Page` object. - -description -: (`string`) Conceptually different than the page `summary`, the description is typically rendered within a `meta` element within the `head` element of the published HTML file. Access this value from a template using the [`Description`] method on a `Page` object. - -draft -: (`bool`) Whether to disable rendering unless you pass the `--buildDrafts` flag to the `hugo` command. Access this value from a template using the [`Draft`] method on a `Page` object. - -expiryDate -: (`string`) The page expiration date. On or after the expiration date, the page will not be rendered unless you pass the `--buildExpired` flag to the `hugo` command. Note that the TOML format also supports unquoted date/time values. See the [dates](#dates) section for examples. Access this value from a template using the [`ExpiryDate`] method on a `Page` object. - -headless -: (`bool`) Applicable to [leaf bundles], whether to set the `render` and `list` [build options] to `never`, creating a headless bundle of [page resources]. - -isCJKLanguage -: (`bool`) Whether the content language is in the [CJK](g) family. This value determines how Hugo calculates word count, and affects the values returned by the [`WordCount`], [`FuzzyWordCount`], [`ReadingTime`], and [`Summary`] methods on a `Page` object. - -keywords -: (`string array`) An array of keywords, typically rendered within a `meta` element within the `head` element of the published HTML file, or used as a [taxonomy](g) to classify content. Access these values from a template using the [`Keywords`] method on a `Page` object. - -lastmod -: (`string`) The date that the page was last modified. Note that the TOML format also supports unquoted date/time values. See the [dates](#dates) section for examples. Access this value from a template using the [`Lastmod`] method on a `Page` object. - -layout -: (`string`) Provide a template name to [target a specific template], overriding the default [template lookup order]. Set the value to the base file name of the template, excluding its extension. Access this value from a template using the [`Layout`] method on a `Page` object. - -linkTitle -: (`string`) Typically a shorter version of the `title`. Access this value from a template using the [`LinkTitle`] method on a `Page` object. - -markup -: (`string`) An identifier corresponding to one of the supported [content formats]. If not provided, Hugo determines the content renderer based on the file extension. - -menus -: (`string`, `string array`, or `map`) If set, Hugo adds the page to the given menu or menus. See the [menus] page for details. - -modified -: Alias to [lastmod](#lastmod). - -outputs -: (`string array`) The [output formats] to render. See [configure outputs] for more information. - -params -: {{< new-in 0.123.0 />}} -: (`map`) A map of custom [page parameters]. - -pubdate -: Alias to [publishDate](#publishdate). - -publishDate -: (`string`) The page publication date. Before the publication date, the page will not be rendered unless you pass the `--buildFuture` flag to the `hugo` command. Note that the TOML format also supports unquoted date/time values. See the [dates](#dates) section for examples. Access this value from a template using the [`PublishDate`] method on a `Page` object. - -published -: Alias to [publishDate](#publishdate). - -resources -: (`map array`) An array of maps to provide metadata for [page resources]. - -sitemap -: (`map`) A map of sitemap options. See the [sitemap templates] page for details. Access these values from a template using the [`Sitemap`] method on a `Page` object. - -slug -: (`string`) Overrides the last segment of the URL path. Not applicable to section pages. See the [URL management] page for details. Access this value from a template using the [`Slug`] method on a `Page` object. - -summary -: (`string`) Conceptually different than the page `description`, the summary either summarizes the content or serves as a teaser to encourage readers to visit the page. Access this value from a template using the [`Summary`] method on a `Page` object. - -title -: (`string`) The page title. Access this value from a template using the [`Title`] method on a `Page` object. - -translationKey -: (`string`) An arbitrary value used to relate two or more translations of the same page, useful when the translated pages do not share a common path. Access this value from a template using the [`TranslationKey`] method on a `Page` object. - -type -: (`string`) The [content type](g), overriding the value derived from the top-level section in which the page resides. Access this value from a template using the [`Type`] method on a `Page` object. - -unpublishdate -: Alias to [expirydate](#expirydate). - -url -: (`string`) Overrides the entire URL path. Applicable to regular pages and section pages. See the [URL management] page for details. - -weight -: (`int`) The page [weight](g), used to order the page within a [page collection](g). Access this value from a template using the [`Weight`] method on a `Page` object. - -[URL management]: /content-management/urls/#slug -[`Summary`]: /methods/page/summary/ -[`aliases`]: /methods/page/aliases/ -[`date`]: /methods/page/date/ -[`description`]: /methods/page/description/ -[`draft`]: /methods/page/draft/ -[`expirydate`]: /methods/page/expirydate/ -[`fuzzywordcount`]: /methods/page/wordcount/ -[`keywords`]: /methods/page/keywords/ -[`lastmod`]: /methods/page/date/ -[`layout`]: /methods/page/layout/ -[`linktitle`]: /methods/page/linktitle/ -[`publishdate`]: /methods/page/publishdate/ -[`readingtime`]: /methods/page/readingtime/ -[`sitemap`]: /methods/page/sitemap/ -[`slug`]: /methods/page/slug/ -[`summary`]: /methods/page/summary/ -[`title`]: /methods/page/title/ -[`translationkey`]: /methods/page/translationkey/ -[`type`]: /methods/page/type/ -[`weight`]: /methods/page/weight/ -[`wordcount`]: /methods/page/wordcount/ -[aliases]: /content-management/urls/#aliases -[build options]: /content-management/build-options/ -[cascade]: #cascade-1 -[configure outputs]: /configuration/outputs/#outputs-per-page -[content formats]: /content-management/formats/#classification -[leaf bundles]: /content-management/page-bundles/#leaf-bundles -[menus]: /content-management/menus/#define-in-front-matter -[output formats]: /configuration/output-formats/ -[page parameters]: #parameters -[page resources]: /content-management/page-resources/#metadata -[sitemap templates]: /templates/sitemap/ -[target a specific template]: /templates/lookup-order/#target-a-template -[template lookup order]: /templates/lookup-order/ - -## Parameters - -{{< new-in 0.123.0 />}} - -Specify custom page parameters under the `params` key in front matter: - -{{< code-toggle file=content/example.md fm=true >}} -title = 'Example' -date = 2024-02-02T04:14:54-08:00 -draft = false -weight = 10 -[params] -author = 'John Smith' -{{< /code-toggle >}} - -Access these values from a template using the [`Params`] or [`Param`] method on a `Page` object. - -[`param`]: /methods/page/param/ -[`params`]: /methods/page/params/ - -Hugo provides [embedded templates] to optionally insert meta data within the `head` element of your rendered pages. These embedded templates expect the following front matter parameters: - -Parameter|Data type|Used by these embedded templates -:--|:--|:-- -`audio`|`[]string`|[`opengraph.html`] -`images`|`[]string`|[`opengraph.html`], [`schema.html`], [`twitter_cards.html`] -`videos`|`[]string`|[`opengraph.html`] - -The embedded templates will skip a parameter if not provided in front matter, but will throw an error if the data type is unexpected. - -## Taxonomies - -Classify content by adding taxonomy terms to front matter. For example, with this site configuration: - -{{< code-toggle file=hugo >}} -[taxonomies] -tag = 'tags' -genre = 'genres' -{{< /code-toggle >}} - -Add taxonomy terms as shown below: - -{{< code-toggle file=content/example.md fm=true >}} -title = 'Example' -date = 2024-02-02T04:14:54-08:00 -draft = false -weight = 10 -tags = ['red','blue'] -genres = ['mystery','romance'] -[params] -author = 'John Smith' -{{< /code-toggle >}} - -You can add taxonomy terms to the front matter of any these [page kinds](g): - -- `home` -- `page` -- `section` -- `taxonomy` -- `term` - -Access taxonomy terms from a template using the [`Params`] or [`GetTerms`] method on a `Page` object. For example: - -```go-html-template {file="layouts/_default/single.html"} -{{ with .GetTerms "tags" }} -

    Tags

    - -{{ end }} -``` - -[`Params`]: /methods/page/params/ -[`GetTerms`]: /methods/page/getterms/ - -## Cascade - -A [node](g) can cascade front matter values to its descendants. However, this cascading will be prevented if the descendant already defines the field, or if a closer ancestor node has already cascaded a value for that same field. - -For example, to cascade a "color" parameter from the home page to all its descendants: - -{{< code-toggle file=content/_index.md fm=true >}} -title = 'Home' -[cascade.params] -color = 'red' -{{< /code-toggle >}} - -### Target - - - -The `target`[^1] keyword allows you to target specific pages or [environments](g). For example, to cascade a "color" parameter from the home page only to pages within the "articles" section, including the "articles" section page itself: - -[^1]: The `_target` alias for `target` is deprecated and will be removed in a future release. - -{{< code-toggle file=content/_index.md fm=true >}} -title = 'Home' -[cascade.params] -color = 'red' -[cascade.target] -path = '{/articles,/articles/**}' -{{< /code-toggle >}} - -Use any combination of these keywords to target pages and/or environments: - -environment -: (`string`) A [glob](g) pattern matching the build [environment](g). For example: `{staging,production}`. - -kind -: (`string`) A [glob](g) pattern matching the [page kind](g). For example: ` {taxonomy,term}`. - -path -: (`string`) A [glob](g) pattern matching the page's [logical path](g). For example: `{/books,/books/**}`. - -### Array - -Define an array of cascade parameters to apply different values to different targets. For example: - -{{< code-toggle file=content/_index.md fm=true >}} -title = 'Home' -[[cascade]] -[cascade.params] -color = 'red' -[cascade.target] -path = '{/books/**}' -kind = 'page' -[[cascade]] -[cascade.params] -color = 'blue' -[cascade.target] -path = '{/films/**}' -kind = 'page' -{{< /code-toggle >}} - -> [!note] -> For multilingual sites, defining cascade values in your site configuration is often more efficient. This avoids repeating the same cascade values on the home, section, taxonomy, or term page for each language. See [details](/configuration/cascade/). -> -> If you choose to define cascade values in front matter for a multilingual site, you must create a corresponding home, section, taxonomy, or term page for every language. - -## Emacs Org Mode - -If your [content format] is [Emacs Org Mode], you may provide front matter using Org Mode keywords. For example: - -```text {file="content/example.org"} -#+TITLE: Example -#+DATE: 2024-02-02T04:14:54-08:00 -#+DRAFT: false -#+AUTHOR: John Smith -#+GENRES: mystery -#+GENRES: romance -#+TAGS: red -#+TAGS: blue -#+WEIGHT: 10 -``` - -Note that you can also specify array elements on a single line: - -```text {file="content/example.org"} -#+TAGS[]: red blue -``` - -[content format]: /content-management/formats/ -[emacs org mode]: https://orgmode.org/ - -## Dates - -When populating a date field, whether a [custom page parameter](#parameters) or one of the four predefined fields ([`date`](#date), [`expiryDate`](#expirydate), [`lastmod`](#lastmod), [`publishDate`](#publishdate)), use one of these parsable formats: - -{{% include "/_common/parsable-date-time-strings.md" %}} - -To override the default time zone, set the [`timeZone`](/configuration/all/#timezone) in your site configuration. The order of precedence for determining the time zone is: - -1. The time zone offset in the date/time string -1. The time zone specified in your site configuration -1. The `Etc/UTC` time zone - -[`opengraph.html`]: {{% eturl opengraph %}} -[`schema.html`]: {{% eturl schema %}} -[`twitter_cards.html`]: {{% eturl twitter_cards %}} -[embedded templates]: /templates/embedded/ diff --git a/docs/content/en/content-management/image-processing/index.md b/docs/content/en/content-management/image-processing/index.md deleted file mode 100644 index 8d60c4f93..000000000 --- a/docs/content/en/content-management/image-processing/index.md +++ /dev/null @@ -1,447 +0,0 @@ ---- -title: Image processing -description: Resize, crop, rotate, filter, and convert images. -categories: [] -keywords: [] ---- - -## Image resources - -To process an image you must access the file as a page resource, global resource, or remote resource. - -### Page resource - -{{% glossary-term "page resource" %}} - -```text -content/ -└── posts/ - └── post-1/ <-- page bundle - ├── index.md - └── sunset.jpg <-- page resource -``` - -To access an image as a page resource: - -```go-html-template -{{ $image := .Resources.Get "sunset.jpg" }} -``` - -### Global resource - -{{% glossary-term "global resource" %}} - -```text -assets/ -└── images/ - └── sunset.jpg <-- global resource -``` - -To access an image as a global resource: - -```go-html-template -{{ $image := resources.Get "images/sunset.jpg" }} -``` - -### Remote resource - -{{% glossary-term "remote resource" %}} - -To access an image as a remote resource: - -```go-html-template -{{ $image := resources.GetRemote "https://gohugo.io/img/hugo-logo.png" }} -``` - -## Image rendering - -Once you have accessed an image as a resource, render it in your templates using the `Permalink`, `RelPermalink`, `Width`, and `Height` properties. - -Example 1: Throws an error if the resource is not found. - -```go-html-template -{{ $image := .Resources.GetMatch "sunset.jpg" }} - -``` - -Example 2: Skips image rendering if the resource is not found. - -```go-html-template -{{ $image := .Resources.GetMatch "sunset.jpg" }} -{{ with $image }} - -{{ end }} -``` - -Example 3: A more concise way to skip image rendering if the resource is not found. - -```go-html-template -{{ with .Resources.GetMatch "sunset.jpg" }} - -{{ end }} -``` - -Example 4: Skips rendering if there's problem accessing a remote resource. - -```go-html-template -{{ $url := "https://gohugo.io/img/hugo-logo.png" }} -{{ with try (resources.GetRemote $url) }} - {{ with .Err }} - {{ errorf "%s" . }} - {{ else with .Value }} - - {{ else }} - {{ errorf "Unable to get remote resource %q" $url }} - {{ end }} -{{ end }} -``` - -## Image processing methods - -The `image` resource implements the [`Process`], [`Resize`], [`Fit`], [`Fill`], [`Crop`], [`Filter`], [`Colors`] and [`Exif`] methods. - -> [!note] -> Metadata (EXIF, IPTC, XMP, etc.) is not preserved during image transformation. Use the `Exif` method with the _original_ image to extract EXIF metadata from JPEG, PNG, TIFF, and WebP images. - -### Process - -{{< new-in 0.119.0 />}} - -> [!note] -> The `Process` method is also available as a filter, which is more effective if you need to apply multiple filters to an image. See [Process filter](/functions/images/process). - -Process processes the image with the given specification. The specification can contain an optional action, one of `resize`, `crop`, `fit` or `fill`. This means that you can use this method instead of [`Resize`], [`Fit`], [`Fill`], or [`Crop`]. - -See [Options](#image-processing-options) for available options. - -You can also use this method apply image processing that does not need any scaling, e.g. format conversions: - -```go-html-template -{{/* Convert the image from JPG to PNG. */}} -{{ $png := $jpg.Process "png" }} -``` - -Some more examples: - -```go-html-template -{{/* Rotate the image 90 degrees counter-clockwise. */}} -{{ $image := $image.Process "r90" }} - -{{/* Scaling actions. */}} -{{ $image := $image.Process "resize 600x" }} -{{ $image := $image.Process "crop 600x400" }} -{{ $image := $image.Process "fit 600x400" }} -{{ $image := $image.Process "fill 600x400" }} -``` - -### Resize - -Resize an image to the given width and/or height. - -If you specify both width and height, the resulting image will be disproportionally scaled unless the original image has the same aspect ratio. - -```go-html-template -{{/* Resize to a width of 600px and preserve aspect ratio */}} -{{ $image := $image.Resize "600x" }} - -{{/* Resize to a height of 400px and preserve aspect ratio */}} -{{ $image := $image.Resize "x400" }} - -{{/* Resize to a width of 600px and a height of 400px */}} -{{ $image := $image.Resize "600x400" }} -``` - -### Fit - -Downscale an image to fit the given dimensions while maintaining aspect ratio. You must provide both width and height. - -```go-html-template -{{ $image := $image.Fit "600x400" }} -``` - -### Fill - -Crop and resize an image to match the given dimensions. You must provide both width and height. Use the [`anchor`] option to change the crop box anchor point. - -```go-html-template -{{ $image := $image.Fill "600x400" }} -``` - -### Crop - -Crop an image to match the given dimensions without resizing. You must provide both width and height. Use the [`anchor`] option to change the crop box anchor point. - -```go-html-template -{{ $image := $image.Crop "600x400" }} -``` - -### Filter - -Apply one or more [filters] to an image. - -```go-html-template -{{ $image := $image.Filter (images.GaussianBlur 6) (images.Pixelate 8) }} -``` - -Write this in a more functional style using pipes. Hugo applies the filters in the order given. - -```go-html-template -{{ $image := $image | images.Filter (images.GaussianBlur 6) (images.Pixelate 8) }} -``` - -Sometimes it can be useful to create the filter chain once and then reuse it. - -```go-html-template -{{ $filters := slice (images.GaussianBlur 6) (images.Pixelate 8) }} -{{ $image1 := $image1.Filter $filters }} -{{ $image2 := $image2.Filter $filters }} -``` - -### Colors - -`.Colors` returns a slice of hex strings with the dominant colors in the image using a simple histogram method. - -```go-html-template -{{ $colors := $image.Colors }} -``` - -This method is fast, but if you also scale down your images, it would be good for performance to extract the colors from the scaled down image. - -### EXIF - -Provides an [EXIF] object containing image metadata. - -You may access EXIF data in JPEG, PNG, TIFF, and WebP images. To prevent errors when processing images without EXIF data, wrap the access in a [`with`] statement. - -```go-html-template -{{ with $image.Exif }} - Date: {{ .Date }} - Lat/Long: {{ .Lat }}/{{ .Long }} - Tags: - {{ range $k, $v := .Tags }} - TAG: {{ $k }}: {{ $v }} - {{ end }} -{{ end }} -``` - -You may also access EXIF fields individually, using the [`lang.FormatNumber`] function to format the fields as needed. - -```go-html-template -{{ with $image.Exif }} -
      - {{ with .Date }}
    • Date: {{ .Format "January 02, 2006" }}
    • {{ end }} - {{ with .Tags.ApertureValue }}
    • Aperture: {{ lang.FormatNumber 2 . }}
    • {{ end }} - {{ with .Tags.BrightnessValue }}
    • Brightness: {{ lang.FormatNumber 2 . }}
    • {{ end }} - {{ with .Tags.ExposureTime }}
    • Exposure Time: {{ . }}
    • {{ end }} - {{ with .Tags.FNumber }}
    • F Number: {{ . }}
    • {{ end }} - {{ with .Tags.FocalLength }}
    • Focal Length: {{ . }}
    • {{ end }} - {{ with .Tags.ISOSpeedRatings }}
    • ISO Speed Ratings: {{ . }}
    • {{ end }} - {{ with .Tags.LensModel }}
    • Lens Model: {{ . }}
    • {{ end }} -
    -{{ end }} -``` - -#### EXIF methods - -Date -: (`time.Time`) Returns the image creation date/time. Format with the [`time.Format`]function. - -Lat -: (`float64`) Returns the GPS latitude in degrees. - -Long -: (`float64`) Returns the GPS longitude in degrees. - -Tags -: (`exif.Tags`) Returns a collection of the available EXIF tags for this image. You may include or exclude specific tags from this collection in the [site configuration]. - -## Image processing options - -The [`Resize`], [`Fit`], [`Fill`], and [`Crop`] methods accept a space-delimited, case-insensitive list of options. The order of the options within the list is irrelevant. - -### Dimensions - -With the [`Resize`] method you must specify width, height, or both. The [`Fit`], [`Fill`], and [`Crop`] methods require both width and height. All dimensions are in pixels. - -```go-html-template -{{ $image := $image.Resize "600x" }} -{{ $image := $image.Resize "x400" }} -{{ $image := $image.Resize "600x400" }} -{{ $image := $image.Fit "600x400" }} -{{ $image := $image.Fill "600x400" }} -{{ $image := $image.Crop "600x400" }} -``` - -### Rotation - -Rotates an image counter-clockwise by the given angle. Hugo performs rotation _before_ scaling. For example, if the original image is 600x400 and you wish to rotate the image 90 degrees counter-clockwise while scaling it by 50%: - -```go-html-template -{{ $image = $image.Resize "200x r90" }} -``` - -In the example above, the width represents the desired width _after_ rotation. - -To rotate an image without scaling, use the dimensions of the original image: - -```go-html-template -{{ with .Resources.GetMatch "sunset.jpg" }} - {{ with .Resize (printf "%dx%d r90" .Height .Width) }} - - {{ end }} -{{ end }} -``` - -In the example above, on the second line, we have reversed width and height to reflect the desired dimensions _after_ rotation. - -### Anchor - -When using the [`Crop`] or [`Fill`] method, the _anchor_ determines the placement of the crop box. You may specify `TopLeft`, `Top`, `TopRight`, `Left`, `Center`, `Right`, `BottomLeft`, `Bottom`, `BottomRight`, or `Smart`. - -The default value is `Smart`, which uses [Smartcrop] image analysis to determine the optimal placement of the crop box. You may override the default value in the [site configuration]. - -For example, if you have a 400x200 image with a bird in the upper left quadrant, you can create a 200x100 thumbnail containing the bird: - -```go-html-template -{{ $image.Crop "200x100 TopLeft" }} -``` - -If you apply [rotation](#rotation) when using the [`Crop`] or [`Fill`] method, specify the anchor relative to the rotated image. - -### Target format - -By default, Hugo encodes the image in the source format. You may convert the image to another format by specifying `bmp`, `gif`, `jpeg`, `jpg`, `png`, `tif`, `tiff`, or `webp`. - -```go-html-template -{{ $image.Resize "600x webp" }} -``` - -To convert an image without scaling, use the dimensions of the original image: - -```go-html-template -{{ with .Resources.GetMatch "sunset.jpg" }} - {{ with .Resize (printf "%dx%d webp" .Width .Height) }} - - {{ end }} -{{ end }} -``` - -### Quality - -Applicable to JPEG and WebP images, the `q` value determines the quality of the converted image. Higher values produce better quality images, while lower values produce smaller files. Set this value to a whole number between 1 and 100, inclusive. - -The default value is 75. You may override the default value in the [site configuration]. - -```go-html-template -{{ $image.Resize "600x webp q50" }} -``` - -### Hint - -Applicable to WebP images, this option corresponds to a set of predefined encoding parameters, and is equivalent to the `-preset` flag for the [`cwebp`] encoder. - -Value|Example -:--|:-- -`drawing`|Hand or line drawing with high-contrast details -`icon`|Small colorful image -`photo`|Outdoor photograph with natural lighting -`picture`|Indoor photograph such as a portrait -`text`|Image that is primarily text - -The default value is `photo`. You may override the default value in the [site configuration]. - -```go-html-template -{{ $image.Resize "600x webp picture" }} -``` - -### Background color - -When converting an image from a format that supports transparency (e.g., PNG) to a format that does _not_ support transparency (e.g., JPEG), you may specify the background color of the resulting image. - -Use either a 3-digit or 6-digit hexadecimal color code (e.g., `#00f` or `#0000ff`). - -The default value is `#ffffff` (white). You may override the default value in the [site configuration]. - -```go-html-template -{{ $image.Resize "600x jpg #b31280" }} -``` - -### Resampling filter - -You may specify the resampling filter used when resizing an image. Commonly used resampling filters include: - -Filter|Description -:--|:-- -`Box`|Simple and fast averaging filter appropriate for downscaling -`Lanczos`|High-quality resampling filter for photographic images yielding sharp results -`CatmullRom`|Sharp cubic filter that is faster than the Lanczos filter while providing similar results -`MitchellNetravali`|Cubic filter that produces smoother results with less ringing artifacts than CatmullRom -`Linear`|Bilinear resampling filter, produces smooth output, faster than cubic filters -`NearestNeighbor`|Fastest resampling filter, no antialiasing - -The default value is `Box`. You may override the default value in the [site configuration]. - -```go-html-template -{{ $image.Resize "600x400 Lanczos" }} -``` - -See [github.com/disintegration/imaging] for the complete list of resampling filters. If you wish to improve image quality at the expense of performance, you may wish to experiment with the alternative filters. - -## Image processing examples - -_The photo of the sunset used in the examples below is Copyright [Bjørn Erik Pedersen](https://bep.is) (Creative Commons Attribution-Share Alike 4.0 International license)_ - -{{< imgproc path="sunset.jpg" spec="resize 480x" alt="A sunset" />}} - -{{< imgproc path="sunset.jpg" spec="fill 120x150 left" alt="A sunset" />}} - -{{< imgproc path="sunset.jpg" spec="fill 120x150 right" alt="A sunset" />}} - -{{< imgproc path="sunset.jpg" spec="fit 120x120" alt="A sunset" />}} - -{{< imgproc path="sunset.jpg" spec="crop 240x240 center" alt="A sunset" />}} - -{{< imgproc path="sunset.jpg" spec="resize 360x q10" alt="A sunset" />}} - -## Configuration - -See [configure imaging](/configuration/imaging). - -## Smart cropping of images - -By default, Hugo uses the [Smartcrop] library when cropping images with the `Crop` or `Fill` methods. You can set the anchor point manually, but in most cases the `Smart` option will make a good choice. - -Examples using the sunset image from above: - -{{< imgproc path="sunset.jpg" spec="fill 200x200 smart" alt="A sunset" />}} - -{{< imgproc path="sunset.jpg" spec="crop 200x200 smart" alt="A sunset" />}} - -## Image processing performance consideration - -Hugo caches processed images in the `resources` directory. If you include this directory in source control, Hugo will not have to regenerate the images in a [CI/CD](g) workflow (e.g., GitHub Pages, GitLab Pages, Netlify, etc.). This results in faster builds. - -If you change image processing methods or options, or if you rename or remove images, the `resources` directory will contain unused images. To remove the unused images, perform garbage collection with: - -```sh -hugo --gc -``` - -[`anchor`]: /content-management/image-processing#anchor -[`Colors`]: #colors -[`Crop`]: #crop -[`cwebp`]: https://developers.google.com/speed/webp/docs/cwebp -[`Exif`]: #exif -[`Fill`]: #fill -[`Filter`]: #filter -[`Fit`]: #fit -[`lang.FormatNumber`]: /functions/lang/formatnumber/ -[`Process`]: #process -[`Resize`]: #resize -[`time.Format`]: /functions/time/format/ -[`with`]: /functions/go-template/with/ -[EXIF]: https://en.wikipedia.org/wiki/Exif -[filters]: /functions/images/filter/#image-filters -[github.com/disintegration/imaging]: https://github.com/disintegration/imaging#image-resizing -[site configuration]: /configuration/imaging/ -[Smartcrop]: https://github.com/muesli/smartcrop#smartcrop diff --git a/docs/content/en/content-management/image-processing/sunset.jpg b/docs/content/en/content-management/image-processing/sunset.jpg deleted file mode 100644 index 4dbcc0836..000000000 Binary files a/docs/content/en/content-management/image-processing/sunset.jpg and /dev/null differ diff --git a/docs/content/en/content-management/markdown-attributes.md b/docs/content/en/content-management/markdown-attributes.md deleted file mode 100644 index f52a48f17..000000000 --- a/docs/content/en/content-management/markdown-attributes.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -title: Markdown attributes -description: Use Markdown attributes to add HTML attributes when rendering Markdown to HTML. -categories: [] -keywords: [] ---- - -## Overview - -Hugo supports Markdown attributes on images and block elements including blockquotes, fenced code blocks, headings, horizontal rules, lists, paragraphs, and tables. - -For example: - -```text -This is a paragraph. -{class="foo bar" id="baz"} -``` - -With `class` and `id` you can use shorthand notation: - -```text -This is a paragraph. -{.foo .bar #baz} -``` - -Hugo renders both of these to: - -```html -

    This is a paragraph.

    -``` - -## Block elements - -Update your site configuration to enable Markdown attributes for block-level elements. - -{{< code-toggle file=hugo >}} -[markup.goldmark.parser.attribute] -title = true # default is true -block = true # default is false -{{< /code-toggle >}} - -## Standalone images - -By default, when the [Goldmark] Markdown renderer encounters a standalone image element (no other elements or text on the same line), it wraps the image element within a paragraph element per the [CommonMark specification]. - -[CommonMark specification]: https://spec.commonmark.org/current/ -[Goldmark]: https://github.com/yuin/goldmark - -If you were to place an attribute list beneath an image element, Hugo would apply the attributes to the surrounding paragraph, not the image. - -To apply attributes to a standalone image element, you must disable the default wrapping behavior: - -{{< code-toggle file=hugo >}} -[markup.goldmark.parser] -wrapStandAloneImageWithinParagraph = false # default is true -{{< /code-toggle >}} - -## Usage - -You may add [global HTML attributes], or HTML attributes specific to the current element type. Consistent with its content security model, Hugo removes HTML event attributes such as `onclick` and `onmouseover`. - -[global HTML attributes]: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes - -The attribute list consists of one or more key-value pairs, separated by spaces or commas, wrapped by braces. You must quote string values that contain spaces. Unlike HTML, boolean attributes must have both key and value. - -For example: - -```text -> This is a blockquote. -{class="foo bar" hidden=hidden} -``` - -Hugo renders this to: - -```html - -``` - -In most cases, place the attribute list beneath the markup element. For headings and fenced code blocks, place the attribute list on the right. - -Element|Position of attribute list -:--|:-- -blockquote | bottom -fenced code block | right -heading | right -horizontal rule | bottom -image | bottom -list | bottom -paragraph | bottom -table | bottom - -For example: - -````text -## Section 1 {class=foo} - -```bash {class=foo linenos=inline} -declare a=1 -echo "${a}" -``` - -This is a paragraph. -{class=foo} -```` - -As shown above, the attribute list for fenced code blocks is not limited to HTML attributes. You can also configure syntax highlighting by passing one or more of [these options](/functions/transform/highlight/#options). diff --git a/docs/content/en/content-management/mathematics.md b/docs/content/en/content-management/mathematics.md deleted file mode 100644 index e0c8ba4d0..000000000 --- a/docs/content/en/content-management/mathematics.md +++ /dev/null @@ -1,238 +0,0 @@ ---- -title: Mathematics in Markdown -linkTitle: Mathematics -description: Include mathematical equations and expressions in Markdown using LaTeX markup. -categories: [] -keywords: [] ---- - -{{< new-in 0.122.0 />}} - -## Overview - -Mathematical equations and expressions written in [LaTeX] are common in academic and scientific publications. Your browser typically renders this mathematical markup using an open-source JavaScript display engine such as [MathJax] or [KaTeX]. - -For example, with this LaTeX markup: - -```text -\[ -\begin{aligned} -KL(\hat{y} || y) &= \sum_{c=1}^{M}\hat{y}_c \log{\frac{\hat{y}_c}{y_c}} \\ -JS(\hat{y} || y) &= \frac{1}{2}(KL(y||\frac{y+\hat{y}}{2}) + KL(\hat{y}||\frac{y+\hat{y}}{2})) -\end{aligned} -\] -``` - -The MathJax display engine renders this: - -\[ -\begin{aligned} -KL(\hat{y} || y) &= \sum_{c=1}^{M}\hat{y}_c \log{\frac{\hat{y}_c}{y_c}} \\ -JS(\hat{y} || y) &= \frac{1}{2}(KL(y||\frac{y+\hat{y}}{2}) + KL(\hat{y}||\frac{y+\hat{y}}{2})) -\end{aligned} -\] - -Equations and expressions can be displayed inline with other text, or as standalone blocks. Block presentation is also known as "display" mode. - -Whether an equation or expression appears inline, or as a block, depends on the delimiters that surround the mathematical markup. Delimiters are defined in pairs, where each pair consists of an opening and closing delimiter. The opening and closing delimiters may be the same, or different. - -> [!note] -> You can configure Hugo to render mathematical markup on the client side using the MathJax or KaTeX display engine, or you can render the markup with the [`transform.ToMath`] function while building your site. -> -> The first approach is described below. - -## Setup - -Follow these instructions to include mathematical equations and expressions in your Markdown using LaTeX markup. - -### Step 1 - -Enable and configure the Goldmark [passthrough extension] in your site configuration. The passthrough extension preserves raw Markdown within delimited snippets of text, including the delimiters themselves. - -{{< code-toggle file=hugo copy=true >}} -[markup.goldmark.extensions.passthrough] -enable = true - -[markup.goldmark.extensions.passthrough.delimiters] -block = [['\[', '\]'], ['$$', '$$']] -inline = [['\(', '\)']] - -[params] -math = true -{{< /code-toggle >}} - -The configuration above enables mathematical rendering on every page unless you set the `math` parameter to `false` in front matter. To enable mathematical rendering as needed, set the `math` parameter to `false` in your site configuration, and set the `math` parameter to `true` in front matter. Use this parameter in your base template as shown in [Step 3]. - -> [!note] -> The configuration above precludes the use of the `$...$` delimiter pair for inline equations. Although you can add this delimiter pair to the configuration and JavaScript, you will need to double-escape the `$` symbol when used outside of math contexts to avoid unintended formatting. -> -> See the [inline delimiters](#inline-delimiters) section for details. - -To disable passthrough of inline snippets, omit the `inline` key from the configuration: - -{{< code-toggle file=hugo >}} -[markup.goldmark.extensions.passthrough.delimiters] -block = [['\[', '\]'], ['$$', '$$']] -{{< /code-toggle >}} - -You can define your own opening and closing delimiters, provided they match the delimiters that you set in [Step 2]. - -{{< code-toggle file=hugo >}} -[markup.goldmark.extensions.passthrough.delimiters] -block = [['@@', '@@']] -inline = [['@', '@']] -{{< /code-toggle >}} - -### Step 2 - -Create a partial template to load MathJax or KaTeX. The example below loads MathJax, or you can use KaTeX as described in the [engines](#engines) section. - -```go-html-template {file="layouts/partials/math.html" copy=true} - - -``` - -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 deleted file mode 100644 index 6d01173dc..000000000 --- a/docs/content/en/content-management/menus.md +++ /dev/null @@ -1,97 +0,0 @@ ---- -title: Menus -description: Create menus by defining entries, localizing each entry, and rendering the resulting data structure. -categories: [] -keywords: [] -aliases: [/extras/menus/] ---- - -## Overview - -To create a menu for your site: - -1. Define the menu entries -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. - -There are three ways to define menu entries: - -1. Automatically -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. - -## Define automatically - -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 >}} -sectionPagesMenu = "main" -{{< /code-toggle >}} - -This creates a menu structure that you can access with `site.Menus.main` in your templates. See [menu templates] for details. - -## Define in front matter - -To add a page to the "main" menu: - -{{< code-toggle file=content/about.md fm=true >}} -title = 'About' -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 fm=true >}} -title = 'Contact' -menus = ['main','footer'] -{{< /code-toggle >}} - -Access the entry with `site.Menus.main` and `site.Menus.footer` in your templates. See [menu templates] for details. - -> [!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: - -{{% include "/_common/menu-entry-properties.md" %}} - -### Example - -This front matter menu entry demonstrates some of the available properties: - -{{< code-toggle file=content/products/software.md fm=true >}} -title = 'Software' -[menus.main] -parent = 'Products' -weight = 20 -pre = '' -[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 - -See [configure menus](/configuration/menus/). - -## Localize - -Hugo provides two methods to localize your menu entries. See [multilingual]. - -## Render - -See [menu templates]. - -[menu templates]: /templates/menu/ -[multilingual]: /content-management/multilingual/#menus -[template]: /templates/menu/ diff --git a/docs/content/en/content-management/multilingual.md b/docs/content/en/content-management/multilingual.md deleted file mode 100644 index d419f4381..000000000 --- a/docs/content/en/content-management/multilingual.md +++ /dev/null @@ -1,429 +0,0 @@ ---- -title: Multilingual mode -linkTitle: Multilingual -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/] ---- - -## Configuration - -See [configure languages](/configuration/languages/). - -## 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 file name - -Considering the following example: - -1. `/content/about.en.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 file name__. - -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. - -### 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` parameter. - -{{< code-toggle file=hugo >}} -languages: - en: - weight: 10 - languageName: "English" - contentDir: "content/english" - fr: - weight: 20 - languageName: "Français" - contentDir: "content/french" -{{< /code-toggle >}} - -The value of `contentDir` can be any valid path -- even absolute path references. The only restriction is that the content directories cannot overlap. - -Considering the following example in conjunction with the configuration above: - -1. `/content/english/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. - -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 - -Any pages sharing the same `translationKey` set in front matter will be linked as translated pages regardless of basename or location. - -Considering the following example: - -1. `/content/about-us.en.md` -1. `/content/om.nn.md` -1. `/content/presentation/a-propos.fr.md` - -{{< code-toggle file=hugo >}} -translationKey: "about" -{{< /code-toggle >}} - -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 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 - -For example, a French translation can have its own localized slug. - -{{< 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 - -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`. - -> [!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 translated content - -To create a list of links to translated content, use a template similar to the following: - -```go-html-template {file="layouts/partials/i18nlist.html"} -{{ if .IsTranslated }} -

    {{ i18n "translations" }}

    - -{{ end }} -``` - -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 - -`.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: - -```go-html-template {file="layouts/partials/allLanguages.html"} - -``` - -## Translation of strings - -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 >}} -defaultContentLanguage = 'en' - -[languages] -[languages.en] -contentDir = 'content/en' -languageName = 'English' -weight = 1 -[languages.fr] -contentDir = 'content/fr' -languageName = 'Français' -weight = 2 -[languages.de] -contentDir = 'content/de' -languageName = 'Deutsch' -weight = 3 - -{{< /code-toggle >}} - -### Dates - -With this front matter: - -{{< code-toggle file=hugo >}} -date = 2021-11-03T12:34:56+01:00 -{{< /code-toggle >}} - -And this template code: - -```go-html-template -{{ .Date | time.Format ":date_full" }} -``` - -The rendered page displays: - -Language|Value -:--|:-- -English|Wednesday, November 3, 2021 -Français|mercredi 3 novembre 2021 -Deutsch|Mittwoch, 3. November 2021 - -See [`time.Format`] for details. - -### Currency - -With this template code: - -```go-html-template -{{ 512.5032 | lang.FormatCurrency 2 "USD" }} -``` - -The rendered page displays: - -Language|Value -:--|:-- -English|$512.50 -Français|512,50 $US -Deutsch|512,50 $ - -See [lang.FormatCurrency] and [lang.FormatAccounting] for details. - -### Numbers - -With this template code: - -```go-html-template -{{ 512.5032 | lang.FormatNumber 2 }} -``` - -The rendered page displays: - -Language|Value -:--|:-- -English|512.50 -Français|512,50 -Deutsch|512,50 - -See [lang.FormatNumber] and [lang.FormatNumberCustom] for details. - -### Percentages - -With this template code: - -```go-html-template -{{ 512.5032 | lang.FormatPercent 2 }} -``` - -The rendered page displays: - -Language|Value -:--|:-- -English|512.50% -Français|512,50 % -Deutsch|512,50 % - -See [lang.FormatPercent] for details. - -## Menus - -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 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 - -When rendering the text that appears in menu each entry, the [example menu template] does this: - -```go-html-template -{{ or (T .Identifier) .Name | safeHTML }} -``` - -It queries the translation table for the current language using the menu entry's `identifier` and returns the translated string. If the translation table does not exist, or if the `identifier` key is not present in the translation table, it falls back to `name`. - -The `identifier` depends on how you define menu entries: - -- If you define the menu entry [automatically] using the section pages menu, the `identifier` is the page's `.Section`. -- If you define the menu entry [in site configuration] or [in front matter], set the `identifier` property to the desired value. - -For example, if you define menu entries in site configuration: - -{{< code-toggle file=hugo >}} -[[menus.main]] - identifier = 'products' - name = 'Products' - pageRef = '/products' - weight = 10 -[[menus.main]] - identifier = 'services' - name = 'Services' - pageRef = '/services' - weight = 20 -{{< / code-toggle >}} - -Create corresponding entries in the translation tables: - -{{< code-toggle file=i18n/de >}} -products = 'Produkte' -services = 'Leistungen' -{{< / code-toggle >}} - -## 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. - -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: - -```sh -hugo --printI18nWarnings | grep i18n -i18n|MISSING_TRANSLATION|en|wordCount -``` - -## 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`] or [`absLangURL`] template function, or be prefixed with `{{ .LanguagePrefix }}` - -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 content` - -If you organize content with translations in the same directory: - -```sh -hugo new content post/test.en.md -hugo new content post/test.de.md -``` - -If you organize content with translations in different directories: - -```sh -hugo new content content/en/post/test.md -hugo new content content/de/post/test.md -``` - -[`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/index.md b/docs/content/en/content-management/organization/index.md deleted file mode 100644 index a7682bfad..000000000 --- a/docs/content/en/content-management/organization/index.md +++ /dev/null @@ -1,151 +0,0 @@ ---- -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: [] -keywords: [] -aliases: [/content/sections/] ---- - -## 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. - -```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. - -## Organization of content source - -In Hugo, your content should be organized in a manner that reflects the rendered website. - -While Hugo supports content nested at any level, the top levels (i.e. `content/`) are special in Hugo and are considered the content type used to determine layouts etc. To read more about sections, including how to nest them, see [sections]. - -Without any additional configuration, the following will automatically work: - -```txt -. -└── content - └── about - | └── index.md // <- https://example.org/about/ - ├── posts - | ├── firstpost.md // <- https://example.org/posts/firstpost/ - | ├── happy - | | └── ness.md // <- https://example.org/posts/happy/ness/ - | └── secondpost.md // <- https://example.org/posts/secondpost/ - └── quote - ├── first.md // <- https://example.org/quote/first/ - └── second.md // <- https://example.org/quote/second/ -``` - -## 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]. - -### 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. - -> [!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 -. ⊢--^-⊣⊢---^---⊣ -. file path -. ⊢------^------⊣ -content/posts/_index.md -``` - -At build, this will output to the following destination with the associated values: - -```txt - - url ("/posts/") - ⊢-^-⊣ - baseurl section ("posts") -⊢--------^---------⊣⊢-^-⊣ - permalink -⊢----------^-------------⊣ -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 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") -. ⊢-----------^------------⊣ -. section slug -. ⊢-^-⊣⊢--------^----------⊣ -content/posts/my-first-hugo-post.md -``` - -When Hugo builds your site, the content will be output to the following destination: - -```txt - - url ("/posts/my-first-hugo-post/") - ⊢------------^----------⊣ - baseurl section slug -⊢--------^--------⊣⊢-^--⊣⊢-------^---------⊣ - permalink -⊢--------------------^---------------------⊣ -https://example.org/posts/my-first-hugo-post/index.html -``` - -## 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. - -### `section` - -A default content type is determined by the section in which a content item is stored. `section` is determined by the location within the project's `content` directory. `section` *cannot* be specified or overridden in front matter. - -### `slug` - -The `slug` is the last segment of the URL path, defined by the file name and optionally overridden by a `slug` value in front matter. See [URL Management](/content-management/urls/#slug) for details. - -### `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 - -### `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]: /configuration/ -[pretty]: /content-management/urls/#appearance -[sections]: /content-management/sections/ -[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 deleted file mode 100644 index f6a5cf771..000000000 --- a/docs/content/en/content-management/page-bundles.md +++ /dev/null @@ -1,145 +0,0 @@ ---- -title: Page bundles -description: Use page bundles to logically associate one or more resources with content. -categories: [] -keywords: [] ---- - -## Introduction - -A page bundle is a directory that encapsulates both content and associated resources. - -By way of example, this site has an "about" page and a "privacy" page: - -```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. - -Page bundles are either _leaf bundles_ or _branch bundles_. - -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. - -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 -├── posts -│ ├── my-post -│ │ ├── content-1.md -│ │ ├── content-2.md -│ │ ├── image-1.jpg -│ │ ├── image-2.png -│ │ └── index.md -│ └── my-other-post -│ └── index.md -└── another-section - ├── foo.md - └── not-a-leaf-bundle - ├── bar.md - └── another-leaf-bundle - └── index.md -``` - -There are four leaf bundles in the example above: - -about -: This leaf bundle does not contain any page resources. - -my-post -: This leaf bundle contains an index file, two resources of [resource type](g) `page`, and two resources of resource type `image`. - - - content-1, content-2 - - These are resources of resource type `page`, accessible via the [`Resources`] method on the `Page` object. Hugo will not render these as individual pages. - - - image-1, image-2 - - These are resources of resource type `image`, accessible via the `Resources` method on the `Page` object - -my-other-post -: This leaf bundle does not contain any page resources. - -another-leaf-bundle -: This leaf bundle does not contain any page resources. - -> [!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. - -## Branch bundles - -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/ -│ ├── _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 -``` - -There are three branch bundles in the example above: - -home page -: This branch bundle contains an index file, two descendant branch bundles, and no resources. - -branch-bundle-1 -: This branch bundle contains an index file, two resources of [resource type](g) `page`, and two resources of resource type `image`. - -branch-bundle-2 -: This branch bundle contains an index file and a leaf bundle. - -> [!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 deleted file mode 100644 index 204ca5301..000000000 --- a/docs/content/en/content-management/page-resources.md +++ /dev/null @@ -1,297 +0,0 @@ ---- -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 -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`. - -```text -content -└── post - ├── first-post - │ ├── images - │ │ ├── a.jpg - │ │ ├── b.jpg - │ │ └── c.jpg - │ ├── index.md (root of page bundle) - │ ├── latest.html - │ ├── manual.json - │ ├── notice.md - │ ├── office.mp3 - │ ├── pocket.mp4 - │ ├── rating.pdf - │ └── safety.txt - └── second-post - └── index.md (root of page bundle) -``` - -## Examples - -Use any of these methods on a `Page` object to capture page resources: - - - [`Resources.ByType`] - - [`Resources.Get`] - - [`Resources.GetMatch`] - - [`Resources.Match`] - - Once you have captured a resource, use any of the applicable [`Resource`] methods to return a value or perform an action. - -The following examples assume this content structure: - -```text -content/ -└── example/ - ├── data/ - │ └── books.json <-- page resource - ├── images/ - │ ├── a.jpg <-- page resource - │ └── b.jpg <-- page resource - ├── snippets/ - │ └── text.md <-- page resource - └── index.md -``` - -Render a single image, and throw an error if the file does not exist: - -```go-html-template -{{ $path := "images/a.jpg" }} -{{ with .Resources.Get $path }} - -{{ else }} - {{ errorf "Unable to get page resource %q" $path }} -{{ end }} -``` - -Render all images, resized to 300 px wide: - -```go-html-template -{{ range .Resources.ByType "image" }} - {{ with .Resize "300x" }} - - {{ end }} -{{ end }} -``` - -Render the markdown snippet: - -```go-html-template -{{ with .Resources.Get "snippets/text.md" }} - {{ .Content }} -{{ end }} -``` - -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 }} -``` - -## 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. - -name -: (`string`) Sets the value returned in `Name`. - -> [!note] -> The methods `Match`, `Get` and `GetMatch` use `Name` to match the resources. - -title -: (`string`) Sets the value returned in `Title` - -params -: (`map`) A map of custom key-value pairs. - -### Resources metadata example - -{{< code-toggle file=content/example.md fm=true >}} -title: Application -date : 2018-01-25 -resources : -- src : "images/sunset.jpg" - name : "header" -- src : "documents/photo_specs.pdf" - title : "Photo Specifications" - params: - icon : "photo" -- src : "documents/guide.pdf" - title : "Instruction Guide" -- src : "documents/checklist.pdf" - title : "Document Checklist" -- src : "documents/payment.docx" - title : "Proof of Payment" -- src : "**.pdf" - name : "pdf-file-:counter" - params : - icon : "pdf" -- src : "**.docx" - params : - icon : "word" -{{}} - -From the example above: - -- `sunset.jpg` will receive a new `Name` and can now be found with `.GetMatch "header"`. -- `documents/photo_specs.pdf` will get the `photo` icon. -- `documents/checklist.pdf`, `documents/guide.pdf` and `documents/payment.docx` will get `Title` as set by `title`. -- Every `PDF` in the bundle except `documents/photo_specs.pdf` will get the `pdf` icon. -- 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. - -### The `:counter` placeholder in `name` and `title` - -The `:counter` is a special placeholder recognized in `name` and `title` parameters `resources`. - -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 file=content/inspections/engine/index.md fm=true >}} -title = 'Engine inspections' -[[resources]] - src = "*specs.pdf" - title = "Specification #:counter" -[[resources]] - src = "**.pdf" - name = "pdf-file-:counter" -{{}} - -the `Name` and `Title` will be assigned to the resource files as follows: - -| Resource file | `Name` | `Title` | -|-------------------|-------------------|-----------------------| -| checklist.pdf | `"pdf-file-1.pdf` | `"checklist.pdf"` | -| 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 deleted file mode 100644 index d7b18dab0..000000000 --- a/docs/content/en/content-management/related-content.md +++ /dev/null @@ -1,102 +0,0 @@ ---- -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/sections.md b/docs/content/en/content-management/sections.md deleted file mode 100644 index f7a2296f5..000000000 --- a/docs/content/en/content-management/sections.md +++ /dev/null @@ -1,139 +0,0 @@ ---- -title: Sections -description: Organize content into sections. - -categories: [] -keywords: [] -aliases: [/content/sections/] ---- - -## Overview - -{{% glossary-term "section" %}} - -```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 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. - -## Explanation - -Sections and non-sections behave differently. - -||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 file structure from the [example above](#overview): - -1. The list page for the articles section includes all articles, regardless of directory structure; none of the subdirectories are sections. -1. The articles/2022 and articles/2023 directories do not have list pages; they are not sections. -1. The list page for the products section, by default, includes product-1 and product-2, but not their descendant pages. To include descendant pages, use the `RegularPagesRecursive` method instead of the `Pages` method in the list template. -1. All directories in the products section have list pages; each directory is a section. - -## Template selection - -Hugo has a defined [lookup order] to determine which template to use when rendering a page. The [lookup rules] consider the top-level section name; subsection names are not considered when selecting a template. - -With the file structure from the [example above](#overview): - -Content directory|Section template -:--|:-- -`content/products`|`layouts/products/list.html` -`content/products/product-1`|`layouts/products/list.html` -`content/products/product-1/benefits`|`layouts/products/list.html` - -Content directory|Single template -:--|:-- -`content/products`|`layouts/products/single.html` -`content/products/product-1`|`layouts/products/single.html` -`content/products/product-1/benefits`|`layouts/products/single.html` - -If you need to use a different template for a subsection, specify `type` and/or `layout` in front matter. - -## Ancestors and descendants - -A section has one or more ancestors (including the home page), and zero or more descendants. With the file structure from the [example above](#overview): - -```text -content/products/product-1/benefits/benefit-1.md -``` - -The content file (benefit-1.md) has four ancestors: benefits, product-1, products, and the home page. This logical relationship allows us to use the `.Parent` and `.Ancestors` methods to traverse the site structure. - -For example, use the `.Ancestors` method to render breadcrumb navigation. - -```go-html-template {file="layouts/partials/breadcrumb.html"} - -``` - -With this CSS: - -```css -.breadcrumb ol { - padding-left: 0; -} - -.breadcrumb li { - display: inline; -} - -.breadcrumb li:not(:last-child)::after { - content: "»"; -} -``` - -Hugo renders this, where each breadcrumb is a link to the corresponding page: - -```text -Home » Products » Product 1 » Benefits » Benefit 1 -``` - -[lookup order]: /templates/lookup-order/ -[lookup rules]: /templates/lookup-order/#lookup-rules diff --git a/docs/content/en/content-management/shortcodes.md b/docs/content/en/content-management/shortcodes.md deleted file mode 100644 index 2de387f39..000000000 --- a/docs/content/en/content-management/shortcodes.md +++ /dev/null @@ -1,230 +0,0 @@ ---- -title: Shortcodes -description: Use embedded, custom, or inline shortcodes to insert elements such as videos, images, and social media embeds into your content. -categories: [] -keywords: [] -aliases: [/extras/shortcodes/] ---- - -## Introduction - -{{% glossary-term shortcode %}} - -There are three types of shortcodes: embedded, custom, and inline. - -## Embedded - -Hugo's embedded shortcodes are pre-defined templates within the application. Refer to each shortcode's documentation for specific usage instructions and available arguments. - -{{% list-pages-in-section path=/shortcodes %}} - -## Custom - -Create custom shortcodes to simplify and standardize content creation. For example, the following shortcode template generates an audio player using a [global resource](g): - -```go-html-template {file="layouts/shortcodes/audio.html"} -{{ with resources.Get (.Get "src") }} - -{{ end }} -``` - -Then call the shortcode from within markup: - -```text {file="content/example.md"} -{{}} -``` - -Learn more about creating shortcodes in the [shortcode templates] section. - -## Inline - -An inline shortcode is a shortcode template defined within content. - -Hugo's security model is based on the premise that template and configuration authors are trusted, but content authors are not. This model enables generation of HTML output safe against code injection. - -To conform with this security model, creating shortcode templates within content is disabled by default. If you trust your content authors, you can enable this functionality in your site's configuration: - -{{< code-toggle file=hugo >}} -[security] -enableInlineShortcodes = true -{{< /code-toggle >}} - -For more information see [configure security](/configuration/security). - -The following example demonstrates an inline shortcode, `date.inline`, that accepts a single positional argument: a date/time [layout string]. - -```text {file="content/example.md"} -Today is -{{}} - {{- now | time.Format (.Get 0) -}} -{{}}. - -Today is {{}}. -``` - -In the example above, the inline shortcode is executed twice: once upon definition and again when subsequently called. Hugo renders this to: - -```html -

    Today is Jan 30, 2025.

    -

    Today is Thursday, January 30, 2025

    -``` - -Inline shortcodes process their inner content within the same context as regular shortcode templates, allowing you to use any available [shortcode method]. - -> [!note] -> You cannot [nest](#nesting) inline shortcodes. - -Learn more about creating shortcodes in the [shortcode templates] section. - -## Calling - -Shortcode calls involve three syntactical elements: tags, arguments, and notation. - -### Tags - -Some shortcodes expect content between opening and closing tags. For example, the embedded [`details`] shortcode requires an opening and closing tag: - -```text -{{}} -This is a **bold** word. -{{}} -``` - -Some shortcodes do not accept content. For example, the embedded [`instagram`] shortcode requires a single _positional_ argument: - -```text -{{}} -``` - -Some shortcodes optionally accept content. For example, you can call the embedded [`qr`] shortcode with content: - -```text -{{}} -https://gohugo.io -{{}} -``` - -Or use the self-closing syntax with a trailing slash to pass the text as an argument: - -```text -{{}} -``` - -Refer to each shortcode's documentation for specific usage instructions and available arguments. - -### Arguments - -Shortcode arguments can be either _named_ or _positional_. - -Named arguments are passed as case-sensitive key-value pairs, as seen in this example with the embedded [`figure`] shortcode. The `src` argument, for instance, is required. - -```text -{{}} -``` - -Positional arguments, on the other hand, are determined by their position. The embedded `instagram` shortcode, for example, expects the first argument to be the Instagram post ID. - -```text -{{}} -``` - -Shortcode arguments are space-delimited, and arguments with internal spaces must be quoted. - -```text -{{}} -``` - -Shortcodes accept [scalar](g) arguments, one of [string](g), [integer](g), [floating point](g), or [boolean](g). - -```text -{{}} -``` - -You can optionally use multiple lines when providing several arguments to a shortcode for better readability: - -```text -{{}} -``` - -Use a [raw string literal](g) if you need to pass a multiline string: - -```text -{{HTML, -and a new line with a "quoted string".` */>}} -``` - -Shortcodes can accept named arguments, positional arguments, or both, but you must use either named or positional arguments exclusively within a single shortcode call; mixing them is not allowed. - -Refer to each shortcode's documentation for specific usage instructions and available arguments. - -### Notation - -Shortcodes can be called using two different notations, distinguished by their tag delimiters. - -Notation|Example -:--|:-- -Markdown|`{{%/* foo */%}} ## Section 1 {{%/* /foo */%}}` -Standard|`{{}} ## Section 2 {{}}` - -#### Markdown notation - -Hugo processes the shortcode before the page content is rendered by the Markdown renderer. This means, for instance, that Markdown headings inside a Markdown-notation shortcode will be included when invoking the [`TableOfContents`] method on the `Page` object. - -#### Standard notation - -With standard notation, Hugo processes the shortcode separately, merging the output into the page content after Markdown rendering. This means, for instance, that Markdown headings inside a standard-notation shortcode will be excluded when invoking the `TableOfContents` method on the `Page` object. - -By way of example, with this shortcode template: - -```go-html-template {file="layouts/shortcodes/foo.html"} -{{ .Inner }} -``` - -And this markdown: - -```text {file="content/example.md"} -{{%/* foo */%}} ## Section 1 {{%/* /foo */%}} - -{{}} ## Section 2 {{}} -``` - -Hugo renders this HTML: - -```html -

    Section 1

    - -## Section 2 -``` - -In the above, "Section 1" will be included when invoking the `TableOfContents` method, while "Section 2" will not. - -The shortcode author determines which notation to use. Consult each shortcode's documentation for specific usage instructions and available arguments. - -## Nesting - -Shortcodes (excluding [inline](#inline) shortcodes) can be nested, creating parent-child relationships. For example, a gallery shortcode might contain several image shortcodes: - -```text {file="content/example.md"} -{{}} - {{}} - {{}} - {{}} -{{}} -``` - -The [shortcode templates][nesting] section provides a detailed explanation and examples. - -[`details`]: /shortcodes/details -[`figure`]: /shortcodes/figure -[`instagram`]: /shortcodes/instagram -[`qr`]: /shortcodes/qr -[`TableOfContents`]: /methods/page/tableofcontents/ -[layout string]: /functions/time/format/#layout-string -[nesting]: /templates/shortcode/#nesting -[shortcode method]: /templates/shortcode/#methods -[shortcode templates]: /templates/shortcode/ diff --git a/docs/content/en/content-management/summaries.md b/docs/content/en/content-management/summaries.md deleted file mode 100644 index da61c2c8e..000000000 --- a/docs/content/en/content-management/summaries.md +++ /dev/null @@ -1,123 +0,0 @@ ---- -title: Content summaries -linkTitle: Summaries -description: Create and render content summaries. -categories: [] -keywords: [] -aliases: [/content/summaries/,/content-management/content-summaries/] ---- - - - - - - -You can define a summary manually, in front matter, or automatically. A manual summary takes precedence over a front matter summary, and a front matter summary takes precedence over an automatic summary. - -Review the [comparison table](#comparison) below to understand the characteristics of each summary type. - -## Manual summary - -Use a `` divider to indicate the end of the summary. Hugo will not render the summary divider itself. - -```text {file="content/example.md"} -+++ -title: 'Example' -date: 2024-05-26T09:10:33-07:00 -+++ - -This is the first paragraph. - - - -This is the second paragraph. -``` - -When using the Emacs Org Mode [content format], use a `# more` divider to indicate the end of the summary. - -[content format]: /content-management/formats/ - -## Front matter summary - -Use front matter to define a summary independent of content. - -```text {file="content/example.md"} -+++ -title: 'Example' -date: 2024-05-26T09:10:33-07:00 -summary: 'This summary is independent of the content.' -+++ - -This is the first paragraph. - -This is the second paragraph. -``` - -## Automatic summary - -If you do not define the summary manually or in front matter, Hugo automatically defines the summary based on the [`summaryLength`] in your site configuration. - -[`summaryLength`]: /configuration/all/#summarylength - -```text {file="content/example.md"} -+++ -title: 'Example' -date: 2024-05-26T09:10:33-07:00 -+++ - -This is the first paragraph. - -This is the second paragraph. - -This is the third paragraph. -``` - -For example, with a `summaryLength` of 7, the automatic summary will be: - -```html -

    This is the first paragraph.

    -

    This is the second paragraph.

    -``` - -## Comparison - -Each summary type has different characteristics: - -Type|Precedence|Renders markdown|Renders shortcodes|Wraps single lines with `

    ` -:--|:-:|:-:|:-:|:-: -Manual|1|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark: -Front matter|2|:heavy_check_mark:|:x:|:x: -Automatic|3|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark: - -## Rendering - -Render the summary in a template by calling the [`Summary`] method on a `Page` object. - -[`Summary`]: /methods/page/summary - -```go-html-template -{{ range site.RegularPages }} -

    {{ .LinkTitle }}

    -
    - {{ .Summary }} - {{ if .Truncated }} - More ... - {{ end }} -
    -{{ end }} -``` - -## Alternative - -Instead of calling the `Summary` method on a `Page` object, use the [`strings.Truncate`] function for granular control of the summary length. For example: - -[`strings.Truncate`]: /functions/strings/truncate/ - -```go-html-template -{{ range site.RegularPages }} -

    {{ .LinkTitle }}

    -
    - {{ .Content | strings.Truncate 42 }} -
    -{{ end }} -``` diff --git a/docs/content/en/content-management/syntax-highlighting.md b/docs/content/en/content-management/syntax-highlighting.md deleted file mode 100644 index 7e87efa49..000000000 --- a/docs/content/en/content-management/syntax-highlighting.md +++ /dev/null @@ -1,102 +0,0 @@ ---- -title: Syntax highlighting -description: Add syntax highlighting to code examples. -categories: [] -keywords: [highlight] -aliases: [/extras/highlighting/,/extras/highlight/,/tools/syntax-highlighting/] ---- - -Hugo provides several methods to add syntax highlighting to code examples: - -- Use the [`transform.Highlight`] function within your templates -- Use the [`highlight`] shortcode with any [content format](g) -- Use fenced code blocks with the Markdown content format - -[`transform.Highlight`]: /functions/transform/highlight/ -[`highlight`]: /shortcodes/highlight/ - -## Fenced code blocks - -In its default configuration, Hugo highlights code examples within fenced code blocks, following this form: - -````text {file="content/example.md"} -```LANG [OPTIONS] -CODE -``` -```` - -CODE -: The code to highlight. - -LANG -: The language of the code to highlight. Choose from one of the [supported languages]. This value is case-insensitive. - -OPTIONS -: One or more space-separated or comma-separated key-value pairs wrapped in braces. Set default values for each option in your [site configuration]. The key names are case-insensitive. - -[supported languages]: #languages -[site configuration]: /configuration/markup/#highlight - -For example, with this Markdown: - -````text {file="content/example.md"} -```go {linenos=inline hl_lines=[3,"6-8"] style=emacs} -package main - -import "fmt" - -func main() { - for i := 0; i < 3; i++ { - fmt.Println("Value of i:", i) - } -} -``` -```` - -Hugo renders this: - -```go {linenos=inline, hl_lines=[3, "6-8"], style=emacs} -package main - -import "fmt" - -func main() { - for i := 0; i < 3; i++ { - fmt.Println("Value of i:", i) - } -} -``` - -## Options - -{{% include "_common/syntax-highlighting-options.md" %}} - -## Escaping - -When documenting shortcode usage, escape the tag delimiters: - -````text {file="content/example.md"} -```text {linenos=inline} -{{}} - -{{%/*/* shortcode-2 */*/%}} -``` -```` - -Hugo renders this to: - -```text {linenos=inline} -{{}} - -{{%/* shortcode-2 */%}} -``` - -## Languages - -These are the supported languages. Use one of the identifiers, not the language name, when specifying a language for: - -- The [`transform.Highlight`] function -- The [`highlight`] shortcode -- Fenced code blocks - -{{< chroma-lexers >}} diff --git a/docs/content/en/content-management/taxonomies.md b/docs/content/en/content-management/taxonomies.md deleted file mode 100644 index e8ba04c28..000000000 --- a/docs/content/en/content-management/taxonomies.md +++ /dev/null @@ -1,125 +0,0 @@ ---- -title: Taxonomies -description: Hugo includes support for user-defined taxonomies. -categories: [] -keywords: [] -aliases: [/taxonomies/overview/,/taxonomies/usage/,/indexes/overview/,/doc/indexes/,/extras/indexes] ---- - -## What is a taxonomy? - -Hugo includes support for user-defined groupings of content called **taxonomies**. Taxonomies are classifications of logical relationships between content. - -### Definitions - -Taxonomy -: A categorization that can be used to classify content - -Term -: A key within the taxonomy - -Value -: A piece of content assigned to a term - -## Example taxonomy: movie website - -Let's assume you are making a website about movies. You may want to include the following taxonomies: - -- Actors -- Directors -- Studios -- Genre -- Year -- Awards - -Then, in each of the movies, you would specify terms for each of these taxonomies (i.e., in the [front matter] of each of your movie content files). From these terms, Hugo would automatically create pages for each Actor, Director, Studio, Genre, Year, and Award, with each listing all of the Movies that matched that specific Actor, Director, Studio, Genre, Year, and Award. - -### Movie taxonomy organization - -To continue with the example of a movie site, the following demonstrates content relationships from the perspective of the taxonomy: - -```txt -Actor <- Taxonomy - Bruce Willis <- Term - The Sixth Sense <- Value - Unbreakable <- Value - Moonrise Kingdom <- Value - Samuel L. Jackson <- Term - Unbreakable <- Value - The Avengers <- Value - xXx <- Value -``` - -From the perspective of the content, the relationships would appear differently, although the data and labels used are the same: - -```txt -Unbreakable <- Value - Actors <- Taxonomy - Bruce Willis <- Term - Samuel L. Jackson <- Term - Director <- Taxonomy - M. Night Shyamalan <- Term - ... -Moonrise Kingdom <- Value - Actors <- Taxonomy - Bruce Willis <- Term - Bill Murray <- Term - Director <- Taxonomy - Wes Anderson <- Term - ... -``` - -### Default destinations - -When taxonomies are used---and [taxonomy templates] are provided---Hugo will automatically create both a page listing all the taxonomy's terms and individual pages with lists of content associated with each term. For example, a `categories` taxonomy declared in your configuration and used in your content front matter will create the following pages: - -- A single page at `example.com/categories/` that lists all the terms within the taxonomy -- [Individual taxonomy list pages][taxonomy templates] (e.g., `/categories/development/`) for each of the terms that shows a listing of all pages marked as part of that taxonomy within any content file's [front matter] - -## Configuration - -See [configure taxonomies](/configuration/taxonomies/). - -## Assign terms to content - -To assign one or more terms to a page, create a front matter field using the plural name of the taxonomy, then add terms to the corresponding array. For example: - -{{< code-toggle file=content/example.md fm=true >}} -title = 'Example' -tags = ['Tag A','Tag B'] -categories = ['Category A','Category B'] -{{< /code-toggle >}} - -## Order taxonomies - -A content file can assign weight for each of its associate taxonomies. Taxonomic weight can be used for sorting or ordering content in [taxonomy templates] and is declared in a content file's [front matter]. The convention for declaring taxonomic weight is `taxonomyname_weight`. - -The following show a piece of content that has a weight of 22, which can be used for ordering purposes when rendering the pages assigned to the "a", "b" and "c" values of the `tags` taxonomy. It has also been assigned the weight of 44 when rendering the "d" category page. - -### Example: taxonomic `weight` - -{{< code-toggle file=hugo >}} -title = "foo" -tags = [ "a", "b", "c" ] -tags_weight = 22 -categories = ["d"] -categories_weight = 44 -{{}} - -By using taxonomic weight, the same piece of content can appear in different positions in different taxonomies. - -## Add custom metadata to a taxonomy or term - -If you need to add custom metadata to your taxonomy terms, you will need to create a page for that term at `/content///_index.md` and add your metadata in its front matter. Continuing with our 'Actors' example, let's say you want to add a Wikipedia page link to each actor. Your terms pages would be something like this: - -{{< code-toggle file=content/actors/bruce-willis/_index.md fm=true >}} -title: "Bruce Willis" -wikipedia: "https://en.wikipedia.org/wiki/Bruce_Willis" -{{< /code-toggle >}} - -[content section]: /content-management/sections/ -[content type]: /content-management/types/ -[documentation on archetypes]: /content-management/archetypes/ -[front matter]: /content-management/front-matter/ -[taxonomy templates]: /templates/types/#taxonomy -[site configuration]: /configuration/ diff --git a/docs/content/en/content-management/types.md b/docs/content/en/content-management/types.md deleted file mode 100644 index 08e9adda2..000000000 --- a/docs/content/en/content-management/types.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: Content types -description: Hugo is built around content organized in sections. -categories: [] -keywords: [] -aliases: [/content/types] ---- - -A **content type** is a way to organize your content. Hugo resolves the content type from either the `type` in front matter or, if not set, the first directory in the file path. E.g. `content/blog/my-first-event.md` will be of type `blog` if no `type` is set. - -A content type is used to - -- Determine how the content is rendered. See [Template Lookup Order](/templates/lookup-order/) and [Content Views](/templates/content-view) for more. -- Determine which [archetype](/content-management/archetypes/) template to use for new content. diff --git a/docs/content/en/content-management/urls.md b/docs/content/en/content-management/urls.md deleted file mode 100644 index 2630105e5..000000000 --- a/docs/content/en/content-management/urls.md +++ /dev/null @@ -1,265 +0,0 @@ ---- -title: URL management -description: Control the structure and appearance of URLs through front matter entries and settings in your site configuration. -categories: [] -keywords: [] -aliases: [/extras/permalinks/,/extras/aliases/,/extras/urls/,/doc/redirects/,/doc/alias/,/doc/aliases/] ---- - -## Overview - -By default, when Hugo renders a page, the resulting URL matches the file path within the `content` directory. For example: - -```text -content/posts/post-1.md → https://example.org/posts/post-1/ -``` - -You can change the structure and appearance of URLs with front matter values and site configuration options. - -## Front matter - -### `slug` - -Set the `slug` in front matter to override the last segment of the path. The `slug` value does not affect section pages. - -{{< code-toggle file=content/posts/post-1.md fm=true >}} -title = 'My First Post' -slug = 'my-first-post' -{{< /code-toggle >}} - -The resulting URL will be: - -```text -https://example.org/posts/my-first-post/ -``` - -### `url` - -Set the `url` in front matter to override the entire path. Use this with either regular pages or section pages. - -> [!note] -> Hugo does not sanitize the `url` front matter field, allowing you to generate: -> - File paths that contain characters reserved by the operating system. For example, file paths on Windows may not contain any of these [reserved characters]. Hugo throws an error if a file path includes a character reserved by the current operating system. -> - URLs that contain disallowed characters. For example, the less than sign (`<`) is not allowed in a URL. - -If you set both `slug` and `url` in front matter, the `url` value takes precedence. - -#### Include a colon - -{{< new-in 0.136.0 />}} - -If you need to include a colon in the `url` front matter field, escape it with backslash characters. Use one backslash if you wrap the string within single quotes, or use two backslashes if you wrap the string within double quotes. With YAML front matter, use a single backslash if you omit quotation marks. - -For example, with this front matter: - -{{< code-toggle file=content/example.md fm=true >}} -title: Example -url: "my\\:example" -{{< /code-toggle >}} - -The resulting URL will be: - -```text -https://example.org/my:example/ -``` - -As described above, this will fail on Windows because the colon (`:`) is a reserved character. - -#### File extensions - -With this front matter: - -{{< code-toggle file=content/posts/post-1.md fm=true >}} -title = 'My First Article' -url = 'articles/my-first-article' -{{< /code-toggle >}} - -The resulting URL will be: - -```text -https://example.org/articles/my-first-article/ -``` - -If you include a file extension: - -{{< code-toggle file=content/posts/post-1.md fm=true >}} -title = 'My First Article' -url = 'articles/my-first-article.html' -{{< /code-toggle >}} - -The resulting URL will be: - -```text -https://example.org/articles/my-first-article.html -``` - -#### Leading slashes - -With monolingual sites, `url` values with or without a leading slash are relative to the [`baseURL`]. With multilingual sites, `url` values with a leading slash are relative to the `baseURL`, and `url` values without a leading slash are relative to the `baseURL` plus the language prefix. - -Site type|Front matter `url`|Resulting URL -:--|:--|:-- -monolingual|`/about`|`https://example.org/about/` -monolingual|`about`|`https://example.org/about/` -multilingual|`/about`|`https://example.org/about/` -multilingual|`about`|`https://example.org/de/about/` - -#### Permalinks tokens in front matter - -{{< new-in 0.131.0 />}} - -You can also usetokens when setting the `url` value. This is typically used in `cascade` sections: - -{{< code-toggle file=content/foo/bar/_index.md fm=true >}} -title ="Bar" -[[cascade]] - url = "/:sections[last]/:slug" -{{< /code-toggle >}} - -Use any of these tokens: - -{{% include "/_common/permalink-tokens.md" %}} - -## Site configuration - -### Permalinks - -See [configure permalinks](/configuration/permalinks). - -### Appearance - -See [configure ugly URLs](/configuration/ugly-urls/). - -### Post-processing - -Hugo provides two mutually exclusive configuration options to alter URLs _after_ it renders a page. - -#### Canonical URLs - -> [!caution] -> This is a legacy configuration option, superseded by template functions and Markdown render hooks, and will likely be [removed in a future release]. -{class="!mt-6"} - -If enabled, Hugo performs a search and replace _after_ it renders the page. It searches for site-relative URLs (those with a leading slash) associated with `action`, `href`, `src`, `srcset`, and `url` attributes. It then prepends the `baseURL` to create absolute URLs. - -```html - - -``` - -This is an imperfect, brute force approach that can affect content as well as HTML attributes. As noted above, this is a legacy configuration option that will likely be removed in a future release. - -To enable: - -{{< code-toggle file=hugo >}} -canonifyURLs = true -{{< /code-toggle >}} - -#### Relative URLs - -> [!caution] -> Do not enable this option unless you are creating a serverless site, navigable via the file system. -{class="!mt-6"} - -If enabled, Hugo performs a search and replace _after_ it renders the page. It searches for site-relative URLs (those with a leading slash) associated with `action`, `href`, `src`, `srcset`, and `url` attributes. It then transforms the URL to be relative to the current page. - -For example, when rendering `content/posts/post-1`: - -```html - - -``` - -This is an imperfect, brute force approach that can affect content as well as HTML attributes. As noted above, do not enable this option unless you are creating a serverless site. - -To enable: - -{{< code-toggle file=hugo >}} -relativeURLs = true -{{< /code-toggle >}} - -## Aliases - -Create redirects from old URLs to new URLs with aliases: - -- An alias with a leading slash is relative to the `baseURL` -- An alias without a leading slash is relative to the current directory - -### Examples {#alias-examples} - -Change the file name of an existing page, and create an alias from the previous URL to the new URL: - -{{< code-toggle file=content/posts/new-file-name.md fm=true >}} -aliases = ['/posts/previous-file-name'] -{{< /code-toggle >}} - -Each of these directory-relative aliases is equivalent to the site-relative alias above: - -- `previous-file-name` -- `./previous-file-name` -- `../posts/previous-file-name` - -You can create more than one alias to the current page: - -{{< code-toggle file=content/posts/new-file-name.md fm=true >}} -aliases = ['previous-file-name','original-file-name'] -{{< /code-toggle >}} - -In a multilingual site, use a directory-relative alias, or include the language prefix with a site-relative alias: - -{{< code-toggle file=content/posts/new-file-name.de.md fm=true >}} -aliases = ['/de/posts/previous-file-name'] -{{< /code-toggle >}} - -### How aliases work - -Using the first example above, Hugo generates the following site structure: - -```text -public/ -├── posts/ -│ ├── new-file-name/ -│ │ └── index.html -│ ├── previous-file-name/ -│ │ └── index.html -│ └── index.html -└── index.html -``` - -The alias from the previous URL to the new URL is a client-side redirect: - -```html {file="posts/previous-file-name/index.html"} - - - - https://example.org/posts/new-file-name/ - - - - - - -``` - -Collectively, the elements in the `head` section: - -- Tell search engines that the new URL is canonical -- Tell search engines not to index the previous URL -- Tell the browser to redirect to the new URL - -Hugo renders alias files before rendering pages. A new page with the previous file name will overwrite the alias, as expected. - -### Customize - -To override Hugo's embedded `alias` template, copy the [source code] to a file with the same name in the `layouts` directory. The template receives the following context: - -Permalink -: The link to the page being aliased. - -Page -: The Page data for the page being aliased. - -[`baseURL`]: /configuration/all/#baseurl -[removed in a future release]: https://github.com/gohugoio/hugo/issues/4733 -[reserved characters]: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions -[source code]: {{% eturl alias %}} diff --git a/docs/content/en/contribute/_index.md b/docs/content/en/contribute/_index.md deleted file mode 100644 index d0ae954b0..000000000 --- a/docs/content/en/contribute/_index.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: Contribute to the Hugo project -linkTitle: Contribute -description: Contribute to development, documentation, and themes. -categories: [] -keywords: [] -weight: 10 -aliases: [/tutorials/how-to-contribute-to-hugo/,/community/contributing/] ---- diff --git a/docs/content/en/contribute/development.md b/docs/content/en/contribute/development.md deleted file mode 100644 index 78ca5ec5c..000000000 --- a/docs/content/en/contribute/development.md +++ /dev/null @@ -1,186 +0,0 @@ ---- -title: Development -description: Contribute to the development of Hugo. -categories: [] -keywords: [] ---- - -## Introduction - -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]. - -## Prerequisites - -To build the extended or extended/deploy edition from source you must: - -1. Install [Git] -1. Install [Go] version 1.23.0 or later -1. Install a C compiler, either [GCC] or [Clang] -1. Update your `PATH` environment variable as described in the [Go documentation] - -> [!note] -> See these [detailed instructions](https://discourse.gohugo.io/t/41370) to install GCC on Windows. - -## GitHub workflow - -> [!note] -> This section assumes that you have a working knowledge of Go, Git and GitHub, and are comfortable working on the command line. - -Use this workflow to create and submit pull requests. - -### Step 1 - -Fork the [project repository]. - -### Step 2 - -Clone your fork. - -### Step 3 - -Create a new branch with a descriptive name that includes the corresponding issue number. - -For a new feature: - -```sh -git checkout -b feat/implement-some-feature-99999 -``` - -For a bug fix: - -```sh -git checkout -b fix/fix-some-bug-99999 -``` - -### Step 4 - -Make changes. - -### Step 5 - -Compile and install. - -To compile and install the standard edition: - -```text -go install -``` - -To compile and install the extended edition: - -```text -CGO_ENABLED=1 go install -tags extended -``` - -To compile and install the extended/deploy edition: - -```text -CGO_ENABLED=1 go install -tags extended,withdeploy -``` - -### Step 6 - -Test your changes: - -```text -go test ./... -``` - -### Step 7 - -Commit your changes with a descriptive commit message: - -- Provide a summary on the first line, typically 50 characters or less, followed by a blank line. - - Begin the summary with one of content, theme, config, all, or misc, followed by a colon, a space, and a brief description of the change beginning with a capital letter - - Use imperative present tense - - See the [commit message guidelines] for requirements -- Optionally, provide a detailed description where each line is 72 characters or less, followed by a blank line. -- Add one or more "Fixes" or "Closes" keywords, each on its own line, referencing the [issues] addressed by this change. - -For example: - -```sh -git commit -m "tpl/strings: Create wrap function - -The strings.Wrap function wraps a string into one or more lines, -splitting the string after the given number of characters, but not -splitting in the middle of a word. - -Fixes #99998 -Closes #99999" -``` - -### Step 8 - -Push the new branch to your fork of the documentation repository. - -### Step 9 - -Visit the [project repository] and create a pull request (PR). - -### Step 10 - -A project maintainer will review your PR and may request changes. You may delete your branch after the maintainer merges your PR. - -## Building from source - -You can build, install, and test Hugo at any point in its development history. The examples below build and install the extended edition of Hugo. - -To build and install the latest release: - -```sh -CGO_ENABLED=1 go install -tags extended github.com/gohugoio/hugo@latest -``` - -To build and install a specific release: - -```sh -CGO_ENABLED=1 go install -tags extended github.com/gohugoio/hugo@v0.144.2 -``` - -To build and install at the latest commit on the master branch: - -```sh -CGO_ENABLED=1 go install -tags extended github.com/gohugoio/hugo@master -``` - -To build and install at a specific commit: - -```sh -CGO_ENABLED=1 go install -tags extended github.com/gohugoio/hugo@0851c17 -``` - -[bugs]: https://github.com/gohugoio/hugo/issues?q=is%3Aopen+is%3Aissue+label%3ABug -[Clang]: https://clang.llvm.org/ -[commit message guidelines]: https://github.com/gohugoio/hugo/blob/master/CONTRIBUTING.md#git-commit-message-guidelines -[Contribution Guide]: https://github.com/gohugoio/hugo/blob/master/CONTRIBUTING.md -[create a proposal]: https://github.com/gohugoio/hugo/issues/new?labels=Proposal%2C+NeedsTriage&template=feature_request.md -[documentation]: /documentation -[documentation repository]: https://github.com/gohugoio/hugoDocs -[forum]: https://discourse.gohugo.io -[GCC]: https://gcc.gnu.org/ -[Git]: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git -[Go]: https://go.dev/doc/install -[Go documentation]: https://go.dev/doc/code#Command -[issue queue]: https://github.com/gohugoio/hugo/issues -[issues]: https://github.com/gohugoio/hugo/issues -[project repository]: https://github.com/gohugoio/hugo/ -[themes]: https://themes.gohugo.io/ diff --git a/docs/content/en/contribute/documentation.md b/docs/content/en/contribute/documentation.md deleted file mode 100644 index 68129912a..000000000 --- a/docs/content/en/contribute/documentation.md +++ /dev/null @@ -1,530 +0,0 @@ ---- -title: Documentation -description: Help us to improve the documentation by identifying issues and suggesting changes. -categories: [] -keywords: [] -aliases: [/contribute/docs/] ---- - -## Introduction - -We welcome corrections and improvements to the documentation. The documentation lives in a separate repository from the main project. To contribute: - -- For corrections and improvements to existing documentation, submit issues and pull requests to the [documentation repository]. -- For documentation of new features, include the documentation changes in your pull request to the [project repository]. - -## Guidelines - -### Style - -Follow Google's [developer documentation style guide]. - -### Markdown - -Adhere to these Markdown conventions: - -- Use [ATX] headings (levels 2-4), not [setext] headings. -- Use [fenced code blocks], not [indented code blocks]. -- Use hyphens, not asterisks, for unordered [list items]. -- Use [callouts](#callouts) instead of bold text for emphasis. -- Do not mix [raw HTML] within Markdown. -- Do not use bold text in place of a heading or description term (`dt`). -- Remove consecutive blank lines. -- Remove trailing spaces. - -### Glossary - -[Glossary] terms are defined on individual pages, providing a central repository for definitions, though these pages are not directly linked from the site. - -Definitions must be complete sentences, with the first sentence defining the term. Italicize the first occurrence of the term and any referenced glossary terms for consistency. - -Link to glossary terms using this syntax: `[term](g)` - -Term lookups are case-insensitive, ignore formatting, and support singular and plural forms. For example, all of these variations will link to the same glossary term: - -```text -[global resource](g) -[Global Resource](g) -[Global Resources](g) -[`Global Resources`](g) -``` - -Use the [glossary-term shortcode](#glossary-term) to insert a term definition: - -```text -{{%/* glossary-term "global resource" */%}} -``` - -### Terminology - -Link to the [glossary] as needed and use terms consistently. Pay particular attention to: - -- "front matter" (two words, except when referring to the configuration key) -- "home page" (two words) -- "website" (one word) -- "standalone" (one word, no hyphen) -- "map" (instead of "dictionary") -- "flag" (instead of "option" for command-line flags) -- "client side" (noun), "client-side" (adjective) -- "server side" (noun), "server-side" (adjective) -- "Markdown" (capitalized) -- "open-source" (hyphenated adjective) - -### Titles and headings - -- Use sentence-style capitalization. -- Avoid formatted strings. -- Keep them concise. - -### Page descriptions - -When writing the page `description` use imperative present tense when possible. For example: - -{{< code-toggle file=content/en/functions/data/_index.md" fm=true >}} -title: Data functions -linkTitle: data -description: Use these functions to read local or remote data files. -{{< /code-toggle >}} - -### Writing style - -Use active voice and present tense wherever possible. - -No → With Hugo you can build a static site.\ -Yes → Build a static site with Hugo. - -No → This will cause Hugo to generate HTML files in the `public` directory.\ -Yes → Hugo generates HTML files in the `public` directory. - -Use second person instead of third person. - -No → Users should exercise caution when deleting files.\ -Better → You must be cautious when deleting files.\ -Best → Be cautious when deleting files. - -Minimize adverbs. - -No → Hugo is extremely fast.\ -Yes → Hugo is fast. - -> [!note] -> "It's an adverb, Sam. It's a lazy tool of a weak mind." (Outbreak, 1995). - -### Function and method descriptions - -Start descriptions in the functions and methods sections with "Returns", or for boolean values, "Reports whether". - -### File paths and names - -Enclose directory names, file names, and file paths in backticks, except when used in: - -- Page titles -- Section headings (h1-h6) -- Definition list terms -- The `description` field in front matter - -### Miscellaneous - -Other best practices: - -- Introduce lists with a sentence or phrase, not directly under a heading. -- Avoid bold text; use [callouts](#callouts) for emphasis. -- Do not put description terms (`dt`) in backticks unless syntactically necessary. -- Do not use Hugo's `ref` or `relref` shortcodes. -- Prioritize current best practices over multiple options or historical information. -- Use short, focused code examples. -- Use [basic english] where possible for a global audience. - -## Front matter fields - -This site uses the front matter fields listed in the table below. - -Of the four required fields, only `title` and `description` require data. - -```text -title: The title -description: The description -categories: [] -keywords: [] -``` - -This example demonstrates the minimum required front matter fields. - -If quotation marks are required, prefer single quotes to double quotes when possible. - -Seq|Field|Description|Required ---:|:--|:--|:-- -1|`title`|The page title|:heavy_check_mark:| -2|`linkTitle`|A short version of the page title|| -3|`description`|A complete sentence describing the page|:heavy_check_mark:| -4|`categories`|An array of terms in the categories taxonomy|:heavy_check_mark: [^1]| -5|`keywords`|An array of keywords used to identify related content|:heavy_check_mark: [^1]| -6|`publishDate`|Applicable to news items: the publication date|| -7|`params.altTitle`|An alternate title: used in the "see also" panel if provided|| -8|`params.functions_and_methods.aliases`|Applicable to function and method pages: an array of alias names|| -9|`params.functions_and_methods.returnType`|Applicable to function and method pages: the data type returned|| -10|`params.functions_and_methods.signatures`|Applicable to function and method pages: an array of signatures|| -11|`params.hide_in_this_section`|Whether to hide the "in this section" panel|| -12|`params.minversion`|Applicable to the quick start page: the minimum Hugo version required|| -13|`params.permalink`|Reserved for use by the news content adapter|| -14|`params.reference (used in glossary term)`|Applicable to glossary entries: a URL for additional information|| -15|`params.show_publish_date`|Whether to show the `publishDate` when rendering the page|| -16|`weight`|The page weight|| -17|`aliases`|Previous URLs used to access this page|| -18|`expirydate`|The expiration date|| - -[^1]: The field is required, but its data is not. - -## Related content - -When available, the "See also" sidebar displays related pages using Hugo's [related content] feature, based on front matter keywords. We ensure consistent keyword usage by validating them against `data/keywords.yaml` during the build process. If a keyword is not found, you'll be alerted and must either modify the keyword or update the data file. This validation process helps to refine the related content for better results. - -If the title in the "See also" sidebar is ambiguous or the same as another page, you can define an alternate title in the front matter: - -{{< code-toggle file=hugo >}} -title = "Long descriptive title" -linkTitle = "Short title" -[params] -altTitle = "Whatever you want" -{{< /code-toggle >}} - -Use of the alternate title is limited to the "See also" sidebar. - -> [!note] -> Think carefully before setting the `altTitle`. Use it only when absolutely necessary. - -## Code examples - -With examples of template code: - -- Indent with two spaces. -- Insert a space after an opening action delimiter. -- Insert a space before a closing action delimiter. -- Do not add white space removal syntax to action delimiters unless required. For example, inline elements like `img` and `a` require whitespace removal on both sides. - -```go-html-template -{{ if eq $foo $bar }} - {{ fmt.Printf "%s is %s" $foo $bar }} -{{ end }} -``` - -### Fenced code blocks - -Always specify the language. - -When providing a Mardown example, set the code language to "text" to prevent -erroneous lexing/highlighting of shortcode calls. - -````text -```go-html-template -{{ if eq $foo "bar" }} - {{ print "foo is bar" }} -{{ end }} -``` -```` - -To include a filename header and copy-to-clipboard button: - -````text -```go-html-template {file="layouts/partials/foo.html" copy=true} -{{ if eq $foo "bar" }} - {{ print "foo is bar" }} -{{ end }} -``` -```` - -To wrap the code block within an initially-opened `details` element using a non-default summary: - -````text -```go-html-template {details=true open=true summary="layouts/partials/foo.html" copy=true} -{{ if eq $foo "bar" }} - {{ print "foo is bar" }} -{{ end }} -``` -```` - -### Shortcode calls - -Use this syntax : - -````text -```text -{{}} -{{%/*/* foo */*/%}} -``` -```` - -### Site configuration - -Use the [code-toggle shortcode](#code-toggle) to include site configuration examples: - -```text -{{}} -baseURL = 'https://example.org/' -languageCode = 'en-US' -title = 'My Site' -{{}} -``` - -### Front matter - -Use the [code-toggle shortcode](#code-toggle) to include front matter examples: - -```text -{{}} -title = 'My first post' -date = 2023-11-09T12:56:07-08:00 -draft = false -{{}} -``` - -## Callouts - -To visually emphasize important information, use callouts (admonitions). Callout types are case-insensitive. Effective March 8, 2025, we utilize only three of the five available types. - -- note (272 instances) -- warning (2 instances) -- caution (1 instance) - -Limiting the number of callout types helps us to use them consistently. - -```text -> [!note] -> Useful information that users should know, even when skimming content. -``` - -> [!note] -> Useful information that users should know, even when skimming content. - -```text -> [!warning] -> Urgent info that needs immediate user attention to avoid problems. -``` - -> [!warning] -> Urgent info that needs immediate user attention to avoid problems. - -```text -> [!caution] -> Advises about risks or negative outcomes of certain actions. -``` - -> [!caution] -> Advises about risks or negative outcomes of certain actions. - -```text -> [!tip] -> Helpful advice for doing things better or more easily. -``` - -> [!tip] -> Helpful advice for doing things better or more easily. - -```text -> [!important] -> Key information users need to know to achieve their goal. -``` - -> [!important] -> Key information users need to know to achieve their goal. - - - -## Shortcodes - -These shortcodes are commonly used throughout the documentation. Other shortcodes are available for specialized use. - -### code-toggle - -Use the `code-toggle` shortcode to display examples of site configuration, front matter, or data files. This shortcode takes these arguments: - -config -: (`string`) The section of `site.Data.docs.config` to render. - -copy -: (`bool`) Whether to display a copy-to-clipboard button. Default is `false`. - -datakey: -: (`string`) The section of `site.Data.docs` to render. - -file -: (`string`) The file name to display above the rendered code. Omit the file extension for site configuration examples. - -fm -: (`bool`) Whether to render the code as front matter. Default is `false`. - -skipHeader -: (`bool`) Whether to omit top-level key(s) when rendering a section of `site.Data.docs.config`. - -```text -{{}} -baseURL = 'https://example.org/' -languageCode = 'en-US' -title = 'My Site' -{{}} -``` - -### deprecated-in - -Use the `deprecated-in` shortcode to indicate that a feature is deprecated: - -```text -{{}} - -Use [`hugo.IsServer`] instead. - -[`hugo.IsServer`]: /functions/hugo/isserver/ -{{}} -``` - -### eturl - -Use the embedded template URL (`eturl`) shortcode to insert an absolute URL to the source code for an embedded template. The shortcode takes a single argument, the base file name of the template (omit the file extension). - -```text -This is a link to the [embedded alias template]. - -[embedded alias template]: {{%/* eturl alias */%}} -``` - -### glossary-term - -Use the `glossary-term` shortcode to insert the definition of the given glossary term. - -```text -{{%/* glossary-term scalar */%}} -``` - -### include - -Use the `include` shortcode to include content from another page. - -```text -{{%/* include "_common/glob-patterns.md" */%}} -``` - -### new-in - -Use the `new-in` shortcode to indicate a new feature: - -```text -{{}} -``` - -You can also include details: - -```text -{{}} -This is a new feature. -{{}} -``` - -## New features - -Use the [new-in shortcode](#new-in) to indicate a new feature: - -```text -{{}} -``` - -The "new in" label will be hidden if the specified version is older than a predefined threshold, based on differences in major and minor versions. See [details](https://github.com/gohugoio/hugoDocs/blob/master/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/new-in.html). - -## Deprecated features - -Use the [deprecated-in shorcode](#deprecated-in) shortcode to indicate that a feature is deprecated: - -```text -{{}} -Use [`hugo.IsServer`] instead. - -[`hugo.IsServer`]: /functions/hugo/isserver/ -{{}} -``` - -When deprecating a function or method, add something like this to front matter: - -{{< code-toggle file=content/something/foo.md fm=true >}} -expiryDate: 2027-02-17 # deprecated 2025-02-17 in v0.144.0 -{{< /code-toggle >}} - -Set the `expiryDate` to two years from the date of deprecation, and add a brief front matter comment to explain the setting. - -## GitHub workflow - -> [!note] -> This section assumes that you have a working knowledge of Git and GitHub, and are comfortable working on the command line. - -Use this workflow to create and submit pull requests. - -### Step 1 - -Fork the [documentation repository]. - -### Step 2 - -Clone your fork. - -### Step 3 - -Create a new branch with a descriptive name that includes the corresponding issue number, if any: - -```sh -git checkout -b restructure-foo-page-99999 -``` - -### Step 4 - -Make changes. - -### Step 5 - -Build the site locally to preview your changes. - -### Step 6 - -Commit your changes with a descriptive commit message: - -- Provide a summary on the first line, typically 50 characters or less, followed by a blank line. - - Begin the summary with one of `content`, `theme`, `config`, `all`, or `misc`, followed by a colon, a space, and a brief description of the change beginning with a capital letter - - Use imperative present tense -- Optionally, provide a detailed description where each line is 72 characters or less, followed by a blank line. -- Optionally, add one or more "Fixes" or "Closes" keywords, each on its own line, referencing the [issues] addressed by this change. - -For example: - -```text -git commit -m "content: Restructure the taxonomy page - -This restructures the taxonomy page by splitting topics into logical -sections, each with one or more examples. - -Fixes #9999 -Closes #9998" -``` - -### Step 7 - -Push the new branch to your fork of the documentation repository. - -### Step 8 - -Visit the [documentation repository] and create a pull request (PR). - -### Step 9 - -A project maintainer will review your PR and may request changes. You may delete your branch after the maintainer merges your PR. - -[ATX]: https://spec.commonmark.org/0.30/#atx-headings -[basic english]: https://simple.wikipedia.org/wiki/Basic_English -[basic english]: https://simple.wikipedia.org/wiki/Basic_English -[developer documentation style guide]: https://developers.google.com/style -[documentation repository]: https://github.com/gohugoio/hugoDocs/ -[fenced code blocks]: https://spec.commonmark.org/0.30/#fenced-code-blocks -[glossary]: /quick-reference/glossary/ -[indented code blocks]: https://spec.commonmark.org/0.30/#indented-code-blocks -[issues]: https://github.com/gohugoio/hugoDocs/issues -[list items]: https://spec.commonmark.org/0.30/#list-items -[project repository]: https://github.com/gohugoio/hugo -[raw HTML]: https://spec.commonmark.org/0.30/#raw-html -[related content]: /content-management/related-content/ -[setext]: https://spec.commonmark.org/0.30/#setext-heading diff --git a/docs/content/en/contribute/themes.md b/docs/content/en/contribute/themes.md deleted file mode 100644 index 8a3457ba3..000000000 --- a/docs/content/en/contribute/themes.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: Themes -description: If you've built a Hugo theme and want to contribute back to the Hugo Community, please share it with us. -categories: [] -keywords: [] -aliases: [/contribute/theme/] ---- - -Visit [themes.gohugo.io] to browse a collection of themes created by the Hugo community. - -To submit your theme: - -1. Read the [submission guidelines] -1. Open a pull request in the [themes repository] - -Other useful theme directories: - -- [jamstack.club] -- [jamstackthemes.dev] - -[jamstack.club]: https://jamstack.club/#ssg=hugo -[jamstackthemes.dev]: https://jamstackthemes.dev/ssg/hugo -[submission guidelines]: https://github.com/gohugoio/hugoThemesSiteBuilder/tree/main#readme -[themes repository]: https://github.com/gohugoio/hugoThemesSiteBuilder -[themes.gohugo.io]: https://themes.gohugo.io/ diff --git a/docs/content/en/documentation.md b/docs/content/en/documentation.md deleted file mode 100644 index 6f96c1f9c..000000000 --- a/docs/content/en/documentation.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -title: Hugo Documentation -linkTitle: Docs -description: Hugo is the world's fastest static website engine. It's written in Go (aka Golang) and developed by bep, spf13 and friends. -layout: list ---- - - diff --git a/docs/content/en/featured.png b/docs/content/en/featured.png deleted file mode 100644 index 09953aed9..000000000 Binary files a/docs/content/en/featured.png and /dev/null differ diff --git a/docs/content/en/functions/_index.md b/docs/content/en/functions/_index.md deleted file mode 100644 index d3081210b..000000000 --- a/docs/content/en/functions/_index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Functions -description: Use these functions within your templates and archetypes. -categories: [] -keywords: [] -weight: 10 -aliases: [/layout/functions/,/templates/functions] ---- diff --git a/docs/content/en/functions/cast/ToFloat.md b/docs/content/en/functions/cast/ToFloat.md deleted file mode 100644 index 572042937..000000000 --- a/docs/content/en/functions/cast/ToFloat.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: cast.ToFloat -description: Converts a value to a decimal floating-point number (base 10). -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [float] - returnType: float64 - signatures: [cast.ToFloat INPUT] -aliases: [/functions/float] ---- - -With a decimal (base 10) input: - -```go-html-template -{{ float 11 }} → 11 (float64) -{{ float "11" }} → 11 (float64) - -{{ float 11.1 }} → 11.1 (float64) -{{ float "11.1" }} → 11.1 (float64) - -{{ float 11.9 }} → 11.9 (float64) -{{ float "11.9" }} → 11.9 (float64) -``` - -With a binary (base 2) input: - -```go-html-template -{{ float 0b11 }} → 3 (float64) -``` - -With an octal (base 8) input (use either notation): - -```go-html-template -{{ float 011 }} → 9 (float64) -{{ float "011" }} → 11 (float64) - -{{ float 0o11 }} → 9 (float64) -``` - -With a hexadecimal (base 16) input: - -```go-html-template -{{ float 0x11 }} → 17 (float64) -``` diff --git a/docs/content/en/functions/cast/ToInt.md b/docs/content/en/functions/cast/ToInt.md deleted file mode 100644 index 4ede69229..000000000 --- a/docs/content/en/functions/cast/ToInt.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: cast.ToInt -description: Converts a value to a decimal integer (base 10). -keywords: [] -params: - functions_and_methods: - aliases: [int] - returnType: int - signatures: [cast.ToInt INPUT] -aliases: [/functions/int] ---- - -With a decimal (base 10) input: - -```go-html-template -{{ int 11 }} → 11 (int) -{{ int "11" }} → 11 (int) - -{{ int 11.1 }} → 11 (int) -{{ int 11.9 }} → 11 (int) -``` - -With a binary (base 2) input: - -```go-html-template -{{ int 0b11 }} → 3 (int) -{{ int "0b11" }} → 3 (int) -``` - -With an octal (base 8) input (use either notation): - -```go-html-template -{{ int 011 }} → 9 (int) -{{ int "011" }} → 9 (int) - -{{ int 0o11 }} → 9 (int) -{{ int "0o11" }} → 9 (int) -``` - -With a hexadecimal (base 16) input: - -```go-html-template -{{ int 0x11 }} → 17 (int) -{{ int "0x11" }} → 17 (int) -``` - -> [!note] -> Values with a leading zero are octal (base 8). When casting a string representation of a decimal (base 10) number, remove leading zeros: - -`{{ strings.TrimLeft "0" "0011" | int }} → 11` diff --git a/docs/content/en/functions/cast/ToString.md b/docs/content/en/functions/cast/ToString.md deleted file mode 100644 index 1bba001c9..000000000 --- a/docs/content/en/functions/cast/ToString.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: cast.ToString -description: Converts a value to a string. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [string] - returnType: string - signatures: [cast.ToString INPUT] -aliases: [/functions/string] ---- - -With a decimal (base 10) input: - -```go-html-template -{{ string 11 }} → 11 (string) -{{ string "11" }} → 11 (string) - -{{ string 11.1 }} → 11.1 (string) -{{ string "11.1" }} → 11.1 (string) - -{{ string 11.9 }} → 11.9 (string) -{{ string "11.9" }} → 11.9 (string) -``` - -With a binary (base 2) input: - -```go-html-template -{{ string 0b11 }} → 3 (string) -{{ string "0b11" }} → 0b11 (string) -``` - -With an octal (base 8) input (use either notation): - -```go-html-template -{{ string 011 }} → 9 (string) -{{ string "011" }} → 011 (string) - -{{ string 0o11 }} → 9 (string) -{{ string "0o11" }} → 0o11 (string) -``` - -With a hexadecimal (base 16) input: - -```go-html-template -{{ string 0x11 }} → 17 (string) -{{ string "0x11" }} → 0x11 (string) -``` diff --git a/docs/content/en/functions/cast/_index.md b/docs/content/en/functions/cast/_index.md deleted file mode 100644 index 1584ab159..000000000 --- a/docs/content/en/functions/cast/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Cast functions -linkTitle: cast -description: Use these functions to cast a value from one data type to another. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/functions/collections/After.md b/docs/content/en/functions/collections/After.md deleted file mode 100644 index c8a822846..000000000 --- a/docs/content/en/functions/collections/After.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: collections.After -description: Slices an array to the items after the Nth item. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [after] - returnType: any - signatures: [collections.After INDEX COLLECTION] -aliases: [/functions/after] ---- - -The following shows `after` being used in conjunction with the [`slice`]function: - -```go-html-template -{{ $data := slice "one" "two" "three" "four" }} -
      - {{ range after 2 $data }} -
    • {{ . }}
    • - {{ end }} -
    -``` - -The template above is rendered to: - -```html -
      -
    • three
    • -
    • four
    • -
    -``` - -## Example of `after` with `first`: 2nd–4th most recent articles - -You can use `after` in combination with the [`first`] function and Hugo's [powerful sorting methods](/quick-reference/page-collections/#sort). Let's assume you have a `section` page at `example.com/articles`. You have 10 articles, but you want your template to show only two rows: - -1. The top row is titled "Featured" and shows only the most recently published article (i.e. by `publishdate` in the content files' front matter). -1. The second row is titled "Recent Articles" and shows only the 2nd- to 4th-most recently published articles. - -```go-html-template {file="layouts/section/articles.html"} -{{ define "main" }} -
    -

    Featured Article

    - {{ range first 1 .Pages.ByPublishDate.Reverse }} -
    -

    {{ .Title }}

    -
    -

    {{ .Description }}

    - {{ end }} -
    -
    -

    Recent Articles

    - {{ range first 3 (after 1 .Pages.ByPublishDate.Reverse) }} -
    -
    -

    {{ .Title }}

    -
    -

    {{ .Description }}

    -
    - {{ end }} -
    -{{ end }} -``` - -[`first`]: /functions/collections/first/ -[`slice`]: /functions/collections/slice/ diff --git a/docs/content/en/functions/collections/Append.md b/docs/content/en/functions/collections/Append.md deleted file mode 100644 index cf1d1a3f5..000000000 --- a/docs/content/en/functions/collections/Append.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -title: collections.Append -description: Appends one or more elements to a slice and returns the resulting slice. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [append] - returnType: any - signatures: - - collections.Append ELEMENT [ELEMENT...] COLLECTION - - collections.Append COLLECTION1 COLLECTION2 -aliases: [/functions/append] ---- - -This function appends all elements, excluding the last, to the last element. This allows [pipe](g) constructs as shown below. - -Append a single element to a slice: - -```go-html-template -{{ $s := slice "a" "b" }} -{{ $s }} → [a b] - -{{ $s = $s | append "c" }} -{{ $s }} → [a b c] -``` - -Append two elements to a slice: - -```go-html-template -{{ $s := slice "a" "b" }} -{{ $s }} → [a b] - -{{ $s = $s | append "c" "d" }} -{{ $s }} → [a b c d] -``` - -Append two elements, as a slice, to a slice. This produces the same result as the previous example: - -```go-html-template -{{ $s := slice "a" "b" }} -{{ $s }} → [a b] - -{{ $s = $s | append (slice "c" "d") }} -{{ $s }} → [a b c d] -``` - -Start with an empty slice: - -```go-html-template -{{ $s := slice }} -{{ $s }} → [] - -{{ $s = $s | append "a" }} -{{ $s }} → [a] - -{{ $s = $s | append "b" "c" }} -{{ $s }} → [a b c] - -{{ $s = $s | append (slice "d" "e") }} -{{ $s }} → [a b c d e] -``` - -If you start with a slice of a slice: - -```go-html-template -{{ $s := slice (slice "a" "b") }} -{{ $s }} → [[a b]] - -{{ $s = $s | append (slice "c" "d") }} -{{ $s }} → [[a b] [c d]] -``` - -To create a slice of slices, starting with an empty slice: - -```go-html-template -{{ $s := slice }} -{{ $s }} → [] - -{{ $s = $s | append (slice (slice "a" "b")) }} -{{ $s }} → [[a b]] - -{{ $s = $s | append (slice "c" "d") }} -{{ $s }} → [[a b] [c d]] -``` - -Although the elements in the examples above are strings, you can use the `append` function with any data type, including Pages. For example, on the home page of a corporate site, to display links to the two most recent press releases followed by links to the four most recent articles: - -```go-html-template -{{ $p := where site.RegularPages "Type" "press-releases" | first 2 }} -{{ $p = $p | append (where site.RegularPages "Type" "articles" | first 4) }} - -{{ with $p }} - -{{ end }} -``` diff --git a/docs/content/en/functions/collections/Apply.md b/docs/content/en/functions/collections/Apply.md deleted file mode 100644 index 7ffe49053..000000000 --- a/docs/content/en/functions/collections/Apply.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: collections.Apply -description: Returns a new collection with each element transformed by the given function. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [apply] - returnType: '[]any' - signatures: [collections.Apply COLLECTION FUNCTION PARAM...] -aliases: [/functions/apply] ---- - -The `apply` function takes three or more arguments, depending on the function being applied to the collection elements. - -The first argument is the collection itself, the second argument is the function name, and the remaining arguments are passed to the function, with the string `"."` representing the collection element. - -```go-html-template -{{ $s := slice "hello" "world" }} - -{{ $s = apply $s "strings.FirstUpper" "." }} -{{ $s }} → [Hello World] - -{{ $s = apply $s "strings.Replace" "." "l" "_" }} -{{ $s }} → [He__o Wor_d] -``` diff --git a/docs/content/en/functions/collections/Complement.md b/docs/content/en/functions/collections/Complement.md deleted file mode 100644 index ce810dc00..000000000 --- a/docs/content/en/functions/collections/Complement.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -title: collections.Complement -description: Returns the elements of the last collection that are not in any of the others. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [complement] - returnType: any - signatures: ['collections.Complement COLLECTION [COLLECTION...]'] -aliases: [/functions/complement] ---- - -To find the elements within `$c3` that do not exist in `$c1` or `$c2`: - -```go-html-template -{{ $c1 := slice 3 }} -{{ $c2 := slice 4 5 }} -{{ $c3 := slice 1 2 3 4 5 }} - -{{ complement $c1 $c2 $c3 }} → [1 2] -``` - -> [!note] -> Make your code simpler to understand by using a [chained pipeline]: - -```go-html-template -{{ $c3 | complement $c1 $c2 }} → [1 2] -``` - -You can also use the `complement` function with page collections. Let's say your site has five content types: - -```text -content/ -├── blog/ -├── books/ -├── faqs/ -├── films/ -└── songs/ -``` - -To list everything except blog articles (`blog`) and frequently asked questions (`faqs`): - -```go-html-template -{{ $blog := where site.RegularPages "Type" "blog" }} -{{ $faqs := where site.RegularPages "Type" "faqs" }} -{{ range site.RegularPages | complement $blog $faqs }} - {{ .LinkTitle }} -{{ end }} -``` - -> [!note] -> Although the example above demonstrates the `complement` function, you could use the [`where`] function as well: - -```go-html-template -{{ range where site.RegularPages "Type" "not in" (slice "blog" "faqs") }} - {{ .LinkTitle }} -{{ end }} -``` - -In this example we use the `complement` function to remove [stop words] from a sentence: - -```go-html-template -{{ $text := "The quick brown fox jumps over the lazy dog" }} -{{ $stopWords := slice "a" "an" "in" "over" "the" "under" }} -{{ $filtered := split $text " " | complement $stopWords }} - -{{ delimit $filtered " " }} → The quick brown fox jumps lazy dog -``` - -[`where`]: /functions/collections/where/ -[chained pipeline]: https://pkg.go.dev/text/template#hdr-Pipelines -[stop words]: https://en.wikipedia.org/wiki/Stop_word diff --git a/docs/content/en/functions/collections/Delimit.md b/docs/content/en/functions/collections/Delimit.md deleted file mode 100644 index 9d09620aa..000000000 --- a/docs/content/en/functions/collections/Delimit.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: collections.Delimit -description: Loops through any array, slice, or map and returns a string of all the values separated by a delimiter. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [delimit] - returnType: string - signatures: ['collections.Delimit COLLECTION DELIMITER [LAST]'] -aliases: [/functions/delimit] ---- - -Delimit a slice: - -```go-html-template -{{ $s := slice "b" "a" "c" }} -{{ delimit $s ", " }} → b, a, c -{{ delimit $s ", " " and "}} → b, a and c -``` - -Delimit a map: - -> [!note] -> The `delimit` function sorts maps by key, returning the values. - -```go-html-template -{{ $m := dict "b" 2 "a" 1 "c" 3 }} -{{ delimit $m ", " }} → 1, 2, 3 -{{ delimit $m ", " " and "}} → 1, 2 and 3 -``` diff --git a/docs/content/en/functions/collections/Dictionary.md b/docs/content/en/functions/collections/Dictionary.md deleted file mode 100644 index 8aa428106..000000000 --- a/docs/content/en/functions/collections/Dictionary.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -title: collections.Dictionary -description: Returns a map composed of the given key-value pairs. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [dict] - returnType: map[string]any - signatures: ['collections.Dictionary [VALUE...]'] -aliases: [/functions/dict] ---- - -Specify the key-value pairs as individual arguments: - -```go-html-template -{{ $m := dict "a" 1 "b" 2 }} -``` - -The above produces this data structure: - -```json -{ - "a": 1, - "b": 2 -} -``` - -To create an empty map: - -```go-html-template -{{ $m := dict }} -``` - -Note that the `key` can be either a `string` or a `[]string`. The latter is useful to create a deeply nested structure, e.g.: - -```go-html-template -{{ $m := dict (slice "a" "b" "c") "value" }} -``` - -The above produces this data structure: - -```json -{ - "a": { - "b": { - "c": "value" - } - } -} -``` diff --git a/docs/content/en/functions/collections/First.md b/docs/content/en/functions/collections/First.md deleted file mode 100644 index 9a672278d..000000000 --- a/docs/content/en/functions/collections/First.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: collections.First -description: Returns the given collection, limited to the first N elements. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [first] - returnType: any - signatures: [collections.First N COLLECTION] -aliases: [/functions/first] ---- - -```go-html-template -{{ range first 5 .Pages }} - {{ .Render "summary" }} -{{ end }} -``` - -Set `N` to zero to return an empty collection. - -```go-html-template -{{ $emptyPageCollection := first 0 .Pages }} -``` - -Use `first` and [`where`] together. - -```go-html-template -{{ range where .Pages "Section" "articles" | first 5 }} - {{ .Render "summary" }} -{{ end }} -``` - -[`where`]: /functions/collections/where/ diff --git a/docs/content/en/functions/collections/Group.md b/docs/content/en/functions/collections/Group.md deleted file mode 100644 index 4b269b92a..000000000 --- a/docs/content/en/functions/collections/Group.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: collections.Group -description: Groups the given page collection by the given key. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [group] - returnType: any - signatures: [collections.Group KEY PAGES] -aliases: [/functions/group] ---- - -```go-html-template -{{ $new := .Site.RegularPages | first 10 | group "New" }} -{{ $old := .Site.RegularPages | last 10 | group "Old" }} -{{ $groups := slice $new $old }} -{{ range $groups }} -

    {{ .Key }}{{/* Prints "New", "Old" */}}

    -
      - {{ range .Pages }} -
    • - {{ .LinkTitle }} -
      {{ .Date.Format "Mon, Jan 2, 2006" }}
      -
    • - {{ end }} -
    -{{ end }} -``` - -The page group you get from `group` is of the same type you get from the built-in [group methods](/quick-reference/page-collections/#group) in Hugo. The example above can be [paginated](/templates/pagination/). diff --git a/docs/content/en/functions/collections/In.md b/docs/content/en/functions/collections/In.md deleted file mode 100644 index e94b4b0ed..000000000 --- a/docs/content/en/functions/collections/In.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: collections.In -description: Reports whether the given value is a member of the given set. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [in] - returnType: bool - signatures: [collections.In SET VALUE] -aliases: [/functions/in] ---- - -The `SET` can be an [array](g), [slice](g), or [string](g). - -```go-html-template -{{ $s := slice "a" "b" "c" }} -{{ in $s "b" }} → true -``` - -```go-html-template -{{ $s := slice 1 2 3 }} -{{ in $s 2 }} → true -``` - -```go-html-template -{{ $s := slice 1.11 2.22 3.33 }} -{{ in $s 2.22 }} → true -``` - -```go-html-template -{{ $s := "abc" }} -{{ in $s "b" }} → true -``` diff --git a/docs/content/en/functions/collections/IndexFunction.md b/docs/content/en/functions/collections/IndexFunction.md deleted file mode 100644 index 248595961..000000000 --- a/docs/content/en/functions/collections/IndexFunction.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: collections.Index -description: Returns the object, element, or value associated with the given key or keys. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [index] - returnType: any - signatures: [collections.Index COLLECTION KEY...] -aliases: [/functions/index,/functions/index-function] ---- - -Each indexed item must be a map or a slice: - -```go-html-template -{{ $s := slice "a" "b" "c" }} -{{ index $s 0 }} → a -{{ index $s 1 }} → b - -{{ $m := dict "a" 100 "b" 200 }} -{{ index $m "b" }} → 200 -``` - -Use two or more keys to access a nested value: - -```go-html-template -{{ $m := dict "a" 100 "b" 200 "c" (slice 10 20 30) }} -{{ index $m "c" 1 }} → 20 - -{{ $m := dict "a" 100 "b" 200 "c" (dict "d" 10 "e" 20) }} -{{ index $m "c" "e" }} → 20 -``` - -You may also use a slice of keys to access a nested value: - -```go-html-template -{{ $m := dict "a" 100 "b" 200 "c" (dict "d" 10 "e" 20) }} -{{ $s := slice "c" "e" }} -{{ index $m $s }} → 20 -``` - -Use the `collections.Index` function to access a nested value when the key is variable. For example, these are equivalent: - -```go-html-template -{{ .Site.Params.foo }} - -{{ $k := "foo" }} -{{ index .Site.Params $k }} -``` diff --git a/docs/content/en/functions/collections/Intersect.md b/docs/content/en/functions/collections/Intersect.md deleted file mode 100644 index ffa9c8196..000000000 --- a/docs/content/en/functions/collections/Intersect.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: collections.Intersect -description: Returns the common elements of two arrays or slices, in the same order as the first array. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [intersect] - returnType: any - signatures: [collections.Intersect SET1 SET2] -aliases: [/functions/intersect] ---- - -A useful example is to use it as `AND` filters when combined with where: - -```go-html-template -{{ $pages := where .Site.RegularPages "Type" "not in" (slice "page" "about") }} -{{ $pages := $pages | union (where .Site.RegularPages "Params.pinned" true) }} -{{ $pages := $pages | intersect (where .Site.RegularPages "Params.images" "!=" nil) }} -``` - -The above fetches regular pages not of `page` or `about` type unless they are pinned. And finally, we exclude all pages with no `images` set in Page parameters. - -See [union](/functions/collections/union) for `OR`. diff --git a/docs/content/en/functions/collections/IsSet.md b/docs/content/en/functions/collections/IsSet.md deleted file mode 100644 index 5457df5d4..000000000 --- a/docs/content/en/functions/collections/IsSet.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -title: collections.IsSet -description: Reports whether the key exists within the collection. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [isset] - returnType: bool - signatures: [collections.IsSet COLLECTION KEY] -aliases: [/functions/isset] ---- - -For example, consider this site configuration: - -{{< code-toggle file=hugo >}} -[params] -showHeroImage = false -{{< /code-toggle >}} - -It the value of `showHeroImage` is `true`, we can detect that it exists using either `if` or `with`: - -```go-html-template -{{ if site.Params.showHeroImage }} - {{ site.Params.showHeroImage }} → true -{{ end }} - -{{ with site.Params.showHeroImage }} - {{ . }} → true -{{ end }} -``` - -But if the value of `showHeroImage` is `false`, we can't use either `if` or `with` to detect its existence. In this case, you must use the `isset` function: - -```go-html-template -{{ if isset site.Params "showheroimage" }} -

    The showHeroImage parameter is set to {{ site.Params.showHeroImage }}.

    -{{ end }} -``` - -> [!note] -> When using the `isset` function you must reference the key using lower case. See the previous example. diff --git a/docs/content/en/functions/collections/KeyVals.md b/docs/content/en/functions/collections/KeyVals.md deleted file mode 100644 index bd58caea0..000000000 --- a/docs/content/en/functions/collections/KeyVals.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -title: collections.KeyVals -description: Returns a KeyVals struct. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [keyVals] - returnType: types.KeyValues - signatures: [collections.KeyVals KEY VALUE...] -aliases: [/functions/keyvals] ---- - -The primary application for this function is the definition of the `namedSlices` value in the options map passed to the [`Related`] method on the `Pages` object. - -[`Related`]: /methods/pages/related/ - -See [related content](/content-management/related-content/). - -```go-html-template -{{ $kv := keyVals "foo" "a" "b" "c" }} -``` - -The resulting data structure is: - -```json -{ - "Key": "foo", - "Values": [ - "a", - "b", - "c" - ] -} -``` - -To extract the key and values: - -```go-html-template -{{ $kv.Key }} → foo -{{ $kv.Values }} → [a b c] -``` diff --git a/docs/content/en/functions/collections/Last.md b/docs/content/en/functions/collections/Last.md deleted file mode 100644 index f0cfff219..000000000 --- a/docs/content/en/functions/collections/Last.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: collections.Last -description: Returns the given collection, limited to the last N elements. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [last] - returnType: any - signatures: [collections.Last N COLLECTION] -aliases: [/functions/last] ---- - -```go-html-template -{{ range last 10 .Pages }} - {{ .Render "summary" }} -{{ end }} -``` - -Set `N` to zero to return an empty collection. - -```go-html-template -{{ $emptyPageCollection := last 0 .Pages }} -``` - -Use `last` and [`where`] together. - -[`where`]: /functions/collections/where/ - -```go-html-template -{{ range where .Pages "Section" "articles" | last 5 }} - {{ .Render "summary" }} -{{ end }} -``` diff --git a/docs/content/en/functions/collections/Merge.md b/docs/content/en/functions/collections/Merge.md deleted file mode 100644 index c9998be39..000000000 --- a/docs/content/en/functions/collections/Merge.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: collections.Merge -description: Returns the result of merging two or more maps. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [merge] - returnType: any - signatures: [collections.Merge MAP MAP...] -aliases: [/functions/merge] ---- - -Returns the result of merging two or more maps from left to right. If a key already exists, `merge` updates its value. If a key is absent, `merge` inserts the value under the new key. - -Key handling is case-insensitive. - -The following examples use these map definitions: - -```go-html-template -{{ $m1 := dict "x" "foo" }} -{{ $m2 := dict "x" "bar" "y" "wibble" }} -{{ $m3 := dict "x" "baz" "y" "wobble" "z" (dict "a" "huey") }} -``` - -Example 1 - -```go-html-template -{{ $merged := merge $m1 $m2 $m3 }} - -{{ $merged.x }} → baz -{{ $merged.y }} → wobble -{{ $merged.z.a }} → huey -``` - -Example 2 - -```go-html-template -{{ $merged := merge $m3 $m2 $m1 }} - -{{ $merged.x }} → foo -{{ $merged.y }} → wibble -{{ $merged.z.a }} → huey -``` - -Example 3 - -```go-html-template -{{ $merged := merge $m2 $m3 $m1 }} - -{{ $merged.x }} → foo -{{ $merged.y }} → wobble -{{ $merged.z.a }} → huey -``` - -Example 4 - -```go-html-template -{{ $merged := merge $m1 $m3 $m2 }} - -{{ $merged.x }} → bar -{{ $merged.y }} → wibble -{{ $merged.z.a }} → huey -``` - -> [!note] -> Regardless of depth, merging only applies to maps. For slices, use [append](/functions/collections/append). diff --git a/docs/content/en/functions/collections/NewScratch.md b/docs/content/en/functions/collections/NewScratch.md deleted file mode 100644 index 34fc7f5d6..000000000 --- a/docs/content/en/functions/collections/NewScratch.md +++ /dev/null @@ -1,119 +0,0 @@ ---- -title: collections.NewScratch -description: Returns a locally scoped "scratch pad" to store and manipulate data. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [newScratch] - returnType: maps.Scratch - signatures: [collections.NewScratch ] ---- - -Use the `collections.NewScratch` function to create a locally scoped [scratch pad](g) to store and manipulate data. To create a scratch pad with a different [scope](g), refer to the [scope](#scope) section below. - -## Methods - -### Set - -Sets the value of the given key. - -```go-html-template -{{ $s := newScratch }} -{{ $s.Set "greeting" "Hello" }} -``` - -### Get - -Gets the value of the given key. - -```go-html-template -{{ $s := newScratch }} -{{ $s.Set "greeting" "Hello" }} -{{ $s.Get "greeting" }} → Hello -``` - -### Add - -Adds the given value to existing value(s) of the given key. - -For single values, `Add` accepts values that support Go's `+` operator. If the first `Add` for a key is an array or slice, the following adds will be appended to that list. - -```go-html-template -{{ $s := newScratch }} -{{ $s.Set "greeting" "Hello" }} -{{ $s.Add "greeting" "Welcome" }} -{{ $s.Get "greeting" }} → HelloWelcome -``` - -```go-html-template -{{ $s := newScratch }} -{{ $s.Set "total" 3 }} -{{ $s.Add "total" 7 }} -{{ $s.Get "total" }} → 10 -``` - -```go-html-template -{{ $s := newScratch }} -{{ $s.Set "greetings" (slice "Hello") }} -{{ $s.Add "greetings" (slice "Welcome" "Cheers") }} -{{ $s.Get "greetings" }} → [Hello Welcome Cheers] -``` - -### SetInMap - -Takes a `key`, `mapKey` and `value` and adds a map of `mapKey` and `value` to the given `key`. - -```go-html-template -{{ $s := newScratch }} -{{ $s.SetInMap "greetings" "english" "Hello" }} -{{ $s.SetInMap "greetings" "french" "Bonjour" }} -{{ $s.Get "greetings" }} → map[english:Hello french:Bonjour] -``` - -### DeleteInMap - -Takes a `key` and `mapKey` and removes the map of `mapKey` from the given `key`. - -```go-html-template -{{ $s := newScratch }} -{{ $s.SetInMap "greetings" "english" "Hello" }} -{{ $s.SetInMap "greetings" "french" "Bonjour" }} -{{ $s.DeleteInMap "greetings" "english" }} -{{ $s.Get "greetings" }} → map[french:Bonjour] -``` - -### GetSortedMapValues - -Returns an array of values from `key` sorted by `mapKey`. - -```go-html-template -{{ $s := newScratch }} -{{ $s.SetInMap "greetings" "english" "Hello" }} -{{ $s.SetInMap "greetings" "french" "Bonjour" }} -{{ $s.GetSortedMapValues "greetings" }} → [Hello Bonjour] -``` - -### Delete - -Removes the given key. - -```go-html-template -{{ $s := newScratch }} -{{ $s.Set "greeting" "Hello" }} -{{ $s.Delete "greeting" }} -``` - -### Values - -Returns the raw backing map. Do not use with `Store` methods on a `Page` object due to concurrency issues. - -```go-html-template -{{ $s := newScratch }} -{{ $s.SetInMap "greetings" "english" "Hello" }} -{{ $s.SetInMap "greetings" "french" "Bonjour" }} - -{{ $map := $s.Values }} -``` - -{{% include "_common/scratch-pad-scope.md" %}} diff --git a/docs/content/en/functions/collections/Querify.md b/docs/content/en/functions/collections/Querify.md deleted file mode 100644 index fd74935b7..000000000 --- a/docs/content/en/functions/collections/Querify.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: collections.Querify -description: Returns a URL query string composed of the given key-value pairs, encoded and sorted by key. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [querify] - returnType: string - signatures: ['collections.Querify [VALUE...]'] -aliases: [/functions/querify] ---- - -Specify the key-value pairs as a map, a slice, or a sequence of scalar values. For example, the following are equivalent: - -```go-html-template -{{ collections.Querify (dict "a" 1 "b" 2) }} -{{ collections.Querify (slice "a" 1 "b" 2) }} -{{ collections.Querify "a" 1 "b" 2 }} -``` - -To append a query string to a URL: - -```go-html-template -{{ $qs := collections.Querify (dict "a" 1 "b" 2) }} -{{ $href := printf "https://example.org?%s" $qs }} - -Link -``` - -Hugo renders this to: - -```html -Link -``` - -You can also pass in a map from your site configuration or front matter. For example: - -{{< code-toggle file=content/example.md fm=true >}} -title = 'Example' -[params.query] -a = 1 -b = 2 -{{< /code-toggle >}} - -```go-html-template -{{ collections.Querify .Params.query }} -``` diff --git a/docs/content/en/functions/collections/Reverse.md b/docs/content/en/functions/collections/Reverse.md deleted file mode 100644 index ee455939c..000000000 --- a/docs/content/en/functions/collections/Reverse.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: collections.Reverse -description: Reverses the order of a collection. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: any - signatures: [collections.Reverse COLLECTION] -aliases: [/functions/collections.reverse] ---- - -```go-html-template -{{ slice 2 1 3 | collections.Reverse }} → [3 1 2] -``` diff --git a/docs/content/en/functions/collections/Seq.md b/docs/content/en/functions/collections/Seq.md deleted file mode 100644 index e396f07e3..000000000 --- a/docs/content/en/functions/collections/Seq.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: collections.Seq -description: Returns a slice of integers. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [seq] - returnType: '[]int' - signatures: - - collections.Seq LAST - - collections.Seq FIRST LAST - - collections.Seq FIRST INCREMENT LAST -aliases: [/functions/seq] ---- - -```go-html-template -{{ seq 2 }} → [1 2] -{{ seq 0 2 }} → [0 1 2] -{{ seq -2 2 }} → [-2 -1 0 1 2] -{{ seq -2 2 2 }} → [-2 0 2] -``` - -A contrived example of iterating over a sequence of integers: - -```go-html-template -{{ $product := 1 }} -{{ range seq 4 }} - {{ $product = mul $product . }} -{{ end }} -{{ $product }} → 24 -``` - -> [!note] -> The slice created by the `seq` function is limited to 2000 elements. diff --git a/docs/content/en/functions/collections/Shuffle.md b/docs/content/en/functions/collections/Shuffle.md deleted file mode 100644 index 3a27c099a..000000000 --- a/docs/content/en/functions/collections/Shuffle.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: collections.Shuffle -description: Returns a random permutation of a given array or slice. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [shuffle] - returnType: any - signatures: [collections.Shuffle COLLECTION] -aliases: [/functions/shuffle] ---- - -```go-html-template -{{ shuffle (seq 1 2 3) }} → [3 1 2] -{{ shuffle (slice "a" "b" "c") }} → [b a c] -``` - -The result will vary from one build to the next. diff --git a/docs/content/en/functions/collections/Slice.md b/docs/content/en/functions/collections/Slice.md deleted file mode 100644 index 76180fabe..000000000 --- a/docs/content/en/functions/collections/Slice.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: collections.Slice -description: Returns a slice composed of the given values. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [slice] - returnType: any - signatures: ['collections.Slice [VALUE...]'] -aliases: [/functions/slice] ---- - -```go-html-template -{{ $s := slice "a" "b" "c" }} -{{ $s }} → [a b c] -``` - -To create an empty slice: - -```go-html-template -{{ $s := slice }} -``` diff --git a/docs/content/en/functions/collections/Sort.md b/docs/content/en/functions/collections/Sort.md deleted file mode 100644 index 67e5de5cb..000000000 --- a/docs/content/en/functions/collections/Sort.md +++ /dev/null @@ -1,150 +0,0 @@ ---- -title: collections.Sort -description: Sorts slices, maps, and page collections. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [sort] - returnType: any - signatures: ['collections.Sort COLLECTION [KEY] [ORDER]'] -aliases: [/functions/sort] ---- - -The `KEY` is optional when sorting slices in ascending order, otherwise it is required. When sorting slices, use the literal `value` in place of the `KEY`. See examples below. - -The `ORDER` may be either `asc` (ascending) or `desc` (descending). The default sort order is ascending. - -## Sort a slice - -The examples below assume this site configuration: - -{{< code-toggle file=hugo >}} -[params] -grades = ['b','a','c'] -{{< /code-toggle >}} - -### Ascending order {#slice-ascending-order} - -Sort slice elements in ascending order using either of these constructs: - -```go-html-template -{{ sort site.Params.grades }} → [a b c] -{{ sort site.Params.grades "value" "asc" }} → [a b c] -``` - -In the examples above, `value` is the `KEY` representing the value of the slice element. - -### Descending order {#slice-descending-order} - -Sort slice elements in descending order: - -```go-html-template -{{ sort site.Params.grades "value" "desc" }} → [c b a] -``` - -In the example above, `value` is the `KEY` representing the value of the slice element. - -## Sort a map - -The examples below assume this site configuration: - -{{< code-toggle file=hugo >}} -[params.authors.a] -firstName = "Marius" -lastName = "Pontmercy" -[params.authors.b] -firstName = "Victor" -lastName = "Hugo" -[params.authors.c] -firstName = "Jean" -lastName = "Valjean" -{{< /code-toggle >}} - -> [!note] -> When sorting maps, the `KEY` argument must be lowercase. - -### Ascending order {#map-ascending-order} - -Sort map objects in ascending order using either of these constructs: - -```go-html-template -{{ range sort site.Params.authors "firstname" }} - {{ .firstName }} -{{ end }} - -{{ range sort site.Params.authors "firstname" "asc" }} - {{ .firstName }} -{{ end }} -``` - -These produce: - -```text -Jean Marius Victor -``` - -### Descending order {#map-descending-order} - -Sort map objects in descending order: - -```go-html-template -{{ range sort site.Params.authors "firstname" "desc" }} - {{ .firstName }} -{{ end }} -``` - -This produces: - -```text -Victor Marius Jean -``` - -### First level key removal - -Hugo removes the first level keys when sorting a map. - -Original map: - -```json -{ - "felix": { - "breed": "malicious", - "type": "cat" - }, - "spot": { - "breed": "boxer", - "type": "dog" - } -} -``` - -After sorting: - -```json -[ - { - "breed": "malicious", - "type": "cat" - }, - { - "breed": "boxer", - "type": "dog" - } -] -``` - -## Sort a page collection - -> [!note] -> Although you can use the `sort` function to sort a page collection, Hugo provides [sorting and grouping methods] as well. - -In this contrived example, sort the site's regular pages by `.Type` in descending order: - -```go-html-template -{{ range sort site.RegularPages "Type" "desc" }} -

    {{ .LinkTitle }}

    -{{ end }} -``` - -[sorting and grouping methods]: /methods/pages/ diff --git a/docs/content/en/functions/collections/SymDiff.md b/docs/content/en/functions/collections/SymDiff.md deleted file mode 100644 index 8974d2d3e..000000000 --- a/docs/content/en/functions/collections/SymDiff.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -title: collections.SymDiff -description: Returns the symmetric difference of two collections. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [symdiff] - returnType: any - signatures: [COLLECTION | collections.SymDiff COLLECTION] -aliases: [/functions/symdiff] ---- - -Example: - -```go-html-template -{{ slice 1 2 3 | symdiff (slice 3 4) }} → [1 2 4] -``` - -Also see . diff --git a/docs/content/en/functions/collections/Union.md b/docs/content/en/functions/collections/Union.md deleted file mode 100644 index ce6d6d010..000000000 --- a/docs/content/en/functions/collections/Union.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: collections.Union -description: Given two arrays or slices, returns a new array that contains the elements that belong to either or both arrays/slices. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [union] - returnType: any - signatures: [collections.Union SET1 SET2] -aliases: [/functions/union] ---- - -Given two arrays (or slices) A and B, this function will return a new array that contains the elements or objects that belong to either A or to B or to both. - -```go-html-template -{{ union (slice 1 2 3) (slice 3 4 5) }} → [1 2 3 4 5] - -{{ union (slice 1 2 3) nil }} → [1 2 3] - -{{ union nil (slice 1 2 3) }} → [1 2 3] - -{{ union nil nil }} → [] -``` - -## OR filter in where query - -This is also very useful to use as `OR` filters when combined with where: - -```go-html-template -{{ $pages := where .Site.RegularPages "Type" "not in" (slice "page" "about") }} -{{ $pages = $pages | union (where .Site.RegularPages "Params.pinned" true) }} -{{ $pages = $pages | intersect (where .Site.RegularPages "Params.images" "!=" nil) }} -``` - -The above fetches regular pages not of `page` or `about` type unless they are pinned. And finally, we exclude all pages with no `images` set in Page parameters. - -See [intersect](/functions/collections/intersect) for `AND`. diff --git a/docs/content/en/functions/collections/Uniq.md b/docs/content/en/functions/collections/Uniq.md deleted file mode 100644 index d19298b21..000000000 --- a/docs/content/en/functions/collections/Uniq.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: collections.Uniq -description: Returns the given collection, removing duplicate elements. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [uniq] - returnType: any - signatures: [collections.Uniq COLLECTION] -aliases: [/functions/uniq] ---- - -```go-html-template -{{ slice 1 3 2 1 | uniq }} → [1 3 2] -``` diff --git a/docs/content/en/functions/collections/Where.md b/docs/content/en/functions/collections/Where.md deleted file mode 100644 index 84fd1d21e..000000000 --- a/docs/content/en/functions/collections/Where.md +++ /dev/null @@ -1,419 +0,0 @@ ---- -title: collections.Where -description: Returns the given collection, removing elements that do not satisfy the comparison condition. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [where] - returnType: any - signatures: ['collections.Where COLLECTION KEY [OPERATOR] VALUE'] -aliases: [/functions/where] ---- - -The `where` function returns the given collection, removing elements that do not satisfy the comparison condition. The comparison condition is composed of the `KEY`, `OPERATOR`, and `VALUE` arguments: - -```text -collections.Where COLLECTION KEY [OPERATOR] VALUE - -------------------- - comparison condition -``` - -Hugo will test for equality if you do not provide an `OPERATOR` argument. For example: - -```go-html-template -{{ $pages := where .Site.RegularPages "Section" "books" }} -{{ $books := where .Site.Data.books "genres" "suspense" }} -``` - -## Arguments - -The where function takes three or four arguments. The `OPERATOR` argument is optional. - -COLLECTION -: (`any`) A [page collection](g) or a [slice](g) of [maps](g). - -KEY -: (`string`) The key of the page or map value to compare with `VALUE`. With page collections, commonly used comparison keys are `Section`, `Type`, and `Params`. To compare with a member of the page `Params` map, [chain](g) the subkey as shown below: - -```go-html-template -{{ $result := where .Site.RegularPages "Params.foo" "bar" }} -``` - -OPERATOR -: (`string`) The logical comparison [operator](#operators). - -VALUE -: (`any`) The value with which to compare. The values to compare must have comparable data types. For example: - -Comparison|Result -:--|:-- -`"123" "eq" "123"`|`true` -`"123" "eq" 123`|`false` -`false "eq" "false"`|`false` -`false "eq" false`|`true` - -When one or both of the values to compare is a slice, use the `in`, `not in`, or `intersect` operators as described below. - -## Operators - -Use any of the following logical operators: - -`=`, `==`, `eq` -: (`bool`) Reports whether the given field value is equal to `VALUE`. - -`!=`, `<>`, `ne` -: (`bool`) Reports whether the given field value is not equal to `VALUE`. - -`>=`, `ge` -: (`bool`) Reports whether the given field value is greater than or equal to `VALUE`. - -`>`, `gt` -: `true` Reports whether the given field value is greater than `VALUE`. - -`<=`, `le` -: (`bool`) Reports whether the given field value is less than or equal to `VALUE`. - -`<`, `lt` -: (`bool`) Reports whether the given field value is less than `VALUE`. - -`in` -: (`bool`) Reports whether the given field value is a member of `VALUE`. Compare string to slice, or string to string. See [details](/functions/collections/in). - -`not in` -: (`bool`) Reports whether the given field value is not a member of `VALUE`. Compare string to slice, or string to string. See [details](/functions/collections/in). - -`intersect` -: (`bool`) Reports whether the given field value (a slice) contains one or more elements in common with `VALUE`. See [details](/functions/collections/intersect). - -`like` -: (`bool`) Reports whether the given field value matches the [regular expression](g) specified in `VALUE`. Use the `like` operator to compare `string` values. The `like` operator returns `false` when comparing other data types to the regular expression. - -> [!note] -> The examples below perform comparisons within a page collection, but the same comparisons are applicable to a slice of maps. - -## String comparison - -Compare the value of the given field to a [`string`](g): - -```go-html-template -{{ $pages := where .Site.RegularPages "Section" "eq" "books" }} -{{ $pages := where .Site.RegularPages "Section" "ne" "books" }} -``` - -## Numeric comparison - -Compare the value of the given field to an [`int`](g) or [`float`](g): - -```go-html-template -{{ $books := where site.RegularPages "Section" "eq" "books" }} - -{{ $pages := where $books "Params.price" "eq" 42 }} -{{ $pages := where $books "Params.price" "ne" 42.67 }} -{{ $pages := where $books "Params.price" "ge" 42 }} -{{ $pages := where $books "Params.price" "gt" 42.67 }} -{{ $pages := where $books "Params.price" "le" 42 }} -{{ $pages := where $books "Params.price" "lt" 42.67 }} -``` - -## Boolean comparison - -Compare the value of the given field to a [`bool`](g): - -```go-html-template -{{ $books := where site.RegularPages "Section" "eq" "books" }} - -{{ $pages := where $books "Params.fiction" "eq" true }} -{{ $pages := where $books "Params.fiction" "eq" false }} -{{ $pages := where $books "Params.fiction" "ne" true }} -{{ $pages := where $books "Params.fiction" "ne" false }} -``` - -## Member comparison - -Compare a [`scalar`](g) to a [`slice`](g). - -For example, to return a collection of pages where the `color` page parameter is either "red" or "yellow": - -```go-html-template -{{ $fruit := where site.RegularPages "Section" "eq" "fruit" }} - -{{ $colors := slice "red" "yellow" }} -{{ $pages := where $fruit "Params.color" "in" $colors }} -``` - -To return a collection of pages where the "color" page parameter is neither "red" nor "yellow": - -```go-html-template -{{ $fruit := where site.RegularPages "Section" "eq" "fruit" }} - -{{ $colors := slice "red" "yellow" }} -{{ $pages := where $fruit "Params.color" "not in" $colors }} -``` - -## Intersection comparison - -Compare a `slice` to a `slice`, returning collection elements with common values. This is frequently used when comparing taxonomy terms. - -For example, to return a collection of pages where any of the terms in the "genres" taxonomy are "suspense" or "romance": - -```go-html-template -{{ $books := where site.RegularPages "Section" "eq" "books" }} - -{{ $genres := slice "suspense" "romance" }} -{{ $pages := where $books "Params.genres" "intersect" $genres }} -``` - -## Regular expression comparison - -To return a collection of pages where the "author" page parameter begins with either "victor" or "Victor": - -```go-html-template -{{ $pages := where .Site.RegularPages "Params.author" "like" `(?i)^victor` }} -``` - -{{% include "/_common/functions/regular-expressions.md" %}} - -> [!note] -> Use the `like` operator to compare string values. Comparing other data types will result in an empty collection. - -## Date comparison - -### Predefined dates - -There are four predefined front matter dates: [`date`], [`publishDate`], [`lastmod`], and [`expiryDate`]. Regardless of the front matter data format (TOML, YAML, or JSON) these are [`time.Time`] values, allowing precise comparisons. - -For example, to return a collection of pages that were created before the current year: - -```go-html-template -{{ $startOfYear := time.AsTime (printf "%d-01-01" now.Year) }} -{{ $pages := where .Site.RegularPages "Date" "lt" $startOfYear }} -``` - -### Custom dates - -With custom front matter dates, the comparison depends on the front matter data format (TOML, YAML, or JSON). - -> [!note] -> Using TOML for pages with custom front matter dates enables precise date comparisons. - -With TOML, date values are first-class citizens. TOML has a date data type while JSON and YAML do not. If you quote a TOML date, it is a string. If you do not quote a TOML date value, it is [`time.Time`] value, enabling precise comparisons. - -In the TOML example below, note that the event date is not quoted. - -```text {file="content/events/2024-user-conference.md"} -+++ -title = '2024 User Conference" -eventDate = 2024-04-01 -+++ -``` - -To return a collection of future events: - -```go-html-template -{{ $events := where .Site.RegularPages "Type" "events" }} -{{ $futureEvents := where $events "Params.eventDate" "gt" now }} -``` - -When working with YAML or JSON, or quoted TOML values, custom dates are strings; you cannot compare them with `time.Time` values. String comparisons may be possible if the custom date layout is consistent from one page to the next. To be safe, filter the pages by ranging through the collection: - -```go-html-template -{{ $events := where .Site.RegularPages "Type" "events" }} -{{ $futureEvents := slice }} -{{ range $events }} - {{ if gt (time.AsTime .Params.eventDate) now }} - {{ $futureEvents = $futureEvents | append . }} - {{ end }} -{{ end }} -``` - -## Nil comparison - -To return a collection of pages where the "color" parameter is present in front matter, compare to `nil`: - -```go-html-template -{{ $pages := where .Site.RegularPages "Params.color" "ne" nil }} -``` - -To return a collection of pages where the "color" parameter is not present in front matter, compare to `nil`: - -```go-html-template -{{ $pages := where .Site.RegularPages "Params.color" "eq" nil }} -``` - -In both examples above, note that `nil` is not quoted. - -## Nested comparison - -These are equivalent: - -```go-html-template -{{ $pages := where .Site.RegularPages "Type" "tutorials" }} -{{ $pages = where $pages "Params.level" "eq" "beginner" }} -``` - -```go-html-template -{{ $pages := where (where .Site.RegularPages "Type" "tutorials") "Params.level" "eq" "beginner" }} -``` - -## Portable section comparison - -Useful for theme authors, avoid hardcoding section names by using the `where` function with the [`MainSections`] method on a `Site` object. - -```go-html-template -{{ $pages := where .Site.RegularPages "Section" "in" .Site.MainSections }} -``` - -With this construct, a theme author can instruct users to specify their main sections in the site configuration: - -{{< code-toggle file=hugo >}} -mainSections = ['blog','galleries'] -{{< /code-toggle >}} - -If `mainSections` is not defined in the site configuration, the `MainSections` method returns a slice with one element---the top-level section with the most pages. - -## Boolean/undefined comparison - -Consider this site content: - -```text -content/ -├── posts/ -│ ├── _index.md -│ ├── post-1.md <-- front matter: exclude = false -│ ├── post-2.md <-- front matter: exclude = true -│ └── post-3.md <-- front matter: exclude not defined -└── _index.md -``` - -The first two pages have an "exclude" field in front matter, but the last page does not. When testing for _equality_, the third page is _excluded_ from the result. When testing for _inequality_, the third page is _included_ in the result. - -### Equality test - -This template: - -```go-html-template -
      - {{ range where .Site.RegularPages "Params.exclude" "eq" false }} -
    • {{ .LinkTitle }}
    • - {{ end }} -
    -``` - -Is rendered to: - -```html - -``` - -This template: - -```go-html-template -
      - {{ range where .Site.RegularPages "Params.exclude" "eq" true }} -
    • {{ .LinkTitle }}
    • - {{ end }} -
    -``` - -Is rendered to: - -```html - -``` - -### Inequality test - -This template: - -```go-html-template -
      - {{ range where .Site.RegularPages "Params.exclude" "ne" false }} -
    • {{ .LinkTitle }}
    • - {{ end }} -
    -``` - -Is rendered to: - -```html - -``` - -This template: - -```go-html-template -
      - {{ range where .Site.RegularPages "Params.exclude" "ne" true }} -
    • {{ .LinkTitle }}
    • - {{ end }} -
    -``` - -Is rendered to: - -```html - -``` - -To exclude a page with an undefined field from a boolean _inequality_ test: - -1. Create a collection using a boolean comparison -1. Create a collection using a nil comparison -1. Subtract the second collection from the first collection using the [`collections.Complement`] function. - -This template: - -```go-html-template -{{ $p1 := where .Site.RegularPages "Params.exclude" "ne" true }} -{{ $p2 := where .Site.RegularPages "Params.exclude" "eq" nil }} - -``` - -Is rendered to: - -```html - -``` - -This template: - -```go-html-template -{{ $p1 := where .Site.RegularPages "Params.exclude" "ne" false }} -{{ $p2 := where .Site.RegularPages "Params.exclude" "eq" nil }} - -``` - -Is rendered to: - -```html - -``` - -[`collections.Complement`]: /functions/collections/complement/ -[`date`]: /methods/page/date/ -[`lastmod`]: /methods/page/lastmod/ -[`MainSections`]: /methods/site/mainsections/ -[`time.Time`]: https://pkg.go.dev/time#Time diff --git a/docs/content/en/functions/collections/_index.md b/docs/content/en/functions/collections/_index.md deleted file mode 100644 index c7b856f4f..000000000 --- a/docs/content/en/functions/collections/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Collections functions -linkTitle: collections -description: Use these functions to work with arrays, slices, maps, and page collections. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/functions/compare/Conditional.md b/docs/content/en/functions/compare/Conditional.md deleted file mode 100644 index c004bf4e6..000000000 --- a/docs/content/en/functions/compare/Conditional.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: compare.Conditional -description: Returns one of two arguments depending on the value of the control argument. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [cond] - returnType: any - signatures: [compare.Conditional CONTROL ARG1 ARG2] -aliases: [/functions/cond] ---- - -If CONTROL is truthy the function returns ARG1, otherwise it returns ARG2. - -```go-html-template -{{ $qty := 42 }} -{{ cond (le $qty 3) "few" "many" }} → many -``` - -Unlike [ternary operators] in other languages, the `compare.Conditional` function does not perform [short-circuit evaluation]. It evaluates both ARG1 and ARG2 regardless of the CONTROL value. - -[short-circuit evaluation]: https://en.wikipedia.org/wiki/Short-circuit_evaluation -[ternary operators]: https://en.wikipedia.org/wiki/Ternary_conditional_operator - -Due to the absence of short-circuit evaluation, these examples throw an error: - -```go-html-template -{{ cond true "true" (div 1 0) }} -{{ cond false (div 1 0) "false" }} -``` diff --git a/docs/content/en/functions/compare/Default.md b/docs/content/en/functions/compare/Default.md deleted file mode 100644 index f8bd06f06..000000000 --- a/docs/content/en/functions/compare/Default.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: compare.Default -description: Returns the second argument if set, else the first argument. -keywords: [] -params: - functions_and_methods: - aliases: [default] - returnType: any - signatures: [compare.Default DEFAULT INPUT] -aliases: [/functions/default] ---- - -The `default` function returns the second argument if set, else the first argument. - -> [!note] -> When the second argument is the boolean `false` value, the `default` function returns `false`. All _other_ falsy values are considered unset. -> -> The falsy values are `false`, `0`, any `nil` pointer or interface value, any array, slice, map, or string of length zero, and zero `time.Time` values. -> -> Everything else is truthy. -> -> To set a default value based on truthiness, use the [`or`] operator instead. - -The `default` function returns the second argument if set: - -```go-html-template -{{ default 42 1 }} → 1 -{{ default 42 "foo" }} → foo -{{ default 42 (dict "k" "v") }} → map[k:v] -{{ default 42 (slice "a" "b") }} → [a b] -{{ default 42 true }} → true - - -{{ default 42 false }} → false -``` - -The `default` function returns the first argument if the second argument is not set: - -```go-html-template -{{ default 42 0 }} → 42 -{{ default 42 "" }} → 42 -{{ default 42 dict }} → 42 -{{ default 42 slice }} → 42 -{{ default 42 }} → 42 -``` - -[`or`]: /functions/go-template/or/ diff --git a/docs/content/en/functions/compare/Eq.md b/docs/content/en/functions/compare/Eq.md deleted file mode 100644 index 583e2e495..000000000 --- a/docs/content/en/functions/compare/Eq.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: compare.Eq -description: Returns the boolean truth of arg1 == arg2 || arg1 == arg3. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [eq] - returnType: bool - signatures: ['compare.Eq ARG1 ARG2 [ARG...]'] -aliases: [/functions/eq] ---- - -```go-html-template -{{ eq 1 1 }} → true -{{ eq 1 2 }} → false - -{{ eq 1 1 1 }} → true -{{ eq 1 1 2 }} → true -{{ eq 1 2 1 }} → true -{{ eq 1 2 2 }} → false -``` - -You can also use the `compare.Eq` function to compare strings, boolean values, dates, slices, maps, and pages. diff --git a/docs/content/en/functions/compare/Ge.md b/docs/content/en/functions/compare/Ge.md deleted file mode 100644 index fd793f0c9..000000000 --- a/docs/content/en/functions/compare/Ge.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: compare.Ge -description: Returns the boolean truth of arg1 >= arg2 && arg1 >= arg3. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [ge] - returnType: bool - signatures: ['compare.Ge ARG1 ARG2 [ARG...]'] -aliases: [/functions/ge] ---- - -```go-html-template -{{ ge 1 1 }} → true -{{ ge 1 2 }} → false -{{ ge 2 1 }} → true - -{{ ge 1 1 1 }} → true -{{ ge 1 1 2 }} → false -{{ ge 1 2 1 }} → false -{{ ge 1 2 2 }} → false - -{{ ge 2 1 1 }} → true -{{ ge 2 1 2 }} → true -{{ ge 2 2 1 }} → true -``` - -Use the `compare.Ge` function to compare other data types as well: - -```go-html-template -{{ ge "ab" "a" }} → true -{{ ge time.Now (time.AsTime "1964-12-30") }} → true -{{ ge true false }} → true -``` diff --git a/docs/content/en/functions/compare/Gt.md b/docs/content/en/functions/compare/Gt.md deleted file mode 100644 index f48312cf7..000000000 --- a/docs/content/en/functions/compare/Gt.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: compare.Gt -description: Returns the boolean truth of arg1 > arg2 && arg1 > arg3. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [gt] - returnType: bool - signatures: ['compare.Gt ARG1 ARG2 [ARG...]'] -aliases: [/functions/gt] ---- - -```go-html-template -{{ gt 1 1 }} → false -{{ gt 1 2 }} → false -{{ gt 2 1 }} → true - -{{ gt 1 1 1 }} → false -{{ gt 1 1 2 }} → false -{{ gt 1 2 1 }} → false -{{ gt 1 2 2 }} → false - -{{ gt 2 1 1 }} → true -{{ gt 2 1 2 }} → false -{{ gt 2 2 1 }} → false -``` - -Use the `compare.Gt` function to compare other data types as well: - -```go-html-template -{{ gt "ab" "a" }} → true -{{ gt time.Now (time.AsTime "1964-12-30") }} → true -{{ gt true false }} → true -``` diff --git a/docs/content/en/functions/compare/Le.md b/docs/content/en/functions/compare/Le.md deleted file mode 100644 index b490d6807..000000000 --- a/docs/content/en/functions/compare/Le.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: compare.Le -description: Returns the boolean truth of arg1 <= arg2 && arg1 <= arg3. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [le] - returnType: bool - signatures: ['compare.Le ARG1 ARG2 [ARG...]'] -aliases: [/functions/le] ---- - -```go-html-template -{{ le 1 1 }} → true -{{ le 1 2 }} → true -{{ le 2 1 }} → false - -{{ le 1 1 1 }} → true -{{ le 1 1 2 }} → true -{{ le 1 2 1 }} → true -{{ le 1 2 2 }} → true - -{{ le 2 1 1 }} → false -{{ le 2 1 2 }} → false -{{ le 2 2 1 }} → false -``` - -Use the `compare.Le` function to compare other data types as well: - -```go-html-template -{{ le "ab" "a" }} → false -{{ le time.Now (time.AsTime "1964-12-30") }} → false -{{ le true false }} → false -``` diff --git a/docs/content/en/functions/compare/Lt.md b/docs/content/en/functions/compare/Lt.md deleted file mode 100644 index a5578cc84..000000000 --- a/docs/content/en/functions/compare/Lt.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: compare.Lt -description: Returns the boolean truth of arg1 < arg2 && arg1 < arg3. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [lt] - returnType: bool - signatures: ['compare.Lt ARG1 ARG2 [ARG...]'] -aliases: [/functions/lt] ---- - -```go-html-template -{{ lt 1 1 }} → false -{{ lt 1 2 }} → true -{{ lt 2 1 }} → false - -{{ lt 1 1 1 }} → false -{{ lt 1 1 2 }} → false -{{ lt 1 2 1 }} → false -{{ lt 1 2 2 }} → true - -{{ lt 2 1 1 }} → false -{{ lt 2 1 2 }} → false -{{ lt 2 2 1 }} → false -``` - -Use the `compare.Lt` function to compare other data types as well: - -```go-html-template -{{ lt "ab" "a" }} → false -{{ lt time.Now (time.AsTime "1964-12-30") }} → false -{{ lt true false }} → false -``` diff --git a/docs/content/en/functions/compare/Ne.md b/docs/content/en/functions/compare/Ne.md deleted file mode 100644 index 8839983f8..000000000 --- a/docs/content/en/functions/compare/Ne.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: compare.Ne -description: Returns the boolean truth of arg1 != arg2 && arg1 != arg3. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [ne] - returnType: bool - signatures: ['compare.Ne ARG1 ARG2 [ARG...]'] -aliases: [/functions/ne] ---- - -```go-html-template -{{ ne 1 1 }} → false -{{ ne 1 2 }} → true - -{{ ne 1 1 1 }} → false -{{ ne 1 1 2 }} → false -{{ ne 1 2 1 }} → false -{{ ne 1 2 2 }} → true -``` - -You can also use the `compare.Ne` function to compare strings, boolean values, dates, slices, maps, and pages. diff --git a/docs/content/en/functions/compare/_index.md b/docs/content/en/functions/compare/_index.md deleted file mode 100644 index 59673a2c8..000000000 --- a/docs/content/en/functions/compare/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Compare functions -linkTitle: compare -description: Use these functions to compare two or more values. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/functions/crypto/FNV32a.md b/docs/content/en/functions/crypto/FNV32a.md deleted file mode 100644 index 03bcc57e7..000000000 --- a/docs/content/en/functions/crypto/FNV32a.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: crypto.FNV32a -description: Returns the 32-bit FNV (Fowler-Noll-Vo) non-cryptographic hash of the given string. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: int - signatures: [crypto.FNV32a STRING] -expiryDate: 2026-07-31 # deprecated 2024-07-31 in v0.129.0 ---- - -{{< deprecated-in 0.129.0 >}} -Use [`hash.FNV32a`] instead. - -[`hash.FNV32a`]: /functions/hash/FNV32a/ -{{< /deprecated-in >}} diff --git a/docs/content/en/functions/crypto/HMAC.md b/docs/content/en/functions/crypto/HMAC.md deleted file mode 100644 index 5929826dd..000000000 --- a/docs/content/en/functions/crypto/HMAC.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: crypto.HMAC -description: Returns a cryptographic hash that uses a key to sign a message. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [hmac] - returnType: string - signatures: ['crypto.HMAC HASH_TYPE KEY MESSAGE [ENCODING]'] -aliases: [/functions/hmac] ---- - -Set the `HASH_TYPE` argument to `md5`, `sha1`, `sha256`, or `sha512`. - -Set the optional `ENCODING` argument to either `hex` (default) or `binary`. - -```go-html-template -{{ hmac "sha256" "Secret key" "Secret message" }} -5cceb491f45f8b154e20f3b0a30ed3a6ff3027d373f85c78ffe8983180b03c84 - -{{ hmac "sha256" "Secret key" "Secret message" "hex" }} -5cceb491f45f8b154e20f3b0a30ed3a6ff3027d373f85c78ffe8983180b03c84 - -{{ hmac "sha256" "Secret key" "Secret message" "binary" | base64Encode }} -XM60kfRfixVOIPOwow7Tpv8wJ9Nz+Fx4/+iYMYCwPIQ= -``` diff --git a/docs/content/en/functions/crypto/MD5.md b/docs/content/en/functions/crypto/MD5.md deleted file mode 100644 index 89bb8cc1b..000000000 --- a/docs/content/en/functions/crypto/MD5.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: crypto.MD5 -description: Hashes the given input and returns its MD5 checksum encoded to a hexadecimal string. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [md5] - returnType: string - signatures: [crypto.MD5 INPUT] -aliases: [/functions/md5] ---- - -```go-html-template -{{ md5 "Hello world" }} → 3e25960a79dbc69b674cd4ec67a72c62 -``` - -This can be useful if you want to use [Gravatar](https://en.gravatar.com/) for generating a unique avatar: - -```html - -``` diff --git a/docs/content/en/functions/crypto/SHA1.md b/docs/content/en/functions/crypto/SHA1.md deleted file mode 100644 index c80dac0a4..000000000 --- a/docs/content/en/functions/crypto/SHA1.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: crypto.SHA1 -description: Hashes the given input and returns its SHA1 checksum encoded to a hexadecimal string. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [sha1] - returnType: string - signatures: [crypto.SHA1 INPUT] -aliases: [/functions/sha,/functions/sha1] ---- - -```go-html-template -{{ sha1 "Hello world" }} → 7b502c3a1f48c8609ae212cdfb639dee39673f5e -``` diff --git a/docs/content/en/functions/crypto/SHA256.md b/docs/content/en/functions/crypto/SHA256.md deleted file mode 100644 index d0a66c069..000000000 --- a/docs/content/en/functions/crypto/SHA256.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: crypto.SHA256 -description: Hashes the given input and returns its SHA256 checksum encoded to a hexadecimal string. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [sha256] - returnType: string - signatures: [crypto.SHA256 INPUT] -aliases: [/functions/sha256] ---- - -```go-html-template -{{ sha256 "Hello world" }} → 64ec88ca00b268e5ba1a35678a1b5316d212f4f366b2477232534a8aeca37f3c -``` diff --git a/docs/content/en/functions/crypto/_index.md b/docs/content/en/functions/crypto/_index.md deleted file mode 100644 index 5771630d4..000000000 --- a/docs/content/en/functions/crypto/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Crypto functions -linkTitle: crypto -description: Use these functions to create cryptographic hashes. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/functions/css/PostCSS.md b/docs/content/en/functions/css/PostCSS.md deleted file mode 100644 index 9cc698248..000000000 --- a/docs/content/en/functions/css/PostCSS.md +++ /dev/null @@ -1,128 +0,0 @@ ---- -title: css.PostCSS -description: Processes the given resource with PostCSS using any PostCSS plugin. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [postCSS] - returnType: resource.Resource - signatures: ['css.PostCSS [OPTIONS] RESOURCE'] ---- - -{{< new-in 0.128.0 />}} - -```go-html-template -{{ with resources.Get "css/main.css" | postCSS }} - -{{ end }} -``` - -## Setup - -Follow the steps below to transform CSS using any of the available [PostCSS plugins]. - -### Step 1 - -Install [Node.js]. - -### Step 2 - -Install the required Node.js packages in the root of your project. For example, to add vendor prefixes to your CSS rules: - -```sh -npm i -D postcss postcss-cli autoprefixer -``` - -### Step 3 - -Create a PostCSS configuration file in the root of your project. - -```js {file="postcss.config.js"} -module.exports = { - plugins: [ - require('autoprefixer') - ] -}; -``` - -> [!note] -> If you are a Windows user, and the path to your project contains a space, you must place the PostCSS configuration within the package.json file. See [this example] and issue [#7333]. - -### Step 4 - -Place your CSS file within the `assets/css` directory. - -### Step 5 - -Process the resource with PostCSS: - -```go-html-template -{{ with resources.Get "css/main.css" | postCSS }} - -{{ end }} -``` - -## Options - -The `css.PostCSS` method takes an optional map of options. - -config -: (`string`) The directory that contains the PostCSS configuration file. Default is the root of the project directory. - -noMap -: (`bool`) Whether to disable inline source maps. Default is `false`. - -inlineImports -: (`bool`) Whether to enable inlining of import statements. It does so recursively, but will only import a file once. URL imports (e.g. `@import url('https://fonts.googleapis.com/css?family=Open+Sans&display=swap');`) and imports with media queries will be ignored. Note that this import routine does not care about the CSS spec, so you can have @import anywhere in the file. Hugo will look for imports relative to the module mount and will respect theme overrides. Default is `false`. - -skipInlineImportsNotFound -: (`bool`) Whether to allow the build process to continue despite unresolved import statements, preserving the original import declarations. If you have regular CSS imports in your CSS that you want to preserve, you can either use imports with URL or media queries (Hugo does not try to resolve those) or set this option to `true`. Default is `false`." - -```go-html-template -{{ $opts := dict "config" "config-directory" "noMap" true }} -{{ with resources.Get "css/main.css" | postCSS $opts }} - -{{ end }} -``` - -## No configuration file - -To avoid using a PostCSS configuration file, you can specify a minimal configuration using the options map. - -use -: (`string`) A space-delimited list of PostCSS plugins to use. - -parser -: (`string`) A custom PostCSS parser. - -stringifier -: (`string`) A custom PostCSS stringifier. - -syntax -: (`string`) Custom postcss syntax. - -```go-html-template -{{ $opts := dict "use" "autoprefixer postcss-color-alpha" }} -{{ with resources.Get "css/main.css" | postCSS $opts }} - -{{ end }} -``` - -## Check environment - -The current Hugo environment name (set by `--environment` or in configuration or OS environment) is available in the Node context, which allows constructs like this: - -```js -const autoprefixer = require('autoprefixer'); -module.exports = { - plugins: [ - process.env.HUGO_ENVIRONMENT !== 'development' ? autoprefixer : null - ] -} -``` - -[#7333]: https://github.com/gohugoio/hugo/issues/7333 -[Node.js]: https://nodejs.org/en -[PostCSS plugins]: https://postcss.org/docs/postcss-plugins -[this example]: https://github.com/postcss/postcss-load-config#packagejson diff --git a/docs/content/en/functions/css/Sass.md b/docs/content/en/functions/css/Sass.md deleted file mode 100644 index 03a4c7451..000000000 --- a/docs/content/en/functions/css/Sass.md +++ /dev/null @@ -1,204 +0,0 @@ ---- -title: css.Sass -description: Transpiles Sass to CSS. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [toCSS] - returnType: resource.Resource - signatures: ['css.Sass [OPTIONS] RESOURCE'] ---- - -{{< new-in 0.128.0 />}} - -Transpile Sass to CSS using the LibSass transpiler included in Hugo's extended and extended/deploy editions, or [install Dart Sass](#dart-sass) to use the latest features of the Sass language. - -Sass has two forms of syntax: [SCSS] and [indented]. Hugo supports both. - -[scss]: https://sass-lang.com/documentation/syntax#scss -[indented]: https://sass-lang.com/documentation/syntax#the-indented-syntax - -## Options - -enableSourceMap -: (`bool`) Whether to generate a source map. Default is `false`. - -includePaths -: (`slice`) A slice of paths, relative to the project root, that the transpiler will use when resolving `@use` and `@import` statements. - -outputStyle -: (`string`) The output style of the resulting CSS. With LibSass, one of `nested` (default), `expanded`, `compact`, or `compressed`. With Dart Sass, either `expanded` (default) or `compressed`. - -precision -: (`int`) The precision of floating point math. Applicable to LibSass. Default is `8`. - -silenceDeprecations -: {{< new-in 0.139.0 />}} -: (`slice`) A slice of deprecation IDs to silence. IDs are enclosed in brackets within Dart Sass warning messages (e.g., `import` in `WARN Dart Sass: DEPRECATED [import]`). Applicable to Dart Sass. Default is `false`. - -silenceDependencyDeprecations -: {{< new-in 0.146.0 />}} -: (`bool`) Whether to silence deprecation warnings from dependencies, where a dependency is considered any file transitively imported through a load path. This does not apply to `@warn` or `@debug` rules.Default is `false`. - -sourceMapIncludeSources -: (`bool`) Whether to embed sources in the generated source map. Applicable to Dart Sass. Default is `false`. - -targetPath -: (`string`) The publish path for the transformed resource, relative to the[`publishDir`]. If unset, the target path defaults to the asset's original path with a `.css` extension. - -transpiler -: (`string`) The transpiler to use, either `libsass` or `dartsass`. Hugo's extended and extended/deploy editions include the LibSass transpiler. To use the Dart Sass transpiler, see the [installation instructions](#dart-sass). Default is `libsass`. - -vars -: (`map`) A map of key-value pairs that will be available in the `hugo:vars` namespace. Useful for [initializing Sass variables from Hugo templates](https://discourse.gohugo.io/t/42053/). - - ```scss - // LibSass - @import "hugo:vars"; - - // Dart Sass - @use "hugo:vars" as v; - ``` - -## Example - -```go-html-template {copy=true} -{{ with resources.Get "sass/main.scss" }} - {{ $opts := dict - "enableSourceMap" (not hugo.IsProduction) - "outputStyle" (cond hugo.IsProduction "compressed" "expanded") - "targetPath" "css/main.css" - "transpiler" "dartsass" - "vars" site.Params.styles - "includePaths" (slice "node_modules/bootstrap/scss") - }} - {{ with . | toCSS $opts }} - {{ if hugo.IsProduction }} - {{ with . | fingerprint }} - - {{ end }} - {{ else }} - - {{ end }} - {{ end }} -{{ end }} -``` - -## Dart Sass - -Hugo's extended and extended/deploy editions include [LibSass] to transpile Sass to CSS. In 2020, the Sass team deprecated LibSass in favor of [Dart Sass]. - -Use the latest features of the Sass language by installing Dart Sass in your development and production environments. - -### Installation overview - -Dart Sass is compatible with Hugo v0.114.0 and later. - -If you have been using Embedded Dart Sass[^1] with Hugo v0.113.0 and earlier, uninstall Embedded Dart Sass, then install Dart Sass. If you have installed both, Hugo will use Dart Sass. - -If you install Hugo as a [Snap package] there is no need to install Dart Sass. The Hugo Snap package includes Dart Sass. - -[^1]: In 2023, the Sass team deprecated Embedded Dart Sass in favor of Dart Sass. - -### Installing in a development environment - -When you install Dart Sass somewhere in your PATH, Hugo will find it. - -OS|Package manager|Site|Installation -:--|:--|:--|:-- -Linux|Homebrew|[brew.sh]|`brew install sass/sass/sass` -Linux|Snap|[snapcraft.io]|`sudo snap install dart-sass` -macOS|Homebrew|[brew.sh]|`brew install sass/sass/sass` -Windows|Chocolatey|[chocolatey.org]|`choco install sass` -Windows|Scoop|[scoop.sh]|`scoop install sass` - -You may also install [prebuilt binaries] for Linux, macOS, and Windows. - -Run `hugo env` to list the active transpilers. - -> [!note] -> If you build Hugo from source and run `mage test -v`, the test will fail if you install Dart Sass as a Snap package. This is due to the Snap package's strict confinement model. - -### Installing in a production environment - -For [CI/CD](g) deployments (e.g., GitHub Pages, GitLab Pages, Netlify, etc.) you must edit the workflow to install Dart Sass before Hugo builds the site[^2]. Some providers allow you to use one of the package managers above, or you can download and extract one of the prebuilt binaries. - -[^2]: You do not have to do this if (a) you have not modified the assets cache location, and (b) you have not set `useResourceCacheWhen` to `never` in your [site configuration], and (c) you add and commit your `resources` directory to your repository. - -#### GitHub Pages - -To install Dart Sass for your builds on GitHub Pages, add this step to the GitHub Pages workflow file: - -```yaml -- name: Install Dart Sass - run: sudo snap install dart-sass -``` - -#### GitLab Pages - -To install Dart Sass for your builds on GitLab Pages, the `.gitlab-ci.yml` file should look something like this: - -```yaml -variables: - HUGO_VERSION: 0.144.2 - DART_SASS_VERSION: 1.85.0 - GIT_DEPTH: 0 - GIT_STRATEGY: clone - GIT_SUBMODULE_STRATEGY: recursive - TZ: America/Los_Angeles -image: - name: golang:1.20-buster -pages: - script: - # Install Dart Sass - - curl -LJO https://github.com/sass/dart-sass/releases/download/${DART_SASS_VERSION}/dart-sass-${DART_SASS_VERSION}-linux-x64.tar.gz - - tar -xf dart-sass-${DART_SASS_VERSION}-linux-x64.tar.gz - - cp -r dart-sass/* /usr/local/bin - - rm -rf dart-sass* - # Install Hugo - - curl -LJO https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb - - apt install -y ./hugo_extended_${HUGO_VERSION}_linux-amd64.deb - - rm hugo_extended_${HUGO_VERSION}_linux-amd64.deb - # Build - - hugo --gc --minify - artifacts: - paths: - - public - rules: - - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH -``` - -#### Netlify - -To install Dart Sass for your builds on Netlify, the `netlify.toml` file should look something like this: - -```toml -[build.environment] -HUGO_VERSION = "0.144.2" -DART_SASS_VERSION = "1.85.0" -NODE_VERSION = "22" -TZ = "America/Los_Angeles" - -[build] -publish = "public" -command = """\ - curl -LJO https://github.com/sass/dart-sass/releases/download/${DART_SASS_VERSION}/dart-sass-${DART_SASS_VERSION}-linux-x64.tar.gz && \ - tar -xf dart-sass-${DART_SASS_VERSION}-linux-x64.tar.gz && \ - rm dart-sass-${DART_SASS_VERSION}-linux-x64.tar.gz && \ - export PATH=/opt/build/repo/dart-sass:$PATH && \ - hugo --gc --minify \ - """ -``` - -[brew.sh]: https://brew.sh/ -[chocolatey.org]: https://community.chocolatey.org/packages/sass -[dart sass]: https://sass-lang.com/dart-sass -[libsass]: https://sass-lang.com/libsass -[prebuilt binaries]: https://github.com/sass/dart-sass/releases/latest -[scoop.sh]: https://scoop.sh/#/apps?q=sass -[site configuration]: /configuration/build/ -[snap package]: /installation/linux/#snap -[snapcraft.io]: https://snapcraft.io/dart-sass -[starter workflow]: https://github.com/actions/starter-workflows/blob/main/pages/hugo.yml -[`publishDir`]: /configuration/all/#publishdir diff --git a/docs/content/en/functions/css/TailwindCSS.md b/docs/content/en/functions/css/TailwindCSS.md deleted file mode 100644 index 6add7373a..000000000 --- a/docs/content/en/functions/css/TailwindCSS.md +++ /dev/null @@ -1,138 +0,0 @@ ---- -title: css.TailwindCSS -description: Processes the given resource with the Tailwind CSS CLI. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: resource.Resource - signatures: ['css.TailwindCSS [OPTIONS] RESOURCE'] ---- - -{{< new-in 0.128.0 />}} - -Use the `css.TailwindCSS` function to process your Tailwind CSS files. This function uses the Tailwind CSS CLI to: - -1. Scan your templates for Tailwind CSS utility class usage. -1. Compile those utility classes into standard CSS. -1. Generate an optimized CSS output file. - -> [!caution] -> Tailwind CSS v4.0 and later requires a relatively [modern browser](https://tailwindcss.com/docs/compatibility#browser-support) to render correctly. - -## Setup - -### Step 1 - -Install the Tailwind CSS CLI v4.0 or later: - -```sh -npm install --save-dev tailwindcss @tailwindcss/cli -``` - -The TailwindCSS CLI is also available as a [standalone executable] if you want to use it without installing Node.js. - -[standalone executable]: https://github.com/tailwindlabs/tailwindcss/releases/latest - -### Step 2 - -Add this to your site configuration: - -{{< code-toggle file=hugo copy=true >}} -[[module.mounts]] -source = "assets" -target = "assets" -[[module.mounts]] -source = "hugo_stats.json" -target = "assets/notwatching/hugo_stats.json" -disableWatch = true -[build.buildStats] -enable = true -[[build.cachebusters]] -source = "assets/notwatching/hugo_stats\\.json" -target = "css" -[[build.cachebusters]] -source = "(postcss|tailwind)\\.config\\.js" -target = "css" -{{< /code-toggle >}} - -### Step 3 - -Create a CSS entry file: - -```css {file="assets/css/main.css" copy=true} -@import "tailwindcss"; -@source "hugo_stats.json"; -``` - -Tailwind CSS respects `.gitignore` files. This means that if `hugo_stats.json` is listed in your `.gitignore` file, Tailwind CSS will ignore it. To make `hugo_stats.json` available to Tailwind CSS you must explicitly source it as shown in the example above. - -### Step 4 - -Create a partial template to process the CSS with the Tailwind CSS CLI: - -```go-html-template {file="layouts/partials/css.html" copy=true} -{{ with (templates.Defer (dict "key" "global")) }} - {{ with resources.Get "css/main.css" }} - {{ $opts := dict - "minify" hugo.IsProduction - "inlineImports" true - }} - {{ with . | css.TailwindCSS $opts }} - {{ if hugo.IsProduction }} - {{ with . | fingerprint }} - - {{ end }} - {{ else }} - - {{ end }} - {{ end }} - {{ end }} -{{ end }} -``` - -### Step 5 - -Call the partial template from your base template: - -```go-html-template {file="layouts/_default/baseof.html"} - - ... - {{ partialCached "css.html" . }} - ... - -``` - -### Step 6 - -Optionally create a `tailwind.config.js` file in the root of your project as shown below. This is necessary if you use the [Tailwind CSS IntelliSense -extension] for Visual Studio Code. - -[Tailwind CSS IntelliSense -extension]: https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss - -```js {file="tailwind.config.js" copy=true} -/* -This file is present to satisfy a requirement of the Tailwind CSS IntelliSense -extension for Visual Studio Code. - -https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss - -The rest of this file is intentionally empty. -*/ -``` - -## Options - -minify -: (`bool`) Whether to optimize and minify the output. Default is `false`. - -optimize -: (`bool`) Whether to optimize the output without minifying. Default is `false`. - -inlineImports -: (`bool`) Whether to enable inlining of `@import` statements. Inlining is performed recursively, but currently once only per file. It is not possible to import the same file in different scopes (root, media query, etc.). Note that this import routine does not care about the CSS specification, so you can have `@import` statements anywhere in the file. Default is `false`. - -skipInlineImportsNotFound -: (`bool`) Whether to allow the build process to continue despite unresolved import statements, preserving the original import declarations. It is important to note that the inline importer does not process URL-based imports or those with media queries, and these will remain unaltered even when this option is disabled. Default is `false`. diff --git a/docs/content/en/functions/css/_index.md b/docs/content/en/functions/css/_index.md deleted file mode 100644 index 9faabbbe9..000000000 --- a/docs/content/en/functions/css/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: CSS functions -linkTitle: css -description: Use these functions to work with CSS and Sass files. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/functions/data/GetCSV.md b/docs/content/en/functions/data/GetCSV.md deleted file mode 100644 index 39c71b06c..000000000 --- a/docs/content/en/functions/data/GetCSV.md +++ /dev/null @@ -1,144 +0,0 @@ ---- -title: data.GetCSV -description: Returns an array of arrays from a local or remote CSV file, or an error if the file does not exist. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [getCSV] - returnType: '[][]string' - signatures: ['data.GetCSV SEPARATOR INPUT... [OPTIONS]'] -expiryDate: 2026-02-19 # deprecated 2024-02-19 in v0.123.0 ---- - -{{< deprecated-in 0.123.0 >}} -Instead, use [`transform.Unmarshal`] with a [global resource](g), [page resource](g), or [remote resource](g). - -See the [remote data example]. - -[`transform.Unmarshal`]: /functions/transform/unmarshal/ -[remote data example]: /functions/resources/getremote/#remote-data -{{< /deprecated-in >}} - -Given the following directory structure: - -```text -my-project/ -└── other-files/ - └── pets.csv -``` - -Access the data with either of the following: - -```go-html-template -{{ $data := getCSV "," "other-files/pets.csv" }} -{{ $data := getCSV "," "other-files/" "pets.csv" }} -``` - -> [!note] -> When working with local data, the file path is relative to the working directory. -> -> You must not place CSV files in the project's `data` directory. - -Access remote data with either of the following: - -```go-html-template -{{ $data := getCSV "," "https://example.org/pets.csv" }} -{{ $data := getCSV "," "https://example.org/" "pets.csv" }} -``` - -The resulting data structure is an array of arrays: - -```json -[ - ["name","type","breed","age"], - ["Spot","dog","Collie","3"], - ["Felix","cat","Malicious","7"] -] -``` - -## Options - -Add headers to the request by providing an options map: - -```go-html-template -{{ $opts := dict "Authorization" "Bearer abcd" }} -{{ $data := getCSV "," "https://example.org/pets.csv" $opts }} -``` - -Add multiple headers using a slice: - -```go-html-template -{{ $opts := dict "X-List" (slice "a" "b" "c") }} -{{ $data := getCSV "," "https://example.org/pets.csv" $opts }} -``` - -## Global resource alternative - -Consider using the [`resources.Get`] function with [`transform.Unmarshal`] when accessing a global resource. - -```text -my-project/ -└── assets/ - └── data/ - └── pets.csv -``` - -```go-html-template -{{ $data := dict }} -{{ $p := "data/pets.csv" }} -{{ with resources.Get $p }} - {{ $opts := dict "delimiter" "," }} - {{ $data = . | transform.Unmarshal $opts }} -{{ else }} - {{ errorf "Unable to get resource %q" $p }} -{{ end }} -``` - -## Page resource alternative - -Consider using the [`Resources.Get`] method with [`transform.Unmarshal`] when accessing a page resource. - -```text -my-project/ -└── content/ - └── posts/ - └── my-pets/ - ├── index.md - └── pets.csv -``` - -```go-html-template -{{ $data := dict }} -{{ $p := "pets.csv" }} -{{ with .Resources.Get $p }} - {{ $opts := dict "delimiter" "," }} - {{ $data = . | transform.Unmarshal $opts }} -{{ else }} - {{ errorf "Unable to get resource %q" $p }} -{{ end }} -``` - -## Remote resource alternative - -Consider using the [`resources.GetRemote`] function with [`transform.Unmarshal`] when accessing a remote resource to improve error handling and cache control. - -```go-html-template -{{ $data := dict }} -{{ $url := "https://example.org/pets.csv" }} -{{ with try (resources.GetRemote $url) }} - {{ with .Err }} - {{ errorf "%s" . }} - {{ else with .Value }} - {{ $opts := dict "delimiter" "," }} - {{ $data = . | transform.Unmarshal $opts }} - {{ else }} - {{ errorf "Unable to get remote resource %q" $url }} - {{ end }} -{{ end }} -``` - -[`Resources.Get`]: /methods/page/resources/ -[`resources.GetRemote`]: /functions/resources/getremote/ -[`resources.Get`]: /functions/resources/get/ -[`transform.Unmarshal`]: /functions/transform/unmarshal/ diff --git a/docs/content/en/functions/data/GetJSON.md b/docs/content/en/functions/data/GetJSON.md deleted file mode 100644 index 9cdea9287..000000000 --- a/docs/content/en/functions/data/GetJSON.md +++ /dev/null @@ -1,146 +0,0 @@ ---- -title: data.GetJSON -description: Returns a JSON object from a local or remote JSON file, or an error if the file does not exist. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [getJSON] - returnType: any - signatures: ['data.GetJSON INPUT... [OPTIONS]'] -expiryDate: 2026-02-19 # deprecated 2024-02-19 in v0.123.0 ---- - -{{< deprecated-in 0.123.0 >}} -Instead, use [`transform.Unmarshal`] with a [global resource](g), [page resource](g), or [remote resource](g). - -See the [remote data example]. - -[`transform.Unmarshal`]: /functions/transform/unmarshal/ -[remote data example]: /functions/resources/getremote/#remote-data -{{< /deprecated-in >}} - -Given the following directory structure: - -```text -my-project/ -└── other-files/ - └── books.json -``` - -Access the data with either of the following: - -```go-html-template -{{ $data := getJSON "other-files/books.json" }} -{{ $data := getJSON "other-files/" "books.json" }} -``` - -> [!note] -> When working with local data, the file path is relative to the working directory. - -Access remote data with either of the following: - -```go-html-template -{{ $data := getJSON "https://example.org/books.json" }} -{{ $data := getJSON "https://example.org/" "books.json" }} -``` - -The resulting data structure is a JSON object: - -```json -[ - { - "author": "Victor Hugo", - "rating": 5, - "title": "Les Misérables" - }, - { - "author": "Victor Hugo", - "rating": 4, - "title": "The Hunchback of Notre Dame" - } -] -``` - -## Options - -Add headers to the request by providing an options map: - -```go-html-template -{{ $opts := dict "Authorization" "Bearer abcd" }} -{{ $data := getJSON "https://example.org/books.json" $opts }} -``` - -Add multiple headers using a slice: - -```go-html-template -{{ $opts := dict "X-List" (slice "a" "b" "c") }} -{{ $data := getJSON "https://example.org/books.json" $opts }} -``` - -## Global resource alternative - -Consider using the [`resources.Get`] function with [`transform.Unmarshal`] when accessing a global resource. - -```text -my-project/ -└── assets/ - └── data/ - └── books.json -``` - -```go-html-template -{{ $data := dict }} -{{ $p := "data/books.json" }} -{{ with resources.Get $p }} - {{ $data = . | transform.Unmarshal }} -{{ else }} - {{ errorf "Unable to get resource %q" $p }} -{{ end }} -``` - -## Page resource alternative - -Consider using the [`Resources.Get`] method with [`transform.Unmarshal`] when accessing a page resource. - -```text -my-project/ -└── content/ - └── posts/ - └── reading-list/ - ├── books.json - └── index.md -``` - -```go-html-template -{{ $data := dict }} -{{ $p := "books.json" }} -{{ with .Resources.Get $p }} - {{ $data = . | transform.Unmarshal }} -{{ else }} - {{ errorf "Unable to get resource %q" $p }} -{{ end }} -``` - -## Remote resource alternative - -Consider using the [`resources.GetRemote`] function with [`transform.Unmarshal`] when accessing a remote resource to improve error handling and cache control. - -```go-html-template -{{ $data := dict }} -{{ $url := "https://example.org/books.json" }} -{{ with try (resources.GetRemote $url) }} - {{ with .Err }} - {{ errorf "%s" . }} - {{ else with .Value }} - {{ $data = . | transform.Unmarshal }} - {{ else }} - {{ errorf "Unable to get remote resource %q" $url }} - {{ end }} -{{ end }} -``` - -[`Resources.Get`]: /methods/page/resources/ -[`resources.GetRemote`]: /functions/resources/getremote/ -[`resources.Get`]: /functions/resources/get/ -[`transform.Unmarshal`]: /functions/transform/unmarshal/ diff --git a/docs/content/en/functions/data/_index.md b/docs/content/en/functions/data/_index.md deleted file mode 100644 index 2177bc528..000000000 --- a/docs/content/en/functions/data/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Data functions -linkTitle: data -description: Use these functions to read local or remote data files. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/functions/debug/Dump.md b/docs/content/en/functions/debug/Dump.md deleted file mode 100644 index df846ac3d..000000000 --- a/docs/content/en/functions/debug/Dump.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: debug.Dump -description: Returns an object dump as a string. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: string - signatures: [debug.Dump VALUE] ---- - -```go-html-template -
    {{ debug.Dump site.Data.books }}
    -``` - -```json -[ - { - "author": "Victor Hugo", - "rating": 4, - "title": "The Hunchback of Notre Dame" - }, - { - "author": "Victor Hugo", - "rating": 5, - "title": "Les Misérables" - } -] -``` - -> [!note] -> Output from this function may change from one release to the next. Use for debugging only. diff --git a/docs/content/en/functions/debug/Timer.md b/docs/content/en/functions/debug/Timer.md deleted file mode 100644 index c2cd59211..000000000 --- a/docs/content/en/functions/debug/Timer.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: debug.Timer -description: Creates a named timer that reports elapsed time to the console. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: debug.Timer - signatures: [debug.Timer NAME] ---- - -{{< new-in 0.120.0 />}} - -Use the `debug.Timer` function to determine execution time for a block of code, useful for finding performance bottlenecks in templates. - -The timer starts when you instantiate it, and stops when you call its `Stop` method. - -```go-html-template -{{ $t := debug.Timer "TestSqrt" }} -{{ range seq 2000 }} - {{ $f := math.Sqrt . }} -{{ end }} -{{ $t.Stop }} -``` - -Use the `--logLevel info` command line flag when you build the site. - -```sh -hugo --logLevel info -``` - -The results are displayed in the console at the end of the build. You can have as many timers as you want and if you don't stop them, they will be stopped at the end of build. - -```text -INFO timer: name TestSqrt count 1002 duration 2.496017496s average 2.491035ms median 2.282291ms -``` diff --git a/docs/content/en/functions/debug/_index.md b/docs/content/en/functions/debug/_index.md deleted file mode 100644 index 49fe416ed..000000000 --- a/docs/content/en/functions/debug/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Debug functions -linkTitle: debug -description: Use these functions to debug your templates. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/functions/diagrams/Goat.md b/docs/content/en/functions/diagrams/Goat.md deleted file mode 100644 index e2f55eee0..000000000 --- a/docs/content/en/functions/diagrams/Goat.md +++ /dev/null @@ -1,115 +0,0 @@ ---- -title: diagrams.Goat -description: Returns an SVGDiagram object created from the given GoAT markup and options. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: diagrams.SVGDiagram - signatures: [diagrams.Goat MARKUP] ---- - -Useful in a [code block render hook], the `diagrams.Goat` function returns an SVGDiagram object created from the given [GoAT] markup. - -## Methods - -The SVGDiagram object has the following methods: - -Inner -: (`template.HTML`) Returns the SVG child elements without a wrapping `svg` element, allowing you to create your own wrapper. - -Wrapped -: (`template.HTML`) Returns the SVG child elements wrapped in an `svg` element. - -Width -: (`int`) Returns the width of the rendered diagram, in pixels. - -Height -: (`int`) Returns the height of the rendered diagram, in pixels. - -## GoAT Diagrams - -Hugo natively supports GoAT diagrams with an [embedded code block render hook]. - -This Markdown: - -````text -```goat -.---. .-. .-. .-. .---. -| A +--->| 1 |<--->| 2 |<--->| 3 |<---+ B | -'---' '-' '+' '+' '---' -``` -```` - -Is rendered to: - -```html -
    - - ... - -
    -``` - -Which appears in your browser as: - -```goat {class="mw6-ns"} -.---. .-. .-. .-. .---. -| A +--->| 1 |<--->| 2 |<--->| 3 |<---+ B | -'---' '-' '+' '+' '---' -``` - -To customize rendering, override Hugo's [embedded code block render hook] for GoAT diagrams. - -## Code block render hook - -By way of example, let's create a code block render hook to render GoAT diagrams as `figure` elements with an optional caption. - -```go-html-template {file="layouts/_default/_markup/render-codeblock-goat.html"} -{{ $caption := or .Attributes.caption "" }} -{{ $class := or .Attributes.class "diagram" }} -{{ $id := or .Attributes.id (printf "diagram-%d" (add 1 .Ordinal)) }} - -
    - {{ with diagrams.Goat (trim .Inner "\n\r") }} - - {{ .Inner }} - - {{ end }} -
    {{ $caption }}
    -
    -``` - -This Markdown: - -````text {file="content/example.md" } -```goat {class="foo" caption="Diagram 1: Example"} -.---. .-. .-. .-. .---. -| A +--->| 1 |<--->| 2 |<--->| 3 |<---+ B | -'---' '-' '+' '+' '---' -``` -```` - -Is rendered to: - -```html -
    - - ... - -
    Diagram 1: Example
    -
    -``` - -Use CSS to style the SVG as needed: - -```css -svg.foo { - font-family: "Segoe UI","Noto Sans",Helvetica,Arial,sans-serif -} -``` - -[code block render hook]: /render-hooks/code-blocks/ -[embedded code block render hook]: {{% eturl render-codeblock-goat %}} -[GoAT]: https://github.com/bep/goat diff --git a/docs/content/en/functions/diagrams/_index.md b/docs/content/en/functions/diagrams/_index.md deleted file mode 100644 index 6aa407071..000000000 --- a/docs/content/en/functions/diagrams/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Diagram functions -linkTitle: diagrams -description: Use these functions to render diagrams. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/functions/encoding/Base64Decode.md b/docs/content/en/functions/encoding/Base64Decode.md deleted file mode 100644 index 5237e904f..000000000 --- a/docs/content/en/functions/encoding/Base64Decode.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: encoding.Base64Decode -description: Returns the base64 decoding of the given content. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [base64Decode] - returnType: string - signatures: [encoding.Base64Decode INPUT] -aliases: [/functions/base64Decode] ---- - -```go-html-template -{{ "SHVnbw==" | base64Decode }} → Hugo -``` - -Use the `base64Decode` function to decode responses from APIs. For example, the result of this call to GitHub's API contains the base64-encoded representation of the repository's README file: - -```text -https://api.github.com/repos/gohugoio/hugo/readme -``` - -To retrieve and render the content: - -```go-html-template -{{ $url := "https://api.github.com/repos/gohugoio/hugo/readme" }} -{{ with try (resources.GetRemote $url) }} - {{ with .Err }} - {{ errorf "%s" . }} - {{ else with .Value }} - {{ with . | transform.Unmarshal }} - {{ .content | base64Decode | markdownify }} - {{ end }} - {{ else }} - {{ errorf "Unable to get remote resource %q" $url }} - {{ end }} -{{ end }} -``` diff --git a/docs/content/en/functions/encoding/Base64Encode.md b/docs/content/en/functions/encoding/Base64Encode.md deleted file mode 100644 index e19d6773c..000000000 --- a/docs/content/en/functions/encoding/Base64Encode.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: encoding.Base64Encode -description: Returns the base64 decoding of the given content. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [base64Encode] - returnType: string - signatures: [encoding.Base64Encode INPUT] -aliases: [/functions/base64, /functions/base64Encode] ---- - -```go-html-template -{{ "Hugo" | base64Encode }} → SHVnbw== -``` diff --git a/docs/content/en/functions/encoding/Jsonify.md b/docs/content/en/functions/encoding/Jsonify.md deleted file mode 100644 index 1d60dd68d..000000000 --- a/docs/content/en/functions/encoding/Jsonify.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: encoding.Jsonify -description: Encodes the given object to JSON. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [jsonify] - returnType: template.HTML - signatures: ['encoding.Jsonify [OPTIONS] INPUT'] -aliases: [/functions/jsonify] ---- - -To customize the printing of the JSON, pass an options map as the first -argument. Supported options are "prefix" and "indent". Each JSON element in -the output will begin on a new line beginning with *prefix* followed by one or -more copies of *indent* according to the indentation nesting. - -```go-html-template -{{ dict "title" .Title "content" .Plain | jsonify }} -{{ dict "title" .Title "content" .Plain | jsonify (dict "indent" " ") }} -{{ dict "title" .Title "content" .Plain | jsonify (dict "prefix" " " "indent" " ") }} -``` - -## Options - -indent -: (`string`) Indentation to use. Default is "". - -prefix -: (`string`) Indentation prefix. Default is "". - -noHTMLEscape -: (`bool`) Whether to disable escaping of problematic HTML characters inside JSON quoted strings. The default behavior is to escape `&`, `<`, and `>` to `\u0026`, `\u003c`, and `\u003e` to avoid certain safety problems that can arise when embedding JSON in HTML. Default is `false`. diff --git a/docs/content/en/functions/encoding/_index.md b/docs/content/en/functions/encoding/_index.md deleted file mode 100644 index f2819f0a7..000000000 --- a/docs/content/en/functions/encoding/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Encoding functions -linkTitle: encoding -description: Use these functions to encode and decode data. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/functions/fmt/Errorf.md b/docs/content/en/functions/fmt/Errorf.md deleted file mode 100644 index 799622f0e..000000000 --- a/docs/content/en/functions/fmt/Errorf.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: fmt.Errorf -description: Log an ERROR from a template. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [errorf] - returnType: string - signatures: ['fmt.Errorf FORMAT [INPUT]'] -aliases: [/functions/errorf] ---- - -{{% include "/_common/functions/fmt/format-string.md" %}} - -The `errorf` function evaluates the format string, then prints the result to the ERROR log and fails the build. - -```go-html-template -{{ errorf "The %q shortcode requires a src argument. See %s" .Name .Position }} -``` - -Use the [`erroridf`] function to allow optional suppression of specific errors. - -[`erroridf`]: /functions/fmt/erroridf/ diff --git a/docs/content/en/functions/fmt/Erroridf.md b/docs/content/en/functions/fmt/Erroridf.md deleted file mode 100644 index 97d628bac..000000000 --- a/docs/content/en/functions/fmt/Erroridf.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: fmt.Erroridf -description: Log a suppressible ERROR from a template. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [erroridf] - returnType: string - signatures: ['fmt.Erroridf ID FORMAT [INPUT]'] -aliases: [/functions/erroridf] ---- - -{{% include "/_common/functions/fmt/format-string.md" %}} - -The `erroridf` function evaluates the format string, then prints the result to the ERROR log and fails the build. Unlike the [`errorf`] function, you may suppress errors logged by the `erroridf` function by adding the message ID to the `ignoreLogs` array in your site configuration. - -This template code: - -```go-html-template -{{ erroridf "error-42" "You should consider fixing this." }} -``` - -Produces this console log: - -```text -ERROR You should consider fixing this. -You can suppress this error by adding the following to your site configuration: -ignoreLogs = ['error-42'] -``` - -To suppress this message: - -{{< code-toggle file=hugo >}} -ignoreLogs = ["error-42"] -{{< /code-toggle >}} - -[`errorf`]: /functions/fmt/errorf/ diff --git a/docs/content/en/functions/fmt/Print.md b/docs/content/en/functions/fmt/Print.md deleted file mode 100644 index f1d169cfa..000000000 --- a/docs/content/en/functions/fmt/Print.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: fmt.Print -description: Prints the default representation of the given arguments using the standard `fmt.Print` function. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [print] - returnType: string - signatures: [fmt.Print INPUT] -aliases: [/functions/print] ---- - -```go-html-template -{{ print "foo" }} → foo -{{ print "foo" "bar" }} → foobar -{{ print (slice 1 2 3) }} → [1 2 3] -``` diff --git a/docs/content/en/functions/fmt/Printf.md b/docs/content/en/functions/fmt/Printf.md deleted file mode 100644 index 68df98609..000000000 --- a/docs/content/en/functions/fmt/Printf.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: fmt.Printf -description: Formats a string using the standard `fmt.Sprintf` function. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [printf] - returnType: string - signatures: ['fmt.Printf FORMAT [INPUT]'] -aliases: [/functions/printf] ---- - -{{% include "/_common/functions/fmt/format-string.md" %}} - -```go-html-template -{{ $var := "world" }} -{{ printf "Hello %s." $var }} → Hello world. -``` - -```go-html-template -{{ $pi := 3.14159265 }} -{{ printf "Pi is approximately %.2f." $pi }} → 3.14 -``` - -Use the `printf` function with the `safeHTMLAttr` function: - -```go-html-template -{{ $desc := "Eat at Joe's" }} - -``` - -Hugo renders this to: - -```html - -``` diff --git a/docs/content/en/functions/fmt/Println.md b/docs/content/en/functions/fmt/Println.md deleted file mode 100644 index b7fe608ff..000000000 --- a/docs/content/en/functions/fmt/Println.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: fmt.Println -description: Prints the default representation of the given argument using the standard `fmt.Print` function and enforces a line break. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [println] - returnType: string - signatures: [fmt.Println INPUT] -aliases: [/functions/println] ---- - -```go-html-template -{{ println "foo" }} → foo\n -``` diff --git a/docs/content/en/functions/fmt/Warnf.md b/docs/content/en/functions/fmt/Warnf.md deleted file mode 100644 index 887a8d47f..000000000 --- a/docs/content/en/functions/fmt/Warnf.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: fmt.Warnf -description: Log a WARNING from a template. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [warnf] - returnType: string - signatures: ['fmt.Warnf FORMAT [INPUT]'] -aliases: [/functions/warnf] ---- - -{{% include "/_common/functions/fmt/format-string.md" %}} - -The `warnf` function evaluates the format string, then prints the result to the WARNING log. Hugo prints each unique message once to avoid flooding the log with duplicate warnings. - -```go-html-template -{{ warnf "The %q shortcode was unable to find %s. See %s" .Name $file .Position }} -``` - -Use the [`warnidf`] function to allow optional suppression of specific warnings. - -To prevent suppression of duplicate messages when using `warnf` for debugging, make each message unique with the [`math.Counter`] function. For example: - -```go-html-template -{{ range site.RegularPages }} - {{ .Section | warnf "%#[2]v [%[1]d]" math.Counter }} -{{ end }} -``` - -[`math.Counter`]: /functions/math/counter/ - -[`warnidf`]: /functions/fmt/warnidf/ diff --git a/docs/content/en/functions/fmt/Warnidf.md b/docs/content/en/functions/fmt/Warnidf.md deleted file mode 100644 index 79ebf81e6..000000000 --- a/docs/content/en/functions/fmt/Warnidf.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -title: fmt.Warnidf -description: Log a suppressible WARNING from a template. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [warnidf] - returnType: string - signatures: ['fmt.Warnidf ID FORMAT [INPUT]'] -aliases: [/functions/warnidf] ---- - -{{< new-in 0.123.0 />}} - -{{% include "/_common/functions/fmt/format-string.md" %}} - -The `warnidf` function evaluates the format string, then prints the result to the WARNING log. Unlike the [`warnf`] function, you may suppress warnings logged by the `warnidf` function by adding the message ID to the `ignoreLogs` array in your site configuration. - -This template code: - -```go-html-template -{{ warnidf "warning-42" "You should consider fixing this." }} -``` - -Produces this console log: - -```text -WARN You should consider fixing this. -You can suppress this warning by adding the following to your site configuration: -ignoreLogs = ['warning-42'] -``` - -To suppress this message: - -{{< code-toggle file=hugo >}} -ignoreLogs = ["warning-42"] -{{< /code-toggle >}} - -[`warnf`]: /functions/fmt/warnf/ diff --git a/docs/content/en/functions/fmt/_index.md b/docs/content/en/functions/fmt/_index.md deleted file mode 100644 index d388df112..000000000 --- a/docs/content/en/functions/fmt/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Fmt functions -linkTitle: fmt -description: Use these functions to print strings within a template or to print messages to the terminal. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/functions/global/_index.md b/docs/content/en/functions/global/_index.md deleted file mode 100644 index 3d935176c..000000000 --- a/docs/content/en/functions/global/_index.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Global functions -linkTitle: global -description: Use these global functions to access page and site data. -categories: [] ---- diff --git a/docs/content/en/functions/global/page.md b/docs/content/en/functions/global/page.md deleted file mode 100644 index 0d4b8070f..000000000 --- a/docs/content/en/functions/global/page.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -title: page -description: Provides global access to a Page object. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: - signatures: [page] -aliases: [/functions/page] ---- - -At the top level of a template that receives a `Page` object in context, these are equivalent: - -```go-html-template -{{ .Params.foo }} -{{ .Page.Params.foo }} -{{ page.Params.foo }} -``` - -When a `Page` object is not in context, you can use the global `page` function: - -```go-html-template -{{ page.Params.foo }} -``` - -> [!note] -> Do not use the global `page` function in shortcodes, partials called by shortcodes, or cached partials. See [warnings](#warnings) below. - -## Explanation - -Hugo almost always passes a `Page` as the data context into the top-level template (e.g., `single.html`). The one exception is the multihost sitemap template. This means that you can access the current page with the `.` in the template. - -But when you are deeply nested inside of a [content view](g), [partial](g), or [render hook](g), it is not always practical or possible to access the `Page` object. - -Use the global `page` function to access the `Page` object from anywhere in any template. - -## Warnings - -### Be aware of top-level context - -The global `page` function accesses the `Page` object passed into the top-level template. - -With this content structure: - -```text -content/ -├── posts/ -│ ├── post-1.md -│ ├── post-2.md -│ └── post-3.md -└── _index.md <-- title is "My Home Page" -``` - -And this code in the home template: - -```go-html-template -{{ range site.Sections }} - {{ range .Pages }} - {{ page.Title }} - {{ end }} -{{ end }} -``` - -The rendered output will be: - -```text -My Home Page -My Home Page -My Home Page -``` - -In the example above, the global `page` function accesses the `Page` object passed into the home template; it does not access the `Page` object of the iterated pages. - -### Be aware of caching - -Do not use the global `page` function in: - -- Shortcodes -- Partials called by shortcodes -- Partials cached by the [`partialCached`] function - -Hugo caches rendered shortcodes. If you use the global `page` function within a shortcode, and the page content is rendered in two or more templates, the cached shortcode may be incorrect. - -Consider this section template: - -```go-html-template -{{ range .Pages }} -

    {{ .LinkTitle }}

    - {{ .Summary }} -{{ end }} -``` - -When you call the [`Summary`] method, Hugo renders the page content including shortcodes. In this case, within a shortcode, the global `page` function accesses the `Page` object of the section page, not the content page. - -If Hugo renders the section page before a content page, the cached rendered shortcode will be incorrect. You cannot control the rendering sequence due to concurrency. - -[`partialCached`]: /functions/partials/includecached/ -[`Summary`]: /methods/page/summary/ diff --git a/docs/content/en/functions/global/site.md b/docs/content/en/functions/global/site.md deleted file mode 100644 index be0c6730e..000000000 --- a/docs/content/en/functions/global/site.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: site -description: Provides global access to the current Site object. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: - signatures: [site] -aliases: [/functions/site] ---- - -Use the `site` function to return the `Site` object regardless of current context. - -```go-html-template -{{ site.Params.foo }} -``` - -When the `Site` object is in context you can use the `Site` property: - -```go-html-template - -{{ .Site.Params.foo }} - -{{ $.Site.Params.foo }} -``` - -> [!note] -> To simplify your templates, use the global `site` function regardless of whether the `Site` object is in context. diff --git a/docs/content/en/functions/go-template/_index.md b/docs/content/en/functions/go-template/_index.md deleted file mode 100644 index 627dc2849..000000000 --- a/docs/content/en/functions/go-template/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Go template functions, operators, and statements -linkTitle: go template -description: These are the functions, operators, and statements provided by Go's text/template package. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/functions/go-template/and.md b/docs/content/en/functions/go-template/and.md deleted file mode 100644 index 77906df52..000000000 --- a/docs/content/en/functions/go-template/and.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: and -description: Returns the first falsy argument. If all arguments are truthy, returns the last argument. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: any - signatures: [and VALUE...] ---- - -{{% include "/_common/functions/truthy-falsy.md" %}} - -```go-html-template -{{ and 1 0 "" }} → 0 (int) -{{ and 1 false 0 }} → false (bool) - -{{ and 1 2 3 }} → 3 (int) -{{ and "a" "b" "c" }} → c (string) -{{ and "a" 1 true }} → true (bool) -``` diff --git a/docs/content/en/functions/go-template/block.md b/docs/content/en/functions/go-template/block.md deleted file mode 100644 index bffab1f8c..000000000 --- a/docs/content/en/functions/go-template/block.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: block -description: Defines a template and executes it in place. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: - signatures: [block NAME CONTEXT] ---- - -A block is shorthand for defining a template: - -```go-html-template -{{ define "name" }} T1 {{ end }} -``` - -and then executing it in place: - -```go-html-template -{{ template "name" pipeline }} -``` -The typical use is to define a set of root templates that are then customized by redefining the block templates within. - -```go-html-template {file="layouts/_default/baseof.html"} - -
    - {{ block "main" . }} - {{ print "default value if 'main' template is empty" }} - {{ end }} -
    - -``` - -```go-html-template {file="layouts/_default/single.html"} -{{ define "main" }} -

    {{ .Title }}

    - {{ .Content }} -{{ end }} -``` - -```go-html-template {file="layouts/_default/list.html"} -{{ define "main" }} -

    {{ .Title }}

    - {{ .Content }} - {{ range .Pages }} -

    {{ .LinkTitle }}

    - {{ end }} -{{ end }} -``` - -{{% include "/_common/functions/go-template/text-template.md" %}} diff --git a/docs/content/en/functions/go-template/break.md b/docs/content/en/functions/go-template/break.md deleted file mode 100644 index 9236ec91e..000000000 --- a/docs/content/en/functions/go-template/break.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: break -description: Used with the range statement, stops the innermost iteration and bypasses all remaining iterations. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: - signatures: [break] ---- - -This template code: - -```go-html-template -{{ $s := slice "foo" "bar" "baz" }} -{{ range $s }} - {{ if eq . "bar" }} - {{ break }} - {{ end }} -

    {{ . }}

    -{{ end }} -``` - -Is rendered to: - -```html -

    foo

    -``` - -{{% include "/_common/functions/go-template/text-template.md" %}} diff --git a/docs/content/en/functions/go-template/continue.md b/docs/content/en/functions/go-template/continue.md deleted file mode 100644 index 0b9339bf4..000000000 --- a/docs/content/en/functions/go-template/continue.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: continue -description: Used with the range statement, stops the innermost iteration and continues to the next iteration. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: - signatures: [continue] ---- - -This template code: - -```go-html-template -{{ $s := slice "foo" "bar" "baz" }} -{{ range $s }} - {{ if eq . "bar" }} - {{ continue }} - {{ end }} -

    {{ . }}

    -{{ end }} -``` - -Is rendered to: - -```html -

    foo

    -

    baz

    -``` - -{{% include "/_common/functions/go-template/text-template.md" %}} diff --git a/docs/content/en/functions/go-template/define.md b/docs/content/en/functions/go-template/define.md deleted file mode 100644 index 19762a3d6..000000000 --- a/docs/content/en/functions/go-template/define.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: define -description: Defines a template. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: - signatures: [define NAME] ---- - -Use with the [`block`] statement: - -```go-html-template -{{ block "main" . }} - {{ print "default value if 'main' template is empty" }} -{{ end }} - -{{ define "main" }} -

    {{ .Title }}

    - {{ .Content }} -{{ end }} -``` - -Use with the [`partial`] function: - -```go-html-template -{{ partial "inline/foo.html" (dict "answer" 42) }} - -{{ define "partials/inline/foo.html" }} - {{ printf "The answer is %v." .answer }} -{{ end }} -``` - -Use with the [`template`] function: - -```go-html-template -{{ template "foo" (dict "answer" 42) }} - -{{ define "foo" }} - {{ printf "The answer is %v." .answer }} -{{ end }} -``` - -[`block`]: /functions/go-template/block/ -[`template`]: /functions/go-template/block/ -[`partial`]: /functions/partials/include/ - -{{% include "/_common/functions/go-template/text-template.md" %}} diff --git a/docs/content/en/functions/go-template/else.md b/docs/content/en/functions/go-template/else.md deleted file mode 100644 index db3980070..000000000 --- a/docs/content/en/functions/go-template/else.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: else -description: Begins an alternate block for if, with, and range statements. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: - signatures: [else VALUE] ---- - -Use with the [`if`] statement: - -```go-html-template -{{ $var := "foo" }} -{{ if $var }} - {{ $var }} → foo -{{ else }} - {{ print "var is falsy" }} -{{ end }} -``` - -Use with the [`with`] statement: - -```go-html-template -{{ $var := "foo" }} -{{ with $var }} - {{ . }} → foo -{{ else }} - {{ print "var is falsy" }} -{{ end }} -``` - -Use with the [`range`] statement: - -```go-html-template -{{ $var := slice 1 2 3 }} -{{ range $var }} - {{ . }} → 1 2 3 -{{ else }} - {{ print "var is falsy" }} -{{ end }} -``` - -Use `else if` to check multiple conditions. - -```go-html-template -{{ $var := 12 }} -{{ if eq $var 6 }} - {{ print "var is 6" }} -{{ else if eq $var 7 }} - {{ print "var is 7" }} -{{ else if eq $var 42 }} - {{ print "var is 42" }} -{{ else }} - {{ print "var is something else" }} -{{ end }} -``` - -{{% include "/_common/functions/go-template/text-template.md" %}} - -[`if`]: /functions/go-template/if/ -[`with`]: /functions/go-template/with/ -[`range`]: /functions/go-template/range/ diff --git a/docs/content/en/functions/go-template/end.md b/docs/content/en/functions/go-template/end.md deleted file mode 100644 index 6de120724..000000000 --- a/docs/content/en/functions/go-template/end.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: end -description: Terminates if, with, range, block, and define statements. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: - signatures: [end] ---- - -Use with the [`if`] statement: - -```go-html-template -{{ $var := "foo" }} -{{ if $var }} - {{ $var }} → foo -{{ end }} -``` - -Use with the [`with`] statement: - -```go-html-template -{{ $var := "foo" }} -{{ with $var }} - {{ . }} → foo -{{ end }} -``` - -Use with the [`range`] statement: - -```go-html-template -{{ $var := slice 1 2 3 }} -{{ range $var }} - {{ . }} → 1 2 3 -{{ end }} -``` - -Use with the [`block`] statement: - -```go-html-template -{{ block "main" . }}{{ end }} -``` - -Use with the [`define`] statement: - -```go-html-template -{{ define "main" }} - {{ print "this is the main section" }} -{{ end }} -``` - -{{% include "/_common/functions/go-template/text-template.md" %}} - -[`block`]: /functions/go-template/block/ -[`define`]: /functions/go-template/define/ -[`if`]: /functions/go-template/if/ -[`range`]: /functions/go-template/range/ -[`with`]: /functions/go-template/with/ diff --git a/docs/content/en/functions/go-template/if.md b/docs/content/en/functions/go-template/if.md deleted file mode 100644 index af2989cca..000000000 --- a/docs/content/en/functions/go-template/if.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: if -description: Executes the block if the expression is truthy. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: - signatures: [if EXPR] ---- - -{{% include "/_common/functions/truthy-falsy.md" %}} - -```go-html-template -{{ $var := "foo" }} -{{ if $var }} - {{ $var }} → foo -{{ end }} -``` - -Use with the [`else`] statement: - -```go-html-template -{{ $var := "foo" }} -{{ if $var }} - {{ $var }} → foo -{{ else }} - {{ print "var is falsy" }} -{{ end }} -``` - -Use `else if` to check multiple conditions: - -```go-html-template -{{ $var := 12 }} -{{ if eq $var 6 }} - {{ print "var is 6" }} -{{ else if eq $var 7 }} - {{ print "var is 7" }} -{{ else if eq $var 42 }} - {{ print "var is 42" }} -{{ else }} - {{ print "var is something else" }} -{{ end }} -``` - -{{% include "/_common/functions/go-template/text-template.md" %}} - -[`else`]: /functions/go-template/else/ diff --git a/docs/content/en/functions/go-template/len.md b/docs/content/en/functions/go-template/len.md deleted file mode 100644 index 6a13784e3..000000000 --- a/docs/content/en/functions/go-template/len.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: len -description: Returns the length of a string, slice, map, or collection. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: int - signatures: [len VALUE] -aliases: [/functions/len] ---- - -With a string: - -```go-html-template -{{ "ab" | len }} → 2 -{{ "" | len }} → 0 -``` - -With a slice: - -```go-html-template -{{ slice "a" "b" | len }} → 2 -{{ slice | len }} → 0 -``` - -With a map: - -```go-html-template -{{ dict "a" 1 "b" 2 | len }} → 2 -{{ dict | len }} → 0 -``` - -With a collection: - -```go-html-template -{{ site.RegularPages | len }} → 42 -``` - -You may also determine the number of pages in a collection with: - -```go-html-template -{{ site.RegularPages.Len }} → 42 -``` - -{{% include "/_common/functions/go-template/text-template.md" %}} diff --git a/docs/content/en/functions/go-template/not.md b/docs/content/en/functions/go-template/not.md deleted file mode 100644 index fd8b9afae..000000000 --- a/docs/content/en/functions/go-template/not.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: not -description: Returns the boolean negation of its single argument. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: bool - signatures: [not VALUE] ---- - -Unlike the `and` and `or` operators, the `not` operator always returns a boolean value. - -```go-html-template -{{ not true }} → false -{{ not false }} → true - -{{ not 1 }} → false -{{ not 0 }} → true - -{{ not "x" }} → false -{{ not "" }} → true -``` - -Use the `not` operator, twice in succession, to cast any value to a boolean value. For example: - -```go-html-template -{{ 42 | not | not }} → true -{{ "" | not | not }} → false -``` - -{{% include "/_common/functions/go-template/text-template.md" %}} diff --git a/docs/content/en/functions/go-template/or.md b/docs/content/en/functions/go-template/or.md deleted file mode 100644 index 2f55fe479..000000000 --- a/docs/content/en/functions/go-template/or.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: or -description: Returns the first truthy argument. If all arguments are falsy, returns the last argument. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: any - signatures: [or VALUE...] ---- - -{{% include "/_common/functions/truthy-falsy.md" %}} - -```go-html-template -{{ or 0 1 2 }} → 1 -{{ or false "a" 1 }} → a -{{ or 0 true "a" }} → true - -{{ or false "" 0 }} → 0 -{{ or 0 "" false }} → false -``` - -{{% include "/_common/functions/go-template/text-template.md" %}} diff --git a/docs/content/en/functions/go-template/range.md b/docs/content/en/functions/go-template/range.md deleted file mode 100644 index a06907c79..000000000 --- a/docs/content/en/functions/go-template/range.md +++ /dev/null @@ -1,190 +0,0 @@ ---- -title: range -description: Iterates over a non-empty collection, binds context (the dot) to successive elements, and executes the block. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: - signatures: [range COLLECTION] -aliases: [/functions/range] ---- - -{{% include "/_common/functions/truthy-falsy.md" %}} - -```go-html-template -{{ $s := slice "foo" "bar" "baz" }} -{{ range $s }} - {{ . }} → foo bar baz -{{ end }} -``` - -Use with the [`else`] statement: - -```go-html-template -{{ $s := slice "foo" "bar" "baz" }} -{{ range $s }} -

    {{ . }}

    -{{ else }} -

    The collection is empty

    -{{ end }} -``` - -Within a range block: - -- Use the [`continue`] statement to stop the innermost iteration and continue to the next iteration -- Use the [`break`] statement to stop the innermost iteration and bypass all remaining iterations - -## Understanding context - -At the top of a page template, the [context](g) (the dot) is a `Page` object. Within the `range` block, the context is bound to each successive element. - -With this contrived example that uses the [`seq`] function to generate a slice of integers: - -```go-html-template -{{ range seq 3 }} - {{ .Title }} -{{ end }} -``` - -Hugo will throw an error: - - can't evaluate field Title in type int - -The error occurs because we are trying to use the `.Title` method on an integer instead of a `Page` object. Within the `range` block, if we want to render the page title, we need to get the context passed into the template. - -> [!note] -> Use the `$` to get the context passed into the template. - -This template will render the page title three times: - -```go-html-template -{{ range seq 3 }} - {{ $.Title }} -{{ end }} -``` - -> [!note] -> Gaining a thorough understanding of context is critical for anyone writing template code. - -## Array or slice of scalars - -This template code: - -```go-html-template -{{ $s := slice "foo" "bar" "baz" }} -{{ range $s }} -

    {{ . }}

    -{{ end }} -``` - -Is rendered to: - -```html -

    foo

    -

    bar

    -

    baz

    -``` - -This template code: - -```go-html-template -{{ $s := slice "foo" "bar" "baz" }} -{{ range $v := $s }} -

    {{ $v }}

    -{{ end }} -``` - -Is rendered to: - -```html -

    foo

    -

    bar

    -

    baz

    -``` - -This template code: - -```go-html-template -{{ $s := slice "foo" "bar" "baz" }} -{{ range $k, $v := $s }} -

    {{ $k }}: {{ $v }}

    -{{ end }} -``` - -Is rendered to: - -```html -

    0: foo

    -

    1: bar

    -

    2: baz

    -``` - -## Array or slice of maps - -This template code: - -```go-html-template -{{ $m := slice - (dict "name" "John" "age" 30) - (dict "name" "Will" "age" 28) - (dict "name" "Joey" "age" 24) -}} -{{ range $m }} -

    {{ .name }} is {{ .age }}

    -{{ end }} -``` - -Is rendered to: - -```html -

    John is 30

    -

    Will is 28

    -

    Joey is 24

    -``` - -## Array or slice of pages - -This template code: - -```go-html-template -{{ range where site.RegularPages "Type" "articles" }} -

    {{ .LinkTitle }}

    -{{ end }} -``` - -Is rendered to: - -```html -

    Article 3

    -

    Article 2

    -

    Article 1

    -``` - -## Maps - -This template code: - -```go-html-template -{{ $m := dict "name" "John" "age" 30 }} -{{ range $k, $v := $m }} -

    key = {{ $k }} value = {{ $v }}

    -{{ end }} -``` - -Is rendered to: - -```go-html-template -

    key = age value = 30

    -

    key = name value = John

    -``` - -Unlike ranging over an array or slice, Hugo sorts by key when ranging over a map. - -{{% include "/_common/functions/go-template/text-template.md" %}} - -[`break`]: /functions/go-template/break/ -[`continue`]: /functions/go-template/continue/ -[`else`]: /functions/go-template/else/ -[`seq`]: /functions/collections/seq/ diff --git a/docs/content/en/functions/go-template/return.md b/docs/content/en/functions/go-template/return.md deleted file mode 100644 index eb6ba30cd..000000000 --- a/docs/content/en/functions/go-template/return.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -title: return -description: Used within partial templates, terminates template execution and returns the given value, if any. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: any - signatures: ['return [VALUE]'] ---- - -The `return` statement is a non-standard extension to Go's [text/template package]. Used within partial templates, the `return` statement terminates template execution and returns the given value, if any. - -The returned value may be of any data type including, but not limited to, [`bool`](g), [`float`](g), [`int`](g), [`map`](g), [`resource`](g), [`slice`](g), or [`string`](g). - -A `return` statement without a value returns an empty string of type `template.HTML`. - -> [!note] -> Unlike `return` statements in other languages, Hugo executes the first occurrence of the `return` statement regardless of its position within logical blocks. See [usage](#usage) notes below. - -## Example - -By way of example, let's create a partial template that _renders_ HTML, describing whether the given number is odd or even: - -```go-html-template {file="layouts/partials/odd-or-even.html"} -{{ if math.ModBool . 2 }} -

    {{ . }} is even

    -{{ else }} -

    {{ . }} is odd

    -{{ end }} -``` - -When called, the partial renders HTML: - -```go-html-template -{{ partial "odd-or-even.html" 42 }} →

    42 is even

    -``` - -Instead of rendering HTML, let's create a partial that _returns_ a boolean value, reporting whether the given number is even: - -```go-html-template {file="layouts/partials/is-even.html"} -{{ return math.ModBool . 2 }} -``` - -With this template: - -```go-html-template -{{ $number := 42 }} -{{ if partial "is-even.html" $number }} -

    {{ $number }} is even

    -{{ else }} -

    {{ $number }} is odd

    -{{ end }} -``` - -Hugo renders: - -```html -

    42 is even

    -``` - -See additional examples in the [partial templates] section. - -## Usage - -> [!note] -> Unlike `return` statements in other languages, Hugo executes the first occurrence of the `return` statement regardless of its position within logical blocks. - -A partial that returns a value must contain only one `return` statement, placed at the end of the template. - -For example: - -```go-html-template {file="layouts/partials/is-even.html"} -{{ $result := false }} -{{ if math.ModBool . 2 }} - {{ $result = "even" }} -{{ else }} - {{ $result = "odd" }} -{{ end }} -{{ return $result }} -``` - -> [!note] -> The construct below is incorrect; it contains more than one `return` statement. - -```go-html-template {file="layouts/partials/do-not-do-this.html"} -{{ if math.ModBool . 2 }} - {{ return "even" }} -{{ else }} - {{ return "odd" }} -{{ end }} -``` - -[partial templates]: /templates/partial/#returning-a-value-from-a-partial -[text/template package]: https://pkg.go.dev/text/template diff --git a/docs/content/en/functions/go-template/template.md b/docs/content/en/functions/go-template/template.md deleted file mode 100644 index 053cfcc22..000000000 --- a/docs/content/en/functions/go-template/template.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -title: template -description: Executes the given template, optionally passing context. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: - signatures: ['template NAME [CONTEXT]'] ---- - -Use the `template` function to execute any of these [embedded templates](g): - -- [`disqus.html`] -- [`google_analytics.html`] -- [`opengraph.html`] -- [`pagination.html`] -- [`schema.html`] -- [`twitter_cards.html`] - - - -For example: - -```go-html-template -{{ range (.Paginate .Pages).Pages }} -

    {{ .LinkTitle }}

    -{{ end }} -{{ template "_internal/pagination.html" . }} -``` - -You can also use the `template` function to execute a defined template: - -```go-html-template -{{ template "foo" (dict "answer" 42) }} - -{{ define "foo" }} - {{ printf "The answer is %v." .answer }} -{{ end }} -``` - -The example above can be rewritten using an [inline partial] template: - -```go-html-template -{{ partial "inline/foo.html" (dict "answer" 42) }} - -{{ define "partials/inline/foo.html" }} - {{ printf "The answer is %v." .answer }} -{{ end }} -``` - -The key distinctions between the preceding two examples are: - -1. Inline partials are globally scoped. That means that an inline partial defined in _one_ template may be called from _any_ template. -2. Leveraging the [`partialCached`] function when calling an inline partial allows for performance optimization through result caching. -3. An inline partial can [`return`] a value of any data type instead of rendering a string. - -{{% include "/_common/functions/go-template/text-template.md" %}} - -[`disqus.html`]: /templates/embedded/#disqus -[`google_analytics.html`]: /templates/embedded/#google-analytics -[`opengraph.html`]: /templates/embedded/#open-graph -[`pagination.html`]: /templates/embedded/#pagination -[`partialCached`]: /functions/partials/includecached/ -[`partial`]: /functions/partials/include/ -[`return`]: /functions/go-template/return/ -[`schema.html`]: /templates/embedded/#schema -[`twitter_cards.html`]: /templates/embedded/#x-twitter-cards -[inline partial]: /templates/partial/#inline-partials diff --git a/docs/content/en/functions/go-template/try.md b/docs/content/en/functions/go-template/try.md deleted file mode 100644 index 6aef4da36..000000000 --- a/docs/content/en/functions/go-template/try.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -title: try -description: Returns a TryValue object after evaluating the given expression. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: TryValue - signatures: ['try EXPRESSION'] ---- - -{{< new-in 0.141.0 />}} - -The `try` statement is a non-standard extension to Go's [text/template] package. It introduces a mechanism for handling errors within templates, mimicking the `try-catch` constructs found in other programming languages. - -## Methods - -The `TryValue` object encapsulates the result of evaluating the expression, and provides two methods: - -### Err - -(`string`) Returns a string representation of the error thrown by the expression, if an error occurred, or returns `nil` if the expression evaluated without errors. - -### Value - -(`any`) Returns the result of the expression if the evaluation was successful, or returns `nil` if an error occurred while evaluating the expression. - -## Explanation - -By way of example, let's divide a number by zero: - -```go-html-template -{{ $x := 1 }} -{{ $y := 0 }} -{{ $result := div $x $y }} -{{ printf "%v divided by %v equals %v" $x $y .Value }} -``` - -As expected, the example above throws an error and fails the build: - -```terminfo -Error: error calling div: can't divide the value by 0 -``` - -Instead of failing the build, we can catch the error and emit a warning: - -```go-html-template -{{ $x := 1 }} -{{ $y := 0 }} -{{ with try (div $x $y) }} - {{ with .Err }} - {{ warnf "%s" . }} - {{ else }} - {{ printf "%v divided by %v equals %v" $x $y .Value }} - {{ end }} -{{ end }} -``` - -The error thrown by the expression is logged to the console as a warning: - -```terminfo -WARN error calling div: can't divide the value by 0 -``` - -Now let's change the arguments to avoid dividing by zero: - -```go-html-template -{{ $x := 42 }} -{{ $y := 6 }} -{{ with try (div $x $y) }} - {{ with .Err }} - {{ warnf "%s" . }} - {{ else }} - {{ printf "%v divided by %v equals %v" $x $y .Value }} - {{ end }} -{{ end }} -``` - -Hugo renders the above to: - -```html -42 divided by 6 equals 7 -``` - -## Example - -Error handling is essential when using the [`resources.GetRemote`] function to capture remote resources such as data or images. When calling this function, if the HTTP request fails, Hugo will fail the build. - -Instead of failing the build, we can catch the error and emit a warning: - -```go-html-template -{{ $url := "https://broken-example.org/images/a.jpg" }} -{{ with try (resources.GetRemote $url) }} - {{ with .Err }} - {{ warnf "%s" . }} - {{ else with .Value }} - - {{ else }} - {{ warnf "Unable to get remote resource %q" $url }} - {{ end }} -{{ end }} -``` -In the above, note that the [context](g) within the last conditional block is the `TryValue` object returned by the `try` statement. At this point neither the `Err` nor `Value` methods returned anything, so the current context is not useful. Use the `$` to access the [template context] if needed. - -> [!note] -> Hugo does not classify an HTTP response with status code 404 as an error. In this case `resources.GetRemote` returns nil. - -[`resources.GetRemote`]: /functions/resources/getremote/ -[template context]: /templates/introduction/#template-context -[text/template]: https://pkg.go.dev/text/template diff --git a/docs/content/en/functions/go-template/urlquery.md b/docs/content/en/functions/go-template/urlquery.md deleted file mode 100644 index dc97f867e..000000000 --- a/docs/content/en/functions/go-template/urlquery.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: urlquery -description: Returns the escaped value of the textual representation of its arguments in a form suitable for embedding in a URL query. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: string - signatures: ['urlquery VALUE [VALUE...]'] -aliases: [/functions/urlquery] ---- - -This template code: - -```go-html-template -{{ $u := urlquery "https://" "example.com" | safeURL }} -Link -``` - -Is rendered to: - -```html -Link -``` - -{{% include "/_common/functions/go-template/text-template.md" %}} diff --git a/docs/content/en/functions/go-template/with.md b/docs/content/en/functions/go-template/with.md deleted file mode 100644 index c25ce3fba..000000000 --- a/docs/content/en/functions/go-template/with.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: with -description: Binds context (the dot) to the expression and executes the block if expression is truthy. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: - signatures: [with EXPR] -aliases: [/functions/with] ---- - -{{% include "/_common/functions/truthy-falsy.md" %}} - -```go-html-template -{{ $var := "foo" }} -{{ with $var }} - {{ . }} → foo -{{ end }} -``` - -Use with the [`else`] statement: - -```go-html-template -{{ $var := "foo" }} -{{ with $var }} - {{ . }} → foo -{{ else }} - {{ print "var is falsy" }} -{{ end }} -``` - -Use `else with` to check multiple conditions: - -```go-html-template -{{ $v1 := 0 }} -{{ $v2 := 42 }} -{{ with $v1 }} - {{ . }} -{{ else with $v2 }} - {{ . }} → 42 -{{ else }} - {{ print "v1 and v2 are falsy" }} -{{ end }} -``` - -Initialize a variable, scoped to the current block: - -```go-html-template -{{ with $var := 42 }} - {{ . }} → 42 - {{ $var }} → 42 -{{ end }} -{{ $var }} → undefined -``` - -## Understanding context - -At the top of a page template, the [context](g) (the dot) is a `Page` object. Inside of the `with` block, the context is bound to the value passed to the `with` statement. - -With this contrived example: - -```go-html-template -{{ with 42 }} - {{ .Title }} -{{ end }} -``` - -Hugo will throw an error: - - can't evaluate field Title in type int - -The error occurs because we are trying to use the `.Title` method on an integer instead of a `Page` object. Inside of the `with` block, if we want to render the page title, we need to get the context passed into the template. - -> [!note] -> Use the `$` to get the context passed into the template. - -This template will render the page title as desired: - -```go-html-template -{{ with 42 }} - {{ $.Title }} -{{ end }} -``` - -> [!note] -> Gaining a thorough understanding of context is critical for anyone writing template code. - -{{% include "/_common/functions/go-template/text-template.md" %}} - -[`else`]: /functions/go-template/else/ diff --git a/docs/content/en/functions/hash/FNV32a.md b/docs/content/en/functions/hash/FNV32a.md deleted file mode 100644 index b108acff8..000000000 --- a/docs/content/en/functions/hash/FNV32a.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: hash.FNV32a -description: Returns the 32-bit FNV (Fowler-Noll-Vo) non-cryptographic hash of the given string. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: int - signatures: [hash.FNV32a STRING] -aliases: [/functions/crypto.fnv32a] ---- - -```go-html-template -{{ hash.FNV32a "Hello world" }} → 1498229191 -``` diff --git a/docs/content/en/functions/hash/XxHash.md b/docs/content/en/functions/hash/XxHash.md deleted file mode 100644 index 6a92b2bdc..000000000 --- a/docs/content/en/functions/hash/XxHash.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: hash.XxHash -description: Returns the 64-bit xxHash non-cryptographic hash of the given string. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [xxhash] - returnType: string - signatures: [hash.XxHash STRING] ---- - -```go-html-template -{{ hash.XxHash "Hello world" }} → c500b0c912b376d8 -``` - -[xxHash](https://xxhash.com/) is a very fast non-cryptographic hash algorithm. Hugo uses [this Go implementation](https://github.com/cespare/xxhash). diff --git a/docs/content/en/functions/hash/_index.md b/docs/content/en/functions/hash/_index.md deleted file mode 100644 index 956f7fb8d..000000000 --- a/docs/content/en/functions/hash/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Hash functions -linkTitle: hash -description: Use these functions to create non-cryptographic hashes. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/functions/hugo/BuildDate.md b/docs/content/en/functions/hugo/BuildDate.md deleted file mode 100644 index a592283b9..000000000 --- a/docs/content/en/functions/hugo/BuildDate.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: hugo.BuildDate -description: Returns the compile date of the Hugo binary. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: string - signatures: [hugo.BuildDate] ---- - -The `hugo.BuildDate` function returns the compile date of the Hugo binary, formatted per [RFC 3339]. - -[RFC 3339]: https://datatracker.ietf.org/doc/html/rfc3339 - -```go-html-template -{{ hugo.BuildDate }} → 2023-11-01T17:57:00Z -``` diff --git a/docs/content/en/functions/hugo/CommitHash.md b/docs/content/en/functions/hugo/CommitHash.md deleted file mode 100644 index 324e985d1..000000000 --- a/docs/content/en/functions/hugo/CommitHash.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: hugo.CommitHash -description: Returns the Git commit hash of the Hugo binary. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: string - signatures: [hugo.CommitHash] ---- - -```go-html-template -{{ hugo.CommitHash }} → a4892a07b41b7b3f1f143140ee4ec0a9a5cf3970 -``` diff --git a/docs/content/en/functions/hugo/Deps.md b/docs/content/en/functions/hugo/Deps.md deleted file mode 100644 index 9d8667ee5..000000000 --- a/docs/content/en/functions/hugo/Deps.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: hugo.Deps -description: Returns a slice of project dependencies, either Hugo Modules or local theme components. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: '[]hugo.Dependency' - signatures: [hugo.Deps] ---- - -The `hugo.Deps` function returns a slice of project dependencies, either Hugo Modules or local theme components. Each dependency contains: - -Owner -: (`hugo.Dependency`) In the dependency tree, this is the first module that defines this module as a dependency (e.g., `github.com/gohugoio/hugo-mod-bootstrap-scss/v5`). - -Path -: (`string`) The module path or the path below your `themes` directory (e.g., `github.com/gohugoio/hugo-mod-jslibs-dist/popperjs/v2`). - -Replace -: (`hugo.Dependency`) Replaced by this dependency. - -Time -: (`time.Time`) The time that the version was created (e.g., `2022-02-13 15:11:28 +0000 UTC`). - -Vendor -: (`bool`) Reports whether the dependency is vendored. - -Version -: (`string`) The module version (e.g., `v2.21100.20000`). - -An example table listing the dependencies: - -```go-html-template -

    Dependencies

    - - - - - - - - - - - - - {{ range $index, $element := hugo.Deps }} - - - - - - - - - {{ end }} - -
    #OwnerPathVersionTimeVendor
    {{ add $index 1 }}{{ with $element.Owner }}{{ .Path }}{{ end }} - {{ $element.Path }} - {{ with $element.Replace }} - => {{ .Path }} - {{ end }} - {{ $element.Version }}{{ with $element.Time }}{{ . }}{{ end }}{{ $element.Vendor }}
    -``` diff --git a/docs/content/en/functions/hugo/Environment.md b/docs/content/en/functions/hugo/Environment.md deleted file mode 100644 index 551306255..000000000 --- a/docs/content/en/functions/hugo/Environment.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: hugo.Environment -description: Returns the current running environment. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: string - signatures: [hugo.Environment] ---- - -The `hugo.Environment` function returns the current running [environment](g) as defined through the `--environment` command line flag. - -```go-html-template -{{ hugo.Environment }} → production -``` - -Command line examples: - -Command|Environment -:--|:-- -`hugo`|`production` -`hugo --environment staging`|`staging` -`hugo server`|`development` -`hugo server --environment staging`|`staging` diff --git a/docs/content/en/functions/hugo/Generator.md b/docs/content/en/functions/hugo/Generator.md deleted file mode 100644 index dc72a7af2..000000000 --- a/docs/content/en/functions/hugo/Generator.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: hugo.Generator -description: Renders an HTML meta element identifying the software that generated the site. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: template.HTML - signatures: [hugo.Generator] ---- - -```go-html-template -{{ hugo.Generator }} → -``` diff --git a/docs/content/en/functions/hugo/GoVersion.md b/docs/content/en/functions/hugo/GoVersion.md deleted file mode 100644 index 94e310deb..000000000 --- a/docs/content/en/functions/hugo/GoVersion.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: hugo.GoVersion -description: Returns the Go version used to compile the Hugo binary -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: string - signatures: [hugo.GoVersion] ---- - -```go-html-template -{{ hugo.GoVersion }} → go1.21.1 -``` diff --git a/docs/content/en/functions/hugo/IsDevelopment.md b/docs/content/en/functions/hugo/IsDevelopment.md deleted file mode 100644 index cea923acd..000000000 --- a/docs/content/en/functions/hugo/IsDevelopment.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: hugo.IsDevelopment -description: Reports whether the current running environment is "development". -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: bool - signatures: [hugo.IsDevelopment] ---- - -{{< new-in 0.120.0 />}} - -```go-html-template -{{ hugo.IsDevelopment }} → true/false -``` diff --git a/docs/content/en/functions/hugo/IsExtended.md b/docs/content/en/functions/hugo/IsExtended.md deleted file mode 100644 index ab7e0f7b1..000000000 --- a/docs/content/en/functions/hugo/IsExtended.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: hugo.IsExtended -description: Reports whether the Hugo binary is either the extended or extended/deploy edition. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: bool - signatures: [hugo.IsExtended] ---- - -```go-html-template -{{ hugo.IsExtended }} → true/false -``` diff --git a/docs/content/en/functions/hugo/IsMultihost.md b/docs/content/en/functions/hugo/IsMultihost.md deleted file mode 100644 index 605afa79a..000000000 --- a/docs/content/en/functions/hugo/IsMultihost.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: hugo.IsMultihost -description: Reports whether each configured language has a unique base URL. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: bool - signatures: [hugo.IsMultihost] ---- - -{{< new-in 0.124.0 />}} - -Site configuration: - -{{< code-toggle file=hugo >}} -defaultContentLanguage = 'de' -defaultContentLanguageInSubdir = true -[languages] - [languages.de] - baseURL = 'https://de.example.org/' - languageCode = 'de-DE' - languageName = 'Deutsch' - title = 'Projekt Dokumentation' - weight = 1 - [languages.en] - baseURL = 'https://en.example.org/' - languageCode = 'en-US' - languageName = 'English' - title = 'Project Documentation' - weight = 2 -{{< /code-toggle >}} - -Template: - -```go-html-template -{{ hugo.IsMultihost }} → true -``` diff --git a/docs/content/en/functions/hugo/IsMultilingual.md b/docs/content/en/functions/hugo/IsMultilingual.md deleted file mode 100644 index 85fc6550f..000000000 --- a/docs/content/en/functions/hugo/IsMultilingual.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: hugo.IsMultilingual -description: Reports whether there are two or more configured languages. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: bool - signatures: [hugo.IsMultilingual] ---- - -{{< new-in 0.124.0 />}} - -Site configuration: - -{{< code-toggle file=hugo >}} -defaultContentLanguage = 'de' -defaultContentLanguageInSubdir = true -[languages] - [languages.de] - languageCode = 'de-DE' - languageName = 'Deutsch' - title = 'Projekt Dokumentation' - weight = 1 - [languages.en] - languageCode = 'en-US' - languageName = 'English' - title = 'Project Documentation' - weight = 2 -{{< /code-toggle >}} - -Template: - -```go-html-template -{{ hugo.IsMultilingual }} → true -``` diff --git a/docs/content/en/functions/hugo/IsProduction.md b/docs/content/en/functions/hugo/IsProduction.md deleted file mode 100644 index e5433c239..000000000 --- a/docs/content/en/functions/hugo/IsProduction.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: hugo.IsProduction -description: Reports whether the current running environment is "production". -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: bool - signatures: [hugo.IsProduction] ---- - -```go-html-template -{{ hugo.IsProduction }} → true/false -``` diff --git a/docs/content/en/functions/hugo/IsServer.md b/docs/content/en/functions/hugo/IsServer.md deleted file mode 100644 index 840ff060d..000000000 --- a/docs/content/en/functions/hugo/IsServer.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: hugo.IsServer -description: Reports whether the built-in development server is running. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: bool - signatures: [hugo.IsServer] ---- - -{{< new-in 0.120.0 />}} - -```go-html-template -{{ hugo.IsServer }} → true/false -``` diff --git a/docs/content/en/functions/hugo/Store.md b/docs/content/en/functions/hugo/Store.md deleted file mode 100644 index 08c684146..000000000 --- a/docs/content/en/functions/hugo/Store.md +++ /dev/null @@ -1,117 +0,0 @@ ---- -title: hugo.Store -description: Returns a globally scoped "scratch pad" to store and manipulate data. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: maps.Scratch - signatures: [hugo.Store] ---- - -{{< new-in 0.139.0 />}} - -Use the `hugo.Store` function to create a globally scoped [scratch pad](g) to store and manipulate data. To create a scratch pad with a different [scope](g), refer to the [scope](#scope) section below. - -## Methods - -### Set - -Sets the value of the given key. - -```go-html-template -{{ hugo.Store.Set "greeting" "Hello" }} -``` - -### Get - -Gets the value of the given key. - -```go-html-template -{{ hugo.Store.Set "greeting" "Hello" }} -{{ hugo.Store.Get "greeting" }} → Hello -``` - -### Add - -Adds the given value to the existing value(s) of the given key. - -For single values, `Add` accepts values that support Go's `+` operator. If the first `Add` for a key is an array or slice, the following adds will be appended to that list. - -```go-html-template -{{ hugo.Store.Set "greeting" "Hello" }} -{{ hugo.Store.Add "greeting" "Welcome" }} -{{ hugo.Store.Get "greeting" }} → HelloWelcome -``` - -```go-html-template -{{ hugo.Store.Set "total" 3 }} -{{ hugo.Store.Add "total" 7 }} -{{ hugo.Store.Get "total" }} → 10 -``` - -```go-html-template -{{ hugo.Store.Set "greetings" (slice "Hello") }} -{{ hugo.Store.Add "greetings" (slice "Welcome" "Cheers") }} -{{ hugo.Store.Get "greetings" }} → [Hello Welcome Cheers] -``` - -### SetInMap - -Takes a `key`, `mapKey` and `value` and adds a map of `mapKey` and `value` to the given `key`. - -```go-html-template -{{ hugo.Store.SetInMap "greetings" "english" "Hello" }} -{{ hugo.Store.SetInMap "greetings" "french" "Bonjour" }} -{{ hugo.Store.Get "greetings" }} → map[english:Hello french:Bonjour] -``` - -### DeleteInMap - -Takes a `key` and `mapKey` and removes the map of `mapKey` from the given `key`. - -```go-html-template -{{ hugo.Store.SetInMap "greetings" "english" "Hello" }} -{{ hugo.Store.SetInMap "greetings" "french" "Bonjour" }} -{{ hugo.Store.DeleteInMap "greetings" "english" }} -{{ hugo.Store.Get "greetings" }} → map[french:Bonjour] - ``` - -### GetSortedMapValues - -Returns an array of values from `key` sorted by `mapKey`. - -```go-html-template -{{ hugo.Store.SetInMap "greetings" "english" "Hello" }} -{{ hugo.Store.SetInMap "greetings" "french" "Bonjour" }} -{{ hugo.Store.GetSortedMapValues "greetings" }} → [Hello Bonjour] -``` - -### Delete - -Removes the given key. - -```go-html-template -{{ hugo.Store.Set "greeting" "Hello" }} -{{ hugo.Store.Delete "greeting" }} -``` - -{{% include "_common/scratch-pad-scope.md" %}} - -## Determinate values - -The `Store` method is often used to set scratch pad values within a shortcode, a partial template called by a shortcode, or by a Markdown render hook. In all three cases, the scratch pad values are indeterminate until Hugo renders the page content. - -If you need to access a scratch pad value from a parent template, and the parent template has not yet rendered the page content, you can trigger content rendering by assigning the returned value to a [noop](g) variable: - -```go-html-template -{{ $noop := .Content }} -{{ hugo.Store.Get "mykey" }} -``` - -You can also trigger content rendering with the `ContentWithoutSummary`, `FuzzyWordCount`, `Len`, `Plain`, `PlainWords`, `ReadingTime`, `Summary`, `Truncated`, and `WordCount` methods. For example: - -```go-html-template -{{ $noop := .WordCount }} -{{ hugo.Store.Get "mykey" }} -``` diff --git a/docs/content/en/functions/hugo/Version.md b/docs/content/en/functions/hugo/Version.md deleted file mode 100644 index 7925af981..000000000 --- a/docs/content/en/functions/hugo/Version.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: hugo.Version -description: Returns the current version of the Hugo binary. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: hugo.VersionString - signatures: [hugo.Version] ---- - -```go-html-template -{{ hugo.Version }} → 0.144.2 -``` diff --git a/docs/content/en/functions/hugo/WorkingDir.md b/docs/content/en/functions/hugo/WorkingDir.md deleted file mode 100644 index e50102558..000000000 --- a/docs/content/en/functions/hugo/WorkingDir.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: hugo.WorkingDir -description: Returns the project working directory. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: string - signatures: [hugo.WorkingDir] ---- - -```go-html-template -{{ hugo.WorkingDir }} → /home/user/projects/my-hugo-site -``` diff --git a/docs/content/en/functions/hugo/_index.md b/docs/content/en/functions/hugo/_index.md deleted file mode 100644 index b1c9216c4..000000000 --- a/docs/content/en/functions/hugo/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Hugo functions -linkTitle: hugo -description: Use these functions to access information about the Hugo application and the current environment. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/functions/images/AutoOrient.md b/docs/content/en/functions/images/AutoOrient.md deleted file mode 100644 index fd8d2ed14..000000000 --- a/docs/content/en/functions/images/AutoOrient.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: images.AutoOrient -description: Returns an image filter that rotates and flips an image as needed per its EXIF orientation tag. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: images.filter - signatures: [images.AutoOrient] ---- - -{{< new-in 0.121.2 />}} - -## Usage - -Create the filter: - -```go-html-template -{{ $filter := images.AutoOrient }} -``` - -{{% include "/_common/functions/images/apply-image-filter.md" %}} - -> [!note] -> When using with other filters, specify `images.AutoOrient` first. - -```go-html-template -{{ $filters := slice - images.AutoOrient - (images.Process "resize 200x") -}} -{{ with resources.Get "images/original.jpg" }} - {{ with images.Filter $filters . }} - - {{ end }} -{{ end }} -``` - -## Example - -{{< img - src="images/examples/landscape-exif-orientation-5.jpg" - alt="Zion National Park" - filter="AutoOrient" - filterArgs="" - example=true ->}} diff --git a/docs/content/en/functions/images/Brightness.md b/docs/content/en/functions/images/Brightness.md deleted file mode 100644 index 0ddfcba55..000000000 --- a/docs/content/en/functions/images/Brightness.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: images.Brightness -description: Returns an image filter that changes the brightness of an image. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: images.filter - signatures: [images.Brightness PERCENTAGE] ---- - -The percentage must be in the range [-100, 100] where 0 has no effect. A value of `-100` produces a solid black image, and a value of `100` produces a solid white image. - -## Usage - -Create the image filter: - -```go-html-template -{{ $filter := images.Brightness 12 }} -``` - -{{% include "/_common/functions/images/apply-image-filter.md" %}} - -## Example - -{{< img - src="images/examples/zion-national-park.jpg" - alt="Zion National Park" - filter="Brightness" - filterArgs="12" - example=true ->}} diff --git a/docs/content/en/functions/images/ColorBalance.md b/docs/content/en/functions/images/ColorBalance.md deleted file mode 100644 index be4a2bce8..000000000 --- a/docs/content/en/functions/images/ColorBalance.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: images.ColorBalance -description: Returns an image filter that changes the color balance of an image. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: images.filter - signatures: [images.ColorBalance PCTRED PCTGREEN PCTBLUE] ---- - -The percentage for each channel (red, green, blue) must be in the range [-100, 500]. - -## Usage - -Create the filter: - -```go-html-template -{{ $filter := images.ColorBalance -10 10 50 }} -``` - -{{% include "/_common/functions/images/apply-image-filter.md" %}} - -## Example - -{{< img - src="images/examples/zion-national-park.jpg" - alt="Zion National Park" - filter="ColorBalance" - filterArgs="-10,10,50" - example=true ->}} diff --git a/docs/content/en/functions/images/Colorize.md b/docs/content/en/functions/images/Colorize.md deleted file mode 100644 index 6b8cd5966..000000000 --- a/docs/content/en/functions/images/Colorize.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: images.Colorize -description: Returns an image filter that produces a colorized version of an image. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: images.filter - signatures: [images.Colorize HUE SATURATION PERCENTAGE] ---- - -The hue is the angle on the color wheel, typically in the range [0, 360]. - -The saturation must be in the range [0, 100]. - -The percentage specifies the strength of the effect, and must be in the range [0, 100]. - -## Usage - -Create the filter: - -```go-html-template -{{ $filter := images.Colorize 180 50 20 }} -``` - -{{% include "/_common/functions/images/apply-image-filter.md" %}} - -## Example - -{{< img - src="images/examples/zion-national-park.jpg" - alt="Zion National Park" - filter="Colorize" - filterArgs="180,50,20" - example=true ->}} diff --git a/docs/content/en/functions/images/Config.md b/docs/content/en/functions/images/Config.md deleted file mode 100644 index 59242fb95..000000000 --- a/docs/content/en/functions/images/Config.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: images.Config -description: Returns an image.Config structure from the image at the specified path, relative to the working directory. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: image.Config - signatures: [images.Config PATH] -aliases: [/functions/imageconfig] ---- - -See [image processing] for an overview of Hugo's image pipeline. - -```go-html-template -{{ $ic := images.Config "/static/images/a.jpg" }} - -{{ $ic.Width }} → 600 (int) -{{ $ic.Height }} → 400 (int) -``` - -Supported image formats include GIF, JPEG, PNG, TIFF, and WebP. - -> [!note] -> This is a legacy function, superseded by the [`Width`] and [`Height`] methods for [global resources](g), [page resources](g), and [remote resources](g). See the [image processing] section for details. - -[`Height`]: /methods/resource/height/ -[`Width`]: /methods/resource/width/ -[image processing]: /content-management/image-processing/ diff --git a/docs/content/en/functions/images/Contrast.md b/docs/content/en/functions/images/Contrast.md deleted file mode 100644 index f5d607440..000000000 --- a/docs/content/en/functions/images/Contrast.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: images.Contrast -description: Returns an image filter that changes the contrast of an image. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: images.filter - signatures: [images.Contrast PERCENTAGE] ---- - -The percentage must be in the range [-100, 100] where 0 has no effect. A value of `-100` produces a solid grey image, and a value of `100` produces an over-contrasted image. - -## Usage - -Create the filter: - -```go-html-template -{{ $filter := images.Contrast -20 }} -``` - -{{% include "/_common/functions/images/apply-image-filter.md" %}} - -## Example - -{{< img - src="images/examples/zion-national-park.jpg" - alt="Zion National Park" - filter="Contrast" - filterArgs="-20" - example=true ->}} diff --git a/docs/content/en/functions/images/Dither.md b/docs/content/en/functions/images/Dither.md deleted file mode 100644 index eab7743f7..000000000 --- a/docs/content/en/functions/images/Dither.md +++ /dev/null @@ -1,156 +0,0 @@ ---- -title: images.Dither -description: Returns an image filter that dithers an image. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: images.filter - signatures: ['images.Dither [OPTIONS]'] ---- - -{{< new-in 0.123.0 />}} - -## Options - -colors -: (`string array`) A slice of two or more colors that make up the dithering palette, each expressed as an RGB or RGBA [hexadecimal] value, with or without a leading hash mark. The default values are opaque black (`000000ff`) and opaque white (`ffffffff`). - -[hexadecimal]: https://developer.mozilla.org/en-US/docs/Web/CSS/hex-color - -method -: (`string`) The dithering method. See the [dithering methods](#dithering-methods) section below for a list of the available methods. Default is `FloydSteinberg`. - -serpentine -: (`bool`) Applicable to error diffusion dithering methods, whether to apply the error diffusion matrix in a serpentine manner, meaning that it goes right-to-left every other line. This greatly reduces line-type artifacts. Default is `true`. - -strength -: (`float`) The strength at which to apply the dithering matrix, typically a value in the range [0, 1]. A value of `1.0` applies the dithering matrix at 100% strength (no modification of the dither matrix). The `strength` is inversely proportional to contrast; reducing the strength increases the contrast. Setting `strength` to a value such as `0.8` can be useful to reduce noise in the dithered image. Default is `1.0`. - -## Usage - -Create the options map: - -```go-html-template -{{ $opts := dict - "colors" (slice "222222" "808080" "dddddd") - "method" "ClusteredDot4x4" - "strength" 0.85 -}} -``` - -Create the filter: - -```go-html-template -{{ $filter := images.Dither $opts }} -``` - -Or create the filter using the default settings: - -```go-html-template -{{ $filter := images.Dither }} -``` - -{{% include "/_common/functions/images/apply-image-filter.md" %}} - -## Dithering methods - -See the [Go documentation] for descriptions of each of the dithering methods below. - -[Go documentation]: https://pkg.go.dev/github.com/makeworld-the-better-one/dither/v2#pkg-variables - -Error diffusion dithering methods: - -- Atkinson -- Burkes -- FalseFloydSteinberg -- FloydSteinberg -- JarvisJudiceNinke -- Sierra -- Sierra2 -- Sierra2_4A -- Sierra3 -- SierraLite -- Simple2D -- StevenPigeon -- Stucki -- TwoRowSierra - -Ordered dithering methods: - -- ClusteredDot4x4 -- ClusteredDot6x6 -- ClusteredDot6x6_2 -- ClusteredDot6x6_3 -- ClusteredDot8x8 -- ClusteredDotDiagonal16x16 -- ClusteredDotDiagonal6x6 -- ClusteredDotDiagonal8x8 -- ClusteredDotDiagonal8x8_2 -- ClusteredDotDiagonal8x8_3 -- ClusteredDotHorizontalLine -- ClusteredDotSpiral5x5 -- ClusteredDotVerticalLine -- Horizontal3x5 -- Vertical5x3 - -## Example - -This example uses the default dithering options. - -{{< img - src="images/examples/zion-national-park.jpg" - alt="Zion National Park" - filter="Dither" - filterArgs="" - example=true ->}} - -## Recommendations - -Regardless of dithering method, do both of the following to obtain the best results: - -1. Scale the image _before_ dithering -1. Output the image to a lossless format such as GIF or PNG - -The example below does both of these, and it sets the dithering palette to the three most dominant colors in the image. - -```go-html-template -{{ with resources.Get "original.jpg" }} - {{ $opts := dict - "method" "ClusteredDotSpiral5x5" - "colors" (first 3 .Colors) - }} - {{ $filters := slice - (images.Process "resize 800x") - (images.Dither $opts) - (images.Process "png") - }} - {{ with . | images.Filter $filters }} - - {{ end }} -{{ end }} -``` - -For best results, if the dithering palette is grayscale, convert the image to grayscale before dithering. - -```go-html-template -{{ $opts := dict "colors" (slice "222" "808080" "ddd") }} -{{ $filters := slice - (images.Process "resize 800x") - (images.Grayscale) - (images.Dither $opts) - (images.Process "png") -}} -{{ with images.Filter $filters . }} - -{{ end }} -``` - -The example above: - -1. Resizes the image to be 800 px wide -1. Converts the image to grayscale -1. Dithers the image using the default (`FloydSteinberg`) dithering method with a grayscale palette -1. Converts the image to the PNG format diff --git a/docs/content/en/functions/images/Filter.md b/docs/content/en/functions/images/Filter.md deleted file mode 100644 index 1f2c268be..000000000 --- a/docs/content/en/functions/images/Filter.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: images.Filter -description: Applies one or more image filters to the given image resource. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: images.ImageResource - signatures: [images.Filter FILTERS... IMAGE] ---- - -Apply one or more [image filters](#image-filters) to the given image. - -To apply a single filter: - -```go-html-template -{{ with resources.Get "images/original.jpg" }} - {{ with images.Filter images.Grayscale . }} - - {{ end }} -{{ end }} -``` - -To apply two or more filters, executing from left to right: - -```go-html-template -{{ $filters := slice - images.Grayscale - (images.GaussianBlur 8) -}} -{{ with resources.Get "images/original.jpg" }} - {{ with images.Filter $filters . }} - - {{ end }} -{{ end }} -``` - -You can also apply image filters using the [`Filter`] method on a `Resource` object. - -[`Filter`]: /methods/resource/filter/ - -## Example - -```go-html-template -{{ with resources.Get "images/original.jpg" }} - {{ with images.Filter images.Grayscale . }} - - {{ end }} -{{ end }} -``` - -{{< img - src="images/examples/zion-national-park.jpg" - alt="Zion National Park" - filter="Grayscale" - filterArgs="" - example=true ->}} - -## Image filters - -Use any of these filters with the `images.Filter` function, or with the `Filter` method on a `Resource` object. - -{{% list-pages-in-section path=/functions/images filter=functions_images_no_filters filterType=exclude %}} diff --git a/docs/content/en/functions/images/Gamma.md b/docs/content/en/functions/images/Gamma.md deleted file mode 100644 index d8cb076f1..000000000 --- a/docs/content/en/functions/images/Gamma.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: images.Gamma -description: Returns an image filter that performs gamma correction on an image. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: images.filter - signatures: [images.Gamma GAMMA] ---- - -The gamma value must be positive. A value greater than 1 lightens the image, while a value less than 1 darkens the image. The filter has no effect when the gamma value is 1. - -## Usage - -Create the filter: - -```go-html-template -{{ $filter := images.Gamma 1.667 }} -``` - -{{% include "/_common/functions/images/apply-image-filter.md" %}} - -## Example - -{{< img - src="images/examples/zion-national-park.jpg" - alt="Zion National Park" - filter="Gamma" - filterArgs="1.667" - example=true ->}} diff --git a/docs/content/en/functions/images/GaussianBlur.md b/docs/content/en/functions/images/GaussianBlur.md deleted file mode 100644 index c5eb136e2..000000000 --- a/docs/content/en/functions/images/GaussianBlur.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: images.GaussianBlur -description: Returns an image filter that applies a gaussian blur to an image. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: images.filter - signatures: [images.GaussianBlur SIGMA] ---- - -The sigma value must be positive, and indicates how much the image will be blurred. The blur-affected radius is approximately 3 times the sigma value. - -## Usage - -Create the filter: - -```go-html-template -{{ $filter := images.GaussianBlur 5 }} -``` - -{{% include "/_common/functions/images/apply-image-filter.md" %}} - -## Example - -{{< img - src="images/examples/zion-national-park.jpg" - alt="Zion National Park" - filter="GaussianBlur" - filterArgs="5" - example=true ->}} diff --git a/docs/content/en/functions/images/Grayscale.md b/docs/content/en/functions/images/Grayscale.md deleted file mode 100644 index d3651b8dc..000000000 --- a/docs/content/en/functions/images/Grayscale.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: images.Grayscale -description: Returns an image filter that produces a grayscale version of an image. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: images.filter - signatures: [images.Grayscale] ---- - -## Usage - -Create the filter: - -```go-html-template -{{ $filter := images.Grayscale }} -``` - -{{% include "/_common/functions/images/apply-image-filter.md" %}} - -## Example - -{{< img - src="images/examples/zion-national-park.jpg" - alt="Zion National Park" - filter="Grayscale" - filterArgs="" - example=true ->}} diff --git a/docs/content/en/functions/images/Hue.md b/docs/content/en/functions/images/Hue.md deleted file mode 100644 index f334eebd8..000000000 --- a/docs/content/en/functions/images/Hue.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: images.Hue -description: Returns an image filter that rotates the hue of an image. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: images.filter - signatures: [images.Hue SHIFT] ---- - -The hue angle shift is typically in the range [-180, 180] where 0 has no effect. - -## Usage - -Create the filter: - -```go-html-template -{{ $filter := images.Hue -15 }} -``` - -{{% include "/_common/functions/images/apply-image-filter.md" %}} - -## Example - -{{< img - src="images/examples/zion-national-park.jpg" - alt="Zion National Park" - filter="Hue" - filterArgs="-15" - example=true ->}} diff --git a/docs/content/en/functions/images/Invert.md b/docs/content/en/functions/images/Invert.md deleted file mode 100644 index 0f9f9a9d2..000000000 --- a/docs/content/en/functions/images/Invert.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: images.Invert -description: Returns an image filter that negates the colors of an image. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: images.filter - signatures: [images.Invert] ---- - -## Usage - -Create the filter: - -```go-html-template -{{ $filter := images.Invert }} -``` - -{{% include "/_common/functions/images/apply-image-filter.md" %}} - -## Example - -{{< img - src="images/examples/zion-national-park.jpg" - alt="Zion National Park" - filter="Invert" - filterArgs="" - example=true ->}} diff --git a/docs/content/en/functions/images/Mask.md b/docs/content/en/functions/images/Mask.md deleted file mode 100644 index 4f3b4aa3f..000000000 --- a/docs/content/en/functions/images/Mask.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -title: images.Mask -description: Returns an image filter that applies a mask to the source image. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: images.filter - signatures: [images.Mask RESOURCE] ---- - -{{< new-in 0.141.0 />}} - -The `images.Mask` filter applies a mask to an image. Black pixels in the mask make the corresponding areas of the base image transparent, while white pixels keep them opaque. Color images are converted to grayscale for masking purposes. The mask is automatically resized to match the dimensions of the base image. - -> [!note] -> Of the formats supported by Hugo's imaging pipeline, only PNG and WebP have an alpha channel to support transparency. If your source image has a different format and you require transparent masked areas, convert it to either PNG or WebP as shown in the example below. - -When applying a mask to a non-transparent image format such as JPEG, the masked areas will be filled with the color specified by the `bgColor` parameter in your [site configuration]. You can override that color with a `Process` image filter: - -```go-html-template -{{ $filter := images.Process "#00ff00" }} -``` - -## Usage - -Create a slice of filters, one for WebP conversion and the other for mask application: - -```go-html-template -{{ $filter1 := images.Process "webp" }} -{{ $filter2 := images.Mask (resources.Get "images/mask.png") }} -{{ $filters := slice $filter1 $filter2 }} -``` - -Apply the filters using the [`images.Filter`] function: - -```go-html-template -{{ with resources.Get "images/original.jpg" }} - {{ with . | images.Filter $filters }} - - {{ end }} -{{ end }} -``` - -You can also apply the filter using the [`Filter`] method on a 'Resource' object: - -```go-html-template -{{ with resources.Get "images/original.jpg" }} - {{ with .Filter $filters }} - - {{ end }} -{{ end }} -``` - -## Example - -Mask - -{{< img - src="images/examples/mask.png" - example=false ->}} - -{{< img - src="images/examples/zion-national-park.jpg" - alt="Zion National Park" - filter="mask" - filterArgs="images/examples/mask.png" - example=true ->}} - -[`Filter`]: /methods/resource/filter/ -[`images.Filter`]: /functions/images/filter/ -[site configuration]: /configuration/imaging/ diff --git a/docs/content/en/functions/images/Opacity.md b/docs/content/en/functions/images/Opacity.md deleted file mode 100644 index b9dcf3fd2..000000000 --- a/docs/content/en/functions/images/Opacity.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: images.Opacity -description: Returns an image filter that changes the opacity of an image. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: images.filter - signatures: [images.Opacity OPACITY] ---- - -{{< new-in 0.119.0 />}} - -The opacity value must be in the range [0, 1]. A value of `0` produces a transparent image, and a value of `1` produces an opaque image (no transparency). - -## Usage - -Create the filter: - -```go-html-template -{{ $filter := images.Opacity 0.65 }} -``` - -{{% include "/_common/functions/images/apply-image-filter.md" %}} - -The `images.Opacity` filter is most useful for target formats such as PNG and WebP that support transparency. If the source image does not support transparency, combine this filter with the `images.Process` filter: - -```go-html-template -{{ with resources.Get "images/original.jpg" }} - {{ $filters := slice - (images.Opacity 0.65) - (images.Process "png") - }} - {{ with . | images.Filter $filters }} - - {{ end }} -{{ end }} -``` - -## Example - -{{< img - src="images/examples/zion-national-park.jpg" - alt="Zion National Park" - filter="Opacity" - filterArgs="0.65" - example=true ->}} diff --git a/docs/content/en/functions/images/Overlay.md b/docs/content/en/functions/images/Overlay.md deleted file mode 100644 index 8e5eec3d1..000000000 --- a/docs/content/en/functions/images/Overlay.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: images.Overlay -description: Returns an image filter that overlays the source image at the given coordinates, relative to the upper left corner. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: images.filter - signatures: [images.Overlay RESOURCE X Y] ---- - -## Usage - -Capture the overlay image as a resource: - -```go-html-template -{{ $overlay := "" }} -{{ $path := "images/logo.png" }} -{{ with resources.Get $path }} - {{ $overlay = . }} -{{ else }} - {{ errorf "Unable to get resource %q" $path }} -{{ end }} -``` - -The overlay image can be a [global resource](g), a [page resource](g), or a [remote resource](g). - -Create the filter: - -```go-html-template -{{ $filter := images.Overlay $overlay 20 20 }} -``` - -{{% include "/_common/functions/images/apply-image-filter.md" %}} - -## Example - -{{< img - src="images/examples/zion-national-park.jpg" - alt="Zion National Park" - filter="Overlay" - filterArgs="images/logos/logo-64x64.png,20,20" - example=true ->}} diff --git a/docs/content/en/functions/images/Padding.md b/docs/content/en/functions/images/Padding.md deleted file mode 100644 index da15a44ca..000000000 --- a/docs/content/en/functions/images/Padding.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: images.Padding -description: Returns an image filter that resizes the image canvas without resizing the image. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: images.filter - signatures: ['images.Padding V1 [V2] [V3] [V4] [COLOR]'] ---- - -{{< new-in 0.120.0 />}} - -The last argument is the canvas color, expressed as an RGB or RGBA [hexadecimal color]. The default value is `ffffffff` (opaque white). The preceding arguments are the padding values, in pixels, using the CSS [shorthand property] syntax. Negative padding values will crop the image. - -[hexadecimal color]: https://developer.mozilla.org/en-US/docs/Web/CSS/hex-color -[shorthand property]: https://developer.mozilla.org/en-US/docs/Web/CSS/Shorthand_properties#edges_of_a_box - -## Usage - -Create the filter: - -```go-html-template -{{ $filter := images.Padding 20 40 "#976941" }} -``` - -{{% include "/_common/functions/images/apply-image-filter.md" %}} - -Combine with the [`Colors`] method to create a border with one of the image's most dominant colors: - -[`Colors`]: /methods/resource/colors/ - -```go-html-template -{{ with resources.Get "images/original.jpg" }} - {{ $filter := images.Padding 20 40 (index .Colors 2) }} - {{ with . | images.Filter $filter }} - - {{ end }} -{{ end }} -``` - -## Example - -{{< img - src="images/examples/zion-national-park.jpg" - alt="Zion National Park" - filter="Padding" - filterArgs="20,40,20,40,#976941" - example=true ->}} - -## Other recipes - -This example resizes an image to 300px wide, converts it to the WebP format, adds 20px vertical padding and 50px horizontal padding, then sets the canvas color to dark green with 33% opacity. - -Conversion to WebP is required to support transparency. PNG and WebP images have an alpha channel; JPEG and GIF do not. - -```go-html-template -{{ $img := resources.Get "images/a.jpg" }} -{{ $filters := slice - (images.Process "resize 300x webp") - (images.Padding 20 50 "#0705") -}} -{{ $img = $img.Filter $filters }} -``` - -To add a 2px gray border to an image: - -```go-html-template -{{ $img = $img.Filter (images.Padding 2 "#777") }} -``` diff --git a/docs/content/en/functions/images/Pixelate.md b/docs/content/en/functions/images/Pixelate.md deleted file mode 100644 index 954950c8b..000000000 --- a/docs/content/en/functions/images/Pixelate.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: images.Pixelate -description: Returns an image filter that applies a pixelation effect to an image. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: images.filter - signatures: [images.Pixelate SIZE] ---- - -## Usage - -Create the filter: - -```go-html-template -{{ $filter := images.Pixelate 4 }} -``` - -{{% include "/_common/functions/images/apply-image-filter.md" %}} - -## Example - -{{< img - src="images/examples/zion-national-park.jpg" - alt="Zion National Park" - filter="Pixelate" - filterArgs="4" - example=true ->}} diff --git a/docs/content/en/functions/images/Process.md b/docs/content/en/functions/images/Process.md deleted file mode 100644 index 134c40c5a..000000000 --- a/docs/content/en/functions/images/Process.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -title: images.Process -description: Returns an image filter that processes the given image using the given specification. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: images.filter - signatures: [images.Process SPEC] ---- - -{{< new-in 0.119.0 />}} - -This filter has the same options as the [`Process`] method on a `Resource` object, but using it as a filter may be more effective if you need to apply multiple filters to an image. - -[`Process`]: /methods/resource/process/ - -The process specification is a space-delimited, case-insensitive list of one or more of the following in any sequence: - -action -: Specify zero or one of `crop`, `fill`, `fit`, or `resize`. If you specify an action you must also provide dimensions. See [details](content-management/image-processing/#image-processing-methods). - -```go-html-template -{{ $filter := images.Process "resize 300x" }} -``` - -dimensions -: Required if you specify an action. Provide width _or_ height when using `resize`, else provide both width _and_ height. See [details](/content-management/image-processing/#dimensions). - -```go-html-template -{{ $filter := images.Process "crop 200x200" }} -``` - -anchor -: Use with the `crop` or `fill` action. Specify zero or one of `TopLeft`, `Top`, `TopRight`, `Left`, `Center`, `Right`, `BottomLeft`, `Bottom`, `BottomRight`, or `Smart`. Default is `Smart`. See [details](/content-management/image-processing/#anchor). - -```go-html-template -{{ $filter := images.Process "crop 200x200 center" }} -``` - -rotation -: Typically specify zero or one of `r90`, `r180`, or `r270`. Also supports arbitrary rotation angles. See [details](/content-management/image-processing/#rotation). - -```go-html-template -{{ $filter := images.Process "r90" }} -{{ $filter := images.Process "crop 200x200 center r90" }} -``` - -target format -: Specify zero or one of `gif`, `jpeg`, `png`, `tiff`, or `webp`. See [details](/content-management/image-processing/#target-format). - -```go-html-template -{{ $filter := images.Process "webp" }} -{{ $filter := images.Process "crop 200x200 center r90 webp" }} -``` - -quality -: Applicable to JPEG and WebP images. Optionally specify `qN` where `N` is an integer in the range [0, 100]. Default is `75`. See [details](/content-management/image-processing/#quality). - -```go-html-template -{{ $filter := images.Process "q50" }} -{{ $filter := images.Process "crop 200x200 center r90 webp q50" }} -``` - -hint -: Applicable to WebP images and equivalent to the `-preset` flag for the [`cwebp`] encoder. Specify zero or one of `drawing`, `icon`, `photo`, `picture`, or `text`. Default is `photo`. See [details](/content-management/image-processing/#hint). - -[`cwebp`]: https://developers.google.com/speed/webp/docs/cwebp - -```go-html-template -{{ $filter := images.Process "webp" "icon" }} -{{ $filter := images.Process "crop 200x200 center r90 webp q50 icon" }} -``` - -background color -: When converting a PNG or WebP with transparency to a format that does not support transparency, optionally specify a background color using a 3-digit or a 6-digit hexadecimal color code. Default is `#ffffff` (white). See [details](/content-management/image-processing/#background-color). - -```go-html-template -{{ $filter := images.Process "jpeg #000" }} -{{ $filter := images.Process "crop 200x200 center r90 q50 jpeg #000" }} -``` - -resampling filter -: Typically specify zero or one of `Box`, `Lanczos`, `CatmullRom`, `MitchellNetravali`, `Linear`, or `NearestNeighbor`. Other resampling filters are available. See [details](/content-management/image-processing/#resampling-filter). - -```go-html-template -{{ $filter := images.Process "resize 300x lanczos" }} -{{ $filter := images.Process "resize 300x r90 q50 jpeg #000 lanczos" }} -``` - -## Usage - -Create a filter: - -```go-html-template -{{ $filter := images.Process "resize 256x q40 webp" }} -``` - -{{% include "/_common/functions/images/apply-image-filter.md" %}} - -## Example - -{{< img - src="images/examples/zion-national-park.jpg" - alt="Zion National Park" - filter="Process" - filterArgs="resize 256x q40 webp" - example=true ->}} diff --git a/docs/content/en/functions/images/QR.md b/docs/content/en/functions/images/QR.md deleted file mode 100644 index eee2dff14..000000000 --- a/docs/content/en/functions/images/QR.md +++ /dev/null @@ -1,138 +0,0 @@ ---- -title: images.QR -description: Encodes the given text into a QR code using the specified options, returning an image resource. -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: images.ImageResource - signatures: ['images.QR TEXT [OPTIONS]'] ---- - -{{< new-in 0.141.0 />}} - -The `images.QR` function encodes the given text into a [QR code] using the specified options, returning an image resource. The size of the generated image depends on three factors: - -- Data length: Longer text necessitates a larger image to accommodate the increased information density. -- Error correction level: Higher error correction levels enhance the QR code's resistance to damage, but this typically results in a slightly larger image size to maintain readability. -- Pixels per module: The number of image pixels assigned to each individual module (the smallest unit of the QR code) directly impacts the overall image size. A higher pixel count per module leads to a larger, higher-resolution image. - -Although the default option values are sufficient for most applications, you should test the rendered QR code both on-screen and in print. - -## Options - -level -: (`string`) The error correction level to use when encoding the text, one of `low`, `medium`, `quartile`, or `high`. Default is `medium`. - - Error correction level|Redundancy - :--|:--|:-- - low|20% - medium|38% - quartile|55% - high|65% - -scale -: (`int`) The number of image pixels per QR code module. Must be greater than or equal to `2`. Default is `4`. - -targetDir -: (`string`) The subdirectory within the [`publishDir`] where Hugo will place the generated image. Use Unix-style slashes (`/`) to separarate path segments. If empty or not provided, the image is placed directly in the `publishDir` root. Hugo automatically creates the necessary subdirectories if they don't exist. - -## Examples - -To create a QR code using the default values for `level` and `scale`: - -```go-html-template -{{ $text := "https://gohugo.io" }} -{{ with images.QR $text }} - -{{ end }} -``` - -{{< qr text="https://gohugo.io" class="qrcode" targetDir="images/qr" />}} - -Specify `level`, `scale`, and `targetDir` as needed to achieve the desired result: - -```go-html-template -{{ $text := "https://gohugo.io" }} -{{ $opts := dict - "level" "high" - "scale" 3 - "targetDir" "images/qr" -}} -{{ with images.QR $text $opts }} - -{{ end }} -``` - -{{< qr text="https://gohugo.io" level="high" scale=3 targetDir="codes" class="qrcode" targetDir="images/qr" />}} - -To include a QR code that points to the `Permalink` of the current page: - -```go-html-template {file="layouts/_default/single.html"} -{{ with images.QR .Permalink }} - QR code linking to {{ $.Permalink }} -{{ end }} -``` - -Then hide the QR code with CSS unless printing the page: - -```css -/* Hide QR code by default */ -.qr-code { - display: none; -} - -/* Show QR code when printing */ -@media print { - .qr-code { - display: block; - } -} -``` - -## Scale - -As you decrease the size of a QR code, the maximum distance at which it can be reliably scanned by a device also decreases. - -In the example above, we set the `scale` to `2`, resulting in a QR code where each module consists of 2x2 pixels. While this might be sufficient for on-screen display, it's likely to be problematic when printed at 600 dpi. - -\[ \frac{2\:px}{module} \times \frac{1\:inch}{600\:px} \times \frac{25.4\:mm}{1\:inch} = \frac{0.085\:mm}{module} \] - -This module size is half of the commonly recommended minimum of 0.170 mm.\ -If the QR code will be printed, use the default `scale` value of `4` pixels per module. - -Avoid using Hugo's image processing methods to resize QR codes. Resizing can introduce blurring due to anti-aliasing when a QR code module occupies a fractional number of pixels. - -> [!note] -> Always test the rendered QR code both on-screen and in print. - -## Shortcode - -Call the `qr` shortcode to insert a QR code into your content. - -Use the self-closing syntax to pass the text as an argument: - -```text -{{}} -``` - -Or insert the text between the opening and closing tags: - -```text -{{}} -https://gohugo.io -{{}} -``` - -The `qr` shortcode accepts several arguments including `level` and `scale`. See the [related documentation] for details. - -[`publishDir`]: /configuration/all/#publishdir -[QR code]: https://en.wikipedia.org/wiki/QR_code -[related documentation]: /shortcodes/qr/ diff --git a/docs/content/en/functions/images/Saturation.md b/docs/content/en/functions/images/Saturation.md deleted file mode 100644 index d1dd48b24..000000000 --- a/docs/content/en/functions/images/Saturation.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: images.Saturation -description: Returns an image filter that changes the saturation of an image. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: images.filter - signatures: [images.Saturation PERCENTAGE] ---- - -The percentage must be in the range [-100, 500] where 0 has no effect. - -## Usage - -Create the filter: - -```go-html-template -{{ $filter := images.Saturation 65 }} -``` - -{{% include "/_common/functions/images/apply-image-filter.md" %}} - -## Example - -{{< img - src="images/examples/zion-national-park.jpg" - alt="Zion National Park" - filter="Saturation" - filterArgs="65" - example=true ->}} diff --git a/docs/content/en/functions/images/Sepia.md b/docs/content/en/functions/images/Sepia.md deleted file mode 100644 index ae43045db..000000000 --- a/docs/content/en/functions/images/Sepia.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: images.Sepia -description: Returns an image filter that produces a sepia-toned version of an image. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: images.filter - signatures: [images.Sepia PERCENTAGE] ---- - -The percentage must be in the range [0, 100] where 0 has no effect. - -## Usage - -Create the filter: - -```go-html-template -{{ $filter := images.Sepia 75 }} -``` - -{{% include "/_common/functions/images/apply-image-filter.md" %}} - -## Example - -{{< img - src="images/examples/zion-national-park.jpg" - alt="Zion National Park" - filter="Sepia" - filterArgs="75" - example=true ->}} diff --git a/docs/content/en/functions/images/Sigmoid.md b/docs/content/en/functions/images/Sigmoid.md deleted file mode 100644 index 9bfcaf91b..000000000 --- a/docs/content/en/functions/images/Sigmoid.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: images.Sigmoid -description: Returns an image filter that changes the contrast of an image using a sigmoidal function. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: images.filter - signatures: [images.Sigmoid MIDPOINT FACTOR] ---- - -This is a non-linear contrast change useful for photo adjustments; it preserves highlight and shadow detail. - -The midpoint is the midpoint of contrast. It must be in the range [0, 1], typically 0.5. - -The factor indicates how much to increase or decrease the contrast, typically in the range [-10, 10] where 0 has no effect. A positive value increases contrast, while a negative value decrease contrast. - -## Usage - -Create the filter: - -```go-html-template -{{ $filter := images.Sigmoid 0.6 -4 }} -``` - -{{% include "/_common/functions/images/apply-image-filter.md" %}} - -## Example - -{{< img - src="images/examples/zion-national-park.jpg" - alt="Zion National Park" - filter="Sigmoid" - filterArgs="0.6,-4" - example=true ->}} diff --git a/docs/content/en/functions/images/Text.md b/docs/content/en/functions/images/Text.md deleted file mode 100644 index 8f7e730ba..000000000 --- a/docs/content/en/functions/images/Text.md +++ /dev/null @@ -1,121 +0,0 @@ ---- -title: images.Text -description: Returns an image filter that adds text to an image. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: images.filter - signatures: ['images.Text TEXT [OPTIONS]'] ---- - -## Options - -Although none of the options are required, at a minimum you will want to set the `size` to be some reasonable percentage of the image height. - -alignx -: {{< new-in 0.141.0 />}} -: (`string`) The horizontal alignment of the text relative to the horizontal offset, one of `left`, `center`, or `right`. Default is `left`. - -aligny -: (`string`) The vertical alignment of the text relative to the vertical offset, one of `top`, `center`, or `bottom`. Default is `top`. - -color -: (`string`) The font color, either a 3-digit or 6-digit hexadecimal color code. Default is `#ffffff` (white). - -font -: (`resource.Resource`) The font can be a [global resource](g), a [page resource](g), or a [remote resource](g). Default is [Go Regular], a proportional sans-serif TrueType font. - -linespacing -: (`int`) The number of pixels between each line. For a line height of 1.4, set the `linespacing` to 0.4 multiplied by the `size`. Default is `2`. - -size -: (`int`) The font size in pixels. Default is `20`. - -x -: (`int`) The horizontal offset, in pixels, relative to the left of the image. Default is `10`. - -y -: (`int`) The vertical offset, in pixels, relative to the top of the image. Default is `10`. - -[Go Regular]: https://go.dev/blog/go-fonts#sans-serif - -## Usage - -Set the text and paths: - -```go-html-template -{{ $text := "Zion National Park" }} -{{ $fontPath := "https://github.com/google/fonts/raw/main/ofl/lato/Lato-Regular.ttf" }} -{{ $imagePath := "images/original.jpg" }} -``` - -Capture the font as a resource: - -```go-html-template -{{ $font := "" }} -{{ with try (resources.GetRemote $fontPath) }} - {{ with .Err }} - {{ errorf "%s" . }} - {{ else with .Value }} - {{ $font = . }} - {{ else }} - {{ errorf "Unable to get resource %s" $fontPath }} - {{ end }} -{{ end }} -``` - -Create the filter, centering the text horizontally and vertically: - -```go-html-template -{{ $r := "" }} -{{ $filter := "" }} -{{ with $r = resources.Get $imagePath }} - {{ $opts := dict - "alignx" "center" - "color" "#fbfaf5" - "font" $font - "linespacing" 8 - "size" 60 - "x" (mul .Width 0.5 | int) - "y" (mul .Height 0.5 | int) - }} - {{ $filter = images.Text $text $opts }} -{{ else }} - {{ errorf "Unable to get resource %s" $imagePath }} -{{ end }} -``` - -Apply the filter using the [`images.Filter`] function: - -```go-html-template -{{ with $r }} - {{ with . | images.Filter $filter }} - - {{ end }} -{{ end }} -``` - -You can also apply the filter using the [`Filter`] method on a `Resource` object: - -```go-html-template -{{ with $r }} - {{ with .Filter $filter }} - - {{ end }} -{{ end }} -``` - -[`images.Filter`]: /functions/images/filter/ -[`Filter`]: /methods/resource/filter/ - -## Example - -{{< img - src="images/examples/zion-national-park.jpg" - alt="Zion National Park" - filter="Text" - filterArgs="Zion National Park,25,190,40,1.2,#fbfaf5" - example=true ->}} diff --git a/docs/content/en/functions/images/UnsharpMask.md b/docs/content/en/functions/images/UnsharpMask.md deleted file mode 100644 index 9c06eb5e1..000000000 --- a/docs/content/en/functions/images/UnsharpMask.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: images.UnsharpMask -description: Returns an image filter that sharpens an image. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: images.filter - signatures: [images.UnsharpMask SIGMA AMOUNT THRESHOLD] ---- - -The sigma argument is used in a gaussian function and affects the radius of effect. Sigma must be positive. The sharpen radius is approximately 3 times the sigma value. - -The amount argument controls how much darker and how much lighter the edge borders become. Typically between 0.5 and 1.5. - -The threshold argument controls the minimum brightness change that will be sharpened. Typically between 0 and 0.05. - -## Usage - -Create the filter: - -```go-html-template -{{ $filter := images.UnsharpMask 10 0.4 0.03 }} -``` - -{{% include "/_common/functions/images/apply-image-filter.md" %}} - -## Example - -{{< img - src="images/examples/zion-national-park.jpg" - alt="Zion National Park" - filter="UnsharpMask" - filterArgs="10,0.4,0.03" - example=true ->}} diff --git a/docs/content/en/functions/images/_index.md b/docs/content/en/functions/images/_index.md deleted file mode 100644 index f92e16e7a..000000000 --- a/docs/content/en/functions/images/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Image functions -linkTitle: images -description: Use these functions to create an image filter, apply an image filter to an image, and to retrieve image information. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/functions/inflect/Humanize.md b/docs/content/en/functions/inflect/Humanize.md deleted file mode 100644 index d3d785243..000000000 --- a/docs/content/en/functions/inflect/Humanize.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: inflect.Humanize -description: Returns the humanized version of the input with the first letter capitalized. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [humanize] - returnType: string - signatures: [inflect.Humanize INPUT] -aliases: [/functions/humanize] ---- - -```go-html-template -{{ humanize "my-first-post" }} → My first post -{{ humanize "myCamelPost" }} → My camel post -``` - -If the input is an integer or a string representation of an integer, humanize returns the number with the proper ordinal appended. - -```go-html-template -{{ humanize "52" }} → 52nd -{{ humanize 103 }} → 103rd -``` diff --git a/docs/content/en/functions/inflect/Pluralize.md b/docs/content/en/functions/inflect/Pluralize.md deleted file mode 100644 index f168770d3..000000000 --- a/docs/content/en/functions/inflect/Pluralize.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: inflect.Pluralize -description: Pluralizes the given word according to a set of common English pluralization rules. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [pluralize] - returnType: string - signatures: [inflect.Pluralize INPUT] -aliases: [/functions/pluralize] ---- - -```go-html-template -{{ "cat" | pluralize }} → cats -``` diff --git a/docs/content/en/functions/inflect/Singularize.md b/docs/content/en/functions/inflect/Singularize.md deleted file mode 100644 index 41e05b784..000000000 --- a/docs/content/en/functions/inflect/Singularize.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: inflect.Singularize -description: Singularizes the given word according to a set of common English singularization rules. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [singularize] - returnType: string - signatures: [inflect.Singularize INPUT] -aliases: [/functions/singularize] ---- - -```go-html-template -{{ "cats" | singularize }} → cat -``` diff --git a/docs/content/en/functions/inflect/_index.md b/docs/content/en/functions/inflect/_index.md deleted file mode 100644 index 2afe4fe33..000000000 --- a/docs/content/en/functions/inflect/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Inflect functions -linkTitle: inflect -description: These functions provide word inflection features such as singularization and pluralization of English nouns. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/functions/js/Babel.md b/docs/content/en/functions/js/Babel.md deleted file mode 100644 index d0007aaa0..000000000 --- a/docs/content/en/functions/js/Babel.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -title: js.Babel -description: Compile the given JavaScript resource with Babel. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [babel] - returnType: resource.Resource - signatures: ['js.Babel [OPTIONS] RESOURCE'] ---- - -```go-html-template -{{ with resources.Get "js/main.js" }} - {{ $opts := dict - "minified" hugo.IsProduction - "noComments" hugo.IsProduction - "sourceMap" (cond hugo.IsProduction "none" "external") - }} - {{ with . | js.Babel $opts }} - {{ if hugo.IsProduction }} - {{ with . | fingerprint }} - - {{ end }} - {{ else }} - - {{ end }} - {{ end }} -{{ end }} -``` - -## Setup - -### Step 1 - -Install [Node.js](https://nodejs.org/en/download) - -### Step 2 - -Install the required Node.js packages in the root of your project. - -```sh -npm install --save-dev @babel/core @babel/cli -``` - -### Step 3 - -Add the babel executable to Hugo's `security.exec.allow` list in your site configuration: - -{{< code-toggle file=hugo >}} -[security.exec] - allow = ['^(dart-)?sass(-embedded)?$', '^go$', '^npx$', '^postcss$', '^babel$'] -{{< /code-toggle >}} - -## Configuration - -We add the main project's `node_modules` to `NODE_PATH` when running Babel and similar tools. There are some known [issues](https://github.com/babel/babel/issues/5618) with Babel in this area, so if you have a `babel.config.js` living in a Hugo Module (and not in the project itself), we recommend using `require` to load the presets/plugins, e.g.: - -```js -module.exports = { - presets: [ - [ - require("@babel/preset-env"), - { - useBuiltIns: "entry", - corejs: 3, - }, - ], - ], -}; -``` - -## Options - -compact -: (`bool`) Whether to remove optional newlines and whitespace. Enabled when `minified` is `true`. Default is `false` - -config -: (`string`) Path to the Babel configuration file. Hugo will, by default, look for a `babel.config.js` file in the root of your project. See [details](https://babeljs.io/docs/en/configuration). - -minified -: (`bool`) Whether to minify the compiled code. Enables the `compact` option. Default is `false`. - -noBabelrc -: (`string`) Whether to ignore `.babelrc` and `.babelignore` files. Default is `false`. - -noComments -: (`bool`) Whether to remove comments. Default is `false`. - -sourceMap -: (`string`) Whether to generate source maps, one of `external`, `inline`, or `none`. Default is `none`. - -verbose -: (`bool`) Whether to enable verbose logging. Default is `false` - - diff --git a/docs/content/en/functions/js/Batch.md b/docs/content/en/functions/js/Batch.md deleted file mode 100644 index a2c8bb893..000000000 --- a/docs/content/en/functions/js/Batch.md +++ /dev/null @@ -1,307 +0,0 @@ ---- -title: js.Batch -description: Build JavaScript bundle groups with global code splitting and flexible hooks/runners setup. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: js.Batcher - signatures: ['js.Batch [ID]'] ---- - -> [!note] -> For a runnable example of this feature, see [this test and demo repo](https://github.com/bep/hugojsbatchdemo/). - -The Batch `ID` is used to create the base directory for this batch. Forward slashes are allowed. `js.Batch` returns an object with an API with this structure: - -- [Group] - - [Script] - - [SetOptions] - - [Instance] - - [SetOptions] - - [Runner] - - [SetOptions] - - [Config] - - [SetOptions] - -## Group - -The `Group` method take an `ID` (`string`) as argument. No slashes. It returns an object with these methods: - -### Script - -The `Script` method takes an `ID` (`string`) as argument. No slashes. It returns an [OptionsSetter] that can be used to set [script options] for this script. - -```go-html-template -{{ with js.Batch "js/mybatch" }} - {{ with .Group "mygroup" }} - {{ with .Script "myscript" }} - {{ .SetOptions (dict "resource" (resources.Get "myscript.js")) }} - {{ end }} - {{ end }} -{{ end }} -``` - -`SetOptions` takes a [script options] map. Note that if you want the script to be handled by a [runner], you need to set the `export` option to match what you want to pass on to the runner (default is `*`). - -### Instance - -The `Instance` method takes two `string` arguments `SCRIPT_ID` and `INSTANCE_ID`. No slashes. It returns an [OptionsSetter] that can be used to set [params options] for this instance. - -```go-html-template -{{ with js.Batch "js/mybatch" }} - {{ with .Group "mygroup" }} - {{ with .Instance "myscript" "myinstance" }} - {{ .SetOptions (dict "params" (dict "param1" "value1")) }} - {{ end }} - {{ end }} -{{ end }} -``` - -`SetOptions` takes a [params options] map. The instance options will be passed to any [runner] script in the same group, as JSON. - -### Runner - -The `Runner` method takes an `ID` (`string`) as argument. No slashes. It returns an [OptionsSetter] that can be used to set [script options] for this runner. - -```go-html-template -{{ with js.Batch "js/mybatch" }} - {{ with .Group "mygroup" }} - {{ with .Runner "myrunner" }} - {{ .SetOptions (dict "resource" (resources.Get "myrunner.js")) }} - {{ end }} - {{ end }} -{{ end }} -``` - -`SetOptions` takes a [script options] map. - -The runner will receive a data structure with all instances for that group with a live binding of the [JavaScript import] of the defined `export`. - -The runner script's export must be a function that takes one argument, the group data structure. An example of a group data structure as JSON is: - -```json -{ - "id": "leaflet", - "scripts": [ - { - "id": "mapjsx", - "binding": JAVASCRIPT_BINDING, - "instances": [ - { - "id": "0", - "params": { - "c": "h-64", - "lat": 48.8533173846729, - "lon": 2.3497416090232535, - "r": "map.jsx", - "title": "Cathédrale Notre-Dame de Paris", - "zoom": 23 - } - }, - { - "id": "1", - "params": { - "c": "h-64", - "lat": 59.96300872062237, - "lon": 10.663529183196863, - "r": "map.jsx", - "title": "Holmenkollen", - "zoom": 3 - } - } - ] - } - ] -} -``` - -Below is an example of a runner script that uses React to render elements. Note that the export (`default`) must match the `export` option in the [script options] (`default` is the default value for runner scripts) (runnable versions of examples on this page can be found at [js.Batch Demo Repo]): - -```js -import * as ReactDOM from 'react-dom/client'; -import * as React from 'react'; - -export default function Run(group) { - console.log('Running react-create-elements.js', group); - const scripts = group.scripts; - for (const script of scripts) { - for (const instance of script.instances) { - /* This is a convention in this project. */ - let elId = `${script.id}-${instance.id}`; - let el = document.getElementById(elId); - if (!el) { - console.warn(`Element with id ${elId} not found`); - continue; - } - const root = ReactDOM.createRoot(el); - const reactEl = React.createElement(script.binding, instance.params); - root.render(reactEl); - } - } -} -``` - -### Config - -Returns an [OptionsSetter] that can be used to set [build options] for the batch. - -These are mostly the same as for `js.Build`, but note that: - -- `targetPath` is set automatically (there may be multiple outputs). -- ``format` must be `esm`, currently the only format supporting [code splitting]. -- ``params` will be available in the `@params/config` namespace in the scripts. This way you can import both the [script] or [runner] params and the [config] params with: - -```js -import * as params from "@params"; -import * as config from "@params/config"; -``` - -Setting the `Config` for a batch can be done from any template (including shortcode templates), but will only be set once (the first will win): - -```go-html-template -{{ with js.Batch "js/mybatch" }} - {{ with .Config }} - {{ .SetOptions (dict - "target" "es2023" - "format" "esm" - "jsx" "automatic" - "loaders" (dict ".png" "dataurl") - "minify" true - "params" (dict "param1" "value1") - ) - }} - {{ end }} -{{ end }} -``` - -## Options - -### Build options - -format -: (`string`) Currently only `esm` is supported in ESBuild's [code splitting]. - -{{% include "/_common/functions/js/options.md" %}} - -### Script options - -resource -: The resource to build. This can be a file resource or a virtual resource. - -export -: The export to bind the runner to. Set it to `*` to export the [entire namespace](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#namespace_import). Default is `default` for [runner] scripts and `*` for other [scripts](#script). - -importContext -: An additional context for resolving imports. Hugo will always check this one first before falling back to `assets` and `node_modules`. A common use of this is to resolve imports inside a page bundle. See [import context](#import-context). - -params -: A map of parameters that will be passed to the script as JSON. These gets bound to the `@params` namespace: - - ```js - import * as params from '@params'; - ``` - -### Params options - -params -: A map of parameters that will be passed to the script as JSON. - -### Import context - -Hugo will, by default, first try to resolve any import in [assets](/hugo-pipes/introduction/#asset-directory) and, if not found, let [ESBuild] resolve it (e.g. from `node_modules`). The `importContext` option can be used to set the first context for resolving imports. A common use of this is to resolve imports inside a [page bundle](/content-management/page-bundles/). - -```go-html-template -{{ $common := resources.Match "/js/headlessui/*.*" }} -{{ $importContext := (slice $.Page ($common.Mount "/js/headlessui" ".")) }} -``` - -You can pass any object that implements [Resource.Get](/methods/page/resources/#get). Pass a slice to set multiple contexts. - -The example above uses [`Resources.Mount`] to resolve a directory inside `assets` relative to the page bundle. - -### OptionsSetter - -An `OptionsSetter` is a special object that is returned once only. This means that you should wrap it with [with]: - -```go-html-template -{{ with .Script "myscript" }} - {{ .SetOptions (dict "resource" (resources.Get "myscript.js"))}} -{{ end }} -``` - -## Build - -The `Build` method returns an object with the following structure: - -- Groups (map) - - [`Resources`] - -Each [`Resource`] will be of media type `application/javascript` or `text/css`. - -In a template you would typically handle one group with a given `ID` (e.g. scripts for the current section). Because of the concurrent build, this needs to be done in a [`templates.Defer`] block: - -> [!note] -> The [`templates.Defer`] acts as a synchronisation point to handle scripts added concurrently by different templates. If you have a setup with where the batch is created in one go (in one template), you don't need it. -> -> See [this discussion](https://discourse.gohugo.io/t/js-batch-with-simple-global-script/53002/5?u=bep) for more. - -```go-html-template -{{ $group := .group }} -{{ with (templates.Defer (dict "key" $group "data" $group )) }} - {{ with (js.Batch "js/mybatch") }} - {{ with .Build }} - {{ with index .Groups $ }} - {{ range . }} - {{ $s := . }} - {{ if eq $s.MediaType.SubType "css" }} - - {{ else }} - - {{ end }} - {{ end }} - {{ end }} - {{ end }} -{{ end }} -``` - -## Known Issues - -In the official documentation for ESBuild's [code splitting], there's a warning note in the header. The two issues are: - - - `esm` is currently the only implemented output format. This means that it will not work for very old browsers. See [caniuse](https://caniuse.com/?search=ESM). - - There's a known import ordering issue. - -We have not seen the ordering issue as a problem during our [extensive testing](https://github.com/bep/hugojsbatchdemo) of this new feature with different libraries. There are two main cases: - -1. Undefined execution order of imports, see [this comment](https://github.com/evanw/esbuild/issues/399#issuecomment-1458680887) -1. Only one execution order of imports, see [this comment](https://github.com/evanw/esbuild/issues/399#issuecomment-735355932) - -Many would say that both of the above are [code smells](https://en.wikipedia.org/wiki/Code_smell). The first one has a simple workaround in Hugo. Define the import order in its own script and make sure it gets passed early to ESBuild, e.g. by putting it in a script group with a name that comes early in the alphabet. - -```js -import './lib2.js'; -import './lib1.js'; - -console.log('entrypoints-workaround.js'); -``` - -[`Resource`]: /methods/resource/ -[`Resources.Mount`]: /methods/page/resources/#mount -[`Resources`]: /methods/page/resources/ -[`templates.Defer`]: /functions/templates/defer/ -[`templates.Defer`]: /functions/templates/defer/ -[build options]: #build-options -[code splitting]: https://esbuild.github.io/api/#splitting -[config]: #config -[ESBuild]: https://github.com/evanw/esbuild -[JavaScript import]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import -[js.Batch Demo Repo]: https://github.com/bep/hugojsbatchdemo/ -[OptionsSetter]: #optionssetter -[params options]: #params-options -[runner]: #runner -[script]: #script -[script options]: #script-options -[SetOptions]: #optionssetter -[with]: /functions/go-template/with/ diff --git a/docs/content/en/functions/js/Build.md b/docs/content/en/functions/js/Build.md deleted file mode 100644 index 1bec6b16f..000000000 --- a/docs/content/en/functions/js/Build.md +++ /dev/null @@ -1,121 +0,0 @@ ---- -title: js.Build -description: Bundle, transpile, tree shake, and minify JavaScript resources. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: resource.Resource - signatures: ['js.Build [OPTIONS] RESOURCE'] ---- - -The `js.Build` function uses the [evanw/esbuild] package to: - -- Bundle -- Transpile (TypeScript and JSX) -- Tree shake -- Minify -- Create source maps - -```go-html-template -{{ with resources.Get "js/main.js" }} - {{ $opts := dict - "minify" hugo.IsProduction - "sourceMap" (cond hugo.IsProduction "" "external") - "targetPath" "js/main.js" - }} - {{ with . | js.Build $opts }} - {{ if hugo.IsProduction }} - {{ with . | fingerprint }} - - {{ end }} - {{ else }} - - {{ end }} - {{ end }} -{{ end }} -``` - -## Options - -targetPath -: (`string`) If not set, the source path will be used as the base target path. Note that the target path's extension may change if the target MIME type is different, e.g. when the source is TypeScript. - -format -: (`string`) The output format. One of: `iife`, `cjs`, `esm`. Default is `iife`, a self-executing function, suitable for inclusion as a ` -``` - -[evanw/esbuild]: https://github.com/evanw/esbuild diff --git a/docs/content/en/functions/js/_index.md b/docs/content/en/functions/js/_index.md deleted file mode 100644 index d3557a2d3..000000000 --- a/docs/content/en/functions/js/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: JavaScript functions -linkTitle: js -description: Use these functions to work with JavaScript and TypeScript files. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/functions/lang/FormatAccounting.md b/docs/content/en/functions/lang/FormatAccounting.md deleted file mode 100644 index d2a1d76f6..000000000 --- a/docs/content/en/functions/lang/FormatAccounting.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: lang.FormatAccounting -description: Returns a currency representation of a number for the given currency and precision for the current language and region in accounting notation. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: string - signatures: [lang.FormatAccounting PRECISION CURRENCY NUMBER] ---- - -```go-html-template -{{ 512.5032 | lang.FormatAccounting 2 "NOK" }} → NOK512.50 -``` - -{{% include "/_common/functions/locales.md" %}} diff --git a/docs/content/en/functions/lang/FormatCurrency.md b/docs/content/en/functions/lang/FormatCurrency.md deleted file mode 100644 index 327413c07..000000000 --- a/docs/content/en/functions/lang/FormatCurrency.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: lang.FormatCurrency -description: Returns a currency representation of a number for the given currency and precision for the current language and region. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: string - signatures: [lang.FormatCurrency PRECISION CURRENCY NUMBER] ---- - -```go-html-template -{{ 512.5032 | lang.FormatCurrency 2 "USD" }} → $512.50 -``` - -{{% include "/_common/functions/locales.md" %}} diff --git a/docs/content/en/functions/lang/FormatNumber.md b/docs/content/en/functions/lang/FormatNumber.md deleted file mode 100644 index 5bf37996a..000000000 --- a/docs/content/en/functions/lang/FormatNumber.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: lang.FormatNumber -description: Returns a numeric representation of a number with the given precision for the current language and region. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: string - signatures: [lang.FormatNumber PRECISION NUMBER] ---- - -```go-html-template -{{ 512.5032 | lang.FormatNumber 2 }} → 512.50 -``` - -{{% include "/_common/functions/locales.md" %}} diff --git a/docs/content/en/functions/lang/FormatNumberCustom.md b/docs/content/en/functions/lang/FormatNumberCustom.md deleted file mode 100644 index 0a70cd938..000000000 --- a/docs/content/en/functions/lang/FormatNumberCustom.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: lang.FormatNumberCustom -description: Returns a numeric representation of a number with the given precision using negative, decimal, and grouping options. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: string - signatures: ['lang.FormatNumberCustom PRECISION NUMBER [OPTIONS...]'] -aliases: ['/functions/numfmt/'] ---- - -This function formats a number with the given precision. The first options parameter is a space-delimited string of characters to represent negativity, the decimal point, and grouping. The default value is `- . ,`. The second options parameter defines an alternative delimiting character. - -Note that numbers are rounded up at 5 or greater. So, with precision set to 0, 1.5 becomes 2, and 1.4 becomes 1. - -For a simpler function that adapts to the current language, see [`lang.FormatNumber`]. - -```go-html-template -{{ lang.FormatNumberCustom 2 12345.6789 }} → 12,345.68 -{{ lang.FormatNumberCustom 2 12345.6789 "- , ." }} → 12.345,68 -{{ lang.FormatNumberCustom 6 -12345.6789 "- ." }} → -12345.678900 -{{ lang.FormatNumberCustom 0 -12345.6789 "- . ," }} → -12,346 -{{ lang.FormatNumberCustom 0 -12345.6789 "-|.| " "|" }} → -12 346 -``` - -{{% include "/_common/functions/locales.md" %}} - -[`lang.FormatNumber`]: /functions/lang/formatnumber/ diff --git a/docs/content/en/functions/lang/FormatPercent.md b/docs/content/en/functions/lang/FormatPercent.md deleted file mode 100644 index aef1fb64c..000000000 --- a/docs/content/en/functions/lang/FormatPercent.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: lang.FormatPercent -description: Returns a percentage representation of a number with the given precision for the current language and region. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: string - signatures: [lang.FormatPercent PRECISION NUMBER] ---- - -```go-html-template -{{ 512.5032 | lang.FormatPercent 2 }} → 512.50% -``` - -{{% include "/_common/functions/locales.md" %}} diff --git a/docs/content/en/functions/lang/Merge.md b/docs/content/en/functions/lang/Merge.md deleted file mode 100644 index db40c2669..000000000 --- a/docs/content/en/functions/lang/Merge.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: lang.Merge -description: Merge missing translations from other languages. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: any - signatures: [lang.Merge FROM TO] -aliases: [/functions/lang.merge] ---- - -As an example: - -```sh -{{ $pages := .Site.RegularPages | lang.Merge $frSite.RegularPages | lang.Merge $enSite.RegularPages }} -``` - -Will "fill in the gaps" in the current site with, from left to right, content from the French site, and lastly the English. - -A more practical example is to fill in the missing translations from the other languages: - -```sh -{{ $pages := .Site.RegularPages }} -{{ range .Site.Home.Translations }} -{{ $pages = $pages | lang.Merge .Site.RegularPages }} -{{ end }} - ``` diff --git a/docs/content/en/functions/lang/Translate.md b/docs/content/en/functions/lang/Translate.md deleted file mode 100644 index 00bb0e3f3..000000000 --- a/docs/content/en/functions/lang/Translate.md +++ /dev/null @@ -1,246 +0,0 @@ ---- -title: lang.Translate -description: Translates a string using the translation tables in the i18n directory. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [T, i18n] - returnType: string - signatures: ['lang.Translate KEY [CONTEXT]'] -aliases: [/functions/i18n] ---- - -The `lang.Translate` function returns the value associated with given key as defined in the translation table for the current language. - -If the key is not found in the translation table for the current language, the `lang.Translate` function falls back to the translation table for the [`defaultContentLanguage`]. - -If the key is not found in the translation table for the `defaultContentLanguage`, the `lang.Translate` function returns an empty string. - -> [!note] -> To list missing and fallback translations, use the `--printI18nWarnings` flag when building your site. -> -> To render placeholders for missing and fallback translations, set [`enableMissingTranslationPlaceholders`] to `true` in your site configuration. - -## Translation tables - -Create translation tables in the `i18n` directory, naming each file according to [RFC 5646]. Translation tables may be JSON, TOML, or YAML. For example: - -```text -i18n/en.toml -i18n/en-US.toml -``` - -The base name must match the [language key] as defined in your site configuration. - -Artificial languages with private use subtags as defined in [RFC 5646 § 2.2.7] are also supported. You may omit the `art-x-` prefix for brevity. For example: - -```text -i18n/art-x-hugolang.toml -i18n/hugolang.toml -``` - -> [!note] -> Private use subtags must not exceed 8 alphanumeric characters. - -## Simple translations - -Let's say your multilingual site supports two languages, English and Polish. Create a translation table for each language in the `i18n` directory. - -```text -i18n/ -├── en.toml -└── pl.toml -``` - -The English translation table: - -{{< code-toggle file=i18n/en >}} -privacy = 'privacy' -security = 'security' -{{< /code-toggle >}} - -The Polish translation table: - -{{< code-toggle file=i18n/pl >}} -privacy = 'prywatność' -security = 'bezpieczeństwo' -{{< /code-toggle >}} - -> [!note] -> The examples below use the `T` alias for brevity. - -When viewing the English language site: - -```go-html-template -{{ T "privacy" }} → privacy -{{ T "security" }} → security -```` - -When viewing the Polish language site: - -```go-html-template -{{ T "privacy" }} → prywatność -{{ T "security" }} → bezpieczeństwo -``` - -## Translations with pluralization - -Let's say your multilingual site supports two languages, English and Polish. Create a translation table for each language in the `i18n` directory. - -```text -i18n/ -├── en.toml -└── pl.toml -``` - -The Unicode [CLDR Plural Rules chart] describes the pluralization categories for each language. - -The English translation table: - -{{< code-toggle file=i18n/en >}} -[day] -one = 'day' -other = 'days' - -[day_with_count] -one = '{{ . }} day' -other = '{{ . }} days' -{{< /code-toggle >}} - -The Polish translation table: - -{{< code-toggle file=i18n/pl >}} -[day] -one = 'miesiąc' -few = 'miesiące' -many = 'miesięcy' -other = 'miesiąca' - -[day_with_count] -one = '{{ . }} miesiąc' -few = '{{ . }} miesiące' -many = '{{ . }} miesięcy' -other = '{{ . }} miesiąca' -{{< /code-toggle >}} - -> [!note] -> The examples below use the `T` alias for brevity. - -When viewing the English language site: - -```go-html-template -{{ T "day" 0 }} → days -{{ T "day" 1 }} → day -{{ T "day" 2 }} → days -{{ T "day" 5 }} → days - -{{ T "day_with_count" 0 }} → 0 days -{{ T "day_with_count" 1 }} → 1 day -{{ T "day_with_count" 2 }} → 2 days -{{ T "day_with_count" 5 }} → 5 days -```` - -When viewing the Polish language site: - -```go-html-template -{{ T "day" 0 }} → miesięcy -{{ T "day" 1 }} → miesiąc -{{ T "day" 2 }} → miesiące -{{ T "day" 5 }} → miesięcy - -{{ T "day_with_count" 0 }} → 0 miesięcy -{{ T "day_with_count" 1 }} → 1 miesiąc -{{ T "day_with_count" 2 }} → 2 miesiące -{{ T "day_with_count" 5 }} → 5 miesięcy -``` - -In the pluralization examples above, we passed an integer in context (the second argument). You can also pass a map in context, providing a `count` key to control pluralization. - -Translation table: - -{{< code-toggle file=i18n/en >}} -[age] -one = '{{ .name }} is {{ .count }} year old.' -other = '{{ .name }} is {{ .count }} years old.' -{{< /code-toggle >}} - -Template code: - -```go-html-template -{{ T "age" (dict "name" "Will" "count" 1) }} → Will is 1 year old. -{{ T "age" (dict "name" "John" "count" 3) }} → John is 3 years old. -``` - -> [!note] -> Translation tables may contain both simple translations and translations with pluralization. - -## Reserved keys - -Hugo uses the [go-i18n] package to look up values in translation tables. This package reserves the following keys for internal use: - -id -: (`string`) Uniquely identifies the message. - -description -: (`string`) Describes the message to give additional context to translators that may be relevant for translation. - -hash -: (`string`) Uniquely identifies the content of the message that this message was translated from. - -leftdelim -: (`string`) The left Go template delimiter. - -rightdelim -: (`string`) The right Go template delimiter. - -zero -: (`string`) The content of the message for the [CLDR] plural form "zero". - -one -: (`string`) The content of the message for the [CLDR] plural form "one". - -two -: (`string`) The content of the message for the [CLDR] plural form "two". - -few -: (`string`) The content of the message for the [CLDR] plural form "few". - -many -: (`string`) The content of the message for the [CLDR] plural form "many". - -other -: (`string`) The content of the message for the [CLDR] plural form "other". - -If you need to provide a translation for one of the reserved keys, you can prepend the word with an underscore. For example: - -{{< code-toggle file=i18n/es >}} -_description = 'descripción' -_few = 'pocos' -_many = 'muchos' -_one = 'uno' -_other = 'otro' -_two = 'dos' -_zero = 'cero' -{{< /code-toggle >}} - -Then in your templates: - -```go-html-template -{{ T "_description" }} → descripción -{{ T "_few" }} → pocos -{{ T "_many" }} → muchos -{{ T "_one" }} → uno -{{ T "_two" }} → dos -{{ T "_zero" }} → cero -{{ T "_other" }} → otro -``` - -[`defaultContentLanguage`]: /configuration/all/#defaultcontentlanguage -[`enableMissingTranslationPlaceholders`]: /configuration/all/#enablemissingtranslationplaceholders -[CLDR]: https://www.unicode.org/cldr/charts/43/supplemental/language_plural_rules.html -[CLDR Plural Rules chart]: https://www.unicode.org/cldr/charts/43/supplemental/language_plural_rules.html -[go-i18n]: https://github.com/nicksnyder/go-i18n -[language key]: /configuration/languages/#language-keys -[RFC 5646]: https://datatracker.ietf.org/doc/html/rfc5646 -[RFC 5646 § 2.2.7]: https://datatracker.ietf.org/doc/html/rfc5646#section-2.2.7 diff --git a/docs/content/en/functions/lang/_index.md b/docs/content/en/functions/lang/_index.md deleted file mode 100644 index de75cad45..000000000 --- a/docs/content/en/functions/lang/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Lang functions -linkTitle: lang -description: Use these functions to adapt your site to meet language and regional requirements. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/functions/math/Abs.md b/docs/content/en/functions/math/Abs.md deleted file mode 100644 index 60d8d6d64..000000000 --- a/docs/content/en/functions/math/Abs.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: math.Abs -description: Returns the absolute value of the given number. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: float64 - signatures: [math.Abs VALUE] ---- - -```go-html-template -{{ math.Abs -2.1 }} → 2.1 -``` diff --git a/docs/content/en/functions/math/Acos.md b/docs/content/en/functions/math/Acos.md deleted file mode 100644 index 32537e2dd..000000000 --- a/docs/content/en/functions/math/Acos.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: math.Acos -description: Returns the arccosine, in radians, of the given number. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: float64 - signatures: [math.Acos VALUE] ---- - -{{< new-in 0.130.0 />}} - -```go-html-template -{{ math.Acos 1 }} → 0 -``` diff --git a/docs/content/en/functions/math/Add.md b/docs/content/en/functions/math/Add.md deleted file mode 100644 index cd137c6c7..000000000 --- a/docs/content/en/functions/math/Add.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: math.Add -description: Adds two or more numbers. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [add] - returnType: any - signatures: [math.Add VALUE VALUE...] ---- - -If one of the numbers is a [`float`](g), the result is a `float`. - -```go-html-template -{{ add 12 3 2 }} → 17 -``` - -You can also use the `add` function to concatenate strings. - -```go-html-template -{{ add "hu" "go" }} → hugo -``` diff --git a/docs/content/en/functions/math/Asin.md b/docs/content/en/functions/math/Asin.md deleted file mode 100644 index 76114a72e..000000000 --- a/docs/content/en/functions/math/Asin.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: math.Asin -description: Returns the arcsine, in radians, of the given number. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: float64 - signatures: [math.Asin VALUE] ---- - -{{< new-in 0.130.0 />}} - -```go-html-template -{{ math.Asin 1 }} → 1.5707963267948966 -``` diff --git a/docs/content/en/functions/math/Atan.md b/docs/content/en/functions/math/Atan.md deleted file mode 100644 index 5c8268b47..000000000 --- a/docs/content/en/functions/math/Atan.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: math.Atan -description: Returns the arctangent, in radians, of the given number. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: float64 - signatures: [math.Atan VALUE] ---- - -{{< new-in 0.130.0 />}} - -```go-html-template -{{ math.Atan 1 }} → 0.7853981633974483 -``` diff --git a/docs/content/en/functions/math/Atan2.md b/docs/content/en/functions/math/Atan2.md deleted file mode 100644 index 942fffdf8..000000000 --- a/docs/content/en/functions/math/Atan2.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: math.Atan2 -description: Returns the arctangent, in radians, of the given number pair, determining the correct quadrant from their signs. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: float64 - signatures: [math.Atan2 VALUE VALUE] ---- - -{{< new-in 0.130.0 />}} - -```go-html-template -{{ math.Atan2 1 2 }} → 0.4636476090008061 -``` diff --git a/docs/content/en/functions/math/Ceil.md b/docs/content/en/functions/math/Ceil.md deleted file mode 100644 index 22a9d0299..000000000 --- a/docs/content/en/functions/math/Ceil.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: math.Ceil -description: Returns the least integer value greater than or equal to the given number. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: float64 - signatures: [math.Ceil VALUE] ---- - -```go-html-template -{{ math.Ceil 2.1 }} → 3 -``` diff --git a/docs/content/en/functions/math/Cos.md b/docs/content/en/functions/math/Cos.md deleted file mode 100644 index 249a064bb..000000000 --- a/docs/content/en/functions/math/Cos.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: math.Cos -description: Returns the cosine of the given radian number. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: float64 - signatures: [math.Cos VALUE] ---- - -{{< new-in 0.130.0 />}} - -```go-html-template -{{ math.Cos 1 }} → 0.5403023058681398 -``` diff --git a/docs/content/en/functions/math/Counter.md b/docs/content/en/functions/math/Counter.md deleted file mode 100644 index 16456cec6..000000000 --- a/docs/content/en/functions/math/Counter.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: math.Counter -description: Increments and returns a global counter. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: uint64 - signatures: [math.Counter] ---- - -The counter is global for both monolingual and multilingual sites, and its initial value for each build is 1. - -```go-html-template -{{ warnf "single.html called %d times" math.Counter }} -``` - -```sh -WARN single.html called 1 times -WARN single.html called 2 times -WARN single.html called 3 times -``` - -Use this function to: - -- Create unique warnings as shown above; the [`warnf`] function suppresses duplicate messages -- Create unique target paths for the `resources.FromString` function where the target path is also the cache key - -> [!note] -> Due to concurrency, the value returned in a given template for a given page will vary from one build to the next. You cannot use this function to assign a static id to each page. - -[`warnf`]: /functions/fmt/warnf/ diff --git a/docs/content/en/functions/math/Div.md b/docs/content/en/functions/math/Div.md deleted file mode 100644 index 0e338a9e9..000000000 --- a/docs/content/en/functions/math/Div.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: math.Div -description: Divides the first number by one or more numbers. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [div] - returnType: any - signatures: [math.Div VALUE VALUE...] ---- - -If one of the numbers is a [`float`](g), the result is a `float`. - -```go-html-template -{{ div 12 3 2 }} → 2 -``` diff --git a/docs/content/en/functions/math/Floor.md b/docs/content/en/functions/math/Floor.md deleted file mode 100644 index dbfd8620e..000000000 --- a/docs/content/en/functions/math/Floor.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: math.Floor -description: Returns the greatest integer value less than or equal to the given number. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: float64 - signatures: [math.Floor VALUE] ---- - -```go-html-template -{{ math.Floor 1.9 }} → 1 -``` diff --git a/docs/content/en/functions/math/Log.md b/docs/content/en/functions/math/Log.md deleted file mode 100644 index 123ffacd7..000000000 --- a/docs/content/en/functions/math/Log.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: math.Log -description: Returns the natural logarithm of the given number. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: float64 - signatures: [math.Log VALUE] ---- - -```go-html-template -{{ math.Log 42 }} → 3.737 -``` diff --git a/docs/content/en/functions/math/Max.md b/docs/content/en/functions/math/Max.md deleted file mode 100644 index d3a7676fc..000000000 --- a/docs/content/en/functions/math/Max.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: math.Max -description: Returns the greater of all numbers. Accepts scalars, slices, or both. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: float64 - signatures: [math.Max VALUE...] ---- - -```go-html-template -{{ math.Max 1 (slice 2 3) 4 }} → 4 -``` diff --git a/docs/content/en/functions/math/Min.md b/docs/content/en/functions/math/Min.md deleted file mode 100644 index 4f7d7b85a..000000000 --- a/docs/content/en/functions/math/Min.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: math.Min -description: Returns the smaller of all numbers. Accepts scalars, slices, or both. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: float64 - signatures: [math.Min VALUE...] ---- - -```go-html-template -{{ math.Min 1 (slice 2 3) 4 }} → 1 -``` diff --git a/docs/content/en/functions/math/Mod.md b/docs/content/en/functions/math/Mod.md deleted file mode 100644 index 6035a361e..000000000 --- a/docs/content/en/functions/math/Mod.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: math.Mod -description: Returns the modulus of two integers. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [mod] - returnType: int64 - signatures: [math.Mod VALUE1 VALUE2] ---- - -```go-html-template -{{ mod 15 3 }} → 0 -``` diff --git a/docs/content/en/functions/math/ModBool.md b/docs/content/en/functions/math/ModBool.md deleted file mode 100644 index fc7813d67..000000000 --- a/docs/content/en/functions/math/ModBool.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: math.ModBool -description: Reports whether the modulus of two integers equals 0. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [modBool] - returnType: bool - signatures: [math.ModBool VALUE1 VALUE2] ---- - -```go-html-template -{{ modBool 15 3 }} → true -``` diff --git a/docs/content/en/functions/math/Mul.md b/docs/content/en/functions/math/Mul.md deleted file mode 100644 index 69f2dbe1b..000000000 --- a/docs/content/en/functions/math/Mul.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: math.Mul -description: Multiplies two or more numbers. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [mul] - returnType: any - signatures: [math.Mul VALUE VALUE...] ---- - -If one of the numbers is a [`float`](g), the result is a `float`. - -```go-html-template -{{ mul 12 3 2 }} → 72 -``` diff --git a/docs/content/en/functions/math/Pi.md b/docs/content/en/functions/math/Pi.md deleted file mode 100644 index 0bc74bf03..000000000 --- a/docs/content/en/functions/math/Pi.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: math.Pi -description: Returns the mathematical constant pi. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: float64 - signatures: [math.Pi] ---- - -{{< new-in 0.130.0 />}} - -```go-html-template -{{ math.Pi }} → 3.141592653589793 -``` diff --git a/docs/content/en/functions/math/Pow.md b/docs/content/en/functions/math/Pow.md deleted file mode 100644 index a4384305d..000000000 --- a/docs/content/en/functions/math/Pow.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: math.Pow -description: Returns the first number raised to the power of the second number. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [pow] - returnType: float64 - signatures: [math.Pow VALUE1 VALUE2] ---- - -```go-html-template -{{ math.Pow 2 3 }} → 8 -``` diff --git a/docs/content/en/functions/math/Product.md b/docs/content/en/functions/math/Product.md deleted file mode 100644 index ffb1afe46..000000000 --- a/docs/content/en/functions/math/Product.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: math.Product -description: Returns the product of all numbers. Accepts scalars, slices, or both. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: float64 - signatures: [math.Product VALUE...] ---- - -```go-html-template -{{ math.Product 1 (slice 2 3) 4 }} → 24 -``` diff --git a/docs/content/en/functions/math/Rand.md b/docs/content/en/functions/math/Rand.md deleted file mode 100644 index d659e651f..000000000 --- a/docs/content/en/functions/math/Rand.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: math.Rand -description: Returns a pseudo-random number in the half-open interval [0.0, 1.0). -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: float64 - signatures: [math.Rand] ---- - -{{< new-in 0.121.2 />}} - -The `math.Rand` function returns a pseudo-random number in the half-open [interval](g) [0.0, 1.0). - -```go-html-template -{{ math.Rand }} → 0.6312770459590062 -``` - -To generate a random integer in the closed interval [0, 5]: - -```go-html-template -{{ math.Rand | mul 6 | math.Floor }} -``` - -To generate a random integer in the closed interval [1, 6]: - -```go-html-template -{{ math.Rand | mul 6 | math.Ceil }} -``` - -To generate a random float, with one digit after the decimal point, in the closed interval [0, 4.9]: - -```go-html-template -{{ div (math.Rand | mul 50 | math.Floor) 10 }} -``` - -To generate a random float, with one digit after the decimal point, in the closed interval [0.1, 5.0]: - -```go-html-template -{{ div (math.Rand | mul 50 | math.Ceil) 10 }} -``` diff --git a/docs/content/en/functions/math/Round.md b/docs/content/en/functions/math/Round.md deleted file mode 100644 index 6bc015ce7..000000000 --- a/docs/content/en/functions/math/Round.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: math.Round -description: Returns the nearest integer, rounding half away from zero. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: float64 - signatures: [math.Round VALUE] ---- - -```go-html-template -{{ math.Round 1.5 }} → 2 -``` diff --git a/docs/content/en/functions/math/Sin.md b/docs/content/en/functions/math/Sin.md deleted file mode 100644 index b5ab86bb8..000000000 --- a/docs/content/en/functions/math/Sin.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: math.Sin -description: Returns the sine of the given radian number. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: float64 - signatures: [math.Sin VALUE] ---- - -{{< new-in 0.130.0 />}} - -```go-html-template -{{ math.Sin 1 }} → 0.8414709848078965 -``` diff --git a/docs/content/en/functions/math/Sqrt.md b/docs/content/en/functions/math/Sqrt.md deleted file mode 100644 index b2f49fdbd..000000000 --- a/docs/content/en/functions/math/Sqrt.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: math.Sqrt -description: Returns the square root of the given number. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: float64 - signatures: [math.Sqrt VALUE] ---- - -```go-html-template -{{ math.Sqrt 81 }} → 9 -``` diff --git a/docs/content/en/functions/math/Sub.md b/docs/content/en/functions/math/Sub.md deleted file mode 100644 index 49459cd71..000000000 --- a/docs/content/en/functions/math/Sub.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: math.Sub -description: Subtracts one or more numbers from the first number. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [sub] - returnType: any - signatures: [math.Sub VALUE VALUE...] ---- - -If one of the numbers is a [`float`](g), the result is a `float`. - -```go-html-template -{{ sub 12 3 2 }} → 7 -``` diff --git a/docs/content/en/functions/math/Sum.md b/docs/content/en/functions/math/Sum.md deleted file mode 100644 index e14bc924b..000000000 --- a/docs/content/en/functions/math/Sum.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: math.Sum -description: Returns the sum of all numbers. Accepts scalars, slices, or both. -categories: [] -params: - functions_and_methods: - aliases: [] - returnType: float64 - signatures: [math.Sum VALUE...] ---- - -```go-html-template -{{ math.Sum 1 (slice 2 3) 4 }} → 10 -``` diff --git a/docs/content/en/functions/math/Tan.md b/docs/content/en/functions/math/Tan.md deleted file mode 100644 index c4f861c05..000000000 --- a/docs/content/en/functions/math/Tan.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: math.Tan -description: Returns the tangent of the given radian number. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: float64 - signatures: [math.Tan VALUE] ---- - -{{< new-in 0.130.0 />}} - -```go-html-template -{{ math.Tan 1 }} → 1.557407724654902 -``` diff --git a/docs/content/en/functions/math/ToDegrees.md b/docs/content/en/functions/math/ToDegrees.md deleted file mode 100644 index f01cd4728..000000000 --- a/docs/content/en/functions/math/ToDegrees.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: math.ToDegrees -description: ToDegrees converts radians into degrees. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: float64 - signatures: [math.ToDegrees VALUE] ---- - -{{< new-in 0.130.0 />}} - -```go-html-template -{{ math.ToDegrees 1.5707963267948966 }} → 90 -``` diff --git a/docs/content/en/functions/math/ToRadians.md b/docs/content/en/functions/math/ToRadians.md deleted file mode 100644 index b5acbb65b..000000000 --- a/docs/content/en/functions/math/ToRadians.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: math.ToRadians -description: ToRadians converts degrees into radians. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: float64 - signatures: [math.ToRadians VALUE] ---- - -{{< new-in 0.130.0 />}} - -```go-html-template -{{ math.ToRadians 90 }} → 1.5707963267948966 -``` diff --git a/docs/content/en/functions/math/_index.md b/docs/content/en/functions/math/_index.md deleted file mode 100644 index c58a5a704..000000000 --- a/docs/content/en/functions/math/_index.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Math functions -linkTitle: math -description: Use these functions to perform mathematical operations. -categories: [] ---- diff --git a/docs/content/en/functions/openapi3/Unmarshal.md b/docs/content/en/functions/openapi3/Unmarshal.md deleted file mode 100644 index d1f928aeb..000000000 --- a/docs/content/en/functions/openapi3/Unmarshal.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: openapi3.Unmarshal -description: Unmarshals the given resource into an OpenAPI 3 document. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: openapi3.OpenAPIDocument - signatures: ['openapi3.Unmarshal RESOURCE'] ---- - -Use the `openapi3.Unmarshal` function with [global resources](g), [page resources](g), or [remote resources](g). - -[OpenAPI]: https://www.openapis.org/ - -For example, to work with a remote [OpenAPI] definition: - -```go-html-template -{{ $url := "https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/examples/v3.0/petstore.json" }} -{{ $api := "" }} -{{ with try (resources.GetRemote $url) }} - {{ with .Err }} - {{ errorf "%s" . }} - {{ else with .Value }} - {{ $api = . | openapi3.Unmarshal }} - {{ else }} - {{ errorf "Unable to get remote resource %q" $url }} - {{ end }} -{{ end }} -``` - -To inspect the data structure: - -```go-html-template -
    {{ debug.Dump $api }}
    -``` - -To list the GET and POST operations for each of the API paths: - -```go-html-template -{{ range $path, $details := $api.Paths }} -

    {{ $path }}

    -
    - {{ with $details.Get }} -
    GET
    -
    {{ .Summary }}
    - {{ end }} - {{ with $details.Post }} -
    POST
    -
    {{ .Summary }}
    - {{ end }} -
    -{{ end }} -``` - -Hugo renders this to: - -```html -

    /pets

    -
    -
    GET
    -
    List all pets
    -
    POST
    -
    Create a pet
    -
    -

    /pets/{petId}

    -
    -
    GET
    -
    Info for a specific pet
    -
    -``` diff --git a/docs/content/en/functions/openapi3/_index.md b/docs/content/en/functions/openapi3/_index.md deleted file mode 100644 index 852daf7b5..000000000 --- a/docs/content/en/functions/openapi3/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: OpenAPI functions -linkTitle: openapi3 -description: Use these functions to work with OpenAPI 3 definitions. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/functions/os/FileExists.md b/docs/content/en/functions/os/FileExists.md deleted file mode 100644 index b8a01a3e7..000000000 --- a/docs/content/en/functions/os/FileExists.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: os.FileExists -description: Reports whether the file or directory exists. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [fileExists] - returnType: bool - signatures: [os.FileExists PATH] -aliases: [/functions/fileexists] ---- - -The `os.FileExists` function attempts to resolve the path relative to the root of your project directory. If a matching file or directory is not found, it will attempt to resolve the path relative to the [`contentDir`](/configuration/all/#contentdir). A leading path separator (`/`) is optional. - -With this directory structure: - -```text -content/ -├── about.md -├── contact.md -└── news/ - ├── article-1.md - └── article-2.md -``` - -The function returns these values: - -```go-html-template -{{ fileExists "content" }} → true -{{ fileExists "content/news" }} → true -{{ fileExists "content/news/article-1" }} → false -{{ fileExists "content/news/article-1.md" }} → true -{{ fileExists "news" }} → true -{{ fileExists "news/article-1" }} → false -{{ fileExists "news/article-1.md" }} → true -``` diff --git a/docs/content/en/functions/os/Getenv.md b/docs/content/en/functions/os/Getenv.md deleted file mode 100644 index 04215e6c3..000000000 --- a/docs/content/en/functions/os/Getenv.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: os.Getenv -description: Returns the value of an environment variable, or an empty string if the environment variable is not set. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [getenv] - returnType: string - signatures: [os.Getenv VARIABLE] -aliases: [/functions/getenv] ---- - -## Security - -By default, when using the `os.Getenv` function Hugo allows access to: - -- The `CI` environment variable -- Any environment variable beginning with `HUGO_` - -To access other environment variables, adjust your site configuration. For example, to allow access to the `HOME` and `USER` environment variables: - -{{< code-toggle file=hugo >}} -[security.funcs] -getenv = ['^HUGO_', '^CI$', '^USER$', '^HOME$'] -{{< /code-toggle >}} - -For more information see [configure security](/configuration/security). - -## Examples - -```go-html-template -{{ getenv "HOME" }} → /home/victor -{{ getenv "USER" }} → victor -``` - -You can pass values when building your site: - -```sh -MY_VAR1=foo MY_VAR2=bar hugo - -OR - -export MY_VAR1=foo -export MY_VAR2=bar -hugo -``` - -And then retrieve the values within a template: - -```go-html-template -{{ getenv "MY_VAR1" }} → foo -{{ getenv "MY_VAR2" }} → bar -``` diff --git a/docs/content/en/functions/os/ReadDir.md b/docs/content/en/functions/os/ReadDir.md deleted file mode 100644 index 65c398a31..000000000 --- a/docs/content/en/functions/os/ReadDir.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: os.ReadDir -description: Returns an array of FileInfo structures sorted by file name, one element for each directory entry. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [readDir] - returnType: os.FileInfo - signatures: [os.ReadDir PATH] -aliases: [/functions/readdir] ---- - -The `os.ReadDir` function resolves the path relative to the root of your project directory. A leading path separator (`/`) is optional. - -With this directory structure: - -```text -content/ -├── about.md -├── contact.md -└── news/ - ├── article-1.md - └── article-2.md -``` - -This template code: - -```go-html-template -{{ range readDir "content" }} - {{ .Name }} → {{ .IsDir }} -{{ end }} -``` - -Produces: - -```html -about.md → false -contact.md → false -news → true -``` - -Note that `os.ReadDir` is not recursive. - -Details of the `FileInfo` structure are available in the [Go documentation](https://pkg.go.dev/io/fs#FileInfo). diff --git a/docs/content/en/functions/os/ReadFile.md b/docs/content/en/functions/os/ReadFile.md deleted file mode 100644 index 7f25327c8..000000000 --- a/docs/content/en/functions/os/ReadFile.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: os.ReadFile -description: Returns the contents of a file. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [readFile] - returnType: string - signatures: [os.ReadFile PATH] -aliases: [/functions/readfile] ---- - -The `os.ReadFile` function attempts to resolve the path relative to the root of your project directory. If a matching file is not found, it will attempt to resolve the path relative to the [`contentDir`](/configuration/all/#contentdir). A leading path separator (`/`) is optional. - -With a file named README.md in the root of your project directory: - -```text -This is **bold** text. -``` - -This template code: - -```go-html-template -{{ readFile "README.md" }} -``` - -Produces: - -```html -This is **bold** text. -``` - -Note that `os.ReadFile` returns raw (uninterpreted) content. diff --git a/docs/content/en/functions/os/Stat.md b/docs/content/en/functions/os/Stat.md deleted file mode 100644 index 63cb3f26a..000000000 --- a/docs/content/en/functions/os/Stat.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: os.Stat -description: Returns a FileInfo structure describing a file or directory. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: os.FileInfo - signatures: [os.Stat PATH] -aliases: [/functions/os.stat] ---- - -The `os.Stat` function attempts to resolve the path relative to the root of your project directory. If a matching file or directory is not found, it will attempt to resolve the path relative to the [`contentDir`](/configuration/all/#contentdir). A leading path separator (`/`) is optional. - -```go-html-template -{{ $f := os.Stat "README.md" }} -{{ $f.IsDir }} → false (bool) -{{ $f.ModTime }} → 2021-11-25 10:06:49.315429236 -0800 PST (time.Time) -{{ $f.Name }} → README.md (string) -{{ $f.Size }} → 241 (int64) - -{{ $d := os.Stat "content" }} -{{ $d.IsDir }} → true (bool) -``` - -Details of the `FileInfo` structure are available in the [Go documentation](https://pkg.go.dev/io/fs#FileInfo). diff --git a/docs/content/en/functions/os/_index.md b/docs/content/en/functions/os/_index.md deleted file mode 100644 index b125f7004..000000000 --- a/docs/content/en/functions/os/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: OS functions -linkTitle: os -description: Use these functions to interact with the operating system. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/functions/partials/Include.md b/docs/content/en/functions/partials/Include.md deleted file mode 100644 index eb7eeafdc..000000000 --- a/docs/content/en/functions/partials/Include.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -title: partials.Include -description: Executes the given partial template, optionally passing context. If the partial template contains a return statement, returns the given value, else returns the rendered output. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [partial] - returnType: any - signatures: ['partials.Include NAME [CONTEXT]'] -aliases: [/functions/partial] ---- - -Without a [`return`] statement, the `partial` function returns a string of type `template.HTML`. With a `return` statement, the `partial` function can return any data type. - -[`return`]: /functions/go-template/return/ - -In this example we have three partial templates: - -```text -layouts/ -└── partials/ - ├── average.html - ├── breadcrumbs.html - └── footer.html -``` - -The "average" partial returns the average of one or more numbers. We pass the numbers in context: - -```go-html-template -{{ $numbers := slice 1 6 7 42 }} -{{ $average := partial "average.html" $numbers }} -``` - -The "breadcrumbs" partial renders [breadcrumb navigation], and needs to receive the current page in context: - -```go-html-template -{{ partial "breadcrumbs.html" . }} -``` - -The "footer" partial renders the site footer. In this contrived example, the footer does not need access to the current page, so we can omit context: - -```go-html-template -{{ partial "footer.html" }} -``` - -You can pass anything in context: a page, a page collection, a scalar value, a slice, or a map. In this example we pass the current page and three scalar values: - -```go-html-template -{{ $ctx := dict - "page" . - "name" "John Doe" - "major" "Finance" - "gpa" 4.0 -}} -{{ partial "render-student-info.html" $ctx }} -``` - -Then, within the partial template: - -```go-html-template -

    {{ .name }} is majoring in {{ .major }}.

    -

    Their grade point average is {{ .gpa }}.

    -

    See details.

    -``` - -To return a value from a partial template, it must contain only one `return` statement, placed at the end of the template: - -```go-html-template -{{ $result := "" }} -{{ if math.ModBool . 2 }} - {{ $result = "even" }} -{{ else }} - {{ $result = "odd" }} -{{ end }} -{{ return $result }} -``` - -See [details][`return`]. - -[`return`]: /functions/go-template/return/ - -[breadcrumb navigation]: /content-management/sections/#ancestors-and-descendants -[details]: /functions/go-template/return/ diff --git a/docs/content/en/functions/partials/IncludeCached.md b/docs/content/en/functions/partials/IncludeCached.md deleted file mode 100644 index 3905ee15e..000000000 --- a/docs/content/en/functions/partials/IncludeCached.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -title: partials.IncludeCached -description: Executes the given template and caches the result, optionally passing context. If the partial template contains a return statement, returns the given value, else returns the rendered output. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [partialCached] - returnType: any - signatures: ['partials.IncludeCached LAYOUT CONTEXT [VARIANT...]'] -aliases: [/functions/partialcached] ---- - -Without a [`return`] statement, the `partialCached` function returns a string of type `template.HTML`. With a `return` statement, the `partialCached` function can return any data type. - -The `partialCached` function can offer significant performance gains for complex templates that don't need to be re-rendered on every invocation. - -> [!note] -> Each Site (or language) has its own `partialCached` cache, so each site will execute a partial once. -> -> Hugo renders pages in parallel, and will render the partial more than once with concurrent calls to the `partialCached` function. After Hugo caches the rendered partial, new pages entering the build pipeline will use the cached result. - -Here is the simplest usage: - -```go-html-template -{{ partialCached "footer.html" . }} -``` - -Pass additional arguments to `partialCached` to create variants of the cached partial. For example, if you have a complex partial that should be identical when rendered for pages within the same section, use a variant based on section so that the partial is only rendered once per section: - -```go-html-template {file="layouts/_default/baseof.html"} -{{ partialCached "footer.html" . .Section }} -``` - -Pass additional arguments, of any data type, as needed to create unique variants: - -```go-html-template -{{ partialCached "footer.html" . .Params.country .Params.province }} -``` - -The variant arguments are not available to the underlying partial template; they are only used to create unique cache keys. - -To return a value from a partial template, it must contain only one `return` statement, placed at the end of the template: - -```go-html-template -{{ $result := "" }} -{{ if math.ModBool . 2 }} - {{ $result = "even" }} -{{ else }} - {{ $result = "odd" }} -{{ end }} -{{ return $result }} -``` - -See [details][`return`]. - -[`return`]: /functions/go-template/return/ diff --git a/docs/content/en/functions/partials/_index.md b/docs/content/en/functions/partials/_index.md deleted file mode 100644 index 09b467399..000000000 --- a/docs/content/en/functions/partials/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Partial functions -linkTitle: partials -description: Use these functions to call partial templates. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/functions/path/Base.md b/docs/content/en/functions/path/Base.md deleted file mode 100644 index 39d52bfcb..000000000 --- a/docs/content/en/functions/path/Base.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -title: path.Base -description: Replaces path separators with slashes (`/`) and returns the last element of the given path. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: string - signatures: [path.Base PATH] -aliases: [/functions/path.base] ---- - -```go-html-template -{{ path.Base "a/news.html" }} → news.html -{{ path.Base "news.html" }} → news.html -{{ path.Base "a/b/c" }} → c -{{ path.Base "/x/y/z/" }} → z -{{ path.Base "" }} → . -``` diff --git a/docs/content/en/functions/path/BaseName.md b/docs/content/en/functions/path/BaseName.md deleted file mode 100644 index 468898a6f..000000000 --- a/docs/content/en/functions/path/BaseName.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -title: path.BaseName -description: Replaces path separators with slashes (`/`) and returns the last element of the given path, removing the extension if present. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: string - signatures: [path.BaseName PATH] -aliases: [/functions/path.basename] ---- - -```go-html-template -{{ path.BaseName "a/news.html" }} → news -{{ path.BaseName "news.html" }} → news -{{ path.BaseName "a/b/c" }} → c -{{ path.BaseName "/x/y/z/" }} → z -{{ path.BaseName "" }} → . -``` diff --git a/docs/content/en/functions/path/Clean.md b/docs/content/en/functions/path/Clean.md deleted file mode 100644 index b9f2de038..000000000 --- a/docs/content/en/functions/path/Clean.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: path.Clean -description: Replaces path separators with slashes (`/`) and returns the shortest path name equivalent to the given path. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: string - signatures: [path.Clean PATH] -aliases: [/functions/path.clean] ---- - -See Go's [`path.Clean`] documentation for details. - -[`path.Clean`]: https://pkg.go.dev/path#Clean - -```go-html-template -{{ path.Clean "foo/bar" }} → foo/bar -{{ path.Clean "/foo/bar" }} → /foo/bar -{{ path.Clean "/foo/bar/" }} → /foo/bar -{{ path.Clean "/foo//bar/" }} → /foo/bar -{{ path.Clean "/foo/./bar/" }} → /foo/bar -{{ path.Clean "/foo/../bar/" }} → /bar -{{ path.Clean "/../foo/../bar/" }} → /bar -{{ path.Clean "" }} → . -``` diff --git a/docs/content/en/functions/path/Dir.md b/docs/content/en/functions/path/Dir.md deleted file mode 100644 index 04d5500f5..000000000 --- a/docs/content/en/functions/path/Dir.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -title: path.Dir -description: Replaces path separators with slashes (/) and returns all but the last element of the given path. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: string - signatures: [path.Dir PATH] -aliases: [/functions/path.dir] ---- - -```go-html-template -{{ path.Dir "a/news.html" }} → a -{{ path.Dir "news.html" }} → . -{{ path.Dir "a/b/c" }} → a/b -{{ path.Dir "/a/b/c" }} → /a/b -{{ path.Dir "/a/b/c/" }} → /a/b/c -{{ path.Dir "" }} → . -``` diff --git a/docs/content/en/functions/path/Ext.md b/docs/content/en/functions/path/Ext.md deleted file mode 100644 index 3646d02d0..000000000 --- a/docs/content/en/functions/path/Ext.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: path.Ext -description: Replaces path separators with slashes (`/`) and returns the file name extension of the given path. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: string - signatures: [path.Ext PATH] -aliases: [/functions/path.ext] ---- - -The extension is the suffix beginning at the final dot in the final slash-separated element of path; it is empty if there is no dot. - -```go-html-template -{{ path.Ext "a/b/c/news.html" }} → .html -``` diff --git a/docs/content/en/functions/path/Join.md b/docs/content/en/functions/path/Join.md deleted file mode 100644 index bda46737f..000000000 --- a/docs/content/en/functions/path/Join.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: path.Join -description: Replaces path separators with slashes (`/`), joins the given path elements into a single path, and returns the shortest path name equivalent to the result. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: string - signatures: [path.Join ELEMENT...] -aliases: [/functions/path.join] ---- - -See Go's [`path.Join`] and [`path.Clean`] documentation for details. - -[`path.Clean`]: https://pkg.go.dev/path#Clean -[`path.Join`]: https://pkg.go.dev/path#Join - -```go-html-template -{{ path.Join "partial" "news.html" }} → partial/news.html -{{ path.Join "partial/" "news.html" }} → partial/news.html -{{ path.Join "foo/bar" "baz" }} → foo/bar/baz -{{ path.Join "foo" "bar" "baz" }} → foo/bar/baz -{{ path.Join "foo" "" "baz" }} → foo/baz -{{ path.Join "foo" "." "baz" }} → foo/baz -{{ path.Join "foo" ".." "baz" }} → baz -{{ path.Join "/.." "foo" ".." "baz" }} → baz -``` diff --git a/docs/content/en/functions/path/Split.md b/docs/content/en/functions/path/Split.md deleted file mode 100644 index d4f8d08e0..000000000 --- a/docs/content/en/functions/path/Split.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: path.Split -description: Replaces path separators with slashes (`/`) and splits the resulting path immediately following the final slash, separating it into a directory and file name component. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: paths.DirFile - signatures: [path.Split PATH] -aliases: [/functions/path.split] ---- - -If there is no slash in the given path, `path.Split` returns an empty directory, and file set to path. The returned values have the property that path = dir+file. - -```go-html-template -{{ $dirFile := path.Split "a/news.html" }} -{{ $dirFile.Dir }} → a/ -{{ $dirFile.File }} → news.html - -{{ $dirFile := path.Split "news.html" }} -{{ $dirFile.Dir }} → "" (empty string) -{{ $dirFile.File }} → news.html - -{{ $dirFile := path.Split "a/b/c" }} -{{ $dirFile.Dir }} → a/b/ -{{ $dirFile.File }} → c -``` diff --git a/docs/content/en/functions/path/_index.md b/docs/content/en/functions/path/_index.md deleted file mode 100644 index 49b2927ce..000000000 --- a/docs/content/en/functions/path/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Path functions -linkTitle: path -description: Use these functions to work with file paths. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/functions/reflect/IsMap.md b/docs/content/en/functions/reflect/IsMap.md deleted file mode 100644 index 55b9811d7..000000000 --- a/docs/content/en/functions/reflect/IsMap.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: reflect.IsMap -description: Reports whether the given value is a map. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: bool - signatures: [reflect.IsMap INPUT] -aliases: [/functions/reflect.ismap] ---- - -```go-html-template -{{ reflect.IsMap (dict "key" "value") }} → true -{{ reflect.IsMap "yo" }} → false -``` diff --git a/docs/content/en/functions/reflect/IsSlice.md b/docs/content/en/functions/reflect/IsSlice.md deleted file mode 100644 index 3a5c1229b..000000000 --- a/docs/content/en/functions/reflect/IsSlice.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: reflect.IsSlice -description: Reports whether the given value is a slice. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: bool - signatures: [reflect.IsSlice INPUT] -aliases: [/functions/reflect.isslice] ---- - -```go-html-template -{{ reflect.IsSlice (slice 1 2 3) }} → true -{{ reflect.IsSlice "yo" }} → false -``` diff --git a/docs/content/en/functions/reflect/_index.md b/docs/content/en/functions/reflect/_index.md deleted file mode 100644 index 58f00c3de..000000000 --- a/docs/content/en/functions/reflect/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Reflect functions -linkTitle: reflect -description: Use these functions to determine a value's data type. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/functions/resources/Babel.md b/docs/content/en/functions/resources/Babel.md deleted file mode 100644 index 799823fc5..000000000 --- a/docs/content/en/functions/resources/Babel.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: resources.Babel -description: Compiles the given JavaScript resource with Babel. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: resource.Resource - signatures: ['resources.Babel [OPTIONS] RESOURCE'] -expiryDate: 2026-06-24 # deprecated 2024-06-24 in v0.128.0 ---- - -{{< deprecated-in 0.128.0 >}} -Use [`js.Babel`] instead. - -[`js.Babel`]: /functions/js/babel/ -{{< /deprecated-in >}} diff --git a/docs/content/en/functions/resources/ByType.md b/docs/content/en/functions/resources/ByType.md deleted file mode 100644 index 99e2b9771..000000000 --- a/docs/content/en/functions/resources/ByType.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: resources.ByType -description: Returns a collection of global resources of the given media type, or nil if none found. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: resource.Resources - signatures: [resources.ByType MEDIATYPE] ---- - -The [media type] is typically one of `image`, `text`, `audio`, `video`, or `application`. - -```go-html-template -{{ range resources.ByType "image" }} - -{{ end }} -``` - -> [!note] -> This function operates on global resources. A global resource is a file within the `assets` directory, or within any directory mounted to the `assets` directory. -> -> For page resources, use the [`Resources.ByType`] method on a `Page` object. - -[`Resources.ByType`]: /methods/page/resources/ -[media type]: https://en.wikipedia.org/wiki/Media_type diff --git a/docs/content/en/functions/resources/Concat.md b/docs/content/en/functions/resources/Concat.md deleted file mode 100644 index fe7226e1b..000000000 --- a/docs/content/en/functions/resources/Concat.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: resources.Concat -description: Returns a concatenated slice of resources. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: resource.Resource - signatures: ['resources.Concat TARGETPATH [RESOURCE...]'] ---- - -The `resources.Concat` function returns a concatenated slice of resources, caching the result using the target path as its cache key. Each resource must have the same [media type]. - -Hugo publishes the resource to the target path when you call its [`Publish`], [`Permalink`], or [`RelPermalink`] method. - -[media type]: https://en.wikipedia.org/wiki/Media_type -[`publish`]: /methods/resource/publish/ -[`permalink`]: /methods/resource/permalink/ -[`relpermalink`]: /methods/resource/relpermalink/ - -```go-html-template -{{ $plugins := resources.Get "js/plugins.js" }} -{{ $global := resources.Get "js/global.js" }} -{{ $js := slice $plugins $global | resources.Concat "js/bundle.js" }} -``` diff --git a/docs/content/en/functions/resources/Copy.md b/docs/content/en/functions/resources/Copy.md deleted file mode 100644 index 220a3db4c..000000000 --- a/docs/content/en/functions/resources/Copy.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: resources.Copy -description: Copies the given resource to the target path. -categories: [] -params: - functions_and_methods: - aliases: [] - returnType: resource.Resource - signatures: [resources.Copy TARGETPATH RESOURCE] ---- - -```go-html-template -{{ with resources.Get "images/a.jpg" }} - {{ with resources.Copy "img/new-image-name.jpg" . }} - - {{ end }} -{{ end }} -``` - -The relative URL of the new published resource will be: - -```text -/img/new-image-name.jpg -``` - -> [!note] -> Use the `resources.Copy` function with global, page, and remote resources. diff --git a/docs/content/en/functions/resources/ExecuteAsTemplate.md b/docs/content/en/functions/resources/ExecuteAsTemplate.md deleted file mode 100644 index bff83832e..000000000 --- a/docs/content/en/functions/resources/ExecuteAsTemplate.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -title: resources.ExecuteAsTemplate -description: Returns a resource created from a Go template, parsed and executed with the given context. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: resource.Resource - signatures: [resources.ExecuteAsTemplate TARGETPATH CONTEXT RESOURCE] ---- - -The `resources.ExecuteAsTemplate` function returns a resource created from a Go template, parsed and executed with the given context, caching the result using the target path as its cache key. - -Hugo publishes the resource to the target path when you call its [`Publish`], [`Permalink`], or [`RelPermalink`] methods. - -Let's say you have a CSS file that you wish to populate with values from your site configuration: - -```go-html-template {file="assets/css/template.css"} -body { - background-color: {{ site.Params.style.bg_color }}; - color: {{ site.Params.style.text_color }}; -} -``` - -And your site configuration contains: - -{{< code-toggle file=hugo >}} -[params.style] -bg_color = '#fefefe' -text_color = '#222' -{{< /code-toggle >}} - -Place this in your baseof.html template: - -```go-html-template -{{ with resources.Get "css/template.css" }} - {{ with resources.ExecuteAsTemplate "css/main.css" $ . }} - - {{ end }} -{{ end }} -``` - -The example above: - -1. Captures the template as a resource -1. Executes the resource as a template, passing the current page in context -1. Publishes the resource to css/main.css - -The result is: - -```css {file="public/css/main.css"} -body { - background-color: #fefefe; - color: #222; -} -``` - -[`publish`]: /methods/resource/publish/ -[`permalink`]: /methods/resource/permalink/ -[`relpermalink`]: /methods/resource/relpermalink/ diff --git a/docs/content/en/functions/resources/Fingerprint.md b/docs/content/en/functions/resources/Fingerprint.md deleted file mode 100644 index 6757a0b6f..000000000 --- a/docs/content/en/functions/resources/Fingerprint.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: resources.Fingerprint -description: Cryptographically hashes the content of the given resource. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [fingerprint] - returnType: resource.Resource - signatures: ['resources.Fingerprint [ALGORITHM] RESOURCE'] ---- - -```go-html-template -{{ with resources.Get "js/main.js" }} - {{ with . | fingerprint "sha256" }} - - {{ end }} -{{ end }} -``` - -Hugo renders this to something like: - -```html - -``` - -Although most commonly used with CSS and JavaScript resources, you can use the `resources.Fingerprint` function with any resource type. - -The hash algorithm may be one of `md5`, `sha256` (default), `sha384`, or `sha512`. - -After cryptographically hashing the resource content: - -1. The values returned by the `.Permalink` and `.RelPermalink` methods include the hash sum -1. The resource's `.Data.Integrity` method returns a [Subresource Integrity] (SRI) value consisting of the name of the hash algorithm, one hyphen, and the base64-encoded hash sum - -[Subresource Integrity]: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity diff --git a/docs/content/en/functions/resources/FromString.md b/docs/content/en/functions/resources/FromString.md deleted file mode 100644 index 4cd04f609..000000000 --- a/docs/content/en/functions/resources/FromString.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -title: resources.FromString -description: Returns a resource created from a string. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: resource.Resource - signatures: [resources.FromString TARGETPATH STRING] ---- - -The `resources.FromString` function returns a resource created from a string, caching the result using the target path as its cache key. - -Hugo publishes the resource to the target path when you call its [`Publish`], [`Permalink`], or [`RelPermalink`] methods. - -[`publish`]: /methods/resource/publish/ -[`permalink`]: /methods/resource/permalink/ -[`relpermalink`]: /methods/resource/relpermalink/ - -Let's say you need to publish a file named "site.json" in the root of your `public` directory, containing the build date, the Hugo version used to build the site, and the date that the content was last modified. For example: - -```json -{ - "build_date": "2025-01-16T19:14:41-08:00", - "hugo_version": "0.141.0", - "last_modified": "2025-01-16T19:14:46-08:00" -} -``` - -Place this in your baseof.html template: - -```go-html-template -{{ if .IsHome }} - {{ $rfc3339 := "2006-01-02T15:04:05Z07:00" }} - {{ $m := dict - "hugo_version" hugo.Version - "build_date" (now.Format $rfc3339) - "last_modified" (site.Lastmod.Format $rfc3339) - }} - {{ $json := jsonify $m }} - {{ $r := resources.FromString "site.json" $json }} - {{ $r.Publish }} -{{ end }} -``` - -The example above: - -1. Creates a map with the relevant key-value pairs using the [`dict`] function -1. Encodes the map as a JSON string using the [`jsonify`] function -1. Creates a resource from the JSON string using the `resources.FromString` function -1. Publishes the file to the root of the `public` directory using the resource's `.Publish` method - -Combine `resources.FromString` with [`resources.ExecuteAsTemplate`] if your string contains template actions. Rewriting the example above: - -```go-html-template -{{ if .IsHome }} - {{ $string := ` - {{ $rfc3339 := "2006-01-02T15:04:05Z07:00" }} - {{ $m := dict - "hugo_version" hugo.Version - "build_date" (now.Format $rfc3339) - "last_modified" (site.Lastmod.Format $rfc3339) - }} - {{ $json := jsonify $m }} - ` - }} - {{ $r := resources.FromString "" $string }} - {{ $r = $r | resources.ExecuteAsTemplate "site.json" . }} - {{ $r.Publish }} -{{ end }} -``` - -[`dict`]: /functions/collections/dictionary/ -[`jsonify`]: /functions/encoding/jsonify/ -[`resources.ExecuteAsTemplate`]: /functions/resources/executeastemplate/ diff --git a/docs/content/en/functions/resources/Get.md b/docs/content/en/functions/resources/Get.md deleted file mode 100644 index db91f0a9a..000000000 --- a/docs/content/en/functions/resources/Get.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: resources.Get -description: Returns a global resource from the given path, or nil if none found. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: resource.Resource - signatures: [resources.Get PATH] ---- - -```go-html-template -{{ with resources.Get "images/a.jpg" }} - -{{ end }} -``` - -> [!note] -> This function operates on global resources. A global resource is a file within the `assets` directory, or within any directory mounted to the `assets` directory. -> -> For page resources, use the [`Resources.Get`] method on a `Page` object. - -[`Resources.Get`]: /methods/page/resources/ diff --git a/docs/content/en/functions/resources/GetMatch.md b/docs/content/en/functions/resources/GetMatch.md deleted file mode 100644 index 8f1b004fe..000000000 --- a/docs/content/en/functions/resources/GetMatch.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: resources.GetMatch -description: Returns the first global resource from paths matching the given glob pattern, or nil if none found. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: resource.Resource - signatures: [resources.GetMatch PATTERN] ---- - -```go-html-template -{{ with resources.GetMatch "images/*.jpg" }} - -{{ end }} -``` - -> [!note] -> This function operates on global resources. A global resource is a file within the `assets` directory, or within any directory mounted to the `assets` directory. -> -> For page resources, use the [`Resources.GetMatch`] method on a `Page` object. - -Hugo determines a match using a case-insensitive [glob](g) pattern. - -{{% include "/_common/glob-patterns.md" %}} - -[`Resources.GetMatch`]: /methods/page/resources/ diff --git a/docs/content/en/functions/resources/GetRemote.md b/docs/content/en/functions/resources/GetRemote.md deleted file mode 100644 index c6f6742b3..000000000 --- a/docs/content/en/functions/resources/GetRemote.md +++ /dev/null @@ -1,236 +0,0 @@ ---- -title: resources.GetRemote -description: Returns a remote resource from the given URL, or nil if none found. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: resource.Resource - signatures: ['resources.GetRemote URL [OPTIONS]'] ---- - -{{< new-in 0.141.0 >}} -The `Err` method on the returned resource was removed in v0.141.0. - -Use the [`try`] statement instead, as shown in the [error handling] example below. - -[`try`]: /functions/go-template/try -[error handling]: #error-handling -{{< /new-in >}} - -```go-html-template -{{ $url := "https://example.org/images/a.jpg" }} -{{ with try (resources.GetRemote $url) }} - {{ with .Err }} - {{ errorf "%s" . }} - {{ else with .Value }} - - {{ else }} - {{ errorf "Unable to get remote resource %q" $url }} - {{ end }} -{{ end }} -``` - -## Options - -The `resources.GetRemote` function takes an optional map of options. - -###### body - -(`string`) The data you want to transmit to the server. - -###### headers - -(`map[string][]string`) The collection of key-value pairs that provide additional information about the request. - -###### key - -(`string`) The cache key. Hugo derives the default value from the URL and options map. See [caching](#caching). - -###### method - -(`string`) The action to perform on the requested resource, typically one of `GET`, `POST`, or `HEAD`. - -###### responseHeaders -{{< new-in 0.143.0 />}} - -(`[]string`) The headers to extract from the server's response, accessible through the resource's [`Data.Headers`] method. Header name matching is case-insensitive. - -[`Data.Headers`]: /methods/resource/data/#headers - -## Options examples - -> [!note] -> For brevity, the examples below do not include [error handling]. - -[error handling]: #error-handling - -To include a header: - -```go-html-template -{{ $url := "https://example.org/api" }} -{{ $opts := dict - "headers" (dict "Authorization" "Bearer abcd") -}} -{{ $resource := resources.GetRemote $url $opts }} -``` - -To specify more than one value for the same header key, use a slice: - -```go-html-template -{{ $url := "https://example.org/api" }} -{{ $opts := dict - "headers" (dict "X-List" (slice "a" "b" "c")) -}} -{{ $resource := resources.GetRemote $url $opts }} -``` - -To post data: - -```go-html-template -{{ $url := "https://example.org/api" }} -{{ $opts := dict - "method" "post" - "body" `{"complete": true}` - "headers" (dict "Content-Type" "application/json") -}} -{{ $resource := resources.GetRemote $url $opts }} -``` - -To override the default cache key: - -```go-html-template -{{ $url := "https://example.org/images/a.jpg" }} -{{ $opts := dict - "key" (print $url (now.Format "2006-01-02")) -}} -{{ $resource := resources.GetRemote $url $opts }} -``` - -To extract specific headers from the server's response: - -```go-html-template -{{ $url := "https://example.org/images/a.jpg" }} -{{ $opts := dict - "method" "HEAD" - "responseHeaders" (slice "X-Frame-Options" "Server") -}} -{{ $resource := resources.GetRemote $url $opts }} -``` - -## Remote data - -When retrieving remote data, use the [`transform.Unmarshal`] function to [unmarshal](g) the response. - -[`transform.Unmarshal`]: /functions/transform/unmarshal/ - -```go-html-template -{{ $data := dict }} -{{ $url := "https://example.org/books.json" }} -{{ with try (resources.GetRemote $url) }} - {{ with .Err }} - {{ errorf "%s" . }} - {{ else with .Value }} - {{ $data = . | transform.Unmarshal }} - {{ else }} - {{ errorf "Unable to get remote resource %q" $url }} - {{ end }} -{{ end }} -``` - -> [!note] -> When retrieving remote data, a misconfigured server may send a response header with an incorrect [Content-Type]. For example, the server may set the Content-Type header to `application/octet-stream` instead of `application/json`. -> -> In these cases, pass the resource `Content` through the `transform.Unmarshal` function instead of passing the resource itself. For example, in the above, do this instead: -> -> `{{ $data = .Content | transform.Unmarshal }}` - -## Error handling - -Use the [`try`] statement to capture HTTP request errors. If you do not handle the error yourself, Hugo will fail the build. - -[`try`]: /functions/go-template/try - -> [!note] -> Hugo does not classify an HTTP response with status code 404 as an error. In this case `resources.GetRemote` returns nil. - -```go-html-template -{{ $url := "https://broken-example.org/images/a.jpg" }} -{{ with try (resources.GetRemote $url) }} - {{ with .Err }} - {{ errorf "%s" . }} - {{ else with .Value }} - - {{ else }} - {{ errorf "Unable to get remote resource %q" $url }} - {{ end }} -{{ end }} -``` - -To log an error as a warning instead of an error: - -```go-html-template -{{ $url := "https://broken-example.org/images/a.jpg" }} -{{ with try (resources.GetRemote $url) }} - {{ with .Err }} - {{ warnf "%s" . }} - {{ else with .Value }} - - {{ else }} - {{ warnf "Unable to get remote resource %q" $url }} - {{ end }} -{{ end }} -``` - -## HTTP response - -The [`Data`] method on a resource returned by the `resources.GetRemote` function returns information from the HTTP response. - -[`Data`]: /methods/resource/data/ - -## Caching - -Resources returned from `resources.GetRemote` are cached to disk. See [configure file caches] for details. - -By default, Hugo derives the cache key from the arguments passed to the function. Override the cache key by setting a `key` in the options map. Use this approach to have more control over how often Hugo fetches a remote resource. - -```go-html-template -{{ $url := "https://example.org/images/a.jpg" }} -{{ $cacheKey := print $url (now.Format "2006-01-02") }} -{{ $opts := dict "key" $cacheKey }} -{{ $resource := resources.GetRemote $url $opts }} -``` - -[configure file caches]: /configuration/caches/ - -## Security - -To protect against malicious intent, the `resources.GetRemote` function inspects the server response including: - -- The [Content-Type] in the response header -- The file extension, if any -- The content itself - -If Hugo is unable to resolve the media type to an entry in its [allowlist], the function throws an error: - -```text -ERROR error calling resources.GetRemote: failed to resolve media type... -``` - -For example, you will see the error above if you attempt to download an executable. - -Although the allowlist contains entries for common media types, you may encounter situations where Hugo is unable to resolve the media type of a file that you know to be safe. In these situations, edit your site configuration to add the media type to the allowlist. For example: - -{{< code-toggle file=hugo >}} -[security.http] -mediaTypes = ['^image/avif$','^application/vnd\.api\+json$'] -{{< /code-toggle >}} - -Note that the entry above is: - -- An _addition_ to the allowlist; it does not _replace_ the allowlist -- An array of [regular expressions](g) - -[allowlist]: https://en.wikipedia.org/wiki/Whitelist -[Content-Type]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type diff --git a/docs/content/en/functions/resources/Match.md b/docs/content/en/functions/resources/Match.md deleted file mode 100644 index 6c7d83649..000000000 --- a/docs/content/en/functions/resources/Match.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: resources.Match -description: Returns a collection of global resources from paths matching the given glob pattern, or nil if none found. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: resource.Resources - signatures: [resources.Match PATTERN] ---- - -```go-html-template -{{ range resources.Match "images/*.jpg" }} - -{{ end }} -``` - -> [!note] -> This function operates on global resources. A global resource is a file within the `assets` directory, or within any directory mounted to the `assets` directory. -> -> For page resources, use the [`Resources.Match`] method on a `Page` object. - -Hugo determines a match using a case-insensitive [glob pattern]. - -{{% include "/_common/glob-patterns.md" %}} - -[`Resources.Match`]: /methods/page/resources/ -[glob pattern]: https://github.com/gobwas/glob#example diff --git a/docs/content/en/functions/resources/Minify.md b/docs/content/en/functions/resources/Minify.md deleted file mode 100644 index 183e6671a..000000000 --- a/docs/content/en/functions/resources/Minify.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: resources.Minify -description: Minifies the given resource. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [minify] - returnType: resource.Resource - signatures: [resources.Minify RESOURCE] ---- - -```go-html-template -{{ $css := resources.Get "css/main.css" }} -{{ $style := $css | minify }} -``` - -Any CSS, JS, JSON, HTML, SVG, or XML resource can be minified using resources.Minify which takes for argument the resource object. diff --git a/docs/content/en/functions/resources/PostCSS.md b/docs/content/en/functions/resources/PostCSS.md deleted file mode 100644 index 3ec0b84cf..000000000 --- a/docs/content/en/functions/resources/PostCSS.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: resources.PostCSS -description: Processes the given resource with PostCSS using any PostCSS plugin. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: resource.Resource - signatures: ['resources.PostCSS [OPTIONS] RESOURCE'] -expiryDate: 2026-06-24 # deprecated 2024-06-24 in v0.128.0 ---- - -{{< deprecated-in 0.128.0 >}} -Use [`css.PostCSS`] instead. - -[`css.PostCSS`]: /functions/css/postcss/ -{{< /deprecated-in >}} diff --git a/docs/content/en/functions/resources/PostProcess.md b/docs/content/en/functions/resources/PostProcess.md deleted file mode 100644 index d70437694..000000000 --- a/docs/content/en/functions/resources/PostProcess.md +++ /dev/null @@ -1,148 +0,0 @@ ---- -title: resources.PostProcess -description: Processes the given resource after the build. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: postpub.PostPublishedResource - signatures: [resources.PostProcess RESOURCE] ---- - -The `resources.PostProcess` function delays resource transformation steps until the build is complete, primarily for tasks like removing unused CSS rules. - -## Example - -In this example, after the build is complete, Hugo will: - -1. Purge unused CSS using the [PurgeCSS] plugin for [PostCSS] -2. Add vendor prefixes to CSS rules using the [Autoprefixer] plugin for PostCSS -3. [Minify] the CSS -4. [Fingerprint] the CSS - -Step 1 -: Install [Node.js]. - -Step 2 -: Install the required Node.js packages in the root of your project: - -```sh -npm i -D postcss postcss-cli autoprefixer @fullhuman/postcss-purgecss -``` - -Step 3 -: Enable creation of the `hugo_stats.json` file when building the site. If you are only using this for the production build, consider placing it below [`config/production`]. - -{{< code-toggle file=hugo >}} -[build.buildStats] -enable = true -{{< /code-toggle >}} - -See the [configure build] documentation for details and options. - -Step 4 -: Create a PostCSS configuration file in the root of your project. - -```js {file="postcss.config.js" copy=true} -const autoprefixer = require('autoprefixer'); -const purgeCSSPlugin = require('@fullhuman/postcss-purgecss').default; - -const purgecss = purgeCSSPlugin({ - content: ['./hugo_stats.json'], - defaultExtractor: content => { - const els = JSON.parse(content).htmlElements; - return [ - ...(els.tags || []), - ...(els.classes || []), - ...(els.ids || []), - ]; - }, - // https://purgecss.com/safelisting.html - safelist: [] -}); - -module.exports = { - plugins: [ - process.env.HUGO_ENVIRONMENT !== 'development' ? purgecss : null, - autoprefixer, - ] -}; -``` - -> [!note] -> If you are a Windows user, and the path to your project contains a space, you must place the PostCSS configuration within the package.json file. See [this example] and issue [#7333]. - -Step 5 -: Place your CSS file within the `assets/css` directory. - -Step 6 -: If the current environment is not `development`, process the resource with PostCSS: - -```go-html-template -{{ with resources.Get "css/main.css" }} - {{ if hugo.IsDevelopment }} - - {{ else }} - {{ with . | postCSS | minify | fingerprint | resources.PostProcess }} - - {{ end }} - {{ end }} -{{ end }} -``` - -## Environment variables - -Hugo passes the environment variables below to PostCSS, allowing you to do something like: - -```js -process.env.HUGO_ENVIRONMENT !== 'development' ? purgecss : null, -``` - -PWD -: The absolute path to the project working directory. - -HUGO_ENVIRONMENT -: The current Hugo environment, set with the `--environment` command line flag. -Default is `production` for `hugo` and `development` for `hugo server`. - -HUGO_PUBLISHDIR -: The absolute path to the publish directory, typically `public`. This value points to a directory on disk, even when rendering to memory with the `--renderToMemory` command line flag. - -HUGO_FILE_X -: Hugo automatically mounts the following files from your project's root directory under `assets/_jsconfig`: - -- `babel.config.js` -- `postcss.config.js` -- `tailwind.config.js` - -For each file, Hugo creates a corresponding environment variable named `HUGO_FILE_:filename:`, where `:filename:` is the uppercase version of the filename with periods replaced by underscores. This allows you to access these files within your JavaScript, for example: - -```js -let tailwindConfig = process.env.HUGO_FILE_TAILWIND_CONFIG_JS || './tailwind.config.js'; -``` - -## Limitations - -Do not use `resources.PostProcess` when running Hugo's built-in development server. The examples above specifically prevent this by verifying that the current environment is not "development". - -The `resources.PostProcess` function only works within templates that produce HTML files. - -You cannot manipulate the values returned from the resource's methods. For example, the `strings.ToUpper` function in this example will not work as expected: - -```go-html-template -{{ $css := resources.Get "css/main.css" }} -{{ $css = $css | css.PostCSS | minify | fingerprint | resources.PostProcess }} -{{ $css.RelPermalink | strings.ToUpper }} -``` - -[#7333]: https://github.com/gohugoio/hugo/issues/7333 -[`config/production`]: /configuration/introduction/#configuration-directory -[Autoprefixer]: https://github.com/postcss/autoprefixer -[configure build]: /configuration/build/ -[Fingerprint]: /functions/resources/fingerprint/ -[Minify]: /functions/resources/minify/ -[Node.js]: https://nodejs.org/en -[PostCSS]: https://postcss.org/ -[PurgeCSS]: https://github.com/FullHuman/purgecss -[this example]: https://github.com/postcss/postcss-load-config#packagejson diff --git a/docs/content/en/functions/resources/ToCSS.md b/docs/content/en/functions/resources/ToCSS.md deleted file mode 100644 index 7be1b8d45..000000000 --- a/docs/content/en/functions/resources/ToCSS.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: resources.ToCSS -description: Transpiles Sass to CSS. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: resource.Resource - signatures: ['resources.ToCSS [OPTIONS] RESOURCE'] -expiryDate: 2026-06-24 # deprecated 2024-06-24 in v0.128.0 ---- - -{{< deprecated-in 0.128.0 >}} -Use [`css.Sass`] instead. - -[`css.Sass`]: /functions/css/sass/ -{{< /deprecated-in >}} diff --git a/docs/content/en/functions/resources/_index.md b/docs/content/en/functions/resources/_index.md deleted file mode 100644 index 030cafd42..000000000 --- a/docs/content/en/functions/resources/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Resource functions -linkTitle: resources -description: Use these functions to work with resources. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/functions/safe/CSS.md b/docs/content/en/functions/safe/CSS.md deleted file mode 100644 index 12ebbf8aa..000000000 --- a/docs/content/en/functions/safe/CSS.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -title: safe.CSS -description: Declares the given string as a safe CSS string. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [safeCSS] - returnType: template.CSS - signatures: [safe.CSS INPUT] -aliases: [/functions/safecss] ---- - -## Introduction - -{{% include "/_common/functions/go-html-template-package.md" %}} - -## Usage - -Use the `safe.CSS` function to encapsulate known safe content that matches any of: - -1. The CSS3 stylesheet production, such as `p { color: purple }`. -1. The CSS3 rule production, such as `a[href=~"https:"].foo#bar`. -1. CSS3 declaration productions, such as `color: red; margin: 2px`. -1. The CSS3 value production, such as `rgba(0, 0, 255, 127)`. - -Use of this type presents a security risk: the encapsulated content should come from a trusted source, as it will be included verbatim in the template output. - -See the [Go documentation] for details. - -## Example - -Without a safe declaration: - -```go-html-template -{{ $style := "color: red;" }} -

    foo

    -``` - -Hugo renders the above to: - -```html -

    foo

    -``` - -> [!note] -> `ZgotmplZ` is a special value that indicates that unsafe content reached a CSS or URL context at runtime. - -To declare the string as safe: - -```go-html-template -{{ $style := "color: red;" }} -

    foo

    -``` - -Hugo renders the above to: - -```html -

    foo

    -``` - -[Go documentation]: https://pkg.go.dev/html/template#CSS diff --git a/docs/content/en/functions/safe/HTML.md b/docs/content/en/functions/safe/HTML.md deleted file mode 100644 index 25ffb3318..000000000 --- a/docs/content/en/functions/safe/HTML.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: safe.HTML -description: Declares the given string as a safeHTML string. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [safeHTML] - returnType: template.HTML - signatures: [safe.HTML INPUT] -aliases: [/functions/safehtml] ---- - -## Introduction - -{{% include "/_common/functions/go-html-template-package.md" %}} - -## Usage - -Use the `safe.HTML` function to encapsulate a known safe HTML document fragment. It should not be used for HTML from a third-party, or HTML with unclosed tags or comments. - -Use of this type presents a security risk: the encapsulated content should come from a trusted source, as it will be included verbatim in the template output. - -See the [Go documentation] for details. - -[Go documentation]: https://pkg.go.dev/html/template#HTML - -## Example - -Without a safe declaration: - -```go-html-template -{{ $html := "emphasized" }} -{{ $html }} -``` - -Hugo renders the above to: - -```html -<em>emphasized</em> -``` - -To declare the string as safe: - -```go-html-template -{{ $html := "emphasized" }} -{{ $html | safeHTML }} -``` - -Hugo renders the above to: - -```html -emphasized -``` diff --git a/docs/content/en/functions/safe/HTMLAttr.md b/docs/content/en/functions/safe/HTMLAttr.md deleted file mode 100644 index 7cfefdfb2..000000000 --- a/docs/content/en/functions/safe/HTMLAttr.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: safe.HTMLAttr -description: Declares the given key-value pair as a safe HTML attribute. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [safeHTMLAttr] - returnType: template.HTMLAttr - signatures: [safe.HTMLAttr INPUT] -aliases: [/functions/safehtmlattr] ---- - -## Introduction - -{{% include "/_common/functions/go-html-template-package.md" %}} - -## Usage - -Use the `safe.HTMLAttr` function to encapsulate an HTML attribute from a trusted source. - -Use of this type presents a security risk: the encapsulated content should come from a trusted source, as it will be included verbatim in the template output. - -See the [Go documentation] for details. - -[Go documentation]: https://pkg.go.dev/html/template#HTMLAttr - -## Example - -Without a safe declaration: - -```go-html-template -{{ with .Date }} - {{ $humanDate := time.Format "2 Jan 2006" . }} - {{ $machineDate := time.Format "2006-01-02T15:04:05-07:00" . }} - -{{ end }} -``` - -Hugo renders the above to: - -```html - -``` - -To declare the key-value pair as safe: - -```go-html-template -{{ with .Date }} - {{ $humanDate := time.Format "2 Jan 2006" . }} - {{ $machineDate := time.Format "2006-01-02T15:04:05-07:00" . }} - -{{ end }} -``` - -Hugo renders the above to: - -```html - -``` diff --git a/docs/content/en/functions/safe/JS.md b/docs/content/en/functions/safe/JS.md deleted file mode 100644 index 0c4d9009d..000000000 --- a/docs/content/en/functions/safe/JS.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: safe.JS -description: Declares the given string as a safe JavaScript expression. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [safeJS] - returnType: template.JS - signatures: [safe.JS INPUT] -aliases: [/functions/safejs] ---- - -## Introduction - -{{% include "/_common/functions/go-html-template-package.md" %}} - -## Usage - -Use the `safe.JS` function to encapsulate a known safe EcmaScript5 Expression. - -Template authors are responsible for ensuring that typed expressions do not break the intended precedence and that there is no statement/expression ambiguity as when passing an expression like `{ foo: bar() }\n['foo']()`, which is both a valid Expression and a valid Program with a very different meaning. - -Use of this type presents a security risk: the encapsulated content should come from a trusted source, as it will be included verbatim in the template output. - -Using the `safe.JS` function to include valid but untrusted JSON is not safe. A safe alternative is to parse the JSON with the [`transform.Unmarshal`] function and then pass the resultant object into the template, where it will be converted to sanitized JSON when presented in a JavaScript context. - -[`transform.Unmarshal`]: /functions/transform/unmarshal/ - -See the [Go documentation] for details. - -[Go documentation]: https://pkg.go.dev/html/template#JS - -## Example - -Without a safe declaration: - -```go-html-template -{{ $js := "x + y" }} - -``` - -Hugo renders the above to: - -```html - -``` - -To declare the string as safe: - -```go-html-template -{{ $js := "x + y" }} - -``` - -Hugo renders the above to: - -```html - -``` diff --git a/docs/content/en/functions/safe/JSStr.md b/docs/content/en/functions/safe/JSStr.md deleted file mode 100644 index 81946a14c..000000000 --- a/docs/content/en/functions/safe/JSStr.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -title: safe.JSStr -description: Declares the given string as a safe JavaScript string. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [safeJSStr] - returnType: template.JSStr - signatures: [safe.JSStr INPUT] -aliases: [/functions/safejsstr] ---- - -## Introduction - -{{% include "/_common/functions/go-html-template-package.md" %}} - -## Usage - -Use the `safe.JSStr` function to encapsulate a sequence of characters meant to be embedded between quotes in a JavaScript expression. - -Use of this type presents a security risk: the encapsulated content should come from a trusted source, as it will be included verbatim in the template output. - -See the [Go documentation] for details. - -[Go documentation]: https://pkg.go.dev/html/template#JSStr - -## Example - -Without a safe declaration: - -```go-html-template -{{ $title := "Lilo & Stitch" }} - -``` - -Hugo renders the above to: - -```html - -``` - -To declare the string as safe: - -```go-html-template -{{ $title := "Lilo & Stitch" }} - -``` - -Hugo renders the above to: - -```html - -``` diff --git a/docs/content/en/functions/safe/URL.md b/docs/content/en/functions/safe/URL.md deleted file mode 100644 index 44bed8064..000000000 --- a/docs/content/en/functions/safe/URL.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -title: safe.URL -description: Declares the given string as a safe URL or URL substring. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [safeURL] - returnType: template.URL - signatures: [safe.URL INPUT] -aliases: [/functions/safeurl] ---- - -## Introduction - -{{% include "/_common/functions/go-html-template-package.md" %}} - -## Usage - -Use the `safe.URL` function to encapsulate a known safe URL or URL substring. Schemes other than the following are considered unsafe: - -- `http:` -- `https:` -- `mailto:` - -Use of this type presents a security risk: the encapsulated content should come from a trusted source, as it will be included verbatim in the template output. - -See the [Go documentation] for details. - -## Example - -Without a safe declaration: - -```go-html-template -{{ $href := "irc://irc.freenode.net/#golang" }} -IRC -``` - -Hugo renders the above to: - -```html -IRC -``` - -> [!note] -> `ZgotmplZ` is a special value that indicates that unsafe content reached a CSS or URL context at runtime. - -To declare the string as safe: - -```go-html-template -{{ $href := "irc://irc.freenode.net/#golang" }} -IRC -``` - -Hugo renders the above to: - -```html -IRC -``` - -[Go documentation]: https://pkg.go.dev/html/template#URL diff --git a/docs/content/en/functions/safe/_index.md b/docs/content/en/functions/safe/_index.md deleted file mode 100644 index 8d5697b8d..000000000 --- a/docs/content/en/functions/safe/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Safe functions -linkTitle: safe -description: Use these functions to declare a value as safe in the context of Go's html/template package. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/functions/strings/Chomp.md b/docs/content/en/functions/strings/Chomp.md deleted file mode 100644 index 1ff2b7f47..000000000 --- a/docs/content/en/functions/strings/Chomp.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: strings.Chomp -description: Returns the given string, removing all trailing newline characters and carriage returns. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [chomp] - returnType: any - signatures: [strings.Chomp STRING] -aliases: [/functions/chomp] ---- - -If the argument is of type `template.HTML`, returns `template.HTML`, else returns a `string`. - -```go-html-template -{{ chomp "foo\n" }} → foo -{{ chomp "foo\n\n" }} → foo - -{{ chomp "foo\r\n" }} → foo -{{ chomp "foo\r\n\r\n" }} → foo -``` diff --git a/docs/content/en/functions/strings/Contains.md b/docs/content/en/functions/strings/Contains.md deleted file mode 100644 index e0e3b087c..000000000 --- a/docs/content/en/functions/strings/Contains.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: strings.Contains -description: Reports whether the given string contains the given substring. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: bool - signatures: [strings.Contains STRING SUBSTRING] -aliases: [/functions/strings.contains] ---- - -```go-html-template -{{ strings.Contains "Hugo" "go" }} → true -``` - -The check is case sensitive: - -```go-html-template -{{ strings.Contains "Hugo" "Go" }} → false -``` diff --git a/docs/content/en/functions/strings/ContainsAny.md b/docs/content/en/functions/strings/ContainsAny.md deleted file mode 100644 index 521ff3421..000000000 --- a/docs/content/en/functions/strings/ContainsAny.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: strings.ContainsAny -description: Reports whether the given string contains any character within the given set. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: bool - signatures: [strings.ContainsAny STRING SET] -aliases: [/functions/strings.containsany] ---- - -```go-html-template -{{ strings.ContainsAny "Hugo" "gm" }} → true -``` - -The check is case sensitive: - -```go-html-template -{{ strings.ContainsAny "Hugo" "Gm" }} → false -``` diff --git a/docs/content/en/functions/strings/ContainsNonSpace.md b/docs/content/en/functions/strings/ContainsNonSpace.md deleted file mode 100644 index 7b8dcb730..000000000 --- a/docs/content/en/functions/strings/ContainsNonSpace.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: strings.ContainsNonSpace -description: Reports whether the given string contains any non-space characters as defined by Unicode. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: bool - signatures: [strings.ContainsNonSpace STRING] -aliases: [/functions/strings.containsnonspace] ---- - -Whitespace characters include `\t`, `\n`, `\v`, `\f`, `\r`, and characters in the [Unicode Space Separator] category. - -[Unicode Space Separator]: https://www.compart.com/en/unicode/category/Zs - -```go-html-template -{{ strings.ContainsNonSpace "\n" }} → false -{{ strings.ContainsNonSpace " " }} → false -{{ strings.ContainsNonSpace "\n abc" }} → true -``` diff --git a/docs/content/en/functions/strings/Count.md b/docs/content/en/functions/strings/Count.md deleted file mode 100644 index 76378b27d..000000000 --- a/docs/content/en/functions/strings/Count.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -title: strings.Count -description: Returns the number of non-overlapping instances of the given substring within the given string. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: int - signatures: [strings.Count SUBSTR STRING] -aliases: [/functions/strings.count] ---- - -If `SUBSTR` is an empty string, this function returns 1 plus the number of Unicode code points in `STRING`. - -```go-html-template -{{ "aaabaab" | strings.Count "a" }} → 5 -{{ "aaabaab" | strings.Count "aa" }} → 2 -{{ "aaabaab" | strings.Count "aaa" }} → 1 -{{ "aaabaab" | strings.Count "" }} → 8 -``` diff --git a/docs/content/en/functions/strings/CountRunes.md b/docs/content/en/functions/strings/CountRunes.md deleted file mode 100644 index 8ad6b00da..000000000 --- a/docs/content/en/functions/strings/CountRunes.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -title: strings.CountRunes -description: Returns the number of runes in the given string excluding whitespace. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [countrunes] - returnType: int - signatures: [strings.CountRunes INPUT] -aliases: [/functions/countrunes] ---- - -In contrast with the [`strings.RuneCount`] function, which counts every rune in a string, `strings.CountRunes` excludes whitespace. - -```go-html-template -{{ "Hello, 世界" | strings.CountRunes }} → 8 -``` - -[`strings.RuneCount`]: /functions/strings/runecount/ diff --git a/docs/content/en/functions/strings/CountWords.md b/docs/content/en/functions/strings/CountWords.md deleted file mode 100644 index e50febf69..000000000 --- a/docs/content/en/functions/strings/CountWords.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: strings.CountWords -description: Returns the number of words in the given string. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [countwords] - returnType: int - signatures: [strings.CountWords INPUT] -aliases: [/functions/countwords] ---- - -```go-html-template -{{ "Hugo is a static site generator." | countwords }} → 6 -``` diff --git a/docs/content/en/functions/strings/Diff/diff-screen-capture.png b/docs/content/en/functions/strings/Diff/diff-screen-capture.png deleted file mode 100644 index 62baa4563..000000000 Binary files a/docs/content/en/functions/strings/Diff/diff-screen-capture.png and /dev/null differ diff --git a/docs/content/en/functions/strings/Diff/index.md b/docs/content/en/functions/strings/Diff/index.md deleted file mode 100644 index 1426764a9..000000000 --- a/docs/content/en/functions/strings/Diff/index.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: strings.Diff -description: Returns an anchored diff of the two texts OLD and NEW in the unified diff format. If OLD and NEW are identical, returns an empty string. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [strings.Diff OLDNAME OLD NEWNAME NEW] ---- - -{{< new-in 0.125.0 />}} - -Use `strings.Diff` to compare two strings and render a highlighted diff: - -```go-html-template -{{ $want := ` -

    The product of 6 and 7 is 42.

    -

    The product of 7 and 6 is 42.

    -`}} - -{{ $got := ` -

    The product of 6 and 7 is 42.

    -

    The product of 7 and 6 is 13.

    -`}} - -{{ $diff := strings.Diff "want" $want "got" $got }} -{{ transform.Highlight $diff "diff" }} -``` - -Rendered: - -![screen capture](diff-screen-capture.png) diff --git a/docs/content/en/functions/strings/FindRESubmatch.md b/docs/content/en/functions/strings/FindRESubmatch.md deleted file mode 100644 index d039607fb..000000000 --- a/docs/content/en/functions/strings/FindRESubmatch.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -title: strings.FindRESubmatch -description: Returns a slice of all successive matches of the regular expression. Each element is a slice of strings holding the text of the leftmost match of the regular expression and the matches, if any, of its subexpressions. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [findRESubmatch] - returnType: '[][]string' - signatures: ['strings.FindRESubmatch PATTERN INPUT [LIMIT]'] -aliases: [/functions/findresubmatch] ---- - -By default, `findRESubmatch` finds all matches. You can limit the number of matches with an optional LIMIT argument. A return value of nil indicates no match. - -{{% include "/_common/functions/regular-expressions.md" %}} - -## Demonstrative examples - -```go-html-template -{{ findRESubmatch `a(x*)b` "-ab-" }} → [["ab" ""]] -{{ findRESubmatch `a(x*)b` "-axxb-" }} → [["axxb" "xx"]] -{{ findRESubmatch `a(x*)b` "-ab-axb-" }} → [["ab" ""] ["axb" "x"]] -{{ findRESubmatch `a(x*)b` "-axxb-ab-" }} → [["axxb" "xx"] ["ab" ""]] -{{ findRESubmatch `a(x*)b` "-axxb-ab-" 1 }} → [["axxb" "xx"]] -``` - -## Practical example - -This Markdown: - -```text -- [Example](https://example.org) -- [Hugo](https://gohugo.io) -``` - -Produces this HTML: - -```html - -``` - -To match the anchor elements, capturing the link destination and text: - -```go-html-template -{{ $regex := `(.+?)` }} -{{ $matches := findRESubmatch $regex .Content }} -``` - -Viewed as JSON, the data structure of `$matches` in the code above is: - -```json -[ - [ - "Example", - "https://example.org", - "Example" - ], - [ - "Hugo", - "https://gohugo.io", - "Hugo" - ] -] -``` - -To render the `href` attributes: - -```go-html-template -{{ range $matches }} - {{ index . 1 }} -{{ end }} -``` - -Result: - -```text -https://example.org -https://gohugo.io -``` - -> [!note] -> You can write and test your regular expression using [regex101.com](https://regex101.com/). Be sure to select the Go flavor before you begin. diff --git a/docs/content/en/functions/strings/FindRe.md b/docs/content/en/functions/strings/FindRe.md deleted file mode 100644 index 45129ec91..000000000 --- a/docs/content/en/functions/strings/FindRe.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: strings.FindRE -description: Returns a slice of strings that match the regular expression. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [findRE] - returnType: '[]string' - signatures: ['strings.FindRE PATTERN INPUT [LIMIT]'] -aliases: [/functions/findre] ---- -By default, `findRE` finds all matches. You can limit the number of matches with an optional LIMIT argument. - -{{% include "/_common/functions/regular-expressions.md" %}} - -This example returns a slice of all second level headings (`h2` elements) within the rendered `.Content`: - -```go-html-template -{{ findRE `(?s).*?` .Content }} -``` - -The `s` flag causes `.` to match `\n` as well, allowing us to find an `h2` element that contains newlines. - -To limit the number of matches to one: - -```go-html-template -{{ findRE `(?s).*?` .Content 1 }} -``` - -> [!note] -> You can write and test your regular expression using [regex101.com](https://regex101.com/). Be sure to select the Go flavor before you begin. diff --git a/docs/content/en/functions/strings/FirstUpper.md b/docs/content/en/functions/strings/FirstUpper.md deleted file mode 100644 index 41bf1f70a..000000000 --- a/docs/content/en/functions/strings/FirstUpper.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: strings.FirstUpper -description: Returns the given string, capitalizing the first character. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: string - signatures: [strings.FirstUpper STRING] -aliases: [/functions/strings.firstupper] ---- - -```go-html-template -{{ strings.FirstUpper "foo" }} → Foo -``` diff --git a/docs/content/en/functions/strings/HasPrefix.md b/docs/content/en/functions/strings/HasPrefix.md deleted file mode 100644 index 2babe8552..000000000 --- a/docs/content/en/functions/strings/HasPrefix.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: strings.HasPrefix -description: Reports whether the given string begins with the given prefix. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [hasPrefix] - returnType: bool - signatures: [strings.HasPrefix STRING PREFIX] -aliases: [/functions/hasprefix,/functions/strings.hasprefix] ---- - -```go-html-template -{{ hasPrefix "Hugo" "Hu" }} → true -``` diff --git a/docs/content/en/functions/strings/HasSuffix.md b/docs/content/en/functions/strings/HasSuffix.md deleted file mode 100644 index c6b5f4ded..000000000 --- a/docs/content/en/functions/strings/HasSuffix.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: strings.HasSuffix -description: Reports whether the given string ends with the given suffix. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [hasSuffix] - returnType: bool - signatures: [strings.HasSuffix STRING SUFFIX] -aliases: [/functions/hassuffix,/functions/strings/hassuffix] ---- - -```go-html-template -{{ hasSuffix "Hugo" "go" }} → true -``` diff --git a/docs/content/en/functions/strings/Repeat.md b/docs/content/en/functions/strings/Repeat.md deleted file mode 100644 index b6027368e..000000000 --- a/docs/content/en/functions/strings/Repeat.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: strings.Repeat -description: Returns a new string consisting of zero or more copies of another string. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: string - signatures: [strings.Repeat COUNT INPUT] -aliases: [/functions/strings.repeat] ---- - -```go-html-template -{{ strings.Repeat 3 "yo" }} → yoyoyo -``` diff --git a/docs/content/en/functions/strings/Replace.md b/docs/content/en/functions/strings/Replace.md deleted file mode 100644 index b449ea84d..000000000 --- a/docs/content/en/functions/strings/Replace.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: strings.Replace -description: Returns a copy of INPUT, replacing all occurrences of OLD with NEW. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [replace] - returnType: string - signatures: ['strings.Replace INPUT OLD NEW [LIMIT]'] -aliases: [/functions/replace] ---- - -```go-html-template -{{ $s := "Batman and Robin" }} -{{ replace $s "Robin" "Catwoman" }} → Batman and Catwoman -``` - -Limit the number of replacements using the `LIMIT` argument: - -```go-html-template -{{ replace "aabbaabb" "a" "z" 2 }} → zzbbaabb -``` diff --git a/docs/content/en/functions/strings/ReplaceRE.md b/docs/content/en/functions/strings/ReplaceRE.md deleted file mode 100644 index dba4bd15a..000000000 --- a/docs/content/en/functions/strings/ReplaceRE.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: strings.ReplaceRE -description: Returns a copy of INPUT, replacing all occurrences of a regular expression with a replacement pattern. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [replaceRE] - returnType: string - signatures: ['strings.ReplaceRE PATTERN REPLACEMENT INPUT [LIMIT]'] -aliases: [/functions/replacere] ---- - -{{% include "/_common/functions/regular-expressions.md" %}} - -```go-html-template -{{ $s := "a-b--c---d" }} -{{ replaceRE `(-{2,})` "-" $s }} → a-b-c-d -``` - -Limit the number of replacements using the LIMIT argument: - -```go-html-template -{{ $s := "a-b--c---d" }} -{{ replaceRE `(-{2,})` "-" $s 1 }} → a-b-c---d -``` - -Use `$1`, `$2`, etc. within the replacement string to insert the content of each capturing group within the regular expression: - -```go-html-template -{{ $s := "http://gohugo.io/docs" }} -{{ replaceRE "^https?://([^/]+).*" "$1" $s }} → gohugo.io -``` - -> [!note] -> You can write and test your regular expression using [regex101.com]. Be sure to select the Go flavor before you begin. - -[regex101.com]: https://regex101.com/ diff --git a/docs/content/en/functions/strings/RuneCount.md b/docs/content/en/functions/strings/RuneCount.md deleted file mode 100644 index 8c0e24342..000000000 --- a/docs/content/en/functions/strings/RuneCount.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -title: strings.RuneCount -description: Returns the number of runes in the given string. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: int - signatures: [strings.RuneCount INPUT] -aliases: [/functions/strings.runecount] ---- - -In contrast with the [`strings.CountRunes`] function, which excludes whitespace, `strings.RuneCount` counts every rune in a string. - -```go-html-template -{{ "Hello, 世界" | strings.RuneCount }} → 9 -``` - -[`strings.CountRunes`]: /functions/strings/countrunes/ diff --git a/docs/content/en/functions/strings/SliceString.md b/docs/content/en/functions/strings/SliceString.md deleted file mode 100644 index 69e4f6f33..000000000 --- a/docs/content/en/functions/strings/SliceString.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: strings.SliceString -description: Returns a substring of the given string, beginning with the start position and ending before the end position. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [slicestr] - returnType: string - signatures: ['strings.SliceString STRING [START] [END]'] -aliases: [/functions/slicestr] ---- - -The START and END positions are zero-based, where `0` represents the first character of the string. If START is not specified, the substring will begin at position `0`. If END is not specified, the substring will end after the last character. - -```go-html-template -{{ slicestr "BatMan" }} → BatMan -{{ slicestr "BatMan" 3 }} → Man -{{ slicestr "BatMan" 0 3 }} → Bat -``` - -The START and END arguments represent the endpoints of a half-open [interval](g), a concept that may be difficult to grasp when first encountered. You may find that the [`strings.Substr`] function is easier to understand. - -[`strings.Substr`]: /functions/strings/substr/ diff --git a/docs/content/en/functions/strings/Split.md b/docs/content/en/functions/strings/Split.md deleted file mode 100644 index bcab1b4d7..000000000 --- a/docs/content/en/functions/strings/Split.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: strings.Split -description: Returns a slice of strings by splitting the given string by a delimiter. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [split] - returnType: '[]string' - signatures: [strings.Split STRING DELIM] -aliases: [/functions/split] ---- - -Examples: - -```go-html-template -{{ split "tag1,tag2,tag3" "," }} → ["tag1", "tag2", "tag3"] -{{ split "abc" "" }} → ["a", "b", "c"] -``` - -> [!note] -> The `strings.Split` function essentially does the opposite of the [`collections.Delimit`] function. While `split` creates a slice from a string, `delimit` creates a string from a slice. - -[`collections.Delimit`]: /functions/collections/delimit/ diff --git a/docs/content/en/functions/strings/Substr.md b/docs/content/en/functions/strings/Substr.md deleted file mode 100644 index a4c779f7d..000000000 --- a/docs/content/en/functions/strings/Substr.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: strings.Substr -description: Returns a substring of the given string, beginning with the start position and ending after the given length. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [substr] - returnType: string - signatures: ['strings.Substr STRING [START] [LENGTH]'] -aliases: [/functions/substr] ---- - -The start position is zero-based, where `0` represents the first character of the string. If START is not specified, the substring will begin at position `0`. Specify a negative START position to extract characters from the end of the string. - -If LENGTH is not specified, the substring will include all characters from the START position to the end of the string. If negative, that number of characters will be omitted from the end of string. - -```go-html-template -{{ substr "abcdef" 0 }} → abcdef -{{ substr "abcdef" 1 }} → bcdef - -{{ substr "abcdef" 0 1 }} → a -{{ substr "abcdef" 1 1 }} → b - -{{ substr "abcdef" 0 -1 }} → abcde -{{ substr "abcdef" 1 -1 }} → bcde - -{{ substr "abcdef" -1 }} → f -{{ substr "abcdef" -2 }} → ef - -{{ substr "abcdef" -1 1 }} → f -{{ substr "abcdef" -2 1 }} → e - -{{ substr "abcdef" -3 -1 }} → de -{{ substr "abcdef" -3 -2 }} → d -``` diff --git a/docs/content/en/functions/strings/Title.md b/docs/content/en/functions/strings/Title.md deleted file mode 100644 index d1b4aca01..000000000 --- a/docs/content/en/functions/strings/Title.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: strings.Title -description: Returns the given string, converting it to title case. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [title] - returnType: string - signatures: [strings.Title STRING] -aliases: [/functions/title] ---- - -```go-html-template -{{ title "table of contents (TOC)" }} → Table of Contents (TOC) -``` - -By default, Hugo follows the capitalization rules published in the [Associated Press Stylebook]. Change your [site configuration] if you would prefer to: - -- Follow the capitalization rules published in the [Chicago Manual of Style] -- Capitalize the first letter of every word -- Capitalize the first letter of the first word -- Disable the effects of the `title` function - -The last option is useful if your theme uses the `title` function, and you would prefer to manually capitalize strings as needed. - -[Associated Press Stylebook]: https://www.apstylebook.com/ -[Chicago Manual of Style]: https://www.chicagomanualofstyle.org/home.html -[site configuration]: /configuration/all/#title-case-style diff --git a/docs/content/en/functions/strings/ToLower.md b/docs/content/en/functions/strings/ToLower.md deleted file mode 100644 index c329b5e55..000000000 --- a/docs/content/en/functions/strings/ToLower.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: strings.ToLower -description: Returns the given string, converting all characters to lowercase. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [lower] - returnType: string - signatures: [strings.ToLower INPUT] -aliases: [/functions/lower] ---- - -```go-html-template -{{ lower "BatMan" }} → batman -``` diff --git a/docs/content/en/functions/strings/ToUpper.md b/docs/content/en/functions/strings/ToUpper.md deleted file mode 100644 index acccb4124..000000000 --- a/docs/content/en/functions/strings/ToUpper.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: strings.ToUpper -description: Returns the given string, converting all characters to uppercase. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [upper] - returnType: string - signatures: [strings.ToUpper INPUT] -aliases: [/functions/upper] ---- - -```go-html-template -{{ upper "BatMan" }} → BATMAN -``` diff --git a/docs/content/en/functions/strings/Trim.md b/docs/content/en/functions/strings/Trim.md deleted file mode 100644 index 1afa0627f..000000000 --- a/docs/content/en/functions/strings/Trim.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: strings.Trim -description: Returns the given string, removing leading and trailing characters specified in the cutset. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [trim] - returnType: string - signatures: [strings.Trim INPUT CUTSET] -aliases: [/functions/trim] ---- - -```go-html-template -{{ trim "++foo--" "+-" }} → foo -``` diff --git a/docs/content/en/functions/strings/TrimLeft.md b/docs/content/en/functions/strings/TrimLeft.md deleted file mode 100644 index c5d6ba60f..000000000 --- a/docs/content/en/functions/strings/TrimLeft.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: strings.TrimLeft -description: Returns the given string, removing leading characters specified in the cutset. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: string - signatures: [strings.TrimLeft CUTSET STRING] -aliases: [/functions/strings.trimleft] ---- - -```go-html-template -{{ strings.TrimLeft "a" "abba" }} → bba -``` - -The `strings.TrimLeft` function converts the arguments to strings if possible: - -```go-html-template -{{ strings.TrimLeft 21 12345 }} → 345 (string) -{{ strings.TrimLeft "rt" true }} → ue -``` diff --git a/docs/content/en/functions/strings/TrimPrefix.md b/docs/content/en/functions/strings/TrimPrefix.md deleted file mode 100644 index b897d8777..000000000 --- a/docs/content/en/functions/strings/TrimPrefix.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: strings.TrimPrefix -description: Returns the given string, removing the prefix from the beginning of the string. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: string - signatures: [strings.TrimPrefix PREFIX STRING] -aliases: [/functions/strings.trimprefix] ---- - -```go-html-template -{{ strings.TrimPrefix "a" "aabbaa" }} → abbaa -{{ strings.TrimPrefix "aa" "aabbaa" }} → bbaa -{{ strings.TrimPrefix "aaa" "aabbaa" }} → aabbaa -``` diff --git a/docs/content/en/functions/strings/TrimRight.md b/docs/content/en/functions/strings/TrimRight.md deleted file mode 100644 index 05c2ed324..000000000 --- a/docs/content/en/functions/strings/TrimRight.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: strings.TrimRight -description: Returns the given string, removing trailing characters specified in the cutset. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: string - signatures: [strings.TrimRight CUTSET STRING] -aliases: [/functions/strings.trimright] ---- - -```go-html-template -{{ strings.TrimRight "a" "abba" }} → abb -``` - -The `strings.TrimRight` function converts the arguments to strings if possible: - -```go-html-template -{{ strings.TrimRight 54 12345 }} → 123 (string) -{{ strings.TrimRight "eu" true }} → tr -``` diff --git a/docs/content/en/functions/strings/TrimSpace.md b/docs/content/en/functions/strings/TrimSpace.md deleted file mode 100644 index cb38b5ab1..000000000 --- a/docs/content/en/functions/strings/TrimSpace.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -title: strings.TrimSpace -description: Returns the given string, removing leading and trailing whitespace as defined by Unicode. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [strings.TrimSpace INPUT] ---- - -{{< new-in 0.136.3 />}} - -Whitespace characters include `\t`, `\n`, `\v`, `\f`, `\r`, and characters in the [Unicode Space Separator] category. - -[Unicode Space Separator]: https://www.compart.com/en/unicode/category/Zs - -```go-html-template -{{ strings.TrimSpace "\n\r\t foo \n\r\t" }} → foo -``` diff --git a/docs/content/en/functions/strings/TrimSuffix.md b/docs/content/en/functions/strings/TrimSuffix.md deleted file mode 100644 index 802842105..000000000 --- a/docs/content/en/functions/strings/TrimSuffix.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: strings.TrimSuffix -description: Returns the given string, removing the suffix from the end of the string. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: string - signatures: [strings.TrimSuffix SUFFIX STRING] -aliases: [/functions/strings.trimsuffix] ---- - -```go-html-template -{{ strings.TrimSuffix "a" "aabbaa" }} → aabba -{{ strings.TrimSuffix "aa" "aabbaa" }} → aabb -{{ strings.TrimSuffix "aaa" "aabbaa" }} → aabbaa -``` diff --git a/docs/content/en/functions/strings/Truncate.md b/docs/content/en/functions/strings/Truncate.md deleted file mode 100644 index c4198229e..000000000 --- a/docs/content/en/functions/strings/Truncate.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: strings.Truncate -description: Returns the given string, truncating it to a maximum length without cutting words or leaving unclosed HTML tags. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [truncate] - returnType: template.HTML - signatures: ['strings.Truncate SIZE [ELLIPSIS] INPUT'] -aliases: [/functions/truncate] ---- - -Since Go templates are HTML-aware, `truncate` will intelligently handle normal strings vs HTML strings: - -```go-html-template -{{ "Keep my HTML" | safeHTML | truncate 10 }} → Keep my … -``` - -> [!note] -> If you have a raw string that contains HTML tags you want to remain treated as HTML, you will need to convert the string to HTML using the [`safeHTML`]function before sending the value to `truncate`. Otherwise, the HTML tags will be escaped when passed through the `truncate` function. - -[`safeHTML`]: /functions/safe/html/ diff --git a/docs/content/en/functions/strings/_index.md b/docs/content/en/functions/strings/_index.md deleted file mode 100644 index 28f26f170..000000000 --- a/docs/content/en/functions/strings/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: String functions -linkTitle: strings -description: Use these functions to work with strings. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/functions/templates/Current.md b/docs/content/en/functions/templates/Current.md deleted file mode 100644 index 805aeec05..000000000 --- a/docs/content/en/functions/templates/Current.md +++ /dev/null @@ -1,155 +0,0 @@ ---- -title: templates.Current -description: Returns information about the currently executing template. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: tpl.CurrentTemplateInfo - signatures: [templates.Current] ---- - -> [!note] -> This function is experimental and subject to change. - -{{< new-in 0.146.0 />}} - -The `templates.Current` function provides introspection capabilities, allowing you to access details about the currently executing templates. This is useful for debugging complex template hierarchies and understanding the flow of execution during rendering. - -## Methods - -Ancestors -: (`tpl.CurrentTemplateInfos`) Returns a slice containing information about each template in the current execution chain, starting from the parent of the current template and going up towards the initial template called. It excludes any base template applied via `define` and `block`. You can chain the `Reverse` method to this result to get the slice in chronological execution order. - -Base -: (`tpl.CurrentTemplateInfoCommonOps`) Returns an object representing the base template that was applied to the current template, if any. This may be `nil`. - -Filename -: (`string`) Returns the absolute path of the current template. This will be empty for embedded templates. - -Name -: (`string`) Returns the name of the current template. This is usually the path relative to the layouts directory. - -Parent -: (`tpl.CurrentTemplateInfo`) Returns an object representing the parent of the current template, if any. This may be `nil`. - -## Examples - -The examples below help visualize template execution and require a `debug` parameter set to `true` in your site configuration: - -{{< code-toggle file=hugo >}} -[params] -debug = true -{{< /code-toggle >}} - -### Boundaries - -To visually mark where a template begins and ends execution: - -```go-html-template {file="layouts/_default/single.html"} -{{ define "main" }} - {{ if site.Params.debug }} -
    [entering {{ templates.Current.Filename }}]
    - {{ end }} - -

    {{ .Title }}

    - {{ .Content }} - - {{ if site.Params.debug }} -
    [leaving {{ templates.Current.Filename }}]
    - {{ end }} -{{ end }} -``` - -### Call stack - -To display the chain of templates that led to the current one, create a partial template that iterates through its ancestors: - -```go-html-template {file="layouts/partials/template-call-stack.html" copy=true} -{{ with templates.Current }} -
    - {{ range .Ancestors }} - {{ .Filename }}
    - {{ with .Base }} - {{ .Filename }}
    - {{ end }} - {{ end }} -
    -{{ end }} -``` - -Then call the partial from any template: - -```go-html-template {file="layouts/partials/footer/copyright.html" copy=true} -{{ if site.Params.debug }} - {{ partial "template-call-stack.html" . }} -{{ end }} -``` - -The rendered template stack would look something like this: - -```text -/home/user/project/layouts/partials/footer/copyright.html -/home/user/project/themes/foo/layouts/partials/footer.html -/home/user/project/layouts/_default/single.html -/home/user/project/themes/foo/layouts/_default/baseof.html -``` - -To reverse the order of the entries, chain the `Reverse` method to the `Ancestors` method: - -```go-html-template {file="layouts/partials/template-call-stack.html" copy=true} -{{ with templates.Current }} -
    - {{ range .Ancestors.Reverse }} - {{ with .Base }} - {{ .Filename }}
    - {{ end }} - {{ .Filename }}
    - {{ end }} -
    -{{ end }} -``` - -### VS Code - -To render links that, when clicked, will open the template in Microsoft Visual Studio Code, create a partial template with anchor elements that use the `vscode` URI scheme: - -```go-html-template {file="layouts/partials/template-open-in-vs-code.html" copy=true} -{{ with templates.Current.Parent }} -
    - {{ .Name }} - {{ with .Base }} - {{ .Name }} - {{ end }} -
    -{{ end }} -``` - -Then call the partial from any template: - -```go-html-template {file="layouts/_default/single.html" copy=true} -{{ define "main" }} -

    {{ .Title }}

    - {{ .Content }} - - {{ if site.Params.debug }} - {{ partial "template-open-in-vs-code.html" . }} - {{ end }} -{{ end }} -``` - -Use the same approach to render the entire call stack as links: - -```go-html-template {file="layouts/partials/template-call-stack.html" copy=true} -{{ with templates.Current }} -
    - {{ range .Ancestors }} - {{ .Filename }}
    - {{ with .Base }} - {{ .Filename }}
    - {{ end }} - {{ end }} -
    -{{ end }} -``` diff --git a/docs/content/en/functions/templates/Defer.md b/docs/content/en/functions/templates/Defer.md deleted file mode 100644 index 6a9ca56ae..000000000 --- a/docs/content/en/functions/templates/Defer.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: templates.Defer -description: Defer execution of a template until after all sites and output formats have been rendered. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: string - signatures: [templates.Defer OPTIONS] -aliases: [/functions/templates.defer] ---- - -{{< new-in 0.128.0 />}} - -> [!note] -> This feature is meant to be used in the main page layout files/templates, and has undefined behavior when used from shortcodes, partials or render hook templates. See [this issue](https://github.com/gohugoio/hugo/issues/13492#issuecomment-2734700391) for more info. - -In some rare use cases, you may need to defer the execution of a template until after all sites and output formats have been rendered. One such example could be [TailwindCSS](/functions/css/tailwindcss/) using the output of [hugo_stats.json](/configuration/build/) to determine which classes and other HTML identifiers are being used in the final output: - -```go-html-template -{{ with (templates.Defer (dict "key" "global")) }} - {{ $t := debug.Timer "tailwindcss" }} - {{ with resources.Get "css/styles.css" }} - {{ $opts := dict - "inlineImports" true - "optimize" hugo.IsProduction - }} - {{ with . | css.TailwindCSS $opts }} - {{ if hugo.IsDevelopment }} - - {{ else }} - {{ with . | minify | fingerprint }} - - {{ end }} - {{ end }} - {{ end }} - {{ end }} - {{ $t.Stop }} -{{ end }} -``` - -> [!note] -> This function only works in combination with the `with` keyword. -> -> Variables defined on the outside are not visible on the inside and vice versa. To pass in data, use the `data` [option](#options). - -For the above to work well when running the server (or `hugo -w`), you want to have a configuration similar to this: - -{{< code-toggle file=hugo >}} -[module] -[[module.mounts]] -source = "hugo_stats.json" -target = "assets/notwatching/hugo_stats.json" -disableWatch = true -[build.buildStats] -enable = true -[[build.cachebusters]] -source = "assets/notwatching/hugo_stats\\.json" -target = "styles\\.css" -[[build.cachebusters]] -source = "(postcss|tailwind)\\.config\\.js" -target = "css" -{{< /code-toggle >}} - -## Options - -The `templates.Defer` function takes a single argument, a map with the following optional keys: - -key (`string`) -: The key to use for the deferred template. This will, combined with a hash of the template content, be used as a cache key. If this is not set, Hugo will execute the deferred template on every render. This is not what you want for shared resources like CSS and JavaScript. - -data (`map`) -: Optional map to pass as data to the deferred template. This will be available in the deferred template as `.` or `$`. - -```go-html-template -Language Outside: {{ site.Language.Lang }} -Page Outside: {{ .RelPermalink }} -I18n Outside: {{ i18n "hello" }} -{{ $data := (dict "page" . )}} -{{ with (templates.Defer (dict "data" $data )) }} - Language Inside: {{ site.Language.Lang }} - Page Inside: {{ .page.RelPermalink }} - I18n Inside: {{ i18n "hello" }} -{{ end }} -``` - -The [output format](/configuration/output-formats/), [site](/methods/page/site/), and [language](/methods/site/language) will be the same, even if the execution is deferred. In the example above, this means that the `site.Language.Lang` and `.RelPermalink` will be the same on the inside and the outside of the deferred template. diff --git a/docs/content/en/functions/templates/Exists.md b/docs/content/en/functions/templates/Exists.md deleted file mode 100644 index 79fc561c8..000000000 --- a/docs/content/en/functions/templates/Exists.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: templates.Exists -description: Reports whether a template file exists under the given path relative to the layouts directory. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: bool - signatures: [templates.Exists PATH] -aliases: [/functions/templates.exists] ---- - -A template file is any file within the `layouts` directory of either the project or any of its theme components. - -Use the `templates.Exists` function with dynamic template paths: - -```go-html-template -{{ $partialPath := printf "headers/%s.html" .Type }} -{{ if templates.Exists ( printf "partials/%s" $partialPath ) }} - {{ partial $partialPath . }} -{{ else }} - {{ partial "headers/default.html" . }} -{{ end }} -``` - -In the example above, if a "headers" partial does not exist for the given content type, Hugo falls back to a default template. diff --git a/docs/content/en/functions/templates/_index.md b/docs/content/en/functions/templates/_index.md deleted file mode 100644 index a385604ea..000000000 --- a/docs/content/en/functions/templates/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Template functions -linkTitle: templates -description: Use these functions to query the template system. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/functions/time/AsTime.md b/docs/content/en/functions/time/AsTime.md deleted file mode 100644 index 760329a13..000000000 --- a/docs/content/en/functions/time/AsTime.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: time.AsTime -description: Returns the given string representation of a date/time value as a time.Time value. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [time] - returnType: time.Time - signatures: ['time.AsTime INPUT [TIMEZONE]'] -aliases: [/functions/time] ---- - -## Overview - -Hugo provides [functions] and [methods] to format, localize, parse, compare, and manipulate date/time values. Before you can do any of these with string representations of date/time values, you must first convert them to [`time.Time`] values using the `time.AsTime` function. - -```go-html-template -{{ $t := "2023-10-15T13:18:50-07:00" }} -{{ time.AsTime $t }} → 2023-10-15 13:18:50 -0700 PDT (time.Time) -``` - -## Parsable strings - -As shown above, the first argument must be a parsable string representation of a date/time value. For example: - -{{% include "/_common/parsable-date-time-strings.md" %}} - -To override the default time zone, set the [`timeZone`] in your site configuration or provide a second argument to the `time.AsTime` function. For example: - -```go-html-template -{{ time.AsTime "15 Oct 2023" "America/Los_Angeles" }} -``` - -The list of valid time zones may be system dependent, but should include `UTC`, `Local`, or any location in the [IANA Time Zone database]. - -The order of precedence for determining the time zone is: - -1. The time zone offset in the date/time string -1. The time zone provided as the second argument to the `time.AsTime` function -1. The time zone specified in your site configuration -1. The `Etc/UTC` time zone - -[IANA Time Zone database]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones -[`time.Time`]: https://pkg.go.dev/time#Time -[`timeZone`]: /configuration/all/#timezone -[functions]: /functions/time/ -[methods]: /methods/time/ diff --git a/docs/content/en/functions/time/Duration.md b/docs/content/en/functions/time/Duration.md deleted file mode 100644 index bd6adfbfa..000000000 --- a/docs/content/en/functions/time/Duration.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: time.Duration -description: Returns a time.Duration value using the given time unit and number. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [duration] - returnType: time.Duration - signatures: [time.Duration TIME_UNIT NUMBER] -aliases: [/functions/duration] ---- - -The `time.Duration` function returns a [`time.Duration`] value that you can use with any of the `Duration` [methods]. - -This template: - -```go-html-template -{{ $duration := time.Duration "hour" 24 }} -{{ printf "There are %.0f seconds in one day." $duration.Seconds }} -``` - -Is rendered to: - -```text -There are 86400 seconds in one day. -``` - -The time unit must be one of the following: - -Duration|Valid time units -:--|:-- -hours|`hour`, `h` -minutes|`minute`, `m` -seconds|`second`, `s` -milliseconds|`millisecond`, `ms` -microseconds|`microsecond`, `us`, `µs` -nanoseconds|`nanosecond`, `ns` - -[`time.Duration`]: https://pkg.go.dev/time#Duration -[methods]: /methods/duration/ diff --git a/docs/content/en/functions/time/Format.md b/docs/content/en/functions/time/Format.md deleted file mode 100644 index f1b0d6d83..000000000 --- a/docs/content/en/functions/time/Format.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -title: time.Format -description: Returns the given date/time as a formatted and localized string. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [dateFormat] - returnType: string - signatures: [time.Format LAYOUT INPUT] -aliases: [/functions/dateformat] ---- - -Use the `time.Format` function with `time.Time` values: - -```go-html-template -{{ $t := time.AsTime "2023-10-15T13:18:50-07:00" }} -{{ time.Format "2 Jan 2006" $t }} → 15 Oct 2023 -``` - -Or use `time.Format` with a parsable string representation of a date/time value: - -```go-html-template -{{ $t := "15 Oct 2023" }} -{{ time.Format "January 2, 2006" $t }} → October 15, 2023 -``` - -Examples of parsable string representations: - -{{% include "/_common/parsable-date-time-strings.md" %}} - -To override the default time zone, set the [`timeZone`] in your site configuration. The order of precedence for determining the time zone is: - -1. The time zone offset in the date/time string -1. The time zone specified in your site configuration -1. The `Etc/UTC` time zone - -[`timeZone`]: /configuration/all/#timezone - -## Layout string - -{{% include "/_common/time-layout-string.md" %}} - -## Localization - -Use the `time.Format` function to localize `time.Time` values for the current language and region. - -{{% include "/_common/functions/locales.md" %}} - -Use the layout string as described above, or one of the tokens below. For example: - -```go-html-template -{{ .Date | time.Format ":date_medium" }} → Jan 27, 2023 -``` - -Localized to en-US: - -Token|Result -:--|:-- -`:date_full`|`Friday, January 27, 2023` -`:date_long`|`January 27, 2023` -`:date_medium`|`Jan 27, 2023` -`:date_short`|`1/27/23` -`:time_full`|`11:44:58 pm Pacific Standard Time` -`:time_long`|`11:44:58 pm PST` -`:time_medium`|`11:44:58 pm` -`:time_short`|`11:44 pm` - -Localized to de-DE: - -Token|Result -:--|:-- -`:date_full`|`Freitag, 27. Januar 2023` -`:date_long`|`27. Januar 2023` -`:date_medium`|`27.01.2023` -`:date_short`|`27.01.23` -`:time_full`|`23:44:58 Nordamerikanische Westküsten-Normalzeit` -`:time_long`|`23:44:58 PST` -`:time_medium`|`23:44:58` -`:time_short`|`23:44` diff --git a/docs/content/en/functions/time/In.md b/docs/content/en/functions/time/In.md deleted file mode 100644 index 821eb99b7..000000000 --- a/docs/content/en/functions/time/In.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: time.In -description: Returns the given date/time as represented in the specified IANA time zone. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: time.Time - signatures: [time.In TIMEZONE INPUT] ---- - -{{< new-in 0.146.0 />}} - -The `time.In` function returns the given date/time as represented in the specified [IANA](g) time zone. - -- If the time zone is an empty string or `UTC`, the time is returned in [UTC](g). -- If the time zone is `Local`, the time is returned in the system's local time zone. -- Otherwise, the time zone must be a valid IANA [time zone name]. - -[time zone name]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List - -```go-html-template -{{ $layout := "2006-01-02T15:04:05-07:00" }} -{{ $t := time.AsTime "2025-03-31T14:45:00-00:00" }} - -{{ $t | time.In "America/Denver" | time.Format $layout }} → 2025-03-31T08:45:00-06:00 -{{ $t | time.In "Australia/Adelaide" | time.Format $layout }} → 2025-04-01T01:15:00+10:30 -{{ $t | time.In "Europe/Oslo" | time.Format $layout }} → 2025-03-31T16:45:00+02:00 -``` diff --git a/docs/content/en/functions/time/Now.md b/docs/content/en/functions/time/Now.md deleted file mode 100644 index 9b6fa4692..000000000 --- a/docs/content/en/functions/time/Now.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -title: time.Now -description: Returns the current local time. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [now] - returnType: time.Time - signatures: [time.Now] -aliases: [/functions/now] ---- - -For example, when building a site on October 15, 2023 in the America/Los_Angeles time zone: - -```go-html-template -{{ time.Now }} -``` - -This produces a `time.Time` value, with a string representation such as: - -```text -2023-10-15 12:59:28.337140706 -0700 PDT m=+0.041752605 -``` - -To format and [localize](g) the value, pass it through the [`time.Format`] function: - -```go-html-template -{{ time.Now | time.Format "Jan 2006" }} → Oct 2023 -``` - -The `time.Now` function returns a `time.Time` value, so you can chain any of the [time methods] to the resulting value. For example: - -```go-html-template -{{ time.Now.Year }} → 2023 (int) -{{ time.Now.Weekday.String }} → Sunday -{{ time.Now.Month.String }} → October -{{ time.Now.Unix }} → 1697400955 (int64) -``` - -[`time.Format`]: /functions/time/format/ -[time methods]: /methods/time/ diff --git a/docs/content/en/functions/time/ParseDuration.md b/docs/content/en/functions/time/ParseDuration.md deleted file mode 100644 index af5d73ad5..000000000 --- a/docs/content/en/functions/time/ParseDuration.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: time.ParseDuration -description: Returns a time.Duration value by parsing the given duration string. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: time.Duration - signatures: [time.ParseDuration DURATION] -aliases: [/functions/time.parseduration] ---- - -The `time.ParseDuration` function returns a time.Duration value that you can use with any of the `Duration` [methods]. - -A duration string is a possibly signed sequence of decimal numbers, each with optional fraction and a unit suffix, such as `300ms`, `-1.5h` or `2h45m`. Valid time units are `ns`, `us` (or `µs`), `ms`, `s`, `m`, `h`. - -This template: - -```go-html-template -{{ $duration := time.ParseDuration "24h" }} -{{ printf "There are %.0f seconds in one day." $duration.Seconds }} -``` - -Is rendered to: - -```text -There are 86400 seconds in one day. -``` - -[`time.Duration`]: https://pkg.go.dev/time#Duration -[methods]: /methods/duration/ diff --git a/docs/content/en/functions/time/_index.md b/docs/content/en/functions/time/_index.md deleted file mode 100644 index 9c2ff2161..000000000 --- a/docs/content/en/functions/time/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Time functions -linkTitle: time -description: Use these functions to work with time values. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/functions/transform/CanHighlight.md b/docs/content/en/functions/transform/CanHighlight.md deleted file mode 100644 index c00445605..000000000 --- a/docs/content/en/functions/transform/CanHighlight.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: transform.CanHighlight -description: Reports whether the given code language is supported by the Chroma highlighter. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: bool - signatures: [transform.CanHighlight LANGUAGE] ---- - -```go-html-template -{{ transform.CanHighlight "go" }} → true -{{ transform.CanHighlight "klingon" }} → false -``` diff --git a/docs/content/en/functions/transform/Emojify.md b/docs/content/en/functions/transform/Emojify.md deleted file mode 100644 index 31c5dce70..000000000 --- a/docs/content/en/functions/transform/Emojify.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: transform.Emojify -description: Runs a string through the Emoji emoticons processor. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [emojify] - returnType: template.HTML - signatures: [transform.Emojify INPUT] -aliases: [/functions/emojify] ---- - -`emojify` runs a passed string through the Emoji emoticons processor. - -See the list of [emoji shortcodes] for available emoticons. - -The `emojify` function can be called in your templates but not directly in your content files by default. For emojis in content files, set [`enableEmoji`] to `true` in your site's configuration. Then you can write emoji shorthand directly into your content files; - -```text -I :heart: Hugo! -``` - -I :heart: Hugo! - -[`enableEmoji`]: /configuration/all/#enableemoji -[emoji shortcodes]: /quick-reference/emojis/ diff --git a/docs/content/en/functions/transform/HTMLEscape.md b/docs/content/en/functions/transform/HTMLEscape.md deleted file mode 100644 index 069fd92f2..000000000 --- a/docs/content/en/functions/transform/HTMLEscape.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: transform.HTMLEscape -description: Returns the given string, escaping special characters by replacing them with HTML entities. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [htmlEscape] - returnType: string - signatures: [transform.HTMLEscape INPUT] -aliases: [/functions/htmlescape] ---- - -The `transform.HTMLEscape` function escapes five special characters by replacing them with [HTML entities]: - -- `&` → `&` -- `<` → `<` -- `>` → `>` -- `'` → `'` -- `"` → `"` - -For example: - -```go-html-template -{{ htmlEscape "Lilo & Stitch" }} → Lilo & Stitch -{{ htmlEscape "7 > 6" }} → 7 > 6 -``` - -[html entities]: https://developer.mozilla.org/en-US/docs/Glossary/Entity diff --git a/docs/content/en/functions/transform/HTMLUnescape.md b/docs/content/en/functions/transform/HTMLUnescape.md deleted file mode 100644 index 563e63cf6..000000000 --- a/docs/content/en/functions/transform/HTMLUnescape.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: transform.HTMLUnescape -description: Returns the given string, replacing each HTML entity with its corresponding character. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [htmlUnescape] - returnType: string - signatures: [transform.HTMLUnescape INPUT] -aliases: [/functions/htmlunescape] ---- - -The `transform.HTMLUnescape` function replaces [HTML entities] with their corresponding characters. - -```go-html-template -{{ htmlUnescape "Lilo & Stitch" }} → Lilo & Stitch -{{ htmlUnescape "7 > 6" }} → 7 > 6 -``` - -In most contexts Go's [html/template] package will escape special characters. To bypass this behavior, pass the unescaped string through the [`safeHTML`] function. - -```go-html-template -{{ htmlUnescape "Lilo & Stitch" | safeHTML }} -``` - -[`safehtml`]: /functions/safe/html/ -[html entities]: https://developer.mozilla.org/en-us/docs/glossary/entity -[html/template]: https://pkg.go.dev/html/template diff --git a/docs/content/en/functions/transform/Highlight.md b/docs/content/en/functions/transform/Highlight.md deleted file mode 100644 index 5633f2256..000000000 --- a/docs/content/en/functions/transform/Highlight.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: transform.Highlight -description: Renders code with a syntax highlighter. -categories: [] -keywords: [highlight] -params: - functions_and_methods: - aliases: [highlight] - returnType: template.HTML - signatures: ['transform.Highlight CODE LANG [OPTIONS]'] -aliases: [/functions/highlight] ---- - -The `highlight` function uses the [Chroma] syntax highlighter, supporting over 200 languages with more than 40 [highlighting styles]. - -[chroma]: https://github.com/alecthomas/chroma -[highlighting styles]: /quick-reference/syntax-highlighting-styles/ - -## Arguments - -The `transform.Highlight` shortcode takes three arguments. - -CODE -: (`string`) The code to highlight. - -LANG -: (`string`) The language of the code to highlight. Choose from one of the [supported languages]. This value is case-insensitive. - -OPTIONS -: (`map or string`) A map or comma-separated key-value pairs wrapped in quotation marks. Set default values for each option in your [site configuration]. The key names are case-insensitive. - -[site configuration]: /configuration/markup#highlight -[supported languages]: /content-management/syntax-highlighting#languages - -## Examples - -```go-html-template -{{ $input := `fmt.Println("Hello World!")` }} -{{ transform.Highlight $input "go" }} - -{{ $input := `console.log('Hello World!');` }} -{{ $lang := "js" }} -{{ transform.Highlight $input $lang "lineNos=table, style=api" }} - -{{ $input := `echo "Hello World!"` }} -{{ $lang := "bash" }} -{{ $opts := dict "lineNos" "table" "style" "dracula" }} -{{ transform.Highlight $input $lang $opts }} -``` - -## Options - -{{% include "_common/syntax-highlighting-options.md" %}} diff --git a/docs/content/en/functions/transform/HighlightCodeBlock.md b/docs/content/en/functions/transform/HighlightCodeBlock.md deleted file mode 100644 index bbebf9459..000000000 --- a/docs/content/en/functions/transform/HighlightCodeBlock.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: transform.HighlightCodeBlock -description: Highlights code received in context within a code block render hook. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: highlight.HighlightResult - signatures: ['transform.HighlightCodeBlock CONTEXT [OPTIONS]'] ---- - -This function is only useful within a code block render hook. - -Given the context passed into a code block render hook, `transform.HighlightCodeBlock` returns a `HighlightResult` object with two methods. - -.Wrapped -: (`template.HTML`) Returns highlighted code wrapped in `
    `, `
    `, and `` elements. This is identical to the value returned by the transform.Highlight function.
    -
    -.Inner
    -: (`template.HTML`) Returns highlighted code without any wrapping elements, allowing you to create your own wrapper.
    -
    -```go-html-template
    -{{ $result := transform.HighlightCodeBlock . }}
    -{{ $result.Wrapped }}
    -```
    -
    -To override the default [highlighting options]:
    -
    -```go-html-template
    -{{ $opts := merge .Options (dict "linenos" true) }}
    -{{ $result := transform.HighlightCodeBlock . $opts }}
    -{{ $result.Wrapped }}
    -```
    -
    -[highlighting options]: /functions/transform/highlight/#options
    diff --git a/docs/content/en/functions/transform/Markdownify.md b/docs/content/en/functions/transform/Markdownify.md
    deleted file mode 100644
    index c22de1efe..000000000
    --- a/docs/content/en/functions/transform/Markdownify.md
    +++ /dev/null
    @@ -1,27 +0,0 @@
    ----
    -title: transform.Markdownify
    -description: Renders Markdown to HTML.
    -categories: []
    -keywords: []
    -params:
    -  functions_and_methods:
    -    aliases: [markdownify]
    -    returnType: template.HTML
    -    signatures: [transform.Markdownify INPUT]
    -aliases: [/functions/markdownify]
    ----
    -
    -```go-html-template
    -

    {{ .Title | markdownify }}

    -``` - -If the resulting HTML is a single paragraph, Hugo removes the wrapping `p` tags to produce inline HTML as required per the example above. - -To keep the wrapping `p` tags for a single paragraph, use the [`RenderString`] method on the `Page` object, setting the `display` option to `block`. - -> [!note] -> Although the `markdownify` function honors [Markdown render hooks] when rendering Markdown to HTML, use the `RenderString` method instead of `markdownify` if a render hook accesses `.Page` context. See issue [#9692] for details. - -[#9692]: https://github.com/gohugoio/hugo/issues/9692 -[`RenderString`]: /methods/page/renderstring/ -[Markdown render hooks]: /render-hooks/ diff --git a/docs/content/en/functions/transform/Plainify.md b/docs/content/en/functions/transform/Plainify.md deleted file mode 100644 index 780cf461a..000000000 --- a/docs/content/en/functions/transform/Plainify.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: transform.Plainify -description: Returns a string with all HTML tags removed. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [plainify] - returnType: template.HTML - signatures: [transform.Plainify INPUT] -aliases: [/functions/plainify] ---- - -```go-html-template -{{ "BatMan" | plainify }} → BatMan -``` diff --git a/docs/content/en/functions/transform/PortableText.md b/docs/content/en/functions/transform/PortableText.md deleted file mode 100644 index 7baba99d4..000000000 --- a/docs/content/en/functions/transform/PortableText.md +++ /dev/null @@ -1,213 +0,0 @@ ---- -title: transform.PortableText -description: Converts Portable Text to Markdown. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [transform.PortableText MAP] ---- - -{{< new-in "0.145.0" />}} - -[Portable Text](https://www.portabletext.org/) is a JSON structure that represent rich text content in the [Sanity](https://www.sanity.io/) CMS. In Hugo, this function is typically used in a [Content Adapter](https://gohugo.io/content-management/content-adapters/) that creates pages from Sanity data. - -## Types supported - -- `block` and `span` -- `image`. Note that the image handling is currently very simple; we link to the `asset.url` using `asset.altText` as the image alt text and `asset.title` as the title. For more fine grained control you may want to process the images in a [image render hook](/render-hooks/images/). -- `code` (see the [code-input](https://www.sanity.io/plugins/code-input) plugin). Code will be rendered as [fenced code blocks](/contribute/documentation/#fenced-code-blocks) with any file name provided passed on as a markdown attribute. - -> [!note] -> Since the Portable Text gets converted to Markdown before it gets passed to Hugo, rendering of links, headings, images and code blocks can be controlled with [Render Hooks](https://gohugo.io/render-hooks/). - -## Example - -### Content Adapter - -```go-html-template {file="content/_content.gotmpl" copy=true} -{{ $projectID := "mysanityprojectid" }} -{{ $useCached := true }} -{{ $api := "api" }} -{{ if $useCached }} - {{/* See https://www.sanity.io/docs/api-cdn */}} - {{ $api = "apicdn" }} -{{ end }} -{{ $url := printf "https://%s.%s.sanity.io/v2021-06-07/data/query/production" $projectID $api }} - -{{/* prettier-ignore-start */ -}} -{{ $q := `*[_type == 'post']{ - title, publishedAt, summary, slug, body[]{ - ..., - _type == "image" => { - ..., - asset->{ - _id, - path, - url, - altText, - title, - description, - metadata { - dimensions { - aspectRatio, - width, - height - } - } - } - } - }, - }` -}} -{{/* prettier-ignore-end */ -}} -{{ $body := dict "query" $q | jsonify }} -{{ $opts := dict "method" "post" "body" $body }} -{{ $r := resources.GetRemote $url $opts }} -{{ $m := $r | transform.Unmarshal }} -{{ $result := $m.result }} -{{ range $result }} - {{ if not .slug }} - {{ continue }} - {{ end }} - {{ $markdown := transform.PortableText .body }} - {{ $content := dict - "mediaType" "text/markdown" - "value" $markdown - }} - {{ $params := dict - "portabletext" (.body | jsonify (dict "indent" " ")) - }} - {{ $page := dict - "content" $content - "kind" "page" - "path" .slug.current - "title" .title - "date" (.publishedAt | time ) - "summary" .summary - "params" $params - }} - {{ $.AddPage $page }} -{{ end }} -``` - -### Sanity setup - -Below outlines a suitable Sanity studio setup for the above example. - -```ts {file="sanity.config.ts" copy=true} -import {defineConfig} from 'sanity' -import {structureTool} from 'sanity/structure' -import {visionTool} from '@sanity/vision' -import {schemaTypes} from './schemaTypes' -import {media} from 'sanity-plugin-media' -import {codeInput} from '@sanity/code-input' - -export default defineConfig({ - name: 'default', - title: 'my-sanity-project', - - projectId: 'mysanityprojectid', - dataset: 'production', - - plugins: [structureTool(), visionTool(), media(),codeInput()], - - schema: { - types: schemaTypes, - }, -}) -``` - -Type/schema definition: - -```ts {file="schemaTypes/postType.ts" copy=true} -import {defineField, defineType} from 'sanity' - -export const postType = defineType({ - name: 'post', - title: 'Post', - type: 'document', - fields: [ - defineField({ - name: 'title', - type: 'string', - validation: (rule) => rule.required(), - }), - defineField({ - name: 'summary', - type: 'string', - validation: (rule) => rule.required(), - }), - defineField({ - name: 'slug', - type: 'slug', - options: {source: 'title'}, - validation: (rule) => rule.required(), - }), - defineField({ - name: 'publishedAt', - type: 'datetime', - initialValue: () => new Date().toISOString(), - validation: (rule) => rule.required(), - }), - defineField({ - name: 'body', - type: 'array', - of: [ - { - type: 'block', - }, - { - type: 'image' - }, - { - type: 'code', - options: { - language: 'css', - languageAlternatives: [ - {title: 'HTML', value: 'html'}, - {title: 'CSS', value: 'css'}, - ], - withFilename: true, - }, - }, - ], - }), - ], -}) -``` - -Note that the above requires some additional plugins to be installed: - -```bash -npm i sanity-plugin-media @sanity/code-input -``` - -```ts {file="schemaTypes/index.ts" copy=true} -import {postType} from './postType' - -export const schemaTypes = [postType] -``` - -## Server setup - -Unfortunately, Sanity's API does not support [RFC 7234](https://tools.ietf.org/html/rfc7234) and their output changes even if the data has not. A recommended setup is therefore to use their cached `apicdn` endpoint (see above) and then set up a reasonable polling and file cache strategy in your Hugo configuration, e.g: - -{{< code-toggle file=hugo >}} -[HTTPCache] - [[HTTPCache.polls]] - disable = false - low = '30s' - high = '3m' - [HTTPCache.polls.for] - includes = ['https://*.*.sanity.io/**'] - -[caches.getresource] - dir = ':cacheDir/:project' - maxAge = "5m" -{{< /code-toggle >}} - -The polling above will be used when running the server/watch mode and rebuild when you push new content in Sanity. - -See [Caching in resources.GetRemote](/functions/resources/getremote/#caching) for more fine grained control. diff --git a/docs/content/en/functions/transform/Remarshal.md b/docs/content/en/functions/transform/Remarshal.md deleted file mode 100644 index ecf7fc905..000000000 --- a/docs/content/en/functions/transform/Remarshal.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -title: transform.Remarshal -description: Marshals a string of serialized data, or a map, into a string of serialized data in the specified format. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: string - signatures: [transform.Remarshal FORMAT INPUT] -aliases: [/functions/transform.remarshal] ---- - -The format must be one of `json`, `toml`, `yaml`, or `xml`. If the input is a string of serialized data, it must be valid JSON, TOML, YAML, or XML. - -> [!note] -> This function is primarily a helper for Hugo's documentation, used to convert configuration and front matter examples to JSON, TOML, and YAML. -> -> This is not a general purpose converter, and may change without notice if required for Hugo's documentation site. - -Example 1 -: Convert a string of TOML to JSON. - -```go-html-template -{{ $s := ` - baseURL = 'https://example.org/' - languageCode = 'en-US' - title = 'ABC Widgets' -`}} -
    {{ transform.Remarshal "json" $s }}
    -``` - -Resulting HTML: - -```html -
    {
    -   "baseURL": "https://example.org/",
    -   "languageCode": "en-US",
    -   "title": "ABC Widgets"
    -}
    -
    -``` - -Rendered in browser: - -```text -{ - "baseURL": "https://example.org/", - "languageCode": "en-US", - "title": "ABC Widgets" -} -``` - -Example 2 -: Convert a map to YAML. - -```go-html-template -{{ $m := dict - "a" "Hugo rocks!" - "b" (dict "question" "What is 6x7?" "answer" 42) - "c" (slice "foo" "bar") -}} -
    {{ transform.Remarshal "yaml" $m }}
    -``` - -Resulting HTML: - -```html -
    a: Hugo rocks!
    -b:
    -  answer: 42
    -  question: What is 6x7?
    -c:
    -- foo
    -- bar
    -
    -``` - -Rendered in browser: - -```text -a: Hugo rocks! -b: - answer: 42 - question: What is 6x7? -c: -- foo -- bar -``` diff --git a/docs/content/en/functions/transform/ToMath.md b/docs/content/en/functions/transform/ToMath.md deleted file mode 100644 index a9f12c546..000000000 --- a/docs/content/en/functions/transform/ToMath.md +++ /dev/null @@ -1,164 +0,0 @@ ---- -title: transform.ToMath -description: Renders mathematical equations and expressions written in the LaTeX markup language. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: types.Result[template.HTML] - signatures: ['transform.ToMath INPUT [OPTIONS]'] -aliases: [/functions/tomath] ---- - -{{< new-in 0.132.0 />}} - -Hugo uses an embedded instance of the [KaTeX] display engine to render mathematical markup to HTML. You do not need to install the KaTeX display engine. - -```go-html-template -{{ transform.ToMath "c = \\pm\\sqrt{a^2 + b^2}" }} -``` - -> [!note] -> By default, Hugo renders mathematical markup to [MathML], and does not require any CSS to display the result. -> -> To optimize rendering quality and accessibility, use the `htmlAndMathml` output option as described below. This approach requires an external stylesheet. - -```go-html-template -{{ $opts := dict "output" "htmlAndMathml" }} -{{ transform.ToMath "c = \\pm\\sqrt{a^2 + b^2}" $opts }} -``` - -## Options - -Pass a map of options as the second argument to the `transform.ToMath` function. The options below are a subset of the KaTeX [rendering options]. - -displayMode -: (`bool`) Whether to render in display mode instead of inline mode. Default is `false`. - -errorColor -: (`string`) The color of the error messages expressed as an RGB [hexadecimal color]. Default is `#cc0000`. - -fleqn -: (`bool`) Whether to render flush left with a 2em left margin. Default is `false`. - -macros -: (`map`) A map of macros to be used in the math expression. Default is `{}`. - - ```go-html-template - {{ $macros := dict - "\\addBar" "\\bar{#1}" - "\\bold" "\\mathbf{#1}" - }} - {{ $opts := dict "macros" $macros }} - {{ transform.ToMath "\\addBar{y} + \\bold{H}" $opts }} - ``` - -minRuleThickness -: (`float`) The minimum thickness of the fraction lines in `em`. Default is `0.04`. - -output -: (`string`). Determines the markup language of the output, one of `html`, `mathml`, or `htmlAndMathml`. Default is `mathml`. - - With `html` and `htmlAndMathml` you must include the KaTeX style sheet within the `head` element of your base template. - - ```html - - -throwOnError -: (`bool`) Whether to throw a `ParseError` when KaTeX encounters an unsupported command or invalid LaTeX. Default is `true`. - -## Error handling - -There are three ways to handle errors: - -1. Let KaTeX throw an error and fail the build. This is the default behavior. -1. Set the `throwOnError` option to `false` to make KaTeX render the expression as an error instead of throwing an error. See [options](#options). -1. Handle the error in your template. - -The example below demonstrates error handing within a template. - -## Example - -Instead of client-side JavaScript rendering of mathematical markup using MathJax or KaTeX, create a passthrough render hook which calls the `transform.ToMath` function. - -### Step 1 - -Enable and configure the Goldmark [passthrough extension] in your site configuration. The passthrough extension preserves raw Markdown within delimited snippets of text, including the delimiters themselves. - -{{< code-toggle file=hugo copy=true >}} -[markup.goldmark.extensions.passthrough] -enable = true - -[markup.goldmark.extensions.passthrough.delimiters] -block = [['\[', '\]'], ['$$', '$$']] -inline = [['\(', '\)']] -{{< /code-toggle >}} - -> [!note] -> The configuration above precludes the use of the `$...$` delimiter pair for inline equations. Although you can add this delimiter pair to the configuration, you will need to double-escape the `$` symbol when used outside of math contexts to avoid unintended formatting. - -### Step 2 - -Create a [passthrough render hook] to capture and render the LaTeX markup. - -```go-html-template {file="layouts/_default/_markup/render-passthrough.html" copy=true} -{{- $opts := dict "output" "htmlAndMathml" "displayMode" (eq .Type "block") }} -{{- with try (transform.ToMath .Inner $opts) }} - {{- with .Err }} - {{- errorf "Unable to render mathematical markup to HTML using the transform.ToMath function. The KaTeX display engine threw the following error: %s: see %s." . $.Position }} - {{- else }} - {{- .Value }} - {{- $.Page.Store.Set "hasMath" true }} - {{- end }} -{{- end -}} -``` - -### Step 3 - -In your base template, conditionally include the KaTeX CSS within the head element. - -```go-html-template {file="layouts/_default/baseof.html" copy=true} - - {{ $noop := .WordCount }} - {{ if .Page.Store.Get "hasMath" }} - - {{ end }} - -``` - -In the above, note the use of a [noop](g) statement to force content rendering before we check the value of `hasMath` with the `Store.Get` method. - -### Step 4 - -Add some mathematical markup to your content, then test. - -```text {file="content/example.md"} -This is an inline \(a^*=x-b^*\) equation. - -These are block equations: - -\[a^*=x-b^*\] - -$$a^*=x-b^*$$ -``` - -## Chemistry - -{{< new-in 0.144.0 />}} - -You can also use the `transform.ToMath` function to render chemical equations, leveraging the `\ce` and `\pu` functions from the [mhchem] package. - -```text -$$C_p[\ce{H2O(l)}] = \pu{75.3 J // mol K}$$ -``` - -$$C_p[\ce{H2O(l)}] = \pu{75.3 J // mol K}$$ - -[hexadecimal color]: https://developer.mozilla.org/en-US/docs/Web/CSS/hex-color -[KaTeX]: https://katex.org/ -[MathML]: https://developer.mozilla.org/en-US/docs/Web/MathML -[mhchem]: https://mhchem.github.io/MathJax-mhchem/ -[passthrough extension]: /configuration/markup/#passthrough -[passthrough render hook]: /render-hooks/passthrough/ -[rendering options]: https://katex.org/docs/options.html diff --git a/docs/content/en/functions/transform/Unmarshal.md b/docs/content/en/functions/transform/Unmarshal.md deleted file mode 100644 index 93168294c..000000000 --- a/docs/content/en/functions/transform/Unmarshal.md +++ /dev/null @@ -1,372 +0,0 @@ ---- -title: transform.Unmarshal -description: Parses serialized data and returns a map or an array. Supports CSV, JSON, TOML, YAML, and XML. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [unmarshal] - returnType: any - signatures: ['transform.Unmarshal [OPTIONS] INPUT'] -aliases: [/functions/transform.unmarshal] ---- - -The input can be a string or a [resource](g). - -## Unmarshal a string - -```go-html-template -{{ $string := ` -title: Les Misérables -author: Victor Hugo -`}} - -{{ $book := unmarshal $string }} -{{ $book.title }} → Les Misérables -{{ $book.author }} → Victor Hugo -``` - -## Unmarshal a resource - -Use the `transform.Unmarshal` function with global, page, and remote resources. - -### Global resource - -A global resource is a file within the `assets` directory, or within any directory mounted to the `assets` directory. - -```text -assets/ -└── data/ - └── books.json -``` - -```go-html-template -{{ $data := dict }} -{{ $path := "data/books.json" }} -{{ with resources.Get $path }} - {{ with . | transform.Unmarshal }} - {{ $data = . }} - {{ end }} -{{ else }} - {{ errorf "Unable to get global resource %q" $path }} -{{ end }} - -{{ range where $data "author" "Victor Hugo" }} - {{ .title }} → Les Misérables -{{ end }} -``` - -### Page resource - -A page resource is a file within a [page bundle]. - -```text -content/ -├── post/ -│ └── book-reviews/ -│ ├── books.json -│ └── index.md -└── _index.md -``` - -```go-html-template -{{ $data := dict }} -{{ $path := "books.json" }} -{{ with .Resources.Get $path }} - {{ with . | transform.Unmarshal }} - {{ $data = . }} - {{ end }} -{{ else }} - {{ errorf "Unable to get page resource %q" $path }} -{{ end }} - -{{ range where $data "author" "Victor Hugo" }} - {{ .title }} → Les Misérables -{{ end }} -``` - -### Remote resource - -A remote resource is a file on a remote server, accessible via HTTP or HTTPS. - -```go-html-template -{{ $data := dict }} -{{ $url := "https://example.org/books.json" }} -{{ with try (resources.GetRemote $url) }} - {{ with .Err }} - {{ errorf "%s" . }} - {{ else with .Value }} - {{ $data = . | transform.Unmarshal }} - {{ else }} - {{ errorf "Unable to get remote resource %q" $url }} - {{ end }} -{{ end }} - -{{ range where $data "author" "Victor Hugo" }} - {{ .title }} → Les Misérables -{{ end }} -``` - -> [!note] -> When retrieving remote data, a misconfigured server may send a response header with an incorrect [Content-Type]. For example, the server may set the Content-Type header to `application/octet-stream` instead of `application/json`. -> -> In these cases, pass the resource `Content` through the `transform.Unmarshal` function instead of passing the resource itself. For example, in the above, do this instead: -> -> `{{ $data = .Content | transform.Unmarshal }}` - -## Working with CSV - -### Options - -When unmarshaling a CSV file, provide an optional map of options. - -delimiter -: (`string`) The delimiter used. Default is `,`. - -comment -: (`string`) The comment character used in the CSV. If set, lines beginning with the comment character without preceding whitespace are ignored. - -lazyQuotes -: {{< new-in 0.122.0 />}} -: (`bool`) Whether to allow a quote in an unquoted field, or to allow a non-doubled quote in a quoted field. Default is `false`. - -targetType -: {{< new-in 0.146.7 />}} -: (`string`) The target data type, either `slice` or `map`. Default is `slice`. - -### Examples - -The examples below use this CSV file: - -```csv -"name","type","breed","age" -"Spot","dog","Collie",3 -"Rover","dog","Boxer",5 -"Felix","cat","Calico",7 -``` - -To render an HTML table from a CSV file: - -```go-html-template -{{ $data := slice }} -{{ $file := "pets.csv" }} -{{ with or (.Resources.Get $file) (resources.Get $file) }} - {{ $opts := dict "targetType" "slice" }} - {{ $data = transform.Unmarshal $opts . }} -{{ end }} - -{{ with $data }} - - - - {{ range index . 0 }} - - {{ end }} - - - - {{ range . | after 1 }} - - {{ range . }} - - {{ end }} - - {{ end }} - -
    {{ . }}
    {{ . }}
    -{{ end }} -``` - -To extract a subset of the data, or to sort the data, unmarshal to a map instead of a slice: - -```go-html-template -{{ $data := slice }} -{{ $file := "pets.csv" }} -{{ with or (.Resources.Get $file) (resources.Get $file) }} - {{ $opts := dict "targetType" "map" }} - {{ $data = transform.Unmarshal $opts . }} -{{ end }} - -{{ with sort (where $data "type" "dog") "name" "asc" }} - - - - - - - - - - - {{ range . }} - - - - - - - {{ end }} - -
    nametypebreedage
    {{ .name }}{{ .type }}{{ .breed }}{{ .age }}
    -{{ end }} -``` - -## Working with XML - -When unmarshaling an XML file, do not include the root node when accessing data. For example, after unmarshaling the RSS feed below, access the feed title with `$data.channel.title`. - -```xml - - - - Books on Example Site - https://example.org/books/ - Recent content in Books on Example Site - en-US - - - The Hunchback of Notre Dame - Written by Victor Hugo - https://example.org/books/the-hunchback-of-notre-dame/ - Mon, 09 Oct 2023 09:27:12 -0700 - https://example.org/books/the-hunchback-of-notre-dame/ - - - Les Misérables - Written by Victor Hugo - https://example.org/books/les-miserables/ - Mon, 09 Oct 2023 09:27:11 -0700 - https://example.org/books/les-miserables/ - - - -``` - -Get the remote data: - -```go-html-template -{{ $data := dict }} -{{ $url := "https://example.org/books/index.xml" }} -{{ with try (resources.GetRemote $url) }} - {{ with .Err }} - {{ errorf "%s" . }} - {{ else with .Value }} - {{ $data = . | transform.Unmarshal }} - {{ else }} - {{ errorf "Unable to get remote resource %q" $url }} - {{ end }} -{{ end }} -``` - -Inspect the data structure: - -```go-html-template -
    {{ debug.Dump $data }}
    -``` - -List the book titles: - -```go-html-template -{{ with $data.channel.item }} -
      - {{ range . }} -
    • {{ .title }}
    • - {{ end }} -
    -{{ end }} -``` - -Hugo renders this to: - -```html -
      -
    • The Hunchback of Notre Dame
    • -
    • Les Misérables
    • -
    -``` - -### XML attributes and namespaces - -Let's add a `lang` attribute to the `title` nodes of our RSS feed, and a namespaced node for the ISBN number: - -```xml - - - - Books on Example Site - https://example.org/books/ - Recent content in Books on Example Site - en-US - - - The Hunchback of Notre Dame - Written by Victor Hugo - 9780140443530 - https://example.org/books/the-hunchback-of-notre-dame/ - Mon, 09 Oct 2023 09:27:12 -0700 - https://example.org/books/the-hunchback-of-notre-dame/ - - - Les Misérables - Written by Victor Hugo - 9780451419439 - https://example.org/books/les-miserables/ - Mon, 09 Oct 2023 09:27:11 -0700 - https://example.org/books/les-miserables/ - - - -``` - -After retrieving the remote data, inspect the data structure: - -```go-html-template -
    {{ debug.Dump $data }}
    -``` - -Each item node looks like this: - -```json -{ - "description": "Written by Victor Hugo", - "guid": "https://example.org/books/the-hunchback-of-notre-dame/", - "link": "https://example.org/books/the-hunchback-of-notre-dame/", - "number": "9780140443530", - "pubDate": "Mon, 09 Oct 2023 09:27:12 -0700", - "title": { - "#text": "The Hunchback of Notre Dame", - "-lang": "en" - } -} -``` - -The title keys do not begin with an underscore or a letter---they are not valid [identifiers](g). Use the [`index`] function to access the values: - -```go-html-template -{{ with $data.channel.item }} -
      - {{ range . }} - {{ $title := index .title "#text" }} - {{ $lang := index .title "-lang" }} - {{ $ISBN := .number }} -
    • {{ $title }} ({{ $lang }}) {{ $ISBN }}
    • - {{ end }} -
    -{{ end }} -``` - -Hugo renders this to: - -```html -
      -
    • The Hunchback of Notre Dame (en) 9780140443530
    • -
    • Les Misérables (fr) 9780451419439
    • -
    -``` - -[`index`]: /functions/collections/indexfunction/ -[Content-Type]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type -[page bundle]: /content-management/page-bundles/ diff --git a/docs/content/en/functions/transform/XMLEscape.md b/docs/content/en/functions/transform/XMLEscape.md deleted file mode 100644 index 9e6c77927..000000000 --- a/docs/content/en/functions/transform/XMLEscape.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -title: transform.XMLEscape -description: Returns the given string, removing disallowed characters then escaping the result to its XML equivalent. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: string - signatures: [transform.XMLEscape INPUT] ---- - -{{< new-in 0.121.0 />}} - -The `transform.XMLEscape` function removes [disallowed characters] as defined in the XML specification, then escapes the result by replacing the following characters with [HTML entities]: - -- `"` → `"` -- `'` → `'` -- `&` → `&` -- `<` → `<` -- `>` → `>` -- `\t` → ` ` -- `\n` → ` ` -- `\r` → ` ` - -For example: - -```go-html-template -{{ transform.XMLEscape "

    abc

    " }} → <p>abc</p> -``` - -When using `transform.XMLEscape` in a template rendered by Go's [html/template] package, declare the string to be safe HTML to avoid double escaping. For example, in an RSS template: - -```xml {file="layouts/_default/rss.xml"} -{{ .Summary | transform.XMLEscape | safeHTML }} -``` - -[disallowed characters]: https://www.w3.org/TR/xml/#charsets -[html entities]: https://developer.mozilla.org/en-us/docs/glossary/entity -[html/template]: https://pkg.go.dev/html/template diff --git a/docs/content/en/functions/transform/_index.md b/docs/content/en/functions/transform/_index.md deleted file mode 100644 index 19c271b65..000000000 --- a/docs/content/en/functions/transform/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Transform functions -linkTitle: transform -description: Use these functions to transform values from one format to another. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/functions/urls/AbsLangURL.md b/docs/content/en/functions/urls/AbsLangURL.md deleted file mode 100644 index da45ca224..000000000 --- a/docs/content/en/functions/urls/AbsLangURL.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: urls.AbsLangURL -description: Returns an absolute URL with a language prefix, if any. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [absLangURL] - returnType: string - signatures: [urls.AbsLangURL INPUT] -aliases: [/functions/abslangurl] ---- - -Use this function with both monolingual and multilingual configurations. The URL returned by this function depends on: - -- Whether the input begins with a slash (`/`) -- The `baseURL` in your site configuration -- The language prefix, if any - -This is the site configuration for the examples that follow: - -{{< code-toggle file=hugo >}} -defaultContentLanguage = 'en' -defaultContentLanguageInSubdir = true -[languages.en] -weight = 1 -[languages.es] -weight = 2 -{{< /code-toggle >}} - -## Input does not begin with a slash - -If the input does not begin with a slash, the path in the resulting URL will be relative to the `baseURL` in your site configuration. - -When rendering the `en` site with `baseURL = https://example.org/` - -```go-html-template -{{ absLangURL "" }} → https://example.org/en/ -{{ absLangURL "articles" }} → https://example.org/en/articles -{{ absLangURL "style.css" }} → https://example.org/en/style.css -``` - -When rendering the `en` site with `baseURL = https://example.org/docs/` - -```go-html-template -{{ absLangURL "" }} → https://example.org/docs/en/ -{{ absLangURL "articles" }} → https://example.org/docs/en/articles -{{ absLangURL "style.css" }} → https://example.org/docs/en/style.css -``` - -## Input begins with a slash - -If the input begins with a slash, the path in the resulting URL will be relative to the protocol+host of the `baseURL` in your site configuration. - -When rendering the `en` site with `baseURL = https://example.org/` - -```go-html-template -{{ absLangURL "/" }} → https://example.org/en/ -{{ absLangURL "/articles" }} → https://example.org/en/articles -{{ absLangURL "/style.css" }} → https://example.org/en/style.css -``` - -When rendering the `en` site with `baseURL = https://example.org/docs/` - -```go-html-template -{{ absLangURL "/" }} → https://example.org/en/ -{{ absLangURL "/articles" }} → https://example.org/en/articles -{{ absLangURL "/style.css" }} → https://example.org/en/style.css -``` - -> [!note] -> As illustrated by the previous example, using a leading slash is rarely desirable and can lead to unexpected outcomes. In nearly all cases, omit the leading slash. diff --git a/docs/content/en/functions/urls/AbsURL.md b/docs/content/en/functions/urls/AbsURL.md deleted file mode 100644 index 72613cd0b..000000000 --- a/docs/content/en/functions/urls/AbsURL.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -title: urls.AbsURL -description: Returns an absolute URL. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [absURL] - returnType: string - signatures: [urls.AbsURL INPUT] -aliases: [/functions/absurl] ---- - -With multilingual configurations, use the [`urls.AbsLangURL`] function instead. The URL returned by this function depends on: - -- Whether the input begins with a slash (`/`) -- The `baseURL` in your site configuration - -## Input does not begin with a slash - -If the input does not begin with a slash, the path in the resulting URL will be relative to the `baseURL` in your site configuration. - -With `baseURL = https://example.org/` - -```go-html-template -{{ absURL "" }} → https://example.org/ -{{ absURL "articles" }} → https://example.org/articles -{{ absURL "style.css" }} → https://example.org/style.css -``` - -With `baseURL = https://example.org/docs/` - -```go-html-template -{{ absURL "" }} → https://example.org/docs/ -{{ absURL "articles" }} → https://example.org/docs/articles -{{ absURL "style.css" }} → https://example.org/docs/style.css -``` - -## Input begins with a slash - -If the input begins with a slash, the path in the resulting URL will be relative to the protocol+host of the `baseURL` in your site configuration. - -With `baseURL = https://example.org/` - -```go-html-template -{{ absURL "/" }} → https://example.org/ -{{ absURL "/articles" }} → https://example.org/articles -{{ absURL "/style.css" }} → https://example.org/style.css -``` - -With `baseURL = https://example.org/docs/` - -```go-html-template -{{ absURL "/" }} → https://example.org/ -{{ absURL "/articles" }} → https://example.org/articles -{{ absURL "/style.css" }} → https://example.org/style.css -``` - -> [!note] -> As illustrated by the previous example, using a leading slash is rarely desirable and can lead to unexpected outcomes. In nearly all cases, omit the leading slash. - -[`urls.AbsLangURL`]: /functions/urls/abslangurl/ diff --git a/docs/content/en/functions/urls/Anchorize.md b/docs/content/en/functions/urls/Anchorize.md deleted file mode 100644 index d18bd9a4d..000000000 --- a/docs/content/en/functions/urls/Anchorize.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: urls.Anchorize -description: Returns the given string, sanitized for usage in an HTML id attribute. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [anchorize] - returnType: string - signatures: [urls.Anchorize INPUT] -aliases: [/functions/anchorize] ---- - -{{% include "/_common/functions/urls/anchorize-vs-urlize.md" %}} - -## Sanitizing logic - -With the default Markdown renderer, Goldmark, the sanitizing logic is controlled by your site configuration: - -{{< code-toggle file=hugo >}} -[markup.goldmark.parser] -autoHeadingIDType = 'github' -{{< /code-toggle >}} - -This controls the behavior of the `anchorize` function and the generation of heading IDs when rendering Markdown to HTML. - -Set `autoHeadingIDType` to one of: - -github -: Compatible with GitHub. This is the default. - -github-ascii -: Similar to the `github` setting, but removes non-ASCII characters. - -blackfriday -: Provided for backwards compatibility with Hugo v0.59.1 and earlier. This option will be removed in a future release. diff --git a/docs/content/en/functions/urls/JoinPath.md b/docs/content/en/functions/urls/JoinPath.md deleted file mode 100644 index b9da7e437..000000000 --- a/docs/content/en/functions/urls/JoinPath.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: urls.JoinPath -description: Joins the provided elements into a URL string and cleans the result of any ./ or ../ elements. If the argument list is empty, JoinPath returns an empty string. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: string - signatures: [urls.JoinPath ELEMENT...] -aliases: [/functions/urls.joinpath] ---- - -```go-html-template -{{ urls.JoinPath }} → "" (empty string) -{{ urls.JoinPath "" }} → / -{{ urls.JoinPath "a" }} → a -{{ urls.JoinPath "a" "b" }} → a/b -{{ urls.JoinPath "/a" "b" }} → /a/b -{{ urls.JoinPath "https://example.org" "b" }} → https://example.org/b - -{{ urls.JoinPath (slice "a" "b") }} → a/b -``` - -Unlike the [`path.Join`] function, `urls.JoinPath` retains consecutive leading slashes. - -[`path.Join`]: /functions/path/join/ diff --git a/docs/content/en/functions/urls/Parse.md b/docs/content/en/functions/urls/Parse.md deleted file mode 100644 index 7def5fb1d..000000000 --- a/docs/content/en/functions/urls/Parse.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: urls.Parse -description: Parses a URL into a URL structure. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [] - returnType: url.URL - signatures: [urls.Parse URL] -aliases: [/functions/urls.parse] ---- - -The `urls.Parse` function parses a URL into a [URL structure](https://godoc.org/net/url#URL). The URL may be relative (a path, without a host) or absolute (starting with a [scheme]). Hugo throws an error when parsing an invalid URL. - -[scheme]: https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml#uri-schemes-1 - -```go-html-template -{{ $url := "https://example.org:123/foo?a=6&b=7#bar" }} -{{ $u := urls.Parse $url }} - -{{ $u.String }} → https://example.org:123/foo?a=6&b=7#bar -{{ $u.IsAbs }} → true -{{ $u.Scheme }} → https -{{ $u.Host }} → example.org:123 -{{ $u.Hostname }} → example.org -{{ $u.RequestURI }} → /foo?a=6&b=7 -{{ $u.Path }} → /foo -{{ $u.RawQuery }} → a=6&b=7 -{{ $u.Query }} → map[a:[6] b:[7]] -{{ $u.Query.a }} → [6] -{{ $u.Query.Get "a" }} → 6 -{{ $u.Query.Has "b" }} → true -{{ $u.Fragment }} → bar -``` diff --git a/docs/content/en/functions/urls/Ref.md b/docs/content/en/functions/urls/Ref.md deleted file mode 100644 index 92abed91a..000000000 --- a/docs/content/en/functions/urls/Ref.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: urls.Ref -description: Returns the absolute URL of the page with the given path, language, and output format. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [ref] - returnType: string - signatures: - - urls.Ref PAGE PATH - - urls.Ref PAGE OPTIONS -aliases: [/functions/ref] ---- - -## Usage - -The `ref` function takes two arguments: - -1. The context for resolving relative paths (typically the current page). -1. Either the target page's path or an options map (see below). - -## Options - -{{% include "_common/ref-and-relref-options.md" %}} - -## Examples - -The following examples show the rendered output for a page on the English version of the site: - -```go-html-template -{{ ref . "/books/book-1" }} → https://example.org/en/books/book-1/ - -{{ $opts := dict "path" "/books/book-1" }} -{{ ref . $opts }} → https://example.org/en/books/book-1/ - -{{ $opts := dict "path" "/books/book-1" "lang" "de" }} -{{ ref . $opts }} → https://example.org/de/books/book-1/ - -{{ $opts := dict "path" "/books/book-1" "lang" "de" "outputFormat" "json" }} -{{ ref . $opts }} → https://example.org/de/books/book-1/index.json -``` - -## Error handling - -{{% include "_common/ref-and-relref-error-handling.md" %}} diff --git a/docs/content/en/functions/urls/RelLangURL.md b/docs/content/en/functions/urls/RelLangURL.md deleted file mode 100644 index af8bff3d7..000000000 --- a/docs/content/en/functions/urls/RelLangURL.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -title: urls.RelLangURL -description: Returns a relative URL with a language prefix, if any. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [relLangURL] - returnType: string - signatures: [urls.RelLangURL INPUT] -aliases: [/functions/rellangurl] ---- - -Use this function with both monolingual and multilingual configurations. The URL returned by this function depends on: - -- Whether the input begins with a slash (`/`) -- The `baseURL` in your site configuration -- The language prefix, if any - -This is the site configuration for the examples that follow: - -{{< code-toggle file=hugo >}} -defaultContentLanguage = 'en' -defaultContentLanguageInSubdir = true -[languages.en] -weight = 1 -[languages.es] -weight = 2 -{{< /code-toggle >}} - -## Input does not begin with a slash - -If the input does not begin with a slash, the resulting URL will be relative to the `baseURL` in your site configuration. - -When rendering the `en` site with `baseURL = https://example.org/` - -```go-html-template -{{ relLangURL "" }} → /en/ -{{ relLangURL "articles" }} → /en/articles -{{ relLangURL "style.css" }} → /en/style.css -{{ relLangURL "https://example.org" }} → https://example.org -{{ relLangURL "https://example.org/" }} → /en -{{ relLangURL "https://www.example.org" }} → https://www.example.org -{{ relLangURL "https://www.example.org/" }} → https://www.example.org/ -``` - -When rendering the `en` site with `baseURL = https://example.org/docs/` - -```go-html-template -{{ relLangURL "" }} → /docs/en/ -{{ relLangURL "articles" }} → /docs/en/articles -{{ relLangURL "style.css" }} → /docs/en/style.css -{{ relLangURL "https://example.org" }} → https://example.org -{{ relLangURL "https://example.org/" }} → https://example.org/ -{{ relLangURL "https://example.org/docs" }} → https://example.org/docs -{{ relLangURL "https://example.org/docs/" }} → /docs/en -{{ relLangURL "https://www.example.org" }} → https://www.example.org -{{ relLangURL "https://www.example.org/" }} → https://www.example.org/ -``` - -## Input begins with a slash - -If the input begins with a slash, the resulting URL will be relative to the protocol+host of the `baseURL` in your site configuration. - -When rendering the `en` site with `baseURL = https://example.org/` - -```go-html-template -{{ relLangURL "/" }} → /en/ -{{ relLangURL "/articles" }} → /en/articles -{{ relLangURL "/style.css" }} → /en/style.css -``` - -When rendering the `en` site with `baseURL = https://example.org/docs/` - -```go-html-template -{{ relLangURL "/" }} → /en/ -{{ relLangURL "/articles" }} → /en/articles -{{ relLangURL "/style.css" }} → /en/style.css -``` - -> [!note] -> As illustrated by the previous example, using a leading slash is rarely desirable and can lead to unexpected outcomes. In nearly all cases, omit the leading slash. diff --git a/docs/content/en/functions/urls/RelRef.md b/docs/content/en/functions/urls/RelRef.md deleted file mode 100644 index aa7acf50b..000000000 --- a/docs/content/en/functions/urls/RelRef.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: urls.RelRef -description: Returns the relative URL of the page with the given path, language, and output format. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [relref] - returnType: string - signatures: - - urls.RelRef PAGE PATH - - urls.RelRef PAGE OPTIONS -aliases: [/functions/relref] ---- - -## Usage - -The `relref` function takes two arguments: - -1. The context for resolving relative paths (typically the current page). -1. Either the target page's path or an options map (see below). - -## Options - -{{% include "_common/ref-and-relref-options.md" %}} - -## Examples - -The following examples show the rendered output for a page on the English version of the site: - -```go-html-template -{{ relref . "/books/book-1" }} → /en/books/book-1/ - -{{ $opts := dict "path" "/books/book-1" }} -{{ relref . $opts }} → /en/books/book-1/ - -{{ $opts := dict "path" "/books/book-1" "lang" "de" }} -{{ relref . $opts }} → /de/books/book-1/ - -{{ $opts := dict "path" "/books/book-1" "lang" "de" "outputFormat" "json" }} -{{ relref . $opts }} → /de/books/book-1/index.json -``` - -## Error handling - -{{% include "_common/ref-and-relref-error-handling.md" %}} diff --git a/docs/content/en/functions/urls/RelURL.md b/docs/content/en/functions/urls/RelURL.md deleted file mode 100644 index 0aef4043f..000000000 --- a/docs/content/en/functions/urls/RelURL.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: urls.RelURL -description: Returns a relative URL. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [relURL] - returnType: string - signatures: [urls.RelURL INPUT] -aliases: [/functions/relurl] ---- - -With multilingual configurations, use the [`urls.RelLangURL`] function instead. The URL returned by this function depends on: - -- Whether the input begins with a slash (`/`) -- The `baseURL` in your site configuration - -## Input does not begin with a slash - -If the input does not begin with a slash, the resulting URL will be relative to the `baseURL` in your site configuration. - -With `baseURL = https://example.org/` - -```go-html-template -{{ relURL "" }} → / -{{ relURL "articles" }} → /articles -{{ relURL "style.css" }} → /style.css -{{ relURL "https://example.org" }} → https://example.org -{{ relURL "https://example.org/" }} → / -{{ relURL "https://www.example.org" }} → https://www.example.org -{{ relURL "https://www.example.org/" }} → https://www.example.org/ -``` - -With `baseURL = https://example.org/docs/` - -```go-html-template -{{ relURL "" }} → /docs/ -{{ relURL "articles" }} → /docs/articles -{{ relURL "style.css" }} → /docs/style.css -{{ relURL "https://example.org" }} → https://example.org -{{ relURL "https://example.org/" }} → https://example.org/ -{{ relURL "https://example.org/docs" }} → https://example.org/docs -{{ relURL "https://example.org/docs/" }} → /docs -{{ relURL "https://www.example.org" }} → https://www.example.org -{{ relURL "https://www.example.org/" }} → https://www.example.org/ -``` - -## Input begins with a slash - -If the input begins with a slash, the resulting URL will be relative to the protocol+host of the `baseURL` in your site configuration. - -With `baseURL = https://example.org/` - -```go-html-template -{{ relURL "/" }} → / -{{ relURL "/articles" }} → /articles -{{ relURL "/style.css" }} → /style.css -``` - -With `baseURL = https://example.org/docs/` - -```go-html-template -{{ relURL "/" }} → / -{{ relURL "/articles" }} → /articles -{{ relURL "/style.css" }} → /style.css -``` - -> [!note] -> As illustrated by the previous example, using a leading slash is rarely desirable and can lead to unexpected outcomes. In nearly all cases, omit the leading slash. - -[`urls.RelLangURL`]: /functions/urls/rellangurl/ diff --git a/docs/content/en/functions/urls/URLize.md b/docs/content/en/functions/urls/URLize.md deleted file mode 100644 index b0cc812ec..000000000 --- a/docs/content/en/functions/urls/URLize.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -title: urls.URLize -description: Returns the given string, sanitized for usage in a URL. -categories: [] -keywords: [] -params: - functions_and_methods: - aliases: [urlize] - returnType: string - signatures: [urls.URLize INPUT] -aliases: [/functions/urlize] ---- - -{{% include "/_common/functions/urls/anchorize-vs-urlize.md" %}} - -## Example - -Use the `urlize` function to create a link to a [term page](g). - -Consider this site configuration: - -{{< code-toggle file=hugo >}} -[taxonomies] -author = 'authors' -{{< /code-toggle >}} - -And this front matter: - -{{< code-toggle file=content/books/les-miserables.md fm=true >}} -title = 'Les Misérables' -authors = ['Victor Hugo'] -{{< /code-toggle >}} - -The published site will have this structure: - -```text -public/ -├── authors/ -│ ├── victor-hugo/ -│ │ └── index.html -│ └── index.html -├── books/ -│ ├── les-miserables/ -│ │ └── index.html -│ └── index.html -└── index.html -``` - -To create a link to the term page: - -```go-html-template -{{ $taxonomy := "authors" }} -{{ $term := "Victor Hugo" }} -{{ with index .Site.Taxonomies $taxonomy (urlize $term) }} - {{ .Page.LinkTitle }} -{{ end }} -``` - -To generate a list of term pages associated with a given content page, use the [`GetTerms`] method on a `Page` object. - -[`GetTerms`]: /methods/page/getterms/ diff --git a/docs/content/en/functions/urls/_index.md b/docs/content/en/functions/urls/_index.md deleted file mode 100644 index 3a1962d7a..000000000 --- a/docs/content/en/functions/urls/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: URL functions -linkTitle: urls -description: Use these functions to work with URLs. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/getting-started/_index.md b/docs/content/en/getting-started/_index.md deleted file mode 100644 index 2e2f57127..000000000 --- a/docs/content/en/getting-started/_index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Getting started -description: How to get started with Hugo. -categories: [] -keywords: [] -weight: 10 -aliases: [/overview/introduction/] ---- diff --git a/docs/content/en/getting-started/directory-structure.md b/docs/content/en/getting-started/directory-structure.md deleted file mode 100644 index 3feecd135..000000000 --- a/docs/content/en/getting-started/directory-structure.md +++ /dev/null @@ -1,207 +0,0 @@ ---- -title: Directory structure -description: An overview of Hugo's directory structure. -categories: [] -keywords: [] -weight: 30 -aliases: [/overview/source-directory/] ---- - -Each Hugo project is a directory, with subdirectories that contribute to the content, structure, behavior, and presentation of your site. - -## Site skeleton - -Hugo generates a project skeleton when you create a new site. For example, this command: - -```sh -hugo new site my-site -``` - -Creates this directory structure: - -```txt -my-site/ -├── archetypes/ -│ └── default.md -├── assets/ -├── content/ -├── data/ -├── i18n/ -├── layouts/ -├── static/ -├── themes/ -└── hugo.toml <-- site configuration -``` - -Depending on requirements, you may wish to organize your site configuration into subdirectories: - -```txt -my-site/ -├── archetypes/ -│ └── default.md -├── assets/ -├── config/ <-- site configuration -│ └── _default/ -│ └── hugo.toml -├── content/ -├── data/ -├── i18n/ -├── layouts/ -├── static/ -└── themes/ -``` - -When you build your site, Hugo creates a `public` directory, and typically a `resources` directory as well: - -```txt -my-site/ -├── archetypes/ -│ └── default.md -├── assets/ -├── config/ -│ └── _default/ -│ └── hugo.toml -├── content/ -├── data/ -├── i18n/ -├── layouts/ -├── public/ <-- created when you build your site -├── resources/ <-- created when you build your site -├── static/ -└── themes/ -``` - -## Directories - -Each of the subdirectories contributes to the content, structure, behavior, or presentation of your site. - -archetypes -: The `archetypes` directory contains templates for new content. See [details](/content-management/archetypes/). - -assets -: The `assets` directory contains global resources typically passed through an asset pipeline. This includes resources such as images, CSS, Sass, JavaScript, and TypeScript. See [details](/hugo-pipes/introduction/). - -config -: The `config` directory contains your site configuration, possibly split into multiple subdirectories and files. For projects with minimal configuration or projects that do not need to behave differently in different environments, a single configuration file named `hugo.toml` in the root of the project is sufficient. See [details](/configuration/introduction/#configuration-directory). - -content -: The `content` directory contains the markup files (typically Markdown) and page resources that comprise the content of your site. See [details](/content-management/organization/). - -data -: The `data` directory contains data files (JSON, TOML, YAML, or XML) that augment content, configuration, localization, and navigation. See [details](/content-management/data-sources/). - -i18n -: The `i18n` directory contains translation tables for multilingual sites. See [details](/content-management/multilingual/). - -layouts -: The `layouts` directory contains templates to transform content, data, and resources into a complete website. See [details](/templates/). - -public -: The `public` directory contains the published website, generated when you run the `hugo` or `hugo server` commands. Hugo recreates this directory and its content as needed. See [details](/getting-started/usage/#build-your-site). - -resources -: The `resources` directory contains cached output from Hugo's asset pipelines, generated when you run the `hugo` or `hugo server` commands. By default this cache directory includes CSS and images. Hugo recreates this directory and its content as needed. - -static -: The `static` directory contains files that will be copied to the `public` directory when you build your site. For example: `favicon.ico`, `robots.txt`, and files that verify site ownership. Before the introduction of [page bundles](g) and [asset pipelines](/hugo-pipes/introduction/), the `static` directory was also used for images, CSS, and JavaScript. - -themes -: The `themes` directory contains one or more [themes](g), each in its own subdirectory. - -## Union file system - -Hugo creates a union file system, allowing you to mount two or more directories to the same location. For example, let's say your home directory contains a Hugo project in one directory, and shared content in another: - -```text -home/ -└── user/ - ├── my-site/ - │ ├── content/ - │ │ ├── books/ - │ │ │ ├── _index.md - │ │ │ ├── book-1.md - │ │ │ └── book-2.md - │ │ └── _index.md - │ ├── themes/ - │ │ └── my-theme/ - │ └── hugo.toml - └── shared-content/ - └── films/ - ├── _index.md - ├── film-1.md - └── film-2.md -``` - -You can include the shared content when you build your site using mounts. In your site configuration: - -{{< code-toggle file=hugo >}} -[[module.mounts]] -source = 'content' -target = 'content' - -[[module.mounts]] -source = '/home/user/shared-content' -target = 'content' -{{< /code-toggle >}} - -> [!note] -> When you overlay one directory on top of another, you must mount both directories. -> -> Hugo does not follow symbolic links. If you need the functionality provided by symbolic links, use Hugo's union file system instead. - -After mounting, the union file system has this structure: - -```text -home/ -└── user/ - └── my-site/ - ├── content/ - │ ├── books/ - │ │ ├── _index.md - │ │ ├── book-1.md - │ │ └── book-2.md - │ ├── films/ - │ │ ├── _index.md - │ │ ├── film-1.md - │ │ └── film-2.md - │ └── _index.md - ├── themes/ - │ └── my-theme/ - └── hugo.toml -``` - -> [!note] -> When two or more files have the same path, the order of precedence follows the order of the mounts. For example, if the shared content directory contains `books/book-1.md`, it will be ignored because the project's `content` directory was mounted first. - -You can mount directories to `archetypes`, `assets`, `content`, `data`, `i18n`, `layouts`, and `static`. See [details](/configuration/module/#mounts). - -You can also mount directories from Git repositories using Hugo Modules. See [details](/hugo-modules/). - -## Theme skeleton - -Hugo generates a functional theme skeleton when you create a new theme. For example, this command: - -```text -hugo new theme my-theme -``` - -Creates this directory structure (subdirectories not shown): - -```text -my-theme/ -├── archetypes/ -├── assets/ -├── content/ -├── data/ -├── i18n/ -├── layouts/ -├── static/ -├── LICENSE -├── README.md -├── hugo.toml -└── theme.toml -``` - -Using the union file system described above, Hugo mounts each of these directories to the corresponding location in the project. When two files have the same path, the file in the project directory takes precedence. This allows you, for example, to override a theme's template by placing a copy in the same location within the project directory. - -If you are simultaneously using components from two or more themes or modules, and there's a path collision, the first mount takes precedence. diff --git a/docs/content/en/getting-started/external-learning-resources/build-websites-with-hugo.png b/docs/content/en/getting-started/external-learning-resources/build-websites-with-hugo.png deleted file mode 100644 index ebed7e89f..000000000 Binary files a/docs/content/en/getting-started/external-learning-resources/build-websites-with-hugo.png and /dev/null differ diff --git a/docs/content/en/getting-started/external-learning-resources/hugo-in-action.png b/docs/content/en/getting-started/external-learning-resources/hugo-in-action.png deleted file mode 100644 index 7bc5c9930..000000000 Binary files a/docs/content/en/getting-started/external-learning-resources/hugo-in-action.png and /dev/null differ diff --git a/docs/content/en/getting-started/external-learning-resources/index.md b/docs/content/en/getting-started/external-learning-resources/index.md deleted file mode 100644 index 7838b6810..000000000 --- a/docs/content/en/getting-started/external-learning-resources/index.md +++ /dev/null @@ -1,83 +0,0 @@ ---- -title: External learning resources -linkTitle: External resources -description: Use these third-party resources to learn Hugo. -categories: [] -keywords: [] -weight: 40 ---- - -## Books - -### Hugo in Action - -Hugo in Action is a step-by-step guide to using Hugo to create static websites. Working with a complete example website and source code samples, you'll learn how to build and host a low-maintenance, high-performance site that will wow your users and stay stable without relying on a third-party server. - -[{{< img src="hugo-in-action.png" alt="Book cover: Hugo in Action" filter="process" filterArgs="resize x350 webp">}}](https://www.manning.com/books/hugo-in-action/) - -Author: Atishay Jain\ -Publisher: [Manning Publications](https://www.manning.com/books/hugo-in-action/)\ -Publication date: March 2022\ -Length: 488 pages\ -ISBN: 9781617297007 - -### Build Websites with Hugo - -In this book, you'll use Hugo to build a personal portfolio site that you can use to showcase your skills and thoughts to the world. You'll build the basic skeleton, develop a custom theme, and use content templates to generate new pages quickly. You'll use internal and external data sources to embed content into your site and render some of your content in JSON and RSS. You'll add a blog section with posts and integrate Disqus with your site, and then make your site searchable. - -[{{< img src="build-websites-with-hugo.png" alt="Book cover: Build Websites with Hugo" filter="process" filterArgs="resize x350 webp">}}](https://pragprog.com/titles/bhhugo/build-websites-with-hugo/) - -Author: Brian P. Hogan\ -Publisher: [Pragmatic Bookshelf](https://pragprog.com/titles/bhhugo/build-websites-with-hugo/)\ -Publication date: May 2020\ -Length: 154 pages\ -ISBN: 9781680507263 - -## Videos - -### Hugo Beginner Tutorial Series - -Welcome to this introduction to Hugo tutorial. This series aims to take you from a lion cub with basic web design knowledge to creating your first Hugo website. In this series, you'll learn how to set up a Hugo site, the basics of using Hugo layouts, partials, and templating, set up a blog, and finally, use data files. By the end of this series, you'll have the foundational knowledge to build your own Hugo sites. - -1. [Getting set up in Hugo](https://cloudcannon.com/tutorials/hugo-beginner-tutorial/) -1. [Layouts in Hugo](https://cloudcannon.com/tutorials/hugo-beginner-tutorial/layouts-in-hugo/) -1. [Hugo Partials](https://cloudcannon.com/tutorials/hugo-beginner-tutorial/hugo-partials/) -1. [Hugo templating basics](https://cloudcannon.com/tutorials/hugo-beginner-tutorial/hugo-templating-basics/) -1. [Blogging in Hugo](https://cloudcannon.com/tutorials/hugo-beginner-tutorial/blogging-in-hugo/) -1. [Using Data in Hugo](https://cloudcannon.com/tutorials/hugo-beginner-tutorial/using-data-in-hugo/) - -Creator: Mike Neumegen\ -Affiliation: [CloudCannon](https://cloudcannon.com/)\ -Creation date: April 2022 - -### Hugo Static Site Generator - -This course covers the basics of using the Hugo static site generator. Work your way through the articles, and we'll teach you everything you need to know to create a professional and scalable website or blog! - -1. [Introduction](https://www.giraffeacademy.com/static-site-generators/hugo/) -1. [Windows Installation](https://www.giraffeacademy.com/static-site-generators/hugo/installing-hugo-on-windows/) -1. [Mac Installation](https://www.giraffeacademy.com/static-site-generators/hugo/installing-hugo-on-mac/) -1. [Creating A New Site](https://www.giraffeacademy.com/static-site-generators/hugo/hugo-directory-structure/) -1. [Installing & Using Themes](https://www.giraffeacademy.com/static-site-generators/hugo/installing-using-themes/) -1. [Content Organization](https://www.giraffeacademy.com/static-site-generators/hugo/content-organization/) -1. [Front Matter](https://www.giraffeacademy.com/static-site-generators/hugo/front-matter/) -1. [Archetypes](https://www.giraffeacademy.com/static-site-generators/hugo/archetypes/) -1. [Shortcodes](https://www.giraffeacademy.com/static-site-generators/hugo/archetypes/) -1. [Taxonomies](https://www.giraffeacademy.com/static-site-generators/hugo/taxonomies/) -1. [Template Basics](https://www.giraffeacademy.com/static-site-generators/hugo/introduction-to-templates/) -1. [List Page Templates](https://www.giraffeacademy.com/static-site-generators/hugo/list-page-templates/) -1. [Single Page Templates](https://www.giraffeacademy.com/static-site-generators/hugo/single-page-templates/) -1. [Home Page Templates](https://www.giraffeacademy.com/static-site-generators/hugo/home-page-templates/) -1. [Section Templates](https://www.giraffeacademy.com/static-site-generators/hugo/section-templates/) -1. [Block Templates](https://www.giraffeacademy.com/static-site-generators/hugo/block-templates/) -1. [Variables](https://www.giraffeacademy.com/static-site-generators/hugo/variables/) -1. [Functions](https://www.giraffeacademy.com/static-site-generators/hugo/functions/) -1. [Conditionals](https://www.giraffeacademy.com/static-site-generators/hugo/conditionals/) -1. [Data Templates](https://www.giraffeacademy.com/static-site-generators/hugo/data-templates/) -1. [Partial Templates](https://www.giraffeacademy.com/static-site-generators/hugo/partial-templates/) -1. [Shortcode Templates](https://www.giraffeacademy.com/static-site-generators/hugo/shortcode-templates/) -1. [Building & Hosting](https://www.giraffeacademy.com/static-site-generators/hugo/building-&-hosting/) - -Creator: Mike Dane\ -Affiliation: [Giraffe Academy](https://www.giraffeacademy.com/)\ -Creation date: September 2017 diff --git a/docs/content/en/getting-started/quick-start.md b/docs/content/en/getting-started/quick-start.md deleted file mode 100644 index dfb78f42e..000000000 --- a/docs/content/en/getting-started/quick-start.md +++ /dev/null @@ -1,218 +0,0 @@ ---- -title: Quick start -description: Create a Hugo site in minutes. -categories: [] -keywords: [] -params: - minVersion: v0.128.0 -weight: 10 -aliases: [/quickstart/,/overview/quickstart/] ---- - -In this tutorial you will: - -1. Create a site -1. Add content -1. Configure the site -1. Publish the site - -## Prerequisites - -Before you begin this tutorial you must: - -1. [Install Hugo] (extended or extended/deploy edition, {{% param "minVersion" %}} or later) -1. [Install Git] - -You must also be comfortable working from the command line. - -## Create a site - -### Commands - -> [!note] -> **If you are a Windows user:** -> -> - Do not use the Command Prompt -> - Do not use Windows PowerShell -> - Run these commands from [PowerShell] or a Linux terminal such as WSL or Git > Bash -> -> PowerShell and Windows PowerShell [are different applications]. - -Verify that you have installed Hugo {{% param "minVersion" %}} or later. - -```text -hugo version -``` - -Run these commands to create a Hugo site with the [Ananke] theme. The next section provides an explanation of each command. - -```text -hugo new site quickstart -cd quickstart -git init -git submodule add https://github.com/theNewDynamic/gohugo-theme-ananke.git themes/ananke -echo "theme = 'ananke'" >> hugo.toml -hugo server -``` - -View your site at the URL displayed in your terminal. Press `Ctrl + C` to stop Hugo's development server. - -### Explanation of commands - -Create the [directory structure] for your project in the `quickstart` directory. - -```text -hugo new site quickstart -``` - -Change the current directory to the root of your project. - -```text -cd quickstart -``` - -Initialize an empty Git repository in the current directory. - -```text -git init -``` - -Clone the [Ananke] theme into the `themes` directory, adding it to your project as a [Git submodule]. - -```text -git submodule add https://github.com/theNewDynamic/gohugo-theme-ananke.git themes/ananke -``` - -Append a line to the site configuration file, indicating the current theme. - -```text -echo "theme = 'ananke'" >> hugo.toml -``` - -Start Hugo's development server to view the site. - -```text -hugo server -``` - -Press `Ctrl + C` to stop Hugo's development server. - -## Add content - -Add a new page to your site. - -```text -hugo new content content/posts/my-first-post.md -``` - -Hugo created the file in the `content/posts` directory. Open the file with your editor. - -```text -+++ -title = 'My First Post' -date = 2024-01-14T07:07:07+01:00 -draft = true -+++ -``` - -Notice the `draft` value in the [front matter] is `true`. By default, Hugo does not publish draft content when you build the site. Learn more about [draft, future, and expired content]. - -Add some [Markdown] to the body of the post, but do not change the `draft` value. - -```text -+++ -title = 'My First Post' -date = 2024-01-14T07:07:07+01:00 -draft = true -+++ -## Introduction - -This is **bold** text, and this is *emphasized* text. - -Visit the [Hugo](https://gohugo.io) website! -``` - -Save the file, then start Hugo's development server to view the site. You can run either of the following commands to include draft content. - -```text -hugo server --buildDrafts -hugo server -D -``` - -View your site at the URL displayed in your terminal. Keep the development server running as you continue to add and change content. - -When satisfied with your new content, set the front matter `draft` parameter to `false`. - -> [!note] -> Hugo's rendering engine conforms to the CommonMark [specification] for Markdown. The CommonMark organization provides a useful [live testing tool] powered by the reference implementation. - -## Configure the site - -With your editor, open the [site configuration] file (`hugo.toml`) in the root of your project. - -```text -baseURL = 'https://example.org/' -languageCode = 'en-us' -title = 'My New Hugo Site' -theme = 'ananke' -``` - -Make the following changes: - -1. Set the `baseURL` for your production site. This value must begin with the protocol and end with a slash, as shown above. -1. Set the `languageCode` to your language and region. -1. Set the `title` for your production site. - -Start Hugo's development server to see your changes, remembering to include draft content. - -```text -hugo server -D -``` - -> [!note] -> Most theme authors provide configuration guidelines and options. Make sure to visit your theme's repository or documentation site for details. -> -> [The New Dynamic], authors of the Ananke theme, provide [documentation] for configuration and usage. They also provide a [demonstration site]. - -## Publish the site - -In this step you will _publish_ your site, but you will not _deploy_ it. - -When you _publish_ your site, Hugo creates the entire static site in the `public` directory in the root of your project. This includes the HTML files, and assets such as images, CSS files, and JavaScript files. - -When you publish your site, you typically do _not_ want to include [draft, future, or expired content]. The command is simple. - -```text -hugo -``` - -To learn how to _deploy_ your site, see the [host and deploy] section. - -## Ask for help - -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. - -## Other resources - -For other resources to help you learn Hugo, including books and video tutorials, see the [external learning resources](/getting-started/external-learning-resources/) page. - -[Ananke]: https://github.com/theNewDynamic/gohugo-theme-ananke -[are different applications]: https://learn.microsoft.com/en-us/powershell/scripting/whats-new/differences-from-windows-powershell?view=powershell-7.3 -[demonstration site]: https://gohugo-ananke-theme-demo.netlify.app/ -[directory structure]: /getting-started/directory-structure/ -[documentation]: https://github.com/theNewDynamic/gohugo-theme-ananke#readme -[draft, future, and expired content]: /getting-started/usage/#draft-future-and-expired-content -[draft, future, or expired content]: /getting-started/usage/#draft-future-and-expired-content -[forum]: https://discourse.gohugo.io/ -[front matter]: /content-management/front-matter/ -[Git submodule]: https://git-scm.com/book/en/v2/Git-Tools-Submodules -[host and deploy]: /host-and-deploy/ -[Install Git]: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git -[Install Hugo]: /installation/ -[live testing tool]: https://spec.commonmark.org/dingus/ -[Markdown]: https://daringfireball.net/projects/markdown -[PowerShell]: https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows -[requesting help]: https://discourse.gohugo.io/t/requesting-help/9132 -[site configuration]: /configuration/ -[specification]: https://spec.commonmark.org/ -[The New Dynamic]: https://www.thenewdynamic.com/ diff --git a/docs/content/en/getting-started/usage.md b/docs/content/en/getting-started/usage.md deleted file mode 100644 index d6bc42550..000000000 --- a/docs/content/en/getting-started/usage.md +++ /dev/null @@ -1,166 +0,0 @@ ---- -title: Basic usage -description: Use the command-line interface (CLI) to perform basic tasks. -categories: [] -keywords: [] -weight: 20 -aliases: [/overview/usage/,/extras/livereload/,/doc/usage/,/usage/] ---- - -## Test your installation - -After [installing] Hugo, test your installation by running: - -```sh -hugo version -``` - -You should see something like: - -```text -hugo v0.123.0-3c8a4713908e48e6523f058ca126710397aa4ed5+extended linux/amd64 BuildDate=2024-02-19T16:32:38Z VendorInfo=gohugoio -``` - -## Display available commands - -To see a list of the available commands and flags: - -```sh -hugo help -``` - -To get help with a subcommand, use the `--help` flag. For example: - -```sh -hugo server --help -``` - -## Build your site - -To build your site, `cd` into your project directory and run: - -```sh -hugo -``` - -The [`hugo`] command builds your site, publishing the files to the `public` directory. To publish your site to a different directory, use the [`--destination`] flag or set [`publishDir`] in your site configuration. - -> [!note] -> Hugo does not clear the `public` directory before building your site. Existing files are overwritten, but not deleted. This behavior is intentional to prevent the inadvertent removal of files that you may have added to the `public` directory after the build. -> -> Depending on your needs, you may wish to manually clear the contents of the `public` directory before every build. - -## Draft, future, and expired content - -Hugo allows you to set `draft`, `date`, `publishDate`, and `expiryDate` in the [front matter] of your content. By default, Hugo will not publish content when: - -- The `draft` value is `true` -- The `date` is in the future -- The `publishDate` is in the future -- The `expiryDate` is in the past - -{{< new-in 0.123.0 />}} - -> [!note] -> Hugo publishes descendants of draft, future, and expired [node](g) pages. To prevent publication of these descendants, use the [`cascade`] front matter field to cascade [build options] to the descendant pages. - -You can override the default behavior when running `hugo` or `hugo server` with command line flags: - -```sh -hugo --buildDrafts # or -D -hugo --buildExpired # or -E -hugo --buildFuture # or -F -``` - -Although you can also set these values in your site configuration, it can lead to unwanted results unless all content authors are aware of, and understand, the settings. - -> [!note] -> As noted above, Hugo does not clear the `public` directory before building your site. Depending on the _current_ evaluation of the four conditions above, after the build your `public` directory may contain extraneous files from a previous build. -> -> A common practice is to manually clear the contents of the `public` directory before each build to remove draft, expired, and future content. - -## Develop and test your site - -To view your site while developing layouts or creating content, `cd` into your project directory and run: - -```sh -hugo server -``` - -The [`hugo server`] command builds your site and serves your pages using a minimal HTTP server. When you run `hugo server` it will display the URL of your local site: - -```text -Web Server is available at http://localhost:1313/ -``` - -While the server is running, it watches your project directory for changes to assets, configuration, content, data, layouts, translations, and static files. When it detects a change, the server rebuilds your site and refreshes your browser using [LiveReload]. - -Most Hugo builds are so fast that you may not notice the change unless you are looking directly at your browser. - -### LiveReload - -While the server is running, Hugo injects JavaScript into the generated HTML pages. The LiveReload script creates a connection from the browser to the server via web sockets. You do not need to install any software or browser plugins, nor is any configuration required. - -### Automatic redirection - -When editing content, if you want your browser to automatically redirect to the page you last modified, run: - -```sh -hugo server --navigateToChanged -``` - -## Deploy your site - -> [!note] -> As noted above, Hugo does not clear the `public` directory before building your site. Manually clear the contents of the `public` directory before each build to remove draft, expired, and future content. - -When you are ready to deploy your site, run: - -```sh -hugo -``` - -This builds your site, publishing the files to the `public` directory. The directory structure will look something like this: - -```text -public/ -├── categories/ -│ ├── index.html -│ └── index.xml <-- RSS feed for this section -├── posts/ -│ ├── my-first-post/ -│ │ └── index.html -│ ├── index.html -│ └── index.xml <-- RSS feed for this section -├── tags/ -│ ├── index.html -│ └── index.xml <-- RSS feed for this section -├── index.html -├── index.xml <-- RSS feed for the site -└── sitemap.xml -``` - -In a simple hosting environment, where you typically `ftp`, `rsync`, or `scp` your files to the root of a virtual host, the contents of the `public` directory are all that you need. - -Most of our users deploy their sites using a [CI/CD](g) workflow, where a push[^1] to their GitHub or GitLab repository triggers a build and deployment. Popular providers include [AWS Amplify], [CloudCannon], [Cloudflare Pages], [GitHub Pages], [GitLab Pages], and [Netlify]. - -Learn more in the [host and deploy] section. - -[^1]: The Git repository contains the entire project directory, typically excluding the `public` directory because the site is built _after_ the push. - -[`--destination`]: /commands/hugo/#options -[`cascade`]: /content-management/front-matter/#cascade -[`hugo server`]: /commands/hugo_server/ -[`hugo`]: /commands/hugo/ -[`publishDir`]: /configuration/all/#publishdir -[AWS Amplify]: https://aws.amazon.com/amplify/ -[build options]: /content-management/build-options/ -[CloudCannon]: https://cloudcannon.com/ -[Cloudflare Pages]: https://pages.cloudflare.com/ -[front matter]: /content-management/front-matter/ -[GitHub Pages]: https://pages.github.com/ -[GitLab Pages]: https://docs.gitlab.com/ee/user/project/pages/ -[host and deploy]: /host-and-deploy/ -[installing]: /installation/ -[LiveReload]: https://github.com/livereload/livereload-js -[Netlify]: https://www.netlify.com/ diff --git a/docs/content/en/host-and-deploy/_index.md b/docs/content/en/host-and-deploy/_index.md deleted file mode 100644 index 627f12c36..000000000 --- a/docs/content/en/host-and-deploy/_index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Host and deploy -description: Services and tools to host and deploy your site. -categories: [] -keywords: [] -weight: 10 -aliases: [/hosting-and-deployment/] ---- diff --git a/docs/content/en/host-and-deploy/deploy-with-hugo-deploy.md b/docs/content/en/host-and-deploy/deploy-with-hugo-deploy.md deleted file mode 100644 index 8feeccbae..000000000 --- a/docs/content/en/host-and-deploy/deploy-with-hugo-deploy.md +++ /dev/null @@ -1,97 +0,0 @@ ---- -title: Deploy with hugo -description: Deploy your site with the hugo CLI. -categories: [] -keywords: [] -aliases: [/hosting-and-deployment/hugo-deploy/] ---- - -Use the `hugo deploy` command to deploy your site Amazon S3, Azure Blob Storage, or Google Cloud Storage. - -> [!note] -> This feature requires the Hugo extended/deploy edition. See the [installation] section for details. - -## Assumptions - -1. You have completed the [Quick Start] or have a Hugo website you are ready to deploy and share with the world. -1. You have an account with the service provider ([AWS], [Azure], or [Google Cloud]) that you want to deploy to. -1. You have authenticated. - - AWS: [Install the CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html) and run [`aws configure`](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html). - - Azure: [Install the CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli) and run [`az login`](https://docs.microsoft.com/en-us/cli/azure/authenticate-azure-cli). - - Google Cloud: [Install the CLI](https://cloud.google.com/sdk) and run [`gcloud auth login`](https://cloud.google.com/sdk/gcloud/reference/auth/login). - - Each service supports various authentication methods, including environment variables. See [details](https://gocloud.dev/howto/blob/#services). - -1. You have created a bucket to deploy to. If you want your site to be - public, be sure to configure the bucket to be publicly readable as a static website. - - AWS: [create a bucket](https://docs.aws.amazon.com/AmazonS3/latest/gsg/CreatingABucket.html) and [host a static website](https://docs.aws.amazon.com/AmazonS3/latest/userguide/WebsiteHosting.html) - - Azure: [create a storage container](https://docs.microsoft.com/en-us/azure/storage/blobs/storage-quickstart-blobs-portal) and [host a static website](https://learn.microsoft.com/en-us/azure/storage/blobs/storage-blob-static-website) - - - Google Cloud: [create a bucket](https://cloud.google.com/storage/docs/creating-buckets) and [host a static website](https://cloud.google.com/storage/docs/hosting-static-website) - -## Configuration - -Create a deployment target in your [site configuration]. The only required parameters are [`name`] and [`url`]: - -{{< code-toggle file=hugo >}} -[deployment] - [[deployment.targets]] - name = 'production' - url = 's3://my_bucket?region=us-west-1' -{{< /code-toggle >}} - -## Deploy - -To deploy to a target: - -```bash -hugo deploy [--target=] -``` - -This command syncs the contents of your local `public` directory (the default publish directory) with the destination bucket. If no target is specified, Hugo deploys to the first configured target. - -For more command-line options, see `hugo help deploy` or the [CLI documentation]. - -### File list creation - -`hugo deploy` creates local and remote file lists by traversing the local publish directory and the remote bucket. Inclusion and exclusion are determined by the deployment target's [configuration]: - -- `include`: All files are skipped by default except those that match the pattern. -- `exclude`: Files matching the pattern are skipped. - -> [!note] -> During local file list creation, Hugo skips `.DS_Store` files and hidden directories (those starting with a period, like `.git`), except for the [`.well-known`] directory, which is traversed if present. - -### File list comparison - -Hugo compares the local and remote file lists to identify necessary changes. It first compares file names. If both exist, it compares sizes and MD5 checksums. Any difference triggers a re-upload, and remote files not present locally are deleted. - -> [!note] -> Excluded remote files (due to `include`/`exclude` configuration) won't be deleted. - -The `--force` flag forces all files to be re-uploaded, even if Hugo detects no local/remote differences. - -The `--confirm` or `--dryRun` flags cause Hugo to display the detected differences and then pause or stop. - -### Synchronization - -Hugo applies the changes to the remote bucket: uploading missing or changed files and deleting remote files not present locally. Uploaded file headers are configured remotely based on the matchers configuration. - -> [!note] -> To prevent accidental data loss, Hugo will not delete more than 256 remote files by default. Use the `--maxDeletes` flag to override this limit. - -## Advanced configuration - -See [configure deployment](/configuration/deployment/). - -[`.well-known`]: https://en.wikipedia.org/wiki/Well-known_URI -[`name`]: /configuration/deployment/#name -[`url`]: /configuration/deployment/#url -[AWS]: https://aws.amazon.com -[Azure]: https://azure.microsoft.com -[CLI documentation]: /commands/hugo_deploy/ -[configuration]: /configuration/deployment/#targets-1 -[Google Cloud]: https://cloud.google.com/ -[installation]: /installation/ -[Quick Start]: /getting-started/quick-start/ -[site configuration]: /configuration/deployment/ diff --git a/docs/content/en/host-and-deploy/deploy-with-rclone.md b/docs/content/en/host-and-deploy/deploy-with-rclone.md deleted file mode 100644 index 8f641bc5d..000000000 --- a/docs/content/en/host-and-deploy/deploy-with-rclone.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: Deploy with rclone -description: Deploy your site with the rclone CLI. -categories: [] -keywords: [] -aliases: [/hosting-and-deployment/deployment-with-rclone/] ---- - -## Assumptions - -- A web host running a web server. This could be a shared hosting environment or a VPS. -- Access to your web host with any of the [protocols supported by rclone](https://rclone.org/#providers), such as SFTP. -- A functional static website built with Hugo -- Deploying from an [Rclone](https://rclone.org) compatible operating system -- You have [installed Rclone](https://rclone.org/install/). - -**NB**: You can remove ``--interactive`` in the commands below once you are comfortable with rclone, if you wish. Also, ``--gc`` and ``--minify`` are optional in the ``hugo`` commands below. - -## Getting started - -The spoiler is that you can even deploy your entire website from any compatible OS with no configuration. Using SFTP for example: - -```txt -hugo --gc --minify -rclone sync --interactive --sftp-host sftp.example.com --sftp-user www-data --sftp-ask-password public/ :sftp:www/ -``` - -## Configure Rclone for even easier usage - -The easiest way is simply to run `rclone config`. - -The [Rclone docs](https://rclone.org/docs/) provide [an example of configuring Rclone to use SFTP](https://rclone.org/sftp/). - -For the next commands, we will assume you configured a remote you named ``hugo-www`` - -The above 'spoiler' commands could become: - -```txt -hugo --gc --minify -rclone sync --interactive public/ hugo-www:www/ -``` - -After you issue the above commands (and respond to any prompts), check your website and you will see that it is deployed. diff --git a/docs/content/en/host-and-deploy/deploy-with-rsync.md b/docs/content/en/host-and-deploy/deploy-with-rsync.md deleted file mode 100644 index d073107fe..000000000 --- a/docs/content/en/host-and-deploy/deploy-with-rsync.md +++ /dev/null @@ -1,135 +0,0 @@ ---- -title: Deploy with rsync -description: Deploy your site with the rsync CLI. -categories: [] -keywords: [] -aliases: [/hosting-and-deployment/deployment-with-rsync/] ---- - -## Assumptions - -- A web host running a web server. This could be a shared hosting environment or a VPS. -- Access to your web host with SSH -- A functional static website built with Hugo - -The spoiler is that you can deploy your entire website with a command that looks like the following: - -```txt -hugo && rsync -avz --delete public/ www-data@ftp.topologix.fr:~/www/ -``` - -As you will see, we'll put this command in a shell script file, which makes building and deployment as easy as executing `./deploy`. - -## Copy Your SSH Key to your host - -To make logging in to your server more secure and less interactive, you can upload your SSH key. If you have already installed your SSH key to your server, you can move on to the next section. - -First, install the ssh client. On Debian distributions, use the following command: - -```sh {file="install-openssh.sh"} -sudo apt-get install openssh-client -``` - -Then generate your ssh key. First, create the `.ssh` directory in your home directory if it doesn't exist: - -```txt -~$ cd && mkdir .ssh & cd .ssh -``` - -Next, execute this command to generate a new keypair called `rsa_id`: - -```txt -~/.ssh/$ ssh-keygen -t rsa -q -C "For SSH" -f rsa_id -``` - -You'll be prompted for a passphrase, which is an extra layer of protection. Enter the passphrase you'd like to use, and then enter it again when prompted, or leave it blank if you don't want to have a passphrase. Not using a passphrase will let you transfer files non-interactively, as you won't be prompted for a password when you log in, but it is slightly less secure. - -To make logging in easier, add a definition for your web host to the file `~/.ssh/config` with the following command, replacing `HOST` with the IP address or hostname of your web host, and `USER` with the username you use to log in to your web host when transferring files: - -```txt -~/.ssh/$ cat >> config < - | Field | Value | - | ----------------- | ------------------------------------------------ | - | Environment | `Static Site` | - | Build Command | `hugo --gc --minify` (or your own build command) | - | Publish Directory | `./public` (or your own output directory) | - -That's it! Your site will be live on your 21YunBox URL (which looks like `yoursite.21yunbox.com`) as soon as the build is done. - -## Continuous deploys - -Now that 21YunBox is connected to your repo, it will automatically build and publish your site any time you push to GitHub. - -Every deploy automatically and instantly invalidates the CDN cache, so your users can always access the latest content on your site. - -## Custom domains - -Add your own domains to your site easily using 21YunBox's [custom domains](https://www.21cloudbox.com/dns-configuration.html) guide. - -## Support - -Click [here](https://www.21cloudbox.com/contact.html) to contact with 21YunBox' experts if you need help. diff --git a/docs/content/en/host-and-deploy/host-on-aws-amplify/amplify-step-05.png b/docs/content/en/host-and-deploy/host-on-aws-amplify/amplify-step-05.png deleted file mode 100644 index bb98d974a..000000000 Binary files a/docs/content/en/host-and-deploy/host-on-aws-amplify/amplify-step-05.png and /dev/null differ diff --git a/docs/content/en/host-and-deploy/host-on-aws-amplify/amplify-step-06.png b/docs/content/en/host-and-deploy/host-on-aws-amplify/amplify-step-06.png deleted file mode 100644 index 2e9b96e2b..000000000 Binary files a/docs/content/en/host-and-deploy/host-on-aws-amplify/amplify-step-06.png and /dev/null differ diff --git a/docs/content/en/host-and-deploy/host-on-aws-amplify/amplify-step-07.png b/docs/content/en/host-and-deploy/host-on-aws-amplify/amplify-step-07.png deleted file mode 100644 index b3260157b..000000000 Binary files a/docs/content/en/host-and-deploy/host-on-aws-amplify/amplify-step-07.png and /dev/null differ diff --git a/docs/content/en/host-and-deploy/host-on-aws-amplify/amplify-step-08.png b/docs/content/en/host-and-deploy/host-on-aws-amplify/amplify-step-08.png deleted file mode 100644 index 55e80e710..000000000 Binary files a/docs/content/en/host-and-deploy/host-on-aws-amplify/amplify-step-08.png and /dev/null differ diff --git a/docs/content/en/host-and-deploy/host-on-aws-amplify/amplify-step-09.png b/docs/content/en/host-and-deploy/host-on-aws-amplify/amplify-step-09.png deleted file mode 100644 index b422e3ad6..000000000 Binary files a/docs/content/en/host-and-deploy/host-on-aws-amplify/amplify-step-09.png and /dev/null differ diff --git a/docs/content/en/host-and-deploy/host-on-aws-amplify/amplify-step-11.png b/docs/content/en/host-and-deploy/host-on-aws-amplify/amplify-step-11.png deleted file mode 100644 index e147edfb9..000000000 Binary files a/docs/content/en/host-and-deploy/host-on-aws-amplify/amplify-step-11.png and /dev/null differ diff --git a/docs/content/en/host-and-deploy/host-on-aws-amplify/index.md b/docs/content/en/host-and-deploy/host-on-aws-amplify/index.md deleted file mode 100644 index aadb89116..000000000 --- a/docs/content/en/host-and-deploy/host-on-aws-amplify/index.md +++ /dev/null @@ -1,160 +0,0 @@ ---- -title: Host on AWS Amplify -description: Host your site on AWS Amplify. -categories: [] -keywords: [] -aliases: [/hosting-and-deployment/hosting-on-aws-amplify/] ---- - -## Prerequisites - -Please complete the following tasks before continuing: - -1. [Create an AWS account] -1. [Install Git] -1. [Create a Hugo site] and test it locally with `hugo server` -1. Commit the changes to your local repository -1. Push the local repository to your [GitHub], [GitLab], or [Bitbucket] account - -[Bitbucket]: https://bitbucket.org/product -[Create a Hugo site]: /getting-started/quick-start/ -[Create an AWS account]: https://aws.amazon.com/resources/create-account/ -[GitHub]: https://github.com -[GitLab]: https://about.gitlab.com/ -[Install Git]: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git - -## Procedure - -This procedure will enable continuous deployment from a GitHub repository. The procedure is essentially the same if you are using GitLab or Bitbucket. - -### Step 1 - -Create a file named `amplify.yml` in the root of your project. - -```sh -touch amplify.yml -``` - -### Step 2 - -Copy and paste the YAML below into the file you created. Change the application versions and time zone as needed. - -```yaml {file="amplify.yml" copy=true} -version: 1 -env: - variables: - # Application versions - DART_SASS_VERSION: 1.85.0 - GO_VERSION: 1.23.3 - HUGO_VERSION: 0.144.2 - # Time zone - TZ: America/Los_Angeles - # Cache - HUGO_CACHEDIR: ${PWD}/.hugo - NPM_CONFIG_CACHE: ${PWD}/.npm -frontend: - phases: - preBuild: - commands: - # Install Dart Sass - - curl -LJO https://github.com/sass/dart-sass/releases/download/${DART_SASS_VERSION}/dart-sass-${DART_SASS_VERSION}-linux-x64.tar.gz - - sudo tar -C /usr/local/bin -xf dart-sass-${DART_SASS_VERSION}-linux-x64.tar.gz - - rm dart-sass-${DART_SASS_VERSION}-linux-x64.tar.gz - - export PATH=/usr/local/bin/dart-sass:$PATH - - # Install Go - - curl -LJO https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz - - sudo tar -C /usr/local -xf go${GO_VERSION}.linux-amd64.tar.gz - - rm go${GO_VERSION}.linux-amd64.tar.gz - - export PATH=/usr/local/go/bin:$PATH - - # Install Hugo - - curl -LJO https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz - - sudo tar -C /usr/local/bin -xf hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz - - rm hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz - - export PATH=/usr/local/bin:$PATH - - # Check installed versions - - go version - - hugo version - - node -v - - npm -v - - sass --embedded --version - - # Install Node.JS dependencies - - "[[ -f package-lock.json || -f npm-shrinkwrap.json ]] && npm ci --prefer-offline || true" - - # https://github.com/gohugoio/hugo/issues/9810 - - git config --add core.quotepath false - build: - commands: - - hugo --gc --minify - artifacts: - baseDirectory: public - files: - - '**/*' - cache: - paths: - - ${HUGO_CACHEDIR}/**/* - - ${NPM_CONFIG_CACHE}/**/* -``` - -### Step 3 - -Commit and push the change to your GitHub repository. - -```sh -git add -A -git commit -m "Create amplify.yml" -git push -``` - -### Step 4 - -Log in to your AWS account, navigate to the [Amplify Console], then press the **Deploy an app** button. - -[Amplify Console]: https://console.aws.amazon.com/amplify/apps - -### Step 5 - -Choose a source code provider, then press the **Next** button. - - ![screen capture](amplify-step-05.png) - -### Step 6 - -Authorize AWS Amplify to access your GitHub account. - - ![screen capture](amplify-step-06.png) - -### Step 7 - -Select your personal account or relevant organization. - - ![screen capture](amplify-step-07.png) - -### Step 8 - -Authorize access to one or more repositories. - - ![screen capture](amplify-step-08.png) - -### Step 9 - -Select a repository and branch, then press the **Next** button. - - ![screen capture](amplify-step-09.png) - -### Step 10 - -On the "App settings" page, scroll to the bottom then press the **Next** button. Amplify reads the `amplify.yml` file you created in Steps 1-3 instead of using the values on this page. - -### Step 11 - -On the "Review" page, scroll to the bottom then press the **Save and deploy** button. - -### Step 12 - -When your site has finished deploying, press the **Visit deployed URL** button to view your published site. - - ![screen capture](amplify-step-11.png) diff --git a/docs/content/en/host-and-deploy/host-on-azure-static-web-apps.md b/docs/content/en/host-and-deploy/host-on-azure-static-web-apps.md deleted file mode 100644 index 68fe145ab..000000000 --- a/docs/content/en/host-and-deploy/host-on-azure-static-web-apps.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Host on Azure Static Web Apps -description: Host your site on Azure Static Web Apps. -categories: [] -keywords: [] -aliases: [/hosting-and-deployment/hosting-on-azure-static-web-apps/] ---- - -You can create and deploy a Hugo web application to Azure Static Web Apps. The final result is a new Azure Static Web App with associated GitHub Actions that give you control over how the app is built and published. You'll learn how to create a Hugo app, set up an Azure Static Web App and deploy the Hugo app to Azure. - -Here's the tutorial on how to [Publish a Hugo site to Azure Static Web Apps](https://docs.microsoft.com/en-us/azure/static-web-apps/publish-hugo). diff --git a/docs/content/en/host-and-deploy/host-on-cloudflare-pages.md b/docs/content/en/host-and-deploy/host-on-cloudflare-pages.md deleted file mode 100644 index 1c3627288..000000000 --- a/docs/content/en/host-and-deploy/host-on-cloudflare-pages.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Host on Cloudflare Pages -description: Host your site on Cloudflare Pages. -categories: [] -keywords: [] -aliases: [/hosting-and-deployment/hosting-on-cloudflare-pages/] ---- - -[Cloudflare Pages](https://developers.cloudflare.com/pages/) are super fast, always up-to-date, and deployed directly from your [Git provider](https://developers.cloudflare.com/pages/get-started/#connect-your-git-provider-to-pages). - -Cloudflare Pages docs have a detailed tutorial on [how to deploy a Hugo site](https://developers.cloudflare.com/pages/framework-guides/deploy-a-hugo-site/). diff --git a/docs/content/en/host-and-deploy/host-on-codeberg-pages.md b/docs/content/en/host-and-deploy/host-on-codeberg-pages.md deleted file mode 100644 index cd137c420..000000000 --- a/docs/content/en/host-and-deploy/host-on-codeberg-pages.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -title: Host on Codeberg Pages -description: Host your site on Codeberg Pages. -categories: [] -keywords: [] -aliases: [/hosting-and-deployment/hosting-on-codeberg/] ---- - -## Assumptions - -- Working familiarity with [Git] for version control -- Completion of the Hugo [Quick Start] -- A [Codeberg account] -- A Hugo website on your local machine that you are ready to publish - -[Codeberg account]: https://codeberg.org/user/login/ -[Git]: https://git-scm.com/ -[Quick Start]: /getting-started/quick-start/ - -Any and all mentions of `` refer to your actual Codeberg username and must be substituted accordingly. Likewise, `` represents your actual website name. - -## BaseURL - -The [`baseURL`] in your site configuration must reflect the full URL provided by Codeberg Pages if using the default address (e.g. `https://.codeberg.page/`). If you want to use another domain, follow the instructions in the [custom domain section] of the official documentation. - -[`baseURL`]: /configuration/all/#baseurl -[custom domain section]: https://docs.codeberg.org/codeberg-pages/using-custom-domain/ - -For more details regarding the URL of your deployed website, refer to Codeberg Pages' [quickstart instructions]. - -[quickstart instructions]: https://codeberg.page/ - -## Manual deployment - -Create a public repository on your Codeberg account titled `pages` or create a branch of the same name in an existing public repository. Finally, push the contents of Hugo's output directory (by default, `public`) to it. Here's an example: - -```sh -# build the website -hugo - -# access the output directory -cd public - -# initialize new git repository -git init - -# commit and push code to main branch -git add . -git commit -m "Initial commit" -git remote add origin https://codeberg.org//pages.git -git push -u origin main -``` - -## Automated deployment - -In order to automatically deploy your Hugo website, you need to have or [request] access to Codeberg's CI, as well as add a `.woodpecker.yaml` file in the root of your project. A template and additional instructions are available in the official [examples repository]. - -[request]: https://codeberg.org/Codeberg-e.V./requests/issues/new?template=ISSUE_TEMPLATE%2fWoodpecker-CI.yaml -[examples repository]: https://codeberg.org/Codeberg-CI/examples/src/branch/main/Hugo/.woodpecker.yaml - -In this case, you must create a public repository on Codeberg (e.g. ``) and push your local project to it. Here's an example: - -```sh -# initialize new git repository -git init - -# add /public directory to our .gitignore file -echo "/public" >> .gitignore - -# commit and push code to main branch -git add . -git commit -m "Initial commit" -git remote add origin https://codeberg.org//.git -git push -u origin main -``` - -Your project will then be built and deployed by Codeberg's CI. - -## Other resources - -- [Codeberg Pages](https://codeberg.page/) -- [Codeberg Pages official documentation](https://docs.codeberg.org/codeberg-pages/) diff --git a/docs/content/en/host-and-deploy/host-on-firebase.md b/docs/content/en/host-and-deploy/host-on-firebase.md deleted file mode 100644 index 267c8d127..000000000 --- a/docs/content/en/host-and-deploy/host-on-firebase.md +++ /dev/null @@ -1,106 +0,0 @@ ---- -title: Host on Firebase -description: Host your site on Firebase. -categories: [] -keywords: [] -aliases: [/hosting-and-deployment/hosting-on-firebase/] ---- - -## Assumptions - -1. You have an account with [Firebase][signup]. (If you don't, you can sign up for free using your Google account.) -1. You have completed the [Quick Start] or have a completed Hugo website ready for deployment. - -## Initial setup - -Go to the [Firebase console][console] and create a new project (unless you already have a project). You will need to globally install `firebase-tools` (node.js): - -```sh -npm install -g firebase-tools -``` - -Log in to Firebase (setup on your local machine) using `firebase login`, which opens a browser where you can select your account. Use `firebase logout` in case you are already logged in but to the wrong account. - -```sh -firebase login -``` - -In the root of your Hugo project, initialize the Firebase project with the `firebase init` command: - -```sh -firebase init -``` - -From here: - -1. Choose Hosting in the feature question -1. Choose the project you just set up -1. Accept the default for your database rules file -1. Accept the default for the publish directory, which is `public` -1. Choose "No" in the question if you are deploying a single-page app - -## Using Firebase & GitHub CI/CD - -In new versions of Firebase, some other questions apply: - -6. Set up automatic builds and deploys with GitHub? - -Here you will be redirected to login in your GitHub account to get permissions. Confirm. - -7. For which GitHub repository would you like to set up a GitHub workflow? (format: user/repository) - -Include the repository you will use in the format above (Account/Repo) -Firebase script with retrieve credentials, create a service account you can later manage in your GitHub settings. - -8. Set up the workflow to run a build script before every deploy? - -Here is your opportunity to include some commands before you run the deploy. - -9. Set up automatic deployment to your site's live channel when a PR is merged? - -You can let in the default option (main) - -After that Firebase has been set in your project with [CI/CD](g). After that run: - -```sh -hugo && firebase deploy -``` - -With this you will have the app initialized manually. After that you can manage and fix your GitHub workflow from: https://github.com/your-account/your-repo/actions - -Don't forget to update your static pages before push! - -## Manual deploy - -To deploy your Hugo site, execute the `firebase deploy` command, and your site will be up in no time: - -```sh -hugo && firebase deploy -``` - -## CI setup (other tools) - -You can generate a deploy token using - -```sh -firebase login:ci -``` - -You can also set up your CI and add the token to a private variable like `$FIREBASE_DEPLOY_TOKEN`. - -> [!note] -> This is a private secret and it should not appear in a public repository. Make sure you understand your chosen CI and that it's not visible to others. - -You can then add a step in your build to do the deployment using the token: - -```sh -firebase deploy --token $FIREBASE_DEPLOY_TOKEN -``` - -## Reference links - -- [Firebase CLI Reference](https://firebase.google.com/docs/cli/#administrative_commands) - -[console]: https://console.firebase.google.com/ -[Quick Start]: /getting-started/quick-start/ -[signup]: https://console.firebase.google.com/ diff --git a/docs/content/en/host-and-deploy/host-on-github-pages/gh-pages-1.png b/docs/content/en/host-and-deploy/host-on-github-pages/gh-pages-1.png deleted file mode 100644 index 29912f25c..000000000 Binary files a/docs/content/en/host-and-deploy/host-on-github-pages/gh-pages-1.png and /dev/null differ diff --git a/docs/content/en/host-and-deploy/host-on-github-pages/gh-pages-2.png b/docs/content/en/host-and-deploy/host-on-github-pages/gh-pages-2.png deleted file mode 100644 index 0050d33e2..000000000 Binary files a/docs/content/en/host-and-deploy/host-on-github-pages/gh-pages-2.png and /dev/null differ diff --git a/docs/content/en/host-and-deploy/host-on-github-pages/gh-pages-3.png b/docs/content/en/host-and-deploy/host-on-github-pages/gh-pages-3.png deleted file mode 100644 index d2904cae1..000000000 Binary files a/docs/content/en/host-and-deploy/host-on-github-pages/gh-pages-3.png and /dev/null differ diff --git a/docs/content/en/host-and-deploy/host-on-github-pages/gh-pages-4.png b/docs/content/en/host-and-deploy/host-on-github-pages/gh-pages-4.png deleted file mode 100644 index 75774462b..000000000 Binary files a/docs/content/en/host-and-deploy/host-on-github-pages/gh-pages-4.png and /dev/null differ diff --git a/docs/content/en/host-and-deploy/host-on-github-pages/gh-pages-5.png b/docs/content/en/host-and-deploy/host-on-github-pages/gh-pages-5.png deleted file mode 100644 index efe26129a..000000000 Binary files a/docs/content/en/host-and-deploy/host-on-github-pages/gh-pages-5.png and /dev/null differ diff --git a/docs/content/en/host-and-deploy/host-on-github-pages/index.md b/docs/content/en/host-and-deploy/host-on-github-pages/index.md deleted file mode 100644 index 7c3201099..000000000 --- a/docs/content/en/host-and-deploy/host-on-github-pages/index.md +++ /dev/null @@ -1,232 +0,0 @@ ---- -title: Host on GitHub Pages -description: Host your site on GitHub Pages. -categories: [] -keywords: [] -aliases: [/hosting-and-deployment/hosting-on-github/] ---- - -## Prerequisites - -Please complete the following tasks before continuing: - -1. [Create a GitHub account] -1. [Install Git] -1. [Create a Hugo site] and test it locally with `hugo server`. - -## Types of sites - -There are three types of GitHub Pages sites: project, user, and organization. Project sites are connected to a specific project hosted on GitHub. User and organization sites are connected to a specific account on GitHub.com. - -> [!note] -> See the [GitHub Pages documentation] to understand the requirements for repository ownership and naming. - -## Procedure - -### Step 1 - -Create a GitHub repository. - -### Step 2 - -Push your local repository to GitHub. - -### Step 3 - -Visit your GitHub repository. From the main menu choose **Settings** > **Pages**. In the center of your screen you will see this: - -![screen capture](gh-pages-1.png) -{style="max-width: 280px"} - -### Step 4 - -Change the **Source** to `GitHub Actions`. The change is immediate; you do not have to press a Save button. - -![screen capture](gh-pages-2.png) -{style="max-width: 280px"} - -### Step 5 - -In your site configuration, change the location of the image cache to the [`cacheDir`] as shown below: - -{{< code-toggle file=hugo >}} -[caches.images] -dir = ":cacheDir/images" -{{< /code-toggle >}} - -See [configure file caches] for more information. - -### Step 6 - -Create a file named `hugo.yaml` in a directory named `.github/workflows`. - -```text -mkdir -p .github/workflows -touch .github/workflows/hugo.yaml -``` - -### Step 7 - -> [!note] -> The workflow below ensures Hugo's `cacheDir` is persistent, preserving modules, processed images, and [`resources.GetRemote`] data between builds. - -Copy and paste the YAML below into the file you created. Change the branch name and Hugo version as needed. - -```yaml {file=".github/workflows/hugo.yaml" copy=true} -# Sample workflow for building and deploying a Hugo site to GitHub Pages -name: Deploy Hugo site to Pages - -on: - # Runs on pushes targeting the default branch - push: - branches: - - main - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write - -# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. -# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. -concurrency: - group: "pages" - cancel-in-progress: false - -# Default to bash -defaults: - run: - shell: bash - -jobs: - # Build job - build: - runs-on: ubuntu-latest - env: - HUGO_VERSION: 0.145.0 - HUGO_ENVIRONMENT: production - TZ: America/Los_Angeles - steps: - - name: Install Hugo CLI - run: | - wget -O ${{ runner.temp }}/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb \ - && sudo dpkg -i ${{ runner.temp }}/hugo.deb - - name: Install Dart Sass - run: sudo snap install dart-sass - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: recursive - fetch-depth: 0 - - name: Setup Pages - id: pages - uses: actions/configure-pages@v5 - - name: Install Node.js dependencies - run: "[[ -f package-lock.json || -f npm-shrinkwrap.json ]] && npm ci || true" - - name: Cache Restore - id: cache-restore - uses: actions/cache/restore@v4 - with: - path: | - ${{ runner.temp }}/hugo_cache - key: hugo-${{ github.run_id }} - restore-keys: - hugo- - - name: Configure Git - run: git config core.quotepath false - - name: Build with Hugo - run: | - hugo \ - --gc \ - --minify \ - --baseURL "${{ steps.pages.outputs.base_url }}/" \ - --cacheDir "${{ runner.temp }}/hugo_cache" - - name: Cache Save - id: cache-save - uses: actions/cache/save@v4 - with: - path: | - ${{ runner.temp }}/hugo_cache - key: ${{ steps.cache-restore.outputs.cache-primary-key }} - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: ./public - - # Deployment job - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - needs: build - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 -``` - -### Step 8 - -Commit and push the change to your GitHub repository. - -```sh -git add -A -git commit -m "Create hugo.yaml" -git push -``` - -### Step 9 - -From GitHub's main menu, choose **Actions**. You will see something like this: - -![screen capture](gh-pages-3.png) -{style="max-width: 350px"} - -### Step 10 - -When GitHub has finished building and deploying your site, the color of the status indicator will change to green. - -![screen capture](gh-pages-4.png) -{style="max-width: 350px"} - -### Step 11 - -Click on the commit message as shown above. You will see this: - -![screen capture](gh-pages-5.png) -{style="max-width: 611px"} - -Under the deploy step, you will see a link to your live site. - -In the future, whenever you push a change from your local repository, GitHub will rebuild your site and deploy the changes. - -## Customize the workflow - -The example workflow above includes this step, which typically takes 10‑15 seconds: - -```yaml -- name: Install Dart Sass - run: sudo snap install dart-sass -``` - -You may remove this step if your site, themes, and modules do not transpile Sass to CSS using the [Dart Sass] transpiler. - -## Other resources - -- [Learn more about GitHub Actions](https://docs.github.com/en/actions) -- [Caching dependencies to speed up workflows](https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows) -- [Manage a custom domain for your GitHub Pages site](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/about-custom-domains-and-github-pages) - -[Create a GitHub account]: https://github.com/signup -[Create a Hugo site]: /getting-started/quick-start/ -[Dart Sass]: /functions/css/sass/#dart-sass -[GitHub Pages documentation]: https://docs.github.com/en/pages/getting-started-with-github-pages/about-github-pages#types-of-github-pages-sites -[Install Git]: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git -[`cacheDir`]: /configuration/all/#cachedir -[`resources.GetRemote`]: /functions/resources/getremote/ -[configure file caches]: /configuration/caches/ diff --git a/docs/content/en/host-and-deploy/host-on-gitlab-pages.md b/docs/content/en/host-and-deploy/host-on-gitlab-pages.md deleted file mode 100644 index 4750b0ff3..000000000 --- a/docs/content/en/host-and-deploy/host-on-gitlab-pages.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -title: Host on GitLab Pages -description: Host your site on GitLab Pages. -categories: [] -keywords: [] -aliases: [/hosting-and-deployment/hosting-on-gitlab/] ---- - -## Assumptions - -- Working familiarity with Git for version control -- Completion of the Hugo [Quick Start] -- A [GitLab account](https://gitlab.com/users/sign_in) -- A Hugo website on your local machine that you are ready to publish - -## BaseURL - -The `baseURL` in your [site configuration](/configuration/) must reflect the full URL of your GitLab pages repository if you are using the default GitLab Pages URL (e.g., `https://.gitlab.io//`) and not a custom domain. - -## Configure GitLab CI/CD - -Define your [CI/CD](g) jobs by creating a `.gitlab-ci.yml` file in the root of your project. - -```yaml {file=".gitlab-ci.yml" copy=true} -variables: - DART_SASS_VERSION: 1.87.0 - GIT_DEPTH: 0 - GIT_STRATEGY: clone - GIT_SUBMODULE_STRATEGY: recursive - HUGO_VERSION: 0.146.7 - NODE_VERSION: 22.x - TZ: America/Los_Angeles -image: - name: golang:1.24.2-bookworm - -pages: - script: - # Install brotli - - apt-get update - - apt-get install -y brotli - # Install Dart Sass - - curl -LJO https://github.com/sass/dart-sass/releases/download/${DART_SASS_VERSION}/dart-sass-${DART_SASS_VERSION}-linux-x64.tar.gz - - tar -xf dart-sass-${DART_SASS_VERSION}-linux-x64.tar.gz - - cp -r dart-sass/ /usr/local/bin - - rm -rf dart-sass* - - export PATH=/usr/local/bin/dart-sass:$PATH - # Install Hugo - - curl -LJO https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb - - apt-get install -y ./hugo_extended_${HUGO_VERSION}_linux-amd64.deb - - rm hugo_extended_${HUGO_VERSION}_linux-amd64.deb - # Install Node.js - - curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION} | bash - - - apt-get install -y nodejs - # Install Node.js dependencies - - "[[ -f package-lock.json || -f npm-shrinkwrap.json ]] && npm ci || true" - # Configure Git - - git config core.quotepath false - # Build - - hugo --gc --minify --baseURL ${CI_PAGES_URL} - # Compress - - find public -type f -regex '.*\.\(css\|html\|js\|txt\|xml\)$' -exec gzip -f -k {} \; - - find public -type f -regex '.*\.\(css\|html\|js\|txt\|xml\)$' -exec brotli -f -k {} \; - artifacts: - paths: - - public - rules: - - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH -``` - -## Push your Hugo website to GitLab - -Next, create a new repository on GitLab. It is *not* necessary to make the repository public. In addition, you might want to add `/public` to your .gitignore file, as there is no need to push compiled assets to GitLab or keep your output website in version control. - -```sh -# initialize new git repository -git init - -# add /public directory to our .gitignore file -echo "/public" >> .gitignore - -# commit and push code to master branch -git add . -git commit -m "Initial commit" -git remote add origin https://gitlab.com/YourUsername/your-hugo-site.git -git push -u origin master -``` - -## Wait for your page to build - -That's it! You can now follow the CI agent building your page at `https://gitlab.com///pipelines`. - -After the build has passed, your new website is available at `https://.gitlab.io//`. - -## Next steps - -GitLab supports using custom CNAME's and TLS certificates. For more details on GitLab Pages, see the [GitLab Pages setup documentation](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/). - -[Quick Start]: /getting-started/quick-start/ diff --git a/docs/content/en/host-and-deploy/host-on-keycdn/index.md b/docs/content/en/host-and-deploy/host-on-keycdn/index.md deleted file mode 100644 index 828e250c6..000000000 --- a/docs/content/en/host-and-deploy/host-on-keycdn/index.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -title: Host on KeyCDN -description: Host your site on KeyCDN. -categories: [] -keywords: [] -aliases: [/hosting-and-deployment/hosting-on-keycdn/] ---- - -[KeyCDN](https://www.keycdn.com/) provides a multitude of features to help accelerate and secure your Hugo site globally including Brotli compression, Let's Encrypt support, Origin Shield, and more. - -## Assumptions - -- You already have a Hugo page configured -- You have a GitLab account -- You have a KeyCDN account - -## Create a KeyCDN Pull Zone - -The first step will be to log in to your KeyCDN account and create a new zone. Name this whatever you like and select the [Pull Zone](https://www.keycdn.com/support/create-a-pull-zone/) option. As for the origin URL, your site will be running on [GitLab Pages](https://docs.gitlab.com/ee/user/project/pages/getting_started_part_one.html) with a URL of `https://youruser.gitlab.io/reponame/`. Use this as the Origin URL. - -![Screenshot of KeyCDN's pull zone creation page](keycdn-pull-zone.png) - -While the origin location doesn't exist yet, you will need to use your new Zone URL address (or [Zone Alias](https://www.keycdn.com/support/create-a-zone-alias/)) in the `.gitlab-ci.yml` file that will be uploaded to your GitLab project. - -Ensure that you use your Zone URL or Zone alias as the `BASEURL` variable in the example below. This will be the user-visible website address. - -## Configure Your .gitlab-ci.yml File - -Your `.gitlab-ci.yml` file should look similar to the example below. Be sure to modify any variables that are specific to your setup. - -```yml -image: alpine:latest - -variables: - BASEURL: "https://cipull-7bb7.kxcdn.com/" - HUGO_VERSION: "0.26" - HUGO_CHECKSUM: "67e4ba5ec2a02c8164b6846e30a17cc765b0165a5b183d5e480149baf54e1a50" - KEYCDN_ZONE_ID: "75544" - -before_script: - - apk update - - apk add curl - -pages: - stage: deploy - script: - - apk add git - - git submodule update --init - - curl -sSL https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_${HUGO_VERSION}_Linux-64bit.tar.gz -o /tmp/hugo.tar.gz - - echo "${HUGO_CHECKSUM} /tmp/hugo.tar.gz" | sha256sum -c - - tar xf /tmp/hugo.tar.gz hugo -C /tmp/ && cp /tmp/hugo /usr/bin - - hugo --baseURL ${BASEURL} - - curl "https://api.keycdn.com/zones/purge/${KEYCDN_ZONE_ID}.json" -u "${KEYCDN_API_KEY}:" - artifacts: - paths: - - public - only: - - master -``` - -Using this integration method, you will have to specify the Zone ID and your [KeyCDN API](https://www.keycdn.com/api) key as secret variables. To do this, navigate to the top-left menu bar in GitLab and select Projects. Then, select your project and click on the Settings page. Finally, select Pipelines from the sub-menu and scroll down to the Secret Variable section. - -The Secret Variable for your Zone ID should look similar to: - -![Screenshot of setting the Zone ID secret variable](secret-zone-id.png) - -While the Secret Variable for your API Key will look similar to: - -![Screenshot of setting the API Key secret variable](secret-api-key.png) - -While not strictly required, providing your Zone ID and API key is recommended for purging your zone. Without them, the CDN may continue serving outdated versions of your assets for an extended period. - -## Push your changes to GitLab - -Now it's time to push the newly created repository to GitLab: - -```sh -git remote add origin git@gitlab.com:youruser/ci-example.git -git push -u origin master -``` - -You can watch the progress and CI job output in your GitLab project under “Pipelines”. - -After verifying your CI job ran without issues, first check that your GitLab page shows up under `https://youruser.gitlab.io/reponame/` (it might look broken depending on your browser settings as all links point to your KeyCDN zone---don't worry about that) and then by heading to whatever Zone alias / Zone URL you defined. - -To learn more about Hugo hosting options with KeyCDN, check out the complete [Hugo hosting with KeyCDN integration guide](https://www.keycdn.com/support/hugo-hosting/). diff --git a/docs/content/en/host-and-deploy/host-on-keycdn/keycdn-pull-zone.png b/docs/content/en/host-and-deploy/host-on-keycdn/keycdn-pull-zone.png deleted file mode 100644 index 7cde4a6a2..000000000 Binary files a/docs/content/en/host-and-deploy/host-on-keycdn/keycdn-pull-zone.png and /dev/null differ diff --git a/docs/content/en/host-and-deploy/host-on-keycdn/secret-api-key.png b/docs/content/en/host-and-deploy/host-on-keycdn/secret-api-key.png deleted file mode 100644 index ad99341d5..000000000 Binary files a/docs/content/en/host-and-deploy/host-on-keycdn/secret-api-key.png and /dev/null differ diff --git a/docs/content/en/host-and-deploy/host-on-keycdn/secret-zone-id.png b/docs/content/en/host-and-deploy/host-on-keycdn/secret-zone-id.png deleted file mode 100644 index 2e5cf5f41..000000000 Binary files a/docs/content/en/host-and-deploy/host-on-keycdn/secret-zone-id.png and /dev/null differ diff --git a/docs/content/en/host-and-deploy/host-on-netlify/index.md b/docs/content/en/host-and-deploy/host-on-netlify/index.md deleted file mode 100644 index 4c89a6c1e..000000000 --- a/docs/content/en/host-and-deploy/host-on-netlify/index.md +++ /dev/null @@ -1,146 +0,0 @@ ---- -title: Host on Netlify -description: Host your site on Netlify. -categories: [] -keywords: [] -aliases: [/hosting-and-deployment/hosting-on-netlify/] ---- - -## Prerequisites - -Please complete the following tasks before continuing: - -1. [Create a Netlify account] -1. [Install Git] -1. [Create a Hugo site] and test it locally with `hugo server` -1. Commit the changes to your local repository -1. Push the local repository to your [GitHub], [GitLab], or [Bitbucket] account - -[Bitbucket]: https://bitbucket.org/product -[Create a Hugo site]: /getting-started/quick-start/ -[Create a Netlify account]: https://app.netlify.com/signup -[GitHub]: https://github.com -[GitLab]: https://about.gitlab.com/ -[Install Git]: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git - -## Procedure - -This procedure will enable continuous deployment from a GitHub repository. The procedure is essentially the same if you are using GitLab or Bitbucket. - -### Step 1 - -Log in to your Netlify account, navigate to the Sites page, press the **Add new site** button, and choose "Import an existing project" from the dropdown menu. - -### Step 2 - -Select your deployment method. - - ![screen capture](netlify-step-02.png) - -### Step 3 - -Authorize Netlify to connect with your GitHub account by pressing the **Authorize Netlify** button. - -![screen capture](netlify-step-03.png) - -### Step 4 - -Press the **Configure Netlify on GitHub** button. - -![screen capture](netlify-step-04.png) - -### Step 5 - -Install the Netlify app by selecting your GitHub account. - -![screen capture](netlify-step-05.png) - -### Step 6 - -Press the **Install** button. - -![screen capture](netlify-step-06.png) - -### Step 7 - -Click on the site's repository from the list. - -![screen capture](netlify-step-07.png) - -### Step 8 - -Set the site name and branch from which to deploy. - -![screen capture](netlify-step-08.png) - -### Step 9 - -Define the build settings, press the **Add environment variables** button, then press the **New variable** button. - -![screen capture](netlify-step-09.png) - -### Step 10 - -Create a new environment variable named `HUGO_VERSION` and set the value to the [latest version]. - -[latest version]: https://github.com/gohugoio/hugo/releases/latest - -![screen capture](netlify-step-10.png) - -### Step 11 - -Press the "Deploy my new site" button at the bottom of the page. - -![screen capture](netlify-step-11.png) - -### Step 12 - -At the bottom of the screen, wait for the deploy to complete, then click on the deploy log entry. - -![screen capture](netlify-step-12.png) - -### Step 13 - -Press the **Open production deploy** button to view the live site. - -![screen capture](netlify-step-13.png) - -## Configuration file - -In the procedure above we configured our site using the Netlify user interface. Most site owners find it easier to use a configuration file checked into source control. - -Create a new file named netlify.toml in the root of your project directory. In its simplest form, the configuration file might look like this: - -```toml {file="netlify.toml"} -[build.environment] -GO_VERSION = "1.24" -HUGO_VERSION = "0.146.7" -NODE_VERSION = "22" -TZ = "America/Los_Angeles" - -[build] -publish = "public" -command = "git config core.quotepath false && hugo --gc --minify" -``` - -If your site requires Dart Sass to transpile Sass to CSS, the configuration file should look something like this: - -```toml {file="netlify.toml"} -[build.environment] -DART_SASS_VERSION = "1.87.0" -GO_VERSION = "1.24" -HUGO_VERSION = "0.146.7" -NODE_VERSION = "22" -TZ = "America/Los_Angeles" - -[build] -publish = "public" -command = """\ - curl -LJO https://github.com/sass/dart-sass/releases/download/${DART_SASS_VERSION}/dart-sass-${DART_SASS_VERSION}-linux-x64.tar.gz && \ - tar -xf dart-sass-${DART_SASS_VERSION}-linux-x64.tar.gz && \ - rm dart-sass-${DART_SASS_VERSION}-linux-x64.tar.gz && \ - export PATH=/opt/build/repo/dart-sass:$PATH && \ - git config core.quotepath false && \ - hugo --gc --minify \ - """ -``` diff --git a/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-02.png b/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-02.png deleted file mode 100644 index 31fceff27..000000000 Binary files a/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-02.png and /dev/null differ diff --git a/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-03.png b/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-03.png deleted file mode 100644 index 7b98e0b8f..000000000 Binary files a/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-03.png and /dev/null differ diff --git a/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-04.png b/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-04.png deleted file mode 100644 index 31304894b..000000000 Binary files a/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-04.png and /dev/null differ diff --git a/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-05.png b/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-05.png deleted file mode 100644 index 6d6eef01d..000000000 Binary files a/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-05.png and /dev/null differ diff --git a/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-06.png b/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-06.png deleted file mode 100644 index 1b766a785..000000000 Binary files a/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-06.png and /dev/null differ diff --git a/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-07.png b/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-07.png deleted file mode 100644 index 7bb3b6eca..000000000 Binary files a/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-07.png and /dev/null differ diff --git a/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-08.png b/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-08.png deleted file mode 100644 index df8e9e59f..000000000 Binary files a/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-08.png and /dev/null differ diff --git a/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-09.png b/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-09.png deleted file mode 100644 index 3f925accc..000000000 Binary files a/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-09.png and /dev/null differ diff --git a/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-10.png b/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-10.png deleted file mode 100644 index e9196d0ce..000000000 Binary files a/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-10.png and /dev/null differ diff --git a/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-11.png b/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-11.png deleted file mode 100644 index 2ac2b08af..000000000 Binary files a/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-11.png and /dev/null differ diff --git a/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-12.png b/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-12.png deleted file mode 100644 index e251305a4..000000000 Binary files a/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-12.png and /dev/null differ diff --git a/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-13.png b/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-13.png deleted file mode 100644 index f955f6369..000000000 Binary files a/docs/content/en/host-and-deploy/host-on-netlify/netlify-step-13.png and /dev/null differ diff --git a/docs/content/en/host-and-deploy/host-on-render.md b/docs/content/en/host-and-deploy/host-on-render.md deleted file mode 100644 index ac486fcbf..000000000 --- a/docs/content/en/host-and-deploy/host-on-render.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -title: Host on Render -description: Host your on Render. -categories: [] -keywords: [] -aliases: [/hosting-and-deployment/hosting-on-render/] ---- - -## Introduction - -[Render](https://render.com) is a fully-managed cloud platform where you can host static sites, backend APIs, databases, cron jobs, and all your other apps in one place. - -Static sites are **completely free** on Render and include the following: - -- Continuous, automatic builds & deploys from [GitHub](https://render.com/docs/github) and [GitLab](https://render.com/docs/gitlab). -- Automatic SSL certificates through [Let's Encrypt](https://letsencrypt.org). -- Instant cache invalidation with a lightning fast, global CDN. -- Unlimited collaborators. -- Unlimited [custom domains](https://render.com/docs/custom-domains). -- Automatic [Brotli compression](https://en.wikipedia.org/wiki/Brotli) for faster sites. -- Native HTTP/2 support. -- [Pull Request Previews](https://render.com/docs/pull-request-previews). -- Automatic HTTP → HTTPS redirects. -- Custom URL redirects and rewrites. - -## Assumptions - -- You have an account with GitHub or GitLab. -- You have completed the [Quick Start] or have a Hugo website you are ready to deploy and share with the world. -- You have a Render account. You can sign up at https://render.com/register. - -## Deployment - -You can set up a Hugo site on Render in two quick steps: - -1. Create a new **Static Site** on Render, and give Render permission to access your GitHub/GitLab repo. -1. Use the following values during creation: - -Field | Value -------------------- | ------------------- -**Build Command** | `hugo --gc --minify` (or your own build command) -**Publish Directory** | `public` (or your own output directory) - -That's it! Your site will be live on your Render URL (which looks like `yoursite.onrender.com`) as soon as the build is done. - -## Continuous deploys - -Now that Render is connected to your repo, it will **automatically build and publish your site** any time you push to your GitHub/GitLab. - -You can choose to disable auto deploys under the **Settings** section for your site and deploy it manually from the Render dashboard. - -## CDN and cache invalidation - -Render hosts your site on a global, lightning fast CDN which ensures the fastest possible download times for all your users across the globe. - -Every deploy automatically and instantly invalidates the CDN cache, so your users can always access the latest content on your site. - -## Custom domains - -Add your own domains to your site easily using Render's [custom domains](https://render.com/docs/custom-domains) guide. - -## Pull Request previews - -With Pull Request (PR) previews, you can visualize changes introduced in a pull request instead of simply relying on code reviews. - -Once enabled, every PR for your site will automatically generate a new static site based on the code in the PR. It will have its own URL, and it will be deleted automatically when the PR is closed. - -Read more about [Pull Request Previews](https://render.com/docs/pull-request-previews) on Render. - -## Hugo themes - -Render automatically downloads all Git submodules defined in your Git repo on every build. This way Hugo themes added as submodules work as expected. - -## Support - -Chat with Render developers at https://render.com/chat or email `support@render.com` if you need help. - -[Quick Start]: /getting-started/quick-start/ diff --git a/docs/content/en/host-and-deploy/host-on-sourcehut-pages.md b/docs/content/en/host-and-deploy/host-on-sourcehut-pages.md deleted file mode 100644 index 6b092fbf2..000000000 --- a/docs/content/en/host-and-deploy/host-on-sourcehut-pages.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -title: Host on SourceHut Pages -description: Host your site on SourceHut Pages. -categories: [] -keywords: [] -aliases: [/hosting-and-deployment/hosting-on-sourcehut/] ---- - -## Assumptions - -- Working familiarity with [Git] or [Mercurial] for version control -- Completion of the Hugo [Quick Start] -- A [SourceHut account] -- A Hugo website on your local machine that you are ready to publish - -[Git]: https://git-scm.com/ -[Mercurial]: https://www.mercurial-scm.org/ -[SourceHut account]: https://meta.sr.ht/login -[Quick Start]: /getting-started/quick-start/ - -Any and all mentions of `` refer to your actual SourceHut username and must be substituted accordingly. - -## BaseURL - -The [`baseURL`] in your site configuration must reflect the full URL provided by SourceHut Pages if you are using the default address (e.g. `https://.srht.site/`). If you want to use another domain, check the [custom domain section] of the official documentation. - -[`baseURL`]: /configuration/all/#baseurl -[custom domain section]: https://srht.site/custom-domains - -## Manual deployment - -This method does not require a paid account. To proceed you will need to create a [SourceHut personal access token] and install and configure the [hut]CLI tool: - -[SourceHut personal access token]: https://meta.sr.ht/oauth2/personal-token/ -[hut]: https://sr.ht/~xenrox/hut/ - -```sh -hugo -tar -C public -cvz . > site.tar.gz -hut init -hut pages publish -d .srht.site site.tar.gz -``` - -A TLS certificate will be automatically obtained for you, and your new website will be available at `https://.srht.site/` (or the provided custom domain). - -## Automated deployment - -This method requires a paid account and relies on the SourceHut build system. - -First, define your [build manifest] by creating a `.build.yml` file in the root of your project. The following is a bare-bones template: - -[build manifest]: https://man.sr.ht/builds.sr.ht/#build-manifests - -```yaml {file=".build.yml" copy=true} -image: alpine/edge -packages: - - hugo - - hut -oauth: pages.sr.ht/PAGES:RW -environment: - site: .srht.site -tasks: -- package: | - cd $site - hugo - tar -C public -cvz . > ../site.tar.gz -- upload: | - hut pages publish -d $site site.tar.gz -``` - -Now what's left is creating a repository titled `.srht.site` (or your custom domain, if applicable) and pushing your local project. Here's an example using Git: - -```sh -# initialize new git repository -git init - -# add /public directory to our .gitignore file -echo "/public" >> .gitignore - -# commit and push code to main branch -git add . -git commit -m "Initial commit" -git remote add origin https://git.sr.ht/~/.srht.site -git push -u origin main -``` - -You can now follow the build progress of your page at `https://builds.sr.ht/`. - -After the build has passed, a TLS certificate will be automatically obtained for you and your new website will be available at `https://.srht.site/` (or the provided custom domain). - -## Other resources - -- [SourceHut Pages](https://srht.site/) -- [SourceHut Builds user manual](https://man.sr.ht/builds.sr.ht/) diff --git a/docs/content/en/hugo-modules/_index.md b/docs/content/en/hugo-modules/_index.md deleted file mode 100644 index 7a538ea15..000000000 --- a/docs/content/en/hugo-modules/_index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Hugo Modules -description: Use Hugo Modules to manage the content, presentation, and behavior of your site. -categories: [] -keywords: [] -weight: 10 -aliases: [/themes/overview/,/themes/] ---- diff --git a/docs/content/en/hugo-modules/introduction.md b/docs/content/en/hugo-modules/introduction.md deleted file mode 100644 index b45607dc0..000000000 --- a/docs/content/en/hugo-modules/introduction.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: Introduction -description: A brief introduction to Hugo Modules. -categories: [] -keywords: [] -weight: 10 ---- - -Hugo uses modules as its fundamental organizational units. A module can be a full Hugo project or a smaller, reusable piece providing one or more of Hugo's seven component types: static files, content, layouts, data, assets, internationalization (i18n) resources, and archetypes. - -Modules are combinable in any arrangement, and external directories (including those from non-Hugo projects) can be mounted, effectively creating a single, unified file system. - -Some example projects: - -[https://github.com/bep/docuapi](https://github.com/bep/docuapi) -: A theme that has been ported to Hugo Modules while testing this feature. It is a good example of a non-Hugo-project mounted into Hugo's directory structure. It even shows a JS Bundler implementation in regular Go templates. - -[https://github.com/bep/my-modular-site](https://github.com/bep/my-modular-site) -: A simple site used for testing. diff --git a/docs/content/en/hugo-modules/theme-components.md b/docs/content/en/hugo-modules/theme-components.md deleted file mode 100644 index 03891ed7a..000000000 --- a/docs/content/en/hugo-modules/theme-components.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: Theme components -description: Hugo provides advanced theming support with theme components. -categories: [] -keywords: [] -weight: 30 -aliases: [/themes/customize/,/themes/customizing/] ---- - -A project can configure a theme as a composite of as many theme components as you need: - -{{< code-toggle file=hugo >}} -theme = ["my-shortcodes", "base-theme", "hyde"] -{{< /code-toggle >}} - -You can even nest this, and have the theme component itself include theme components in its own `hugo.toml` (theme inheritance). - -The theme definition example above in `hugo.toml` creates a theme with 3 theme components with precedence from left to right. - -For any given file, data entry, etc., Hugo will look first in the project and then in `my-shortcodes`, `base-theme`, and lastly `hyde`. - -Hugo uses two different algorithms to merge the file systems, depending on the file type: - -- For `i18n` and `data` files, Hugo merges deeply using the translation ID and data key inside the files. -- For `static`, `layouts` (templates), and `archetypes` files, these are merged on file level. So the left-most file will be chosen. - -The name used in the `theme` definition above must match a directory in `/your-site/themes`, e.g. `/your-site/themes/my-shortcodes`. - -Also note that a component that is part of a theme can have its own configuration file, e.g. `hugo.toml`. There are currently some restrictions to what a theme component can configure: - -- `params` (global and per language) -- `menu` (global and per language) -- `outputformats` and `mediatypes` - -The same rules apply here: The left-most parameter/menu etc. with the same ID will win. There are some hidden and experimental namespace support in the above, which we will work to improve in the future, but theme authors are encouraged to create their own namespaces to avoid naming conflicts. diff --git a/docs/content/en/hugo-modules/use-modules.md b/docs/content/en/hugo-modules/use-modules.md deleted file mode 100644 index 86d2ad1cc..000000000 --- a/docs/content/en/hugo-modules/use-modules.md +++ /dev/null @@ -1,154 +0,0 @@ ---- -title: Use Hugo Modules -description: How to use Hugo Modules. -categories: [] -keywords: [] -weight: 20 -aliases: [/themes/usage/,/themes/installing/,/installing-and-using-themes/] ---- - -## Prerequisite - -{{% include "/_common/gomodules-info.md" %}} - -## Initialize a new module - -Use `hugo mod init` to initialize a new Hugo Module. If it fails to guess the module path, you must provide it as an argument, e.g.: - -```sh -hugo mod init github.com// -``` - -Also see the [CLI Doc](/commands/hugo_mod_init/). - -## Use a module for a theme - -The easiest way to use a Module for a theme is to import it in the configuration. - -1. Initialize the hugo module system: `hugo mod init github.com//` -1. Import the theme: - - {{< code-toggle file=hugo >}} - [module] - [[module.imports]] - path = "github.com/spf13/hyde" - {{< /code-toggle >}} - -## Update modules - -Modules will be downloaded and added when you add them as imports to your configuration. See [configure modules](/configuration/module/#imports). - -To update or manage versions, you can use `hugo mod get`. - -Some examples: - -### Update all modules - -```sh -hugo mod get -u -``` - -### Update all modules recursively - -```sh -hugo mod get -u ./... -``` - -### Update one module - -```sh -hugo mod get -u github.com/gohugoio/myShortcodes -``` - -### Get a specific version - -```sh -hugo mod get github.com/gohugoio/myShortcodes@v1.0.7 -``` - -Also see the [CLI Doc](/commands/hugo_mod_get/). - -## Make and test changes in a module - -One way to do local development of a module imported in a project is to add a replace directive to a local directory with the source in `go.mod`: - -```sh -replace github.com/bep/hugotestmods/mypartials => /Users/bep/hugotestmods/mypartials -``` - -If you have the `hugo server` running, the configuration will be reloaded and `/Users/bep/hugotestmods/mypartials` put on the watch list. - -Instead of modifying the `go.mod` files, you can also use the modules configuration [`replacements`](/configuration/module/#top-level-options) option. - -## Print dependency graph - -Use `hugo mod graph` from the relevant module directory and it will print the dependency graph, including vendoring, module replacement or disabled status. - -E.g.: - -```txt -hugo mod graph - -github.com/bep/my-modular-site github.com/bep/hugotestmods/mymounts@v1.2.0 -github.com/bep/my-modular-site github.com/bep/hugotestmods/mypartials@v1.0.7 -github.com/bep/hugotestmods/mypartials@v1.0.7 github.com/bep/hugotestmods/myassets@v1.0.4 -github.com/bep/hugotestmods/mypartials@v1.0.7 github.com/bep/hugotestmods/myv2@v1.0.0 -DISABLED github.com/bep/my-modular-site github.com/spf13/hyde@v0.0.0-20190427180251-e36f5799b396 -github.com/bep/my-modular-site github.com/bep/hugo-fresh@v1.0.1 -github.com/bep/my-modular-site in-themesdir -``` - -Also see the [CLI Doc](/commands/hugo_mod_graph/). - -## Vendor your modules - -`hugo mod vendor` will write all the module dependencies to a `_vendor` directory, which will then be used for all subsequent builds. - -Note that: - -- You can run `hugo mod vendor` on any level in the module tree. -- Vendoring will not store modules stored in your `themes` directory. -- Most commands accept a `--ignoreVendorPaths` flag, which will then not use the vendored modules in `_vendor` for the module paths matching the given [glob](g) pattern. - -Also see the [CLI Doc](/commands/hugo_mod_vendor/). - -## Tidy go.mod, go.sum - -Run `hugo mod tidy` to remove unused entries in `go.mod` and `go.sum`. - -Also see the [CLI Doc](/commands/hugo_mod_clean/). - -## Clean module cache - -Run `hugo mod clean` to delete the entire modules cache. - -Note that you can also configure the `modules` cache with a `maxAge`. See [configure caches](/configuration/caches/). - -Also see the [CLI Doc](/commands/hugo_mod_clean/). - -## Module workspaces - -Workspace support was added in [Go 1.18](https://go.dev/blog/get-familiar-with-workspaces) and Hugo got solid support for it in the `v0.109.0` version. - -A common use case for a workspace is to simplify local development of a site with its theme modules. - -A workspace can be configured in a `*.work` file and activated with the [module.workspace](/configuration/module/) setting, which for this use is commonly controlled via the `HUGO_MODULE_WORKSPACE` OS environment variable. - -See the [hugo.work](https://github.com/gohugoio/hugo/blob/master/docs/hugo.work) file in the Hugo Docs repo for an example: - -```text -go 1.20 - -use . -use ../gohugoioTheme -``` - -Using the `use` directive, list all the modules you want to work on, pointing to its relative location. As in the example above, it's recommended to always include the main project (the `.`) in the list. - -With that you can start the Hugo server with that workspace enabled: - -```sh -HUGO_MODULE_WORKSPACE=hugo.work hugo server --ignoreVendorPaths "**" -``` - -The `--ignoreVendorPaths` flag is added above to ignore any of the vendored dependencies inside `_vendor`. If you don't use vendoring, you don't need that flag. But now the server is set up watching the files and directories in the workspace and you can see your local edits reloaded. diff --git a/docs/content/en/hugo-pipes/_index.md b/docs/content/en/hugo-pipes/_index.md deleted file mode 100755 index 51028152b..000000000 --- a/docs/content/en/hugo-pipes/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Hugo Pipes -description: Use asset pipelines to transform and optimize images, stylesheets, and JavaScript. -categories: [] -keywords: [] -weight: 10 ---- diff --git a/docs/content/en/hugo-pipes/bundling.md b/docs/content/en/hugo-pipes/bundling.md deleted file mode 100755 index 335cc5cae..000000000 --- a/docs/content/en/hugo-pipes/bundling.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: Concat -linkTitle: Concatenating assets -description: Bundle any number of assets into one resource. -categories: [] -keywords: [] ---- - -See the [`resources.Concat`](/functions/resources/concat/) function. diff --git a/docs/content/en/hugo-pipes/fingerprint.md b/docs/content/en/hugo-pipes/fingerprint.md deleted file mode 100755 index d38dfc106..000000000 --- a/docs/content/en/hugo-pipes/fingerprint.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: Fingerprint -linkTitle: Fingerprinting and SRI hashing -description: Cryptographically hash the content of the given resource. -categories: [] -keywords: [] ---- - -See the [`resources.Fingerprint`](/functions/resources/fingerprint/) function. diff --git a/docs/content/en/hugo-pipes/introduction.md b/docs/content/en/hugo-pipes/introduction.md deleted file mode 100755 index d1b93094f..000000000 --- a/docs/content/en/hugo-pipes/introduction.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -title: Hugo Pipes -linkTitle: Introduction -description: Hugo Pipes is Hugo's asset processing set of functions. -categories: [] -keywords: [] -weight: 10 -aliases: [/assets/] ---- - -## Find resources in assets - -This is about global and remote resources. - -global resource -: A file within the `assets` directory, or within any directory [mounted] to the `assets` directory. - -remote resource -: A file on a remote server, accessible via HTTP or HTTPS. - -For `.Page` scoped resources, see the [page resources] section. - -[mounted]: /configuration/module/#mounts -[page resources]: /content-management/page-resources/ - -## Get a resource - -In order to process an asset with Hugo Pipes, it must be retrieved as a resource. - -For global resources, use: - -- [`resources.ByType`](/functions/resources/bytype/) -- [`resources.Get`](/functions/resources/get/) -- [`resources.GetMatch`](/functions/resources/getmatch/) -- [`resources.Match`](/functions/resources/match/) - -For remote resources, use: - -- [`resources.GetRemote`](/functions/resources/getremote/) - -See the [GoDoc Page](https://pkg.go.dev/github.com/gohugoio/hugo/tpl/resources) for the `resources` package for an up to date overview of all template functions in this namespace. - -## Copy a resource - -See the [`resources.Copy`](/functions/resources/copy/) function. - -## Asset directory - -Asset files must be stored in the asset directory. This is `assets` by default, but can be configured via the configuration file's `assetDir` key. - -## Asset publishing - -Hugo publishes assets to the `publishDir` (typically `public`) when you invoke `.Permalink`, `.RelPermalink`, or `.Publish`. You can use `.Content` to inline the asset. - -## Go Pipes - -For improved readability, the Hugo Pipes examples of this documentation will be written using [Go Pipes](/templates/introduction/#pipes): - -```go-html-template -{{ $style := resources.Get "sass/main.scss" | css.Sass | resources.Minify | resources.Fingerprint }} - -``` - -## Caching - -Hugo Pipes invocations are cached based on the entire *pipe chain*. - -An example of a pipe chain is: - -```go-html-template -{{ $mainJs := resources.Get "js/main.js" | js.Build "main.js" | minify | fingerprint }} -``` - -The pipe chain is only invoked the first time it is encountered in a site build, and results are otherwise loaded from cache. As such, Hugo Pipes can be used in templates which are executed thousands or millions of times without negatively impacting the build performance. diff --git a/docs/content/en/hugo-pipes/js.md b/docs/content/en/hugo-pipes/js.md deleted file mode 100644 index 18572d538..000000000 --- a/docs/content/en/hugo-pipes/js.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: JavaScript -linkTitle: JavaScript building -description: Bundle, transpile, tree shake, code split, and minify JavaScript resources. -categories: [] -keywords: [] ---- - -See [JS functions](/functions/js/). diff --git a/docs/content/en/hugo-pipes/minification.md b/docs/content/en/hugo-pipes/minification.md deleted file mode 100755 index 4ba1ea641..000000000 --- a/docs/content/en/hugo-pipes/minification.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: Minify -linkTitle: Asset minification -description: Minify a given resource. -categories: [] -keywords: [] ---- - -See the [`resources.Minify`](/functions/resources/minify/) function. diff --git a/docs/content/en/hugo-pipes/postcss.md b/docs/content/en/hugo-pipes/postcss.md deleted file mode 100755 index 1b47e8aef..000000000 --- a/docs/content/en/hugo-pipes/postcss.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: PostCSS -description: Process the given resource with PostCSS using any PostCSS plugin. -categories: [] -keywords: [] ---- - -See the [`css.PostCSS`](/functions/css/postcss/) function. diff --git a/docs/content/en/hugo-pipes/postprocess.md b/docs/content/en/hugo-pipes/postprocess.md deleted file mode 100755 index 72540fd5d..000000000 --- a/docs/content/en/hugo-pipes/postprocess.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: PostProcess -description: Process the given resource after the build. -categories: [] -keywords: [] ---- - -See the [`resources.PostProcess`](/functions/resources/postprocess/) function. diff --git a/docs/content/en/hugo-pipes/resource-from-string.md b/docs/content/en/hugo-pipes/resource-from-string.md deleted file mode 100755 index ed5eaff80..000000000 --- a/docs/content/en/hugo-pipes/resource-from-string.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: FromString -linkTitle: Resource from string -description: Create a resource from a string. -categories: [] -keywords: [] ---- - -See the [`resources.FromString`](/functions/resources/fromstring/) function. diff --git a/docs/content/en/hugo-pipes/resource-from-template.md b/docs/content/en/hugo-pipes/resource-from-template.md deleted file mode 100755 index bc50f630b..000000000 --- a/docs/content/en/hugo-pipes/resource-from-template.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: ExecuteAsTemplate -linkTitle: Resource from template -description: Create a resource from a Go template, parsed and executed with the given context. -categories: [] -keywords: [] ---- - -See the [`resources.ExecuteAsTemplate`](/functions/resources/executeastemplate/) function. diff --git a/docs/content/en/hugo-pipes/transpile-sass-to-css.md b/docs/content/en/hugo-pipes/transpile-sass-to-css.md deleted file mode 100644 index 756914297..000000000 --- a/docs/content/en/hugo-pipes/transpile-sass-to-css.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: ToCSS -linkTitle: Transpile Sass to CSS -description: Transpile Sass to CSS. -categories: [] -keywords: [] -aliases: [/hugo-pipes/transform-to-css/] ---- - -See the [`css.Sass`](/functions/css/sass) function. diff --git a/docs/content/en/installation/_index.md b/docs/content/en/installation/_index.md deleted file mode 100644 index fdcb8f9eb..000000000 --- a/docs/content/en/installation/_index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Installation -description: Install Hugo on macOS, Linux, Windows, BSD, and on any machine that can run the Go compiler tool chain. -categories: [] -keywords: [] -weight: 10 -aliases: [/getting-started/installing/] ---- diff --git a/docs/content/en/installation/bsd.md b/docs/content/en/installation/bsd.md deleted file mode 100644 index 2f6519e6d..000000000 --- a/docs/content/en/installation/bsd.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -title: BSD -description: Install Hugo on BSD derivatives. -categories: [] -keywords: [] -weight: 40 ---- - -## Editions - -{{% include "/_common/installation/01-editions.md" %}} - -Unless your specific deployment needs require the extended/deploy edition, we recommend the extended edition. - -{{% include "/_common/installation/02-prerequisites.md" %}} - -{{% include "/_common/installation/03-prebuilt-binaries.md" %}} - -## Repository packages - -Most BSD derivatives maintain a repository for commonly installed applications. Please note that these repositories may not contain the [latest release]. - -[latest release]: https://github.com/gohugoio/hugo/releases/latest - -### DragonFly BSD - -[DragonFly BSD] includes Hugo in its package repository. To install the extended edition of Hugo: - -```sh -sudo pkg install gohugo -``` - -[DragonFly BSD]: https://www.dragonflybsd.org/ - -### FreeBSD - -[FreeBSD] includes Hugo in its package repository. To install the extended edition of Hugo: - -```sh -sudo pkg install gohugo -``` - -[FreeBSD]: https://www.freebsd.org/ - -### NetBSD - -[NetBSD] includes Hugo in its package repository. To install the extended edition of Hugo: - -```sh -sudo pkgin install go-hugo -``` - -[NetBSD]: https://www.netbsd.org/ - -### OpenBSD - -[OpenBSD] includes Hugo in its package repository. This will prompt you to select which edition of Hugo to install: - -```sh -doas pkg_add hugo -``` - -[OpenBSD]: https://www.openbsd.org/ - -{{% include "/_common/installation/04-build-from-source.md" %}} - -## Comparison - -||Prebuilt binaries|Repository packages|Build from source -:--|:--:|:--:|:--: -Easy to install?|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark: -Easy to upgrade?|:heavy_check_mark:|varies|:heavy_check_mark: -Easy to downgrade?|:heavy_check_mark:|varies|:heavy_check_mark: -Automatic updates?|:x:|varies|:x: -Latest version available?|:heavy_check_mark:|varies|:heavy_check_mark: diff --git a/docs/content/en/installation/linux.md b/docs/content/en/installation/linux.md deleted file mode 100644 index 591bf0818..000000000 --- a/docs/content/en/installation/linux.md +++ /dev/null @@ -1,218 +0,0 @@ ---- -title: Linux -description: Install Hugo on Linux. -categories: [] -keywords: [] -weight: 20 ---- - -## Editions - -{{% include "/_common/installation/01-editions.md" %}} - -Unless your specific deployment needs require the extended/deploy edition, we recommend the extended edition. - -{{% include "/_common/installation/02-prerequisites.md" %}} - -{{% include "/_common/installation/03-prebuilt-binaries.md" %}} - -## Package managers - -### Snap - -[Snap] is a free and open-source package manager for Linux. Available for [most distributions], snap packages are simple to install and are automatically updated. - -The Hugo snap package is [strictly confined]. Strictly confined snaps run in complete isolation, up to a minimal access level that's deemed always safe. The sites you create and build must be located within your home directory, or on removable media. - -To install the extended edition of Hugo: - -```sh -sudo snap install hugo -``` - -To control automatic updates: - -```sh -# disable automatic updates -sudo snap refresh --hold hugo - -# enable automatic updates -sudo snap refresh --unhold hugo -``` - -To control access to removable media: - -```sh -# allow access -sudo snap connect hugo:removable-media - -# revoke access -sudo snap disconnect hugo:removable-media -``` - -To control access to SSH keys: - -```sh -# allow access -sudo snap connect hugo:ssh-keys - -# revoke access -sudo snap disconnect hugo:ssh-keys -``` - -{{% include "/_common/installation/homebrew.md" %}} - -## Repository packages - -Most Linux distributions maintain a repository for commonly installed applications. - -> [!note] -> The Hugo version available in package repositories varies based on Linux distribution and release, and in some cases will not be the [latest version]. -> -> Use one of the other installation methods if your package repository does not provide the desired version. - -### Alpine Linux - -To install the extended edition of Hugo on [Alpine Linux]: - -```sh -doas apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community hugo -``` - -### Arch Linux - -Derivatives of the [Arch Linux] distribution of Linux include [EndeavourOS], [Garuda Linux], [Manjaro], and others. To install the extended edition of Hugo: - -```sh -sudo pacman -S hugo -``` - -### Debian - -Derivatives of the [Debian] distribution of Linux include [elementary OS], [KDE neon], [Linux Lite], [Linux Mint], [MX Linux], [Pop!_OS], [Ubuntu], [Zorin OS], and others. To install the extended edition of Hugo: - -```sh -sudo apt install hugo -``` - -You can also download Debian packages from the [latest release] page. - -### Exherbo - -To install the extended edition of Hugo on [Exherbo]: - -1. Add this line to /etc/paludis/options.conf: - - ```text - www-apps/hugo extended - ``` - -1. Install using the Paludis package manager: - - ```sh - cave resolve -x repository/heirecka - cave resolve -x hugo - ``` - -### Fedora - -Derivatives of the [Fedora] distribution of Linux include [CentOS], [Red Hat Enterprise Linux], and others. To install the extended edition of Hugo: - -```sh -sudo dnf install hugo -``` - -### Gentoo - -Derivatives of the [Gentoo] distribution of Linux include [Calculate Linux], [Funtoo], and others. To install the extended edition of Hugo: - -1. Specify the `extended` [USE] flag in /etc/portage/package.use/hugo: - - ```text - www-apps/hugo extended - ``` - -1. Build using the Portage package manager: - - ```sh - sudo emerge www-apps/hugo - ``` - -### NixOS - -The NixOS distribution of Linux includes Hugo in its package repository. To install the extended edition of Hugo: - -```sh -nix-env -iA nixos.hugo -``` - -### openSUSE - -Derivatives of the [openSUSE] distribution of Linux include [GeckoLinux], [Linux Karmada], and others. To install the extended edition of Hugo: - -```sh -sudo zypper install hugo -``` - -### Solus - -The [Solus] distribution of Linux includes Hugo in its package repository. To install the extended edition of Hugo: - -```sh -sudo eopkg install hugo -``` - -### Void Linux - -To install the extended edition of Hugo on [Void Linux]: - -```sh -sudo xbps-install -S hugo -``` - -{{% include "/_common/installation/04-build-from-source.md" %}} - -## Comparison - -||Prebuilt binaries|Package managers|Repository packages|Build from source -:--|:--:|:--:|:--:|:--: -Easy to install?|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark: -Easy to upgrade?|:heavy_check_mark:|:heavy_check_mark:|varies|:heavy_check_mark: -Easy to downgrade?|:heavy_check_mark:|:heavy_check_mark: [^1]|varies|:heavy_check_mark: -Automatic updates?|:x:|varies [^2]|:x:|:x: -Latest version available?|:heavy_check_mark:|:heavy_check_mark:|varies|:heavy_check_mark: - -[^1]: Easy if a previous version is still installed. -[^2]: Snap packages are automatically updated. Homebrew requires advanced configuration. - -[Alpine Linux]: https://alpinelinux.org/ -[Arch Linux]: https://archlinux.org/ -[Calculate Linux]: https://www.calculate-linux.org/ -[CentOS]: https://www.centos.org/ -[Debian]: https://www.debian.org/ -[elementary OS]: https://elementary.io/ -[EndeavourOS]: https://endeavouros.com/ -[Exherbo]: https://www.exherbolinux.org/ -[Fedora]: https://getfedora.org/ -[Funtoo]: https://www.funtoo.org/ -[Garuda Linux]: https://garudalinux.org/ -[GeckoLinux]: https://geckolinux.github.io/ -[Gentoo]: https://www.gentoo.org/ -[KDE neon]: https://neon.kde.org/ -[latest version]: https://github.com/gohugoio/hugo/releases/latest -[Linux Karmada]: https://linuxkamarada.com/ -[Linux Lite]: https://www.linuxliteos.com/ -[Linux Mint]: https://linuxmint.com/ -[Manjaro]: https://manjaro.org/ -[most distributions]: https://snapcraft.io/docs/installing-snapd -[MX Linux]: https://mxlinux.org/ -[openSUSE]: https://www.opensuse.org/ -[Pop!_OS]: https://pop.system76.com/ -[Red Hat Enterprise Linux]: https://www.redhat.com/ -[Snap]: https://snapcraft.io/ -[Solus]: https://getsol.us/ -[strictly confined]: https://snapcraft.io/docs/snap-confinement -[Ubuntu]: https://ubuntu.com/ -[USE]: https://packages.gentoo.org/packages/www-apps/hugo -[Void Linux]: https://voidlinux.org/ -[Zorin OS]: https://zorin.com/os/ diff --git a/docs/content/en/installation/macos.md b/docs/content/en/installation/macos.md deleted file mode 100644 index a873b512a..000000000 --- a/docs/content/en/installation/macos.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: macOS -description: Install Hugo on macOS. -categories: [] -keywords: [] -weight: 10 ---- - -## Editions - -{{% include "/_common/installation/01-editions.md" %}} - -Unless your specific deployment needs require the extended/deploy edition, we recommend the extended edition. - -{{% include "/_common/installation/02-prerequisites.md" %}} - -{{% include "/_common/installation/03-prebuilt-binaries.md" %}} - -## Package managers - -{{% include "/_common/installation/homebrew.md" %}} - -### MacPorts - -[MacPorts] is a free and open-source package manager for macOS. To install the extended edition of Hugo: - -```sh -sudo port install hugo -``` - -[MacPorts]: https://www.macports.org/ - -{{% include "/_common/installation/04-build-from-source.md" %}} - -## Comparison - -||Prebuilt binaries|Package managers|Build from source -:--|:--:|:--:|:--: -Easy to install?|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:| -Easy to upgrade?|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark: -Easy to downgrade?|:heavy_check_mark:|:heavy_check_mark: [^1]|:heavy_check_mark: -Automatic updates?|:x:|:x: [^2]|:x: -Latest version available?|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark: - -[^1]: Easy if a previous version is still installed. -[^2]: Possible but requires advanced configuration. diff --git a/docs/content/en/installation/windows.md b/docs/content/en/installation/windows.md deleted file mode 100644 index a5920d45f..000000000 --- a/docs/content/en/installation/windows.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -title: Windows -description: Install Hugo on Windows. -categories: [] -keywords: [] -weight: 30 ---- - -> [!note] -> Hugo v0.121.1 and later require at least Windows 10 or Windows Server 2016. - -## Editions - -{{% include "/_common/installation/01-editions.md" %}} - -Unless your specific deployment needs require the extended/deploy edition, we recommend the extended edition. - -{{% include "/_common/installation/02-prerequisites.md" %}} - -{{% include "/_common/installation/03-prebuilt-binaries.md" %}} - -## Package managers - -### Chocolatey - -[Chocolatey] is a free and open-source package manager for Windows. To install the extended edition of Hugo: - -```sh -choco install hugo-extended -``` - -### Scoop - -[Scoop] is a free and open-source package manager for Windows. To install the extended edition of Hugo: - -```sh -scoop install hugo-extended -``` - -### Winget - -[Winget] is Microsoft's official free and open-source package manager for Windows. To install the extended edition of Hugo: - -```sh -winget install Hugo.Hugo.Extended -``` - -To uninstall the extended edition of Hugo: - -```sh -winget uninstall --name "Hugo (Extended)" -``` - -{{% include "/_common/installation/04-build-from-source.md" %}} - -> [!note] -> See these [detailed instructions](https://discourse.gohugo.io/t/41370) to install GCC on Windows. - -## Comparison - -||Prebuilt binaries|Package managers|Build from source -:--|:--:|:--:|:--: -Easy to install?|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:| -Easy to upgrade?|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark: -Easy to downgrade?|:heavy_check_mark:|:heavy_check_mark: [^2]|:heavy_check_mark: -Automatic updates?|:x:|:x: [^1]|:x: -Latest version available?|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark: - -[^1]: Possible but requires advanced configuration. -[^2]: Easy if a previous version is still installed. - -[Chocolatey]: https://chocolatey.org/ -[Scoop]: https://scoop.sh/ -[Winget]: https://learn.microsoft.com/en-us/windows/package-manager/ diff --git a/docs/content/en/methods/_index.md b/docs/content/en/methods/_index.md deleted file mode 100644 index 39f2b6146..000000000 --- a/docs/content/en/methods/_index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Methods -description: Use these methods within your templates. -categories: [] -keywords: [] -weight: 10 -aliases: ['/variables/'] ---- diff --git a/docs/content/en/methods/duration/Abs.md b/docs/content/en/methods/duration/Abs.md deleted file mode 100644 index 2e85797ea..000000000 --- a/docs/content/en/methods/duration/Abs.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Abs -description: Returns the absolute value of the given time.Duration value. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: time.Duration - signatures: [DURATION.Abs] ---- - -```go-html-template -{{ $d = time.ParseDuration "-3h" }} -{{ $d.Abs }} → 3h0m0s -``` diff --git a/docs/content/en/methods/duration/Hours.md b/docs/content/en/methods/duration/Hours.md deleted file mode 100644 index 23655510e..000000000 --- a/docs/content/en/methods/duration/Hours.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Hours -description: Returns the time.Duration value as a floating point number of hours. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: float64 - signatures: [DURATION.Hours] ---- - -```go-html-template -{{ $d = time.ParseDuration "3.5h2.5m1.5s" }} -{{ $d.Hours }} → 3.5420833333333333 -``` diff --git a/docs/content/en/methods/duration/Microseconds.md b/docs/content/en/methods/duration/Microseconds.md deleted file mode 100644 index c090316d0..000000000 --- a/docs/content/en/methods/duration/Microseconds.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Microseconds -description: Returns the time.Duration value as an integer microsecond count. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: int64 - signatures: [DURATION.Microseconds] ---- - -```go-html-template -{{ $d = time.ParseDuration "3.5h2.5m1.5s" }} -{{ $d.Microseconds }} → 12751500000 -``` diff --git a/docs/content/en/methods/duration/Milliseconds.md b/docs/content/en/methods/duration/Milliseconds.md deleted file mode 100644 index 288f3695a..000000000 --- a/docs/content/en/methods/duration/Milliseconds.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Milliseconds -description: Returns the time.Duration value as an integer millisecond count. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: int64 - signatures: [DURATION.Milliseconds] ---- - -```go-html-template -{{ $d = time.ParseDuration "3.5h2.5m1.5s" }} -{{ $d.Milliseconds }} → 12751500 -``` diff --git a/docs/content/en/methods/duration/Minutes.md b/docs/content/en/methods/duration/Minutes.md deleted file mode 100644 index aec904fa7..000000000 --- a/docs/content/en/methods/duration/Minutes.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Minutes -description: Returns the time.Duration value as a floating point number of minutes. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: float64 - signatures: [DURATION.Minutes] ---- - -```go-html-template -{{ $d = time.ParseDuration "3.5h2.5m1.5s" }} -{{ $d.Minutes }} → 212.525 -``` diff --git a/docs/content/en/methods/duration/Nanoseconds.md b/docs/content/en/methods/duration/Nanoseconds.md deleted file mode 100644 index fd1b9e496..000000000 --- a/docs/content/en/methods/duration/Nanoseconds.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Nanoseconds -description: Returns the time.Duration value as an integer nanosecond count. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: int64 - signatures: [DURATION.Nanoseconds] ---- - -```go-html-template -{{ $d = time.ParseDuration "3.5h2.5m1.5s" }} -{{ $d.Nanoseconds }} → 12751500000000 -``` diff --git a/docs/content/en/methods/duration/Round.md b/docs/content/en/methods/duration/Round.md deleted file mode 100644 index dfd06253f..000000000 --- a/docs/content/en/methods/duration/Round.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: Round -description: Returns the result of rounding DURATION1 to the nearest multiple of DURATION2. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: - signatures: [DURATION1.Round DURATION2] ---- - -```go-html-template -{{ $d = time.ParseDuration "3.5h2.5m1.5s" }} - -{{ $d.Round (time.ParseDuration "2h") }} → 4h0m0s -{{ $d.Round (time.ParseDuration "3m") }} → 3h33m0s -{{ $d.Round (time.ParseDuration "4s") }} → 3h32m32s -``` diff --git a/docs/content/en/methods/duration/Seconds.md b/docs/content/en/methods/duration/Seconds.md deleted file mode 100644 index 8b6d060b9..000000000 --- a/docs/content/en/methods/duration/Seconds.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Seconds -description: Returns the time.Duration value as a floating point number of seconds. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: float64 - signatures: [DURATION.Seconds] ---- - -```go-html-template -{{ $d = time.ParseDuration "3.5h2.5m1.5s" }} -{{ $d.Seconds }} → 12751.5 -``` diff --git a/docs/content/en/methods/duration/Truncate.md b/docs/content/en/methods/duration/Truncate.md deleted file mode 100644 index 5a785a77a..000000000 --- a/docs/content/en/methods/duration/Truncate.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: Truncate -description: Returns the result of rounding DURATION1 toward zero to a multiple of DURATION2. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: time.Duration - signatures: [DURATION1.Truncate DURATION2] ---- - -```go-html-template -{{ $d = time.ParseDuration "3.5h2.5m1.5s" }} - -{{ $d.Truncate (time.ParseDuration "2h") }} → 2h0m0s -{{ $d.Truncate (time.ParseDuration "3m") }} → 3h30m0s -{{ $d.Truncate (time.ParseDuration "4s") }} → 3h32m28s -``` diff --git a/docs/content/en/methods/duration/_index.md b/docs/content/en/methods/duration/_index.md deleted file mode 100644 index aeb113820..000000000 --- a/docs/content/en/methods/duration/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Duration methods -linkTitle: Duration -description: Use these methods with time.Duration values. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/methods/menu-entry/Children.md b/docs/content/en/methods/menu-entry/Children.md deleted file mode 100644 index ecad415fa..000000000 --- a/docs/content/en/methods/menu-entry/Children.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: Children -description: Returns a collection of child menu entries, if any, under the given menu entry. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: navigation.Menu - signatures: [MENUENTRY.Children] ---- - -Use the `Children` method when rendering a nested menu. - -With this site configuration: - -{{< code-toggle file=hugo >}} -[[menus.main]] -name = 'Products' -pageRef = '/product' -weight = 10 - -[[menus.main]] -name = 'Product 1' -pageRef = '/products/product-1' -parent = 'Products' -weight = 1 - -[[menus.main]] -name = 'Product 2' -pageRef = '/products/product-2' -parent = 'Products' -weight = 2 -{{< /code-toggle >}} - -And this template: - -```go-html-template -
      - {{ range .Site.Menus.main }} -
    • - {{ .Name }} - {{ if .HasChildren }} - - {{ end }} -
    • - {{ end }} -
    -``` - -Hugo renders this HTML: - -```html - -``` diff --git a/docs/content/en/methods/menu-entry/HasChildren.md b/docs/content/en/methods/menu-entry/HasChildren.md deleted file mode 100644 index 03e6cb672..000000000 --- a/docs/content/en/methods/menu-entry/HasChildren.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: HasChildren -description: Reports whether the given menu entry has child menu entries. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: bool - signatures: [MENUENTRY.HasChildren] ---- - -Use the `HasChildren` method when rendering a nested menu. - -With this site configuration: - -{{< code-toggle file=hugo >}} -[[menus.main]] -name = 'Products' -pageRef = '/product' -weight = 10 - -[[menus.main]] -name = 'Product 1' -pageRef = '/products/product-1' -parent = 'Products' -weight = 1 - -[[menus.main]] -name = 'Product 2' -pageRef = '/products/product-2' -parent = 'Products' -weight = 2 -{{< /code-toggle >}} - -And this template: - -```go-html-template -
      - {{ range .Site.Menus.main }} -
    • - {{ .Name }} - {{ if .HasChildren }} - - {{ end }} -
    • - {{ end }} -
    -``` - -Hugo renders this HTML: - -```html - -``` diff --git a/docs/content/en/methods/menu-entry/Identifier.md b/docs/content/en/methods/menu-entry/Identifier.md deleted file mode 100644 index 809624459..000000000 --- a/docs/content/en/methods/menu-entry/Identifier.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: Identifier -description: Returns the `identifier` property of the given menu entry. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [MENUENTRY.Identifier] ---- - -The `Identifier` method returns the `identifier` property of the menu entry. If you define the menu entry [automatically], it returns the page's section. - -{{< code-toggle file=hugo >}} -[[menus.main]] -identifier = 'about' -name = 'About' -pageRef = '/about' -weight = 10 - -[[menus.main]] -identifier = 'contact' -name = 'Contact' -pageRef = '/contact' -weight = 20 -{{< /code-toggle >}} - -This example uses the `Identifier` method when querying the translation table on a multilingual site, falling back the `name` property if a matching key in the translation table does not exist: - -```go-html-template - -``` - -> [!note] -> In the menu definition above, note that the `identifier` property is only required when two or more menu entries have the same name, or when localizing the name using translation tables. - -[automatically]: /content-management/menus/#define-automatically diff --git a/docs/content/en/methods/menu-entry/KeyName.md b/docs/content/en/methods/menu-entry/KeyName.md deleted file mode 100644 index d614a5a87..000000000 --- a/docs/content/en/methods/menu-entry/KeyName.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: KeyName -description: Returns the `identifier` property of the given menu entry, falling back to its `name` property. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [MENUENTRY.KeyName] ---- - -In this menu definition, the second entry does not contain an `identifier`, so the `Identifier` method returns its `name` property instead: - -{{< code-toggle file=hugo >}} -[[menus.main]] -identifier = 'about' -name = 'About' -pageRef = '/about' -weight = 10 - -[[menus.main]] -name = 'Contact' -pageRef = '/contact' -weight = 20 -{{< /code-toggle >}} - -This example uses the `KeyName` method when querying the translation table on a multilingual site, falling back the `name` property if a matching key in the translation table does not exist: - -```go-html-template - -``` - -In the example above, we need to pass the value returned by `.KeyName` through the [`lower`] function because the keys in the translation table are lowercase. - -[`lower`]: /functions/strings/tolower/ diff --git a/docs/content/en/methods/menu-entry/Menu.md b/docs/content/en/methods/menu-entry/Menu.md deleted file mode 100644 index 074911eeb..000000000 --- a/docs/content/en/methods/menu-entry/Menu.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: Menu -description: Returns the identifier of the menu that contains the given menu entry. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [MENUENTRY.Menu] ---- - -```go-html-template -{{ range .Site.Menus.main }} - {{ .Menu }} → main -{{ end }} -``` - -Use this method with the [`IsMenuCurrent`] and [`HasMenuCurrent`] methods on a `Page` object to set "active" and "ancestor" classes on a rendered entry. See [this example]. - -[`HasMenuCurrent`]: /methods/page/hasmenucurrent/ -[`IsMenuCurrent`]: /methods/page/ismenucurrent/ -[this example]: /templates/menu/#example diff --git a/docs/content/en/methods/menu-entry/Name.md b/docs/content/en/methods/menu-entry/Name.md deleted file mode 100644 index 706d0f8c8..000000000 --- a/docs/content/en/methods/menu-entry/Name.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: Name -description: Returns the `name` property of the given menu entry. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [MENUENTRY.Name] ---- - -If you define the menu entry [automatically], the `Name` method returns the page's [`LinkTitle`], falling back to its [`Title`]. - -If you define the menu entry [in front matter] or [in site configuration], the `Name` method returns the `name` property of the given menu entry. If the `name` is not defined, and the menu entry resolves to a page, the `Name` returns the page [`LinkTitle`], falling back to its [`Title`]. - -[`LinkTitle`]: /methods/page/linktitle/ -[`Title`]: /methods/page/title/ -[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 - -```go-html-template -
      - {{ range .Site.Menus.main }} -
    • {{ .Name }}
    • - {{ end }} -
    -``` diff --git a/docs/content/en/methods/menu-entry/Page.md b/docs/content/en/methods/menu-entry/Page.md deleted file mode 100644 index 489ee7acc..000000000 --- a/docs/content/en/methods/menu-entry/Page.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: Page -description: Returns the Page object associated with the given menu entry. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Page - signatures: [MENUENTRY.Page] ---- - -Regardless of how you [define menu entries], an entry associated with a page has access to its [methods]. - -In this menu definition, the first two entries are associated with a page, the last entry is not: - -{{< code-toggle file=hugo >}} -[[menus.main]] -pageRef = '/about' -weight = 10 - -[[menus.main]] -pageRef = '/contact' -weight = 20 - -[[menus.main]] -name = 'Hugo' -url = 'https://gohugo.io' -weight = 30 -{{< /code-toggle >}} - -In this example, if the menu entry is associated with a page, we use page's [`RelPermalink`] and [`LinkTitle`] when rendering the anchor element. - -If the entry is not associated with a page, we use its `url` and `name` properties. - -```go-html-template -
      - {{ range .Site.Menus.main }} - {{ with .Page }} -
    • {{ .Title }}
    • - {{ else }} -
    • {{ .Name }}
    • - {{ end }} - {{ end }} -
    -``` - -See the [menu templates] section for more information. - -[`LinkTitle`]: /methods/page/linktitle/ -[`RelPermalink`]: /methods/page/relpermalink/ -[define menu entries]: /content-management/menus/ -[menu templates]: /templates/menu/#page-references -[methods]: /methods/page/ diff --git a/docs/content/en/methods/menu-entry/PageRef.md b/docs/content/en/methods/menu-entry/PageRef.md deleted file mode 100644 index 979879b03..000000000 --- a/docs/content/en/methods/menu-entry/PageRef.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -title: PageRef -description: Returns the `pageRef` property of the given menu entry. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [MENUENTRY.PageRef] ---- - -The use case for this method is rare. - -In almost also scenarios you should use the [`URL`] method instead. - -## Explanation - -If you specify a `pageRef` property when [defining a menu entry] in your site configuration, Hugo looks for a matching page when rendering the entry. - -If a matching page is found: - -- The [`URL`] method returns the page's relative permalink -- The [`Page`] method returns the corresponding `Page` object -- The [`HasMenuCurrent`] and [`IsMenuCurrent`] methods on a `Page` object return the expected values - -If a matching page is not found: - -- The [`URL`] method returns the entry's `url` property if set, else an empty string -- The [`Page`] method returns nil -- The [`HasMenuCurrent`] and [`IsMenuCurrent`] methods on a `Page` object return `false` - -> [!note] -> In almost also scenarios you should use the [`URL`] method instead. - -## Example - -This example is contrived. - -> [!note] -> In almost also scenarios you should use the [`URL`] method instead. - -Consider this content structure: - -```text -content/ -├── products.md -└── _index.md -``` - -And this menu definition: - -{{< code-toggle file=hugo >}} -[[menus.main]] -name = 'Products' -pageRef = '/products' -weight = 10 -[[menus.main]] -name = 'Services' -pageRef = '/services' -weight = 20 -{{< /code-toggle >}} - -With this template code: - -```go-html-template {file="layouts/partials/menu.html"} -
      - {{ range .Site.Menus.main }} -
    • {{ .Name }}
    • - {{ end }} -
    -``` - -Hugo render this HTML: - -```html - -``` - -In the above note that the `href` attribute of the second `anchor` element is blank because Hugo was unable to find the "services" page. - -With this template code: - -```go-html-template {file="layouts/partials/menu.html"} -
      - {{ range .Site.Menus.main }} -
    • {{ .Name }}
    • - {{ end }} -
    -``` - -Hugo renders this HTML: - -```html - -``` - -In the above note that Hugo populates the `href` attribute of the second `anchor` element with the `pageRef` property as defined in the site configuration because the template code falls back to the `PageRef` method. - -[`HasMenuCurrent`]: /methods/page/hasmenucurrent/ -[`IsMenuCurrent`]: /methods/page/ismenucurrent/ -[`Page`]: /methods/menu-entry/page/ -[`URL`]: /methods/menu-entry/url/ -[defining a menu entry]: /content-management/menus/#define-in-site-configuration diff --git a/docs/content/en/methods/menu-entry/Params.md b/docs/content/en/methods/menu-entry/Params.md deleted file mode 100644 index 20c4f7fc7..000000000 --- a/docs/content/en/methods/menu-entry/Params.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -title: Params -description: Returns the `params` property of the given menu entry. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: maps.Params - signatures: [MENUENTRY.Params] ---- - -When you define menu entries [in site configuration] or [in front matter], you can include a `params` key to attach additional information to the entry. For example: - -{{< code-toggle file=hugo >}} -[[menus.main]] -name = 'About' -pageRef = '/about' -weight = 10 - -[[menus.main]] -name = 'Contact' -pageRef = '/contact' -weight = 20 - -[[menus.main]] -name = 'Hugo' -url = 'https://gohugo.io' -weight = 30 -[menus.main.params] - rel = 'external' -{{< /code-toggle >}} - -With this template: - -```go-html-template - -``` - -Hugo renders: - -```html - -``` - -See the [menu templates] section for more information. - -[menu templates]: /templates/menu/#menu-entry-parameters -[in front matter]: /content-management/menus/#define-in-front-matter -[in site configuration]: /content-management/menus/ diff --git a/docs/content/en/methods/menu-entry/Parent.md b/docs/content/en/methods/menu-entry/Parent.md deleted file mode 100644 index 7c617527b..000000000 --- a/docs/content/en/methods/menu-entry/Parent.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: Parent -description: Returns the `parent` property of the given menu entry. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [MENUENTRY.Parent] ---- - -With this menu definition: - -{{< code-toggle file=hugo >}} -[[menus.main]] -name = 'Products' -pageRef = '/product' -weight = 10 - -[[menus.main]] -name = 'Product 1' -pageRef = '/products/product-1' -parent = 'Products' -weight = 1 - -[[menus.main]] -name = 'Product 2' -pageRef = '/products/product-2' -parent = 'Products' -weight = 2 -{{< /code-toggle >}} - -This template renders the nested menu, listing the `parent` property next each of the child entries: - -```go-html-template -
      - {{ range .Site.Menus.main }} -
    • - {{ .Name }} - {{ if .HasChildren }} -
        - {{ range .Children }} -
      • {{ .Name }} ({{ .Parent }})
      • - {{ end }} -
      - {{ end }} -
    • - {{ end }} -
    -``` diff --git a/docs/content/en/methods/menu-entry/Post.md b/docs/content/en/methods/menu-entry/Post.md deleted file mode 100644 index 2da8c38d8..000000000 --- a/docs/content/en/methods/menu-entry/Post.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -title: Post -description: Returns the `post` property of the given menu entry. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: template.HTML - signatures: [MENUENTRY.Post] ---- - -{{% include "/_common/menu-entries/pre-and-post.md" %}} diff --git a/docs/content/en/methods/menu-entry/Pre.md b/docs/content/en/methods/menu-entry/Pre.md deleted file mode 100644 index 19af243e7..000000000 --- a/docs/content/en/methods/menu-entry/Pre.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -title: Pre -description: Returns the `pre` property of the given menu entry. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: template.HTML - signatures: [MENUENTRY.Pre] ---- - -{{% include "/_common/menu-entries/pre-and-post.md" %}} diff --git a/docs/content/en/methods/menu-entry/Title.md b/docs/content/en/methods/menu-entry/Title.md deleted file mode 100644 index 526132d7c..000000000 --- a/docs/content/en/methods/menu-entry/Title.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: Title -description: Returns the `title` property of the given menu entry. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [MENUENTRY.Title] ---- - -The `Title` method returns the `title` property of the given menu entry. If the `title` is not defined, and the menu entry resolves to a page, the `Title` returns the page [`Title`]. - -[`Title`]: /methods/page/title/ - -```go-html-template -
      - {{ range .Site.Menus.main }} -
    • {{ .Name }}
    • - {{ end }} -
    -``` - -[`RelPermalink`]: /methods/page/relpermalink/ diff --git a/docs/content/en/methods/menu-entry/Weight.md b/docs/content/en/methods/menu-entry/Weight.md deleted file mode 100644 index b96e2cc87..000000000 --- a/docs/content/en/methods/menu-entry/Weight.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: Weight -description: Returns the `weight` property of the given menu entry. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: int - signatures: [MENUENTRY.Weight] ---- - -If you define the menu entry [automatically], the `Weight` method returns the page's [`Weight`]. - -If you define the menu entry [in front matter] or [in site configuration], the `Weight` method returns the `weight` property, falling back to the page's `Weight`. - -[`Weight`]: /methods/page/weight/ -[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 - -In this contrived example, we limit the number of menu entries based on weight: - -```go-html-template -
      - {{ range .Site.Menus.main }} - {{ if le .Weight 42 }} -
    • {{ .Name }}
    • - {{ end }} - {{ end }} -
    -``` diff --git a/docs/content/en/methods/menu-entry/_index.md b/docs/content/en/methods/menu-entry/_index.md deleted file mode 100644 index 129e9bcdc..000000000 --- a/docs/content/en/methods/menu-entry/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Menu entry methods -linkTitle: Menu entry -description: Use these methods in your menu templates. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/methods/menu/ByName.md b/docs/content/en/methods/menu/ByName.md deleted file mode 100644 index d98a4aced..000000000 --- a/docs/content/en/methods/menu/ByName.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: ByName -description: Returns the given menu with its entries sorted by name. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: navigation.Menu - signatures: [MENU.ByName] ---- - -The `Sort` method returns the given menu with its entries sorted by `name`. - -Consider this menu definition: - -{{< code-toggle file=hugo >}} -[[menus.main]] -name = 'Services' -pageRef = '/services' -weight = 10 - -[[menus.main]] -name = 'About' -pageRef = '/about' -weight = 20 - -[[menus.main]] -name = 'Contact' -pageRef = '/contact' -weight = 30 -{{< /code-toggle >}} - -To sort the entries by `name`: - -```go-html-template -
      - {{ range .Site.Menus.main.ByName }} -
    • {{ .Name }}
    • - {{ end }} -
    -``` - -Hugo renders this to: - -```html - -``` - -You can also sort menu entries using the [`sort`] function. For example, to sort by `name` in descending order: - -```go-html-template -
      - {{ range sort .Site.Menus.main "Name" "desc" }} -
    • {{ .Name }}
    • - {{ end }} -
    -``` - -When using the sort function with menu entries, specify any of the following keys: `Identifier`, `Name`, `Parent`, `Post`, `Pre`, `Title`, `URL`, or `Weight`. - -[`sort`]: /functions/collections/sort/ diff --git a/docs/content/en/methods/menu/ByWeight.md b/docs/content/en/methods/menu/ByWeight.md deleted file mode 100644 index 013d37e13..000000000 --- a/docs/content/en/methods/menu/ByWeight.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -title: ByWeight -description: Returns the given menu with its entries sorted by weight, then by name, then by identifier. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: navigation.Menu - signatures: [MENU.ByWeight] ---- - -The `ByWeight` method returns the given menu with its entries sorted by [`weight`](g), then by `name`, then by `identifier`. This is the default sort order. - -Consider this menu definition: - -{{< code-toggle file=hugo >}} -[[menus.main]] -identifier = 'about' -name = 'About' -pageRef = '/about' -weight = 20 - -[[menus.main]] -identifier = 'services' -name = 'Services' -pageRef = '/services' -weight = 10 - -[[menus.main]] -identifier = 'contact' -name = 'Contact' -pageRef = '/contact' -weight = 30 -{{< /code-toggle >}} - -To sort the entries by `weight`, then by `name`, then by `identifier`: - -```go-html-template -
      - {{ range .Site.Menus.main.ByWeight }} -
    • {{ .Name }}
    • - {{ end }} -
    -``` - -Hugo renders this to: - -```html - -``` - -> [!note] -> In the menu definition above, note that the `identifier` property is only required when two or more menu entries have the same name, or when localizing the name using translation tables. - -You can also sort menu entries using the [`sort`] function. For example, to sort by `weight` in descending order: - -```go-html-template -
      - {{ range sort .Site.Menus.main "Weight" "desc" }} -
    • {{ .Name }}
    • - {{ end }} -
    -``` - -When using the sort function with menu entries, specify any of the following keys: `Identifier`, `Name`, `Parent`, `Post`, `Pre`, `Title`, `URL`, or `Weight`. - -[`sort`]: /functions/collections/sort/ diff --git a/docs/content/en/methods/menu/Limit.md b/docs/content/en/methods/menu/Limit.md deleted file mode 100644 index 005fef144..000000000 --- a/docs/content/en/methods/menu/Limit.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: Limit -description: Returns the given menu, limited to the first N entries. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: navigation.Menu - signatures: [MENU.Limit N] ---- - -The `Limit` method returns the given menu, limited to the first N entries. - -Consider this menu definition: - -{{< code-toggle file=hugo >}} -[[menus.main]] -name = 'Services' -pageRef = '/services' -weight = 10 - -[[menus.main]] -name = 'About' -pageRef = '/about' -weight = 20 - -[[menus.main]] -name = 'Contact' -pageRef = '/contact' -weight = 30 -{{< /code-toggle >}} - -To sort the entries by name, and limit to the first 2 entries: - -```go-html-template -
      - {{ range .Site.Menus.main.ByName.Limit 2 }} -
    • {{ .Name }}
    • - {{ end }} -
    -``` - -Hugo renders this to: - -```html - -``` diff --git a/docs/content/en/methods/menu/Reverse.md b/docs/content/en/methods/menu/Reverse.md deleted file mode 100644 index 1ee31aa51..000000000 --- a/docs/content/en/methods/menu/Reverse.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -title: Reverse -description: Returns the given menu, reversing the sort order of its entries. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: navigation.Menu - signatures: [MENU.Reverse] ---- - -The `Reverse` method returns the given menu, reversing the sort order of its entries. - -Consider this menu definition: - -{{< code-toggle file=hugo >}} -[[menus.main]] -name = 'Services' -pageRef = '/services' -weight = 10 - -[[menus.main]] -name = 'About' -pageRef = '/about' -weight = 20 - -[[menus.main]] -name = 'Contact' -pageRef = '/contact' -weight = 30 -{{< /code-toggle >}} - -To sort the entries by name in descending order: - -```go-html-template -
      - {{ range .Site.Menus.main.ByName.Reverse }} -
    • {{ .Name }}
    • - {{ end }} -
    -``` - -Hugo renders this to: - -```html - -``` diff --git a/docs/content/en/methods/menu/_index.md b/docs/content/en/methods/menu/_index.md deleted file mode 100644 index 41084fdba..000000000 --- a/docs/content/en/methods/menu/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Menu methods -linkTitle: Menu -description: Use these methods when ranging through menu entries. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/methods/page/Aliases.md b/docs/content/en/methods/page/Aliases.md deleted file mode 100644 index 775404bd3..000000000 --- a/docs/content/en/methods/page/Aliases.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: Aliases -description: Returns the URL aliases as defined in front matter. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: '[]string' - signatures: [PAGE.Aliases] ---- - -The `Aliases` method on a `Page` object returns the URL [aliases] as defined in front matter. - -For example: - -{{< code-toggle file=content/about.md fm=true >}} -title = 'About' -aliases = ['/old-url','/really-old-url'] -{{< /code-toggle >}} - -To list the aliases: - -```go-html-template -{{ range .Aliases }} - {{ . }} -{{ end }} -``` - -[aliases]: /content-management/urls/#aliases diff --git a/docs/content/en/methods/page/AllTranslations.md b/docs/content/en/methods/page/AllTranslations.md deleted file mode 100644 index 62117b429..000000000 --- a/docs/content/en/methods/page/AllTranslations.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -title: AllTranslations -description: Returns all translations of the given page, including the current language. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pages - signatures: [PAGE.AllTranslations] ---- - -With this site configuration: - -{{< code-toggle file=hugo >}} -defaultContentLanguage = 'en' - -[languages.en] -contentDir = 'content/en' -languageCode = 'en-US' -languageName = 'English' -weight = 1 - -[languages.de] -contentDir = 'content/de' -languageCode = 'de-DE' -languageName = 'Deutsch' -weight = 2 - -[languages.fr] -contentDir = 'content/fr' -languageCode = 'fr-FR' -languageName = 'Français' -weight = 3 -{{< /code-toggle >}} - -And this content: - -```text -content/ -├── de/ -│ ├── books/ -│ │ ├── book-1.md -│ │ └── book-2.md -│ └── _index.md -├── en/ -│ ├── books/ -│ │ ├── book-1.md -│ │ └── book-2.md -│ └── _index.md -├── fr/ -│ ├── books/ -│ │ └── book-1.md -│ └── _index.md -└── _index.md -``` - -And this template: - -```go-html-template -{{ with .AllTranslations }} - -{{ end }} -``` - -Hugo will render this list on the "Book 1" page of each site: - -```html - -``` - -On the "Book 2" page of the English and German sites, Hugo will render this: - -```html - -``` diff --git a/docs/content/en/methods/page/AlternativeOutputFormats.md b/docs/content/en/methods/page/AlternativeOutputFormats.md deleted file mode 100644 index c4075d010..000000000 --- a/docs/content/en/methods/page/AlternativeOutputFormats.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -title: AlternativeOutputFormats -description: Returns a slice of OutputFormat objects, excluding the current output format, each representing one of the output formats enabled for the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.OutputFormats - signatures: [PAGE.AlternativeOutputFormats] ---- - -{{% glossary-term "output format" %}} - -The `AlternativeOutputFormats` method on a `Page` object returns a slice of `OutputFormat` objects, excluding the current output format, each representing one of the output formats enabled for the given page. See [details](/configuration/output-formats/). - -## Methods - -{{% include "/_common/methods/page/output-format-methods.md" %}} - -## Example - -Generate a `link` element in the `` of each page for each of the alternative output formats: - -```go-html-template - - ... - {{ $title := printf "%s | %s" .Title site.Title }} - {{ if .IsHome }} - {{ $title = site.Title }} - {{ end }} - {{ range .AlternativeOutputFormats }} - {{ printf `` .Rel .MediaType.Type .Permalink $title | safeHTML }} - {{ end }} - ... - -``` - -On the site's home page, Hugo renders this to: - -```html - -``` diff --git a/docs/content/en/methods/page/Ancestors.md b/docs/content/en/methods/page/Ancestors.md deleted file mode 100644 index d8275cf76..000000000 --- a/docs/content/en/methods/page/Ancestors.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -title: Ancestors -description: Returns a collection of Page objects, one for each ancestor section of the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pages - signatures: [PAGE.Ancestors] ---- - -With this content structure: - -```text -content/ -├── auctions/ -│ ├── 2023-11/ -│ │ ├── _index.md <-- front matter: weight = 202311 -│ │ ├── auction-1.md -│ │ └── auction-2.md -│ ├── 2023-12/ -│ │ ├── _index.md <-- front matter: weight = 202312 -│ │ ├── auction-3.md -│ │ └── auction-4.md -│ ├── _index.md <-- front matter: weight = 30 -│ ├── bidding.md -│ └── payment.md -├── books/ -│ ├── _index.md <-- front matter: weight = 10 -│ ├── book-1.md -│ └── book-2.md -├── films/ -│ ├── _index.md <-- front matter: weight = 20 -│ ├── film-1.md -│ └── film-2.md -└── _index.md -``` - -And this template: - -```go-html-template -{{ range .Ancestors }} - {{ .LinkTitle }} -{{ end }} -``` - -On the November 2023 auctions page, Hugo renders: - -```html -Auctions in November 2023 -Auctions -Home -``` - -In the example above, notice that Hugo orders the ancestors from closest to furthest. This makes breadcrumb navigation simple: - -```go-html-template - -``` - -With some CSS, the code above renders something like this, where each breadcrumb links to its page: - -```text -Home > Auctions > Auctions in November 2023 > Auction 1 -``` diff --git a/docs/content/en/methods/page/BundleType.md b/docs/content/en/methods/page/BundleType.md deleted file mode 100644 index e919511da..000000000 --- a/docs/content/en/methods/page/BundleType.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: BundleType -description: Returns the bundle type of the given page, or an empty string if the page is not a page bundle. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [PAGE.BundleType] ---- - -A page bundle is a directory that encapsulates both content and associated [resources](g). There are two types of page bundles: [leaf bundles](g) and [branch bundles](g). See [details](/content-management/page-bundles/). - -The `BundleType` method on a `Page` object returns `branch` for branch bundles, `leaf` for leaf bundles, and an empty string if the page is not a page bundle. - -```text -content/ -├── films/ -│ ├── film-1/ -│ │ ├── a.jpg -│ │ └── index.md <-- leaf bundle -│ ├── _index.md <-- branch bundle -│ ├── b.jpg -│ ├── film-2.md -│ └── film-3.md -└── _index.md <-- branch bundle -``` - -To get the value within a template: - -```go-html-template -{{ .BundleType }} -``` diff --git a/docs/content/en/methods/page/CodeOwners.md b/docs/content/en/methods/page/CodeOwners.md deleted file mode 100644 index 00afa7549..000000000 --- a/docs/content/en/methods/page/CodeOwners.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: CodeOwners -description: Returns of slice of code owners for the given page, derived from the CODEOWNERS file in the root of the project directory. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: '[]string' - signatures: [PAGE.CodeOwners] ---- - -GitHub and GitLab support CODEOWNERS files. This file specifies the users responsible for developing and maintaining software and documentation. This definition can apply to the entire repository, specific directories, or to individual files. To learn more: - -- [GitHub CODEOWNERS documentation] -- [GitLab CODEOWNERS documentation] - -Use the `CodeOwners` method on a `Page` object to determine the code owners for the given page. - -[GitHub CODEOWNERS documentation]: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners -[GitLab CODEOWNERS documentation]: https://docs.gitlab.com/ee/user/project/code_owners.html - -To use the `CodeOwners` method you must enable access to your local Git repository: - -{{< code-toggle file=hugo >}} -enableGitInfo = true -{{< /code-toggle >}} - -Consider this project structure: - -```text -my-project/ -├── content/ -│ ├── books/ -│ │ └── les-miserables.md -│ └── films/ -│ └── the-hunchback-of-notre-dame.md -└── CODEOWNERS -``` - -And this CODEOWNERS file: - -```text -* @jdoe -/content/books/ @tjones -/content/films/ @mrichards @rsmith -``` - -The table below shows the slice of code owners returned for each file: - -Path|Code owners -:--|:-- -`books/les-miserables.md`|`[@tjones]` -`films/the-hunchback-of-notre-dame.md`|`[@mrichards @rsmith]` - -Render the code owners for each content page: - -```go-html-template -{{ range .CodeOwners }} - {{ . }} -{{ end }} -``` - -Combine this method with [`resources.GetRemote`] to retrieve names and avatars from your Git provider by querying their API. - -[`resources.GetRemote`]: /functions/resources/getremote/ diff --git a/docs/content/en/methods/page/Content.md b/docs/content/en/methods/page/Content.md deleted file mode 100644 index 21348ebe6..000000000 --- a/docs/content/en/methods/page/Content.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: Content -description: Returns the rendered content of the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: template.HTML - signatures: [PAGE.Content] ---- - -The `Content` method on a `Page` object renders Markdown and shortcodes to HTML. - -```go-html-template -{{ .Content }} -``` diff --git a/docs/content/en/methods/page/ContentWithoutSummary.md b/docs/content/en/methods/page/ContentWithoutSummary.md deleted file mode 100644 index 4923b1197..000000000 --- a/docs/content/en/methods/page/ContentWithoutSummary.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: ContentWithoutSummary -description: Returns the rendered content of the given page, excluding the content summary. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: template.HTML - signatures: [PAGE.ContentWithoutSummary] ---- - -{{< new-in 0.134.0 />}} - -Applicable when using manual or automatic [content summaries], the `ContentWithoutSummary` method on a `Page` object renders Markdown and shortcodes to HTML, excluding the content summary from the result. - -[content summaries]: /content-management/summaries/#manual-summary - -```go-html-template -{{ .ContentWithoutSummary }} -``` - -The `ContentWithoutSummary` method returns the same as `Content` if you define the content summary in front matter. diff --git a/docs/content/en/methods/page/CurrentSection.md b/docs/content/en/methods/page/CurrentSection.md deleted file mode 100644 index 93457f13f..000000000 --- a/docs/content/en/methods/page/CurrentSection.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: CurrentSection -description: Returns the Page object of the section in which the given page resides. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Page - signatures: [PAGE.CurrentSection] ---- - -{{% glossary-term section %}} - -> [!note] -> The current section of a [section page](g), [taxonomy page](g), [term page](g), or the home page, is itself. - -Consider this content structure: - -```text -content/ -├── auctions/ -│ ├── 2023-11/ -│ │ ├── _index.md <-- current section: 2023-11 -│ │ ├── auction-1.md -│ │ └── auction-2.md <-- current section: 2023-11 -│ ├── 2023-12/ -│ │ ├── _index.md -│ │ ├── auction-3.md -│ │ └── auction-4.md -│ ├── _index.md <-- current section: auctions -│ ├── bidding.md -│ └── payment.md <-- current section: auctions -├── books/ -│ ├── _index.md <-- current section: books -│ ├── book-1.md -│ └── book-2.md <-- current section: books -├── films/ -│ ├── _index.md <-- current section: films -│ ├── film-1.md -│ └── film-2.md <-- current section: films -└── _index.md <-- current section: home -``` - -To create a link to the current section page: - -```go-html-template -{{ .CurrentSection.LinkTitle }} -``` diff --git a/docs/content/en/methods/page/Data.md b/docs/content/en/methods/page/Data.md deleted file mode 100644 index ae0bdc57f..000000000 --- a/docs/content/en/methods/page/Data.md +++ /dev/null @@ -1,101 +0,0 @@ ---- -title: Data -description: Returns a unique data object for each page kind. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Data - signatures: [PAGE.Data] ---- - -The `Data` method on a `Page` object returns a unique data object for each [page kind](g). - -> [!note] -> The `Data` method is only useful within [taxonomy](g) and [term](g) templates. -> -> Themes that are not actively maintained may still use `.Data.Pages` in list templates. Although that syntax remains functional, use one of these methods instead: [`Pages`], [`RegularPages`], or [`RegularPagesRecursive`] - -The examples that follow are based on this site configuration: - -{{< code-toggle file=hugo >}} -[taxonomies] -genre = 'genres' -author = 'authors' -{{< /code-toggle >}} - -And this content structure: - -```text -content/ -├── books/ -│ ├── and-then-there-were-none.md --> genres: suspense -│ ├── death-on-the-nile.md --> genres: suspense -│ └── jamaica-inn.md --> genres: suspense, romance -│ └── pride-and-prejudice.md --> genres: romance -└── _index.md -``` - -## In a taxonomy template - -Use these methods on the `Data` object within a taxonomy template. - -Singular -: (`string`) Returns the singular name of the taxonomy. - -```go-html-template -{{ .Data.Singular }} → genre -``` - -Plural -: (`string`) Returns the plural name of the taxonomy. - -```go-html-template -{{ .Data.Plural }} → genres -``` - -Terms -: (`page.Taxonomy`) Returns the `Taxonomy` object, consisting of a map of terms and the [weighted pages](g) associated with each term. - -```go-html-template -{{ $taxonomyObject := .Data.Terms }} -``` - -> [!note] -> Once you have captured the `Taxonomy` object, use any of the [taxonomy methods] to sort, count, or capture a subset of its weighted pages. - -Learn more about [taxonomy templates]. - -## In a term template - -Use these methods on the `Data` object within a term template. - -Singular -: (`string`) Returns the singular name of the taxonomy. - -```go-html-template -{{ .Data.Singular }} → genre -``` - -Plural -: (`string`) Returns the plural name of the taxonomy. - -```go-html-template -{{ .Data.Plural }} → genres -``` - -Term -: (`string`) Returns the name of the term. - -```go-html-template -{{ .Data.Term }} → suspense -``` - -Learn more about [term templates]. - -[`Pages`]: /methods/page/pages/ -[`RegularPages`]: /methods/page/regularpages/ -[`RegularPagesRecursive`]: /methods/page/regularpagesrecursive/ -[taxonomy methods]: /methods/taxonomy/ -[taxonomy templates]: /templates/types/#taxonomy -[term templates]: /templates/types/#term diff --git a/docs/content/en/methods/page/Date.md b/docs/content/en/methods/page/Date.md deleted file mode 100644 index b6c2042c2..000000000 --- a/docs/content/en/methods/page/Date.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: Date -description: Returns the date of the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: time.Time - signatures: [PAGE.Date] ---- - -Set the date in front matter: - -{{< code-toggle file=content/news/article-1.md fm=true >}} -title = 'Article 1' -date = 2023-10-19T00:40:04-07:00 -{{< /code-toggle >}} - -> [!note] -> The date field in front matter is often considered to be the creation date, You can change its meaning, and its effect on your site, in the site configuration. See [details]. - -The date is a [time.Time] value. Format and localize the value with the [`time.Format`] function, or use it with any of the [time methods]. - -```go-html-template -{{ .Date | time.Format ":date_medium" }} → Oct 19, 2023 -``` - -In the example above we explicitly set the date in front matter. With Hugo's default configuration, the `Date` method returns the front matter value. This behavior is configurable, allowing you to set fallback values if the date is not defined in front matter. See [details]. - -[`time.Format`]: /functions/time/format/ -[details]: /configuration/front-matter/#dates -[details]: /configuration/front-matter/#dates -[time methods]: /methods/time/ -[time.Time]: https://pkg.go.dev/time#Time diff --git a/docs/content/en/methods/page/Description.md b/docs/content/en/methods/page/Description.md deleted file mode 100644 index 5287aa699..000000000 --- a/docs/content/en/methods/page/Description.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: Description -description: Returns the description of the given page as defined in front matter. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [PAGE.Description] ---- - -Conceptually different from a [content summary], a page description is typically used in metadata about the page. - -{{< code-toggle file=content/recipes/sushi.md fm=true >}} -title = 'How to make spicy tuna hand rolls' -description = 'Instructions for making spicy tuna hand rolls.' -{{< /code-toggle >}} - -```go-html-template {file="layouts/_default/baseof.html"} - - ... - - ... - -``` - -[content summary]: /content-management/summaries/ diff --git a/docs/content/en/methods/page/Draft.md b/docs/content/en/methods/page/Draft.md deleted file mode 100644 index 482a370bf..000000000 --- a/docs/content/en/methods/page/Draft.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -title: Draft -description: Reports whether the given page is a draft as defined in front matter. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: bool - signatures: [PAGE.Draft] ---- - -By default, Hugo does not publish draft pages when you build your site. To include draft pages when you build your site, use the `--buildDrafts` command line flag. - -{{< code-toggle file=content/posts/post-1.md fm=true >}} -title = 'Post 1' -draft = true -{{< /code-toggle >}} - -```go-html-template -{{ .Draft }} → true -``` diff --git a/docs/content/en/methods/page/Eq.md b/docs/content/en/methods/page/Eq.md deleted file mode 100644 index 4947a4bfa..000000000 --- a/docs/content/en/methods/page/Eq.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -title: Eq -description: Reports whether two Page objects are equal. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: bool - signatures: [PAGE1.Eq PAGE2] ---- - -In this contrived example from a single template, we list all pages in the current section except for the current page. - -```go-html-template -{{ $currentPage := . }} -{{ range .CurrentSection.Pages }} - {{ if not (.Eq $currentPage) }} - {{ .LinkTitle }} - {{ end }} -{{ end }} -``` diff --git a/docs/content/en/methods/page/ExpiryDate.md b/docs/content/en/methods/page/ExpiryDate.md deleted file mode 100644 index a72155c33..000000000 --- a/docs/content/en/methods/page/ExpiryDate.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: ExpiryDate -description: Returns the expiry date of the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: time.Time - signatures: [PAGE.ExpiryDate] ---- - -By default, Hugo excludes expired pages when building your site. To include expired pages, use the `--buildExpired` command line flag. - -Set the expiry date in front matter: - -{{< code-toggle file=content/news/article-1.md fm=true >}} -title = 'Article 1' -expiryDate = 2024-10-19T00:32:13-07:00 -{{< /code-toggle >}} - -The expiry date is a [time.Time] value. Format and localize the value with the [`time.Format`] function, or use it with any of the [time methods]. - -```go-html-template -{{ .ExpiryDate | time.Format ":date_medium" }} → Oct 19, 2024 -``` - -In the example above we explicitly set the expiry date in front matter. With Hugo's default configuration, the `ExpiryDate` method returns the front matter value. This behavior is configurable, allowing you to set fallback values if the expiry date is not defined in front matter. See [details]. - -[`time.Format`]: /functions/time/format/ -[details]: /configuration/front-matter/#dates -[time methods]: /methods/time/ -[time.Time]: https://pkg.go.dev/time#Time diff --git a/docs/content/en/methods/page/File.md b/docs/content/en/methods/page/File.md deleted file mode 100644 index 2af60a719..000000000 --- a/docs/content/en/methods/page/File.md +++ /dev/null @@ -1,194 +0,0 @@ ---- -title: File -description: For pages backed by a file, returns file information for the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: hugolib.fileInfo - signatures: [PAGE.File] ---- - -By default, not all pages are backed by a file, including top-level [section pages](g), [taxonomy pages](g), and [term pages](g). By definition, you cannot retrieve file information when the file does not exist. - -To back one of the pages above with a file, create an `_index.md` file in the corresponding directory. For example: - -```text -content/ -└── books/ - ├── _index.md <-- the top-slevel section page - ├── book-1.md - └── book-2.md -``` - -> [!note] -> Code defensively by verifying file existence as shown in the examples below. - -## Methods - -> [!note] -> The path separators (slash or backslash) in `Path`, `Dir`, and `Filename` depend on the operating system. - -### BaseFileName - -(`string`) The file name, excluding the extension. - -```go-html-template -{{ with .File }} - {{ .BaseFileName }} -{{ end }} -``` - -### ContentBaseName - -(`string`) If the page is a branch or leaf bundle, the name of the containing directory, else the `TranslationBaseName`. - -```go-html-template -{{ with .File }} - {{ .ContentBaseName }} -{{ end }} -``` - -### Dir - -(`string`) The file path, excluding the file name, relative to the `content` directory. - -```go-html-template -{{ with .File }} - {{ .Dir }} -{{ end }} -``` - -### Ext - -(`string`) The file extension. - -```go-html-template -{{ with .File }} - {{ .Ext }} -{{ end }} -``` - -### Filename - -(`string`) The absolute file path. - -```go-html-template -{{ with .File }} - {{ .Filename }} -{{ end }} -``` - -### IsContentAdapter - -{{< new-in 0.126.0 />}} - -(`bool`) Reports whether the file is a [content adapter]. - -```go-html-template -{{ with .File }} - {{ .IsContentAdapter }} -{{ end }} -``` - -### LogicalName - -(`string`) The file name. - -```go-html-template -{{ with .File }} - {{ .LogicalName }} -{{ end }} -``` - -### Path - -(`string`) The file path, relative to the `content` directory. - -```go-html-template -{{ with .File }} - {{ .Path }} -{{ end }} -``` - -### Section - -(`string`) The name of the top-level section in which the file resides. - -```go-html-template -{{ with .File }} - {{ .Section }} -{{ end }} -``` - -### TranslationBaseName - -(`string`) The file name, excluding the extension and language identifier. - -```go-html-template -{{ with .File }} - {{ .TranslationBaseName }} -{{ end }} -``` - -### UniqueID - -(`string`) The MD5 hash of `.File.Path`. - -```go-html-template -{{ with .File }} - {{ .UniqueID }} -{{ end }} -``` - -## Examples - -Consider this content structure in a multilingual project: - -```text -content/ -├── news/ -│ ├── b/ -│ │ ├── index.de.md <-- leaf bundle -│ │ └── index.en.md <-- leaf bundle -│ ├── a.de.md <-- regular content -│ ├── a.en.md <-- regular content -│ ├── _index.de.md <-- branch bundle -│ └── _index.en.md <-- branch bundle -├── _index.de.md -└── _index.en.md -``` - -With the English language site: - - |regular content|leaf bundle|branch bundle -:--|:--|:--|:-- -BaseFileName|a.en|index.en|_index.en -ContentBaseName|a|b|news -Dir|news/|news/b/|news/ -Ext|md|md|md -Filename|/home/user/...|/home/user/...|/home/user/... -IsContentAdapter|false|false|false -LogicalName|a.en.md|index.en.md|_index.en.md -Path|news/a.en.md|news/b/index.en.md|news/_index.en.md -Section|news|news|news -TranslationBaseName|a|index|_index -UniqueID|15be14b...|186868f...|7d9159d... - -## Defensive coding - -Some of the pages on a site may not be backed by a file. For example: - -- Top-level section pages -- Taxonomy pages -- Term pages - -Without a backing file, Hugo will throw an error if you attempt to access a `.File` property. To code defensively, first check for file existence: - -```go-html-template -{{ with .File }} - {{ .ContentBaseName }} -{{ end }} -``` - -[content adapter]: /content-management/content-adapters/ diff --git a/docs/content/en/methods/page/FirstSection.md b/docs/content/en/methods/page/FirstSection.md deleted file mode 100644 index 73ddd2d7b..000000000 --- a/docs/content/en/methods/page/FirstSection.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: FirstSection -description: Returns the Page object of the top-level section of which the given page is a descendant. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Page - signatures: [PAGE.FirstSection] ---- - -{{% glossary-term section %}} - -> [!note] -> When called on the home page, the `FirstSection` method returns the `Page` object of the home page itself. - -Consider this content structure: - -```text -content/ -├── auctions/ -│ ├── 2023-11/ -│ │ ├── _index.md <-- first section: auctions -│ │ ├── auction-1.md -│ │ └── auction-2.md <-- first section: auctions -│ ├── 2023-12/ -│ │ ├── _index.md -│ │ ├── auction-3.md -│ │ └── auction-4.md -│ ├── _index.md <-- first section: auctions -│ ├── bidding.md -│ └── payment.md <-- first section: auctions -├── books/ -│ ├── _index.md <-- first section: books -│ ├── book-1.md -│ └── book-2.md <-- first section: books -├── films/ -│ ├── _index.md <-- first section: films -│ ├── film-1.md -│ └── film-2.md <-- first section: films -└── _index.md <-- first section: home -``` - -To link to the top-level section of which the current page is a descendant: - -```go-html-template -{{ .FirstSection.LinkTitle }} -``` diff --git a/docs/content/en/methods/page/Fragments.md b/docs/content/en/methods/page/Fragments.md deleted file mode 100644 index 2c0460def..000000000 --- a/docs/content/en/methods/page/Fragments.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -title: Fragments -description: Returns a data structure of the fragments in the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: tableofcontents.Fragments - signatures: [PAGE.Fragments] ---- - -In a URL, whether absolute or relative, the [fragment](g) links to an `id` attribute of an HTML element on the page. - -```text -/articles/article-1#section-2 -------------------- --------- - path fragment -``` - -Hugo assigns an `id` attribute to each Markdown [ATX] and [setext] heading within the page content. You can override the `id` with a [Markdown attribute](g) as needed. This creates the relationship between an entry in the [table of contents] (TOC) and a heading on the page. - -Use the `Fragments` method on a `Page` object to create a table of contents with the `Fragments.ToHTML` method, or by [walking](g) the `Fragments.Map` data structure. - -## Methods - -### Headings - -(`slice`) A slice of maps of all headings on the page, with first-level keys for each heading. Each map contains the following keys: `ID`, `Level`, `Title` and `Headings`. To inspect the data structure: - -```go-html-template -
    {{ debug.Dump .Fragments.Headings }}
    -``` - -### HeadingsMap - -(`map`) A nested map of all headings on the page. Each map contains the following keys: `ID`, `Level`, `Title` and `Headings`. To inspect the data structure: - -```go-html-template -
    {{ debug.Dump .Fragments.HeadingsMap }}
    -``` - -### Identifiers - -(`slice`) A slice containing the `id` attribute of each heading on the page. If so configured, will also contain the `id` attribute of each description term (i.e., `dt` element) on the page. - -See [configure Markup](/configuration/markup/#parserautodefinitiontermid). - -To inspect the data structure: - -```go-html-template -
    {{ debug.Dump .Fragments.Identifiers }}
    -``` - -### Identifiers.Contains ID - -(`bool`) Reports whether one or more headings on the page has the given `id` attribute, useful for validating fragments within a link [render hook](g). - -```go-html-template -{{ .Fragments.Identifiers.Contains "section-2" }} → true -``` - -### Identifiers.Count ID - -(`int`) The number of headings on a page with the given `id` attribute, useful for detecting duplicates. - -```go-html-template -{{ .Fragments.Identifiers.Count "section-2" }} → 1 -``` - -### ToHTML - -(`template.HTML`) Returns a TOC as a nested list, either ordered or unordered, identical to the HTML returned by the [`TableOfContents`] method. This method take three arguments: the start level (`int`), the end level (`int`), and a boolean (`true` to return an ordered list, `false` to return an unordered list). - -Use this method when you want to control the start level, end level, or list type independently from the table of contents settings in your site configuration. - -```go-html-template -{{ $startLevel := 2 }} -{{ $endLevel := 3 }} -{{ $ordered := true }} -{{ .Fragments.ToHTML $startLevel $endLevel $ordered }} -``` - -Hugo renders this to: - -```html - -``` - -> [!note] -> It is safe to use the `Fragments` methods within a render hook, even for the current page. -> -> When using the `Fragments` methods within a shortcode, call the shortcode using [standard notation]. If you use [Markdown notation] the rendered shortcode is included in the creation of the fragments map, resulting in a circular loop. - -[`TableOfContents`]: /methods/page/tableofcontents/ -[ATX]: https://spec.commonmark.org/0.30/#atx-headings -[Markdown notation]: /content-management/shortcodes/#notation -[setext]: https://spec.commonmark.org/0.30/#setext-headings -[standard notation]: /content-management/shortcodes/#notation -[table of contents]: /methods/page/tableofcontents/ diff --git a/docs/content/en/methods/page/FuzzyWordCount.md b/docs/content/en/methods/page/FuzzyWordCount.md deleted file mode 100644 index 815a07402..000000000 --- a/docs/content/en/methods/page/FuzzyWordCount.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: FuzzyWordCount -description: Returns the number of words in the content of the given page, rounded up to the nearest multiple of 100. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: int - signatures: [PAGE.FuzzyWordCount] ---- - -```go-html-template -{{ .FuzzyWordCount }} → 200 -``` - -To get the exact word count, use the [`WordCount`] method. - -[`WordCount`]: /methods/page/wordcount/ diff --git a/docs/content/en/methods/page/GetPage.md b/docs/content/en/methods/page/GetPage.md deleted file mode 100644 index 02f6888e0..000000000 --- a/docs/content/en/methods/page/GetPage.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -title: GetPage -description: Returns a Page object from the given path. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Page - signatures: [PAGE.GetPage PATH] -aliases: [/functions/getpage] ---- - -The `GetPage` method is also available on a `Site` object. See [details]. - -[details]: /methods/site/getpage/ - -When using the `GetPage` method on the `Page` object, specify a path relative to the current directory or relative to the `content` directory. - -If Hugo cannot resolve the path to a page, the method returns nil. If the path is ambiguous, Hugo throws an error and fails the build. - -Consider this content structure: - -```text -content/ -├── works/ -│ ├── paintings/ -│ │ ├── _index.md -│ │ ├── starry-night.md -│ │ └── the-mona-lisa.md -│ ├── sculptures/ -│ │ ├── _index.md -│ │ ├── david.md -│ │ └── the-thinker.md -│ └── _index.md -└── _index.md -``` - -The examples below depict the result of rendering works/paintings/the-mona-lisa.md: - -```go-html-template {file="layouts/works/single.html"} -{{ with .GetPage "starry-night" }} - {{ .Title }} → Starry Night -{{ end }} - -{{ with .GetPage "./starry-night" }} - {{ .Title }} → Starry Night -{{ end }} - -{{ with .GetPage "../paintings/starry-night" }} - {{ .Title }} → Starry Night -{{ end }} - -{{ with .GetPage "/works/paintings/starry-night" }} - {{ .Title }} → Starry Night -{{ end }} - -{{ with .GetPage "../sculptures/david" }} - {{ .Title }} → David -{{ end }} - -{{ with .GetPage "/works/sculptures/david" }} - {{ .Title }} → David -{{ end }} -``` diff --git a/docs/content/en/methods/page/GetTerms.md b/docs/content/en/methods/page/GetTerms.md deleted file mode 100644 index 53b996fc5..000000000 --- a/docs/content/en/methods/page/GetTerms.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: GetTerms -description: Returns a collection of term pages for terms defined on the given page in the given taxonomy, ordered according to the sequence in which they appear in front matter. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pages - signatures: [PAGE.GetTerms TAXONOMY] ---- - -Given this front matter: - -{{< code-toggle file=content/books/les-miserables.md fm=true >}} -title = 'Les Misérables' -tags = ['historical','classic','fiction'] -{{< /code-toggle >}} - -This template code: - -```go-html-template -{{ with .GetTerms "tags" }} -

    Tags

    - -{{ end }} -``` - -Is rendered to: - -```html -

    Tags

    - -``` diff --git a/docs/content/en/methods/page/GitInfo.md b/docs/content/en/methods/page/GitInfo.md deleted file mode 100644 index 5fde05b07..000000000 --- a/docs/content/en/methods/page/GitInfo.md +++ /dev/null @@ -1,151 +0,0 @@ ---- -title: GitInfo -description: Returns Git information related to the last commit of the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: source.GitInfo - signatures: [PAGE.GitInfo] ---- - -The `GitInfo` method on a `Page` object returns an object with additional methods. - -> [!note] -> Hugo's Git integration is performant, but may increase build times on large sites. - -## Prerequisites - -Install [Git], create a repository, and commit your project files. - -You must also allow Hugo to access your repository. In your site configuration: - -{{< code-toggle file=hugo >}} -enableGitInfo = true -{{< /code-toggle >}} - -Alternatively, use the command line flag when building your site: - -```sh -hugo --enableGitInfo -``` - -> [!note] -> When you set `enableGitInfo` to `true`, or enable the feature with the command line flag, the last modification date for each content page will be the Author Date of the last commit for that file. -> -> This is configurable. See [details]. - -## Methods - -### AbbreviatedHash - -(`string`) The abbreviated commit hash. - -```go-html-template -{{ with .GitInfo }} - {{ .AbbreviatedHash }} → aab9ec0b3 -{{ end }} -``` - -### AuthorDate - -(`time.Time`) The author date. - -```go-html-template -{{ with .GitInfo }} - {{ .AuthorDate.Format "2006-01-02" }} → 2023-10-09 -{{ end }} -``` - -### AuthorEmail - -(`string`) The author's email address, respecting [gitmailmap]. - -```go-html-template -{{ with .GitInfo }} - {{ .AuthorEmail }} → jsmith@example.org -{{ end }} -``` - -### AuthorName - -(`string`) The author's name, respecting [gitmailmap]. - -```go-html-template -{{ with .GitInfo }} - {{ .AuthorName }} → John Smith -{{ end }} -``` - -### CommitDate - -(`time.Time`) The commit date. - -```go-html-template -{{ with .GitInfo }} - {{ .CommitDate.Format "2006-01-02" }} → 2023-10-09 -{{ end }} -``` - -### Hash - -(`string`) The commit hash. - -```go-html-template -{{ with .GitInfo }} - {{ .Hash }} → aab9ec0b31ebac916a1468c4c9c305f2bebf78d4 -{{ end }} -``` - -### Subject - -(`string`) The commit message subject. - -```go-html-template -{{ with .GitInfo }} - {{ .Subject }} → Add tutorials -{{ end }} -``` - -### Body - -(`string`) The commit message body. - -```go-html-template -{{ with .GitInfo }} - {{ .Body }} → - Two new pages added. -{{ end }} -``` - -## Last modified date - -By default, when `enableGitInfo` is `true`, the `Lastmod` method on a `Page` object returns the Git AuthorDate of the last commit that included the file. - -You can change this behavior in your [site configuration]. - -## Hosting considerations - -When hosting your site in a [CI/CD](g) environment, the step that clones your project repository must perform a deep clone. If the clone is shallow, the Git information for a given file may not be accurate---it may reflect the most recent repository commit, not the commit that last modified the file. - -Some providers perform deep clones by default, others allow you to configure the clone depth, and some only perform shallow clones. - -Hosting service | Default clone depth | Configurable -:-- | :-- | :-- -AWS Amplify | Deep | N/A -Cloudflare Pages | Shallow | Yes [^1] -DigitalOcean App Platform | Deep | N/A -GitHub Pages | Shallow | Yes [^2] -GitLab Pages | Shallow | Yes [^3] -Netlify | Deep | N/A -Render | Shallow | No -Vercel | Shallow | No - -[^1]: To configure a Cloudflare Pages site for deep cloning, run `git fetch --unshallow` before building the site. - -[^2]: You can configure the GitHub Action to do a deep clone by specifying `fetch-depth: 0` in the applicable "checkout" step of your workflow file, as shown in the Hugo documentation's [example workflow file](/host-and-deploy/host-on-github-pages/#procedure). - -[^3]: You can configure the GitLab Runner's clone depth [as explained in the GitLab documentation](https://docs.gitlab.com/ee/ci/large_repositories/#shallow-cloning); see also the Hugo documentation's [example workflow file](/host-and-deploy/host-on-gitlab-pages/#configure-gitlab-cicd). - -[details]: /configuration/front-matter/#dates -[gitmailmap]: https://git-scm.com/docs/gitmailmap -[site configuration]: /configuration/front-matter/ diff --git a/docs/content/en/methods/page/HasMenuCurrent.md b/docs/content/en/methods/page/HasMenuCurrent.md deleted file mode 100644 index 207882167..000000000 --- a/docs/content/en/methods/page/HasMenuCurrent.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: HasMenuCurrent -description: Reports whether the given Page object matches the Page object associated with one of the child menu entries under the given menu entry in the given menu. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: bool - signatures: [PAGE.HasMenuCurrent MENU MENUENTRY] -aliases: [/functions/hasmenucurrent] ---- - -If the `Page` object associated with the menu entry is a section, this method also returns `true` for any descendant of that section. - -```go-html-template -{{ $currentPage := . }} -{{ range site.Menus.main }} - {{ if $currentPage.IsMenuCurrent .Menu . }} - {{ .Name }} - {{ else if $currentPage.HasMenuCurrent .Menu . }} - {{ .Name }} - {{ else }} - {{ .Name }} - {{ end }} -{{ end }} -``` - -See [menu templates] for a complete example. - -> [!note] -> When using this method you must either define the menu entry in front matter, or specify a `pageRef` property when defining the menu entry in your site configuration. - -[menu templates]: /templates/menu/#example diff --git a/docs/content/en/methods/page/HasShortcode.md b/docs/content/en/methods/page/HasShortcode.md deleted file mode 100644 index 616b6de09..000000000 --- a/docs/content/en/methods/page/HasShortcode.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: HasShortcode -description: Reports whether the given shortcode is called by the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: bool - signatures: [PAGE.HasShortcode NAME] ---- - -By example, let's use [Plotly] to render a chart: - -[Plotly]: https://plotly.com/javascript/ - -```text {file="content/example.md"} -{{}} -{ - "data": [ - { - "x": ["giraffes", "orangutans", "monkeys"], - "y": [20, 14, 23], - "type": "bar" - } - ], -} -{{}} -``` - -The shortcode is simple: - -```go-html-template {file="layouts/shortcodes/plotly.html"} -{{ $id := printf "plotly-%02d" .Ordinal }} -
    - -``` - -Now we can selectively load the required JavaScript on pages that call the "plotly" shortcode: - -```go-html-template {file="layouts/_default/baseof.html"} - - ... - {{ if .HasShortcode "plotly" }} - - {{ end }} - ... - -``` diff --git a/docs/content/en/methods/page/HeadingsFiltered.md b/docs/content/en/methods/page/HeadingsFiltered.md deleted file mode 100644 index 86c989d43..000000000 --- a/docs/content/en/methods/page/HeadingsFiltered.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: HeadingsFiltered -description: Returns a slice of headings for each page related to the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: tableofcontents.Headings - signatures: [PAGE.HeadingsFiltered] ---- - -Use in conjunction with the [`Related`] method on a [`Pages`] object. See [details]. - -[`Pages`]: /methods/pages/ -[`Related`]: /methods/pages/related/ -[details]: /content-management/related-content/#index-content-headings diff --git a/docs/content/en/methods/page/InSection.md b/docs/content/en/methods/page/InSection.md deleted file mode 100644 index adca82d86..000000000 --- a/docs/content/en/methods/page/InSection.md +++ /dev/null @@ -1,91 +0,0 @@ ---- -title: InSection -description: Reports whether the given page is in the given section. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: bool - signatures: [PAGE.InSection SECTION] ---- - -{{% glossary-term section %}} - -The `InSection` method on a `Page` object reports whether the given page is in the given section. Note that the method returns `true` when comparing a page to a sibling. - -With this content structure: - -```text -content/ -├── auctions/ -│ ├── 2023-11/ -│ │ ├── _index.md -│ │ ├── auction-1.md -│ │ └── auction-2.md -│ ├── 2023-12/ -│ │ ├── _index.md -│ │ ├── auction-3.md -│ │ └── auction-4.md -│ ├── _index.md -│ ├── bidding.md -│ └── payment.md -└── _index.md -``` - -When rendering the "auction-1" page: - -```go-html-template -{{ with .Site.GetPage "/" }} - {{ $.InSection . }} → false -{{ end }} - -{{ with .Site.GetPage "/auctions" }} - {{ $.InSection . }} → false -{{ end }} - -{{ with .Site.GetPage "/auctions/2023-11" }} - {{ $.InSection . }} → true -{{ end }} - -{{ with .Site.GetPage "/auctions/2023-11/auction-2" }} - {{ $.InSection . }} → true -{{ end }} -``` - -In the examples above we are coding defensively using the [`with`] statement, returning nothing if the page does not exist. By adding an [`else`] clause we can do some error reporting: - -```go-html-template -{{ $path := "/auctions/2023-11" }} -{{ with .Site.GetPage $path }} - {{ $.InSection . }} → true -{{ else }} - {{ errorf "Unable to find the section with path %s" $path }} -{{ end }} - ``` - -## Understanding context - -Inside of the `with` block, the [context](g) (the dot) is the section `Page` object, not the `Page` object passed into the template. If we were to use this syntax: - -```go-html-template -{{ with .Site.GetPage "/auctions" }} - {{ .InSection . }} → true -{{ end }} -``` - -The result would be wrong when rendering the "auction-1" page because we are comparing the section page to itself. - -> [!note] -> Use the `$` to get the context passed into the template. - -```go-html-template -{{ with .Site.GetPage "/auctions" }} - {{ $.InSection . }} → true -{{ end }} -``` - -> [!note] -> Gaining a thorough understanding of context is critical for anyone writing template code. - -[`else`]: /functions/go-template/else/ -[`with`]: /functions/go-template/with/ diff --git a/docs/content/en/methods/page/IsAncestor.md b/docs/content/en/methods/page/IsAncestor.md deleted file mode 100644 index fe1b78454..000000000 --- a/docs/content/en/methods/page/IsAncestor.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -title: IsAncestor -description: Reports whether PAGE1 is an ancestor of PAGE2. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: bool - signatures: [PAGE1.IsAncestor PAGE2] ---- - -With this content structure: - -```text -content/ -├── auctions/ -│ ├── 2023-11/ -│ │ ├── _index.md -│ │ ├── auction-1.md -│ │ └── auction-2.md -│ ├── 2023-12/ -│ │ ├── _index.md -│ │ ├── auction-3.md -│ │ └── auction-4.md -│ ├── _index.md -│ ├── bidding.md -│ └── payment.md -└── _index.md -``` - -When rendering the "auctions" page: - -```go-html-template -{{ with .Site.GetPage "/" }} - {{ $.IsAncestor . }} → false -{{ end }} - -{{ with .Site.GetPage "/auctions" }} - {{ $.IsAncestor . }} → false -{{ end }} - -{{ with .Site.GetPage "/auctions/2023-11" }} - {{ $.IsAncestor . }} → true -{{ end }} - -{{ with .Site.GetPage "/auctions/2023-11/auction-2" }} - {{ $.IsAncestor . }} → true -{{ end }} -``` - -In the examples above we are coding defensively using the [`with`] statement, returning nothing if the page does not exist. By adding an [`else`] clause we can do some error reporting: - -```go-html-template -{{ $path := "/auctions/2023-11" }} -{{ with .Site.GetPage $path }} - {{ $.IsAncestor . }} → true -{{ else }} - {{ errorf "Unable to find the section with path %s" $path }} -{{ end }} - ``` - -## Understanding context - -Inside of the `with` block, the [context](g) (the dot) is the section `Page` object, not the `Page` object passed into the template. If we were to use this syntax: - -```go-html-template -{{ with .Site.GetPage "/auctions" }} - {{ .IsAncestor . }} → true -{{ end }} -``` - -The result would be wrong when rendering the "auction-1" page because we are comparing the section page to itself. - -> [!note] -> Use the `$` to get the context passed into the template. - -```go-html-template -{{ with .Site.GetPage "/auctions" }} - {{ $.IsAncestor . }} → true -{{ end }} -``` - -> [!note] -> Gaining a thorough understanding of context is critical for anyone writing template code. - -[`else`]: /functions/go-template/else/ -[`with`]: /functions/go-template/with/ diff --git a/docs/content/en/methods/page/IsDescendant.md b/docs/content/en/methods/page/IsDescendant.md deleted file mode 100644 index 6ee8d3c4f..000000000 --- a/docs/content/en/methods/page/IsDescendant.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -title: IsDescendant -description: Reports whether PAGE1 is a descendant of PAGE2. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: bool - signatures: [PAGE1.IsDescendant PAGE2] ---- - -With this content structure: - -```text -content/ -├── auctions/ -│ ├── 2023-11/ -│ │ ├── _index.md -│ │ ├── auction-1.md -│ │ └── auction-2.md -│ ├── 2023-12/ -│ │ ├── _index.md -│ │ ├── auction-3.md -│ │ └── auction-4.md -│ ├── _index.md -│ ├── bidding.md -│ └── payment.md -└── _index.md -``` - -When rendering the "auctions" page: - -```go-html-template -{{ with .Site.GetPage "/" }} - {{ $.IsDescendant . }} → true -{{ end }} - -{{ with .Site.GetPage "/auctions" }} - {{ $.IsDescendant . }} → false -{{ end }} - -{{ with .Site.GetPage "/auctions/2023-11" }} - {{ $.IsDescendant . }} → false -{{ end }} - -{{ with .Site.GetPage "/auctions/2023-11/auction-2" }} - {{ $.IsDescendant . }} → false -{{ end }} -``` - -In the examples above we are coding defensively using the [`with`] statement, returning nothing if the page does not exist. By adding an [`else`] clause we can do some error reporting: - -```go-html-template -{{ $path := "/auctions/2023-11" }} -{{ with .Site.GetPage $path }} - {{ $.IsDescendant . }} → true -{{ else }} - {{ errorf "Unable to find the section with path %s" $path }} -{{ end }} - ``` - -## Understanding context - -Inside of the `with` block, the [context](g) (the dot) is the section `Page` object, not the `Page` object passed into the template. If we were to use this syntax: - -```go-html-template -{{ with .Site.GetPage "/auctions" }} - {{ .IsDescendant . }} → true -{{ end }} -``` - -The result would be wrong when rendering the "auction-1" page because we are comparing the section page to itself. - -> [!note] -> Use the `$` to get the context passed into the template. - -```go-html-template -{{ with .Site.GetPage "/auctions" }} - {{ $.IsDescendant . }} → true -{{ end }} -``` - -> [!note] -> Gaining a thorough understanding of context is critical for anyone writing template code. - -[`else`]: /functions/go-template/else/ -[`with`]: /functions/go-template/with/ diff --git a/docs/content/en/methods/page/IsHome.md b/docs/content/en/methods/page/IsHome.md deleted file mode 100644 index 66d8180b0..000000000 --- a/docs/content/en/methods/page/IsHome.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: IsHome -description: Reports whether the given page is the home page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: bool - signatures: [PAGE.IsHome] ---- - -The `IsHome` method on a `Page` object returns `true` if the [page kind](g) is `home`. - -```text -content/ -├── books/ -│ ├── book-1/ -│ │ └── index.md <-- kind = page -│ ├── book-2.md <-- kind = page -│ └── _index.md <-- kind = section -└── _index.md <-- kind = home -``` - -```go-html-template -{{ .IsHome }} -``` diff --git a/docs/content/en/methods/page/IsMenuCurrent.md b/docs/content/en/methods/page/IsMenuCurrent.md deleted file mode 100644 index 9bbacd018..000000000 --- a/docs/content/en/methods/page/IsMenuCurrent.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: IsMenuCurrent -description: Reports whether the given Page object matches the Page object associated with the given menu entry in the given menu. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: bool - signatures: [PAGE.IsMenuCurrent MENU MENUENTRY] -aliases: [/functions/ismenucurrent] ---- - -```go-html-template -{{ $currentPage := . }} -{{ range site.Menus.main }} - {{ if $currentPage.IsMenuCurrent .Menu . }} - {{ .Name }} - {{ else if $currentPage.HasMenuCurrent .Menu . }} - {{ .Name }} - {{ else }} - {{ .Name }} - {{ end }} -{{ end }} -``` - -See [menu templates] for a complete example. - -> [!note] -> When using this method you must either define the menu entry in front matter, or specify a `pageRef` property when defining the menu entry in your site configuration. - -[menu templates]: /templates/menu/#example diff --git a/docs/content/en/methods/page/IsNode.md b/docs/content/en/methods/page/IsNode.md deleted file mode 100644 index 194a2cac8..000000000 --- a/docs/content/en/methods/page/IsNode.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: IsNode -description: Reports whether the given page is a node. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: bool - signatures: [PAGE.IsNode] ---- - -The `IsNode` method on a `Page` object returns `true` if the [page kind](g) is `home`, `section`, `taxonomy`, or `term`. - -It returns `false` is the page kind is `page`. - -```text -content/ -├── books/ -│ ├── book-1/ -│ │ └── index.md <-- kind = page, node = false -│ ├── book-2.md <-- kind = page, node = false -│ └── _index.md <-- kind = section, node = true -├── tags/ -│ ├── fiction/ -│ │ └── _index.md <-- kind = term, node = true -│ └── _index.md <-- kind = taxonomy, node = true -└── _index.md <-- kind = home, node = true -``` - -```go-html-template -{{ .IsNode }} -``` diff --git a/docs/content/en/methods/page/IsPage.md b/docs/content/en/methods/page/IsPage.md deleted file mode 100644 index 910a3a7e1..000000000 --- a/docs/content/en/methods/page/IsPage.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: IsPage -description: Reports whether the given page is a regular page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: bool - signatures: [PAGE.IsPage] ---- - -The `IsPage` method on a `Page` object returns `true` if the [page kind](g) is `page`. - -```text -content/ -├── books/ -│ ├── book-1/ -│ │ └── index.md <-- kind = page -│ ├── book-2.md <-- kind = page -│ └── _index.md <-- kind = section -└── _index.md <-- kind = home -``` - -```go-html-template -{{ .IsPage }} -``` diff --git a/docs/content/en/methods/page/IsSection.md b/docs/content/en/methods/page/IsSection.md deleted file mode 100644 index 7a04fbd8f..000000000 --- a/docs/content/en/methods/page/IsSection.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: IsSection -description: Reports whether the given page is a section page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: bool - signatures: [PAGE.IsSection] ---- - -The `IsSection` method on a `Page` object returns `true` if the [page kind](g) is `section`. - -```text -content/ -├── books/ -│ ├── book-1/ -│ │ └── index.md <-- kind = page -│ ├── book-2.md <-- kind = page -│ └── _index.md <-- kind = section -└── _index.md <-- kind = home -``` - -```go-html-template -{{ .IsSection }} -``` diff --git a/docs/content/en/methods/page/IsTranslated.md b/docs/content/en/methods/page/IsTranslated.md deleted file mode 100644 index 2cdf911ac..000000000 --- a/docs/content/en/methods/page/IsTranslated.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -title: IsTranslated -description: Reports whether the given page has one or more translations. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: bool - signatures: [PAGE.IsTranslated] ---- - -With this site configuration: - -{{< code-toggle file=hugo >}} -defaultContentLanguage = 'en' - -[languages.en] -contentDir = 'content/en' -languageCode = 'en-US' -languageName = 'English' -weight = 1 - -[languages.de] -contentDir = 'content/de' -languageCode = 'de-DE' -languageName = 'Deutsch' -weight = 2 -{{< /code-toggle >}} - -And this content: - -```text -content/ -├── de/ -│ ├── books/ -│ │ └── book-1.md -│ └── _index.md -├── en/ -│ ├── books/ -│ │ ├── book-1.md -│ │ └── book-2.md -│ └── _index.md -└── _index.md -``` - -When rendering `content/en/books/book-1.md`: - -```go-html-template -{{ .IsTranslated }} → true -``` - -When rendering `content/en/books/book-2.md`: - -```go-html-template -{{ .IsTranslated }} → false -``` diff --git a/docs/content/en/methods/page/Keywords.md b/docs/content/en/methods/page/Keywords.md deleted file mode 100644 index 7c940984e..000000000 --- a/docs/content/en/methods/page/Keywords.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: Keywords -description: Returns a slice of keywords as defined in front matter. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: '[]string' - signatures: [PAGE.Keywords] ---- - -By default, Hugo evaluates the keywords when creating collections of [related content]. - -[related content]: /content-management/related-content/ - -{{< code-toggle file=content/recipes/sushi.md fm=true >}} -title = 'How to make spicy tuna hand rolls' -keywords = ['tuna','sriracha','nori','rice'] -{{< /code-toggle >}} - -To list the keywords within a template: - -```go-html-template -{{ range .Keywords }} - {{ . }} -{{ end }} -``` - -Or use the [delimit] function: - -```go-html-template -{{ delimit .Keywords ", " ", and " }} → tuna, sriracha, nori, and rice -``` - -[delimit]: /functions/collections/delimit/ - -Keywords are also a useful [taxonomy]: - -{{< code-toggle file=hugo >}} -[taxonomies] -tag = 'tags' -keyword = 'keywords' -category = 'categories' -{{< /code-toggle >}} - -[taxonomy]: /content-management/taxonomies/ diff --git a/docs/content/en/methods/page/Kind.md b/docs/content/en/methods/page/Kind.md deleted file mode 100644 index a01877e8c..000000000 --- a/docs/content/en/methods/page/Kind.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: Kind -description: Returns the kind of the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [PAGE.Kind] ---- - -The [page kind](g) is one of `home`, `page`, `section`, `taxonomy`, or `term`. - -```text -content/ -├── books/ -│ ├── book-1/ -│ │ └── index.md <-- kind = page -│ ├── book-2.md <-- kind = page -│ └── _index.md <-- kind = section -├── tags/ -│ ├── fiction/ -│ │ └── _index.md <-- kind = term -│ └── _index.md <-- kind = taxonomy -└── _index.md <-- kind = home -``` - -To get the value within a template: - -```go-html-template -{{ .Kind }} -``` diff --git a/docs/content/en/methods/page/Language.md b/docs/content/en/methods/page/Language.md deleted file mode 100644 index 9fd604df3..000000000 --- a/docs/content/en/methods/page/Language.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: Language -description: Returns the language object for the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: langs.Language - signatures: [PAGE.Language] ---- - -The `Language` method on a `Page` object returns the language object for the given page. The language object points to the language definition in the site configuration. - -You can also use the `Language` method on a `Site` object. See [details]. - -## Methods - -The examples below assume the following in your site configuration: - -{{< code-toggle file=hugo >}} -[languages.de] -languageCode = 'de-DE' -languageDirection = 'ltr' -languageName = 'Deutsch' -weight = 2 -{{< /code-toggle >}} - -### Lang - -(`string`) The language tag as defined by [RFC 5646]. - -```go-html-template -{{ .Language.Lang }} → de -``` - -### LanguageCode - -(`string`) The language code from the site configuration. Falls back to `Lang` if not defined. - -```go-html-template -{{ .Language.LanguageCode }} → de-DE -``` - -### LanguageDirection - -(`string`) The language direction from the site configuration, either `ltr` or `rtl`. - -```go-html-template -{{ .Language.LanguageDirection }} → ltr -``` - -### LanguageName - -(`string`) The language name from the site configuration. - -```go-html-template -{{ .Language.LanguageName }} → Deutsch -``` - -### Weight - -(`int`) The language weight from the site configuration which determines its order in the slice of languages returned by the `Languages` method on a `Site` object. - -```go-html-template -{{ .Language.Weight }} → 2 -``` - -[details]: /methods/site/language/ -[RFC 5646]: https://datatracker.ietf.org/doc/html/rfc5646 diff --git a/docs/content/en/methods/page/Lastmod.md b/docs/content/en/methods/page/Lastmod.md deleted file mode 100644 index 643eddc5e..000000000 --- a/docs/content/en/methods/page/Lastmod.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: Lastmod -description: Returns the last modification date of the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: time.Time - signatures: [PAGE.Lastmod] ---- - -Set the last modification date in front matter: - -{{< code-toggle file=content/news/article-1.md fm=true >}} -title = 'Article 1' -lastmod = 2023-10-19T00:40:04-07:00 -{{< /code-toggle >}} - -The last modification date is a [time.Time] value. Format and localize the value with the [`time.Format`] function, or use it with any of the [time methods]. - -```go-html-template -{{ .Lastmod | time.Format ":date_medium" }} → Oct 19, 2023 -``` - -In the example above we explicitly set the last modification date in front matter. With Hugo's default configuration, the `Lastmod` method returns the front matter value. This behavior is configurable, allowing you to: - -- Set the last modification date to the Author Date of the last Git commit for that file. See [`GitInfo`] for details. -- Set fallback values if the last modification date is not defined in front matter. - -Learn more about [date configuration]. - -[`gitinfo`]: /methods/page/gitinfo/ -[`time.format`]: /functions/time/format/ -[date configuration]: /configuration/front-matter/#dates -[time methods]: /methods/time/ -[time.time]: https://pkg.go.dev/time#Time diff --git a/docs/content/en/methods/page/Layout.md b/docs/content/en/methods/page/Layout.md deleted file mode 100644 index f9aa5b6ab..000000000 --- a/docs/content/en/methods/page/Layout.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: Layout -description: Returns the layout for the given page as defined in front matter. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [PAGE.Layout] ---- - -Specify the `layout` field in front matter to target a particular template. See [details]. - -[details]: /templates/lookup-order/#target-a-template - -{{< code-toggle file=content/contact.md fm=true >}} -title = 'Contact' -layout = 'contact' -{{< /code-toggle >}} - -Hugo will render the page using contact.html. - -```text -layouts/ -└── _default/ - ├── baseof.html - ├── contact.html - ├── home.html - ├── list.html - └── single.html -``` - -Although rarely used within a template, you can access the value with: - -```go-html-template -{{ .Layout }} -``` - -The `Layout` method returns an empty string if the `layout` field in front matter is not defined. diff --git a/docs/content/en/methods/page/Len.md b/docs/content/en/methods/page/Len.md deleted file mode 100644 index 010da88d1..000000000 --- a/docs/content/en/methods/page/Len.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: Len -description: Returns the length, in bytes, of the rendered content of the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: int - signatures: [PAGE.Len] ---- - -```go-html-template -{{ .Len }} → 42 -``` diff --git a/docs/content/en/methods/page/LinkTitle.md b/docs/content/en/methods/page/LinkTitle.md deleted file mode 100644 index fcfd5318d..000000000 --- a/docs/content/en/methods/page/LinkTitle.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: LinkTitle -description: Returns the link title of the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [PAGE.LinkTitle] ---- - -The `LinkTitle` method returns the `linkTitle` field as defined in front matter, falling back to the value returned by the [`Title`] method. - -[`Title`]: /methods/page/title/ - -{{< code-toggle file=content/articles/healthy-desserts.md fm=true >}} -title = 'Seventeen delightful recipes for healthy desserts' -linkTitle = 'Dessert recipes' -{{< /code-toggle >}} - -```go-html-template -{{ .LinkTitle }} → Dessert recipes -``` - -As demonstrated above, defining a link title in front matter is advantageous when the page title is long. Use it when generating anchor elements in your templates: - -```go-html-template -{{ .LinkTitle }} -``` diff --git a/docs/content/en/methods/page/Next.md b/docs/content/en/methods/page/Next.md deleted file mode 100644 index 996603083..000000000 --- a/docs/content/en/methods/page/Next.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -title: Next -description: Returns the next page in a site's collection of regular pages, relative to the current page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Page - signatures: [PAGE.Next] ---- - -{{% include "/_common/methods/page/next-and-prev.md" %}} diff --git a/docs/content/en/methods/page/NextInSection.md b/docs/content/en/methods/page/NextInSection.md deleted file mode 100644 index eb02c9492..000000000 --- a/docs/content/en/methods/page/NextInSection.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -title: NextInSection -description: Returns the next regular page in a section, relative to the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Page - signatures: [PAGE.NextInSection] ---- - -{{% include "/_common/methods/page/nextinsection-and-previnsection.md" %}} diff --git a/docs/content/en/methods/page/OutputFormats.md b/docs/content/en/methods/page/OutputFormats.md deleted file mode 100644 index 0e648efaa..000000000 --- a/docs/content/en/methods/page/OutputFormats.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: OutputFormats -description: Returns a slice of OutputFormat objects, each representing one of the output formats enabled for the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: '[]OutputFormat' - signatures: [PAGE.OutputFormats] ---- - -{{% glossary-term "output format" %}} - -The `OutputFormats` method on a `Page` object returns a slice of `OutputFormat` objects, each representing one of the output formats enabled for the given page. See [details](/configuration/output-formats/). - -## Methods - -{{% include "/_common/methods/page/output-format-methods.md" %}} - -## Example - -To link to the RSS feed for the current page: - -```go-html-template -{{ with .OutputFormats.Get "rss" }} - RSS Feed -{{ end }} -``` - -On the site's home page, Hugo renders this to: - -```html -RSS Feed -``` - -Please see the [link to output formats] section to understand the importance of the construct above. - -[link to output formats]: /configuration/output-formats/#link-to-output-formats diff --git a/docs/content/en/methods/page/Page.md b/docs/content/en/methods/page/Page.md deleted file mode 100644 index 7c7728b2f..000000000 --- a/docs/content/en/methods/page/Page.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: Page -description: Returns the Page object of the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Page - signatures: [PAGE.Page] ---- - -This is a convenience method, useful within partial templates that are called from both [shortcodes](g) and page templates. - -```go-html-template {file="layouts/shortcodes/foo.html"} -{{ partial "my-partial.html" . }} -``` - -When the shortcode calls the partial, it passes the current [context](g) (the dot). The context includes identifiers such as `Page`, `Params`, `Inner`, and `Name`. - -```go-html-template {file="layouts/_default/single.html"} -{{ partial "my-partial.html" . }} -``` - -When the page template calls the partial, it also passes the current context (the dot). But in this case, the dot _is_ the `Page` object. - -```go-html-template {file="layouts/partials/my-partial.html"} -The page title is: {{ .Page.Title }} -``` - -To handle both scenarios, the partial template must be able to access the `Page` object with `Page.Page`. - -> [!note] -> And yes, that means you can do `.Page.Page.Page.Page.Title` too. -> -> But don't. diff --git a/docs/content/en/methods/page/Pages.md b/docs/content/en/methods/page/Pages.md deleted file mode 100644 index ba43c36a8..000000000 --- a/docs/content/en/methods/page/Pages.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -title: Pages -description: Returns a collection of regular pages within the current section, and section pages of immediate descendant sections. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pages - signatures: [PAGE.Pages] ---- - -The `Pages` method on a `Page` object is available to these [page kinds](g): `home`, `section`, `taxonomy`, and `term`. The templates for these page kinds receive a page [collection](g) in [context](g), in the [default sort order](g). - -Range through the page collection in your template: - -```go-html-template -{{ range .Pages.ByTitle }} -

    {{ .Title }}

    -{{ end }} -``` - -Consider this content structure: - -```text -content/ -├── lessons/ -│ ├── lesson-1/ -│ │ ├── _index.md -│ │ ├── part-1.md -│ │ └── part-2.md -│ ├── lesson-2/ -│ │ ├── resources/ -│ │ │ ├── task-list.md -│ │ │ └── worksheet.md -│ │ ├── _index.md -│ │ ├── part-1.md -│ │ └── part-2.md -│ ├── _index.md -│ ├── grading-policy.md -│ └── lesson-plan.md -├── _index.md -├── contact.md -└── legal.md -``` - -When rendering the home page, the `Pages` method returns: - - contact.md - legal.md - lessons/_index.md - -When rendering the lessons page, the `Pages` method returns: - - lessons/grading-policy.md - lessons/lesson-plan.md - lessons/lesson-1/_index.md - lessons/lesson-2/_index.md - -When rendering lesson-1, the `Pages` method returns: - - lessons/lesson-1/part-1.md - lessons/lesson-1/part-2.md - -When rendering lesson-2, the `Pages` method returns: - - lessons/lesson-2/part-1.md - lessons/lesson-2/part-2.md - lessons/lesson-2/resources/task-list.md - lessons/lesson-2/resources/worksheet.md - -In the last example, the collection includes pages in the resources subdirectory. That directory is not a [section](g)---it does not contain an `_index.md` file. Its contents are part of the lesson-2 section. - -> [!note] -> When used with a `Site` object, the `Pages` method recursively returns all pages within the site. See [details]. - -```go-html-template -{{ range .Site.Pages.ByTitle }} -

    {{ .Title }}

    -{{ end }} -``` - -[details]: /methods/site/pages/ diff --git a/docs/content/en/methods/page/Paginate.md b/docs/content/en/methods/page/Paginate.md deleted file mode 100644 index 0b699d6b2..000000000 --- a/docs/content/en/methods/page/Paginate.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: Paginate -description: Paginates a collection of pages. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pager - signatures: ['PAGE.Paginate COLLECTION [N]'] ---- - -Pagination is the process of splitting a list page into two or more pagers, where each pager contains a subset of the page collection and navigation links to other pagers. - -By default, the number of elements on each pager is determined by your [site configuration]. The default is `10`. Override that value by providing a second argument, an integer, when calling the `Paginate` method. - -> [!note] -> There is also a `Paginator` method on `Page` objects, but it can neither filter nor sort the page collection. -> -> The `Paginate` method is more flexible. - -You can invoke pagination on the [home template], [section templates], [taxonomy templates], and [term templates]. - -```go-html-template {file="layouts/_default/list.html"} -{{ $pages := where .Site.RegularPages "Section" "articles" }} -{{ $pages = $pages.ByTitle }} -{{ range (.Paginate $pages 7).Pages }} -

    {{ .Title }}

    -{{ end }} -{{ template "_internal/pagination.html" . }} -``` - -In the example above, we: - -1. Build a page collection -1. Sort the collection by title -1. Paginate the collection, with 7 elements per pager -1. Range over the paginated page collection, rendering a link to each page -1. Call the embedded pagination template to create navigation links between pagers - -> [!note] -> Please note that the results of pagination are cached. Once you have invoked either the `Paginator` or `Paginate` method, the paginated collection is immutable. Additional invocations of these methods will have no effect. - -[home template]: /templates/types/#home -[section templates]: /templates/types/#section -[site configuration]: /configuration/pagination/ -[taxonomy templates]: /templates/types/#taxonomy -[term templates]: /templates/types/#term diff --git a/docs/content/en/methods/page/Paginator.md b/docs/content/en/methods/page/Paginator.md deleted file mode 100644 index bff7ea90c..000000000 --- a/docs/content/en/methods/page/Paginator.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -title: Paginator -description: Paginates the collection of regular pages received in context. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pager - signatures: [PAGE.Paginator] ---- - -Pagination is the process of splitting a list page into two or more pagers, where each pager contains a subset of the page collection and navigation links to other pagers. - -The number of elements on each pager is determined by your [site configuration]. The default is `10`. - -You can invoke pagination on the [home template], [section templates], [taxonomy templates], and [term templates]. Each of these receives a collection of regular pages in [context](g). When you invoke the `Paginator` method, it paginates the page collection received in context. - -```go-html-template {file="layouts/_default/list.html"} -{{ range .Paginator.Pages }} -

    {{ .LinkTitle }}

    -{{ end }} -{{ template "_internal/pagination.html" . }} -``` - -In the example above, the embedded pagination template creates navigation links between pagers. - -> [!note] -> Although simple to invoke, with the `Paginator` method you can neither filter nor sort the page collection. It acts upon the page collection received in context. -> -> The [`Paginate`] method is more flexible, and strongly recommended. - -> [!note] -> Please note that the results of pagination are cached. Once you have invoked either the `Paginator` or `Paginate` method, the paginated collection is immutable. Additional invocations of these methods will have no effect. - -[home template]: /templates/types/#home -[section templates]: /templates/types/#section -[site configuration]: /configuration/pagination/ -[taxonomy templates]: /templates/types/#taxonomy -[term templates]: /templates/types/#term -[`Paginate`]: /methods/page/paginate/ diff --git a/docs/content/en/methods/page/Param.md b/docs/content/en/methods/page/Param.md deleted file mode 100644 index b07c1cd92..000000000 --- a/docs/content/en/methods/page/Param.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: Param -description: Returns a page parameter with the given key, falling back to a site parameter if present. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: any - signatures: [PAGE.Param KEY] -aliases: [/functions/param] ---- - -The `Param` method on a `Page` object looks for the given `KEY` in page parameters, and returns the corresponding value. If it cannot find the `KEY` in page parameters, it looks for the `KEY` in site parameters. If it cannot find the `KEY` in either location, the `Param` method returns `nil`. - -Site and theme developers commonly set parameters at the site level, allowing content authors to override those parameters at the page level. - -For example, to show a table of contents on every page, but allow authors to hide the table of contents as needed: - -Configuration: - -{{< code-toggle file=hugo >}} -[params] -display_toc = true -{{< /code-toggle >}} - -Content: - -{{< code-toggle file=content/example.md fm=true >}} -title = 'Example' -date = 2023-01-01 -draft = false -[params] -display_toc = false -{{< /code-toggle >}} - -Template: - -```go-html-template -{{ if .Param "display_toc" }} - {{ .TableOfContents }} -{{ end }} -``` - -The `Param` method returns the value associated with the given `KEY`, regardless of whether the value is truthy or falsy. If you need to ignore falsy values, use this construct instead: - -```go-html-template -{{ or .Params.foo site.Params.foo }} -``` diff --git a/docs/content/en/methods/page/Params.md b/docs/content/en/methods/page/Params.md deleted file mode 100644 index eeb253437..000000000 --- a/docs/content/en/methods/page/Params.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -title: Params -description: Returns a map of custom parameters as defined in the front matter of the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: maps.Params - signatures: [PAGE.Params] ---- - -By way of example, consider this front matter: - -{{< code-toggle file=content/annual-conference.md fm=true >}} -title = 'Annual conference' -date = 2023-10-17T15:11:37-07:00 -[params] -display_related = true -key-with-hyphens = 'must use index function' -[params.author] - email = 'jsmith@example.org' - name = 'John Smith' -{{< /code-toggle >}} - -The `title` and `date` fields are standard [front matter fields], while the other fields are user-defined. - -Access the custom fields by [chaining](g) the [identifiers](g) when needed: - -```go-html-template -{{ .Params.display_related }} → true -{{ .Params.author.email }} → jsmith@example.org -{{ .Params.author.name }} → John Smith -``` - -In the template example above, each of the keys is a valid identifier. For example, none of the keys contains a hyphen. To access a key that is not a valid identifier, use the [`index`] function: - -```go-html-template -{{ index .Params "key-with-hyphens" }} → must use index function -``` - -[`index`]: /functions/collections/indexfunction/ -[front matter fields]: /content-management/front-matter/#fields diff --git a/docs/content/en/methods/page/Parent.md b/docs/content/en/methods/page/Parent.md deleted file mode 100644 index 0946a7993..000000000 --- a/docs/content/en/methods/page/Parent.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -title: Parent -description: Returns the Page object of the parent section of the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Page - signatures: [PAGE.Parent] ---- - -{{% glossary-term section %}} - -> [!note] -> The parent section of a regular page is the [current section]. - -Consider this content structure: - -```text -content/ -├── auctions/ -│ ├── 2023-11/ -│ │ ├── _index.md <-- parent: auctions -│ │ ├── auction-1.md -│ │ └── auction-2.md <-- parent: 2023-11 -│ ├── 2023-12/ -│ │ ├── _index.md -│ │ ├── auction-3.md -│ │ └── auction-4.md -│ ├── _index.md <-- parent: home -│ ├── bidding.md -│ └── payment.md <-- parent: auctions -├── books/ -│ ├── _index.md <-- parent: home -│ ├── book-1.md -│ └── book-2.md <-- parent: books -├── films/ -│ ├── _index.md <-- parent: home -│ ├── film-1.md -│ └── film-2.md <-- parent: films -└── _index.md <-- parent: nil -``` - -In the example above, note the parent section of the home page is nil. Code defensively by verifying existence of the parent section before calling methods on its `Page` object. To create a link to the parent section page of the current page: - -```go-html-template -{{ with .Parent }} - {{ .LinkTitle }} -{{ end }} -``` - -[current section]: /methods/page/currentsection/ diff --git a/docs/content/en/methods/page/Path.md b/docs/content/en/methods/page/Path.md deleted file mode 100644 index db4e7d629..000000000 --- a/docs/content/en/methods/page/Path.md +++ /dev/null @@ -1,140 +0,0 @@ ---- -title: Path -description: Returns the logical path of the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [PAGE.Path] ---- - -{{< new-in 0.123.0 />}} - -The `Path` method on a `Page` object returns the logical path of the given page, regardless of whether the page is backed by a file. - -{{% glossary-term "logical path" %}} - -```go-html-template -{{ .Path }} → /posts/post-1 -``` - -> [!note] -> Beginning with the release of [v0.92.0] in January 2022, Hugo emitted a warning whenever calling the `Path` method. The warning indicated that this method would change in a future release. -> -> The meaning of, and value returned by, the `Path` method on a `Page` object changed with the release of [v0.123.0] in February 2024. - -The value returned by the `Path` method on a `Page` object is independent of content format, language, and URL modifiers such as the `slug` and `url` front matter fields. - -## Examples - -### Monolingual site - -Note that the logical path is independent of content format and URL modifiers. - -File path|Front matter slug|Logical path -:--|:--|:-- -`content/_index.md`||`/` -`content/posts/_index.md`||`/posts` -`content/posts/post-1.md`|`foo`|`/posts/post-1` -`content/posts/post-2.html`|`bar`|`/posts/post-2` - -### Multilingual site - -Note that the logical path is independent of content format, language identifiers, and URL modifiers. - -File path|Front matter slug|Logical path -:--|:--|:-- -`content/_index.en.md`||`/` -`content/_index.de.md`||`/` -`content/posts/_index.en.md`||`/posts` -`content/posts/_index.de.md`||`/posts` -`content/posts/posts-1.en.md`|`foo`|`/posts/post-1` -`content/posts/posts-1.de.md`|`foo`|`/posts/post-1` -`content/posts/posts-2.en.html`|`bar`|`/posts/post-2` -`content/posts/posts-2.de.html`|`bar`|`/posts/post-2` - -### Pages not backed by a file - -The `Path` method on a `Page` object returns a value regardless of whether the page is backed by a file. - -```text -content/ -└── posts/ - └── post-1.md <-- front matter: tags = ['hugo'] -``` - -When you build the site: - -```text -public/ -├── posts/ -│ ├── post-1/ -│ │ └── index.html .Page.Path = /posts/post-1 -│ └── index.html .Page.Path = /posts -├── tags/ -│ ├── hugo/ -│ │ └── index.html .Page.Path = /tags/hugo -│ └── index.html .Page.Path = /tags -└── index.html .Page.Path = / -``` - -## Finding pages - -These methods, functions, and shortcodes use the logical path to find the given page: - -Methods|Functions|Shortcodes -:--|:--|:-- -[`Site.GetPage`]|[`urls.Ref`]|[`ref`] -[`Page.GetPage`]|[`urls.RelRef`]|[`relref`] -[`Page.Ref`]||| -[`Page.RelRef`]||| -[`Shortcode.Ref`]||| -[`Shortcode.RelRef`]||| - -> [!note] -> Specify the logical path when using any of these methods, functions, or shortcodes. If you include a file extension or language identifier, Hugo will strip these values before finding the page in the logical tree. - -## Logical tree - -Just as file paths form a file tree, logical paths form a logical tree. - -A file tree: - -```text -content/ -└── s1/ - ├── p1/ - │ └── index.md - └── p2.md -``` - -The same content represented as a logical tree: - -```text -content/ -└── s1/ - ├── p1 - └── p2 -``` - -A key difference between these trees is the relative path from p1 to p2: - -- In the file tree, the relative path from p1 to p2 is `../p2.md` -- In the logical tree, the relative path is `p2` - -> [!note] -> Remember to use the logical path when using any of the methods, functions, or shortcodes listed in the previous section. If you include a file extension or language identifier, Hugo will strip these values before finding the page in the logical tree. - -[`Page.GetPage`]: /methods/page/getpage/ -[`Page.Ref`]: /methods/page/ref/ -[`Page.RelRef`]: /methods/page/relref/ -[`ref`]: /shortcodes/ref/ -[`relref`]: /shortcodes/relref/ -[`Shortcode.Ref`]: /methods/shortcode/ref -[`Shortcode.RelRef`]: /methods/shortcode/relref -[`Site.GetPage`]: /methods/site/getpage/ -[`urls.Ref`]: /functions/urls/ref/ -[`urls.RelRef`]: /functions/urls/relref/ -[v0.123.0]: https://github.com/gohugoio/hugo/releases/tag/v0.123.0 -[v0.92.0]: https://github.com/gohugoio/hugo/releases/tag/v0.92.0 diff --git a/docs/content/en/methods/page/Permalink.md b/docs/content/en/methods/page/Permalink.md deleted file mode 100644 index cc74f3342..000000000 --- a/docs/content/en/methods/page/Permalink.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: Permalink -description: Returns the permalink of the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [PAGE.Permalink] ---- - -Site configuration: - -{{< code-toggle file=hugo >}} -title = 'Documentation' -baseURL = 'https://example.org/docs/' -{{< /code-toggle >}} - -Template: - -```go-html-template -{{ $page := .Site.GetPage "/about" }} -{{ $page.Permalink }} → https://example.org/docs/about/ -``` diff --git a/docs/content/en/methods/page/Plain.md b/docs/content/en/methods/page/Plain.md deleted file mode 100644 index 65d11166e..000000000 --- a/docs/content/en/methods/page/Plain.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: Plain -description: Returns the rendered content of the given page, removing all HTML tags. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [PAGE.Plain] ---- - -The `Plain` method on a `Page` object renders Markdown and [shortcodes](g) to HTML, then strips the HTML [tags]. It does not strip HTML [entities]. - -To prevent Go's [html/template] package from escaping HTML entities, pass the result through the [`htmlUnescape`] function. - -```go-html-template -{{ .Plain | htmlUnescape }} -``` - -[html/template]: https://pkg.go.dev/html/template -[entities]: https://developer.mozilla.org/en-US/docs/Glossary/Entity -[tags]: https://developer.mozilla.org/en-US/docs/Glossary/Tag -[`htmlUnescape`]: /functions/transform/htmlunescape/ diff --git a/docs/content/en/methods/page/PlainWords.md b/docs/content/en/methods/page/PlainWords.md deleted file mode 100644 index 5749a21f9..000000000 --- a/docs/content/en/methods/page/PlainWords.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: PlainWords -description: Calls the Plain method, splits the result into a slice of words, and returns the slice. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: '[]string' - signatures: [PAGE.PlainWords] ---- - -The `PlainWords` method on a `Page` object calls the [`Plain`] method, then uses Go's [`strings.Fields`] function to split the result into words. - -> [!note] -> `Fields` splits the string `s` around each instance of one or more consecutive whitespace characters, as defined by [`unicode.IsSpace`], returning a slice of substrings of `s` or an empty slice if `s` contains only whitespace. - -As a result, elements within the slice may contain leading or trailing punctuation. - -```go-html-template -{{ .PlainWords }} -``` - -To determine the approximate number of unique words on a page: - -```go-html-template -{{ .PlainWords | uniq }} → 42 -``` - -[`Plain`]: /methods/page/plain/ -[`strings.Fields`]: https://pkg.go.dev/strings#Fields -[`unicode.IsSpace`]: https://pkg.go.dev/unicode#IsSpace diff --git a/docs/content/en/methods/page/Prev.md b/docs/content/en/methods/page/Prev.md deleted file mode 100644 index 5a6e2162d..000000000 --- a/docs/content/en/methods/page/Prev.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -title: Prev -description: Returns the previous page in a site's collection of regular pages, relative to the current page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Page - signatures: [PAGE.Prev] ---- - -{{% include "/_common/methods/page/next-and-prev.md" %}} diff --git a/docs/content/en/methods/page/PrevInSection.md b/docs/content/en/methods/page/PrevInSection.md deleted file mode 100644 index 14d3ca082..000000000 --- a/docs/content/en/methods/page/PrevInSection.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -title: PrevInSection -description: Returns the previous regular page in a section, relative to the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Page - signatures: [PAGE.PrevInSection] ---- - -{{% include "/_common/methods/page/nextinsection-and-previnsection.md" %}} diff --git a/docs/content/en/methods/page/PublishDate.md b/docs/content/en/methods/page/PublishDate.md deleted file mode 100644 index 7500a08aa..000000000 --- a/docs/content/en/methods/page/PublishDate.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: PublishDate -description: Returns the publish date of the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: time.Time - signatures: [PAGE.PublishDate] ---- - -By default, Hugo excludes pages with future publish dates when building your site. To include future pages, use the `--buildFuture` command line flag. - -Set the publish date in front matter: - -{{< code-toggle file=content/news/article-1.md fm=true >}} -title = 'Article 1' -publishDate = 2023-10-19T00:40:04-07:00 -{{< /code-toggle >}} - -The publish date is a [time.Time] value. Format and localize the value with the [`time.Format`] function, or use it with any of the [time methods]. - -```go-html-template -{{ .PublishDate | time.Format ":date_medium" }} → Oct 19, 2023 -``` - -In the example above we explicitly set the publish date in front matter. With Hugo's default configuration, the `PublishDate` method returns the front matter value. This behavior is configurable, allowing you to set fallback values if the publish date is not defined in front matter. See [details]. - -[`time.Format`]: /functions/time/format/ -[details]: /configuration/front-matter/#dates -[time methods]: /methods/time/ -[time.Time]: https://pkg.go.dev/time#Time diff --git a/docs/content/en/methods/page/RawContent.md b/docs/content/en/methods/page/RawContent.md deleted file mode 100644 index 41215ef53..000000000 --- a/docs/content/en/methods/page/RawContent.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: RawContent -description: Returns the raw content of the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [PAGE.RawContent] ---- - -The `RawContent` method on a `Page` object returns the raw content. The raw content does not include front matter. - -```go-html-template -{{ .RawContent }} -``` - -This is useful when rendering a page in a plain text [output format](g). - -> [!note] -> [Shortcodes](g) within the content are not rendered. To get the raw content with shortcodes rendered, use the [`RenderShortcodes`] method on a `Page` object. - -[`RenderShortcodes`]: /methods/page/rendershortcodes/ diff --git a/docs/content/en/methods/page/ReadingTime.md b/docs/content/en/methods/page/ReadingTime.md deleted file mode 100644 index 1bd7dea31..000000000 --- a/docs/content/en/methods/page/ReadingTime.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: ReadingTime -description: Returns the estimated reading time, in minutes, for the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: int - signatures: [PAGE.ReadingTime] ---- - -The estimated reading time is calculated by dividing the number of words in the content by the reading speed. - -By default, Hugo assumes a reading speed of 212 words per minute. For CJK languages, it assumes 500 words per minute. - -```go-html-template -{{ printf "Estimated reading time: %d minutes" .ReadingTime }} -``` - -Reading speed varies by language. Create language-specific estimated reading times on your multilingual site using site parameters. - -{{< code-toggle file=hugo >}} -[languages] - [languages.de] - contentDir = 'content/de' - languageCode = 'de-DE' - languageName = 'Deutsch' - weight = 2 - [languages.de.params] - reading_speed = 179 - [languages.en] - contentDir = 'content/en' - languageCode = 'en-US' - languageName = 'English' - weight = 1 - [languages.en.params] - reading_speed = 228 -{{< /code-toggle >}} - -Then in your template: - -```go-html-template -{{ $readingTime := div (float .WordCount) .Site.Params.reading_speed }} -{{ $readingTime = math.Ceil $readingTime }} -``` - -We cast the `.WordCount` to a float to obtain a float when we divide by the reading speed. Then round up to the nearest integer. diff --git a/docs/content/en/methods/page/Ref.md b/docs/content/en/methods/page/Ref.md deleted file mode 100644 index 35f9460ba..000000000 --- a/docs/content/en/methods/page/Ref.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: Ref -description: Returns the absolute URL of the page with the given path, language, and output format. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [PAGE.Ref OPTIONS] ---- - -## Usage - -The `Ref` method accepts a single argument: an options map. - -## Options - -{{% include "_common/ref-and-relref-options.md" %}} - -## Examples - -The following examples show the rendered output for a page on the English version of the site: - -```go-html-template -{{ $opts := dict "path" "/books/book-1" }} -{{ .Ref $opts }} → https://example.org/en/books/book-1/ - -{{ $opts := dict "path" "/books/book-1" "lang" "de" }} -{{ .Ref $opts }} → https://example.org/de/books/book-1/ - -{{ $opts := dict "path" "/books/book-1" "lang" "de" "outputFormat" "json" }} -{{ .Ref $opts }} → https://example.org/de/books/book-1/index.json -``` - -## Error handling - -{{% include "_common/ref-and-relref-error-handling.md" %}} diff --git a/docs/content/en/methods/page/RegularPages.md b/docs/content/en/methods/page/RegularPages.md deleted file mode 100644 index 761de3af5..000000000 --- a/docs/content/en/methods/page/RegularPages.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -title: RegularPages -description: Returns a collection of regular pages within the current section. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pages - signatures: [PAGE.RegularPages] ---- - -The `RegularPages` method on a `Page` object is available to these [page kinds](g): `home`, `section`, `taxonomy`, and `term`. The templates for these page kinds receive a page [collection](g) in [context](g), in the [default sort order](g). - -Range through the page collection in your template: - -```go-html-template -{{ range .RegularPages.ByTitle }} -

    {{ .Title }}

    -{{ end }} -``` - -Consider this content structure: - -```text -content/ -├── lessons/ -│ ├── lesson-1/ -│ │ ├── _index.md -│ │ ├── part-1.md -│ │ └── part-2.md -│ ├── lesson-2/ -│ │ ├── resources/ -│ │ │ ├── task-list.md -│ │ │ └── worksheet.md -│ │ ├── _index.md -│ │ ├── part-1.md -│ │ └── part-2.md -│ ├── _index.md -│ ├── grading-policy.md -│ └── lesson-plan.md -├── _index.md -├── contact.md -└── legal.md -``` - -When rendering the home page, the `RegularPages` method returns: - - contact.md - legal.md - -When rendering the lessons page, the `RegularPages` method returns: - - lessons/grading-policy.md - lessons/lesson-plan.md - -When rendering lesson-1, the `RegularPages` method returns: - - lessons/lesson-1/part-1.md - lessons/lesson-1/part-2.md - -When rendering lesson-2, the `RegularPages` method returns: - - lessons/lesson-2/part-1.md - lessons/lesson-2/part-2.md - lessons/lesson-2/resources/task-list.md - lessons/lesson-2/resources/worksheet.md - -In the last example, the collection includes pages in the resources subdirectory. That directory is not a [section](g)---it does not contain an `_index.md` file. Its contents are part of the lesson-2 section. - -> [!note] -> When used with the `Site` object, the `RegularPages` method recursively returns all regular pages within the site. See [details]. - -```go-html-template -{{ range .Site.RegularPages.ByTitle }} -

    {{ .Title }}

    -{{ end }} -``` - -[details]: /methods/site/regularpages/ diff --git a/docs/content/en/methods/page/RegularPagesRecursive.md b/docs/content/en/methods/page/RegularPagesRecursive.md deleted file mode 100644 index d85cd0b48..000000000 --- a/docs/content/en/methods/page/RegularPagesRecursive.md +++ /dev/null @@ -1,83 +0,0 @@ ---- -title: RegularPagesRecursive -description: Returns a collection of regular pages within the current section, and regular pages within all descendant sections. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pages - signatures: [PAGE.RegularPagesRecursive] ---- - -The `RegularPagesRecursive` method on a `Page` object is available to these [page kinds](g): `home`, `section`, `taxonomy`, and `term`. The templates for these page kinds receive a page [collection](g) in [context](g), in the [default sort order](g). - -Range through the page collection in your template: - -```go-html-template -{{ range .RegularPagesRecursive.ByTitle }} -

    {{ .Title }}

    -{{ end }} -``` - -Consider this content structure: - -```text -content/ -├── lessons/ -│ ├── lesson-1/ -│ │ ├── _index.md -│ │ ├── part-1.md -│ │ └── part-2.md -│ ├── lesson-2/ -│ │ ├── resources/ -│ │ │ ├── task-list.md -│ │ │ └── worksheet.md -│ │ ├── _index.md -│ │ ├── part-1.md -│ │ └── part-2.md -│ ├── _index.md -│ ├── grading-policy.md -│ └── lesson-plan.md -├── _index.md -├── contact.md -└── legal.md -``` - -When rendering the home page, the `RegularPagesRecursive` method returns: - - contact.md - lessons/grading-policy.md - legal.md - lessons/lesson-plan.md - lessons/lesson-2/part-1.md - lessons/lesson-1/part-1.md - lessons/lesson-2/part-2.md - lessons/lesson-1/part-2.md - lessons/lesson-2/resources/task-list.md - lessons/lesson-2/resources/worksheet.md - -When rendering the lessons page, the `RegularPagesRecursive` method returns: - - lessons/grading-policy.md - lessons/lesson-plan.md - lessons/lesson-2/part-1.md - lessons/lesson-1/part-1.md - lessons/lesson-2/part-2.md - lessons/lesson-1/part-2.md - lessons/lesson-2/resources/task-list.md - lessons/lesson-2/resources/worksheet.md - -When rendering lesson-1, the `RegularPagesRecursive` method returns: - - lessons/lesson-1/part-1.md - lessons/lesson-1/part-2.md - -When rendering lesson-2, the `RegularPagesRecursive` method returns: - - lessons/lesson-2/part-1.md - lessons/lesson-2/part-2.md - lessons/lesson-2/resources/task-list.md - lessons/lesson-2/resources/worksheet.md - -> [!note] -> The `RegularPagesRecursive` method is not available on a `Site` object. diff --git a/docs/content/en/methods/page/RelPermalink.md b/docs/content/en/methods/page/RelPermalink.md deleted file mode 100644 index a3c610d50..000000000 --- a/docs/content/en/methods/page/RelPermalink.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: RelPermalink -description: Returns the relative permalink of the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [PAGE.RelPermalink] ---- - -Site configuration: - -{{< code-toggle file=hugo >}} -title = 'Documentation' -baseURL = 'https://example.org/docs/' -{{< /code-toggle >}} - -Template: - -```go-html-template -{{ $page := .Site.GetPage "/about" }} -{{ $page.RelPermalink }} → /docs/about/ -``` diff --git a/docs/content/en/methods/page/RelRef.md b/docs/content/en/methods/page/RelRef.md deleted file mode 100644 index 7edab5740..000000000 --- a/docs/content/en/methods/page/RelRef.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: RelRef -description: Returns the relative URL of the page with the given path, language, and output format. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [PAGE.RelRef OPTIONS] ---- - -## Usage - -The `RelRef` method accepts a single argument: an options map. - -## Options - -{{% include "_common/ref-and-relref-options.md" %}} - -## Examples - -The following examples show the rendered output for a page on the English version of the site: - -```go-html-template -{{ $opts := dict "path" "/books/book-1" }} -{{ .RelRef $opts }} → /en/books/book-1/ - -{{ $opts := dict "path" "/books/book-1" "lang" "de" }} -{{ .RelRef $opts }} → /de/books/book-1/ - -{{ $opts := dict "path" "/books/book-1" "lang" "de" "outputFormat" "json" }} -{{ .RelRef $opts }} → /de/books/book-1/index.json -``` - -## Error handling - -{{% include "_common/ref-and-relref-error-handling.md" %}} diff --git a/docs/content/en/methods/page/Render.md b/docs/content/en/methods/page/Render.md deleted file mode 100644 index 10f7f9ca5..000000000 --- a/docs/content/en/methods/page/Render.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: Render -description: Renders the given template with the given page as context. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: template.HTML - signatures: [PAGE.Render NAME] -aliases: [/functions/render] ---- - -Typically used when ranging over a page collection, the `Render` method on a `Page` object renders the given template, passing the given page as context. - -```go-html-template -{{ range site.RegularPages }} -

    {{ .LinkTitle }}

    - {{ .Render "summary" }} -{{ end }} -``` - -In the example above, note that the template ("summary") is identified by its file name without directory or extension. - -Although similar to the [`partial`] function, there are key differences. - -`Render` method|`partial` function| -:--|:-- -The `Page` object is automatically passed to the given template. You cannot pass additional context.| You must specify the context, allowing you to pass a combination of objects, slices, maps, and scalars. -The path to the template is determined by the [content type](g).|You must specify the path to the template, relative to the `layouts/partials` directory. - -Consider this layout structure: - -```text -layouts/ -├── _default/ -│ ├── baseof.html -│ ├── home.html -│ ├── li.html <-- used for other content types -│ ├── list.html -│ ├── single.html -│ └── summary.html -└── books/ - ├── li.html <-- used when content type is "books" - └── summary.html -``` - -And this template: - -```go-html-template -
      - {{ range site.RegularPages.ByDate }} - {{ .Render "li" }} - {{ end }} -
    -``` - -When rendering content of type "books" the `Render` method calls: - -```text -layouts/books/li.html -``` - -For all other content types the `Render` methods calls: - -```text -layouts/_default/li.html -``` - -See [content views] for more examples. - -[content views]: /templates/content-view/ -[`partial`]: /functions/partials/include/ diff --git a/docs/content/en/methods/page/RenderShortcodes.md b/docs/content/en/methods/page/RenderShortcodes.md deleted file mode 100644 index d124606f0..000000000 --- a/docs/content/en/methods/page/RenderShortcodes.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -title: RenderShortcodes -description: Renders all shortcodes in the content of the given page, preserving the surrounding markup. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: template.HTML - signatures: [PAGE.RenderShortcodes] ---- - -{{< new-in 0.117.0 />}} - -Use this method in shortcode templates to compose a page from multiple content files, while preserving a global context for footnotes and the table of contents. - -For example: - -```go-html-template {file="layouts/shortcodes/include.html" copy=true} -{{ with .Get 0 }} - {{ with $.Page.GetPage . }} - {{- .RenderShortcodes }} - {{ else }} - {{ errorf "The %q shortcode was unable to find %q. See %s" $.Name . $.Position }} - {{ end }} -{{ else }} - {{ errorf "The %q shortcode requires a positional parameter indicating the logical path of the file to include. See %s" .Name .Position }} -{{ end }} -``` - -Then call the shortcode in your Markdown: - -```text {file="content/about.md"} -{{%/* include "/snippets/services" */%}} -{{%/* include "/snippets/values" */%}} -{{%/* include "/snippets/leadership" */%}} -``` - -Each of the included Markdown files can contain calls to other shortcodes. - -## Shortcode notation - -In the example above it's important to understand the difference between the two delimiters used when calling a shortcode: - -- `{{}}` tells Hugo that the rendered shortcode does not need further processing. For example, the shortcode content is HTML. -- `{{%/* myshortcode */%}}` tells Hugo that the rendered shortcode needs further processing. For example, the shortcode content is Markdown. - -Use the latter for the "include" shortcode described above. - -## Further explanation - -To understand what is returned by the `RenderShortcodes` method, consider this content file - -```text {file="content/about.md"} -+++ -title = 'About' -date = 2023-10-07T12:28:33-07:00 -+++ - -{{}} - -An *emphasized* word. -``` - -With this template code: - -```go-html-template -{{ $p := site.GetPage "/about" }} -{{ $p.RenderShortcodes }} -``` - -Hugo renders this:; - -```html -https://example.org/privacy/ - -An *emphasized* word. -``` - -Note that the shortcode within the content file was rendered, but the surrounding Markdown was preserved. - -## Limitations - -The primary use case for `.RenderShortcodes` is inclusion of Markdown content. If you try to use `.RenderShortcodes` inside `HTML` blocks when inside Markdown, you will get a warning similar to this: - -```text -WARN .RenderShortcodes detected inside HTML block in "/content/mypost.md"; this may not be what you intended ... -``` - -The above warning can be turned off is this is what you really want. diff --git a/docs/content/en/methods/page/RenderString.md b/docs/content/en/methods/page/RenderString.md deleted file mode 100644 index c7774774c..000000000 --- a/docs/content/en/methods/page/RenderString.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: RenderString -description: Renders markup to HTML. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: template.HTML - signatures: ['PAGE.RenderString [OPTIONS] MARKUP'] -aliases: [/functions/renderstring] ---- - -```go-html-template -{{ $s := "An *emphasized* word" }} -{{ $s | .RenderString }} → An emphasized word -``` - -This method takes an optional map of options: - -display -: (`string`) Specify either `inline` or `block`. If `inline`, removes surrounding `p` tags from short snippets. Default is `inline`. - -markup -: (`string`) Specify a [markup identifier] for the provided markup. Default is the `markup` front matter value, falling back to the value derived from the page's file extension. - -Render with the default markup renderer: - -```go-html-template -{{ $s := "An *emphasized* word" }} -{{ $s | .RenderString }} → An emphasized word - -{{ $opts := dict "display" "block" }} -{{ $s | .RenderString $opts }} →

    An emphasized word

    -``` - -Render with [Pandoc]: - -```go-html-template -{{ $s := "H~2~O" }} - -{{ $opts := dict "markup" "pandoc" }} -{{ $s | .RenderString $opts }} → H2O - -{{ $opts := dict "display" "block" "markup" "pandoc" }} -{{ .RenderString $opts $s }} →

    H2O

    -``` - -[markup identifier]: /content-management/formats/#classification -[pandoc]: https://www.pandoc.org/ diff --git a/docs/content/en/methods/page/Resources.md b/docs/content/en/methods/page/Resources.md deleted file mode 100644 index dd472de88..000000000 --- a/docs/content/en/methods/page/Resources.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -title: Resources -description: Returns a collection of page resources. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: resource.Resources - signatures: [PAGE.Resources] ---- - -The `Resources` method on a `Page` object returns a collection of page resources. A page resource is a file within a [page bundle](g). - -To work with global or remote resources, see the [`resources`] functions. - -## Methods - -### ByType - -(`resource.Resources`) Returns a collection of page resources of the given [media type], or nil if none found. The media type is typically one of `image`, `text`, `audio`, `video`, or `application`. - -```go-html-template -{{ range .Resources.ByType "image" }} - -{{ end }} -``` - -When working with global resources instead of page resources, use the [`resources.ByType`] function. - -### Get - -(`resource.Resource`) Returns a page resource from the given path, or nil if none found. - -```go-html-template -{{ with .Resources.Get "images/a.jpg" }} - -{{ end }} -``` - -When working with global resources instead of page resources, use the [`resources.Get`] function. - -### GetMatch - -(`resource.Resource`) Returns the first page resource from paths matching the given [glob](g) pattern, or nil if none found. - -```go-html-template -{{ with .Resources.GetMatch "images/*.jpg" }} - -{{ end }} -``` - -When working with global resources instead of page resources, use the [`resources.GetMatch`] function. - -### Match - -(`resource.Resources`) Returns a collection of page resources from paths matching the given [glob](g) pattern, or nil if none found. - -```go-html-template -{{ range .Resources.Match "images/*.jpg" }} - -{{ end }} -``` - -When working with global resources instead of page resources, use the [`resources.Match`] function. - -### Mount - -{{< new-in 0.140.0 />}} - -(`ResourceGetter`) Mounts the given resources from the two arguments base (`string`) to the given target path (`string`) and returns an object that implements [Get](#get). Note that leading slashes in target marks an absolute path. Relative target paths allows you to mount resources relative to another set, e.g. a [Page bundle](/content-management/page-bundles/): - -```go-html-template -{{ $common := resources.Match "/js/headlessui/*.*" }} -{{ $importContext := (slice $.Page ($common.Mount "/js/headlessui" ".")) }} -``` - -This method is currently only useful in [js.Batch](/functions/js/batch/#import-context). - -## Pattern matching - -With the `GetMatch` and `Match` methods, Hugo determines a match using a case-insensitive [glob](g) pattern. - -{{% include "/_common/glob-patterns.md" %}} - -[`resources.ByType`]: /functions/resources/ByType/ -[`resources.GetMatch`]: /functions/resources/ByType/ -[`resources.Get`]: /functions/resources/ByType/ -[`resources.Match`]: /functions/resources/ByType/ -[`resources`]: /functions/resources/ -[media type]: https://en.wikipedia.org/wiki/Media_type diff --git a/docs/content/en/methods/page/Scratch.md b/docs/content/en/methods/page/Scratch.md deleted file mode 100644 index 61c5dc19e..000000000 --- a/docs/content/en/methods/page/Scratch.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -title: Scratch -description: Returns a "scratch pad" to store and manipulate data, scoped to the current page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: maps.Scratch - signatures: [PAGE.Scratch] -expiryDate: 2026-11-18 # deprecated 2024-11-18 (soft) ---- - -{{< deprecated-in 0.138.0 >}} -Use the [`PAGE.Store`] method instead. - -This is a soft deprecation. This method will be removed in a future release, but the removal date has not been established. Although Hugo will not emit a warning if you continue to use this method, you should begin using `PAGE.Store` as soon as possible. - -Beginning with v0.138.0 the `PAGE.Scratch` method is aliased to `PAGE.Store`. - -[`PAGE.Store`]: /methods/page/store/ -{{< /deprecated-in >}} diff --git a/docs/content/en/methods/page/Section.md b/docs/content/en/methods/page/Section.md deleted file mode 100644 index 04c6a8a24..000000000 --- a/docs/content/en/methods/page/Section.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: Section -description: Returns the name of the top-level section in which the given page resides. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [PAGE.Section] ---- - -{{% glossary-term section %}} - -With this content structure: - -```text -content/ -├── lessons/ -│ ├── math/ -│ │ ├── _index.md -│ │ ├── lesson-1.md -│ │ └── lesson-2.md -│ └── _index.md -└── _index.md -``` - -When rendering lesson-1.md: - -```go-html-template -{{ .Section }} → lessons -``` - -In the example above "lessons" is the top-level section. - -The `Section` method is often used with the [`where`] function to build a page collection. - -```go-html-template -{{ range where .Site.RegularPages "Section" "lessons" }} -

    {{ .LinkTitle }}

    -{{ end }} -``` - -This is similar to using the [`Type`] method with the `where` function - -```go-html-template -{{ range where .Site.RegularPages "Type" "lessons" }} -

    {{ .LinkTitle }}

    -{{ end }} -``` - -However, if the `type` field in front matter has been defined on one or more pages, the page collection based on `Type` will be different than the page collection based on `Section`. - -[`where`]: /functions/collections/where/ -[`Type`]: /methods/page/type/ diff --git a/docs/content/en/methods/page/Sections.md b/docs/content/en/methods/page/Sections.md deleted file mode 100644 index 12f0a8c24..000000000 --- a/docs/content/en/methods/page/Sections.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -title: Sections -description: Returns a collection of section pages, one for each immediate descendant section of the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pages - signatures: [PAGE.Sections] ---- - -The `Sections` method on a `Page` object is available to these [page kinds](g): `home`, `section`, and `taxonomy`. The templates for these page kinds receive a page [collection](g) in [context](g), in the [default sort order](g). - -With this content structure: - -```text -content/ -├── auctions/ -│ ├── 2023-11/ -│ │ ├── _index.md <-- front matter: weight = 202311 -│ │ ├── auction-1.md -│ │ └── auction-2.md -│ ├── 2023-12/ -│ │ ├── _index.md <-- front matter: weight = 202312 -│ │ ├── auction-3.md -│ │ └── auction-4.md -│ ├── _index.md <-- front matter: weight = 30 -│ ├── bidding.md -│ └── payment.md -├── books/ -│ ├── _index.md <-- front matter: weight = 20 -│ ├── book-1.md -│ └── book-2.md -├── films/ -│ ├── _index.md <-- front matter: weight = 10 -│ ├── film-1.md -│ └── film-2.md -└── _index.md -``` - -And this template: - -```go-html-template -{{ range .Sections.ByWeight }} -

    {{ .LinkTitle }}

    -{{ end }} -``` - -On the home page, Hugo renders: - -```html -

    Films

    -

    Books

    -

    Auctions

    -``` - -On the auctions page, Hugo renders: - -```html -

    Auctions in November 2023

    -

    Auctions in December 2023

    -``` diff --git a/docs/content/en/methods/page/Site.md b/docs/content/en/methods/page/Site.md deleted file mode 100644 index 4649e5e00..000000000 --- a/docs/content/en/methods/page/Site.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: Site -description: Returns the Site object. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.siteWrapper - signatures: [PAGE.Site] ---- - -See [Site methods]. - -[Site methods]: /methods/site/ - -```go-html-template -{{ .Site.Title }} -``` diff --git a/docs/content/en/methods/page/Sitemap.md b/docs/content/en/methods/page/Sitemap.md deleted file mode 100644 index bb1360493..000000000 --- a/docs/content/en/methods/page/Sitemap.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -title: Sitemap -description: Returns the sitemap settings for the given page as defined in front matter, falling back to the sitemap settings as defined in the site configuration. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: config.SitemapConfig - signatures: [PAGE.Sitemap] ---- - -Access to the `Sitemap` method on a `Page` object is restricted to [sitemap templates]. - -## Methods - -### ChangeFreq - -(`string`) How frequently a page is likely to change. Valid values are `always`, `hourly`, `daily`, `weekly`, `monthly`, `yearly`, and `never`. With the default value of `""` Hugo will omit this field from the sitemap. See [details](https://www.sitemaps.org/protocol.html#changefreqdef). - -```go-html-template -{{ .Sitemap.ChangeFreq }} -``` - -### Disable - -{{< new-in 0.125.0 />}} - -(`bool`) Whether to disable page inclusion. Default is `false`. Set to `true` in front matter to exclude the page. - -```go-html-template -{{ .Sitemap.Disable }} -``` - -### Priority - -(`float`) The priority of a page relative to any other page on the site. Valid values range from 0.0 to 1.0. With the default value of `-1` Hugo will omit this field from the sitemap. See [details](https://www.sitemaps.org/protocol.html#prioritydef). - -```go-html-template -{{ .Sitemap.Priority }} -``` - -## Example - -With this site configuration: - -{{< code-toggle file=hugo >}} -[sitemap] -changeFreq = 'monthly' -{{< /code-toggle >}} - -And this content: - -{{< code-toggle file=content/news.md fm=true >}} -title = 'News' -[sitemap] -changeFreq = 'hourly' -{{< /code-toggle >}} - -And this simplistic sitemap template: - -```xml {file="layouts/_default/sitemap.xml"} -{{ printf "" | safeHTML }} - - {{ range .Pages }} - - {{ .Permalink }} - {{ if not .Lastmod.IsZero }} - {{ .Lastmod.Format "2006-01-02T15:04:05-07:00" | safeHTML }} - {{ end }} - {{ with .Sitemap.ChangeFreq }} - {{ . }} - {{ end }} - - {{ end }} - -``` - -The change frequency will be `hourly` for the news page, and `monthly` for other pages. - -[sitemap templates]: /templates/sitemap/ diff --git a/docs/content/en/methods/page/Sites.md b/docs/content/en/methods/page/Sites.md deleted file mode 100644 index 8677226d7..000000000 --- a/docs/content/en/methods/page/Sites.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: Sites -description: Returns a collection of all Site objects, one for each language, ordered by language weight. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Sites - signatures: [PAGE.Sites] ---- - -This is a convenience method to access `.Site.Sites`. - -With this site configuration: - -{{< code-toggle file=hugo >}} -defaultContentLanguage = 'de' -defaultContentLanguageInSubdir = false - -[languages.de] -languageCode = 'de-DE' -languageDirection = 'ltr' -languageName = 'Deutsch' -title = 'Projekt Dokumentation' -weight = 1 - -[languages.en] -languageCode = 'en-US' -languageDirection = 'ltr' -languageName = 'English' -title = 'Project Documentation' -weight = 2 -{{< /code-toggle >}} - -This template: - -```go-html-template - -``` - -Produces a list of links to each home page: - -```html - -``` - -To render a link to the home page of the site corresponding to the default content language: - -```go-html-template -{{ with .Sites.Default }} - {{ .Title }} -{{ end }} -``` - -This is equivalent to: - -```go-html-template -{{ with index .Sites 0 }} - {{ .Title }} -{{ end }} -``` diff --git a/docs/content/en/methods/page/Slug.md b/docs/content/en/methods/page/Slug.md deleted file mode 100644 index 34000b660..000000000 --- a/docs/content/en/methods/page/Slug.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: Slug -description: Returns the URL slug of the given page as defined in front matter. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [PAGE.Slug] ---- - -{{< code-toggle file=content/recipes/spicy-tuna-hand-rolls.md fm=true >}} -title = 'How to make spicy tuna hand rolls' -slug = 'sushi' -{{< /code-toggle >}} - -This page will be served from: - - https://example.org/recipes/sushi - -To get the slug value within a template: - -```go-html-template -{{ .Slug }} → sushi -``` diff --git a/docs/content/en/methods/page/Store.md b/docs/content/en/methods/page/Store.md deleted file mode 100644 index 0b1049b0a..000000000 --- a/docs/content/en/methods/page/Store.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: Store -description: Returns a "scratch pad" to store and manipulate data, scoped to the current page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: maps.Scratch - signatures: [PAGE.Store] -aliases: [/functions/store/,/extras/scratch/,/doc/scratch/,/functions/scratch] ---- - -Use the `Store` method on a `Page` object to create a [scratch pad](g) to store and manipulate data, scoped to the current page. To create a scratch pad with a different [scope](g), refer to the [scope](#scope) section below. - -{{% include "_common/store-methods.md" %}} - -{{% include "_common/scratch-pad-scope.md" %}} - -## Determinate values - -The `Store` method is often used to set scratch pad values within a shortcode, a partial template called by a shortcode, or by a Markdown render hook. In all three cases, the scratch pad values are indeterminate until Hugo renders the page content. - -If you need to access a scratch pad value from a parent template, and the parent template has not yet rendered the page content, you can trigger content rendering by assigning the returned value to a [noop](g) variable: - -```go-html-template -{{ $noop := .Content }} -{{ .Store.Get "mykey" }} -``` - -You can also trigger content rendering with the `ContentWithoutSummary`, `FuzzyWordCount`, `Len`, `Plain`, `PlainWords`, `ReadingTime`, `Summary`, `Truncated`, and `WordCount` methods. For example: - -```go-html-template -{{ $noop := .WordCount }} -{{ .Store.Get "mykey" }} -``` diff --git a/docs/content/en/methods/page/Summary.md b/docs/content/en/methods/page/Summary.md deleted file mode 100644 index 9158e571d..000000000 --- a/docs/content/en/methods/page/Summary.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -title: Summary -description: Returns the summary of the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: template.HTML - signatures: [PAGE.Summary] ---- - - - - - - -You can define a [summary] manually, in front matter, or automatically. A manual summary takes precedence over a front matter summary, and a front matter summary takes precedence over an automatic summary. - -To list the pages in a section with a summary beneath each link: - -```go-html-template -{{ range .Pages }} -

    {{ .LinkTitle }}

    - {{ .Summary }} -{{ end }} -``` - -Depending on content length and how you define the summary, the summary may be equivalent to the content itself. To determine whether the content length exceeds the summary length, use the [`Truncated`] method on a `Page` object. This is useful for conditionally rendering a “read more” link: - -```go-html-template -{{ range .Pages }} -

    {{ .LinkTitle }}

    - {{ .Summary }} - {{ if .Truncated }} - Read more... - {{ end }} -{{ end }} -``` - -> [!note] -> The `Truncated` method returns `false` if you define the summary in front matter. - -[`Truncated`]: /methods/page/truncated -[summary]: /content-management/summaries/ diff --git a/docs/content/en/methods/page/TableOfContents.md b/docs/content/en/methods/page/TableOfContents.md deleted file mode 100644 index 7ec9fe614..000000000 --- a/docs/content/en/methods/page/TableOfContents.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: TableOfContents -description: Returns a table of contents for the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: template.HTML - signatures: [PAGE.TableOfContents] -aliases: [/content-management/toc/] ---- - -The `TableOfContents` method on a `Page` object returns an ordered or unordered list of the Markdown [ATX] and [setext] headings within the page content. - -[atx]: https://spec.commonmark.org/0.30/#atx-headings -[setext]: https://spec.commonmark.org/0.30/#setext-headings - -This template code: - -```go-html-template -{{ .TableOfContents }} -``` - -Produces this HTML: - -```html - -``` - -By default, the `TableOfContents` method returns an unordered list of level 2 and level 3 headings. You can adjust this in your site configuration: - -{{< code-toggle file=hugo >}} -[markup.tableOfContents] -endLevel = 3 -ordered = false -startLevel = 2 -{{< /code-toggle >}} diff --git a/docs/content/en/methods/page/Title.md b/docs/content/en/methods/page/Title.md deleted file mode 100644 index dae4ba6dd..000000000 --- a/docs/content/en/methods/page/Title.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: Title -description: Returns the title of the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [PAGE.Title] ---- - -With pages backed by a file, the `Title` method returns the `title` field as defined in front matter: - -{{< code-toggle file=content/about.md fm=true >}} -title = 'About us' -{{< /code-toggle >}} - -```go-html-template -{{ .Title }} → About us -``` - -With section, taxonomy, and term pages not backed by a file, the `Title` method returns the section name, capitalized and pluralized. You can disable these transformations by setting [`capitalizeListTitles`] and [`pluralizeListTitles`] in your site configuration. For example: - -{{< code-toggle file=hugo >}} -capitalizeListTitles = false -pluralizeListTitles = false -{{< /code-toggle >}} - -You can change the capitalization style in your site configuration to one of `ap`, `chicago`, `go`, `firstupper`, or `none`. For example: - -{{< code-toggle file=hugo >}} -titleCaseStyle = "firstupper" -{{< /code-toggle >}} - - See [details]. - -[`capitalizeListTitles`]: /configuration/all/#capitalizelisttitles -[`pluralizeListTitles`]: /configuration/all/#pluralizelisttitles -[details]: /configuration/all/#title-case-style diff --git a/docs/content/en/methods/page/TranslationKey.md b/docs/content/en/methods/page/TranslationKey.md deleted file mode 100644 index 1e930687e..000000000 --- a/docs/content/en/methods/page/TranslationKey.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -title: TranslationKey -description: Returns the translation key of the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [PAGE.TranslationKey] ---- - -The translation key creates a relationship between all translations of a given page. The translation key is derived from the file path, or from the `translationKey` parameter if defined in front matter. - -With this site configuration: - -{{< code-toggle file=hugo >}} -defaultContentLanguage = 'en' - -[languages.en] -contentDir = 'content/en' -languageCode = 'en-US' -languageName = 'English' -weight = 1 - -[languages.de] -contentDir = 'content/de' -languageCode = 'de-DE' -languageName = 'Deutsch' -weight = 2 -{{< /code-toggle >}} - -And this content: - -```text -content/ -├── de/ -│ ├── books/ -│ │ ├── buch-1.md -│ │ └── book-2.md -│ └── _index.md -├── en/ -│ ├── books/ -│ │ ├── book-1.md -│ │ └── book-2.md -│ └── _index.md -└── _index.md -``` - -And this front matter: - -{{< code-toggle file=content/en/books/book-1.md fm=true >}} -title = 'Book 1' -translationKey = 'foo' -{{< /code-toggle >}} - -{{< code-toggle file=content/de/books/buch-1.md fm=true >}} -title = 'Buch 1' -translationKey = 'foo' -{{< /code-toggle >}} - -When rendering either either of the pages above: - -```go-html-template -{{ .TranslationKey }} → page/foo -``` - -If the front matter of Book 2, in both languages, does not include a translation key: - -```go-html-template -{{ .TranslationKey }} → page/books/book-2 -``` diff --git a/docs/content/en/methods/page/Translations.md b/docs/content/en/methods/page/Translations.md deleted file mode 100644 index 4bab9fe11..000000000 --- a/docs/content/en/methods/page/Translations.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -title: Translations -description: Returns all translations of the given page, excluding the current language. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pages - signatures: [PAGE.Translations] ---- - -With this site configuration: - -{{< code-toggle file=hugo >}} -defaultContentLanguage = 'en' - -[languages.en] -contentDir = 'content/en' -languageCode = 'en-US' -languageName = 'English' -weight = 1 - -[languages.de] -contentDir = 'content/de' -languageCode = 'de-DE' -languageName = 'Deutsch' -weight = 2 - -[languages.fr] -contentDir = 'content/fr' -languageCode = 'fr-FR' -languageName = 'Français' -weight = 3 -{{< /code-toggle >}} - -And this content: - -```text -content/ -├── de/ -│ ├── books/ -│ │ ├── book-1.md -│ │ └── book-2.md -│ └── _index.md -├── en/ -│ ├── books/ -│ │ ├── book-1.md -│ │ └── book-2.md -│ └── _index.md -├── fr/ -│ ├── books/ -│ │ └── book-1.md -│ └── _index.md -└── _index.md -``` - -And this template: - -```go-html-template -{{ with .Translations }} - -{{ end }} -``` - -Hugo will render this list on the "Book 1" page of the English site: - -```html - -``` - -Hugo will render this list on the "Book 2" page of the English site: - -```html - -``` diff --git a/docs/content/en/methods/page/Truncated.md b/docs/content/en/methods/page/Truncated.md deleted file mode 100644 index 8c2573069..000000000 --- a/docs/content/en/methods/page/Truncated.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: Truncated -description: Reports whether the content length exceeds the summary length. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: bool - signatures: [PAGE.Truncated] ---- - -You can define a [summary] manually, in front matter, or automatically. A manual summary takes precedence over a front matter summary, and a front matter summary takes precedence over an automatic summary. - -[summary]: /content-management/summaries/ - -The `Truncated` method returns `true` if the content length exceeds the summary length. This is useful for conditionally rendering a "read more" link: - -```go-html-template -{{ range .Pages }} -

    {{ .LinkTitle }}

    - {{ .Summary }} - {{ if .Truncated }} - Read more... - {{ end }} -{{ end }} -``` - -> [!note] -> The `Truncated` method returns `false` if you define the summary in front matter. diff --git a/docs/content/en/methods/page/Type.md b/docs/content/en/methods/page/Type.md deleted file mode 100644 index 6f855fbe3..000000000 --- a/docs/content/en/methods/page/Type.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -title: Type -description: Returns the content type of the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [PAGE.Type] ---- - -The `Type` method on a `Page` object returns the [content type](g) of the given page. The content type is defined by the `type` field in front matter, or inferred from the top-level directory name if the `type` field in front matter is not defined. - -With this content structure: - -```text -content/ -├── auction/ -│ ├── _index.md -│ ├── item-1.md -│ └── item-2.md <-- front matter: type = books -├── books/ -│ ├── _index.md -│ ├── book-1.md -│ └── book-2.md -├── films/ -│ ├── _index.md -│ ├── film-1.md -│ └── film-2.md -└── _index.md -``` - -To list the books, regardless of [section](g): - -```go-html-template -{{ range where .Site.RegularPages.ByTitle "Type" "books" }} -

    {{ .Title }}

    -{{ end }} -``` - -Hugo renders this to; - -```html -

    Book 1

    -

    Book 2

    -

    Item 2

    -``` - -The `type` field in front matter is also useful for targeting a template. See [details]. - -[details]: /templates/lookup-order/#target-a-template diff --git a/docs/content/en/methods/page/Weight.md b/docs/content/en/methods/page/Weight.md deleted file mode 100644 index c14af0257..000000000 --- a/docs/content/en/methods/page/Weight.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: Weight -description: Returns the weight of the given page as defined in front matter. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: int - signatures: [PAGE.Weight] ---- - -The `Weight` method on a `Page` object returns the [weight](g) of the given page as defined in front matter. - -{{< code-toggle file=content/recipes/sushi.md fm=true >}} -title = 'How to make spicy tuna hand rolls' -weight = 42 -{{< /code-toggle >}} - -Page weight controls the position of a page within a collection that is sorted by weight. Assign weights using non-zero integers. Lighter items float to the top, while heavier items sink to the bottom. Unweighted or zero-weighted elements are placed at the end of the collection. - -Although rarely used within a template, you can access the value with: - -```go-html-template -{{ .Weight }} → 42 -``` diff --git a/docs/content/en/methods/page/WordCount.md b/docs/content/en/methods/page/WordCount.md deleted file mode 100644 index 3950244ca..000000000 --- a/docs/content/en/methods/page/WordCount.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: WordCount -description: Returns the number of words in the content of the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: int - signatures: [PAGE.WordCount] ---- - -```go-html-template -{{ .WordCount }} → 103 -``` - -To round up to nearest multiple of 100, use the [`FuzzyWordCount`] method. - -[`FuzzyWordCount`]: /methods/page/fuzzywordcount/ diff --git a/docs/content/en/methods/page/_index.md b/docs/content/en/methods/page/_index.md deleted file mode 100644 index c7ae7ad5d..000000000 --- a/docs/content/en/methods/page/_index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Page methods -linkTitle: Page -description: Use these methods with a Page object. -categories: [] -keywords: [] -aliases: [/variables/page/] ---- diff --git a/docs/content/en/methods/pager/First.md b/docs/content/en/methods/pager/First.md deleted file mode 100644 index 9cd58989b..000000000 --- a/docs/content/en/methods/pager/First.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: First -description: Returns the first pager in the pager collection. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pager - signatures: [PAGER.First] ---- - -Use the `First` method to build navigation between pagers. - -```go-html-template -{{ $pages := where site.RegularPages "Type" "posts" }} -{{ $paginator := .Paginate $pages }} - -{{ range $paginator.Pages }} -

    {{ .LinkTitle }}

    -{{ end }} - -{{ with $paginator }} -
      - {{ with .First }} -
    • First
    • - {{ end }} - {{ with .Prev }} -
    • Previous
    • - {{ end }} - {{ with .Next }} -
    • Next
    • - {{ end }} - {{ with .Last }} -
    • Last
    • - {{ end }} -
    -{{ end }} -``` diff --git a/docs/content/en/methods/pager/HasNext.md b/docs/content/en/methods/pager/HasNext.md deleted file mode 100644 index cf3688efd..000000000 --- a/docs/content/en/methods/pager/HasNext.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: HasNext -description: Reports whether there is a pager after the current pager. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: bool - signatures: [PAGER.HasNext] ---- - -Use the `HasNext` method to build navigation between pagers. - -```go-html-template -{{ $pages := where site.RegularPages "Type" "posts" }} -{{ $paginator := .Paginate $pages }} - -{{ range $paginator.Pages }} -

    {{ .LinkTitle }}

    -{{ end }} - -{{ with $paginator }} -
      - {{ with .First }} -
    • First
    • - {{ end }} - {{ if .HasPrev }} -
    • Previous
    • - {{ end }} - {{ if .HasNext }} -
    • Next
    • - {{ end }} - {{ with .Last }} -
    • Last
    • - {{ end }} -
    -{{ end }} -``` - -You can also write the above without using the `HasNext` method: - -```go-html-template -{{ $pages := where site.RegularPages "Type" "posts" }} -{{ $paginator := .Paginate $pages }} - -{{ range $paginator.Pages }} -

    {{ .LinkTitle }}

    -{{ end }} - -{{ with $paginator }} -
      - {{ with .First }} -
    • First
    • - {{ end }} - {{ with .Prev }} -
    • Previous
    • - {{ end }} - {{ with .Next }} -
    • Next
    • - {{ end }} - {{ with .Last }} -
    • Last
    • - {{ end }} -
    -{{ end }} -``` diff --git a/docs/content/en/methods/pager/HasPrev.md b/docs/content/en/methods/pager/HasPrev.md deleted file mode 100644 index 4b486b7c5..000000000 --- a/docs/content/en/methods/pager/HasPrev.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: HasPrev -description: Reports whether there is a pager before the current pager. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: bool - signatures: [PAGER.HasPrev] ---- - -Use the `HasPrev` method to build navigation between pagers. - -```go-html-template -{{ $pages := where site.RegularPages "Type" "posts" }} -{{ $paginator := .Paginate $pages }} - -{{ range $paginator.Pages }} -

    {{ .LinkTitle }}

    -{{ end }} - -{{ with $paginator }} -
      - {{ with .First }} -
    • First
    • - {{ end }} - {{ if .HasPrev }} -
    • Previous
    • - {{ end }} - {{ if .HasNext }} -
    • Next
    • - {{ end }} - {{ with .Last }} -
    • Last
    • - {{ end }} -
    -{{ end }} -``` - -You can also write the above without using the `HasPrev` method: - -```go-html-template -{{ $pages := where site.RegularPages "Type" "posts" }} -{{ $paginator := .Paginate $pages }} - -{{ range $paginator.Pages }} -

    {{ .LinkTitle }}

    -{{ end }} - -{{ with $paginator }} -
      - {{ with .First }} -
    • First
    • - {{ end }} - {{ with .Prev }} -
    • Previous
    • - {{ end }} - {{ with .Next }} -
    • Next
    • - {{ end }} - {{ with .Last }} -
    • Last
    • - {{ end }} -
    -{{ end }} -``` diff --git a/docs/content/en/methods/pager/Last.md b/docs/content/en/methods/pager/Last.md deleted file mode 100644 index 71dea183d..000000000 --- a/docs/content/en/methods/pager/Last.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: Last -description: Returns the last pager in the pager collection. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pager - signatures: [PAGER.Last] ---- - -Use the `Last` method to build navigation between pagers. - -```go-html-template -{{ $pages := where site.RegularPages "Type" "posts" }} -{{ $paginator := .Paginate $pages }} - -{{ range $paginator.Pages }} -

    {{ .LinkTitle }}

    -{{ end }} - -{{ with $paginator }} -
      - {{ with .First }} -
    • First
    • - {{ end }} - {{ with .Prev }} -
    • Previous
    • - {{ end }} - {{ with .Next }} -
    • Next
    • - {{ end }} - {{ with .Last }} -
    • Last
    • - {{ end }} -
    -{{ end }} -``` diff --git a/docs/content/en/methods/pager/Next.md b/docs/content/en/methods/pager/Next.md deleted file mode 100644 index d7ea9caa4..000000000 --- a/docs/content/en/methods/pager/Next.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: Next -description: Returns the next pager in the pager collection. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pager - signatures: [PAGER.Next] ---- - -Use the `Next` method to build navigation between pagers. - -```go-html-template -{{ $pages := where site.RegularPages "Type" "posts" }} -{{ $paginator := .Paginate $pages }} - -{{ range $paginator.Pages }} -

    {{ .LinkTitle }}

    -{{ end }} - -{{ with $paginator }} -
      - {{ with .First }} -
    • First
    • - {{ end }} - {{ with .Prev }} -
    • Previous
    • - {{ end }} - {{ with .Next }} -
    • Next
    • - {{ end }} - {{ with .Last }} -
    • Last
    • - {{ end }} -
    -{{ end }} -``` diff --git a/docs/content/en/methods/pager/NumberOfElements.md b/docs/content/en/methods/pager/NumberOfElements.md deleted file mode 100644 index 9f88126fc..000000000 --- a/docs/content/en/methods/pager/NumberOfElements.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: NumberOfElements -description: Returns the number of pages in the current pager. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: int - signatures: [PAGER.NumberOfElements] ---- - -```go-html-template -{{ $pages := where site.RegularPages "Type" "posts" }} -{{ $paginator := .Paginate $pages }} - -{{ range $paginator.Pages }} -

    {{ .LinkTitle }}

    -{{ end }} - -{{ with $paginator }} - {{ .NumberOfElements }} -{{ end }} -``` diff --git a/docs/content/en/methods/pager/PageGroups.md b/docs/content/en/methods/pager/PageGroups.md deleted file mode 100644 index 46f6d81cb..000000000 --- a/docs/content/en/methods/pager/PageGroups.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: PageGroups -description: Returns the page groups in the current pager. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.PagesGroup - signatures: [PAGER.PageGroups] ---- - -Use the `PageGroups` method with any of the [grouping methods]. - -[grouping methods]: /quick-reference/page-collections/#group - -```go-html-template -{{ $pages := where site.RegularPages "Type" "posts" }} -{{ $paginator := .Paginate ($pages.GroupByDate "Jan 2006") }} - -{{ range $paginator.PageGroups }} -

    {{ .Key }}

    - {{ range .Pages }} -

    {{ .LinkTitle }}

    - {{ end }} -{{ end }} - -{{ template "_internal/pagination.html" . }} -``` diff --git a/docs/content/en/methods/pager/PageNumber.md b/docs/content/en/methods/pager/PageNumber.md deleted file mode 100644 index 6d0b8e35d..000000000 --- a/docs/content/en/methods/pager/PageNumber.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: PageNumber -description: Returns the current pager's number within the pager collection. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: int - signatures: [PAGER.PageNumber] ---- - -Use the `PageNumber` method to build navigation between pagers. - -```go-html-template -{{ $pages := where site.RegularPages "Type" "posts" }} -{{ $paginator := .Paginate $pages }} - -{{ range $paginator.Pages }} -

    {{ .LinkTitle }}

    -{{ end }} - -{{ with $paginator }} - -{{ end }} -``` diff --git a/docs/content/en/methods/pager/PageSize.md b/docs/content/en/methods/pager/PageSize.md deleted file mode 100644 index 5aad88682..000000000 --- a/docs/content/en/methods/pager/PageSize.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: PageSize -description: Returns the number of pages per pager. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: int - signatures: [PAGER.PageSize] -expiryDate: 2026-06-09 # deprecated 2024-06-09 in v0.128.0 ---- - -{{< deprecated-in 0.128.0 >}} -Use [`PAGER.PagerSize`] instead. - -[`PAGER.PagerSize`]: /methods/pager/pagersize/ -{{< /deprecated-in >}} diff --git a/docs/content/en/methods/pager/PagerSize.md b/docs/content/en/methods/pager/PagerSize.md deleted file mode 100644 index b2397a3e8..000000000 --- a/docs/content/en/methods/pager/PagerSize.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: PagerSize -description: Returns the number of pages per pager. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: int - signatures: [PAGER.PagerSize] ---- - -{{< new-in 0.128.0 />}} - -The number of pages per pager is determined by the optional second argument passed to the [`Paginate`] method, falling back to the `pagerSize` as defined in your [site configuration]. - -[`Paginate`]: /methods/page/paginate/ -[site configuration]: /templates/pagination/#configuration - -```go-html-template -{{ $pages := where site.RegularPages "Type" "posts" }} -{{ $paginator := .Paginate $pages }} - -{{ range $paginator.Pages }} -

    {{ .LinkTitle }}

    -{{ end }} - -{{ with $paginator }} - {{ .PagerSize }} -{{ end }} -``` diff --git a/docs/content/en/methods/pager/Pagers.md b/docs/content/en/methods/pager/Pagers.md deleted file mode 100644 index e431069f4..000000000 --- a/docs/content/en/methods/pager/Pagers.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: Pagers -description: Returns the pagers collection. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.pagers - signatures: [PAGER.Pagers] ---- - -Use the `Pagers` method to build navigation between pagers. - -```go-html-template -{{ $pages := where site.RegularPages "Type" "posts" }} -{{ $paginator := .Paginate $pages }} - -{{ range $paginator.Pages }} -

    {{ .LinkTitle }}

    -{{ end }} - -{{ with $paginator }} - -{{ end }} -``` diff --git a/docs/content/en/methods/pager/Pages.md b/docs/content/en/methods/pager/Pages.md deleted file mode 100644 index 6e5772a48..000000000 --- a/docs/content/en/methods/pager/Pages.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -title: Pages -description: Returns the pages in the current pager. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pages - signatures: [PAGER.Pages] ---- - -```go-html-template -{{ $pages := where site.RegularPages "Type" "posts" }} -{{ $paginator := .Paginate $pages }} - -{{ range $paginator.Pages }} -

    {{ .LinkTitle }}

    -{{ end }} - -{{ template "_internal/pagination.html" . }} -``` diff --git a/docs/content/en/methods/pager/Prev.md b/docs/content/en/methods/pager/Prev.md deleted file mode 100644 index eb79f96e9..000000000 --- a/docs/content/en/methods/pager/Prev.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: Prev -description: Returns the previous pager in the pager collection. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pager - signatures: [PAGER.Prev] ---- - -Use the `Prev` method to build navigation between pagers. - -```go-html-template -{{ $pages := where site.RegularPages "Type" "posts" }} -{{ $paginator := .Paginate $pages }} - -{{ range $paginator.Pages }} -

    {{ .LinkTitle }}

    -{{ end }} - -{{ with $paginator }} -
      - {{ with .First }} -
    • First
    • - {{ end }} - {{ with .Prev }} -
    • Previous
    • - {{ end }} - {{ with .Next }} -
    • Next
    • - {{ end }} - {{ with .Last }} -
    • Last
    • - {{ end }} -
    -{{ end }} -``` diff --git a/docs/content/en/methods/pager/TotalNumberOfElements.md b/docs/content/en/methods/pager/TotalNumberOfElements.md deleted file mode 100644 index ad29a01f3..000000000 --- a/docs/content/en/methods/pager/TotalNumberOfElements.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: TotalNumberOfElements -description: Returns the number of pages in the pager collection. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: int - signatures: [PAGER.TotalNumberOfElements] ---- - -```go-html-template -{{ $pages := where site.RegularPages "Type" "posts" }} -{{ $paginator := .Paginate $pages }} - -{{ range $paginator.Pages }} -

    {{ .LinkTitle }}

    -{{ end }} - -{{ with $paginator }} - {{ .TotalNumberOfElements }} -{{ end }} -``` diff --git a/docs/content/en/methods/pager/TotalPages.md b/docs/content/en/methods/pager/TotalPages.md deleted file mode 100644 index 63da5d786..000000000 --- a/docs/content/en/methods/pager/TotalPages.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: TotalPages -description: Returns the number of pagers in the pager collection. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: int - signatures: [PAGER.TotalPages] ---- - -Use the `TotalPages` method to build navigation between pagers. - -```go-html-template -{{ $pages := where site.RegularPages "Type" "posts" }} -{{ $paginator := .Paginate $pages }} - -{{ range $paginator.Pages }} -

    {{ .LinkTitle }}

    -{{ end }} - -{{ with $paginator }} -

    Pager {{ .PageNumber }} of {{ .TotalPages }}

    -
      - {{ with .First }} -
    • First
    • - {{ end }} - {{ with .Prev }} -
    • Previous
    • - {{ end }} - {{ with .Next }} -
    • Next
    • - {{ end }} - {{ with .Last }} -
    • Last
    • - {{ end }} -
    -{{ end }} -``` diff --git a/docs/content/en/methods/pager/URL.md b/docs/content/en/methods/pager/URL.md deleted file mode 100644 index a3558ba7c..000000000 --- a/docs/content/en/methods/pager/URL.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: URL -description: Returns the URL of the current pager relative to the site root. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [PAGER.URL] ---- - -Use the `URL` method to build navigation between pagers. - -```go-html-template -{{ $pages := where site.RegularPages "Type" "posts" }} -{{ $paginator := .Paginate $pages }} - -{{ range $paginator.Pages }} -

    {{ .LinkTitle }}

    -{{ end }} - -{{ with $paginator }} -
      - {{ with .First }} -
    • First
    • - {{ end }} - {{ with .Prev }} -
    • Previous
    • - {{ end }} - {{ with .Next }} -
    • Next
    • - {{ end }} - {{ with .Last }} -
    • Last
    • - {{ end }} -
    -{{ end }} -``` diff --git a/docs/content/en/methods/pager/_index.md b/docs/content/en/methods/pager/_index.md deleted file mode 100644 index 7a79bf42f..000000000 --- a/docs/content/en/methods/pager/_index.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Pager methods -linkTitle: Pager -description: Use these methods with Pager objects when building navigation for a paginated list page. -keywords: [] ---- diff --git a/docs/content/en/methods/pages/ByDate.md b/docs/content/en/methods/pages/ByDate.md deleted file mode 100644 index 18f1b985e..000000000 --- a/docs/content/en/methods/pages/ByDate.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: ByDate -description: Returns the given page collection sorted by date in ascending order. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pages - signatures: [PAGES.ByDate] ---- - -When sorting by date, the value is determined by your [site configuration], defaulting to the `date` field in front matter. - -[site configuration]: /configuration/front-matter/#dates - -```go-html-template -{{ range .Pages.ByDate }} -

    {{ .LinkTitle }}

    -{{ end }} -``` - -To sort in descending order: - -```go-html-template -{{ range .Pages.ByDate.Reverse }} -

    {{ .LinkTitle }}

    -{{ end }} -``` diff --git a/docs/content/en/methods/pages/ByExpiryDate.md b/docs/content/en/methods/pages/ByExpiryDate.md deleted file mode 100644 index 703988c4e..000000000 --- a/docs/content/en/methods/pages/ByExpiryDate.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: ByExpiryDate -description: Returns the given page collection sorted by expiration date in ascending order. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pages - signatures: [PAGES.ByExpiryDate] ---- - -When sorting by expiration date, the value is determined by your [site configuration], defaulting to the `expiryDate` field in front matter. - -[site configuration]: /configuration/front-matter/#dates - -```go-html-template -{{ range .Pages.ByExpiryDate }} -

    {{ .LinkTitle }}

    -{{ end }} -``` - -To sort in descending order: - -```go-html-template -{{ range .Pages.ByExpiryDate.Reverse }} -

    {{ .LinkTitle }}

    -{{ end }} -``` diff --git a/docs/content/en/methods/pages/ByLanguage.md b/docs/content/en/methods/pages/ByLanguage.md deleted file mode 100644 index 36244eb4d..000000000 --- a/docs/content/en/methods/pages/ByLanguage.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: ByLanguage -description: Returns the given page collection sorted by language in ascending order. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pages - signatures: [PAGES.ByLanguage] ---- - -```go-html-template -{{ range .Site.AllPages.ByLanguage }} -

    {{ .LinkTitle }}

    -{{ end }} -``` - -To sort in descending order: - -```go-html-template -{{ range .Site.AllPages.ByLanguage.Reverse }} -

    {{ .LinkTitle }}

    -{{ end }} -``` diff --git a/docs/content/en/methods/pages/ByLastmod.md b/docs/content/en/methods/pages/ByLastmod.md deleted file mode 100644 index 3c03d2a6e..000000000 --- a/docs/content/en/methods/pages/ByLastmod.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: ByLastmod -description: Returns the given page collection sorted by last modification date in ascending order. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pages - signatures: [PAGES.ByLastmod] ---- - -When sorting by last modification date, the value is determined by your [site configuration], defaulting to the `lastmod` field in front matter. - -[site configuration]: /configuration/front-matter/#dates - -```go-html-template -{{ range .Pages.ByLastmod }} -

    {{ .LinkTitle }}

    -{{ end }} -``` - -To sort in descending order: - -```go-html-template -{{ range .Pages.ByLastmod.Reverse }} -

    {{ .LinkTitle }}

    -{{ end }} -``` diff --git a/docs/content/en/methods/pages/ByLength.md b/docs/content/en/methods/pages/ByLength.md deleted file mode 100644 index c47bf98ba..000000000 --- a/docs/content/en/methods/pages/ByLength.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: ByLength -description: Returns the given page collection sorted by content length in ascending order. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pages - signatures: [PAGES.ByLength] ---- - -```go-html-template -{{ range .Pages.ByLength }} -

    {{ .LinkTitle }}

    -{{ end }} -``` - -To sort in descending order: - -```go-html-template -{{ range .Pages.ByLength.Reverse }} -

    {{ .LinkTitle }}

    -{{ end }} -``` diff --git a/docs/content/en/methods/pages/ByLinkTitle.md b/docs/content/en/methods/pages/ByLinkTitle.md deleted file mode 100644 index 4a024d25a..000000000 --- a/docs/content/en/methods/pages/ByLinkTitle.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: ByLinkTitle -description: Returns the given page collection sorted by link title in ascending order, falling back to title if link title is not defined. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pages - signatures: [PAGES.ByLinkTitle] ---- - -```go-html-template -{{ range .Pages.ByLinkTitle }} -

    {{ .LinkTitle }}

    -{{ end }} -``` - -To sort in descending order: - -```go-html-template -{{ range .Pages.ByLinkTitle.Reverse }} -

    {{ .LinkTitle }}

    -{{ end }} -``` diff --git a/docs/content/en/methods/pages/ByParam.md b/docs/content/en/methods/pages/ByParam.md deleted file mode 100644 index 9544122a6..000000000 --- a/docs/content/en/methods/pages/ByParam.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: ByParam -description: Returns the given page collection sorted by the given parameter in ascending order. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pages - signatures: [PAGES.ByParam PARAM] ---- - -If the given parameter is not present in front matter, Hugo will use the matching parameter in your site configuration if present. - -```go-html-template -{{ range .Pages.ByParam "author" }} -

    {{ .LinkTitle }}

    -{{ end }} -``` - -To sort in descending order: - -```go-html-template -{{ range (.Pages.ByParam "author").Reverse }} -

    {{ .LinkTitle }}

    -{{ end }} -``` - -If the targeted parameter is nested, access the field using dot notation: - -```go-html-template -{{ range .Pages.ByParam "author.last_name" }} -

    {{ .LinkTitle }}

    -{{ end }} -``` diff --git a/docs/content/en/methods/pages/ByPublishDate.md b/docs/content/en/methods/pages/ByPublishDate.md deleted file mode 100644 index 3dde6fd95..000000000 --- a/docs/content/en/methods/pages/ByPublishDate.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: ByPublishDate -description: Returns the given page collection sorted by publish date in ascending order. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pages - signatures: [PAGES.ByPublishDate] ---- - -When sorting by publish date, the value is determined by your [site configuration], defaulting to the `publishDate` field in front matter. - -[site configuration]: /configuration/front-matter/#dates - -```go-html-template -{{ range .Pages.ByPublishDate }} -

    {{ .LinkTitle }}

    -{{ end }} -``` - -To sort in descending order: - -```go-html-template -{{ range .Pages.ByPublishDate.Reverse }} -

    {{ .LinkTitle }}

    -{{ end }} -``` diff --git a/docs/content/en/methods/pages/ByTitle.md b/docs/content/en/methods/pages/ByTitle.md deleted file mode 100644 index e10c41714..000000000 --- a/docs/content/en/methods/pages/ByTitle.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: ByTitle -description: Returns the given page collection sorted by title in ascending order. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pages - signatures: [PAGES.ByTitle] ---- - -```go-html-template -{{ range .Pages.ByTitle }} -

    {{ .Title }}

    -{{ end }} -``` - -To sort in descending order: - -```go-html-template -{{ range .Pages.ByTitle.Reverse }} -

    {{ .Title }}

    -{{ end }} -``` diff --git a/docs/content/en/methods/pages/ByWeight.md b/docs/content/en/methods/pages/ByWeight.md deleted file mode 100644 index ba255d3c3..000000000 --- a/docs/content/en/methods/pages/ByWeight.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: ByWeight -description: Returns the given page collection sorted by weight in ascending order. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pages - signatures: [PAGES.ByWeight] ---- - -Assign a [weight](g) to a page using the `weight` field in front matter. The weight must be a non-zero integer. Lighter items float to the top, while heavier items sink to the bottom. Unweighted or zero-weighted pages are placed at the end of the collection. - -```go-html-template -{{ range .Pages.ByWeight }} -

    {{ .LinkTitle }}

    -{{ end }} -``` - -To sort in descending order: - -```go-html-template -{{ range .Pages.ByWeight.Reverse }} -

    {{ .LinkTitle }}

    -{{ end }} -``` diff --git a/docs/content/en/methods/pages/GroupBy.md b/docs/content/en/methods/pages/GroupBy.md deleted file mode 100644 index aff0800e9..000000000 --- a/docs/content/en/methods/pages/GroupBy.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: GroupBy -description: Returns the given page collection grouped by the given field in ascending order. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.PagesGroup - signatures: ['PAGES.GroupBy FIELD [SORT]'] ---- - -{{% include "/_common/methods/pages/group-sort-order.md" %}} - -```go-html-template -{{ range .Pages.GroupBy "Section" }} -

    {{ .Key }}

    - -{{ end }} -``` - -To sort the groups in descending order: - -```go-html-template -{{ range .Pages.GroupBy "Section" "desc" }} -

    {{ .Key }}

    - -{{ end }} -``` diff --git a/docs/content/en/methods/pages/GroupByDate.md b/docs/content/en/methods/pages/GroupByDate.md deleted file mode 100644 index 7ef4843a4..000000000 --- a/docs/content/en/methods/pages/GroupByDate.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -title: GroupByDate -description: Returns the given page collection grouped by date in descending order. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.PagesGroup - signatures: ['PAGES.GroupByDate LAYOUT [SORT]'] ---- - -When grouping by date, the value is determined by your [site configuration], defaulting to the `date` field in front matter. - -The [layout string] has the same format as the layout string for the [`time.Format`] function. The resulting group key is [localized](g) for language and region. - -[`time.Format`]: /functions/time/format/ -[layout string]: #layout-string -[site configuration]: /configuration/front-matter/#dates - -{{% include "/_common/methods/pages/group-sort-order.md" %}} - -To group content by year and month: - -```go-html-template -{{ range .Pages.GroupByDate "January 2006" }} -

    {{ .Key }}

    - -{{ end }} -``` - -To sort the groups in ascending order: - -```go-html-template -{{ range .Pages.GroupByDate "January 2006" "asc" }} -

    {{ .Key }}

    - -{{ end }} -``` - -The pages within each group will also be sorted by date, either ascending or descending depending on the grouping option. To sort the pages within each group, use one of the sorting methods. For example, to sort the pages within each group by title: - -```go-html-template -{{ range .Pages.GroupByDate "January 2006" }} -

    {{ .Key }}

    - -{{ end }} -``` - -## Layout string - -{{% include "/_common/time-layout-string.md" %}} diff --git a/docs/content/en/methods/pages/GroupByExpiryDate.md b/docs/content/en/methods/pages/GroupByExpiryDate.md deleted file mode 100644 index d209e6c2b..000000000 --- a/docs/content/en/methods/pages/GroupByExpiryDate.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -title: GroupByExpiryDate -description: Returns the given page collection grouped by expiration date in descending order. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.PagesGroup - signatures: ['PAGES.GroupByExpiryDate LAYOUT [SORT]'] ---- - -When grouping by expiration date, the value is determined by your [site configuration], defaulting to the `expiryDate` field in front matter. - -The [layout string] has the same format as the layout string for the [`time.Format`] function. The resulting group key is [localized](g) for language and region. - -[`time.Format`]: /functions/time/format/ -[layout string]: #layout-string -[site configuration]: /configuration/front-matter/#dates - -{{% include "/_common/methods/pages/group-sort-order.md" %}} - -To group content by year and month: - -```go-html-template -{{ range .Pages.GroupByExpiryDate "January 2006" }} -

    {{ .Key }}

    - -{{ end }} -``` - -To sort the groups in ascending order: - -```go-html-template -{{ range .Pages.GroupByExpiryDate "January 2006" "asc" }} -

    {{ .Key }}

    - -{{ end }} -``` - -The pages within each group will also be sorted by expiration date, either ascending or descending depending on your grouping option. To sort the pages within each group, use one of the sorting methods. For example, to sort the pages within each group by title: - -```go-html-template -{{ range .Pages.GroupByExpiryDate "January 2006" }} -

    {{ .Key }}

    - -{{ end }} -``` - -## Layout string - -{{% include "/_common/time-layout-string.md" %}} diff --git a/docs/content/en/methods/pages/GroupByLastmod.md b/docs/content/en/methods/pages/GroupByLastmod.md deleted file mode 100644 index 8729cd3c9..000000000 --- a/docs/content/en/methods/pages/GroupByLastmod.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -title: GroupByLastmod -description: Returns the given page collection grouped by last modification date in descending order. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.PagesGroup - signatures: ['PAGES.GroupByLastmod LAYOUT [SORT]'] ---- - -When grouping by last modification date, the value is determined by your [site configuration], defaulting to the `lastmod` field in front matter. - -The [layout string] has the same format as the layout string for the [`time.Format`] function. The resulting group key is [localized](g) for language and region. - -[`time.Format`]: /functions/time/format/ -[layout string]: #layout-string -[site configuration]: /configuration/front-matter/#dates - -{{% include "/_common/methods/pages/group-sort-order.md" %}} - -To group content by year and month: - -```go-html-template -{{ range .Pages.GroupByLastmod "January 2006" }} -

    {{ .Key }}

    - -{{ end }} -``` - -To sort the groups in ascending order: - -```go-html-template -{{ range .Pages.GroupByLastmod "January 2006" "asc" }} -

    {{ .Key }}

    - -{{ end }} -``` - -The pages within each group will also be sorted by last modification date, either ascending or descending depending on your grouping option. To sort the pages within each group, use one of the sorting methods. For example, to sort the pages within each group by title: - -```go-html-template -{{ range .Pages.GroupByLastmod "January 2006" }} -

    {{ .Key }}

    - -{{ end }} -``` - -## Layout string - -{{% include "/_common/time-layout-string.md" %}} diff --git a/docs/content/en/methods/pages/GroupByParam.md b/docs/content/en/methods/pages/GroupByParam.md deleted file mode 100644 index 6764144d6..000000000 --- a/docs/content/en/methods/pages/GroupByParam.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: GroupByParam -description: Returns the given page collection grouped by the given parameter in ascending order. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.PagesGroup - signatures: ['PAGES.GroupByParam PARAM [SORT]'] ---- - -{{% include "/_common/methods/pages/group-sort-order.md" %}} - -```go-html-template -{{ range .Pages.GroupByParam "color" }} -

    {{ .Key | title }}

    - -{{ end }} -``` - -To sort the groups in descending order: - -```go-html-template -{{ range .Pages.GroupByParam "color" "desc" }} -

    {{ .Key | title }}

    - -{{ end }} -``` diff --git a/docs/content/en/methods/pages/GroupByParamDate.md b/docs/content/en/methods/pages/GroupByParamDate.md deleted file mode 100644 index b05a096d2..000000000 --- a/docs/content/en/methods/pages/GroupByParamDate.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: GroupByParamDate -description: Returns the given page collection grouped by the given date parameter in descending order. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.PagesGroup - signatures: ['PAGES.GroupByParamDate PARAM LAYOUT [SORT]'] ---- - -The [layout string] has the same format as the layout string for the [`time.Format`] function. The resulting group key is [localized](g) for language and region. - -[`time.Format`]: /functions/time/format/ -[layout string]: #layout-string - -{{% include "/_common/methods/pages/group-sort-order.md" %}} - -To group content by year and month: - -```go-html-template -{{ range .Pages.GroupByParamDate "eventDate" "January 2006" }} -

    {{ .Key }}

    - -{{ end }} -``` - -To sort the groups in ascending order: - -```go-html-template -{{ range .Pages.GroupByParamDate "eventDate" "January 2006" "asc" }} -

    {{ .Key }}

    - -{{ end }} -``` - -The pages within each group will also be sorted by the parameter date, either ascending or descending depending on your grouping option. To sort the pages within each group, use one of the sorting methods. For example, to sort the pages within each group by title: - -```go-html-template -{{ range .Pages.GroupByParamDate "eventDate" "January 2006" }} -

    {{ .Key }}

    - -{{ end }} -``` - -## Layout string - -{{% include "/_common/time-layout-string.md" %}} diff --git a/docs/content/en/methods/pages/GroupByPublishDate.md b/docs/content/en/methods/pages/GroupByPublishDate.md deleted file mode 100644 index 50e12f085..000000000 --- a/docs/content/en/methods/pages/GroupByPublishDate.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -title: GroupByPublishDate -description: Returns the given page collection grouped by publish date in descending order. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.PagesGroup - signatures: ['PAGES.GroupByPublishDate LAYOUT [SORT]'] ---- - -When grouping by publish date, the value is determined by your [site configuration], defaulting to the `publishDate` field in front matter. - -The [layout string] has the same format as the layout string for the [`time.Format`] function. The resulting group key is [localized](g) for language and region. - -[`time.Format`]: /functions/time/format/ -[layout string]: #layout-string -[site configuration]: /configuration/front-matter/#dates - -{{% include "/_common/methods/pages/group-sort-order.md" %}} - -To group content by year and month: - -```go-html-template -{{ range .Pages.GroupByPublishDate "January 2006" }} -

    {{ .Key }}

    - -{{ end }} -``` - -To sort the groups in ascending order: - -```go-html-template -{{ range .Pages.GroupByPublishDate "January 2006" "asc" }} -

    {{ .Key }}

    - -{{ end }} -``` - -The pages within each group will also be sorted by publish date, either ascending or descending depending on your grouping option. To sort the pages within each group, use one of the sorting methods. For example, to sort the pages within each group by title: - -```go-html-template -{{ range .Pages.GroupByPublishDate "January 2006" }} -

    {{ .Key }}

    - -{{ end }} -``` - -## Layout string - -{{% include "/_common/time-layout-string.md" %}} diff --git a/docs/content/en/methods/pages/Len.md b/docs/content/en/methods/pages/Len.md deleted file mode 100644 index 85b3267cd..000000000 --- a/docs/content/en/methods/pages/Len.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: Len -description: Returns the number of pages in the given page collection. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: int - signatures: [PAGES.Len] ---- - -```go-html-template -{{ .Pages.Len }} → 42 -``` diff --git a/docs/content/en/methods/pages/Limit.md b/docs/content/en/methods/pages/Limit.md deleted file mode 100644 index 6ee3de24d..000000000 --- a/docs/content/en/methods/pages/Limit.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: Limit -description: Returns the first N pages from the given page collection. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pages - signatures: [PAGES.Limit NUMBER] ---- - -```go-html-template -{{ range .Pages.Limit 3 }} -

    {{ .LinkTitle }}

    -{{ end }} -``` diff --git a/docs/content/en/methods/pages/Next.md b/docs/content/en/methods/pages/Next.md deleted file mode 100644 index ce091c1ab..000000000 --- a/docs/content/en/methods/pages/Next.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -title: Next -description: Returns the next page in a page collection, relative to the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Page - signatures: [PAGES.Next PAGE] ---- - -{{% include "/_common/methods/pages/next-and-prev.md" %}} diff --git a/docs/content/en/methods/pages/Prev.md b/docs/content/en/methods/pages/Prev.md deleted file mode 100644 index 004b9496d..000000000 --- a/docs/content/en/methods/pages/Prev.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -title: Prev -description: Returns the previous page in a page collection, relative to the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pages - signatures: [PAGES.Prev PAGE] ---- - -{{% include "/_common/methods/pages/next-and-prev.md" %}} diff --git a/docs/content/en/methods/pages/Related.md b/docs/content/en/methods/pages/Related.md deleted file mode 100644 index 22eeb4dfa..000000000 --- a/docs/content/en/methods/pages/Related.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -title: Related -description: Returns a collection of pages related to the given page. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pages - signatures: - - PAGES.Related PAGE - - PAGES.Related OPTIONS ---- - -Based on front matter, Hugo uses several factors to identify content related to the given page. Use the default [related content configuration], or tune the results to the desired indices and parameters. See [details]. - -The argument passed to the `Related` method may be a `Page` or an options map. For example, to pass the current page: - -```go-html-template {file="layouts/_default/single.html"} -{{ with .Site.RegularPages.Related . | first 5 }} -

    Related pages:

    - -{{ end }} -``` - -To pass an options map: - -```go-html-template {file="layouts/_default/single.html"} -{{ $opts := dict - "document" . - "indices" (slice "tags" "keywords") -}} -{{ with .Site.RegularPages.Related $opts | first 5 }} -

    Related pages:

    - -{{ end }} -``` - -## 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. - -[`keyVals`]: /functions/collections/keyvals/ - -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 contrived example using all of the above: - -```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") -}} -``` - -[details]: /content-management/related-content/ -[related content configuration]: /configuration/related-content/ diff --git a/docs/content/en/methods/pages/Reverse.md b/docs/content/en/methods/pages/Reverse.md deleted file mode 100644 index 23c4b0324..000000000 --- a/docs/content/en/methods/pages/Reverse.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: Reverse -description: Returns the given page collection in reverse order. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pages - signatures: [PAGES.Reverse] ---- - -```go-html-template -{{ range .Pages.ByDate.Reverse }} -

    {{ .LinkTitle }}

    -{{ end }} -``` diff --git a/docs/content/en/methods/pages/_index.md b/docs/content/en/methods/pages/_index.md deleted file mode 100644 index f2495ae49..000000000 --- a/docs/content/en/methods/pages/_index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Pages methods -linkTitle: Pages -description: Use these methods with a collection of Page objects. -categories: [] -keywords: [] -aliases: [/variables/pages] ---- diff --git a/docs/content/en/methods/resource/Colors.md b/docs/content/en/methods/resource/Colors.md deleted file mode 100644 index 14d0a40d8..000000000 --- a/docs/content/en/methods/resource/Colors.md +++ /dev/null @@ -1,174 +0,0 @@ ---- -title: Colors -description: Applicable to images, returns a slice of the most dominant colors using a simple histogram method. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: '[]images.Color' - signatures: [RESOURCE.Colors] ---- - -{{% include "/_common/methods/resource/global-page-remote-resources.md" %}} - -The `Resources.Colors` method returns a slice of the most dominant colors in an image, ordered from most dominant to least dominant. This method is fast, but if you also downsize your image you can improve performance by extracting the colors from the scaled image. - -## Methods - -Each color is an object with the following methods: - -### ColorHex - -{{< new-in 0.125.0 />}} - -(`string`) Returns the [hexadecimal color] value, prefixed with a hash sign. - -### Luminance - -{{< new-in 0.125.0 />}} - -(`float64`) Returns the [relative luminance] of the color in the sRGB colorspace in the range [0, 1]. A value of `0` represents the darkest black, while a value of `1` represents the lightest white. - -> [!note] -> Image filters such as [`images.Dither`], [`images.Padding`], and [`images.Text`] accept either hexadecimal color values or `images.Color` objects as arguments. -> -> Hugo renders an `images.Color` object as a hexadecimal color value. - -## Sorting - -As a contrived example, create a table of an image's dominant colors with the most dominant color first, and display the relative luminance of each dominant color: - -```go-html-template -{{ with resources.Get "images/a.jpg" }} - - - - - - - - - {{ range .Colors }} - - - - - {{ end }} - -
    ColorRelative luminance
    {{ .ColorHex }}{{ .Luminance | lang.FormatNumber 4 }}
    -{{ end }} -``` - -Hugo renders this to: - -ColorHex|Relative luminance -:--|:-- -`#bebebd`|`0.5145` -`#514947`|`0.0697` -`#768a9a`|`0.2436` -`#647789`|`0.1771` -`#90725e`|`0.1877` -`#a48974`|`0.2704` - -To sort by dominance with the least dominant color first: - -```go-html-template -{{ range .Colors | collections.Reverse }} -``` - -To sort by relative luminance with the darkest color first: - -```go-html-template -{{ range sort .Colors "Luminance" }} -``` - -To sort by relative luminance with the lightest color first, use either of these constructs: - -```go-html-template -{{ range sort .Colors "Luminance" | collections.Reverse }} -{{ range sort .Colors "Luminance" "desc" }} -``` - -## Examples - -### Image borders - -To add a 5 pixel border to an image using the most dominant color: - -```go-html-template -{{ with resources.Get "images/a.jpg" }} - {{ $mostDominant := index .Colors 0 }} - {{ $filter := images.Padding 5 $mostDominant }} - {{ with .Filter $filter }} - - {{ end }} -{{ end }} -``` - -To add a 5 pixel border to an image using the darkest dominant color: - -```go-html-template -{{ with resources.Get "images/a.jpg" }} - {{ $darkest := index (sort .Colors "Luminance") 0 }} - {{ $filter := images.Padding 5 $darkest }} - {{ with .Filter $filter }} - - {{ end }} -{{ end }} -``` - -### Light text on dark background - -To create a text box where the foreground and background colors are derived from an image's lightest and darkest dominant colors: - -```go-html-template -{{ with resources.Get "images/a.jpg" }} - {{ $darkest := index (sort .Colors "Luminance") 0 }} - {{ $lightest := index (sort .Colors "Luminance" "desc") 0 }} -
    -
    -

    This is light text on a dark background.

    -
    -
    -{{ end }} -``` - -### WCAG contrast ratio - -In the previous example we placed light text on a dark background, but does this color combination conform to [WCAG] guidelines for either the [minimum] or the [enhanced] contrast ratio? - -The WCAG defines the [contrast ratio] as: - -$$contrast\ ratio = { L_1 + 0.05 \over L_2 + 0.05 }$$ - -where $L_1$ is the relative luminance of the lightest color and $L_2$ is the relative luminance of the darkest color. - -Calculate the contrast ratio to determine WCAG conformance: - -```go-html-template -{{ with resources.Get "images/a.jpg" }} - {{ $lightest := index (sort .Colors "Luminance" "desc") 0 }} - {{ $darkest := index (sort .Colors "Luminance") 0 }} - {{ $cr := div - (add $lightest.Luminance 0.05) - (add $darkest.Luminance 0.05) - }} - {{ if ge $cr 7.5 }} - {{ printf "The %.2f contrast ratio conforms to WCAG Level AAA." $cr }} - {{ else if ge $cr 4.5 }} - {{ printf "The %.2f contrast ratio conforms to WCAG Level AA." $cr }} - {{ else }} - {{ printf "The %.2f contrast ratio does not conform to WCAG guidelines." $cr }} - {{ end }} -{{ end }} -``` - -[`images.Dither`]: /functions/images/dither/ -[`images.Padding`]: /functions/images/padding/ -[`images.Text`]: /functions/images/text/ -[contrast ratio]: https://www.w3.org/TR/WCAG21/#dfn-contrast-ratio -[enhanced]: https://www.w3.org/WAI/WCAG22/quickref/?showtechniques=145#contrast-enhanced -[hexadecimal color]: https://developer.mozilla.org/en-US/docs/Web/CSS/hex-color -[minimum]: https://www.w3.org/WAI/WCAG22/quickref/?showtechniques=145#contrast-minimum -[relative luminance]: https://www.w3.org/TR/WCAG21/#dfn-relative-luminance -[WCAG]: https://en.wikipedia.org/wiki/Web_Content_Accessibility_Guidelines diff --git a/docs/content/en/methods/resource/Content.md b/docs/content/en/methods/resource/Content.md deleted file mode 100644 index 2c2c12d3a..000000000 --- a/docs/content/en/methods/resource/Content.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: Content -description: Returns the content of the given resource. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: any - signatures: [RESOURCE.Content] ---- - -{{% include "/_common/methods/resource/global-page-remote-resources.md" %}} - -The `Content` method on a `Resource` object returns `template.HTML` when the resource type is `page`, otherwise it returns a `string`. - -[resource type]: /methods/resource/resourcetype/ - -```text {file="assets/quotations/kipling.txt"} -He travels the fastest who travels alone. -``` - -To get the content: - -```go-html-template -{{ with resources.Get "quotations/kipling.txt" }} - {{ .Content }} → He travels the fastest who travels alone. -{{ end }} -``` - -To get the size in bytes: - -```go-html-template -{{ with resources.Get "quotations/kipling.txt" }} - {{ .Content | len }} → 42 -{{ end }} -``` - -To create an inline image: - -```go-html-template -{{ with resources.Get "images/a.jpg" }} - -{{ end }} -``` - -To create inline CSS: - -```go-html-template -{{ with resources.Get "css/style.css" }} - -{{ end }} -``` - -To create inline JavaScript: - -```go-html-template -{{ with resources.Get "js/script.js" }} - -{{ end }} -``` diff --git a/docs/content/en/methods/resource/Crop.md b/docs/content/en/methods/resource/Crop.md deleted file mode 100644 index 97b3b95d3..000000000 --- a/docs/content/en/methods/resource/Crop.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -title: Crop -description: Applicable to images, returns an image resource cropped to the given dimensions without resizing. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: images.ImageResource - signatures: [RESOURCE.Crop SPEC] ---- - -{{% include "/_common/methods/resource/global-page-remote-resources.md" %}} - -Crop an image to match the given dimensions without resizing. You must provide both width and height. - -```go-html-template -{{ with resources.Get "images/original.jpg" }} - {{ with .Crop "200x200" }} - - {{ end }} -{{ end }} -``` - -{{% include "/_common/methods/resource/processing-spec.md" %}} - -## Example - -```go-html-template -{{ with resources.Get "images/original.jpg" }} - {{ with .Crop "200x200 topright webp q85 lanczos" }} - - {{ end }} -{{ end }} -``` - -{{< img - src="images/examples/zion-national-park.jpg" - alt="Zion National Park" - filter="Process" - filterArgs="crop 200x200 topright webp q85 lanczos" - example=true ->}} diff --git a/docs/content/en/methods/resource/Data.md b/docs/content/en/methods/resource/Data.md deleted file mode 100644 index 0709ca50a..000000000 --- a/docs/content/en/methods/resource/Data.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: Data -description: Applicable to resources returned by the resources.GetRemote function, returns information from the HTTP response. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: map - signatures: [RESOURCE.Data] ---- - -The `Data` method on a resource returned by the [`resources.GetRemote`] function returns information from the HTTP response. - -[`resources.GetRemote`]: /functions/resources/getremote/ - -## Example - -```go-html-template -{{ $url := "https://example.org/images/a.jpg" }} -{{ $opts := dict "responseHeaders" (slice "Server") }} -{{ with try (resources.GetRemote $url) }} - {{ with .Err }} - {{ errorf "%s" . }} - {{ else with .Value }} - {{ with .Data }} - {{ .ContentLength }} → 42764 - {{ .ContentType }} → image/jpeg - {{ .Headers }} → map[Server:[Netlify]] - {{ .Status }} → 200 OK - {{ .StatusCode }} → 200 - {{ .TransferEncoding }} → [] - {{ end }} - {{ else }} - {{ errorf "Unable to get remote resource %q" $url }} - {{ end }} -{{ end }} -``` - -## Methods - -### ContentLength - -(`int`) The content length in bytes. - -### ContentType - -(`string`) The content type. - -### Headers - -(`map[string][]string`) A map of response headers matching those requested in the [`responseHeaders`] option passed to the `resources.GetRemote` function. The header name matching is case-insensitive. In most cases there will be one value per header key. - -### Status - -(`string`) The HTTP status text. - -### StatusCode - -(`int`) The HTTP status code. - -### TransferEncoding - -(`string`) The transfer encoding. - -[`resources.GetRemote`]: /functions/resources/getremote/ -[`responseHeaders`]: /functions/resources/getremote/#responseheaders diff --git a/docs/content/en/methods/resource/Err.md b/docs/content/en/methods/resource/Err.md deleted file mode 100644 index 591af8266..000000000 --- a/docs/content/en/methods/resource/Err.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: Err -description: Applicable to resources returned by the resources.GetRemote function, returns an error message if the HTTP request fails, else nil. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: resource.resourceError - signatures: [RESOURCE.Err] -expiryDate: 2027-01-16 # deprecated 2025-01-16 in v0.141.0 ---- - -{{< deprecated-in 0.141.0 >}} -Use the `try` statement instead. See [example]. - -[example]: /functions/go-template/try/#example -{{< /deprecated-in >}} - -The `Err` method on a resource returned by the [`resources.GetRemote`] function returns an error message if the HTTP request fails, else nil. If you do not handle the error yourself, Hugo will fail the build. - -[`resources.GetRemote`]: /functions/resources/getremote/ - -In this example we send an HTTP request to a nonexistent domain: - -```go-html-template -{{ $url := "https://broken-example.org/images/a.jpg" }} -{{ with resources.GetRemote $url }} - {{ with .Err }} - {{ errorf "%s" . }} - {{ else }} - - {{ end }} -{{ else }} - {{ errorf "Unable to get remote resource %q" $url }} -{{ end }} -``` - -The code above captures the error from the HTTP request, then fails the build: - -```text -ERROR error calling resources.GetRemote: Get "https://broken-example.org/images/a.jpg": dial tcp: lookup broken-example.org on 127.0.0.53:53: no such host -``` - -To log an error as a warning instead of an error: - -```go-html-template -{{ $url := "https://broken-example.org/images/a.jpg" }} -{{ with resources.GetRemote $url }} - {{ with .Err }} - {{ warnf "%s" . }} - {{ else }} - - {{ end }} -{{ else }} - {{ errorf "Unable to get remote resource %q" $url }} -{{ end }} -``` - -> [!note] -> An HTTP response with a 404 status code is not an HTTP request error. To handle 404 status codes, code defensively using the nested `with-else-end` construct as shown above. diff --git a/docs/content/en/methods/resource/Exif.md b/docs/content/en/methods/resource/Exif.md deleted file mode 100644 index 443a0ee1a..000000000 --- a/docs/content/en/methods/resource/Exif.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -title: Exif -description: Applicable to JPEG, PNG, TIFF, and WebP images, returns an EXIF object containing image metadata. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: exif.ExifInfo - signatures: [RESOURCE.Exif] ---- - -{{% include "/_common/methods/resource/global-page-remote-resources.md" %}} - -Applicable to JPEG, PNG, TIFF, and WebP images, the `Exif` method on an image `Resource` object returns an [EXIF] object containing image metadata. - -## Methods - -### Date - -(`time.Time`) Returns the image creation date/time. Format with the [`time.Format`] function. - -### Lat - -(`float64`) Returns the GPS latitude in degrees. - -### Long - -(`float64`) Returns the GPS longitude in degrees. - -### Tags - -(`exif.Tags`) Returns a collection of the available EXIF tags for this image. You may include or exclude specific tags from this collection. See [configure imaging]. - -[configure imaging]: /configuration/imaging/#exif-data - -## Examples - -To list the creation date, location, and EXIF tags: - -```go-html-template -{{ with resources.Get "images/a.jpg" }} - {{ with .Exif }} -

    Date: {{ .Date }}

    -

    Lat/Long: {{ .Lat }}/{{ .Long }}

    - {{ with .Tags }} -

    Tags

    - - - - - - {{ range $k, $v := . }} - - {{ end }} - -
    TagValue
    {{ $k }}{{ $v }}
    - {{ end }} - {{ end }} -{{ end }} -``` - -To list specific values: - -```go-html-template -{{ with resources.Get "images/a.jpg" }} - {{ with .Exif }} -
      - {{ with .Date }}
    • Date: {{ .Format "January 02, 2006" }}
    • {{ end }} - {{ with .Tags.ApertureValue }}
    • Aperture: {{ lang.FormatNumber 2 . }}
    • {{ end }} - {{ with .Tags.BrightnessValue }}
    • Brightness: {{ lang.FormatNumber 2 . }}
    • {{ end }} - {{ with .Tags.ExposureTime }}
    • Exposure Time: {{ . }}
    • {{ end }} - {{ with .Tags.FNumber }}
    • F Number: {{ . }}
    • {{ end }} - {{ with .Tags.FocalLength }}
    • Focal Length: {{ . }}
    • {{ end }} - {{ with .Tags.ISOSpeedRatings }}
    • ISO Speed Ratings: {{ . }}
    • {{ end }} - {{ with .Tags.LensModel }}
    • Lens Model: {{ . }}
    • {{ end }} -
    - {{ end }} -{{ end }} -``` - -[exif]: https://en.wikipedia.org/wiki/Exif -[`time.Format`]: /functions/time/format/ diff --git a/docs/content/en/methods/resource/Fill.md b/docs/content/en/methods/resource/Fill.md deleted file mode 100644 index 82c696c91..000000000 --- a/docs/content/en/methods/resource/Fill.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -title: Fill -description: Applicable to images, returns an image resource cropped and resized to the given dimensions. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: images.ImageResource - signatures: [RESOURCE.Fill SPEC] ---- - -{{% include "/_common/methods/resource/global-page-remote-resources.md" %}} - -Crop and resize an image to match the given dimensions. You must provide both width and height. - -```go-html-template -{{ with resources.Get "images/original.jpg" }} - {{ with .Fill "200x200" }} - - {{ end }} -{{ end }} -``` - -{{% include "/_common/methods/resource/processing-spec.md" %}} - -## Example - -```go-html-template -{{ with resources.Get "images/original.jpg" }} - {{ with .Fill "200x200 top webp q85 lanczos" }} - - {{ end }} -{{ end }} -``` - -{{< img - src="images/examples/zion-national-park.jpg" - alt="Zion National Park" - filter="Process" - filterArgs="fill 200x200 top webp q85 lanczos" - example=true ->}} diff --git a/docs/content/en/methods/resource/Filter.md b/docs/content/en/methods/resource/Filter.md deleted file mode 100644 index b83c3d8cb..000000000 --- a/docs/content/en/methods/resource/Filter.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: Filter -description: Applicable to images, applies one or more image filters to the given image resource. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: images.ImageResource - signatures: [RESOURCE.Filter FILTER...] ---- - -{{% include "/_common/methods/resource/global-page-remote-resources.md" %}} - -Apply one or more [image filters](#image-filters) to the given image. - -To apply a single filter: - -```go-html-template -{{ with resources.Get "images/original.jpg" }} - {{ with .Filter images.Grayscale }} - - {{ end }} -{{ end }} -``` - -To apply two or more filters, executing from left to right: - -```go-html-template -{{ $filters := slice - images.Grayscale - (images.GaussianBlur 8) -}} -{{ with resources.Get "images/original.jpg" }} - {{ with .Filter $filters }} - - {{ end }} -{{ end }} -``` - -You can also apply image filters using the [`images.Filter`] function. - -[`images.Filter`]: /functions/images/filter/ - -## Example - -```go-html-template -{{ with resources.Get "images/original.jpg" }} - {{ with .Filter images.Grayscale }} - - {{ end }} -{{ end }} -``` - -{{< img - src="images/examples/zion-national-park.jpg" - alt="Zion National Park" - filter="Grayscale" - filterArgs="" - example=true ->}} - -## Image filters - -Use any of these filters with the `Filter` method. - -{{% list-pages-in-section path=/functions/images filter=functions_images_no_filters filterType=exclude %}} diff --git a/docs/content/en/methods/resource/Fit.md b/docs/content/en/methods/resource/Fit.md deleted file mode 100644 index 7b416c4a1..000000000 --- a/docs/content/en/methods/resource/Fit.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -title: Fit -description: Applicable to images, returns an image resource downscaled to fit the given dimensions while maintaining aspect ratio. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: images.ImageResource - signatures: [RESOURCE.Fit SPEC] ---- - -{{% include "/_common/methods/resource/global-page-remote-resources.md" %}} - -Downscale an image to fit the given dimensions while maintaining aspect ratio. You must provide both width and height. - -```go-html-template -{{ with resources.Get "images/original.jpg" }} - {{ with .Fit "200x200" }} - - {{ end }} -{{ end }} -``` - -{{% include "/_common/methods/resource/processing-spec.md" %}} - -## Example - -```go-html-template -{{ with resources.Get "images/original.jpg" }} - {{ with .Fit "300x175 webp q85 lanczos" }} - - {{ end }} -{{ end }} -``` - -{{< img - src="images/examples/zion-national-park.jpg" - alt="Zion National Park" - filter="Process" - filterArgs="fit 300x175 webp q85 lanczos" - example=true ->}} diff --git a/docs/content/en/methods/resource/Height.md b/docs/content/en/methods/resource/Height.md deleted file mode 100644 index cc131378a..000000000 --- a/docs/content/en/methods/resource/Height.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: Height -description: Applicable to images, returns the height of the given resource. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: int - signatures: [RESOURCE.Height] ---- - -{{% include "/_common/methods/resource/global-page-remote-resources.md" %}} - -```go-html-template -{{ with resources.Get "images/a.jpg" }} - {{ .Height }} → 400 -{{ end }} -``` - -Use the `Width` and `Height` methods together when rendering an `img` element: - -```go-html-template -{{ with resources.Get "images/a.jpg" }} - -{{ end }} -``` diff --git a/docs/content/en/methods/resource/MediaType.md b/docs/content/en/methods/resource/MediaType.md deleted file mode 100644 index 7721f69ba..000000000 --- a/docs/content/en/methods/resource/MediaType.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: MediaType -description: Returns a media type object for the given resource. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: media.Type - signatures: [RESOURCE.MediaType] ---- - -{{% include "/_common/methods/resource/global-page-remote-resources.md" %}} - -The `MediaType` method on a `Resource` object returns an object with additional methods. - -## Methods - -### Type - -(`string`) The resource's media type. - -```go-html-template -{{ with resources.Get "images/a.jpg" }} - {{ .MediaType.Type }} → image/jpeg -{{ end }} -``` - -### MainType - -(`string`) The main type of the resource's media type. - -```go-html-template -{{ with resources.Get "images/a.jpg" }} - {{ .MediaType.MainType }} → image -{{ end }} -``` - -### SubType - -(`string`) The subtype of the resource's media type. This may or may not correspond to the file suffix. - -```go-html-template -{{ with resources.Get "images/a.jpg" }} - {{ .MediaType.SubType }} → jpeg -{{ end }} -``` - -### Suffixes - -(`slice`) A slice of possible file suffixes for the resource's media type. - -```go-html-template -{{ with resources.Get "images/a.jpg" }} - {{ .MediaType.Suffixes }} → [jpg jpeg jpe jif jfif] -{{ end }} -``` - -### FirstSuffix.Suffix - -(`string`) The first of the possible file suffixes for the resource's media type. - -```go-html-template -{{ with resources.Get "images/a.jpg" }} - {{ .MediaType.FirstSuffix.Suffix }} → jpg -{{ end }} -``` diff --git a/docs/content/en/methods/resource/Name.md b/docs/content/en/methods/resource/Name.md deleted file mode 100644 index c678c96c9..000000000 --- a/docs/content/en/methods/resource/Name.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -title: Name -description: Returns the name of the given resource as optionally defined in front matter, falling back to its file path. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [RESOURCE.Name] ---- - -The value returned by the `Name` method on a `Resource` object depends on the resource type. - -## Global resource - -With a [global resource](g), the `Name` method returns the path to the resource, relative to the `assets` directory. - -```text -assets/ -└── images/ - └── Sunrise in Bryce Canyon.jpg -``` - -```go-html-template -{{ with resources.Get "images/Sunrise in Bryce Canyon.jpg" }} - {{ .Name }} → /images/Sunrise in Bryce Canyon.jpg -{{ end }} -``` - -## Page resource - -With a [page resource](g), if you create an element in the `resources` array in front matter, the `Name` method returns the value of the `name` parameter. - -```text -content/ -├── example/ -│ ├── images/ -│ │ └── a.jpg -│ └── index.md -└── _index.md -``` - -{{< code-toggle file=content/example/index.md fm=true >}} -title = 'Example' -[[resources]] -src = 'images/a.jpg' -name = 'Sunrise in Bryce Canyon' -{{< /code-toggle >}} - -```go-html-template -{{ with .Resources.Get "images/a.jpg" }} - {{ .Name }} → Sunrise in Bryce Canyon -{{ end }} -``` - -You can also capture the image by specifying its `name` instead of its path: - -```go-html-template -{{ with .Resources.Get "Sunrise in Bryce Canyon" }} - {{ .Name }} → Sunrise in Bryce Canyon -{{ end }} -``` - -If you do not create an element in the `resources` array in front matter, the `Name` method returns the file path, relative to the page bundle. - -```text -content/ -├── example/ -│ ├── images/ -│ │ └── Sunrise in Bryce Canyon.jpg -│ └── index.md -└── _index.md -``` - -```go-html-template -{{ with .Resources.Get "images/Sunrise in Bryce Canyon.jpg" }} - {{ .Name }} → images/Sunrise in Bryce Canyon.jpg -{{ end }} -``` -## Remote resource - -With a [remote resource](g), the `Name` method returns a hashed file name. - -```go-html-template -{{ with resources.GetRemote "https://example.org/images/a.jpg" }} - {{ .Name }} → /a_18432433023265451104.jpg -{{ end }} -``` diff --git a/docs/content/en/methods/resource/Params.md b/docs/content/en/methods/resource/Params.md deleted file mode 100644 index 38f2ef6c2..000000000 --- a/docs/content/en/methods/resource/Params.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -title: Params -description: Returns a map of resource parameters as defined in front matter. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: map - signatures: [RESOURCE.Params] ---- - -Use the `Params` method with [page resources](g). It is not applicable to either [global resources](g) or [remote resources](g). - -With this content structure: - -```text -content/ -├── posts/ -│ ├── cats/ -│ │ ├── images/ -│ │ │ └── a.jpg -│ │ └── index.md -│ └── _index.md -└── _index.md -``` - -And this front matter: - -{{< code-toggle file=content/posts/cats.md fm=true >}} -title = 'Cats' -[[resources]] - src = 'images/a.jpg' - title = 'Felix the cat' - [resources.params] - alt = 'Photograph of black cat' - temperament = 'vicious' -{{< /code-toggle >}} - -And this template: - -```go-html-template -{{ with .Resources.Get "images/a.jpg" }} -
    - {{ .Params.alt }} -
    {{ .Title }} is {{ .Params.temperament }}
    -
    -{{ end }} -``` - -Hugo renders: - -```html -
    - Photograph of black cat -
    Felix the cat is vicious
    -
    -``` - -See the [page resources] section for more information. - -[page resources]: /content-management/page-resources/ diff --git a/docs/content/en/methods/resource/Permalink.md b/docs/content/en/methods/resource/Permalink.md deleted file mode 100644 index a8ec2d323..000000000 --- a/docs/content/en/methods/resource/Permalink.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -title: Permalink -description: Publishes the given resource and returns its permalink. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [RESOURCE.Permalink] ---- - -{{% include "/_common/methods/resource/global-page-remote-resources.md" %}} - -The `Permalink` method on a `Resource` object writes the resource to the publish directory, typically `public`, and returns its [permalink](g). - -```go-html-template -{{ with resources.Get "images/a.jpg" }} - {{ .Permalink }} → https://example.org/images/a.jpg -{{ end }} -``` diff --git a/docs/content/en/methods/resource/Process.md b/docs/content/en/methods/resource/Process.md deleted file mode 100644 index fb27da54e..000000000 --- a/docs/content/en/methods/resource/Process.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: Process -description: Applicable to images, returns an image resource processed with the given specification. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: images.ImageResource - signatures: [RESOURCE.Process SPEC] ---- - -{{% include "/_common/methods/resource/global-page-remote-resources.md" %}} - -Process an image with the given specification. The specification can contain an optional action, one of `crop`, `fill`, `fit`, or `resize`. This means that you can use this method instead of [`Crop`], [`Fill`], [`Fit`], or [`Resize`]. - -```go-html-template -{{ with resources.Get "images/original.jpg" }} - {{ with .Process "crop 200x200" }} - - {{ end }} -{{ end }} -``` - -You can also use this method to apply simple transformations such as rotation and conversion: - -```go-html-template -{{/* Rotate 90 degrees counter-clockwise. */}} -{{ $image := $image.Process "r90" }} - -{{/* Convert to WebP. */}} -{{ $image := $image.Process "webp" }} -``` - -The `Process` method is also available as a filter, which is more effective if you need to apply multiple filters to an image. See [`images.Process`]. - -{{% include "/_common/methods/resource/processing-spec.md" %}} - -## Example - -```go-html-template -{{ with resources.Get "images/original.jpg" }} - {{ with .Process "crop 200x200 topright webp q85 lanczos" }} - - {{ end }} -{{ end }} -``` - -{{< img - src="images/examples/zion-national-park.jpg" - alt="Zion National Park" - filter="Process" - filterArgs="crop 200x200 topright webp q85 lanczos" - example=true ->}} - -[`Crop`]: /methods/resource/crop/ -[`Fill`]: /methods/resource/fill/ -[`Fit`]: /methods/resource/fit/ -[`Resize`]: /methods/resource/resize/ -[`images.Process`]: /functions/images/process/ diff --git a/docs/content/en/methods/resource/Publish.md b/docs/content/en/methods/resource/Publish.md deleted file mode 100644 index 0ecdf7e74..000000000 --- a/docs/content/en/methods/resource/Publish.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: Publish -description: Publishes the given resource. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: nil - signatures: [RESOURCE.Publish] ---- - -{{% include "/_common/methods/resource/global-page-remote-resources.md" %}} - -The `Publish` method on a `Resource` object writes the resource to the publish directory, typically `public`. - -```go-html-template -{{ with resources.Get "images/a.jpg" }} - {{ .Publish }} -{{ end }} -``` - -The `Permalink` and `RelPermalink` methods also publish a resource. `Publish` is a convenience method for publishing without a return value. For example, this: - -```go-html-template -{{ $resource.Publish }} -``` - -Instead of this: - -```go-html-template -{{ $noop := $resource.Permalink }} -``` diff --git a/docs/content/en/methods/resource/RelPermalink.md b/docs/content/en/methods/resource/RelPermalink.md deleted file mode 100644 index d4c907bff..000000000 --- a/docs/content/en/methods/resource/RelPermalink.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -title: RelPermalink -description: Publishes the given resource and returns its relative permalink. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [RESOURCE.RelPermalink] ---- - -{{% include "/_common/methods/resource/global-page-remote-resources.md" %}} - -The `Permalink` method on a `Resource` object writes the resource to the publish directory, typically `public`, and returns its [relative permalink](g). - -```go-html-template -{{ with resources.Get "images/a.jpg" }} - {{ .RelPermalink }} → /images/a.jpg -{{ end }} -``` diff --git a/docs/content/en/methods/resource/Resize.md b/docs/content/en/methods/resource/Resize.md deleted file mode 100644 index 93c029ba6..000000000 --- a/docs/content/en/methods/resource/Resize.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -title: Resize -description: Applicable to images, returns an image resource resized to the given width and/or height. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: images.ImageResource - signatures: [RESOURCE.Resize SPEC] ---- - -{{% include "/_common/methods/resource/global-page-remote-resources.md" %}} - -Resize an image to the given width and/or height. - -If you specify both width and height, the resulting image will be disproportionally scaled unless the original image has the same aspect ratio. - -```go-html-template -{{ with resources.Get "images/original.jpg" }} - {{ with .Resize "300x" }} - - {{ end }} -{{ end }} -``` - -{{% include "/_common/methods/resource/processing-spec.md" %}} - -## Example - -```go-html-template -{{ with resources.Get "images/original.jpg" }} - {{ with .Resize "300x webp q85 lanczos" }} - - {{ end }} -{{ end }} -``` - -{{< img - src="images/examples/zion-national-park.jpg" - alt="Zion National Park" - filter="Process" - filterArgs="resize 300x webp q85 lanczos" - example=true ->}} diff --git a/docs/content/en/methods/resource/ResourceType.md b/docs/content/en/methods/resource/ResourceType.md deleted file mode 100644 index 0ea9c0cf9..000000000 --- a/docs/content/en/methods/resource/ResourceType.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: ResourceType -description: Returns the main type of the given resource's media type. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [RESOURCE.ResourceType] ---- - -{{% include "/_common/methods/resource/global-page-remote-resources.md" %}} - -Common resource types include `audio`, `image`, `text`, and `video`. - -```go-html-template -{{ with resources.Get "image/a.jpg" }} - {{ .ResourceType }} → image - {{ .MediaType.MainType }} → image -{{ end }} -``` - -When working with content files, the resource type is `page`. - -```text -content/ -├── lessons/ -│ ├── lesson-1/ -│ │ ├── _objectives.md <-- resource type = page -│ │ ├── _topics.md <-- resource type = page -│ │ ├── _example.jpg <-- resource type = image -│ │ └── index.md -│ └── _index.md -└── _index.md -``` - -With the structure above, we can range through page resources of type `page` to build content: - -```go-html-template {file="layouts/lessons/single.html"} -{{ range .Resources.ByType "page" }} - {{ .Content }} -{{ end }} -``` diff --git a/docs/content/en/methods/resource/Title.md b/docs/content/en/methods/resource/Title.md deleted file mode 100644 index c02d29ff8..000000000 --- a/docs/content/en/methods/resource/Title.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -title: Title -description: Returns the title of the given resource as optionally defined in front matter, falling back to a relative path or hashed file name depending on resource type. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [RESOURCE.Title] ---- - -The value returned by the `Title` method on a `Resource` object depends on the resource type. - -## Global resource - -With a [global resource](g), the `Title` method returns the path to the resource, relative to the `assets` directory. - -```text -assets/ -└── images/ - └── Sunrise in Bryce Canyon.jpg -``` - -```go-html-template -{{ with resources.Get "images/Sunrise in Bryce Canyon.jpg" }} - {{ .Title }} → /images/Sunrise in Bryce Canyon.jpg -{{ end }} -``` - -## Page resource - -With a [page resource](g), if you create an element in the `resources` array in front matter, the `Title` method returns the value of the `title` parameter. - -```text -content/ -├── example/ -│ ├── images/ -│ │ └── a.jpg -│ └── index.md -└── _index.md -``` - -{{< code-toggle file=content/example/index.md fm=true >}} -title = 'Example' -[[resources]] -src = 'images/a.jpg' -title = 'A beautiful sunrise in Bryce Canyon' -{{< /code-toggle >}} - -```go-html-template -{{ with .Resources.Get "images/a.jpg" }} - {{ .Title }} → A beautiful sunrise in Bryce Canyon -{{ end }} -``` - -If you do not create an element in the `resources` array in front matter, the `Title` method returns the file path, relative to the page bundle. - -```text -content/ -├── example/ -│ ├── images/ -│ │ └── Sunrise in Bryce Canyon.jpg -│ └── index.md -└── _index.md -``` - -```go-html-template -{{ with .Resources.Get "Sunrise in Bryce Canyon.jpg" }} - {{ .Title }} → images/Sunrise in Bryce Canyon.jpg -{{ end }} -``` - -## Remote resource - -With a [remote resource](g), the `Title` method returns a hashed file name. - -```go-html-template -{{ with resources.GetRemote "https://example.org/images/a.jpg" }} - {{ .Title }} → /a_18432433023265451104.jpg -{{ end }} -``` diff --git a/docs/content/en/methods/resource/Width.md b/docs/content/en/methods/resource/Width.md deleted file mode 100644 index e1b43f44c..000000000 --- a/docs/content/en/methods/resource/Width.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: Width -description: Applicable to images, returns the width of the given resource. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: int - signatures: [RESOURCE.Width] ---- - -{{% include "/_common/methods/resource/global-page-remote-resources.md" %}} - -```go-html-template -{{ with resources.Get "images/a.jpg" }} - {{ .Width }} → 600 -{{ end }} -``` - -Use the `Width` and `Height` methods together when rendering an `img` element: - -```go-html-template -{{ with resources.Get "images/a.jpg" }} - -{{ end }} -``` diff --git a/docs/content/en/methods/resource/_index.md b/docs/content/en/methods/resource/_index.md deleted file mode 100644 index edfbc5b14..000000000 --- a/docs/content/en/methods/resource/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Resource methods -linkTitle: Resource -description: Use these methods with global, page, and remote Resource objects. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/methods/shortcode/Get.md b/docs/content/en/methods/shortcode/Get.md deleted file mode 100644 index b9c01cfc4..000000000 --- a/docs/content/en/methods/shortcode/Get.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: Get -description: Returns the value of the given argument. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: any - signatures: [SHORTCODE.Get ARG] ---- - -Specify the argument by position or by name. When calling a shortcode within Markdown, use either positional or named argument, but not both. - -> [!note] -> Some shortcodes support positional arguments, some support named arguments, and others support both. Refer to the shortcode's documentation for usage details. - -## Positional arguments - -This shortcode call uses positional arguments: - -```text {file="content/about.md"} -{{}} -``` - -To retrieve arguments by position: - -```go-html-template {file="layouts/shortcodes/myshortcode.html"} -{{ printf "%s %s." (.Get 0) (.Get 1) }} → Hello world. -``` - -## Named arguments - -This shortcode call uses named arguments: - -```text {file="content/about.md"} -{{}} -``` - -To retrieve arguments by name: - -```go-html-template {file="layouts/shortcodes/myshortcode.html"} -{{ printf "%s %s." (.Get "greeting") (.Get "firstName") }} → Hello world. -``` - -> [!note] -> Argument names are case-sensitive. diff --git a/docs/content/en/methods/shortcode/Inner.md b/docs/content/en/methods/shortcode/Inner.md deleted file mode 100644 index cdce4c1c3..000000000 --- a/docs/content/en/methods/shortcode/Inner.md +++ /dev/null @@ -1,143 +0,0 @@ ---- -title: Inner -description: Returns the content between opening and closing shortcode tags, applicable when the shortcode call includes a closing tag. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: template.HTML - signatures: [SHORTCODE.Inner] ---- - -This content: - -```text {file="content/services.md"} -{{}} -We design the **best** widgets in the world. -{{}} -``` - -With this shortcode: - -```go-html-template {file="layouts/shortcodes/card.html"} -
    - {{ with .Get "title" }} -
    {{ . }}
    - {{ end }} -
    - {{ .Inner | strings.TrimSpace }} -
    -
    -``` - -Is rendered to: - -```html -
    -
    Product Design
    -
    - We design the **best** widgets in the world. -
    -
    -``` - -> [!note] -> Content between opening and closing shortcode tags may include leading and/or trailing newlines, depending on placement within the Markdown. Use the [`strings.TrimSpace`] function as shown above to remove carriage returns and newlines. - -> [!note] -> In the example above, the value returned by `Inner` is Markdown, but it was rendered as plain text. Use either of the following approaches to render Markdown to HTML. - -## Use RenderString - -Let's modify the example above to pass the value returned by `Inner` through the [`RenderString`] method on the `Page` object: - -```go-html-template {file="layouts/shortcodes/card.html"} -
    - {{ with .Get "title" }} -
    {{ . }}
    - {{ end }} -
    - {{ .Inner | strings.TrimSpace | .Page.RenderString }} -
    -
    -``` - -Hugo renders this to: - -```html -
    -
    Product design
    -
    - We produce the best widgets in the world. -
    -
    -``` - -You can use the [`markdownify`] function instead of the `RenderString` method, but the latter is more flexible. See [details]. - -## Alternative notation - -Instead of calling the shortcode with the `{{}}` notation, use the `{{%/* */%}}` notation: - -```text {file="content/services.md"} -{{%/* card title="Product Design" */%}} -We design the **best** widgets in the world. -{{%/* /card */%}} -``` - -When you use the `{{%/* */%}}` notation, Hugo renders the entire shortcode as Markdown, requiring the following changes. - -First, configure the renderer to allow raw HTML within Markdown: - -{{< code-toggle file=hugo >}} -[markup.goldmark.renderer] -unsafe = true -{{< /code-toggle >}} - -This configuration is not unsafe if _you_ control the content. Read more about Hugo's [security model]. - -Second, because we are rendering the entire shortcode as Markdown, we must adhere to the rules governing [indentation] and inclusion of [raw HTML blocks] as provided in the [CommonMark] specification. - -```go-html-template {file="layouts/shortcodes/card.html"} -
    - {{ with .Get "title" }} -
    {{ . }}
    - {{ end }} -
    - - {{ .Inner | strings.TrimSpace }} -
    -
    -``` - -The difference between this and the previous example is subtle but required. Note the change in indentation, the addition of a blank line, and removal of the `RenderString` method. - -```diff ---- layouts/shortcodes/a.html -+++ layouts/shortcodes/b.html -@@ -1,8 +1,9 @@ -
    - {{ with .Get "title" }} --
    {{ . }}
    -+
    {{ . }}
    - {{ end }} -
    -- {{ .Inner | strings.TrimSpace | .Page.RenderString }} -+ -+ {{ .Inner | strings.TrimSpace }} -
    -
    -``` - -> [!note] -> Don't process the `Inner` value with `RenderString` or `markdownify` when using [Markdown notation] to call the shortcode. - -[`markdownify`]: /functions/transform/markdownify/ -[`RenderString`]: /methods/page/renderstring/ -[`strings.TrimSpace`]: /functions/strings/trimspace/ -[CommonMark]: https://spec.commonmark.org/current/ -[details]: /methods/page/renderstring/ -[indentation]: https://spec.commonmark.org/0.30/#indented-code-blocks -[Markdown notation]: /content-management/shortcodes/#notation -[raw HTML blocks]: https://spec.commonmark.org/0.31.2/#html-blocks -[security model]: /about/security/ diff --git a/docs/content/en/methods/shortcode/InnerDeindent.md b/docs/content/en/methods/shortcode/InnerDeindent.md deleted file mode 100644 index 0b8c8e2d8..000000000 --- a/docs/content/en/methods/shortcode/InnerDeindent.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -title: InnerDeindent -description: Returns the content between opening and closing shortcode tags, with indentation removed, applicable when the shortcode call includes a closing tag. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: template.HTML - signatures: [SHORTCODE.InnerDeindent] ---- - -Similar to the [`Inner`] method, `InnerDeindent` returns the content between opening and closing shortcode tags. However, with `InnerDeindent`, indentation before the content is removed. - -This allows us to effectively bypass the rules governing [indentation] as provided in the [CommonMark] specification. - -Consider this Markdown, an unordered list with a small gallery of thumbnail images within each list item: - -```text {file="content/about.md"} -- Gallery one - - {{}} - ![kitten a](thumbnails/a.jpg) - ![kitten b](thumbnails/b.jpg) - {{}} - -- Gallery two - - {{}} - ![kitten c](thumbnails/c.jpg) - ![kitten d](thumbnails/d.jpg) - {{}} -``` - -In the example above, notice that the content between the opening and closing shortcode tags is indented by four spaces. Per the CommonMark specification, this is treated as an indented code block. - -With this shortcode, calling `Inner` instead of `InnerDeindent`: - -```go-html-template {file="layouts/shortcodes/gallery.html"} - -``` - -Hugo renders the Markdown to: - -```html -
      -
    • -

      Gallery one

      - -
    • -
    • -

      Gallery two

      - -
    • -
    -``` - -Although technically correct per the CommonMark specification, this is not what we want. If we remove the indentation using the `InnerDeindent` method: - -```go-html-template {file="layouts/shortcodes/gallery.html"} - -``` - -Hugo renders the Markdown to: - -```html -
      -
    • -

      Gallery one

      - -
    • -
    • -

      Gallery two

      - -
    • -
    -``` - -[commonmark]: https://commonmark.org/ -[indentation]: https://spec.commonmark.org/0.30/#indented-code-blocks -[`Inner`]: /methods/shortcode/inner/ diff --git a/docs/content/en/methods/shortcode/IsNamedParams.md b/docs/content/en/methods/shortcode/IsNamedParams.md deleted file mode 100644 index 1e0a7f00e..000000000 --- a/docs/content/en/methods/shortcode/IsNamedParams.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: IsNamedParams -description: Reports whether the shortcode call uses named arguments. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: bool - signatures: [SHORTCODE.IsNamedParams] ---- - -To support both positional and named arguments when calling a shortcode, use the `IsNamedParams` method to determine how the shortcode was called. - -With this shortcode template: - -```go-html-template {file="layouts/shortcodes/myshortcode.html"} -{{ if .IsNamedParams }} - {{ printf "%s %s." (.Get "greeting") (.Get "firstName") }} -{{ else }} - {{ printf "%s %s." (.Get 0) (.Get 1) }} -{{ end }} -``` - -Both of these calls return the same value: - -```text {file="content/about.md"} -{{}} -{{}} -``` diff --git a/docs/content/en/methods/shortcode/Name.md b/docs/content/en/methods/shortcode/Name.md deleted file mode 100644 index b5f9b6c17..000000000 --- a/docs/content/en/methods/shortcode/Name.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: Name -description: Returns the shortcode file name, excluding the file extension. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [SHORTCODE.Name] ---- - -The `Name` method is useful for error reporting. For example, if your shortcode requires a "greeting" argument: - -```go-html-template {file="layouts/shortcodes/myshortcode.html"} -{{ $greeting := "" }} -{{ with .Get "greeting" }} - {{ $greeting = . }} -{{ else }} - {{ errorf "The %q shortcode requires a 'greeting' argument. See %s" .Name .Position }} -{{ end }} -``` - -In the absence of a "greeting" argument, Hugo will throw an error message and fail the build: - -```text -ERROR The "myshortcode" shortcode requires a 'greeting' argument. See "/home/user/project/content/about.md:11:1" -``` diff --git a/docs/content/en/methods/shortcode/Ordinal.md b/docs/content/en/methods/shortcode/Ordinal.md deleted file mode 100644 index def0c016f..000000000 --- a/docs/content/en/methods/shortcode/Ordinal.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -title: Ordinal -description: Returns the zero-based ordinal of the shortcode in relation to its parent. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: int - signatures: [SHORTCODE.Ordinal] ---- - -The `Ordinal` method returns the zero-based ordinal of the shortcode in relation to its parent. If the parent is the page itself, the ordinal represents the position of this shortcode in the page content. - -> [!note] -> Hugo increments the ordinal with each shortcode call, regardless of the specific shortcode type. This means that the ordinal value is tracked sequentially across all shortcodes within a given page. - -This method is useful for, among other things, assigning unique element IDs when a shortcode is called two or more times from the same page. For example: - -```text {file="content/about.md"} -{{}} - -{{}} -``` - -This shortcode performs error checking, then renders an HTML `img` element with a unique `id` attribute: - -```go-html-template {file="layouts/shortcodes/img.html"} -{{ $src := "" }} -{{ with .Get "src" }} - {{ $src = . }} - {{ with resources.Get $src }} - {{ $id := printf "img-%03d" $.Ordinal }} - - {{ else }} - {{ errorf "The %q shortcode was unable to find %s. See %s" $.Name $src $.Position }} - {{ end }} -{{ else }} - {{ errorf "The %q shortcode requires a 'src' argument. See %s" .Name .Position }} -{{ end }} -``` - -Hugo renders the page to: - -```html - - -``` - -> [!note] -> In the shortcode template above, the [`with`] statement is used to create conditional blocks. Remember that the `with` statement binds context (the dot) to its expression. Inside of a `with` block, preface shortcode method calls with a `$` to access the top-level context passed into the template. - -[`with`]: /functions/go-template/with/ diff --git a/docs/content/en/methods/shortcode/Page.md b/docs/content/en/methods/shortcode/Page.md deleted file mode 100644 index 774caf9fc..000000000 --- a/docs/content/en/methods/shortcode/Page.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: Page -description: Returns the Page object from which the shortcode was called. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: hugolib.pageForShortcode - signatures: [SHORTCODE.Page] ---- - -With this content: - -{{< code-toggle file=content/books/les-miserables.md fm=true >}} -title = 'Les Misérables' -author = 'Victor Hugo' -publication_year = 1862 -isbn = '978-0451419439' -{{< /code-toggle >}} - -Calling this shortcode: - -```text -{{}} -``` - -We can access the front matter values using the `Page` method: - -```go-html-template {file="layouts/shortcodes/book-details.html"} -
      -
    • Title: {{ .Page.Title }}
    • -
    • Author: {{ .Page.Params.author }}
    • -
    • Published: {{ .Page.Params.publication_year }}
    • -
    • ISBN: {{ .Page.Params.isbn }}
    • -
    -``` diff --git a/docs/content/en/methods/shortcode/Params.md b/docs/content/en/methods/shortcode/Params.md deleted file mode 100644 index f001e737f..000000000 --- a/docs/content/en/methods/shortcode/Params.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: Params -description: Returns a collection of the shortcode arguments. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: any - signatures: [SHORTCODE.Params] ---- - -When you call a shortcode using positional arguments, the `Params` method returns a slice. - -```text {file="content/about.md"} -{{}} -``` - -```go-html-template {file="layouts/shortcodes/myshortcode.html"} -{{ index .Params 0 }} → Hello -{{ index .Params 1 }} → world -``` - -When you call a shortcode using named arguments, the `Params` method returns a map. - -```text {file="content/about.md"} -{{}} -``` - -```go-html-template {file="layouts/shortcodes/myshortcode.html"} -{{ .Params.greeting }} → Hello -{{ .Params.name }} → world -``` diff --git a/docs/content/en/methods/shortcode/Parent.md b/docs/content/en/methods/shortcode/Parent.md deleted file mode 100644 index 91c445d2a..000000000 --- a/docs/content/en/methods/shortcode/Parent.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: Parent -description: Returns the parent shortcode context in nested shortcodes. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: hugolib.ShortcodeWithPage - signatures: [SHORTCODE.Parent] ---- - -This is useful for inheritance of common shortcode arguments from the root. - -In this contrived example, the "greeting" shortcode is the parent, and the "now" shortcode is child. - -```text {file="content/welcome.md"} -{{}} -Welcome. Today is {{}}. -{{}} -``` - -```go-html-template {file="layouts/shortcodes/greeting.html"} -
    - {{ .Inner | strings.TrimSpace | .Page.RenderString }} -
    -``` - -```go-html-template {file="layouts/shortcodes/now.html"} -{{- $dateFormat := "January 2, 2006 15:04:05" }} - -{{- with .Params }} - {{- with .dateFormat }} - {{- $dateFormat = . }} - {{- end }} -{{- else }} - {{- with .Parent.Params }} - {{- with .dateFormat }} - {{- $dateFormat = . }} - {{- end }} - {{- end }} -{{- end }} - -{{- now | time.Format $dateFormat -}} -``` - -The "now" shortcode formats the current time using: - -1. The `dateFormat` argument passed to the "now" shortcode, if present -1. The `dateFormat` argument passed to the "greeting" shortcode, if present -1. The default layout string defined at the top of the shortcode diff --git a/docs/content/en/methods/shortcode/Position.md b/docs/content/en/methods/shortcode/Position.md deleted file mode 100644 index 24810e825..000000000 --- a/docs/content/en/methods/shortcode/Position.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: Position -description: Returns the file name and position from which the shortcode was called. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: text.Position - signatures: [SHORTCODE.Position] ---- - -The `Position` method is useful for error reporting. For example, if your shortcode requires a "greeting" argument: - -```go-html-template {file="layouts/shortcodes/myshortcode.html"} -{{ $greeting := "" }} -{{ with .Get "greeting" }} - {{ $greeting = . }} -{{ else }} - {{ errorf "The %q shortcode requires a 'greeting' argument. See %s" .Name .Position }} -{{ end }} -``` - -In the absence of a "greeting" argument, Hugo will throw an error message and fail the build: - -```text -ERROR The "myshortcode" shortcode requires a 'greeting' argument. See "/home/user/project/content/about.md:11:1" -``` - -> [!note] -> The position can be expensive to calculate. Limit its use to error reporting. diff --git a/docs/content/en/methods/shortcode/Ref.md b/docs/content/en/methods/shortcode/Ref.md deleted file mode 100644 index 3a877d568..000000000 --- a/docs/content/en/methods/shortcode/Ref.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: Ref -description: Returns the absolute URL of the page with the given path, language, and output format. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [SHORTCODE.Ref OPTIONS] ---- - -## Usage - -The `Ref` method accepts a single argument: an options map. - -## Options - -{{% include "_common/ref-and-relref-options.md" %}} - -## Examples - -The following examples show the rendered output for a page on the English version of the site: - -```go-html-template -{{ $opts := dict "path" "/books/book-1" }} -{{ .Ref $opts }} → https://example.org/en/books/book-1/ - -{{ $opts := dict "path" "/books/book-1" "lang" "de" }} -{{ .Ref $opts }} → https://example.org/de/books/book-1/ - -{{ $opts := dict "path" "/books/book-1" "lang" "de" "outputFormat" "json" }} -{{ .Ref $opts }} → https://example.org/de/books/book-1/index.json -``` - -## Error handling - -{{% include "_common/ref-and-relref-error-handling.md" %}} diff --git a/docs/content/en/methods/shortcode/RelRef.md b/docs/content/en/methods/shortcode/RelRef.md deleted file mode 100644 index 273705a95..000000000 --- a/docs/content/en/methods/shortcode/RelRef.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: RelRef -description: Returns the relative URL of the page with the given path, language, and output format. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [SHORTCODE.RelRef OPTIONS] ---- - -## Usage - -The `RelRef` method accepts a single argument: an options map. - -## Options - -{{% include "_common/ref-and-relref-options.md" %}} - -## Examples - -The following examples show the rendered output for a page on the English version of the site: - -```go-html-template -{{ $opts := dict "path" "/books/book-1" }} -{{ .RelRef $opts }} → /en/books/book-1/ - -{{ $opts := dict "path" "/books/book-1" "lang" "de" }} -{{ .RelRef $opts }} → /de/books/book-1/ - -{{ $opts := dict "path" "/books/book-1" "lang" "de" "outputFormat" "json" }} -{{ .RelRef $opts }} → /de/books/book-1/index.json -``` - -## Error handling - -{{% include "_common/ref-and-relref-error-handling.md" %}} diff --git a/docs/content/en/methods/shortcode/Scratch.md b/docs/content/en/methods/shortcode/Scratch.md deleted file mode 100644 index 6efec2097..000000000 --- a/docs/content/en/methods/shortcode/Scratch.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -title: Scratch -description: Returns a "scratch pad" to store and manipulate data, scoped to the current shortcode. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: maps.Scratch - signatures: [SHORTCODE.Scratch] -expiryDate: 2026-11-18 # deprecated 2024-11-18 (soft) ---- - -{{< deprecated-in 0.139.0 >}} -Use the [`SHORTCODE.Store`] method instead. - -This is a soft deprecation. This method will be removed in a future release, but the removal date has not been established. Although Hugo will not emit a warning if you continue to use this method, you should begin using `SHORTCODE.Store` as soon as possible. - -Beginning with v0.139.0 the `SHORTCODE.Scratch` method is aliased to `SHORTCODE.Store`. - -[`SHORTCODE.Store`]: /methods/shortcode/store/ -{{< /deprecated-in >}} diff --git a/docs/content/en/methods/shortcode/Site.md b/docs/content/en/methods/shortcode/Site.md deleted file mode 100644 index 4c5a9a9b5..000000000 --- a/docs/content/en/methods/shortcode/Site.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: Site -description: Returns the Site object. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.siteWrapper - signatures: [SHORTCODE.Site] ---- - -See [Site methods]. - -[Site methods]: /methods/site/ - -```go-html-template -{{ .Site.Title }} -``` diff --git a/docs/content/en/methods/shortcode/Store.md b/docs/content/en/methods/shortcode/Store.md deleted file mode 100644 index 76cb9237d..000000000 --- a/docs/content/en/methods/shortcode/Store.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: Store -description: Returns a "scratch pad" to store and manipulate data, scoped to the current shortcode. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: maps.Scratch - signatures: [SHORTCODE.Store] ---- - -{{< new-in 0.139.0 />}} - -Use the `Store` method to create a [scratch pad](g) to store and manipulate data, scoped to the current shortcode. To create a scratch pad with a different [scope](g), refer to the [scope](#scope) section below. - -> [!note] -> With the introduction of the [`newScratch`] function, and the ability to [assign values to template variables] after initialization, the `Store` method within a shortcode is mostly obsolete. - -{{% include "_common/store-methods.md" %}} - -{{% include "_common/scratch-pad-scope.md" %}} - -[`newScratch`]: /functions/collections/newScratch/ -[assign values to template variables]: https://go.dev/doc/go1.11#texttemplatepkgtexttemplate diff --git a/docs/content/en/methods/shortcode/_index.md b/docs/content/en/methods/shortcode/_index.md deleted file mode 100644 index 0064f42aa..000000000 --- a/docs/content/en/methods/shortcode/_index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Shortcode methods -linkTitle: Shortcode -description: Use these methods in your shortcode templates. -categories: [] -keywords: [] -aliases: [/variables/shortcodes] ---- diff --git a/docs/content/en/methods/site/AllPages.md b/docs/content/en/methods/site/AllPages.md deleted file mode 100644 index 90cceee8c..000000000 --- a/docs/content/en/methods/site/AllPages.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: AllPages -description: Returns a collection of all pages in all languages. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pages - signatures: [SITE.AllPages] ---- - -This method returns all page [kinds](g) in all languages, in the [default sort order](g). That includes the home page, section pages, taxonomy pages, term pages, and regular pages. - -In most cases you should use the [`RegularPages`] method instead. - -[`RegularPages`]: /methods/site/regularpages/ - -```go-html-template -{{ range .Site.AllPages }} -

    {{ .LinkTitle }}

    -{{ end }} -``` diff --git a/docs/content/en/methods/site/BaseURL.md b/docs/content/en/methods/site/BaseURL.md deleted file mode 100644 index 3644443cb..000000000 --- a/docs/content/en/methods/site/BaseURL.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: BaseURL -description: Returns the base URL as defined in the site configuration. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [SITE.BaseURL] ---- - -Site configuration: - -{{< code-toggle file=hugo >}} -baseURL = 'https://example.org/docs/' -{{< /code-toggle >}} - -Template: - -```go-html-template -{{ .Site.BaseURL }} → https://example.org/docs/ -``` - -> [!note] -> There is almost never a good reason to use this method in your templates. Its usage tends to be fragile due to misconfiguration. -> -> Use the [`absURL`], [`absLangURL`], [`relURL`], or [`relLangURL`] functions instead. - -[`absLangURL`]: /functions/urls/absLangURL/ -[`absURL`]: /functions/urls/absURL/ -[`relLangURL`]: /functions/urls/relLangURL/ -[`relURL`]: /functions/urls/relURL/ diff --git a/docs/content/en/methods/site/BuildDrafts.md b/docs/content/en/methods/site/BuildDrafts.md deleted file mode 100644 index 4beceeb6b..000000000 --- a/docs/content/en/methods/site/BuildDrafts.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: BuildDrafts -description: Reports whether the current build includes draft pages. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: bool - signatures: [SITE.BuildDrafts] ---- - -By default, draft pages are not published when building a site. You can change this behavior with a command line flag: - -```sh -hugo --buildDrafts -``` - -Or by setting `buildDrafts` to `true` in your site configuration: - -{{< code-toggle file=hugo >}} -buildDrafts = true -{{< /code-toggle >}} - -Use the `BuildDrafts` method on a `Site` object to determine the current configuration: - -```go-html-template -{{ .Site.BuildDrafts }} → true -``` diff --git a/docs/content/en/methods/site/Config.md b/docs/content/en/methods/site/Config.md deleted file mode 100644 index d1b4d1f42..000000000 --- a/docs/content/en/methods/site/Config.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -title: Config -description: Returns a subset of the site configuration. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.SiteConfig - signatures: [SITE.Config] ---- - -The `Config` method on a `Site` object provides access to a subset of the site configuration, specifically the `services` and `privacy` keys. - -## Services - -See [configure services](/configuration/services). - -For example, to use Hugo's built-in Google Analytics template you must add a [Google tag ID]: - -[Google tag ID]: https://support.google.com/tagmanager/answer/12326985?hl=en - -{{< code-toggle file=hugo >}} -[services.googleAnalytics] -id = 'G-XXXXXXXXX' -{{< /code-toggle >}} - -To access this value from a template: - -```go-html-template -{{ .Site.Config.Services.GoogleAnalytics.ID }} → G-XXXXXXXXX -``` - -You must capitalize each identifier as shown above. - -## Privacy - -See [configure privacy](/configuration/privacy). - -For example, to disable usage of the built-in YouTube shortcode: - -{{< code-toggle file=hugo >}} -[privacy.youtube] -disable = true -{{< /code-toggle >}} - -To access this value from a template: - -```go-html-template -{{ .Site.Config.Privacy.YouTube.Disable }} → true -``` - -You must capitalize each identifier as shown above. diff --git a/docs/content/en/methods/site/Copyright.md b/docs/content/en/methods/site/Copyright.md deleted file mode 100644 index dd8bdb4a3..000000000 --- a/docs/content/en/methods/site/Copyright.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: Copyright -description: Returns the copyright notice as defined in the site configuration. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [SITE.Copyright] ---- - -Site configuration: - -{{< code-toggle file=hugo >}} -copyright = '© 2023 ABC Widgets, Inc.' -{{< /code-toggle >}} - -Template: - -```go-html-template -{{ .Site.Copyright }} → © 2023 ABC Widgets, Inc. -``` diff --git a/docs/content/en/methods/site/Data.md b/docs/content/en/methods/site/Data.md deleted file mode 100644 index 296851874..000000000 --- a/docs/content/en/methods/site/Data.md +++ /dev/null @@ -1,103 +0,0 @@ ---- -title: Data -description: Returns a data structure composed from the files in the data directory. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: map - signatures: [SITE.Data] ---- - -Use the `Data` method on a `Site` object to access data within the `data` directory, or within any directory [mounted] to the `data` directory. Supported data formats include JSON, TOML, YAML, and XML. - -> [!note] -> Although Hugo can unmarshal CSV files with the [`transform.Unmarshal`] function, do not place CSV files in the `data` directory. You cannot access data within CSV files using this method. - -Consider this `data` directory: - -```text -data/ -├── books/ -│ ├── fiction.yaml -│ └── nonfiction.yaml -├── films.json -├── paintings.xml -└── sculptures.toml -``` - -And these data files: - -```yaml {file="data/books/fiction.yaml"} -- title: The Hunchback of Notre Dame - author: Victor Hugo - isbn: 978-0140443530 -- title: Les Misérables - author: Victor Hugo - isbn: 978-0451419439 -``` - -```yaml {file="data/books/nonfiction.yaml"} -- title: The Ancien Régime and the Revolution - author: Alexis de Tocqueville - isbn: 978-0141441641 -- title: Interpreting the French Revolution - author: François Furet - isbn: 978-0521280495 -``` - -Access the data by [chaining](g) the [identifiers](g): - -```go-html-template -{{ range $category, $books := .Site.Data.books }} -

    {{ $category | title }}

    -
      - {{ range $books }} -
    • {{ .title }} ({{ .isbn }})
    • - {{ end }} -
    -{{ end }} -``` - -Hugo renders this to: - -```html -

    Fiction

    -
      -
    • The Hunchback of Notre Dame (978-0140443530)
    • -
    • Les Misérables (978-0451419439)
    • -
    -

    Nonfiction

    -
      -
    • The Ancien Régime and the Revolution (978-0141441641)
    • -
    • Interpreting the French Revolution (978-0521280495)
    • -
    -``` - -To limit the listing to fiction, and sort by title: - -```go-html-template -
      - {{ range sort .Site.Data.books.fiction "title" }} -
    • {{ .title }} ({{ .author }})
    • - {{ end }} -
    -``` - -To find a fiction book by ISBN: - -```go-html-template -{{ range where .Site.Data.books.fiction "isbn" "978-0140443530" }} -
  • {{ .title }} ({{ .author }})
  • -{{ end }} -``` - -In the template examples above, each of the keys is a valid identifier. For example, none of the keys contains a hyphen. To access a key that is not a valid identifier, use the [`index`] function. For example: - -```go-html-template -{{ index .Site.Data.books "historical-fiction" }} -``` - -[`index`]: /functions/collections/indexfunction/ -[`transform.Unmarshal`]: /functions/transform/unmarshal/ -[mounted]: /configuration/module/#mounts diff --git a/docs/content/en/methods/site/DisqusShortname.md b/docs/content/en/methods/site/DisqusShortname.md deleted file mode 100644 index de679fd7e..000000000 --- a/docs/content/en/methods/site/DisqusShortname.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: DisqusShortname -description: Returns the Disqus shortname as defined in the site configuration. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [SITE.DisqusShortname] -expiryDate: 2025-10-30 # deprecated 2023-10-30 in v0.120.0 ---- - -{{< deprecated-in 0.120.0 >}} -Use [`Site.Config.Services.Disqus.Shortname`] instead. - -[`Site.Config.Services.Disqus.Shortname`]: /methods/site/config/ -{{< /deprecated-in >}} diff --git a/docs/content/en/methods/site/GetPage.md b/docs/content/en/methods/site/GetPage.md deleted file mode 100644 index 2a3bd7d59..000000000 --- a/docs/content/en/methods/site/GetPage.md +++ /dev/null @@ -1,105 +0,0 @@ ---- -title: GetPage -description: Returns a Page object from the given path. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Page - signatures: [SITE.GetPage PATH] ---- - -The `GetPage` method is also available on `Page` objects, allowing you to specify a path relative to the current page. See [details]. - -[details]: /methods/page/getpage/ - -When using the `GetPage` method on a `Site` object, specify a path relative to the `content` directory. - -If Hugo cannot resolve the path to a page, the method returns nil. - -Consider this content structure: - -```text -content/ -├── works/ -│ ├── paintings/ -│ │ ├── _index.md -│ │ ├── starry-night.md -│ │ └── the-mona-lisa.md -│ ├── sculptures/ -│ │ ├── _index.md -│ │ ├── david.md -│ │ └── the-thinker.md -│ └── _index.md -└── _index.md -``` - -This home template: - -```go-html-template -{{ with .Site.GetPage "/works/paintings" }} -
      - {{ range .Pages }} -
    • {{ .Title }} by {{ .Params.artist }}
    • - {{ end }} -
    -{{ end }} -``` - -Is rendered to: - -```html -
      -
    • Starry Night by Vincent van Gogh
    • -
    • The Mona Lisa by Leonardo da Vinci
    • -
    -``` - -To get a regular page instead of a section page: - -```go-html-template -{{ with .Site.GetPage "/works/paintings/starry-night" }} - {{ .Title }} → Starry Night - {{ .Params.artist }} → Vincent van Gogh -{{ end }} -``` - -## Multilingual projects - -With multilingual projects, the `GetPage` method on a `Site` object resolves the given path to a page in the current language. - -To get a page from a different language, query the `Sites` object: - -```go-html-template -{{ with where .Site.Sites "Language.Lang" "eq" "de" }} - {{ with index . 0 }} - {{ with .GetPage "/works/paintings/starry-night" }} - {{ .Title }} → Sternenklare Nacht - {{ end }} - {{ end }} -{{ end }} -``` - -## Page bundles - -Consider this content structure: - -```text -content/ -├── headless/ -│ ├── a.jpg -│ ├── b.jpg -│ ├── c.jpg -│ └── index.md <-- front matter: headless = true -└── _index.md -``` - -In the home template, use the `GetPage` method on a `Site` object to render all the images in the headless [page bundle](g): - -```go-html-template -{{ with .Site.GetPage "/headless" }} - {{ range .Resources.ByType "image" }} - - {{ end }} -{{ end }} -``` diff --git a/docs/content/en/methods/site/GoogleAnalytics.md b/docs/content/en/methods/site/GoogleAnalytics.md deleted file mode 100644 index e4d28bcce..000000000 --- a/docs/content/en/methods/site/GoogleAnalytics.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: GoogleAnalytics -description: Returns the Google Analytics tracking ID as defined in the site configuration. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [SITE.GoogleAnalytics] -expiryDate: 2025-10-30 # deprecated 2023-10-30 in v0.120.0 ---- - -{{< deprecated-in 0.120.0 >}} -Use [`Site.Config.Services.GoogleAnalytics.ID`] instead. - -[`Site.Config.Services.GoogleAnalytics.ID`]: /methods/site/config/ -{{< /deprecated-in >}} diff --git a/docs/content/en/methods/site/Home.md b/docs/content/en/methods/site/Home.md deleted file mode 100644 index 19ab61747..000000000 --- a/docs/content/en/methods/site/Home.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: Home -description: Returns the home Page object for the given site. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Page - signatures: [SITE.Home] ---- - -This method is useful for obtaining a link to the home page. - -Site configuration: - -{{< code-toggle file=hugo >}} -baseURL = 'https://example.org/docs/' -{{< /code-toggle >}} - -Template: - -```go-html-template -{{ .Site.Home.Permalink }} → https://example.org/docs/ -{{ .Site.Home.RelPermalink }} → /docs/ -``` diff --git a/docs/content/en/methods/site/IsDevelopment.md b/docs/content/en/methods/site/IsDevelopment.md deleted file mode 100644 index cddd18818..000000000 --- a/docs/content/en/methods/site/IsDevelopment.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: IsDevelopment -description: Reports whether the current running environment is “development”. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: bool - signatures: [SITE.IsDevelopment] -expiryDate: 2025-10-30 # deprecated 2023-10-30 in v0.120.0 ---- - -{{< deprecated-in 0.120.0 >}} -Use [`hugo.IsDevelopment`] instead. - -[`hugo.IsDevelopment`]: /functions/hugo/isdevelopment/ -{{< /deprecated-in >}} diff --git a/docs/content/en/methods/site/IsMultiLingual.md b/docs/content/en/methods/site/IsMultiLingual.md deleted file mode 100644 index 3f9723f1c..000000000 --- a/docs/content/en/methods/site/IsMultiLingual.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: IsMultiLingual -description: Reports whether there are two or more configured languages. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: bool - signatures: [SITE.IsMultiLingual] -expiryDate: 2026-03-16 # deprecated 2024-03-16 in 0.124.0 ---- - -{{< deprecated-in 0.124.0 >}} -Use [`hugo.IsMultilingual`] instead. - -[`hugo.IsMultilingual`]: /functions/hugo/ismultilingual/ -{{< /deprecated-in >}} diff --git a/docs/content/en/methods/site/IsServer.md b/docs/content/en/methods/site/IsServer.md deleted file mode 100644 index 8b09c8492..000000000 --- a/docs/content/en/methods/site/IsServer.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: IsServer -description: Reports whether the built-in development server is running. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: bool - signatures: [SITE.IsServer] -expiryDate: 2025-10-30 # deprecated 2023-10-30 in v0.120.0 ---- - -{{< deprecated-in 0.120.0 >}} -Use [`hugo.IsServer`] instead. - -[`hugo.IsServer`]: /functions/hugo/isserver/ -{{< /deprecated-in >}} diff --git a/docs/content/en/methods/site/Language.md b/docs/content/en/methods/site/Language.md deleted file mode 100644 index 31f15b8cb..000000000 --- a/docs/content/en/methods/site/Language.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -title: Language -description: Returns the language object for the given site. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: langs.Language - signatures: [SITE.Language] ---- - -The `Language` method on a `Site` object returns the language object for the given site. The language object points to the language definition in the site configuration. - -You can also use the `Language` method on a `Page` object. See [details]. - -## Methods - -The examples below assume the following in your site configuration: - -{{< code-toggle file=hugo >}} -[languages.de] -languageCode = 'de-DE' -languageDirection = 'ltr' -languageName = 'Deutsch' -weight = 1 -{{< /code-toggle >}} - -### Lang - -(`string`) The language tag as defined by [RFC 5646]. - -```go-html-template -{{ .Site.Language.Lang }} → de -``` - -### LanguageCode - -(`string`) The language code from the site configuration. Falls back to `Lang` if not defined. - -```go-html-template -{{ .Site.Language.LanguageCode }} → de-DE -``` - -### LanguageDirection - -(`string`) The language direction from the site configuration, either `ltr` or `rtl`. - -```go-html-template -{{ .Site.Language.LanguageDirection }} → ltr -``` - -### LanguageName - -(`string`) The language name from the site configuration. - -```go-html-template -{{ .Site.Language.LanguageName }} → Deutsch -``` - -### Weight - -(`int`) The language weight from the site configuration which determines its order in the slice of languages returned by the `Languages` method on a `Site` object. - -```go-html-template -{{ .Site.Language.Weight }} → 1 -``` - -## Example - -Some of the methods above are commonly used in a base template as attributes for the `html` element. - -```go-html-template -{{ debug.Dump .Site.Languages }}
    -``` - -With this site configuration: - -{{< code-toggle file=hugo >}} -defaultContentLanguage = 'de' -defaultContentLanguageInSubdir = false - -[languages.de] -languageCode = 'de-DE' -languageDirection = 'ltr' -languageName = 'Deutsch' -title = 'Projekt Dokumentation' -weight = 1 - -[languages.en] -languageCode = 'en-US' -languageDirection = 'ltr' -languageName = 'English' -title = 'Project Documentation' -weight = 2 -{{< /code-toggle >}} - -This template: - -```go-html-template -
      - {{ range .Site.Languages }} -
    • {{ .Title }} ({{ .LanguageName }})
    • - {{ end }} -
    -``` - -Is rendered to: - -```html -
      -
    • Projekt Dokumentation (Deutsch)
    • -
    • Project Documentation (English)
    • -
    -``` diff --git a/docs/content/en/methods/site/LastChange.md b/docs/content/en/methods/site/LastChange.md deleted file mode 100644 index e02937bf1..000000000 --- a/docs/content/en/methods/site/LastChange.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: LastChange -description: Returns the last modification date of site content. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: time.Time - signatures: [SITE.LastChange] -expiryDate: 2026-02-19 # deprecated 2024-02-19 in v0.123.0 ---- - -{{< deprecated-in 0.123.0 >}} -Use [`.Site.Lastmod`] instead. - -[`.Site.Lastmod`]: /methods/site/lastmod/ -{{< /deprecated-in >}} diff --git a/docs/content/en/methods/site/Lastmod.md b/docs/content/en/methods/site/Lastmod.md deleted file mode 100644 index 38f6da2fa..000000000 --- a/docs/content/en/methods/site/Lastmod.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: Lastmod -description: Returns the last modification date of site content. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: time.Time - signatures: [SITE.Lastmod] ---- - -{{< new-in 0.123.0 />}} - -The `Lastmod` method on a `Site` object returns a [`time.Time`] value. Use this with time [functions] and [methods]. For example: - -```go-html-template -{{ .Site.Lastmod | time.Format ":date_long" }} → January 31, 2024 - -``` - -[`time.Time`]: https://pkg.go.dev/time#Time -[functions]: /functions/time/ -[methods]: /methods/time/ diff --git a/docs/content/en/methods/site/MainSections.md b/docs/content/en/methods/site/MainSections.md deleted file mode 100644 index bee4f2d57..000000000 --- a/docs/content/en/methods/site/MainSections.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: MainSections -description: Returns a slice of the main section names as defined in the site configuration, falling back to the top-level section with the most pages. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: '[]string' - signatures: [SITE.MainSections] ---- - -Site configuration: - -{{< code-toggle file=hugo >}} -mainSections = ['books','films'] -{{< /code-toggle >}} - -Template: - -```go-html-template -{{ .Site.MainSections }} → [books films] -``` - -If `mainSections` is not defined in the site configuration, this method returns a slice with one element---the top-level section with the most pages. - -With this content structure, the "films" section has the most pages: - -```text -content/ -├── books/ -│ ├── book-1.md -│ └── book-2.md -├── films/ -│ ├── film-1.md -│ ├── film-2.md -│ └── film-3.md -└── _index.md -``` - -Template: - -```go-html-template -{{ .Site.MainSections }} → [films] -``` - -When creating a theme, instead of hardcoding section names when listing the most relevant pages on the front page, instruct site authors to set `mainSections` in their site configuration. - -Then your home template can do something like this: - -```go-html-template -{{ range where .Site.RegularPages "Section" "in" .Site.MainSections }} -

    {{ .LinkTitle }}

    -{{ end }} -``` diff --git a/docs/content/en/methods/site/Menus.md b/docs/content/en/methods/site/Menus.md deleted file mode 100644 index 398a9b022..000000000 --- a/docs/content/en/methods/site/Menus.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -title: Menus -description: Returns a collection of menu objects for the given site. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: navigation.Menus - signatures: [SITE.Menus] ---- - -The `Menus` method on a `Site` object returns a collection of menus, where each menu contains one or more entries, either flat or nested. Each entry points to a page within the site, or to an external resource. - -> [!note] -> Menus can be defined and localized in several ways. Please see the [menus] section for a complete explanation and examples. - -A site can have multiple menus. For example, a main menu and a footer menu: - -{{< code-toggle file=hugo >}} -[[menus.main]] -name = 'Home' -pageRef = '/' -weight = 10 - -[[menus.main]] -name = 'Books' -pageRef = '/books' -weight = 20 - -[[menus.main]] -name = 'Films' -pageRef = '/films' -weight = 30 - -[[menus.footer]] -name = 'Legal' -pageRef = '/legal' -weight = 10 - -[[menus.footer]] -name = 'Privacy' -pageRef = '/privacy' -weight = 20 -{{< /code-toggle >}} - -This template renders the main menu: - -```go-html-template -{{ with site.Menus.main }} - -{{ end }} -``` - -When viewing the home page, the result is: - -```html - -``` - -When viewing the "books" page, the result is: - -```html - -``` - -You will typically render a menu using a partial template. As the active menu entry will be different on each page, use the [`partial`] function to call the template. Do not use the [`partialCached`] function. - -The example above is simplistic. Please see the [menu templates] section for more information. - -[`partial`]: /functions/partials/include/ -[`partialCached`]: /functions/partials/includecached/ -[menu templates]: /templates/menu/ -[menus]: /content-management/menus/ diff --git a/docs/content/en/methods/site/Pages.md b/docs/content/en/methods/site/Pages.md deleted file mode 100644 index a6ba5e029..000000000 --- a/docs/content/en/methods/site/Pages.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: Pages -description: Returns a collection of all pages. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pages - signatures: [SITE.Pages] ---- - -This method returns all page [kinds](g) in the current language, in the [default sort order](g). That includes the home page, section pages, taxonomy pages, term pages, and regular pages. - -In most cases you should use the [`RegularPages`] method instead. - -[`RegularPages`]: /methods/site/regularpages/ - -```go-html-template -{{ range .Site.Pages }} -

    {{ .LinkTitle }}

    -{{ end }} -``` diff --git a/docs/content/en/methods/site/Param.md b/docs/content/en/methods/site/Param.md deleted file mode 100644 index 929e30e98..000000000 --- a/docs/content/en/methods/site/Param.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: Param -description: Returns the site parameter with the given key. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: any - signatures: [SITE.Param KEY] ---- - -The `Param` method on a `Site` object is a convenience method to return the value of a user-defined parameter in the site configuration. - -{{< code-toggle file=hugo >}} -[params] -display_toc = true -{{< /code-toggle >}} - -```go-html-template -{{ .Site.Param "display_toc" }} → true -``` - -The above is equivalent to either of these: - -```go-html-template -{{ .Site.Params.display_toc }} -{{ index .Site.Params "display_toc" }} -``` diff --git a/docs/content/en/methods/site/Params.md b/docs/content/en/methods/site/Params.md deleted file mode 100644 index 8467be41d..000000000 --- a/docs/content/en/methods/site/Params.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -title: Params -description: Returns a map of custom parameters as defined in the site configuration. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: maps.Params - signatures: [SITE.Params] ---- - -With this site configuration: - -{{< code-toggle file=hugo >}} -[params] - subtitle = 'The Best Widgets on Earth' - copyright-year = '2023' - [params.author] - email = 'jsmith@example.org' - name = 'John Smith' - [params.layouts] - rfc_1123 = 'Mon, 02 Jan 2006 15:04:05 MST' - rfc_3339 = '2006-01-02T15:04:05-07:00' -{{< /code-toggle >}} - -Access the custom parameters by [chaining](g) the [identifiers](g): - -```go-html-template -{{ .Site.Params.subtitle }} → The Best Widgets on Earth -{{ .Site.Params.author.name }} → John Smith - -{{ $layout := .Site.Params.layouts.rfc_1123 }} -{{ .Site.Lastmod.Format $layout }} → Tue, 17 Oct 2023 13:21:02 PDT -``` - -In the template example above, each of the keys is a valid identifier. For example, none of the keys contains a hyphen. To access a key that is not a valid identifier, use the [`index`] function: - -```go-html-template -{{ index .Site.Params "copyright-year" }} → 2023 -``` - -[`index`]: /functions/collections/indexfunction/ diff --git a/docs/content/en/methods/site/RegularPages.md b/docs/content/en/methods/site/RegularPages.md deleted file mode 100644 index 69a460529..000000000 --- a/docs/content/en/methods/site/RegularPages.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: RegularPages -description: Returns a collection of all regular pages. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pages - signatures: [SITE.RegularPages] ---- - -The `RegularPages` method on a `Site` object returns a collection of all [regular pages](g), in the [default sort order](g). - -```go-html-template -{{ range .Site.RegularPages }} -

    {{ .LinkTitle }}

    -{{ end }} -``` - -{{% glossary-term "default sort order" %}} - -[default sort order](g) - -To change the sort order, use any of the `Pages` [sorting methods]. For example: - -```go-html-template -{{ range .Site.RegularPages.ByTitle }} -

    {{ .Title }}

    -{{ end }} -``` - -[sorting methods]: /methods/pages/ diff --git a/docs/content/en/methods/site/Sections.md b/docs/content/en/methods/site/Sections.md deleted file mode 100644 index 0ddaf0626..000000000 --- a/docs/content/en/methods/site/Sections.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -title: Sections -description: Returns a collection of top-level section pages. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Pages - signatures: [SITE.Sections] ---- - -The `Sections` method on a `Site` object returns a collection of top-level [section pages](g), in the [default sort order](g). - -Given this content structure: - -```text -content/ -├── books/ -│ ├── book-1.md -│ └── book-2.md -├── films/ -│ ├── film-1.md -│ └── film-2.md -└── _index.md -``` - -This template: - -```go-html-template -{{ range .Site.Sections }} -

    {{ .LinkTitle }}

    -{{ end }} -``` - -Is rendered to: - -```html -

    Books

    -

    Films

    -``` diff --git a/docs/content/en/methods/site/Sites.md b/docs/content/en/methods/site/Sites.md deleted file mode 100644 index cca71a40a..000000000 --- a/docs/content/en/methods/site/Sites.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: Sites -description: Returns a collection of all Site objects, one for each language, ordered by default content language then by language weight. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Sites - signatures: [SITE.Sites] ---- - -With this site configuration: - -{{< code-toggle file=hugo >}} -defaultContentLanguage = 'de' -defaultContentLanguageInSubdir = false - -[languages.de] -languageCode = 'de-DE' -languageDirection = 'ltr' -languageName = 'Deutsch' -title = 'Projekt Dokumentation' -weight = 1 - -[languages.en] -languageCode = 'en-US' -languageDirection = 'ltr' -languageName = 'English' -title = 'Project Documentation' -weight = 2 -{{< /code-toggle >}} - -This template: - -```go-html-template - -``` - -Produces a list of links to each home page: - -```html - -``` - -To render a link to the home page of the site corresponding to the default content language: - -```go-html-template -{{ with .Site.Sites.Default }} - {{ .Title }} -{{ end }} -``` - -This is equivalent to: - -```go-html-template -{{ with index .Site.Sites 0 }} - {{ .Title }} -{{ end }} -``` diff --git a/docs/content/en/methods/site/Store.md b/docs/content/en/methods/site/Store.md deleted file mode 100644 index 7dcf7d095..000000000 --- a/docs/content/en/methods/site/Store.md +++ /dev/null @@ -1,117 +0,0 @@ ---- -title: Store -description: Returns a "scratch pad" to store and manipulate data, scoped to the current site. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: maps.Scratch - signatures: [site.Store] ---- - -{{< new-in 0.139.0 />}} - -Use the `Store` method on a `Site` object to create a [scratch pad](g) to store and manipulate data, scoped to the current site. To create a scratch pad with a different [scope](g), refer to the [scope](#scope) section below. - -## Methods - -### Set - -Sets the value of a given key. - -```go-html-template -{{ site.Store.Set "greeting" "Hello" }} -``` - -### Get - -Gets the value of a given key. - -```go-html-template -{{ site.Store.Set "greeting" "Hello" }} -{{ site.Store.Get "greeting" }} → Hello -``` - -### Add - -Adds a given value to existing value(s) of the given key. - -For single values, `Add` accepts values that support Go's `+` operator. If the first `Add` for a key is an array or slice, the following adds will be appended to that list. - -```go-html-template -{{ site.Store.Set "greeting" "Hello" }} -{{ site.Store.Add "greeting" "Welcome" }} -{{ site.Store.Get "greeting" }} → HelloWelcome -``` - -```go-html-template -{{ site.Store.Set "total" 3 }} -{{ site.Store.Add "total" 7 }} -{{ site.Store.Get "total" }} → 10 -``` - -```go-html-template -{{ site.Store.Set "greetings" (slice "Hello") }} -{{ site.Store.Add "greetings" (slice "Welcome" "Cheers") }} -{{ site.Store.Get "greetings" }} → [Hello Welcome Cheers] - ``` - -### SetInMap - -Takes a `key`, `mapKey` and `value` and adds a map of `mapKey` and `value` to the given `key`. - -```go-html-template -{{ site.Store.SetInMap "greetings" "english" "Hello" }} -{{ site.Store.SetInMap "greetings" "french" "Bonjour" }} -{{ site.Store.Get "greetings" }} → map[english:Hello french:Bonjour] -``` - -### DeleteInMap - -Takes a `key` and `mapKey` and removes the map of `mapKey` from the given `key`. - -```go-html-template -{{ site.Store.SetInMap "greetings" "english" "Hello" }} -{{ site.Store.SetInMap "greetings" "french" "Bonjour" }} -{{ site.Store.DeleteInMap "greetings" "english" }} -{{ site.Store.Get "greetings" }} → map[french:Bonjour] -``` - -### GetSortedMapValues - -Returns an array of values from `key` sorted by `mapKey`. - -```go-html-template -{{ site.Store.SetInMap "greetings" "english" "Hello" }} -{{ site.Store.SetInMap "greetings" "french" "Bonjour" }} -{{ site.Store.GetSortedMapValues "greetings" }} → [Hello Bonjour] -``` - -### Delete - -Removes the given key. - -```go-html-template -{{ site.Store.Set "greeting" "Hello" }} -{{ site.Store.Delete "greeting" }} -``` - -{{% include "_common/scratch-pad-scope.md" %}} - -## Determinate values - -The `Store` method is often used to set scratch pad values within a shortcode, a partial template called by a shortcode, or by a Markdown render hook. In all three cases, the scratch pad values are indeterminate until Hugo renders the page content. - -If you need to access a scratch pad value from a parent template, and the parent template has not yet rendered the page content, you can trigger content rendering by assigning the returned value to a [noop](g) variable: - -```go-html-template -{{ $noop := .Content }} -{{ site.Store.Get "mykey" }} -``` - -You can also trigger content rendering with the `ContentWithoutSummary`, `FuzzyWordCount`, `Len`, `Plain`, `PlainWords`, `ReadingTime`, `Summary`, `Truncated`, and `WordCount` methods. For example: - -```go-html-template -{{ $noop := .WordCount }} -{{ site.Store.Get "mykey" }} -``` diff --git a/docs/content/en/methods/site/Taxonomies.md b/docs/content/en/methods/site/Taxonomies.md deleted file mode 100644 index 92dc41a9b..000000000 --- a/docs/content/en/methods/site/Taxonomies.md +++ /dev/null @@ -1,176 +0,0 @@ ---- -title: Taxonomies -description: Returns a data structure containing the site's Taxonomy objects, the terms within each Taxonomy object, and the pages to which the terms are assigned. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.TaxonomyList - signatures: [SITE.Taxonomies] ---- - -Conceptually, the `Taxonomies` method on a `Site` object returns a data structure such as: - -{{< code-toggle file=hugo >}} -taxonomy a: - - term 1: - - page 1 - - page 2 - - term 2: - - page 1 -taxonomy b: - - term 1: - - page 2 - - term 2: - - page 1 - - page 2 -{{< /code-toggle >}} - -For example, on a book review site you might create two taxonomies; one for genres and another for authors. - -With this site configuration: - -{{< code-toggle file=hugo >}} -[taxonomies] -genre = 'genres' -author = 'authors' -{{< /code-toggle >}} - -And this content structure: - -```text -content/ -├── books/ -│ ├── and-then-there-were-none.md --> genres: suspense -│ ├── death-on-the-nile.md --> genres: suspense -│ └── jamaica-inn.md --> genres: suspense, romance -│ └── pride-and-prejudice.md --> genres: romance -└── _index.md -``` - -Conceptually, the taxonomies data structure looks like: - -{{< code-toggle file=hugo >}} -genres: - - suspense: - - And Then There Were None - - Death on the Nile - - Jamaica Inn - - romance: - - Jamaica Inn - - Pride and Prejudice -authors: - - achristie: - - And Then There Were None - - Death on the Nile - - ddmaurier: - - Jamaica Inn - - jausten: - - Pride and Prejudice -{{< /code-toggle >}} - -To list the "suspense" books: - -```go-html-template -
      - {{ range .Site.Taxonomies.genres.suspense }} -
    • {{ .LinkTitle }}
    • - {{ end }} -
    -``` - -Hugo renders this to: - -```html - -``` - -> [!note] -> Hugo's taxonomy system is powerful, allowing you to classify content and create relationships between pages. -> -> Please see the [taxonomies] section for a complete explanation and examples. - -## Examples - -### List content with the same taxonomy term - -If you are using a taxonomy for something like a series of posts, you can list individual pages associated with the same term. For example: - -```go-html-template - -``` - -### List all content in a given taxonomy - -This would be very useful in a sidebar as “featured content”. You could even have different sections of “featured content” by assigning different terms to the content. - -```go-html-template - -``` - -### Render a site's taxonomies - -The following example displays all terms in a site's tags taxonomy: - -```go-html-template - -``` -This example will list all taxonomies and their terms, as well as all the content assigned to each of the terms. - -```go-html-template {file="layouts/partials/all-taxonomies.html"} -{{ with .Site.Taxonomies }} - {{ $numberOfTerms := 0 }} - {{ range $taxonomy, $terms := . }} - {{ $numberOfTerms = len . | add $numberOfTerms }} - {{ end }} - - {{ if gt $numberOfTerms 0 }} - - {{ end }} -{{ end }} -``` - -[taxonomies]: /content-management/taxonomies/ diff --git a/docs/content/en/methods/site/Title.md b/docs/content/en/methods/site/Title.md deleted file mode 100644 index 935edda0c..000000000 --- a/docs/content/en/methods/site/Title.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: Title -description: Returns the title as defined in the site configuration. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [SITE.Title] ---- - -Site configuration: - -{{< code-toggle file=hugo >}} -title = 'My Documentation Site' -{{< /code-toggle >}} - -Template: - -```go-html-template -{{ .Site.Title }} → My Documentation Site -``` diff --git a/docs/content/en/methods/site/_index.md b/docs/content/en/methods/site/_index.md deleted file mode 100644 index f395a3693..000000000 --- a/docs/content/en/methods/site/_index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Site methods -linkTitle: Site -description: Use these methods with Site objects. -categories: [] -keywords: [] -aliases: [/variables/site/] ---- diff --git a/docs/content/en/methods/taxonomy/Alphabetical.md b/docs/content/en/methods/taxonomy/Alphabetical.md deleted file mode 100644 index af4af596c..000000000 --- a/docs/content/en/methods/taxonomy/Alphabetical.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: Alphabetical -description: Returns an ordered taxonomy, sorted alphabetically by term. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.OrderedTaxonomy - signatures: [TAXONOMY.Alphabetical] ---- - -The `Alphabetical` method on a `Taxonomy` object returns an [ordered taxonomy](g), sorted alphabetically by [term](g). - -While a `Taxonomy` object is a [map](g), an ordered taxonomy is a [slice](g), where each element is an object that contains the term and a slice of its [weighted pages](g). - -{{% include "/_common/methods/taxonomy/get-a-taxonomy-object.md" %}} - -## Get the ordered taxonomy - -Now that we have captured the “genres” Taxonomy object, let's get the ordered taxonomy sorted alphabetically by term: - -```go-html-template -{{ $taxonomyObject.Alphabetical }} -``` - -To reverse the sort order: - -```go-html-template -{{ $taxonomyObject.Alphabetical.Reverse }} -``` - -To inspect the data structure: - -```go-html-template -
    {{ debug.Dump $taxonomyObject.Alphabetical }}
    -``` - -{{% include "/_common/methods/taxonomy/ordered-taxonomy-element-methods.md" %}} - -## Example - -With this template: - -```go-html-template -{{ range $taxonomyObject.Alphabetical }} -

    {{ .Page.LinkTitle }} ({{ .Count }})

    - -{{ end }} -``` - -Hugo renders: - -```html -

    romance (2)

    - -

    suspense (3)

    - -``` diff --git a/docs/content/en/methods/taxonomy/ByCount.md b/docs/content/en/methods/taxonomy/ByCount.md deleted file mode 100644 index fbf9bb4a1..000000000 --- a/docs/content/en/methods/taxonomy/ByCount.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: ByCount -description: Returns an ordered taxonomy, sorted by the number of pages associated with each term. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.OrderedTaxonomy - signatures: [TAXONOMY.ByCount] ---- - -The `ByCount` method on a `Taxonomy` object returns an [ordered taxonomy](g), sorted by the number of pages associated with each [term](g). - -While a `Taxonomy` object is a [map](g), an ordered taxonomy is a [slice](g), where each element is an object that contains the term and a slice of its [weighted pages](g). - -{{% include "/_common/methods/taxonomy/get-a-taxonomy-object.md" %}} - -## Get the ordered taxonomy - -Now that we have captured the “genres” Taxonomy object, let's get the ordered taxonomy sorted by the number of pages associated with each term: - -```go-html-template -{{ $taxonomyObject.ByCount }} -``` - -To reverse the sort order: - -```go-html-template -{{ $taxonomyObject.ByCount.Reverse }} -``` - -To inspect the data structure: - -```go-html-template -
    {{ debug.Dump $taxonomyObject.ByCount }}
    -``` - -{{% include "/_common/methods/taxonomy/ordered-taxonomy-element-methods.md" %}} - -## Example - -With this template: - -```go-html-template -{{ range $taxonomyObject.ByCount }} -

    {{ .Page.LinkTitle }} ({{ .Count }})

    - -{{ end }} -``` - -Hugo renders: - -```html -

    suspense (3)

    - -

    romance (2)

    - -``` diff --git a/docs/content/en/methods/taxonomy/Count.md b/docs/content/en/methods/taxonomy/Count.md deleted file mode 100644 index 76af8ee04..000000000 --- a/docs/content/en/methods/taxonomy/Count.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: Count -description: Returns the number of number of weighted pages to which the given term has been assigned. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: int - signatures: [TAXONOMY.Count TERM] ---- - -The `Count` method on a `Taxonomy` object returns the number of number of [weighted pages](g) to which the given [term](g) has been assigned. - -{{% include "/_common/methods/taxonomy/get-a-taxonomy-object.md" %}} - -## Count the weighted pages - -Now that we have captured the "genres" `Taxonomy` object, let's count the number of weighted pages to which the "suspense" term has been assigned: - -```go-html-template -{{ $taxonomyObject.Count "suspense" }} → 3 -``` diff --git a/docs/content/en/methods/taxonomy/Get.md b/docs/content/en/methods/taxonomy/Get.md deleted file mode 100644 index 03c184868..000000000 --- a/docs/content/en/methods/taxonomy/Get.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: Get -description: Returns a slice of weighted pages to which the given term has been assigned. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.WeightedPages - signatures: [TAXONOMY.Get TERM] ---- - -The `Get` method on a `Taxonomy` object returns a slice of [weighted pages](g) to which the given [term](g) has been assigned. - -{{% include "/_common/methods/taxonomy/get-a-taxonomy-object.md" %}} - -## Get the weighted pages - -Now that we have captured the "genres" `Taxonomy` object, let's get the weighted pages to which the "suspense" term has been assigned: - -```go-html-template -{{ $weightedPages := $taxonomyObject.Get "suspense" }} -``` - -The above is equivalent to: - -```go-html-template -{{ $weightedPages := $taxonomyObject.suspense }} -``` - -But, if the term is not a valid [identifier](g), you cannot use the [chaining](g) syntax. For example, this will throw an error because the identifier contains a hyphen: - -```go-html-template -{{ $weightedPages := $taxonomyObject.my-genre }} -``` - -You could also use the [`index`] function, but the syntax is more verbose: - -```go-html-template -{{ $weightedPages := index $taxonomyObject "my-genre" }} -``` - -To inspect the data structure: - -```go-html-template -
    {{ debug.Dump $weightedPages }}
    -``` - -## Example - -With this template: - -```go-html-template -{{ $weightedPages := $taxonomyObject.Get "suspense" }} -{{ range $weightedPages }} -

    {{ .LinkTitle }}

    -{{ end }} -``` - -Hugo renders: - -```html -

    Jamaica inn

    -

    Death on the nile

    -

    And then there were none

    -``` - -[`index`]: /functions/collections/indexfunction/ diff --git a/docs/content/en/methods/taxonomy/Page.md b/docs/content/en/methods/taxonomy/Page.md deleted file mode 100644 index b0b5d3aff..000000000 --- a/docs/content/en/methods/taxonomy/Page.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: Page -description: Returns the taxonomy page or nil if the taxonomy has no terms. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: page.Page - signatures: [TAXONOMY.Page] ---- - -{{< new-in 0.125.0 />}} - -This `TAXONOMY` method returns nil if the taxonomy has no terms, so you must code defensively: - -```go-html-template -{{ with .Site.Taxonomies.tags.Page }} - {{ .LinkTitle }} -{{ end }} -``` - -This is rendered to: - -```html -Tags -``` diff --git a/docs/content/en/methods/taxonomy/_index.md b/docs/content/en/methods/taxonomy/_index.md deleted file mode 100644 index 13acdb10c..000000000 --- a/docs/content/en/methods/taxonomy/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Taxonomy methods -linkTitle: Taxonomy -description: Use these methods with Taxonomy objects. -keywords: [] -aliases: [/variables/taxonomy/] ---- diff --git a/docs/content/en/methods/time/Add.md b/docs/content/en/methods/time/Add.md deleted file mode 100644 index e518a1633..000000000 --- a/docs/content/en/methods/time/Add.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -title: Add -description: Returns the given time plus the given duration. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: time.Time - signatures: [TIME.Add DURATION] ---- - -```go-html-template -{{ $t := time.AsTime "2023-01-27T23:44:58-08:00" }} - -{{ $d1 = time.ParseDuration "3h20m10s" }} -{{ $d2 = time.ParseDuration "-3h20m10s" }} - -{{ $t.Add $d1 }} → 2023-01-28 03:05:08 -0800 PST -{{ $t.Add $d2 }} → 2023-01-27 20:24:48 -0800 PST -``` diff --git a/docs/content/en/methods/time/AddDate.md b/docs/content/en/methods/time/AddDate.md deleted file mode 100644 index ffc93c712..000000000 --- a/docs/content/en/methods/time/AddDate.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: AddDate -description: Returns the time corresponding to adding the given number of years, months, and days to the given time.Time value. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: time.Time - signatures: [TIME.AddDate YEARS MONTHS DAYS] -aliases: [/functions/adddate] ---- - -```go-html-template -{{ $d := "2022-01-01" | time.AsTime }} - -{{ $d.AddDate 0 0 1 | time.Format "2006-01-02" }} → 2022-01-02 -{{ $d.AddDate 0 1 1 | time.Format "2006-01-02" }} → 2022-02-02 -{{ $d.AddDate 1 1 1 | time.Format "2006-01-02" }} → 2023-02-02 - -{{ $d.AddDate -1 -1 -1 | time.Format "2006-01-02" }} → 2020-11-30 -``` - -> [!note] -> When adding months or years, Hugo normalizes the final `time.Time` value if the resulting day does not exist. For example, adding one month to 31 January produces 2 March or 3 March, depending on the year. -> -> See [this explanation](https://github.com/golang/go/issues/31145#issuecomment-479067967) from the Go team. - -```go-html-template -{{ $d := "2023-01-31" | time.AsTime }} -{{ $d.AddDate 0 1 0 | time.Format "2006-01-02" }} → 2023-03-03 - -{{ $d := "2024-01-31" | time.AsTime }} -{{ $d.AddDate 0 1 0 | time.Format "2006-01-02" }} → 2024-03-02 - -{{ $d := "2024-02-29" | time.AsTime }} -{{ $d.AddDate 1 0 0 | time.Format "2006-01-02" }} → 2025-03-01 -``` diff --git a/docs/content/en/methods/time/After.md b/docs/content/en/methods/time/After.md deleted file mode 100644 index 1c8d41f64..000000000 --- a/docs/content/en/methods/time/After.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: After -description: Reports whether TIME1 is after TIME2. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: bool - signatures: [TIME1.After TIME2] ---- - -```go-html-template -{{ $t1 := time.AsTime "2023-01-01T17:00:00-08:00" }} -{{ $t2 := time.AsTime "2010-01-01T17:00:00-08:00" }} - -{{ $t1.After $t2 }} → true -``` diff --git a/docs/content/en/methods/time/Before.md b/docs/content/en/methods/time/Before.md deleted file mode 100644 index f6dc3a8e7..000000000 --- a/docs/content/en/methods/time/Before.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: Before -description: Reports whether TIME1 is before TIME2. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: bool - signatures: [TIME1.Before TIME2] ---- - -```go-html-template -{{ $t1 := time.AsTime "2023-01-01T17:00:00-08:00" }} -{{ $t2 := time.AsTime "2030-01-01T17:00:00-08:00" }} - -{{ $t1.Before $t2 }} → true diff --git a/docs/content/en/methods/time/Day.md b/docs/content/en/methods/time/Day.md deleted file mode 100644 index e9e67873c..000000000 --- a/docs/content/en/methods/time/Day.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Day -description: Returns the day of the month of the given time.Time value. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: int - signatures: [TIME.Day] ---- - -```go-html-template -{{ $t := time.AsTime "2023-01-27T23:44:58-08:00" }} -{{ $t.Day }} → 27 -``` diff --git a/docs/content/en/methods/time/Equal.md b/docs/content/en/methods/time/Equal.md deleted file mode 100644 index 6db10423c..000000000 --- a/docs/content/en/methods/time/Equal.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: Equal -description: Reports whether TIME1 is equal to TIME2. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: bool - signatures: [TIME1.Equal TIME2] ---- - -```go-html-template -{{ $t1 := time.AsTime "2023-01-01T17:00:00-08:00" }} -{{ $t2 := time.AsTime "2023-01-01T20:00:00-05:00" }} - -{{ $t1.Equal $t2 }} → true -``` diff --git a/docs/content/en/methods/time/Format.md b/docs/content/en/methods/time/Format.md deleted file mode 100644 index 8a484b74e..000000000 --- a/docs/content/en/methods/time/Format.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -title: Format -description: Returns a textual representation of the time.Time value formatted according to the layout string. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: string - signatures: [TIME.Format LAYOUT] -aliases: [/methods/time/format] ---- - -```go-template -{{ $t := "2023-01-27T23:44:58-08:00" }} -{{ $t = time.AsTime $t }} -{{ $format := "2 Jan 2006" }} - -{{ $t.Format $format }} → 27 Jan 2023 -``` - -> [!note] -> To [localize](g) the return value, use the [`time.Format`] function instead. - -Use the `Format` method with any `time.Time` value, including the four predefined front matter dates: - -```go-html-template -{{ $format := "2 Jan 2006" }} - -{{ .Date.Format $format }} -{{ .PublishDate.Format $format }} -{{ .ExpiryDate.Format $format }} -{{ .Lastmod.Format $format }} -``` - -> [!note] -> Use the [`time.Format`] function to format string representations of dates, and to format raw TOML dates that exclude time and time zone offset. - -## Layout string - -{{% include "/_common/time-layout-string.md" %}} - -## Examples - -Given this front matter: - -{{< code-toggle fm=true >}} -title = "About time" -date = 2023-01-27T23:44:58-08:00 -{{< /code-toggle >}} - -The examples below were rendered in the `America/Los_Angeles` time zone: - -Format string|Result -:--|:-- -`Monday, January 2, 2006`|`Friday, January 27, 2023` -`Mon Jan 2 2006`|`Fri Jan 27 2023` -`January 2006`|`January 2023` -`2006-01-02`|`2023-01-27` -`Monday`|`Friday` -`02 Jan 06 15:04 MST`|`27 Jan 23 23:44 PST` -`Mon, 02 Jan 2006 15:04:05 MST`|`Fri, 27 Jan 2023 23:44:58 PST` -`Mon, 02 Jan 2006 15:04:05 -0700`|`Fri, 27 Jan 2023 23:44:58 -0800` - -## UTC and local time - -Convert and format any `time.Time` value to either Coordinated Universal Time (UTC) or local time. - -```go-html-template -{{ $t := "2023-01-27T23:44:58-08:00" }} -{{ $t = time.AsTime $t }} -{{ $format := "2 Jan 2006 3:04:05 PM MST" }} - -{{ $t.UTC.Format $format }} → 28 Jan 2023 7:44:58 AM UTC -{{ $t.Local.Format $format }} → 27 Jan 2023 11:44:58 PM PST -``` - -## Ordinal representation - -Use the [`humanize`](/functions/inflect/humanize) function to render the day of the month as an ordinal number: - -```go-html-template -{{ $t := "2023-01-27T23:44:58-08:00" }} -{{ $t = time.AsTime $t }} - -{{ humanize $t.Day }} of {{ $t.Format "January 2006" }} → 27th of January 2023 -``` - -[`time.Format`]: /functions/time/format/ diff --git a/docs/content/en/methods/time/Hour.md b/docs/content/en/methods/time/Hour.md deleted file mode 100644 index 28ecf62ac..000000000 --- a/docs/content/en/methods/time/Hour.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Hour -description: Returns the hour within the day of the given time.Time value, in the range [0, 23]. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: int - signatures: [TIME.Hour] ---- - -```go-html-template -{{ $t := time.AsTime "2023-01-27T23:44:58-08:00" }} -{{ $t.Hour }} → 23 -``` diff --git a/docs/content/en/methods/time/IsDST.md b/docs/content/en/methods/time/IsDST.md deleted file mode 100644 index 28177b105..000000000 --- a/docs/content/en/methods/time/IsDST.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: IsDST -description: Reports whether the given time.Time value is in Daylight Savings Time. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: bool - signatures: [TIME.IsDST] ---- - -```go-html-template -{{ $t1 := time.AsTime "2023-01-01T00:00:00-08:00" }} -{{ $t2 := time.AsTime "2023-07-01T00:00:00-07:00" }} - -{{ $t1.IsDST }} → false -{{ $t2.IsDST }} → true -``` diff --git a/docs/content/en/methods/time/IsZero.md b/docs/content/en/methods/time/IsZero.md deleted file mode 100644 index 400172794..000000000 --- a/docs/content/en/methods/time/IsZero.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: IsZero -description: Reports whether the given time.Time value represents the zero time instant, January 1, year 1, 00:00:00 UTC. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: bool - signatures: [TIME.IsZero] ---- - -````go-html-template -{{ $t1 := time.AsTime "2023-01-01T00:00:00-08:00" }} -{{ $t2 := time.AsTime "0001-01-01T00:00:00-00:00" }} - -{{ $t1.IsZero }} → false -{{ $t2.IsZero }} → true -``` diff --git a/docs/content/en/methods/time/Local.md b/docs/content/en/methods/time/Local.md deleted file mode 100644 index 74fe889e0..000000000 --- a/docs/content/en/methods/time/Local.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Local -description: Returns the given time.Time value with the location set to local time. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: time.Time - signatures: [TIME.Local] ---- - -```go-html-template -{{ $t := time.AsTime "2023-01-28T07:44:58+00:00" }} -{{ $t.Local }} → 2023-01-27 23:44:58 -0800 PST -``` diff --git a/docs/content/en/methods/time/Minute.md b/docs/content/en/methods/time/Minute.md deleted file mode 100644 index b53db6d83..000000000 --- a/docs/content/en/methods/time/Minute.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Minute -description: Returns the minute offset within the hour of the given time.Time value, in the range [0, 59]. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: int - signatures: [TIME.Minute] ---- - -```go-html-template -{{ $t := time.AsTime "2023-01-27T23:44:58-08:00" }} -{{ $t.Minute }} → 44 -``` diff --git a/docs/content/en/methods/time/Month.md b/docs/content/en/methods/time/Month.md deleted file mode 100644 index b0ccea9c3..000000000 --- a/docs/content/en/methods/time/Month.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: Month -description: Returns the month of the year of the given time.Time value. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: time.Month - signatures: [TIME.Month] ---- - -To convert the `time.Month` value to a string: - -```go-html-template -{{ $t := time.AsTime "2023-01-27T23:44:58-08:00" }} -{{ $t.Month.String }} → January -``` - -To convert the `time.Month` value to an integer. - -```go-html-template -{{ $t := time.AsTime "2023-01-27T23:44:58-08:00" }} -{{ $t.Month | int }} → 1 -``` diff --git a/docs/content/en/methods/time/Nanosecond.md b/docs/content/en/methods/time/Nanosecond.md deleted file mode 100644 index d895f9622..000000000 --- a/docs/content/en/methods/time/Nanosecond.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Nanosecond -description: Returns the nanosecond offset within the second of the given time.Time value, in the range [0, 999999999]. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: int - signatures: [TIME.Nanosecond] ---- - -```go-html-template -{{ $t := time.AsTime "2023-01-27T23:44:58-08:00" }} -{{ $t.Nanosecond }} → 0 -``` diff --git a/docs/content/en/methods/time/Round.md b/docs/content/en/methods/time/Round.md deleted file mode 100644 index 816d41b44..000000000 --- a/docs/content/en/methods/time/Round.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -title: Round -description: Returns the result of rounding TIME to the nearest multiple of DURATION since January 1, 0001, 00:00:00 UTC. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: time.Time - signatures: [TIME.Round DURATION] ---- - -The rounding behavior for halfway values is to round up. - -The `Round` method operates on TIME as an absolute duration since the [zero time](g); it does not operate on the presentation form of the time. If DURATION is a multiple of one hour, `Round` may return a time with a non-zero minute, depending on the time zone. - -```go-html-template -{{ $t := time.AsTime "2023-01-27T23:44:58-08:00" }} -{{ $d := time.ParseDuration "1h"}} - -{{ ($t.Round $d).Format "2006-01-02T15:04:05-00:00" }} → 2023-01-28T00:00:00-00:00 -``` diff --git a/docs/content/en/methods/time/Second.md b/docs/content/en/methods/time/Second.md deleted file mode 100644 index 3af086fd3..000000000 --- a/docs/content/en/methods/time/Second.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Second -description: Returns the second offset within the minute of the given time.Time value, in the range [0, 59]. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: int - signatures: [TIME.Second] ---- - -```go-html-template -{{ $t := time.AsTime "2023-01-27T23:44:58-08:00" }} -{{ $t.Second }} → 58 -``` diff --git a/docs/content/en/methods/time/Sub.md b/docs/content/en/methods/time/Sub.md deleted file mode 100644 index d48bf3467..000000000 --- a/docs/content/en/methods/time/Sub.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: Sub -description: Returns the duration computed by subtracting TIME2 from TIME1. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: time.Duration - signatures: [TIME1.Sub TIME2] ---- - -```go-html-template -{{ $t1 := time.AsTime "2023-01-27T23:44:58-08:00" }} -{{ $t2 := time.AsTime "2023-01-26T22:34:38-08:00" }} - -{{ $t1.Sub $t2 }} → 25h10m20s -``` diff --git a/docs/content/en/methods/time/Truncate.md b/docs/content/en/methods/time/Truncate.md deleted file mode 100644 index b797afec0..000000000 --- a/docs/content/en/methods/time/Truncate.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: Truncate -description: Returns the result of rounding TIME down to a multiple of DURATION since January 1, 0001, 00:00:00 UTC. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: time.Time - signatures: [TIME.Truncate DURATION] ---- - -The `Truncate` method operates on TIME as an absolute duration since the [zero time](g); it does not operate on the presentation form of the time. If DURATION is a multiple of one hour, `Truncate` may return a time with a non-zero minute, depending on the time zone. - -```go-html-template -{{ $t := time.AsTime "2023-01-27T23:44:58-08:00" }} -{{ $d := time.ParseDuration "1h"}} - -{{ ($t.Truncate $d).Format "2006-01-02T15:04:05-00:00" }} → 2023-01-27T23:00:00-00:00 -``` diff --git a/docs/content/en/methods/time/UTC.md b/docs/content/en/methods/time/UTC.md deleted file mode 100644 index e131a003e..000000000 --- a/docs/content/en/methods/time/UTC.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: UTC -description: Returns the given time.Time value with the location set to UTC. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: time.Time - signatures: [TIME.UTC] ---- - -```go-html-template -{{ $t := time.AsTime "2023-01-27T23:44:58-08:00" }} -{{ $t.UTC }} → 2023-01-28 07:44:58 +0000 UTC diff --git a/docs/content/en/methods/time/Unix.md b/docs/content/en/methods/time/Unix.md deleted file mode 100644 index 73deb524e..000000000 --- a/docs/content/en/methods/time/Unix.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: Unix -description: Returns the given time.Time value expressed as the number of seconds elapsed since January 1, 1970 UTC. -categories: [] -params: - functions_and_methods: - returnType: int64 - signatures: [TIME.Unix] -aliases: [/functions/unix] ---- - -See [Unix epoch](https://en.wikipedia.org/wiki/Unix_time). - -```go-html-template -{{ $t := time.AsTime "2023-01-27T23:44:58-08:00" }} -{{ $t.Unix }} → 1674891898 -``` diff --git a/docs/content/en/methods/time/UnixMicro.md b/docs/content/en/methods/time/UnixMicro.md deleted file mode 100644 index fadb0916c..000000000 --- a/docs/content/en/methods/time/UnixMicro.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: UnixMicro -description: Returns the given time.Time value expressed as the number of microseconds elapsed since January 1, 1970 UTC. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: int64 - signatures: [TIME.UnixMicro] ---- - -See [Unix epoch](https://en.wikipedia.org/wiki/Unix_time). - -```go-html-template -{{ $t := time.AsTime "2023-01-27T23:44:58-08:00" }} -{{ $t.UnixMicro }} → 1674891898000000 -``` diff --git a/docs/content/en/methods/time/UnixMilli.md b/docs/content/en/methods/time/UnixMilli.md deleted file mode 100644 index 9d2261d91..000000000 --- a/docs/content/en/methods/time/UnixMilli.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: UnixMilli -description: Returns the given time.Time value expressed as the number of milliseconds elapsed since January 1, 1970 UTC. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: int64 - signatures: [TIME.UnixMilli] ---- - -See [Unix epoch](https://en.wikipedia.org/wiki/Unix_time). - -```go-html-template -{{ $t := time.AsTime "2023-01-27T23:44:58-08:00" }} -{{ $t.UnixMilli }} → 1674891898000 -``` diff --git a/docs/content/en/methods/time/UnixNano.md b/docs/content/en/methods/time/UnixNano.md deleted file mode 100644 index 4159ddee2..000000000 --- a/docs/content/en/methods/time/UnixNano.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: UnixNano -description: Returns the given time.Time value expressed as the number of nanoseconds elapsed since January 1, 1970 UTC. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: int64 - signatures: [TIME.UnixNano] ---- - -See [Unix epoch](https://en.wikipedia.org/wiki/Unix_time). - -```go-html-template -{{ $t := time.AsTime "2023-01-27T23:44:58-08:00" }} -{{ $t.UnixNano }} → 1674891898000000000 -``` diff --git a/docs/content/en/methods/time/Weekday.md b/docs/content/en/methods/time/Weekday.md deleted file mode 100644 index da939ff87..000000000 --- a/docs/content/en/methods/time/Weekday.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: Weekday -description: Returns the day of the week of the given time.Time value. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: time.Weekday - signatures: [TIME.Weekday] ---- - -To convert the `time.Weekday` value to a string: - -```go-html-template -{{ $t := time.AsTime "2023-01-27T23:44:58-08:00" }} -{{ $t.Weekday.String }} → Friday -``` - -To convert the `time.Weekday` value to an integer. - -```go-html-template -{{ $t := time.AsTime "2023-01-27T23:44:58-08:00" }} -{{ $t.Weekday | int }} → 5 diff --git a/docs/content/en/methods/time/Year.md b/docs/content/en/methods/time/Year.md deleted file mode 100644 index 3f647ea34..000000000 --- a/docs/content/en/methods/time/Year.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Year -description: Returns the year of the given time.Time value. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: int - signatures: [TIME.Year] ---- - -```go-html-template -{{ $t := time.AsTime "2023-01-27T23:44:58-08:00" }} -{{ $t.Year }} → 2023 -``` diff --git a/docs/content/en/methods/time/YearDay.md b/docs/content/en/methods/time/YearDay.md deleted file mode 100644 index a93158b45..000000000 --- a/docs/content/en/methods/time/YearDay.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: YearDay -description: Returns the day of the year of the given time.Time value, in the range [1, 365] for non-leap years, and [1, 366] in leap years. -categories: [] -keywords: [] -params: - functions_and_methods: - returnType: int - signatures: [TIME.YearDay] ---- - -```go-html-template -{{ $t := time.AsTime "2023-01-27T23:44:58-08:00" }} -{{ $t.YearDay }} → 27 -``` diff --git a/docs/content/en/methods/time/_index.md b/docs/content/en/methods/time/_index.md deleted file mode 100644 index 8114664d3..000000000 --- a/docs/content/en/methods/time/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Time methods -linkTitle: Time -description: Use these methods with time.Time values. -categories: [] -keywords: [] ---- diff --git a/docs/content/en/news/_content.gotmpl b/docs/content/en/news/_content.gotmpl deleted file mode 100644 index af3cf47ed..000000000 --- a/docs/content/en/news/_content.gotmpl +++ /dev/null @@ -1,31 +0,0 @@ -{{/* Get releases from GitHub. */}} -{{ $u := "https://api.github.com/repos/gohugoio/hugo/releases" }} -{{ $releases := partial "helpers/funcs/get-remote-data.html" $u }} -{{ $releases = where $releases "draft" false }} -{{ $releases = where $releases "prerelease" false }} - -{{/* Add pages. */}} -{{ range $releases | first 24 }} - {{ $publishDate := .published_at | time.AsTime }} - - {{/* Correct the v0.138.0 release date. See https://github.com/gohugoio/hugo/issues/13066. */}} - {{ if eq .name "v0.138.0" }} - {{ $publishDate = "2024-11-06T11:22:34Z" }} - {{ end }} - - {{ $content := dict "mediaType" "text/markdown" "value" "" }} - {{ $dates := dict "publishDate" (time.AsTime $publishDate) }} - {{ $params := dict "permalink" .html_url }} - {{ $build := dict "render" "never" "list" "local" }} - {{ $page := dict - "build" $build - "content" $content - "dates" $dates - "kind" "page" - "params" $params - "path" (strings.Replace .name "." "-") - "slug" .name - "title" (printf "Release %s" .name) - }} - {{ $.AddPage $page }} -{{ end }} diff --git a/docs/content/en/news/_index.md b/docs/content/en/news/_index.md deleted file mode 100644 index 23c082cb7..000000000 --- a/docs/content/en/news/_index.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: News -description: Stay up-to-date with the latest news and announcements. -outputs: - - html - - rss -weight: 10 -aliases: [/release-notes/] ---- diff --git a/docs/content/en/quick-reference/_index.md b/docs/content/en/quick-reference/_index.md deleted file mode 100644 index 98f978f4f..000000000 --- a/docs/content/en/quick-reference/_index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Quick reference guides -linkTitle: Quick reference -description: Use these quick reference guides for quick access to key information. -categories: [] -keywords: [] -weight: 10 ---- diff --git a/docs/content/en/quick-reference/emojis.md b/docs/content/en/quick-reference/emojis.md deleted file mode 100644 index 15c2e60e6..000000000 --- a/docs/content/en/quick-reference/emojis.md +++ /dev/null @@ -1,1679 +0,0 @@ ---- -title: Emojis -description: Include emoji shortcodes in your Markdown or templates. -categories: [] -keywords: [] ---- - -## Attribution - -This quick reference guide was generated using the [ikatyang/emoji-cheat-sheet] project which reads from the [GitHub Emoji API] and the [Unicode Full Emoji List]. - -Note that GitHub [custom emoji] are not supported. - -[custom emoji]: #github-custom-emoji -[github emoji api]: https://api.github.com/emojis -[ikatyang/emoji-cheat-sheet]: https://github.com/ikatyang/emoji-cheat-sheet/ -[unicode full emoji list]: https://unicode.org/emoji/charts/full-emoji-list.html - -## Usage - -Configure Hugo to enable emoji processing in Markdown: - -{{< code-toggle file=hugo >}} -enableEmoji = true -{{< /code-toggle >}} - -With emoji processing enabled, this Markdown: - -```md -Hello! :wave: -``` - -Is rendered to: - -```html -Hello! 👋 -``` - -And in your browser... Hello! :wave: - -To process an emoji shortcode from within a template, use the [`emojify`] function or pass the string through the [`RenderString`] method on a `Page` object: - -```go-html-template -{{ "Hello! :wave:" | .RenderString }} -``` - -[`emojify`]: /functions/transform/emojify/ -[`RenderString`]: /methods/page/renderstring/ - - - -## Table of Contents - -- [Smileys & Emotion](#smileys--emotion) -- [People & Body](#people--body) -- [Animals & Nature](#animals--nature) -- [Food & Drink](#food--drink) -- [Travel & Places](#travel--places) -- [Activities](#activities) -- [Objects](#objects) -- [Symbols](#symbols) -- [Flags](#flags) -- [GitHub Custom Emoji](#github-custom-emoji) - -## Smileys & Emotion - -- [Face Smiling](#face-smiling) -- [Face Affection](#face-affection) -- [Face Tongue](#face-tongue) -- [Face Hand](#face-hand) -- [Face Neutral Skeptical](#face-neutral-skeptical) -- [Face Sleepy](#face-sleepy) -- [Face Unwell](#face-unwell) -- [Face Hat](#face-hat) -- [Face Glasses](#face-glasses) -- [Face Concerned](#face-concerned) -- [Face Negative](#face-negative) -- [Face Costume](#face-costume) -- [Cat Face](#cat-face) -- [Monkey Face](#monkey-face) -- [Heart](#heart) -- [Emotion](#emotion) - -### Face Smiling - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#smileys--emotion) | :grinning: | `:grinning:` | :smiley: | `:smiley:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :smile: | `:smile:` | :grin: | `:grin:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :laughing: | `:laughing:` `:satisfied:` | :sweat_smile: | `:sweat_smile:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :rofl: | `:rofl:` | :joy: | `:joy:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :slightly_smiling_face: | `:slightly_smiling_face:` | :upside_down_face: | `:upside_down_face:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :melting_face: | `:melting_face:` | :wink: | `:wink:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :blush: | `:blush:` | :innocent: | `:innocent:` | [top](#table-of-contents) | - -### Face Affection - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#smileys--emotion) | :smiling_face_with_three_hearts: | `:smiling_face_with_three_hearts:` | :heart_eyes: | `:heart_eyes:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :star_struck: | `:star_struck:` | :kissing_heart: | `:kissing_heart:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :kissing: | `:kissing:` | :relaxed: | `:relaxed:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :kissing_closed_eyes: | `:kissing_closed_eyes:` | :kissing_smiling_eyes: | `:kissing_smiling_eyes:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :smiling_face_with_tear: | `:smiling_face_with_tear:` | | | [top](#table-of-contents) | - -### Face Tongue - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#smileys--emotion) | :yum: | `:yum:` | :stuck_out_tongue: | `:stuck_out_tongue:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :stuck_out_tongue_winking_eye: | `:stuck_out_tongue_winking_eye:` | :zany_face: | `:zany_face:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :stuck_out_tongue_closed_eyes: | `:stuck_out_tongue_closed_eyes:` | :money_mouth_face: | `:money_mouth_face:` | [top](#table-of-contents) | - -### Face Hand - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#smileys--emotion) | :hugs: | `:hugs:` | :hand_over_mouth: | `:hand_over_mouth:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :face_with_open_eyes_and_hand_over_mouth: | `:face_with_open_eyes_and_hand_over_mouth:` | :face_with_peeking_eye: | `:face_with_peeking_eye:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :shushing_face: | `:shushing_face:` | :thinking: | `:thinking:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :saluting_face: | `:saluting_face:` | | | [top](#table-of-contents) | - -### Face Neutral Skeptical - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#smileys--emotion) | :zipper_mouth_face: | `:zipper_mouth_face:` | :raised_eyebrow: | `:raised_eyebrow:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :neutral_face: | `:neutral_face:` | :expressionless: | `:expressionless:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :no_mouth: | `:no_mouth:` | :dotted_line_face: | `:dotted_line_face:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :face_in_clouds: | `:face_in_clouds:` | :smirk: | `:smirk:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :unamused: | `:unamused:` | :roll_eyes: | `:roll_eyes:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :grimacing: | `:grimacing:` | :face_exhaling: | `:face_exhaling:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :lying_face: | `:lying_face:` | :shaking_face: | `:shaking_face:` | [top](#table-of-contents) | - -### Face Sleepy - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#smileys--emotion) | :relieved: | `:relieved:` | :pensive: | `:pensive:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :sleepy: | `:sleepy:` | :drooling_face: | `:drooling_face:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :sleeping: | `:sleeping:` | | | [top](#table-of-contents) | - -### Face Unwell - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#smileys--emotion) | :mask: | `:mask:` | :face_with_thermometer: | `:face_with_thermometer:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :face_with_head_bandage: | `:face_with_head_bandage:` | :nauseated_face: | `:nauseated_face:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :vomiting_face: | `:vomiting_face:` | :sneezing_face: | `:sneezing_face:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :hot_face: | `:hot_face:` | :cold_face: | `:cold_face:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :woozy_face: | `:woozy_face:` | :dizzy_face: | `:dizzy_face:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :face_with_spiral_eyes: | `:face_with_spiral_eyes:` | :exploding_head: | `:exploding_head:` | [top](#table-of-contents) | - -### Face Hat - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#smileys--emotion) | :cowboy_hat_face: | `:cowboy_hat_face:` | :partying_face: | `:partying_face:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :disguised_face: | `:disguised_face:` | | | [top](#table-of-contents) | - -### Face Glasses - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#smileys--emotion) | :sunglasses: | `:sunglasses:` | :nerd_face: | `:nerd_face:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :monocle_face: | `:monocle_face:` | | | [top](#table-of-contents) | - -### Face Concerned - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#smileys--emotion) | :confused: | `:confused:` | :face_with_diagonal_mouth: | `:face_with_diagonal_mouth:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :worried: | `:worried:` | :slightly_frowning_face: | `:slightly_frowning_face:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :frowning_face: | `:frowning_face:` | :open_mouth: | `:open_mouth:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :hushed: | `:hushed:` | :astonished: | `:astonished:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :flushed: | `:flushed:` | :pleading_face: | `:pleading_face:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :face_holding_back_tears: | `:face_holding_back_tears:` | :frowning: | `:frowning:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :anguished: | `:anguished:` | :fearful: | `:fearful:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :cold_sweat: | `:cold_sweat:` | :disappointed_relieved: | `:disappointed_relieved:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :cry: | `:cry:` | :sob: | `:sob:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :scream: | `:scream:` | :confounded: | `:confounded:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :persevere: | `:persevere:` | :disappointed: | `:disappointed:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :sweat: | `:sweat:` | :weary: | `:weary:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :tired_face: | `:tired_face:` | :yawning_face: | `:yawning_face:` | [top](#table-of-contents) | - -### Face Negative - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#smileys--emotion) | :triumph: | `:triumph:` | :pout: | `:pout:` `:rage:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :angry: | `:angry:` | :cursing_face: | `:cursing_face:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :smiling_imp: | `:smiling_imp:` | :imp: | `:imp:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :skull: | `:skull:` | :skull_and_crossbones: | `:skull_and_crossbones:` | [top](#table-of-contents) | - -### Face Costume - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#smileys--emotion) | :hankey: | `:hankey:` `:poop:` `:shit:` | :clown_face: | `:clown_face:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :japanese_ogre: | `:japanese_ogre:` | :japanese_goblin: | `:japanese_goblin:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :ghost: | `:ghost:` | :alien: | `:alien:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :space_invader: | `:space_invader:` | :robot: | `:robot:` | [top](#table-of-contents) | - -### Cat Face - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#smileys--emotion) | :smiley_cat: | `:smiley_cat:` | :smile_cat: | `:smile_cat:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :joy_cat: | `:joy_cat:` | :heart_eyes_cat: | `:heart_eyes_cat:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :smirk_cat: | `:smirk_cat:` | :kissing_cat: | `:kissing_cat:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :scream_cat: | `:scream_cat:` | :crying_cat_face: | `:crying_cat_face:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :pouting_cat: | `:pouting_cat:` | | | [top](#table-of-contents) | - -### Monkey Face - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#smileys--emotion) | :see_no_evil: | `:see_no_evil:` | :hear_no_evil: | `:hear_no_evil:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :speak_no_evil: | `:speak_no_evil:` | | | [top](#table-of-contents) | - -### Heart - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#smileys--emotion) | :love_letter: | `:love_letter:` | :cupid: | `:cupid:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :gift_heart: | `:gift_heart:` | :sparkling_heart: | `:sparkling_heart:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :heartpulse: | `:heartpulse:` | :heartbeat: | `:heartbeat:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :revolving_hearts: | `:revolving_hearts:` | :two_hearts: | `:two_hearts:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :heart_decoration: | `:heart_decoration:` | :heavy_heart_exclamation: | `:heavy_heart_exclamation:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :broken_heart: | `:broken_heart:` | :heart_on_fire: | `:heart_on_fire:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :mending_heart: | `:mending_heart:` | :heart: | `:heart:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :pink_heart: | `:pink_heart:` | :orange_heart: | `:orange_heart:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :yellow_heart: | `:yellow_heart:` | :green_heart: | `:green_heart:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :blue_heart: | `:blue_heart:` | :light_blue_heart: | `:light_blue_heart:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :purple_heart: | `:purple_heart:` | :brown_heart: | `:brown_heart:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :black_heart: | `:black_heart:` | :grey_heart: | `:grey_heart:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :white_heart: | `:white_heart:` | | | [top](#table-of-contents) | - -### Emotion - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#smileys--emotion) | :kiss: | `:kiss:` | :100: | `:100:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :anger: | `:anger:` | :boom: | `:boom:` `:collision:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :dizzy: | `:dizzy:` | :sweat_drops: | `:sweat_drops:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :dash: | `:dash:` | :hole: | `:hole:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :speech_balloon: | `:speech_balloon:` | :eye_speech_bubble: | `:eye_speech_bubble:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :left_speech_bubble: | `:left_speech_bubble:` | :right_anger_bubble: | `:right_anger_bubble:` | [top](#table-of-contents) | -| [top](#smileys--emotion) | :thought_balloon: | `:thought_balloon:` | :zzz: | `:zzz:` | [top](#table-of-contents) | - -## People & Body - -- [Hand Fingers Open](#hand-fingers-open) -- [Hand Fingers Partial](#hand-fingers-partial) -- [Hand Single Finger](#hand-single-finger) -- [Hand Fingers Closed](#hand-fingers-closed) -- [Hands](#hands) -- [Hand Prop](#hand-prop) -- [Body Parts](#body-parts) -- [Person](#person) -- [Person Gesture](#person-gesture) -- [Person Role](#person-role) -- [Person Fantasy](#person-fantasy) -- [Person Activity](#person-activity) -- [Person Sport](#person-sport) -- [Person Resting](#person-resting) -- [Family](#family) -- [Person Symbol](#person-symbol) - -### Hand Fingers Open - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#people--body) | :wave: | `:wave:` | :raised_back_of_hand: | `:raised_back_of_hand:` | [top](#table-of-contents) | -| [top](#people--body) | :raised_hand_with_fingers_splayed: | `:raised_hand_with_fingers_splayed:` | :hand: | `:hand:` `:raised_hand:` | [top](#table-of-contents) | -| [top](#people--body) | :vulcan_salute: | `:vulcan_salute:` | :rightwards_hand: | `:rightwards_hand:` | [top](#table-of-contents) | -| [top](#people--body) | :leftwards_hand: | `:leftwards_hand:` | :palm_down_hand: | `:palm_down_hand:` | [top](#table-of-contents) | -| [top](#people--body) | :palm_up_hand: | `:palm_up_hand:` | :leftwards_pushing_hand: | `:leftwards_pushing_hand:` | [top](#table-of-contents) | -| [top](#people--body) | :rightwards_pushing_hand: | `:rightwards_pushing_hand:` | | | [top](#table-of-contents) | - -### Hand Fingers Partial - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#people--body) | :ok_hand: | `:ok_hand:` | :pinched_fingers: | `:pinched_fingers:` | [top](#table-of-contents) | -| [top](#people--body) | :pinching_hand: | `:pinching_hand:` | :v: | `:v:` | [top](#table-of-contents) | -| [top](#people--body) | :crossed_fingers: | `:crossed_fingers:` | :hand_with_index_finger_and_thumb_crossed: | `:hand_with_index_finger_and_thumb_crossed:` | [top](#table-of-contents) | -| [top](#people--body) | :love_you_gesture: | `:love_you_gesture:` | :metal: | `:metal:` | [top](#table-of-contents) | -| [top](#people--body) | :call_me_hand: | `:call_me_hand:` | | | [top](#table-of-contents) | - -### Hand Single Finger - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#people--body) | :point_left: | `:point_left:` | :point_right: | `:point_right:` | [top](#table-of-contents) | -| [top](#people--body) | :point_up_2: | `:point_up_2:` | :fu: | `:fu:` `:middle_finger:` | [top](#table-of-contents) | -| [top](#people--body) | :point_down: | `:point_down:` | :point_up: | `:point_up:` | [top](#table-of-contents) | -| [top](#people--body) | :index_pointing_at_the_viewer: | `:index_pointing_at_the_viewer:` | | | [top](#table-of-contents) | - -### Hand Fingers Closed - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#people--body) | :+1: | `:+1:` `:thumbsup:` | :-1: | `:-1:` `:thumbsdown:` | [top](#table-of-contents) | -| [top](#people--body) | :fist: | `:fist:` `:fist_raised:` | :facepunch: | `:facepunch:` `:fist_oncoming:` `:punch:` | [top](#table-of-contents) | -| [top](#people--body) | :fist_left: | `:fist_left:` | :fist_right: | `:fist_right:` | [top](#table-of-contents) | - -### Hands - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#people--body) | :clap: | `:clap:` | :raised_hands: | `:raised_hands:` | [top](#table-of-contents) | -| [top](#people--body) | :heart_hands: | `:heart_hands:` | :open_hands: | `:open_hands:` | [top](#table-of-contents) | -| [top](#people--body) | :palms_up_together: | `:palms_up_together:` | :handshake: | `:handshake:` | [top](#table-of-contents) | -| [top](#people--body) | :pray: | `:pray:` | | | [top](#table-of-contents) | - -### Hand Prop - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#people--body) | :writing_hand: | `:writing_hand:` | :nail_care: | `:nail_care:` | [top](#table-of-contents) | -| [top](#people--body) | :selfie: | `:selfie:` | | | [top](#table-of-contents) | - -### Body Parts - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#people--body) | :muscle: | `:muscle:` | :mechanical_arm: | `:mechanical_arm:` | [top](#table-of-contents) | -| [top](#people--body) | :mechanical_leg: | `:mechanical_leg:` | :leg: | `:leg:` | [top](#table-of-contents) | -| [top](#people--body) | :foot: | `:foot:` | :ear: | `:ear:` | [top](#table-of-contents) | -| [top](#people--body) | :ear_with_hearing_aid: | `:ear_with_hearing_aid:` | :nose: | `:nose:` | [top](#table-of-contents) | -| [top](#people--body) | :brain: | `:brain:` | :anatomical_heart: | `:anatomical_heart:` | [top](#table-of-contents) | -| [top](#people--body) | :lungs: | `:lungs:` | :tooth: | `:tooth:` | [top](#table-of-contents) | -| [top](#people--body) | :bone: | `:bone:` | :eyes: | `:eyes:` | [top](#table-of-contents) | -| [top](#people--body) | :eye: | `:eye:` | :tongue: | `:tongue:` | [top](#table-of-contents) | -| [top](#people--body) | :lips: | `:lips:` | :biting_lip: | `:biting_lip:` | [top](#table-of-contents) | - -### Person - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#people--body) | :baby: | `:baby:` | :child: | `:child:` | [top](#table-of-contents) | -| [top](#people--body) | :boy: | `:boy:` | :girl: | `:girl:` | [top](#table-of-contents) | -| [top](#people--body) | :adult: | `:adult:` | :blond_haired_person: | `:blond_haired_person:` | [top](#table-of-contents) | -| [top](#people--body) | :man: | `:man:` | :bearded_person: | `:bearded_person:` | [top](#table-of-contents) | -| [top](#people--body) | :man_beard: | `:man_beard:` | :woman_beard: | `:woman_beard:` | [top](#table-of-contents) | -| [top](#people--body) | :red_haired_man: | `:red_haired_man:` | :curly_haired_man: | `:curly_haired_man:` | [top](#table-of-contents) | -| [top](#people--body) | :white_haired_man: | `:white_haired_man:` | :bald_man: | `:bald_man:` | [top](#table-of-contents) | -| [top](#people--body) | :woman: | `:woman:` | :red_haired_woman: | `:red_haired_woman:` | [top](#table-of-contents) | -| [top](#people--body) | :person_red_hair: | `:person_red_hair:` | :curly_haired_woman: | `:curly_haired_woman:` | [top](#table-of-contents) | -| [top](#people--body) | :person_curly_hair: | `:person_curly_hair:` | :white_haired_woman: | `:white_haired_woman:` | [top](#table-of-contents) | -| [top](#people--body) | :person_white_hair: | `:person_white_hair:` | :bald_woman: | `:bald_woman:` | [top](#table-of-contents) | -| [top](#people--body) | :person_bald: | `:person_bald:` | :blond_haired_woman: | `:blond_haired_woman:` `:blonde_woman:` | [top](#table-of-contents) | -| [top](#people--body) | :blond_haired_man: | `:blond_haired_man:` | :older_adult: | `:older_adult:` | [top](#table-of-contents) | -| [top](#people--body) | :older_man: | `:older_man:` | :older_woman: | `:older_woman:` | [top](#table-of-contents) | - -### Person Gesture - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#people--body) | :frowning_person: | `:frowning_person:` | :frowning_man: | `:frowning_man:` | [top](#table-of-contents) | -| [top](#people--body) | :frowning_woman: | `:frowning_woman:` | :pouting_face: | `:pouting_face:` | [top](#table-of-contents) | -| [top](#people--body) | :pouting_man: | `:pouting_man:` | :pouting_woman: | `:pouting_woman:` | [top](#table-of-contents) | -| [top](#people--body) | :no_good: | `:no_good:` | :ng_man: | `:ng_man:` `:no_good_man:` | [top](#table-of-contents) | -| [top](#people--body) | :ng_woman: | `:ng_woman:` `:no_good_woman:` | :ok_person: | `:ok_person:` | [top](#table-of-contents) | -| [top](#people--body) | :ok_man: | `:ok_man:` | :ok_woman: | `:ok_woman:` | [top](#table-of-contents) | -| [top](#people--body) | :information_desk_person: | `:information_desk_person:` `:tipping_hand_person:` | :sassy_man: | `:sassy_man:` `:tipping_hand_man:` | [top](#table-of-contents) | -| [top](#people--body) | :sassy_woman: | `:sassy_woman:` `:tipping_hand_woman:` | :raising_hand: | `:raising_hand:` | [top](#table-of-contents) | -| [top](#people--body) | :raising_hand_man: | `:raising_hand_man:` | :raising_hand_woman: | `:raising_hand_woman:` | [top](#table-of-contents) | -| [top](#people--body) | :deaf_person: | `:deaf_person:` | :deaf_man: | `:deaf_man:` | [top](#table-of-contents) | -| [top](#people--body) | :deaf_woman: | `:deaf_woman:` | :bow: | `:bow:` | [top](#table-of-contents) | -| [top](#people--body) | :bowing_man: | `:bowing_man:` | :bowing_woman: | `:bowing_woman:` | [top](#table-of-contents) | -| [top](#people--body) | :facepalm: | `:facepalm:` | :man_facepalming: | `:man_facepalming:` | [top](#table-of-contents) | -| [top](#people--body) | :woman_facepalming: | `:woman_facepalming:` | :shrug: | `:shrug:` | [top](#table-of-contents) | -| [top](#people--body) | :man_shrugging: | `:man_shrugging:` | :woman_shrugging: | `:woman_shrugging:` | [top](#table-of-contents) | - -### Person Role - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#people--body) | :health_worker: | `:health_worker:` | :man_health_worker: | `:man_health_worker:` | [top](#table-of-contents) | -| [top](#people--body) | :woman_health_worker: | `:woman_health_worker:` | :student: | `:student:` | [top](#table-of-contents) | -| [top](#people--body) | :man_student: | `:man_student:` | :woman_student: | `:woman_student:` | [top](#table-of-contents) | -| [top](#people--body) | :teacher: | `:teacher:` | :man_teacher: | `:man_teacher:` | [top](#table-of-contents) | -| [top](#people--body) | :woman_teacher: | `:woman_teacher:` | :judge: | `:judge:` | [top](#table-of-contents) | -| [top](#people--body) | :man_judge: | `:man_judge:` | :woman_judge: | `:woman_judge:` | [top](#table-of-contents) | -| [top](#people--body) | :farmer: | `:farmer:` | :man_farmer: | `:man_farmer:` | [top](#table-of-contents) | -| [top](#people--body) | :woman_farmer: | `:woman_farmer:` | :cook: | `:cook:` | [top](#table-of-contents) | -| [top](#people--body) | :man_cook: | `:man_cook:` | :woman_cook: | `:woman_cook:` | [top](#table-of-contents) | -| [top](#people--body) | :mechanic: | `:mechanic:` | :man_mechanic: | `:man_mechanic:` | [top](#table-of-contents) | -| [top](#people--body) | :woman_mechanic: | `:woman_mechanic:` | :factory_worker: | `:factory_worker:` | [top](#table-of-contents) | -| [top](#people--body) | :man_factory_worker: | `:man_factory_worker:` | :woman_factory_worker: | `:woman_factory_worker:` | [top](#table-of-contents) | -| [top](#people--body) | :office_worker: | `:office_worker:` | :man_office_worker: | `:man_office_worker:` | [top](#table-of-contents) | -| [top](#people--body) | :woman_office_worker: | `:woman_office_worker:` | :scientist: | `:scientist:` | [top](#table-of-contents) | -| [top](#people--body) | :man_scientist: | `:man_scientist:` | :woman_scientist: | `:woman_scientist:` | [top](#table-of-contents) | -| [top](#people--body) | :technologist: | `:technologist:` | :man_technologist: | `:man_technologist:` | [top](#table-of-contents) | -| [top](#people--body) | :woman_technologist: | `:woman_technologist:` | :singer: | `:singer:` | [top](#table-of-contents) | -| [top](#people--body) | :man_singer: | `:man_singer:` | :woman_singer: | `:woman_singer:` | [top](#table-of-contents) | -| [top](#people--body) | :artist: | `:artist:` | :man_artist: | `:man_artist:` | [top](#table-of-contents) | -| [top](#people--body) | :woman_artist: | `:woman_artist:` | :pilot: | `:pilot:` | [top](#table-of-contents) | -| [top](#people--body) | :man_pilot: | `:man_pilot:` | :woman_pilot: | `:woman_pilot:` | [top](#table-of-contents) | -| [top](#people--body) | :astronaut: | `:astronaut:` | :man_astronaut: | `:man_astronaut:` | [top](#table-of-contents) | -| [top](#people--body) | :woman_astronaut: | `:woman_astronaut:` | :firefighter: | `:firefighter:` | [top](#table-of-contents) | -| [top](#people--body) | :man_firefighter: | `:man_firefighter:` | :woman_firefighter: | `:woman_firefighter:` | [top](#table-of-contents) | -| [top](#people--body) | :cop: | `:cop:` `:police_officer:` | :policeman: | `:policeman:` | [top](#table-of-contents) | -| [top](#people--body) | :policewoman: | `:policewoman:` | :detective: | `:detective:` | [top](#table-of-contents) | -| [top](#people--body) | :male_detective: | `:male_detective:` | :female_detective: | `:female_detective:` | [top](#table-of-contents) | -| [top](#people--body) | :guard: | `:guard:` | :guardsman: | `:guardsman:` | [top](#table-of-contents) | -| [top](#people--body) | :guardswoman: | `:guardswoman:` | :ninja: | `:ninja:` | [top](#table-of-contents) | -| [top](#people--body) | :construction_worker: | `:construction_worker:` | :construction_worker_man: | `:construction_worker_man:` | [top](#table-of-contents) | -| [top](#people--body) | :construction_worker_woman: | `:construction_worker_woman:` | :person_with_crown: | `:person_with_crown:` | [top](#table-of-contents) | -| [top](#people--body) | :prince: | `:prince:` | :princess: | `:princess:` | [top](#table-of-contents) | -| [top](#people--body) | :person_with_turban: | `:person_with_turban:` | :man_with_turban: | `:man_with_turban:` | [top](#table-of-contents) | -| [top](#people--body) | :woman_with_turban: | `:woman_with_turban:` | :man_with_gua_pi_mao: | `:man_with_gua_pi_mao:` | [top](#table-of-contents) | -| [top](#people--body) | :woman_with_headscarf: | `:woman_with_headscarf:` | :person_in_tuxedo: | `:person_in_tuxedo:` | [top](#table-of-contents) | -| [top](#people--body) | :man_in_tuxedo: | `:man_in_tuxedo:` | :woman_in_tuxedo: | `:woman_in_tuxedo:` | [top](#table-of-contents) | -| [top](#people--body) | :person_with_veil: | `:person_with_veil:` | :man_with_veil: | `:man_with_veil:` | [top](#table-of-contents) | -| [top](#people--body) | :bride_with_veil: | `:bride_with_veil:` `:woman_with_veil:` | :pregnant_woman: | `:pregnant_woman:` | [top](#table-of-contents) | -| [top](#people--body) | :pregnant_man: | `:pregnant_man:` | :pregnant_person: | `:pregnant_person:` | [top](#table-of-contents) | -| [top](#people--body) | :breast_feeding: | `:breast_feeding:` | :woman_feeding_baby: | `:woman_feeding_baby:` | [top](#table-of-contents) | -| [top](#people--body) | :man_feeding_baby: | `:man_feeding_baby:` | :person_feeding_baby: | `:person_feeding_baby:` | [top](#table-of-contents) | - -### Person Fantasy - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#people--body) | :angel: | `:angel:` | :santa: | `:santa:` | [top](#table-of-contents) | -| [top](#people--body) | :mrs_claus: | `:mrs_claus:` | :mx_claus: | `:mx_claus:` | [top](#table-of-contents) | -| [top](#people--body) | :superhero: | `:superhero:` | :superhero_man: | `:superhero_man:` | [top](#table-of-contents) | -| [top](#people--body) | :superhero_woman: | `:superhero_woman:` | :supervillain: | `:supervillain:` | [top](#table-of-contents) | -| [top](#people--body) | :supervillain_man: | `:supervillain_man:` | :supervillain_woman: | `:supervillain_woman:` | [top](#table-of-contents) | -| [top](#people--body) | :mage: | `:mage:` | :mage_man: | `:mage_man:` | [top](#table-of-contents) | -| [top](#people--body) | :mage_woman: | `:mage_woman:` | :fairy: | `:fairy:` | [top](#table-of-contents) | -| [top](#people--body) | :fairy_man: | `:fairy_man:` | :fairy_woman: | `:fairy_woman:` | [top](#table-of-contents) | -| [top](#people--body) | :vampire: | `:vampire:` | :vampire_man: | `:vampire_man:` | [top](#table-of-contents) | -| [top](#people--body) | :vampire_woman: | `:vampire_woman:` | :merperson: | `:merperson:` | [top](#table-of-contents) | -| [top](#people--body) | :merman: | `:merman:` | :mermaid: | `:mermaid:` | [top](#table-of-contents) | -| [top](#people--body) | :elf: | `:elf:` | :elf_man: | `:elf_man:` | [top](#table-of-contents) | -| [top](#people--body) | :elf_woman: | `:elf_woman:` | :genie: | `:genie:` | [top](#table-of-contents) | -| [top](#people--body) | :genie_man: | `:genie_man:` | :genie_woman: | `:genie_woman:` | [top](#table-of-contents) | -| [top](#people--body) | :zombie: | `:zombie:` | :zombie_man: | `:zombie_man:` | [top](#table-of-contents) | -| [top](#people--body) | :zombie_woman: | `:zombie_woman:` | :troll: | `:troll:` | [top](#table-of-contents) | - -### Person Activity - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#people--body) | :massage: | `:massage:` | :massage_man: | `:massage_man:` | [top](#table-of-contents) | -| [top](#people--body) | :massage_woman: | `:massage_woman:` | :haircut: | `:haircut:` | [top](#table-of-contents) | -| [top](#people--body) | :haircut_man: | `:haircut_man:` | :haircut_woman: | `:haircut_woman:` | [top](#table-of-contents) | -| [top](#people--body) | :walking: | `:walking:` | :walking_man: | `:walking_man:` | [top](#table-of-contents) | -| [top](#people--body) | :walking_woman: | `:walking_woman:` | :standing_person: | `:standing_person:` | [top](#table-of-contents) | -| [top](#people--body) | :standing_man: | `:standing_man:` | :standing_woman: | `:standing_woman:` | [top](#table-of-contents) | -| [top](#people--body) | :kneeling_person: | `:kneeling_person:` | :kneeling_man: | `:kneeling_man:` | [top](#table-of-contents) | -| [top](#people--body) | :kneeling_woman: | `:kneeling_woman:` | :person_with_probing_cane: | `:person_with_probing_cane:` | [top](#table-of-contents) | -| [top](#people--body) | :man_with_probing_cane: | `:man_with_probing_cane:` | :woman_with_probing_cane: | `:woman_with_probing_cane:` | [top](#table-of-contents) | -| [top](#people--body) | :person_in_motorized_wheelchair: | `:person_in_motorized_wheelchair:` | :man_in_motorized_wheelchair: | `:man_in_motorized_wheelchair:` | [top](#table-of-contents) | -| [top](#people--body) | :woman_in_motorized_wheelchair: | `:woman_in_motorized_wheelchair:` | :person_in_manual_wheelchair: | `:person_in_manual_wheelchair:` | [top](#table-of-contents) | -| [top](#people--body) | :man_in_manual_wheelchair: | `:man_in_manual_wheelchair:` | :woman_in_manual_wheelchair: | `:woman_in_manual_wheelchair:` | [top](#table-of-contents) | -| [top](#people--body) | :runner: | `:runner:` `:running:` | :running_man: | `:running_man:` | [top](#table-of-contents) | -| [top](#people--body) | :running_woman: | `:running_woman:` | :dancer: | `:dancer:` `:woman_dancing:` | [top](#table-of-contents) | -| [top](#people--body) | :man_dancing: | `:man_dancing:` | :business_suit_levitating: | `:business_suit_levitating:` | [top](#table-of-contents) | -| [top](#people--body) | :dancers: | `:dancers:` | :dancing_men: | `:dancing_men:` | [top](#table-of-contents) | -| [top](#people--body) | :dancing_women: | `:dancing_women:` | :sauna_person: | `:sauna_person:` | [top](#table-of-contents) | -| [top](#people--body) | :sauna_man: | `:sauna_man:` | :sauna_woman: | `:sauna_woman:` | [top](#table-of-contents) | -| [top](#people--body) | :climbing: | `:climbing:` | :climbing_man: | `:climbing_man:` | [top](#table-of-contents) | -| [top](#people--body) | :climbing_woman: | `:climbing_woman:` | | | [top](#table-of-contents) | - -### Person Sport - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#people--body) | :person_fencing: | `:person_fencing:` | :horse_racing: | `:horse_racing:` | [top](#table-of-contents) | -| [top](#people--body) | :skier: | `:skier:` | :snowboarder: | `:snowboarder:` | [top](#table-of-contents) | -| [top](#people--body) | :golfing: | `:golfing:` | :golfing_man: | `:golfing_man:` | [top](#table-of-contents) | -| [top](#people--body) | :golfing_woman: | `:golfing_woman:` | :surfer: | `:surfer:` | [top](#table-of-contents) | -| [top](#people--body) | :surfing_man: | `:surfing_man:` | :surfing_woman: | `:surfing_woman:` | [top](#table-of-contents) | -| [top](#people--body) | :rowboat: | `:rowboat:` | :rowing_man: | `:rowing_man:` | [top](#table-of-contents) | -| [top](#people--body) | :rowing_woman: | `:rowing_woman:` | :swimmer: | `:swimmer:` | [top](#table-of-contents) | -| [top](#people--body) | :swimming_man: | `:swimming_man:` | :swimming_woman: | `:swimming_woman:` | [top](#table-of-contents) | -| [top](#people--body) | :bouncing_ball_person: | `:bouncing_ball_person:` | :basketball_man: | `:basketball_man:` `:bouncing_ball_man:` | [top](#table-of-contents) | -| [top](#people--body) | :basketball_woman: | `:basketball_woman:` `:bouncing_ball_woman:` | :weight_lifting: | `:weight_lifting:` | [top](#table-of-contents) | -| [top](#people--body) | :weight_lifting_man: | `:weight_lifting_man:` | :weight_lifting_woman: | `:weight_lifting_woman:` | [top](#table-of-contents) | -| [top](#people--body) | :bicyclist: | `:bicyclist:` | :biking_man: | `:biking_man:` | [top](#table-of-contents) | -| [top](#people--body) | :biking_woman: | `:biking_woman:` | :mountain_bicyclist: | `:mountain_bicyclist:` | [top](#table-of-contents) | -| [top](#people--body) | :mountain_biking_man: | `:mountain_biking_man:` | :mountain_biking_woman: | `:mountain_biking_woman:` | [top](#table-of-contents) | -| [top](#people--body) | :cartwheeling: | `:cartwheeling:` | :man_cartwheeling: | `:man_cartwheeling:` | [top](#table-of-contents) | -| [top](#people--body) | :woman_cartwheeling: | `:woman_cartwheeling:` | :wrestling: | `:wrestling:` | [top](#table-of-contents) | -| [top](#people--body) | :men_wrestling: | `:men_wrestling:` | :women_wrestling: | `:women_wrestling:` | [top](#table-of-contents) | -| [top](#people--body) | :water_polo: | `:water_polo:` | :man_playing_water_polo: | `:man_playing_water_polo:` | [top](#table-of-contents) | -| [top](#people--body) | :woman_playing_water_polo: | `:woman_playing_water_polo:` | :handball_person: | `:handball_person:` | [top](#table-of-contents) | -| [top](#people--body) | :man_playing_handball: | `:man_playing_handball:` | :woman_playing_handball: | `:woman_playing_handball:` | [top](#table-of-contents) | -| [top](#people--body) | :juggling_person: | `:juggling_person:` | :man_juggling: | `:man_juggling:` | [top](#table-of-contents) | -| [top](#people--body) | :woman_juggling: | `:woman_juggling:` | | | [top](#table-of-contents) | - -### Person Resting - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#people--body) | :lotus_position: | `:lotus_position:` | :lotus_position_man: | `:lotus_position_man:` | [top](#table-of-contents) | -| [top](#people--body) | :lotus_position_woman: | `:lotus_position_woman:` | :bath: | `:bath:` | [top](#table-of-contents) | -| [top](#people--body) | :sleeping_bed: | `:sleeping_bed:` | | | [top](#table-of-contents) | - -### Family - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#people--body) | :people_holding_hands: | `:people_holding_hands:` | :two_women_holding_hands: | `:two_women_holding_hands:` | [top](#table-of-contents) | -| [top](#people--body) | :couple: | `:couple:` | :two_men_holding_hands: | `:two_men_holding_hands:` | [top](#table-of-contents) | -| [top](#people--body) | :couplekiss: | `:couplekiss:` | :couplekiss_man_woman: | `:couplekiss_man_woman:` | [top](#table-of-contents) | -| [top](#people--body) | :couplekiss_man_man: | `:couplekiss_man_man:` | :couplekiss_woman_woman: | `:couplekiss_woman_woman:` | [top](#table-of-contents) | -| [top](#people--body) | :couple_with_heart: | `:couple_with_heart:` | :couple_with_heart_woman_man: | `:couple_with_heart_woman_man:` | [top](#table-of-contents) | -| [top](#people--body) | :couple_with_heart_man_man: | `:couple_with_heart_man_man:` | :couple_with_heart_woman_woman: | `:couple_with_heart_woman_woman:` | [top](#table-of-contents) | -| [top](#people--body) | :family_man_woman_boy: | `:family_man_woman_boy:` | :family_man_woman_girl: | `:family_man_woman_girl:` | [top](#table-of-contents) | -| [top](#people--body) | :family_man_woman_girl_boy: | `:family_man_woman_girl_boy:` | :family_man_woman_boy_boy: | `:family_man_woman_boy_boy:` | [top](#table-of-contents) | -| [top](#people--body) | :family_man_woman_girl_girl: | `:family_man_woman_girl_girl:` | :family_man_man_boy: | `:family_man_man_boy:` | [top](#table-of-contents) | -| [top](#people--body) | :family_man_man_girl: | `:family_man_man_girl:` | :family_man_man_girl_boy: | `:family_man_man_girl_boy:` | [top](#table-of-contents) | -| [top](#people--body) | :family_man_man_boy_boy: | `:family_man_man_boy_boy:` | :family_man_man_girl_girl: | `:family_man_man_girl_girl:` | [top](#table-of-contents) | -| [top](#people--body) | :family_woman_woman_boy: | `:family_woman_woman_boy:` | :family_woman_woman_girl: | `:family_woman_woman_girl:` | [top](#table-of-contents) | -| [top](#people--body) | :family_woman_woman_girl_boy: | `:family_woman_woman_girl_boy:` | :family_woman_woman_boy_boy: | `:family_woman_woman_boy_boy:` | [top](#table-of-contents) | -| [top](#people--body) | :family_woman_woman_girl_girl: | `:family_woman_woman_girl_girl:` | :family_man_boy: | `:family_man_boy:` | [top](#table-of-contents) | -| [top](#people--body) | :family_man_boy_boy: | `:family_man_boy_boy:` | :family_man_girl: | `:family_man_girl:` | [top](#table-of-contents) | -| [top](#people--body) | :family_man_girl_boy: | `:family_man_girl_boy:` | :family_man_girl_girl: | `:family_man_girl_girl:` | [top](#table-of-contents) | -| [top](#people--body) | :family_woman_boy: | `:family_woman_boy:` | :family_woman_boy_boy: | `:family_woman_boy_boy:` | [top](#table-of-contents) | -| [top](#people--body) | :family_woman_girl: | `:family_woman_girl:` | :family_woman_girl_boy: | `:family_woman_girl_boy:` | [top](#table-of-contents) | -| [top](#people--body) | :family_woman_girl_girl: | `:family_woman_girl_girl:` | | | [top](#table-of-contents) | - -### Person Symbol - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#people--body) | :speaking_head: | `:speaking_head:` | :bust_in_silhouette: | `:bust_in_silhouette:` | [top](#table-of-contents) | -| [top](#people--body) | :busts_in_silhouette: | `:busts_in_silhouette:` | :people_hugging: | `:people_hugging:` | [top](#table-of-contents) | -| [top](#people--body) | :family: | `:family:` | :footprints: | `:footprints:` | [top](#table-of-contents) | - -## Animals & Nature - -- [Animal Mammal](#animal-mammal) -- [Animal Bird](#animal-bird) -- [Animal Amphibian](#animal-amphibian) -- [Animal Reptile](#animal-reptile) -- [Animal Marine](#animal-marine) -- [Animal Bug](#animal-bug) -- [Plant Flower](#plant-flower) -- [Plant Other](#plant-other) - -### Animal Mammal - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#animals--nature) | :monkey_face: | `:monkey_face:` | :monkey: | `:monkey:` | [top](#table-of-contents) | -| [top](#animals--nature) | :gorilla: | `:gorilla:` | :orangutan: | `:orangutan:` | [top](#table-of-contents) | -| [top](#animals--nature) | :dog: | `:dog:` | :dog2: | `:dog2:` | [top](#table-of-contents) | -| [top](#animals--nature) | :guide_dog: | `:guide_dog:` | :service_dog: | `:service_dog:` | [top](#table-of-contents) | -| [top](#animals--nature) | :poodle: | `:poodle:` | :wolf: | `:wolf:` | [top](#table-of-contents) | -| [top](#animals--nature) | :fox_face: | `:fox_face:` | :raccoon: | `:raccoon:` | [top](#table-of-contents) | -| [top](#animals--nature) | :cat: | `:cat:` | :cat2: | `:cat2:` | [top](#table-of-contents) | -| [top](#animals--nature) | :black_cat: | `:black_cat:` | :lion: | `:lion:` | [top](#table-of-contents) | -| [top](#animals--nature) | :tiger: | `:tiger:` | :tiger2: | `:tiger2:` | [top](#table-of-contents) | -| [top](#animals--nature) | :leopard: | `:leopard:` | :horse: | `:horse:` | [top](#table-of-contents) | -| [top](#animals--nature) | :moose: | `:moose:` | :donkey: | `:donkey:` | [top](#table-of-contents) | -| [top](#animals--nature) | :racehorse: | `:racehorse:` | :unicorn: | `:unicorn:` | [top](#table-of-contents) | -| [top](#animals--nature) | :zebra: | `:zebra:` | :deer: | `:deer:` | [top](#table-of-contents) | -| [top](#animals--nature) | :bison: | `:bison:` | :cow: | `:cow:` | [top](#table-of-contents) | -| [top](#animals--nature) | :ox: | `:ox:` | :water_buffalo: | `:water_buffalo:` | [top](#table-of-contents) | -| [top](#animals--nature) | :cow2: | `:cow2:` | :pig: | `:pig:` | [top](#table-of-contents) | -| [top](#animals--nature) | :pig2: | `:pig2:` | :boar: | `:boar:` | [top](#table-of-contents) | -| [top](#animals--nature) | :pig_nose: | `:pig_nose:` | :ram: | `:ram:` | [top](#table-of-contents) | -| [top](#animals--nature) | :sheep: | `:sheep:` | :goat: | `:goat:` | [top](#table-of-contents) | -| [top](#animals--nature) | :dromedary_camel: | `:dromedary_camel:` | :camel: | `:camel:` | [top](#table-of-contents) | -| [top](#animals--nature) | :llama: | `:llama:` | :giraffe: | `:giraffe:` | [top](#table-of-contents) | -| [top](#animals--nature) | :elephant: | `:elephant:` | :mammoth: | `:mammoth:` | [top](#table-of-contents) | -| [top](#animals--nature) | :rhinoceros: | `:rhinoceros:` | :hippopotamus: | `:hippopotamus:` | [top](#table-of-contents) | -| [top](#animals--nature) | :mouse: | `:mouse:` | :mouse2: | `:mouse2:` | [top](#table-of-contents) | -| [top](#animals--nature) | :rat: | `:rat:` | :hamster: | `:hamster:` | [top](#table-of-contents) | -| [top](#animals--nature) | :rabbit: | `:rabbit:` | :rabbit2: | `:rabbit2:` | [top](#table-of-contents) | -| [top](#animals--nature) | :chipmunk: | `:chipmunk:` | :beaver: | `:beaver:` | [top](#table-of-contents) | -| [top](#animals--nature) | :hedgehog: | `:hedgehog:` | :bat: | `:bat:` | [top](#table-of-contents) | -| [top](#animals--nature) | :bear: | `:bear:` | :polar_bear: | `:polar_bear:` | [top](#table-of-contents) | -| [top](#animals--nature) | :koala: | `:koala:` | :panda_face: | `:panda_face:` | [top](#table-of-contents) | -| [top](#animals--nature) | :sloth: | `:sloth:` | :otter: | `:otter:` | [top](#table-of-contents) | -| [top](#animals--nature) | :skunk: | `:skunk:` | :kangaroo: | `:kangaroo:` | [top](#table-of-contents) | -| [top](#animals--nature) | :badger: | `:badger:` | :feet: | `:feet:` `:paw_prints:` | [top](#table-of-contents) | - -### Animal Bird - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#animals--nature) | :turkey: | `:turkey:` | :chicken: | `:chicken:` | [top](#table-of-contents) | -| [top](#animals--nature) | :rooster: | `:rooster:` | :hatching_chick: | `:hatching_chick:` | [top](#table-of-contents) | -| [top](#animals--nature) | :baby_chick: | `:baby_chick:` | :hatched_chick: | `:hatched_chick:` | [top](#table-of-contents) | -| [top](#animals--nature) | :bird: | `:bird:` | :penguin: | `:penguin:` | [top](#table-of-contents) | -| [top](#animals--nature) | :dove: | `:dove:` | :eagle: | `:eagle:` | [top](#table-of-contents) | -| [top](#animals--nature) | :duck: | `:duck:` | :swan: | `:swan:` | [top](#table-of-contents) | -| [top](#animals--nature) | :owl: | `:owl:` | :dodo: | `:dodo:` | [top](#table-of-contents) | -| [top](#animals--nature) | :feather: | `:feather:` | :flamingo: | `:flamingo:` | [top](#table-of-contents) | -| [top](#animals--nature) | :peacock: | `:peacock:` | :parrot: | `:parrot:` | [top](#table-of-contents) | -| [top](#animals--nature) | :wing: | `:wing:` | :black_bird: | `:black_bird:` | [top](#table-of-contents) | -| [top](#animals--nature) | :goose: | `:goose:` | | | [top](#table-of-contents) | - -### Animal Amphibian - -| | ico | shortcode | | -| - | :-: | - | - | -| [top](#animals--nature) | :frog: | `:frog:` | [top](#table-of-contents) | - -### Animal Reptile - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#animals--nature) | :crocodile: | `:crocodile:` | :turtle: | `:turtle:` | [top](#table-of-contents) | -| [top](#animals--nature) | :lizard: | `:lizard:` | :snake: | `:snake:` | [top](#table-of-contents) | -| [top](#animals--nature) | :dragon_face: | `:dragon_face:` | :dragon: | `:dragon:` | [top](#table-of-contents) | -| [top](#animals--nature) | :sauropod: | `:sauropod:` | :t-rex: | `:t-rex:` | [top](#table-of-contents) | - -### Animal Marine - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#animals--nature) | :whale: | `:whale:` | :whale2: | `:whale2:` | [top](#table-of-contents) | -| [top](#animals--nature) | :dolphin: | `:dolphin:` `:flipper:` | :seal: | `:seal:` | [top](#table-of-contents) | -| [top](#animals--nature) | :fish: | `:fish:` | :tropical_fish: | `:tropical_fish:` | [top](#table-of-contents) | -| [top](#animals--nature) | :blowfish: | `:blowfish:` | :shark: | `:shark:` | [top](#table-of-contents) | -| [top](#animals--nature) | :octopus: | `:octopus:` | :shell: | `:shell:` | [top](#table-of-contents) | -| [top](#animals--nature) | :coral: | `:coral:` | :jellyfish: | `:jellyfish:` | [top](#table-of-contents) | - -### Animal Bug - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#animals--nature) | :snail: | `:snail:` | :butterfly: | `:butterfly:` | [top](#table-of-contents) | -| [top](#animals--nature) | :bug: | `:bug:` | :ant: | `:ant:` | [top](#table-of-contents) | -| [top](#animals--nature) | :bee: | `:bee:` `:honeybee:` | :beetle: | `:beetle:` | [top](#table-of-contents) | -| [top](#animals--nature) | :lady_beetle: | `:lady_beetle:` | :cricket: | `:cricket:` | [top](#table-of-contents) | -| [top](#animals--nature) | :cockroach: | `:cockroach:` | :spider: | `:spider:` | [top](#table-of-contents) | -| [top](#animals--nature) | :spider_web: | `:spider_web:` | :scorpion: | `:scorpion:` | [top](#table-of-contents) | -| [top](#animals--nature) | :mosquito: | `:mosquito:` | :fly: | `:fly:` | [top](#table-of-contents) | -| [top](#animals--nature) | :worm: | `:worm:` | :microbe: | `:microbe:` | [top](#table-of-contents) | - -### Plant Flower - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#animals--nature) | :bouquet: | `:bouquet:` | :cherry_blossom: | `:cherry_blossom:` | [top](#table-of-contents) | -| [top](#animals--nature) | :white_flower: | `:white_flower:` | :lotus: | `:lotus:` | [top](#table-of-contents) | -| [top](#animals--nature) | :rosette: | `:rosette:` | :rose: | `:rose:` | [top](#table-of-contents) | -| [top](#animals--nature) | :wilted_flower: | `:wilted_flower:` | :hibiscus: | `:hibiscus:` | [top](#table-of-contents) | -| [top](#animals--nature) | :sunflower: | `:sunflower:` | :blossom: | `:blossom:` | [top](#table-of-contents) | -| [top](#animals--nature) | :tulip: | `:tulip:` | :hyacinth: | `:hyacinth:` | [top](#table-of-contents) | - -### Plant Other - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#animals--nature) | :seedling: | `:seedling:` | :potted_plant: | `:potted_plant:` | [top](#table-of-contents) | -| [top](#animals--nature) | :evergreen_tree: | `:evergreen_tree:` | :deciduous_tree: | `:deciduous_tree:` | [top](#table-of-contents) | -| [top](#animals--nature) | :palm_tree: | `:palm_tree:` | :cactus: | `:cactus:` | [top](#table-of-contents) | -| [top](#animals--nature) | :ear_of_rice: | `:ear_of_rice:` | :herb: | `:herb:` | [top](#table-of-contents) | -| [top](#animals--nature) | :shamrock: | `:shamrock:` | :four_leaf_clover: | `:four_leaf_clover:` | [top](#table-of-contents) | -| [top](#animals--nature) | :maple_leaf: | `:maple_leaf:` | :fallen_leaf: | `:fallen_leaf:` | [top](#table-of-contents) | -| [top](#animals--nature) | :leaves: | `:leaves:` | :empty_nest: | `:empty_nest:` | [top](#table-of-contents) | -| [top](#animals--nature) | :nest_with_eggs: | `:nest_with_eggs:` | :mushroom: | `:mushroom:` | [top](#table-of-contents) | - -## Food & Drink - -- [Food Fruit](#food-fruit) -- [Food Vegetable](#food-vegetable) -- [Food Prepared](#food-prepared) -- [Food Asian](#food-asian) -- [Food Marine](#food-marine) -- [Food Sweet](#food-sweet) -- [Drink](#drink) -- [Dishware](#dishware) - -### Food Fruit - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#food--drink) | :grapes: | `:grapes:` | :melon: | `:melon:` | [top](#table-of-contents) | -| [top](#food--drink) | :watermelon: | `:watermelon:` | :mandarin: | `:mandarin:` `:orange:` `:tangerine:` | [top](#table-of-contents) | -| [top](#food--drink) | :lemon: | `:lemon:` | :banana: | `:banana:` | [top](#table-of-contents) | -| [top](#food--drink) | :pineapple: | `:pineapple:` | :mango: | `:mango:` | [top](#table-of-contents) | -| [top](#food--drink) | :apple: | `:apple:` | :green_apple: | `:green_apple:` | [top](#table-of-contents) | -| [top](#food--drink) | :pear: | `:pear:` | :peach: | `:peach:` | [top](#table-of-contents) | -| [top](#food--drink) | :cherries: | `:cherries:` | :strawberry: | `:strawberry:` | [top](#table-of-contents) | -| [top](#food--drink) | :blueberries: | `:blueberries:` | :kiwi_fruit: | `:kiwi_fruit:` | [top](#table-of-contents) | -| [top](#food--drink) | :tomato: | `:tomato:` | :olive: | `:olive:` | [top](#table-of-contents) | -| [top](#food--drink) | :coconut: | `:coconut:` | | | [top](#table-of-contents) | - -### Food Vegetable - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#food--drink) | :avocado: | `:avocado:` | :eggplant: | `:eggplant:` | [top](#table-of-contents) | -| [top](#food--drink) | :potato: | `:potato:` | :carrot: | `:carrot:` | [top](#table-of-contents) | -| [top](#food--drink) | :corn: | `:corn:` | :hot_pepper: | `:hot_pepper:` | [top](#table-of-contents) | -| [top](#food--drink) | :bell_pepper: | `:bell_pepper:` | :cucumber: | `:cucumber:` | [top](#table-of-contents) | -| [top](#food--drink) | :leafy_green: | `:leafy_green:` | :broccoli: | `:broccoli:` | [top](#table-of-contents) | -| [top](#food--drink) | :garlic: | `:garlic:` | :onion: | `:onion:` | [top](#table-of-contents) | -| [top](#food--drink) | :peanuts: | `:peanuts:` | :beans: | `:beans:` | [top](#table-of-contents) | -| [top](#food--drink) | :chestnut: | `:chestnut:` | :ginger_root: | `:ginger_root:` | [top](#table-of-contents) | -| [top](#food--drink) | :pea_pod: | `:pea_pod:` | | | [top](#table-of-contents) | - -### Food Prepared - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#food--drink) | :bread: | `:bread:` | :croissant: | `:croissant:` | [top](#table-of-contents) | -| [top](#food--drink) | :baguette_bread: | `:baguette_bread:` | :flatbread: | `:flatbread:` | [top](#table-of-contents) | -| [top](#food--drink) | :pretzel: | `:pretzel:` | :bagel: | `:bagel:` | [top](#table-of-contents) | -| [top](#food--drink) | :pancakes: | `:pancakes:` | :waffle: | `:waffle:` | [top](#table-of-contents) | -| [top](#food--drink) | :cheese: | `:cheese:` | :meat_on_bone: | `:meat_on_bone:` | [top](#table-of-contents) | -| [top](#food--drink) | :poultry_leg: | `:poultry_leg:` | :cut_of_meat: | `:cut_of_meat:` | [top](#table-of-contents) | -| [top](#food--drink) | :bacon: | `:bacon:` | :hamburger: | `:hamburger:` | [top](#table-of-contents) | -| [top](#food--drink) | :fries: | `:fries:` | :pizza: | `:pizza:` | [top](#table-of-contents) | -| [top](#food--drink) | :hotdog: | `:hotdog:` | :sandwich: | `:sandwich:` | [top](#table-of-contents) | -| [top](#food--drink) | :taco: | `:taco:` | :burrito: | `:burrito:` | [top](#table-of-contents) | -| [top](#food--drink) | :tamale: | `:tamale:` | :stuffed_flatbread: | `:stuffed_flatbread:` | [top](#table-of-contents) | -| [top](#food--drink) | :falafel: | `:falafel:` | :egg: | `:egg:` | [top](#table-of-contents) | -| [top](#food--drink) | :fried_egg: | `:fried_egg:` | :shallow_pan_of_food: | `:shallow_pan_of_food:` | [top](#table-of-contents) | -| [top](#food--drink) | :stew: | `:stew:` | :fondue: | `:fondue:` | [top](#table-of-contents) | -| [top](#food--drink) | :bowl_with_spoon: | `:bowl_with_spoon:` | :green_salad: | `:green_salad:` | [top](#table-of-contents) | -| [top](#food--drink) | :popcorn: | `:popcorn:` | :butter: | `:butter:` | [top](#table-of-contents) | -| [top](#food--drink) | :salt: | `:salt:` | :canned_food: | `:canned_food:` | [top](#table-of-contents) | - -### Food Asian - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#food--drink) | :bento: | `:bento:` | :rice_cracker: | `:rice_cracker:` | [top](#table-of-contents) | -| [top](#food--drink) | :rice_ball: | `:rice_ball:` | :rice: | `:rice:` | [top](#table-of-contents) | -| [top](#food--drink) | :curry: | `:curry:` | :ramen: | `:ramen:` | [top](#table-of-contents) | -| [top](#food--drink) | :spaghetti: | `:spaghetti:` | :sweet_potato: | `:sweet_potato:` | [top](#table-of-contents) | -| [top](#food--drink) | :oden: | `:oden:` | :sushi: | `:sushi:` | [top](#table-of-contents) | -| [top](#food--drink) | :fried_shrimp: | `:fried_shrimp:` | :fish_cake: | `:fish_cake:` | [top](#table-of-contents) | -| [top](#food--drink) | :moon_cake: | `:moon_cake:` | :dango: | `:dango:` | [top](#table-of-contents) | -| [top](#food--drink) | :dumpling: | `:dumpling:` | :fortune_cookie: | `:fortune_cookie:` | [top](#table-of-contents) | -| [top](#food--drink) | :takeout_box: | `:takeout_box:` | | | [top](#table-of-contents) | - -### Food Marine - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#food--drink) | :crab: | `:crab:` | :lobster: | `:lobster:` | [top](#table-of-contents) | -| [top](#food--drink) | :shrimp: | `:shrimp:` | :squid: | `:squid:` | [top](#table-of-contents) | -| [top](#food--drink) | :oyster: | `:oyster:` | | | [top](#table-of-contents) | - -### Food Sweet - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#food--drink) | :icecream: | `:icecream:` | :shaved_ice: | `:shaved_ice:` | [top](#table-of-contents) | -| [top](#food--drink) | :ice_cream: | `:ice_cream:` | :doughnut: | `:doughnut:` | [top](#table-of-contents) | -| [top](#food--drink) | :cookie: | `:cookie:` | :birthday: | `:birthday:` | [top](#table-of-contents) | -| [top](#food--drink) | :cake: | `:cake:` | :cupcake: | `:cupcake:` | [top](#table-of-contents) | -| [top](#food--drink) | :pie: | `:pie:` | :chocolate_bar: | `:chocolate_bar:` | [top](#table-of-contents) | -| [top](#food--drink) | :candy: | `:candy:` | :lollipop: | `:lollipop:` | [top](#table-of-contents) | -| [top](#food--drink) | :custard: | `:custard:` | :honey_pot: | `:honey_pot:` | [top](#table-of-contents) | - -### Drink - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#food--drink) | :baby_bottle: | `:baby_bottle:` | :milk_glass: | `:milk_glass:` | [top](#table-of-contents) | -| [top](#food--drink) | :coffee: | `:coffee:` | :teapot: | `:teapot:` | [top](#table-of-contents) | -| [top](#food--drink) | :tea: | `:tea:` | :sake: | `:sake:` | [top](#table-of-contents) | -| [top](#food--drink) | :champagne: | `:champagne:` | :wine_glass: | `:wine_glass:` | [top](#table-of-contents) | -| [top](#food--drink) | :cocktail: | `:cocktail:` | :tropical_drink: | `:tropical_drink:` | [top](#table-of-contents) | -| [top](#food--drink) | :beer: | `:beer:` | :beers: | `:beers:` | [top](#table-of-contents) | -| [top](#food--drink) | :clinking_glasses: | `:clinking_glasses:` | :tumbler_glass: | `:tumbler_glass:` | [top](#table-of-contents) | -| [top](#food--drink) | :pouring_liquid: | `:pouring_liquid:` | :cup_with_straw: | `:cup_with_straw:` | [top](#table-of-contents) | -| [top](#food--drink) | :bubble_tea: | `:bubble_tea:` | :beverage_box: | `:beverage_box:` | [top](#table-of-contents) | -| [top](#food--drink) | :mate: | `:mate:` | :ice_cube: | `:ice_cube:` | [top](#table-of-contents) | - -### Dishware - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#food--drink) | :chopsticks: | `:chopsticks:` | :plate_with_cutlery: | `:plate_with_cutlery:` | [top](#table-of-contents) | -| [top](#food--drink) | :fork_and_knife: | `:fork_and_knife:` | :spoon: | `:spoon:` | [top](#table-of-contents) | -| [top](#food--drink) | :hocho: | `:hocho:` `:knife:` | :jar: | `:jar:` | [top](#table-of-contents) | -| [top](#food--drink) | :amphora: | `:amphora:` | | | [top](#table-of-contents) | - -## Travel & Places - -- [Place Map](#place-map) -- [Place Geographic](#place-geographic) -- [Place Building](#place-building) -- [Place Religious](#place-religious) -- [Place Other](#place-other) -- [Transport Ground](#transport-ground) -- [Transport Water](#transport-water) -- [Transport Air](#transport-air) -- [Hotel](#hotel) -- [Time](#time) -- [Sky & Weather](#sky--weather) - -### Place Map - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#travel--places) | :earth_africa: | `:earth_africa:` | :earth_americas: | `:earth_americas:` | [top](#table-of-contents) | -| [top](#travel--places) | :earth_asia: | `:earth_asia:` | :globe_with_meridians: | `:globe_with_meridians:` | [top](#table-of-contents) | -| [top](#travel--places) | :world_map: | `:world_map:` | :japan: | `:japan:` | [top](#table-of-contents) | -| [top](#travel--places) | :compass: | `:compass:` | | | [top](#table-of-contents) | - -### Place Geographic - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#travel--places) | :mountain_snow: | `:mountain_snow:` | :mountain: | `:mountain:` | [top](#table-of-contents) | -| [top](#travel--places) | :volcano: | `:volcano:` | :mount_fuji: | `:mount_fuji:` | [top](#table-of-contents) | -| [top](#travel--places) | :camping: | `:camping:` | :beach_umbrella: | `:beach_umbrella:` | [top](#table-of-contents) | -| [top](#travel--places) | :desert: | `:desert:` | :desert_island: | `:desert_island:` | [top](#table-of-contents) | -| [top](#travel--places) | :national_park: | `:national_park:` | | | [top](#table-of-contents) | - -### Place Building - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#travel--places) | :stadium: | `:stadium:` | :classical_building: | `:classical_building:` | [top](#table-of-contents) | -| [top](#travel--places) | :building_construction: | `:building_construction:` | :bricks: | `:bricks:` | [top](#table-of-contents) | -| [top](#travel--places) | :rock: | `:rock:` | :wood: | `:wood:` | [top](#table-of-contents) | -| [top](#travel--places) | :hut: | `:hut:` | :houses: | `:houses:` | [top](#table-of-contents) | -| [top](#travel--places) | :derelict_house: | `:derelict_house:` | :house: | `:house:` | [top](#table-of-contents) | -| [top](#travel--places) | :house_with_garden: | `:house_with_garden:` | :office: | `:office:` | [top](#table-of-contents) | -| [top](#travel--places) | :post_office: | `:post_office:` | :european_post_office: | `:european_post_office:` | [top](#table-of-contents) | -| [top](#travel--places) | :hospital: | `:hospital:` | :bank: | `:bank:` | [top](#table-of-contents) | -| [top](#travel--places) | :hotel: | `:hotel:` | :love_hotel: | `:love_hotel:` | [top](#table-of-contents) | -| [top](#travel--places) | :convenience_store: | `:convenience_store:` | :school: | `:school:` | [top](#table-of-contents) | -| [top](#travel--places) | :department_store: | `:department_store:` | :factory: | `:factory:` | [top](#table-of-contents) | -| [top](#travel--places) | :japanese_castle: | `:japanese_castle:` | :european_castle: | `:european_castle:` | [top](#table-of-contents) | -| [top](#travel--places) | :wedding: | `:wedding:` | :tokyo_tower: | `:tokyo_tower:` | [top](#table-of-contents) | -| [top](#travel--places) | :statue_of_liberty: | `:statue_of_liberty:` | | | [top](#table-of-contents) | - -### Place Religious - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#travel--places) | :church: | `:church:` | :mosque: | `:mosque:` | [top](#table-of-contents) | -| [top](#travel--places) | :hindu_temple: | `:hindu_temple:` | :synagogue: | `:synagogue:` | [top](#table-of-contents) | -| [top](#travel--places) | :shinto_shrine: | `:shinto_shrine:` | :kaaba: | `:kaaba:` | [top](#table-of-contents) | - -### Place Other - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#travel--places) | :fountain: | `:fountain:` | :tent: | `:tent:` | [top](#table-of-contents) | -| [top](#travel--places) | :foggy: | `:foggy:` | :night_with_stars: | `:night_with_stars:` | [top](#table-of-contents) | -| [top](#travel--places) | :cityscape: | `:cityscape:` | :sunrise_over_mountains: | `:sunrise_over_mountains:` | [top](#table-of-contents) | -| [top](#travel--places) | :sunrise: | `:sunrise:` | :city_sunset: | `:city_sunset:` | [top](#table-of-contents) | -| [top](#travel--places) | :city_sunrise: | `:city_sunrise:` | :bridge_at_night: | `:bridge_at_night:` | [top](#table-of-contents) | -| [top](#travel--places) | :hotsprings: | `:hotsprings:` | :carousel_horse: | `:carousel_horse:` | [top](#table-of-contents) | -| [top](#travel--places) | :playground_slide: | `:playground_slide:` | :ferris_wheel: | `:ferris_wheel:` | [top](#table-of-contents) | -| [top](#travel--places) | :roller_coaster: | `:roller_coaster:` | :barber: | `:barber:` | [top](#table-of-contents) | -| [top](#travel--places) | :circus_tent: | `:circus_tent:` | | | [top](#table-of-contents) | - -### Transport Ground - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#travel--places) | :steam_locomotive: | `:steam_locomotive:` | :railway_car: | `:railway_car:` | [top](#table-of-contents) | -| [top](#travel--places) | :bullettrain_side: | `:bullettrain_side:` | :bullettrain_front: | `:bullettrain_front:` | [top](#table-of-contents) | -| [top](#travel--places) | :train2: | `:train2:` | :metro: | `:metro:` | [top](#table-of-contents) | -| [top](#travel--places) | :light_rail: | `:light_rail:` | :station: | `:station:` | [top](#table-of-contents) | -| [top](#travel--places) | :tram: | `:tram:` | :monorail: | `:monorail:` | [top](#table-of-contents) | -| [top](#travel--places) | :mountain_railway: | `:mountain_railway:` | :train: | `:train:` | [top](#table-of-contents) | -| [top](#travel--places) | :bus: | `:bus:` | :oncoming_bus: | `:oncoming_bus:` | [top](#table-of-contents) | -| [top](#travel--places) | :trolleybus: | `:trolleybus:` | :minibus: | `:minibus:` | [top](#table-of-contents) | -| [top](#travel--places) | :ambulance: | `:ambulance:` | :fire_engine: | `:fire_engine:` | [top](#table-of-contents) | -| [top](#travel--places) | :police_car: | `:police_car:` | :oncoming_police_car: | `:oncoming_police_car:` | [top](#table-of-contents) | -| [top](#travel--places) | :taxi: | `:taxi:` | :oncoming_taxi: | `:oncoming_taxi:` | [top](#table-of-contents) | -| [top](#travel--places) | :car: | `:car:` `:red_car:` | :oncoming_automobile: | `:oncoming_automobile:` | [top](#table-of-contents) | -| [top](#travel--places) | :blue_car: | `:blue_car:` | :pickup_truck: | `:pickup_truck:` | [top](#table-of-contents) | -| [top](#travel--places) | :truck: | `:truck:` | :articulated_lorry: | `:articulated_lorry:` | [top](#table-of-contents) | -| [top](#travel--places) | :tractor: | `:tractor:` | :racing_car: | `:racing_car:` | [top](#table-of-contents) | -| [top](#travel--places) | :motorcycle: | `:motorcycle:` | :motor_scooter: | `:motor_scooter:` | [top](#table-of-contents) | -| [top](#travel--places) | :manual_wheelchair: | `:manual_wheelchair:` | :motorized_wheelchair: | `:motorized_wheelchair:` | [top](#table-of-contents) | -| [top](#travel--places) | :auto_rickshaw: | `:auto_rickshaw:` | :bike: | `:bike:` | [top](#table-of-contents) | -| [top](#travel--places) | :kick_scooter: | `:kick_scooter:` | :skateboard: | `:skateboard:` | [top](#table-of-contents) | -| [top](#travel--places) | :roller_skate: | `:roller_skate:` | :busstop: | `:busstop:` | [top](#table-of-contents) | -| [top](#travel--places) | :motorway: | `:motorway:` | :railway_track: | `:railway_track:` | [top](#table-of-contents) | -| [top](#travel--places) | :oil_drum: | `:oil_drum:` | :fuelpump: | `:fuelpump:` | [top](#table-of-contents) | -| [top](#travel--places) | :wheel: | `:wheel:` | :rotating_light: | `:rotating_light:` | [top](#table-of-contents) | -| [top](#travel--places) | :traffic_light: | `:traffic_light:` | :vertical_traffic_light: | `:vertical_traffic_light:` | [top](#table-of-contents) | -| [top](#travel--places) | :stop_sign: | `:stop_sign:` | :construction: | `:construction:` | [top](#table-of-contents) | - -### Transport Water - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#travel--places) | :anchor: | `:anchor:` | :ring_buoy: | `:ring_buoy:` | [top](#table-of-contents) | -| [top](#travel--places) | :boat: | `:boat:` `:sailboat:` | :canoe: | `:canoe:` | [top](#table-of-contents) | -| [top](#travel--places) | :speedboat: | `:speedboat:` | :passenger_ship: | `:passenger_ship:` | [top](#table-of-contents) | -| [top](#travel--places) | :ferry: | `:ferry:` | :motor_boat: | `:motor_boat:` | [top](#table-of-contents) | -| [top](#travel--places) | :ship: | `:ship:` | | | [top](#table-of-contents) | - -### Transport Air - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#travel--places) | :airplane: | `:airplane:` | :small_airplane: | `:small_airplane:` | [top](#table-of-contents) | -| [top](#travel--places) | :flight_departure: | `:flight_departure:` | :flight_arrival: | `:flight_arrival:` | [top](#table-of-contents) | -| [top](#travel--places) | :parachute: | `:parachute:` | :seat: | `:seat:` | [top](#table-of-contents) | -| [top](#travel--places) | :helicopter: | `:helicopter:` | :suspension_railway: | `:suspension_railway:` | [top](#table-of-contents) | -| [top](#travel--places) | :mountain_cableway: | `:mountain_cableway:` | :aerial_tramway: | `:aerial_tramway:` | [top](#table-of-contents) | -| [top](#travel--places) | :artificial_satellite: | `:artificial_satellite:` | :rocket: | `:rocket:` | [top](#table-of-contents) | -| [top](#travel--places) | :flying_saucer: | `:flying_saucer:` | | | [top](#table-of-contents) | - -### Hotel - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#travel--places) | :bellhop_bell: | `:bellhop_bell:` | :luggage: | `:luggage:` | [top](#table-of-contents) | - -### Time - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#travel--places) | :hourglass: | `:hourglass:` | :hourglass_flowing_sand: | `:hourglass_flowing_sand:` | [top](#table-of-contents) | -| [top](#travel--places) | :watch: | `:watch:` | :alarm_clock: | `:alarm_clock:` | [top](#table-of-contents) | -| [top](#travel--places) | :stopwatch: | `:stopwatch:` | :timer_clock: | `:timer_clock:` | [top](#table-of-contents) | -| [top](#travel--places) | :mantelpiece_clock: | `:mantelpiece_clock:` | :clock12: | `:clock12:` | [top](#table-of-contents) | -| [top](#travel--places) | :clock1230: | `:clock1230:` | :clock1: | `:clock1:` | [top](#table-of-contents) | -| [top](#travel--places) | :clock130: | `:clock130:` | :clock2: | `:clock2:` | [top](#table-of-contents) | -| [top](#travel--places) | :clock230: | `:clock230:` | :clock3: | `:clock3:` | [top](#table-of-contents) | -| [top](#travel--places) | :clock330: | `:clock330:` | :clock4: | `:clock4:` | [top](#table-of-contents) | -| [top](#travel--places) | :clock430: | `:clock430:` | :clock5: | `:clock5:` | [top](#table-of-contents) | -| [top](#travel--places) | :clock530: | `:clock530:` | :clock6: | `:clock6:` | [top](#table-of-contents) | -| [top](#travel--places) | :clock630: | `:clock630:` | :clock7: | `:clock7:` | [top](#table-of-contents) | -| [top](#travel--places) | :clock730: | `:clock730:` | :clock8: | `:clock8:` | [top](#table-of-contents) | -| [top](#travel--places) | :clock830: | `:clock830:` | :clock9: | `:clock9:` | [top](#table-of-contents) | -| [top](#travel--places) | :clock930: | `:clock930:` | :clock10: | `:clock10:` | [top](#table-of-contents) | -| [top](#travel--places) | :clock1030: | `:clock1030:` | :clock11: | `:clock11:` | [top](#table-of-contents) | -| [top](#travel--places) | :clock1130: | `:clock1130:` | | | [top](#table-of-contents) | - -### Sky & Weather - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#travel--places) | :new_moon: | `:new_moon:` | :waxing_crescent_moon: | `:waxing_crescent_moon:` | [top](#table-of-contents) | -| [top](#travel--places) | :first_quarter_moon: | `:first_quarter_moon:` | :moon: | `:moon:` `:waxing_gibbous_moon:` | [top](#table-of-contents) | -| [top](#travel--places) | :full_moon: | `:full_moon:` | :waning_gibbous_moon: | `:waning_gibbous_moon:` | [top](#table-of-contents) | -| [top](#travel--places) | :last_quarter_moon: | `:last_quarter_moon:` | :waning_crescent_moon: | `:waning_crescent_moon:` | [top](#table-of-contents) | -| [top](#travel--places) | :crescent_moon: | `:crescent_moon:` | :new_moon_with_face: | `:new_moon_with_face:` | [top](#table-of-contents) | -| [top](#travel--places) | :first_quarter_moon_with_face: | `:first_quarter_moon_with_face:` | :last_quarter_moon_with_face: | `:last_quarter_moon_with_face:` | [top](#table-of-contents) | -| [top](#travel--places) | :thermometer: | `:thermometer:` | :sunny: | `:sunny:` | [top](#table-of-contents) | -| [top](#travel--places) | :full_moon_with_face: | `:full_moon_with_face:` | :sun_with_face: | `:sun_with_face:` | [top](#table-of-contents) | -| [top](#travel--places) | :ringed_planet: | `:ringed_planet:` | :star: | `:star:` | [top](#table-of-contents) | -| [top](#travel--places) | :star2: | `:star2:` | :stars: | `:stars:` | [top](#table-of-contents) | -| [top](#travel--places) | :milky_way: | `:milky_way:` | :cloud: | `:cloud:` | [top](#table-of-contents) | -| [top](#travel--places) | :partly_sunny: | `:partly_sunny:` | :cloud_with_lightning_and_rain: | `:cloud_with_lightning_and_rain:` | [top](#table-of-contents) | -| [top](#travel--places) | :sun_behind_small_cloud: | `:sun_behind_small_cloud:` | :sun_behind_large_cloud: | `:sun_behind_large_cloud:` | [top](#table-of-contents) | -| [top](#travel--places) | :sun_behind_rain_cloud: | `:sun_behind_rain_cloud:` | :cloud_with_rain: | `:cloud_with_rain:` | [top](#table-of-contents) | -| [top](#travel--places) | :cloud_with_snow: | `:cloud_with_snow:` | :cloud_with_lightning: | `:cloud_with_lightning:` | [top](#table-of-contents) | -| [top](#travel--places) | :tornado: | `:tornado:` | :fog: | `:fog:` | [top](#table-of-contents) | -| [top](#travel--places) | :wind_face: | `:wind_face:` | :cyclone: | `:cyclone:` | [top](#table-of-contents) | -| [top](#travel--places) | :rainbow: | `:rainbow:` | :closed_umbrella: | `:closed_umbrella:` | [top](#table-of-contents) | -| [top](#travel--places) | :open_umbrella: | `:open_umbrella:` | :umbrella: | `:umbrella:` | [top](#table-of-contents) | -| [top](#travel--places) | :parasol_on_ground: | `:parasol_on_ground:` | :zap: | `:zap:` | [top](#table-of-contents) | -| [top](#travel--places) | :snowflake: | `:snowflake:` | :snowman_with_snow: | `:snowman_with_snow:` | [top](#table-of-contents) | -| [top](#travel--places) | :snowman: | `:snowman:` | :comet: | `:comet:` | [top](#table-of-contents) | -| [top](#travel--places) | :fire: | `:fire:` | :droplet: | `:droplet:` | [top](#table-of-contents) | -| [top](#travel--places) | :ocean: | `:ocean:` | | | [top](#table-of-contents) | - -## Activities - -- [Event](#event) -- [Award Medal](#award-medal) -- [Sport](#sport) -- [Game](#game) -- [Arts & Crafts](#arts--crafts) - -### Event - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#activities) | :jack_o_lantern: | `:jack_o_lantern:` | :christmas_tree: | `:christmas_tree:` | [top](#table-of-contents) | -| [top](#activities) | :fireworks: | `:fireworks:` | :sparkler: | `:sparkler:` | [top](#table-of-contents) | -| [top](#activities) | :firecracker: | `:firecracker:` | :sparkles: | `:sparkles:` | [top](#table-of-contents) | -| [top](#activities) | :balloon: | `:balloon:` | :tada: | `:tada:` | [top](#table-of-contents) | -| [top](#activities) | :confetti_ball: | `:confetti_ball:` | :tanabata_tree: | `:tanabata_tree:` | [top](#table-of-contents) | -| [top](#activities) | :bamboo: | `:bamboo:` | :dolls: | `:dolls:` | [top](#table-of-contents) | -| [top](#activities) | :flags: | `:flags:` | :wind_chime: | `:wind_chime:` | [top](#table-of-contents) | -| [top](#activities) | :rice_scene: | `:rice_scene:` | :red_envelope: | `:red_envelope:` | [top](#table-of-contents) | -| [top](#activities) | :ribbon: | `:ribbon:` | :gift: | `:gift:` | [top](#table-of-contents) | -| [top](#activities) | :reminder_ribbon: | `:reminder_ribbon:` | :tickets: | `:tickets:` | [top](#table-of-contents) | -| [top](#activities) | :ticket: | `:ticket:` | | | [top](#table-of-contents) | - -### Award Medal - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#activities) | :medal_military: | `:medal_military:` | :trophy: | `:trophy:` | [top](#table-of-contents) | -| [top](#activities) | :medal_sports: | `:medal_sports:` | :1st_place_medal: | `:1st_place_medal:` | [top](#table-of-contents) | -| [top](#activities) | :2nd_place_medal: | `:2nd_place_medal:` | :3rd_place_medal: | `:3rd_place_medal:` | [top](#table-of-contents) | - -### Sport - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#activities) | :soccer: | `:soccer:` | :baseball: | `:baseball:` | [top](#table-of-contents) | -| [top](#activities) | :softball: | `:softball:` | :basketball: | `:basketball:` | [top](#table-of-contents) | -| [top](#activities) | :volleyball: | `:volleyball:` | :football: | `:football:` | [top](#table-of-contents) | -| [top](#activities) | :rugby_football: | `:rugby_football:` | :tennis: | `:tennis:` | [top](#table-of-contents) | -| [top](#activities) | :flying_disc: | `:flying_disc:` | :bowling: | `:bowling:` | [top](#table-of-contents) | -| [top](#activities) | :cricket_game: | `:cricket_game:` | :field_hockey: | `:field_hockey:` | [top](#table-of-contents) | -| [top](#activities) | :ice_hockey: | `:ice_hockey:` | :lacrosse: | `:lacrosse:` | [top](#table-of-contents) | -| [top](#activities) | :ping_pong: | `:ping_pong:` | :badminton: | `:badminton:` | [top](#table-of-contents) | -| [top](#activities) | :boxing_glove: | `:boxing_glove:` | :martial_arts_uniform: | `:martial_arts_uniform:` | [top](#table-of-contents) | -| [top](#activities) | :goal_net: | `:goal_net:` | :golf: | `:golf:` | [top](#table-of-contents) | -| [top](#activities) | :ice_skate: | `:ice_skate:` | :fishing_pole_and_fish: | `:fishing_pole_and_fish:` | [top](#table-of-contents) | -| [top](#activities) | :diving_mask: | `:diving_mask:` | :running_shirt_with_sash: | `:running_shirt_with_sash:` | [top](#table-of-contents) | -| [top](#activities) | :ski: | `:ski:` | :sled: | `:sled:` | [top](#table-of-contents) | -| [top](#activities) | :curling_stone: | `:curling_stone:` | | | [top](#table-of-contents) | - -### Game - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#activities) | :dart: | `:dart:` | :yo_yo: | `:yo_yo:` | [top](#table-of-contents) | -| [top](#activities) | :kite: | `:kite:` | :gun: | `:gun:` | [top](#table-of-contents) | -| [top](#activities) | :8ball: | `:8ball:` | :crystal_ball: | `:crystal_ball:` | [top](#table-of-contents) | -| [top](#activities) | :magic_wand: | `:magic_wand:` | :video_game: | `:video_game:` | [top](#table-of-contents) | -| [top](#activities) | :joystick: | `:joystick:` | :slot_machine: | `:slot_machine:` | [top](#table-of-contents) | -| [top](#activities) | :game_die: | `:game_die:` | :jigsaw: | `:jigsaw:` | [top](#table-of-contents) | -| [top](#activities) | :teddy_bear: | `:teddy_bear:` | :pinata: | `:pinata:` | [top](#table-of-contents) | -| [top](#activities) | :mirror_ball: | `:mirror_ball:` | :nesting_dolls: | `:nesting_dolls:` | [top](#table-of-contents) | -| [top](#activities) | :spades: | `:spades:` | :hearts: | `:hearts:` | [top](#table-of-contents) | -| [top](#activities) | :diamonds: | `:diamonds:` | :clubs: | `:clubs:` | [top](#table-of-contents) | -| [top](#activities) | :chess_pawn: | `:chess_pawn:` | :black_joker: | `:black_joker:` | [top](#table-of-contents) | -| [top](#activities) | :mahjong: | `:mahjong:` | :flower_playing_cards: | `:flower_playing_cards:` | [top](#table-of-contents) | - -### Arts & Crafts - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#activities) | :performing_arts: | `:performing_arts:` | :framed_picture: | `:framed_picture:` | [top](#table-of-contents) | -| [top](#activities) | :art: | `:art:` | :thread: | `:thread:` | [top](#table-of-contents) | -| [top](#activities) | :sewing_needle: | `:sewing_needle:` | :yarn: | `:yarn:` | [top](#table-of-contents) | -| [top](#activities) | :knot: | `:knot:` | | | [top](#table-of-contents) | - -## Objects - -- [Clothing](#clothing) -- [Sound](#sound) -- [Music](#music) -- [Musical Instrument](#musical-instrument) -- [Phone](#phone) -- [Computer](#computer) -- [Light & Video](#light--video) -- [Book Paper](#book-paper) -- [Money](#money) -- [Mail](#mail) -- [Writing](#writing) -- [Office](#office) -- [Lock](#lock) -- [Tool](#tool) -- [Science](#science) -- [Medical](#medical) -- [Household](#household) -- [Other Object](#other-object) - -### Clothing - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#objects) | :eyeglasses: | `:eyeglasses:` | :dark_sunglasses: | `:dark_sunglasses:` | [top](#table-of-contents) | -| [top](#objects) | :goggles: | `:goggles:` | :lab_coat: | `:lab_coat:` | [top](#table-of-contents) | -| [top](#objects) | :safety_vest: | `:safety_vest:` | :necktie: | `:necktie:` | [top](#table-of-contents) | -| [top](#objects) | :shirt: | `:shirt:` `:tshirt:` | :jeans: | `:jeans:` | [top](#table-of-contents) | -| [top](#objects) | :scarf: | `:scarf:` | :gloves: | `:gloves:` | [top](#table-of-contents) | -| [top](#objects) | :coat: | `:coat:` | :socks: | `:socks:` | [top](#table-of-contents) | -| [top](#objects) | :dress: | `:dress:` | :kimono: | `:kimono:` | [top](#table-of-contents) | -| [top](#objects) | :sari: | `:sari:` | :one_piece_swimsuit: | `:one_piece_swimsuit:` | [top](#table-of-contents) | -| [top](#objects) | :swim_brief: | `:swim_brief:` | :shorts: | `:shorts:` | [top](#table-of-contents) | -| [top](#objects) | :bikini: | `:bikini:` | :womans_clothes: | `:womans_clothes:` | [top](#table-of-contents) | -| [top](#objects) | :folding_hand_fan: | `:folding_hand_fan:` | :purse: | `:purse:` | [top](#table-of-contents) | -| [top](#objects) | :handbag: | `:handbag:` | :pouch: | `:pouch:` | [top](#table-of-contents) | -| [top](#objects) | :shopping: | `:shopping:` | :school_satchel: | `:school_satchel:` | [top](#table-of-contents) | -| [top](#objects) | :thong_sandal: | `:thong_sandal:` | :mans_shoe: | `:mans_shoe:` `:shoe:` | [top](#table-of-contents) | -| [top](#objects) | :athletic_shoe: | `:athletic_shoe:` | :hiking_boot: | `:hiking_boot:` | [top](#table-of-contents) | -| [top](#objects) | :flat_shoe: | `:flat_shoe:` | :high_heel: | `:high_heel:` | [top](#table-of-contents) | -| [top](#objects) | :sandal: | `:sandal:` | :ballet_shoes: | `:ballet_shoes:` | [top](#table-of-contents) | -| [top](#objects) | :boot: | `:boot:` | :hair_pick: | `:hair_pick:` | [top](#table-of-contents) | -| [top](#objects) | :crown: | `:crown:` | :womans_hat: | `:womans_hat:` | [top](#table-of-contents) | -| [top](#objects) | :tophat: | `:tophat:` | :mortar_board: | `:mortar_board:` | [top](#table-of-contents) | -| [top](#objects) | :billed_cap: | `:billed_cap:` | :military_helmet: | `:military_helmet:` | [top](#table-of-contents) | -| [top](#objects) | :rescue_worker_helmet: | `:rescue_worker_helmet:` | :prayer_beads: | `:prayer_beads:` | [top](#table-of-contents) | -| [top](#objects) | :lipstick: | `:lipstick:` | :ring: | `:ring:` | [top](#table-of-contents) | -| [top](#objects) | :gem: | `:gem:` | | | [top](#table-of-contents) | - -### Sound - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#objects) | :mute: | `:mute:` | :speaker: | `:speaker:` | [top](#table-of-contents) | -| [top](#objects) | :sound: | `:sound:` | :loud_sound: | `:loud_sound:` | [top](#table-of-contents) | -| [top](#objects) | :loudspeaker: | `:loudspeaker:` | :mega: | `:mega:` | [top](#table-of-contents) | -| [top](#objects) | :postal_horn: | `:postal_horn:` | :bell: | `:bell:` | [top](#table-of-contents) | -| [top](#objects) | :no_bell: | `:no_bell:` | | | [top](#table-of-contents) | - -### Music - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#objects) | :musical_score: | `:musical_score:` | :musical_note: | `:musical_note:` | [top](#table-of-contents) | -| [top](#objects) | :notes: | `:notes:` | :studio_microphone: | `:studio_microphone:` | [top](#table-of-contents) | -| [top](#objects) | :level_slider: | `:level_slider:` | :control_knobs: | `:control_knobs:` | [top](#table-of-contents) | -| [top](#objects) | :microphone: | `:microphone:` | :headphones: | `:headphones:` | [top](#table-of-contents) | -| [top](#objects) | :radio: | `:radio:` | | | [top](#table-of-contents) | - -### Musical Instrument - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#objects) | :saxophone: | `:saxophone:` | :accordion: | `:accordion:` | [top](#table-of-contents) | -| [top](#objects) | :guitar: | `:guitar:` | :musical_keyboard: | `:musical_keyboard:` | [top](#table-of-contents) | -| [top](#objects) | :trumpet: | `:trumpet:` | :violin: | `:violin:` | [top](#table-of-contents) | -| [top](#objects) | :banjo: | `:banjo:` | :drum: | `:drum:` | [top](#table-of-contents) | -| [top](#objects) | :long_drum: | `:long_drum:` | :maracas: | `:maracas:` | [top](#table-of-contents) | -| [top](#objects) | :flute: | `:flute:` | | | [top](#table-of-contents) | - -### Phone - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#objects) | :iphone: | `:iphone:` | :calling: | `:calling:` | [top](#table-of-contents) | -| [top](#objects) | :phone: | `:phone:` `:telephone:` | :telephone_receiver: | `:telephone_receiver:` | [top](#table-of-contents) | -| [top](#objects) | :pager: | `:pager:` | :fax: | `:fax:` | [top](#table-of-contents) | - -### Computer - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#objects) | :battery: | `:battery:` | :low_battery: | `:low_battery:` | [top](#table-of-contents) | -| [top](#objects) | :electric_plug: | `:electric_plug:` | :computer: | `:computer:` | [top](#table-of-contents) | -| [top](#objects) | :desktop_computer: | `:desktop_computer:` | :printer: | `:printer:` | [top](#table-of-contents) | -| [top](#objects) | :keyboard: | `:keyboard:` | :computer_mouse: | `:computer_mouse:` | [top](#table-of-contents) | -| [top](#objects) | :trackball: | `:trackball:` | :minidisc: | `:minidisc:` | [top](#table-of-contents) | -| [top](#objects) | :floppy_disk: | `:floppy_disk:` | :cd: | `:cd:` | [top](#table-of-contents) | -| [top](#objects) | :dvd: | `:dvd:` | :abacus: | `:abacus:` | [top](#table-of-contents) | - -### Light & Video - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#objects) | :movie_camera: | `:movie_camera:` | :film_strip: | `:film_strip:` | [top](#table-of-contents) | -| [top](#objects) | :film_projector: | `:film_projector:` | :clapper: | `:clapper:` | [top](#table-of-contents) | -| [top](#objects) | :tv: | `:tv:` | :camera: | `:camera:` | [top](#table-of-contents) | -| [top](#objects) | :camera_flash: | `:camera_flash:` | :video_camera: | `:video_camera:` | [top](#table-of-contents) | -| [top](#objects) | :vhs: | `:vhs:` | :mag: | `:mag:` | [top](#table-of-contents) | -| [top](#objects) | :mag_right: | `:mag_right:` | :candle: | `:candle:` | [top](#table-of-contents) | -| [top](#objects) | :bulb: | `:bulb:` | :flashlight: | `:flashlight:` | [top](#table-of-contents) | -| [top](#objects) | :izakaya_lantern: | `:izakaya_lantern:` `:lantern:` | :diya_lamp: | `:diya_lamp:` | [top](#table-of-contents) | - -### Book Paper - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#objects) | :notebook_with_decorative_cover: | `:notebook_with_decorative_cover:` | :closed_book: | `:closed_book:` | [top](#table-of-contents) | -| [top](#objects) | :book: | `:book:` `:open_book:` | :green_book: | `:green_book:` | [top](#table-of-contents) | -| [top](#objects) | :blue_book: | `:blue_book:` | :orange_book: | `:orange_book:` | [top](#table-of-contents) | -| [top](#objects) | :books: | `:books:` | :notebook: | `:notebook:` | [top](#table-of-contents) | -| [top](#objects) | :ledger: | `:ledger:` | :page_with_curl: | `:page_with_curl:` | [top](#table-of-contents) | -| [top](#objects) | :scroll: | `:scroll:` | :page_facing_up: | `:page_facing_up:` | [top](#table-of-contents) | -| [top](#objects) | :newspaper: | `:newspaper:` | :newspaper_roll: | `:newspaper_roll:` | [top](#table-of-contents) | -| [top](#objects) | :bookmark_tabs: | `:bookmark_tabs:` | :bookmark: | `:bookmark:` | [top](#table-of-contents) | -| [top](#objects) | :label: | `:label:` | | | [top](#table-of-contents) | - -### Money - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#objects) | :moneybag: | `:moneybag:` | :coin: | `:coin:` | [top](#table-of-contents) | -| [top](#objects) | :yen: | `:yen:` | :dollar: | `:dollar:` | [top](#table-of-contents) | -| [top](#objects) | :euro: | `:euro:` | :pound: | `:pound:` | [top](#table-of-contents) | -| [top](#objects) | :money_with_wings: | `:money_with_wings:` | :credit_card: | `:credit_card:` | [top](#table-of-contents) | -| [top](#objects) | :receipt: | `:receipt:` | :chart: | `:chart:` | [top](#table-of-contents) | - -### Mail - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#objects) | :envelope: | `:envelope:` | :e-mail: | `:e-mail:` `:email:` | [top](#table-of-contents) | -| [top](#objects) | :incoming_envelope: | `:incoming_envelope:` | :envelope_with_arrow: | `:envelope_with_arrow:` | [top](#table-of-contents) | -| [top](#objects) | :outbox_tray: | `:outbox_tray:` | :inbox_tray: | `:inbox_tray:` | [top](#table-of-contents) | -| [top](#objects) | :package: | `:package:` | :mailbox: | `:mailbox:` | [top](#table-of-contents) | -| [top](#objects) | :mailbox_closed: | `:mailbox_closed:` | :mailbox_with_mail: | `:mailbox_with_mail:` | [top](#table-of-contents) | -| [top](#objects) | :mailbox_with_no_mail: | `:mailbox_with_no_mail:` | :postbox: | `:postbox:` | [top](#table-of-contents) | -| [top](#objects) | :ballot_box: | `:ballot_box:` | | | [top](#table-of-contents) | - -### Writing - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#objects) | :pencil2: | `:pencil2:` | :black_nib: | `:black_nib:` | [top](#table-of-contents) | -| [top](#objects) | :fountain_pen: | `:fountain_pen:` | :pen: | `:pen:` | [top](#table-of-contents) | -| [top](#objects) | :paintbrush: | `:paintbrush:` | :crayon: | `:crayon:` | [top](#table-of-contents) | -| [top](#objects) | :memo: | `:memo:` `:pencil:` | | | [top](#table-of-contents) | - -### Office - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#objects) | :briefcase: | `:briefcase:` | :file_folder: | `:file_folder:` | [top](#table-of-contents) | -| [top](#objects) | :open_file_folder: | `:open_file_folder:` | :card_index_dividers: | `:card_index_dividers:` | [top](#table-of-contents) | -| [top](#objects) | :date: | `:date:` | :calendar: | `:calendar:` | [top](#table-of-contents) | -| [top](#objects) | :spiral_notepad: | `:spiral_notepad:` | :spiral_calendar: | `:spiral_calendar:` | [top](#table-of-contents) | -| [top](#objects) | :card_index: | `:card_index:` | :chart_with_upwards_trend: | `:chart_with_upwards_trend:` | [top](#table-of-contents) | -| [top](#objects) | :chart_with_downwards_trend: | `:chart_with_downwards_trend:` | :bar_chart: | `:bar_chart:` | [top](#table-of-contents) | -| [top](#objects) | :clipboard: | `:clipboard:` | :pushpin: | `:pushpin:` | [top](#table-of-contents) | -| [top](#objects) | :round_pushpin: | `:round_pushpin:` | :paperclip: | `:paperclip:` | [top](#table-of-contents) | -| [top](#objects) | :paperclips: | `:paperclips:` | :straight_ruler: | `:straight_ruler:` | [top](#table-of-contents) | -| [top](#objects) | :triangular_ruler: | `:triangular_ruler:` | :scissors: | `:scissors:` | [top](#table-of-contents) | -| [top](#objects) | :card_file_box: | `:card_file_box:` | :file_cabinet: | `:file_cabinet:` | [top](#table-of-contents) | -| [top](#objects) | :wastebasket: | `:wastebasket:` | | | [top](#table-of-contents) | - -### Lock - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#objects) | :lock: | `:lock:` | :unlock: | `:unlock:` | [top](#table-of-contents) | -| [top](#objects) | :lock_with_ink_pen: | `:lock_with_ink_pen:` | :closed_lock_with_key: | `:closed_lock_with_key:` | [top](#table-of-contents) | -| [top](#objects) | :key: | `:key:` | :old_key: | `:old_key:` | [top](#table-of-contents) | - -### Tool - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#objects) | :hammer: | `:hammer:` | :axe: | `:axe:` | [top](#table-of-contents) | -| [top](#objects) | :pick: | `:pick:` | :hammer_and_pick: | `:hammer_and_pick:` | [top](#table-of-contents) | -| [top](#objects) | :hammer_and_wrench: | `:hammer_and_wrench:` | :dagger: | `:dagger:` | [top](#table-of-contents) | -| [top](#objects) | :crossed_swords: | `:crossed_swords:` | :bomb: | `:bomb:` | [top](#table-of-contents) | -| [top](#objects) | :boomerang: | `:boomerang:` | :bow_and_arrow: | `:bow_and_arrow:` | [top](#table-of-contents) | -| [top](#objects) | :shield: | `:shield:` | :carpentry_saw: | `:carpentry_saw:` | [top](#table-of-contents) | -| [top](#objects) | :wrench: | `:wrench:` | :screwdriver: | `:screwdriver:` | [top](#table-of-contents) | -| [top](#objects) | :nut_and_bolt: | `:nut_and_bolt:` | :gear: | `:gear:` | [top](#table-of-contents) | -| [top](#objects) | :clamp: | `:clamp:` | :balance_scale: | `:balance_scale:` | [top](#table-of-contents) | -| [top](#objects) | :probing_cane: | `:probing_cane:` | :link: | `:link:` | [top](#table-of-contents) | -| [top](#objects) | :chains: | `:chains:` | :hook: | `:hook:` | [top](#table-of-contents) | -| [top](#objects) | :toolbox: | `:toolbox:` | :magnet: | `:magnet:` | [top](#table-of-contents) | -| [top](#objects) | :ladder: | `:ladder:` | | | [top](#table-of-contents) | - -### Science - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#objects) | :alembic: | `:alembic:` | :test_tube: | `:test_tube:` | [top](#table-of-contents) | -| [top](#objects) | :petri_dish: | `:petri_dish:` | :dna: | `:dna:` | [top](#table-of-contents) | -| [top](#objects) | :microscope: | `:microscope:` | :telescope: | `:telescope:` | [top](#table-of-contents) | -| [top](#objects) | :satellite: | `:satellite:` | | | [top](#table-of-contents) | - -### Medical - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#objects) | :syringe: | `:syringe:` | :drop_of_blood: | `:drop_of_blood:` | [top](#table-of-contents) | -| [top](#objects) | :pill: | `:pill:` | :adhesive_bandage: | `:adhesive_bandage:` | [top](#table-of-contents) | -| [top](#objects) | :crutch: | `:crutch:` | :stethoscope: | `:stethoscope:` | [top](#table-of-contents) | -| [top](#objects) | :x_ray: | `:x_ray:` | | | [top](#table-of-contents) | - -### Household - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#objects) | :door: | `:door:` | :elevator: | `:elevator:` | [top](#table-of-contents) | -| [top](#objects) | :mirror: | `:mirror:` | :window: | `:window:` | [top](#table-of-contents) | -| [top](#objects) | :bed: | `:bed:` | :couch_and_lamp: | `:couch_and_lamp:` | [top](#table-of-contents) | -| [top](#objects) | :chair: | `:chair:` | :toilet: | `:toilet:` | [top](#table-of-contents) | -| [top](#objects) | :plunger: | `:plunger:` | :shower: | `:shower:` | [top](#table-of-contents) | -| [top](#objects) | :bathtub: | `:bathtub:` | :mouse_trap: | `:mouse_trap:` | [top](#table-of-contents) | -| [top](#objects) | :razor: | `:razor:` | :lotion_bottle: | `:lotion_bottle:` | [top](#table-of-contents) | -| [top](#objects) | :safety_pin: | `:safety_pin:` | :broom: | `:broom:` | [top](#table-of-contents) | -| [top](#objects) | :basket: | `:basket:` | :roll_of_paper: | `:roll_of_paper:` | [top](#table-of-contents) | -| [top](#objects) | :bucket: | `:bucket:` | :soap: | `:soap:` | [top](#table-of-contents) | -| [top](#objects) | :bubbles: | `:bubbles:` | :toothbrush: | `:toothbrush:` | [top](#table-of-contents) | -| [top](#objects) | :sponge: | `:sponge:` | :fire_extinguisher: | `:fire_extinguisher:` | [top](#table-of-contents) | -| [top](#objects) | :shopping_cart: | `:shopping_cart:` | | | [top](#table-of-contents) | - -### Other Object - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#objects) | :smoking: | `:smoking:` | :coffin: | `:coffin:` | [top](#table-of-contents) | -| [top](#objects) | :headstone: | `:headstone:` | :funeral_urn: | `:funeral_urn:` | [top](#table-of-contents) | -| [top](#objects) | :nazar_amulet: | `:nazar_amulet:` | :hamsa: | `:hamsa:` | [top](#table-of-contents) | -| [top](#objects) | :moyai: | `:moyai:` | :placard: | `:placard:` | [top](#table-of-contents) | -| [top](#objects) | :identification_card: | `:identification_card:` | | | [top](#table-of-contents) | - -## Symbols - -- [Transport Sign](#transport-sign) -- [Warning](#warning) -- [Arrow](#arrow) -- [Religion](#religion) -- [Zodiac](#zodiac) -- [Av Symbol](#av-symbol) -- [Gender](#gender) -- [Math](#math) -- [Punctuation](#punctuation) -- [Currency](#currency) -- [Other Symbol](#other-symbol) -- [Keycap](#keycap) -- [Alphanum](#alphanum) -- [Geometric](#geometric) - -### Transport Sign - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#symbols) | :atm: | `:atm:` | :put_litter_in_its_place: | `:put_litter_in_its_place:` | [top](#table-of-contents) | -| [top](#symbols) | :potable_water: | `:potable_water:` | :wheelchair: | `:wheelchair:` | [top](#table-of-contents) | -| [top](#symbols) | :mens: | `:mens:` | :womens: | `:womens:` | [top](#table-of-contents) | -| [top](#symbols) | :restroom: | `:restroom:` | :baby_symbol: | `:baby_symbol:` | [top](#table-of-contents) | -| [top](#symbols) | :wc: | `:wc:` | :passport_control: | `:passport_control:` | [top](#table-of-contents) | -| [top](#symbols) | :customs: | `:customs:` | :baggage_claim: | `:baggage_claim:` | [top](#table-of-contents) | -| [top](#symbols) | :left_luggage: | `:left_luggage:` | | | [top](#table-of-contents) | - -### Warning - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#symbols) | :warning: | `:warning:` | :children_crossing: | `:children_crossing:` | [top](#table-of-contents) | -| [top](#symbols) | :no_entry: | `:no_entry:` | :no_entry_sign: | `:no_entry_sign:` | [top](#table-of-contents) | -| [top](#symbols) | :no_bicycles: | `:no_bicycles:` | :no_smoking: | `:no_smoking:` | [top](#table-of-contents) | -| [top](#symbols) | :do_not_litter: | `:do_not_litter:` | :non-potable_water: | `:non-potable_water:` | [top](#table-of-contents) | -| [top](#symbols) | :no_pedestrians: | `:no_pedestrians:` | :no_mobile_phones: | `:no_mobile_phones:` | [top](#table-of-contents) | -| [top](#symbols) | :underage: | `:underage:` | :radioactive: | `:radioactive:` | [top](#table-of-contents) | -| [top](#symbols) | :biohazard: | `:biohazard:` | | | [top](#table-of-contents) | - -### Arrow - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#symbols) | :arrow_up: | `:arrow_up:` | :arrow_upper_right: | `:arrow_upper_right:` | [top](#table-of-contents) | -| [top](#symbols) | :arrow_right: | `:arrow_right:` | :arrow_lower_right: | `:arrow_lower_right:` | [top](#table-of-contents) | -| [top](#symbols) | :arrow_down: | `:arrow_down:` | :arrow_lower_left: | `:arrow_lower_left:` | [top](#table-of-contents) | -| [top](#symbols) | :arrow_left: | `:arrow_left:` | :arrow_upper_left: | `:arrow_upper_left:` | [top](#table-of-contents) | -| [top](#symbols) | :arrow_up_down: | `:arrow_up_down:` | :left_right_arrow: | `:left_right_arrow:` | [top](#table-of-contents) | -| [top](#symbols) | :leftwards_arrow_with_hook: | `:leftwards_arrow_with_hook:` | :arrow_right_hook: | `:arrow_right_hook:` | [top](#table-of-contents) | -| [top](#symbols) | :arrow_heading_up: | `:arrow_heading_up:` | :arrow_heading_down: | `:arrow_heading_down:` | [top](#table-of-contents) | -| [top](#symbols) | :arrows_clockwise: | `:arrows_clockwise:` | :arrows_counterclockwise: | `:arrows_counterclockwise:` | [top](#table-of-contents) | -| [top](#symbols) | :back: | `:back:` | :end: | `:end:` | [top](#table-of-contents) | -| [top](#symbols) | :on: | `:on:` | :soon: | `:soon:` | [top](#table-of-contents) | -| [top](#symbols) | :top: | `:top:` | | | [top](#table-of-contents) | - -### Religion - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#symbols) | :place_of_worship: | `:place_of_worship:` | :atom_symbol: | `:atom_symbol:` | [top](#table-of-contents) | -| [top](#symbols) | :om: | `:om:` | :star_of_david: | `:star_of_david:` | [top](#table-of-contents) | -| [top](#symbols) | :wheel_of_dharma: | `:wheel_of_dharma:` | :yin_yang: | `:yin_yang:` | [top](#table-of-contents) | -| [top](#symbols) | :latin_cross: | `:latin_cross:` | :orthodox_cross: | `:orthodox_cross:` | [top](#table-of-contents) | -| [top](#symbols) | :star_and_crescent: | `:star_and_crescent:` | :peace_symbol: | `:peace_symbol:` | [top](#table-of-contents) | -| [top](#symbols) | :menorah: | `:menorah:` | :six_pointed_star: | `:six_pointed_star:` | [top](#table-of-contents) | -| [top](#symbols) | :khanda: | `:khanda:` | | | [top](#table-of-contents) | - -### Zodiac - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#symbols) | :aries: | `:aries:` | :taurus: | `:taurus:` | [top](#table-of-contents) | -| [top](#symbols) | :gemini: | `:gemini:` | :cancer: | `:cancer:` | [top](#table-of-contents) | -| [top](#symbols) | :leo: | `:leo:` | :virgo: | `:virgo:` | [top](#table-of-contents) | -| [top](#symbols) | :libra: | `:libra:` | :scorpius: | `:scorpius:` | [top](#table-of-contents) | -| [top](#symbols) | :sagittarius: | `:sagittarius:` | :capricorn: | `:capricorn:` | [top](#table-of-contents) | -| [top](#symbols) | :aquarius: | `:aquarius:` | :pisces: | `:pisces:` | [top](#table-of-contents) | -| [top](#symbols) | :ophiuchus: | `:ophiuchus:` | | | [top](#table-of-contents) | - -### Av Symbol - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#symbols) | :twisted_rightwards_arrows: | `:twisted_rightwards_arrows:` | :repeat: | `:repeat:` | [top](#table-of-contents) | -| [top](#symbols) | :repeat_one: | `:repeat_one:` | :arrow_forward: | `:arrow_forward:` | [top](#table-of-contents) | -| [top](#symbols) | :fast_forward: | `:fast_forward:` | :next_track_button: | `:next_track_button:` | [top](#table-of-contents) | -| [top](#symbols) | :play_or_pause_button: | `:play_or_pause_button:` | :arrow_backward: | `:arrow_backward:` | [top](#table-of-contents) | -| [top](#symbols) | :rewind: | `:rewind:` | :previous_track_button: | `:previous_track_button:` | [top](#table-of-contents) | -| [top](#symbols) | :arrow_up_small: | `:arrow_up_small:` | :arrow_double_up: | `:arrow_double_up:` | [top](#table-of-contents) | -| [top](#symbols) | :arrow_down_small: | `:arrow_down_small:` | :arrow_double_down: | `:arrow_double_down:` | [top](#table-of-contents) | -| [top](#symbols) | :pause_button: | `:pause_button:` | :stop_button: | `:stop_button:` | [top](#table-of-contents) | -| [top](#symbols) | :record_button: | `:record_button:` | :eject_button: | `:eject_button:` | [top](#table-of-contents) | -| [top](#symbols) | :cinema: | `:cinema:` | :low_brightness: | `:low_brightness:` | [top](#table-of-contents) | -| [top](#symbols) | :high_brightness: | `:high_brightness:` | :signal_strength: | `:signal_strength:` | [top](#table-of-contents) | -| [top](#symbols) | :wireless: | `:wireless:` | :vibration_mode: | `:vibration_mode:` | [top](#table-of-contents) | -| [top](#symbols) | :mobile_phone_off: | `:mobile_phone_off:` | | | [top](#table-of-contents) | - -### Gender - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#symbols) | :female_sign: | `:female_sign:` | :male_sign: | `:male_sign:` | [top](#table-of-contents) | -| [top](#symbols) | :transgender_symbol: | `:transgender_symbol:` | | | [top](#table-of-contents) | - -### Math - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#symbols) | :heavy_multiplication_x: | `:heavy_multiplication_x:` | :heavy_plus_sign: | `:heavy_plus_sign:` | [top](#table-of-contents) | -| [top](#symbols) | :heavy_minus_sign: | `:heavy_minus_sign:` | :heavy_division_sign: | `:heavy_division_sign:` | [top](#table-of-contents) | -| [top](#symbols) | :heavy_equals_sign: | `:heavy_equals_sign:` | :infinity: | `:infinity:` | [top](#table-of-contents) | - -### Punctuation - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#symbols) | :bangbang: | `:bangbang:` | :interrobang: | `:interrobang:` | [top](#table-of-contents) | -| [top](#symbols) | :question: | `:question:` | :grey_question: | `:grey_question:` | [top](#table-of-contents) | -| [top](#symbols) | :grey_exclamation: | `:grey_exclamation:` | :exclamation: | `:exclamation:` `:heavy_exclamation_mark:` | [top](#table-of-contents) | -| [top](#symbols) | :wavy_dash: | `:wavy_dash:` | | | [top](#table-of-contents) | - -### Currency - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#symbols) | :currency_exchange: | `:currency_exchange:` | :heavy_dollar_sign: | `:heavy_dollar_sign:` | [top](#table-of-contents) | - -### Other Symbol - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#symbols) | :medical_symbol: | `:medical_symbol:` | :recycle: | `:recycle:` | [top](#table-of-contents) | -| [top](#symbols) | :fleur_de_lis: | `:fleur_de_lis:` | :trident: | `:trident:` | [top](#table-of-contents) | -| [top](#symbols) | :name_badge: | `:name_badge:` | :beginner: | `:beginner:` | [top](#table-of-contents) | -| [top](#symbols) | :o: | `:o:` | :white_check_mark: | `:white_check_mark:` | [top](#table-of-contents) | -| [top](#symbols) | :ballot_box_with_check: | `:ballot_box_with_check:` | :heavy_check_mark: | `:heavy_check_mark:` | [top](#table-of-contents) | -| [top](#symbols) | :x: | `:x:` | :negative_squared_cross_mark: | `:negative_squared_cross_mark:` | [top](#table-of-contents) | -| [top](#symbols) | :curly_loop: | `:curly_loop:` | :loop: | `:loop:` | [top](#table-of-contents) | -| [top](#symbols) | :part_alternation_mark: | `:part_alternation_mark:` | :eight_spoked_asterisk: | `:eight_spoked_asterisk:` | [top](#table-of-contents) | -| [top](#symbols) | :eight_pointed_black_star: | `:eight_pointed_black_star:` | :sparkle: | `:sparkle:` | [top](#table-of-contents) | -| [top](#symbols) | :copyright: | `:copyright:` | :registered: | `:registered:` | [top](#table-of-contents) | -| [top](#symbols) | :tm: | `:tm:` | | | [top](#table-of-contents) | - -### Keycap - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#symbols) | :hash: | `:hash:` | :asterisk: | `:asterisk:` | [top](#table-of-contents) | -| [top](#symbols) | :zero: | `:zero:` | :one: | `:one:` | [top](#table-of-contents) | -| [top](#symbols) | :two: | `:two:` | :three: | `:three:` | [top](#table-of-contents) | -| [top](#symbols) | :four: | `:four:` | :five: | `:five:` | [top](#table-of-contents) | -| [top](#symbols) | :six: | `:six:` | :seven: | `:seven:` | [top](#table-of-contents) | -| [top](#symbols) | :eight: | `:eight:` | :nine: | `:nine:` | [top](#table-of-contents) | -| [top](#symbols) | :keycap_ten: | `:keycap_ten:` | | | [top](#table-of-contents) | - -### Alphanum - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#symbols) | :capital_abcd: | `:capital_abcd:` | :abcd: | `:abcd:` | [top](#table-of-contents) | -| [top](#symbols) | :1234: | `:1234:` | :symbols: | `:symbols:` | [top](#table-of-contents) | -| [top](#symbols) | :abc: | `:abc:` | :a: | `:a:` | [top](#table-of-contents) | -| [top](#symbols) | :ab: | `:ab:` | :b: | `:b:` | [top](#table-of-contents) | -| [top](#symbols) | :cl: | `:cl:` | :cool: | `:cool:` | [top](#table-of-contents) | -| [top](#symbols) | :free: | `:free:` | :information_source: | `:information_source:` | [top](#table-of-contents) | -| [top](#symbols) | :id: | `:id:` | :m: | `:m:` | [top](#table-of-contents) | -| [top](#symbols) | :new: | `:new:` | :ng: | `:ng:` | [top](#table-of-contents) | -| [top](#symbols) | :o2: | `:o2:` | :ok: | `:ok:` | [top](#table-of-contents) | -| [top](#symbols) | :parking: | `:parking:` | :sos: | `:sos:` | [top](#table-of-contents) | -| [top](#symbols) | :up: | `:up:` | :vs: | `:vs:` | [top](#table-of-contents) | -| [top](#symbols) | :koko: | `:koko:` | :sa: | `:sa:` | [top](#table-of-contents) | -| [top](#symbols) | :u6708: | `:u6708:` | :u6709: | `:u6709:` | [top](#table-of-contents) | -| [top](#symbols) | :u6307: | `:u6307:` | :ideograph_advantage: | `:ideograph_advantage:` | [top](#table-of-contents) | -| [top](#symbols) | :u5272: | `:u5272:` | :u7121: | `:u7121:` | [top](#table-of-contents) | -| [top](#symbols) | :u7981: | `:u7981:` | :accept: | `:accept:` | [top](#table-of-contents) | -| [top](#symbols) | :u7533: | `:u7533:` | :u5408: | `:u5408:` | [top](#table-of-contents) | -| [top](#symbols) | :u7a7a: | `:u7a7a:` | :congratulations: | `:congratulations:` | [top](#table-of-contents) | -| [top](#symbols) | :secret: | `:secret:` | :u55b6: | `:u55b6:` | [top](#table-of-contents) | -| [top](#symbols) | :u6e80: | `:u6e80:` | | | [top](#table-of-contents) | - -### Geometric - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#symbols) | :red_circle: | `:red_circle:` | :orange_circle: | `:orange_circle:` | [top](#table-of-contents) | -| [top](#symbols) | :yellow_circle: | `:yellow_circle:` | :green_circle: | `:green_circle:` | [top](#table-of-contents) | -| [top](#symbols) | :large_blue_circle: | `:large_blue_circle:` | :purple_circle: | `:purple_circle:` | [top](#table-of-contents) | -| [top](#symbols) | :brown_circle: | `:brown_circle:` | :black_circle: | `:black_circle:` | [top](#table-of-contents) | -| [top](#symbols) | :white_circle: | `:white_circle:` | :red_square: | `:red_square:` | [top](#table-of-contents) | -| [top](#symbols) | :orange_square: | `:orange_square:` | :yellow_square: | `:yellow_square:` | [top](#table-of-contents) | -| [top](#symbols) | :green_square: | `:green_square:` | :blue_square: | `:blue_square:` | [top](#table-of-contents) | -| [top](#symbols) | :purple_square: | `:purple_square:` | :brown_square: | `:brown_square:` | [top](#table-of-contents) | -| [top](#symbols) | :black_large_square: | `:black_large_square:` | :white_large_square: | `:white_large_square:` | [top](#table-of-contents) | -| [top](#symbols) | :black_medium_square: | `:black_medium_square:` | :white_medium_square: | `:white_medium_square:` | [top](#table-of-contents) | -| [top](#symbols) | :black_medium_small_square: | `:black_medium_small_square:` | :white_medium_small_square: | `:white_medium_small_square:` | [top](#table-of-contents) | -| [top](#symbols) | :black_small_square: | `:black_small_square:` | :white_small_square: | `:white_small_square:` | [top](#table-of-contents) | -| [top](#symbols) | :large_orange_diamond: | `:large_orange_diamond:` | :large_blue_diamond: | `:large_blue_diamond:` | [top](#table-of-contents) | -| [top](#symbols) | :small_orange_diamond: | `:small_orange_diamond:` | :small_blue_diamond: | `:small_blue_diamond:` | [top](#table-of-contents) | -| [top](#symbols) | :small_red_triangle: | `:small_red_triangle:` | :small_red_triangle_down: | `:small_red_triangle_down:` | [top](#table-of-contents) | -| [top](#symbols) | :diamond_shape_with_a_dot_inside: | `:diamond_shape_with_a_dot_inside:` | :radio_button: | `:radio_button:` | [top](#table-of-contents) | -| [top](#symbols) | :white_square_button: | `:white_square_button:` | :black_square_button: | `:black_square_button:` | [top](#table-of-contents) | - -## Flags - -- [Flag](#flag) -- [Country Flag](#country-flag) -- [Subdivision Flag](#subdivision-flag) - -### Flag - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#flags) | :checkered_flag: | `:checkered_flag:` | :triangular_flag_on_post: | `:triangular_flag_on_post:` | [top](#table-of-contents) | -| [top](#flags) | :crossed_flags: | `:crossed_flags:` | :black_flag: | `:black_flag:` | [top](#table-of-contents) | -| [top](#flags) | :white_flag: | `:white_flag:` | :rainbow_flag: | `:rainbow_flag:` | [top](#table-of-contents) | -| [top](#flags) | :transgender_flag: | `:transgender_flag:` | :pirate_flag: | `:pirate_flag:` | [top](#table-of-contents) | - -### Country Flag - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#flags) | :ascension_island: | `:ascension_island:` | :andorra: | `:andorra:` | [top](#table-of-contents) | -| [top](#flags) | :united_arab_emirates: | `:united_arab_emirates:` | :afghanistan: | `:afghanistan:` | [top](#table-of-contents) | -| [top](#flags) | :antigua_barbuda: | `:antigua_barbuda:` | :anguilla: | `:anguilla:` | [top](#table-of-contents) | -| [top](#flags) | :albania: | `:albania:` | :armenia: | `:armenia:` | [top](#table-of-contents) | -| [top](#flags) | :angola: | `:angola:` | :antarctica: | `:antarctica:` | [top](#table-of-contents) | -| [top](#flags) | :argentina: | `:argentina:` | :american_samoa: | `:american_samoa:` | [top](#table-of-contents) | -| [top](#flags) | :austria: | `:austria:` | :australia: | `:australia:` | [top](#table-of-contents) | -| [top](#flags) | :aruba: | `:aruba:` | :aland_islands: | `:aland_islands:` | [top](#table-of-contents) | -| [top](#flags) | :azerbaijan: | `:azerbaijan:` | :bosnia_herzegovina: | `:bosnia_herzegovina:` | [top](#table-of-contents) | -| [top](#flags) | :barbados: | `:barbados:` | :bangladesh: | `:bangladesh:` | [top](#table-of-contents) | -| [top](#flags) | :belgium: | `:belgium:` | :burkina_faso: | `:burkina_faso:` | [top](#table-of-contents) | -| [top](#flags) | :bulgaria: | `:bulgaria:` | :bahrain: | `:bahrain:` | [top](#table-of-contents) | -| [top](#flags) | :burundi: | `:burundi:` | :benin: | `:benin:` | [top](#table-of-contents) | -| [top](#flags) | :st_barthelemy: | `:st_barthelemy:` | :bermuda: | `:bermuda:` | [top](#table-of-contents) | -| [top](#flags) | :brunei: | `:brunei:` | :bolivia: | `:bolivia:` | [top](#table-of-contents) | -| [top](#flags) | :caribbean_netherlands: | `:caribbean_netherlands:` | :brazil: | `:brazil:` | [top](#table-of-contents) | -| [top](#flags) | :bahamas: | `:bahamas:` | :bhutan: | `:bhutan:` | [top](#table-of-contents) | -| [top](#flags) | :bouvet_island: | `:bouvet_island:` | :botswana: | `:botswana:` | [top](#table-of-contents) | -| [top](#flags) | :belarus: | `:belarus:` | :belize: | `:belize:` | [top](#table-of-contents) | -| [top](#flags) | :canada: | `:canada:` | :cocos_islands: | `:cocos_islands:` | [top](#table-of-contents) | -| [top](#flags) | :congo_kinshasa: | `:congo_kinshasa:` | :central_african_republic: | `:central_african_republic:` | [top](#table-of-contents) | -| [top](#flags) | :congo_brazzaville: | `:congo_brazzaville:` | :switzerland: | `:switzerland:` | [top](#table-of-contents) | -| [top](#flags) | :cote_divoire: | `:cote_divoire:` | :cook_islands: | `:cook_islands:` | [top](#table-of-contents) | -| [top](#flags) | :chile: | `:chile:` | :cameroon: | `:cameroon:` | [top](#table-of-contents) | -| [top](#flags) | :cn: | `:cn:` | :colombia: | `:colombia:` | [top](#table-of-contents) | -| [top](#flags) | :clipperton_island: | `:clipperton_island:` | :costa_rica: | `:costa_rica:` | [top](#table-of-contents) | -| [top](#flags) | :cuba: | `:cuba:` | :cape_verde: | `:cape_verde:` | [top](#table-of-contents) | -| [top](#flags) | :curacao: | `:curacao:` | :christmas_island: | `:christmas_island:` | [top](#table-of-contents) | -| [top](#flags) | :cyprus: | `:cyprus:` | :czech_republic: | `:czech_republic:` | [top](#table-of-contents) | -| [top](#flags) | :de: | `:de:` | :diego_garcia: | `:diego_garcia:` | [top](#table-of-contents) | -| [top](#flags) | :djibouti: | `:djibouti:` | :denmark: | `:denmark:` | [top](#table-of-contents) | -| [top](#flags) | :dominica: | `:dominica:` | :dominican_republic: | `:dominican_republic:` | [top](#table-of-contents) | -| [top](#flags) | :algeria: | `:algeria:` | :ceuta_melilla: | `:ceuta_melilla:` | [top](#table-of-contents) | -| [top](#flags) | :ecuador: | `:ecuador:` | :estonia: | `:estonia:` | [top](#table-of-contents) | -| [top](#flags) | :egypt: | `:egypt:` | :western_sahara: | `:western_sahara:` | [top](#table-of-contents) | -| [top](#flags) | :eritrea: | `:eritrea:` | :es: | `:es:` | [top](#table-of-contents) | -| [top](#flags) | :ethiopia: | `:ethiopia:` | :eu: | `:eu:` `:european_union:` | [top](#table-of-contents) | -| [top](#flags) | :finland: | `:finland:` | :fiji: | `:fiji:` | [top](#table-of-contents) | -| [top](#flags) | :falkland_islands: | `:falkland_islands:` | :micronesia: | `:micronesia:` | [top](#table-of-contents) | -| [top](#flags) | :faroe_islands: | `:faroe_islands:` | :fr: | `:fr:` | [top](#table-of-contents) | -| [top](#flags) | :gabon: | `:gabon:` | :gb: | `:gb:` `:uk:` | [top](#table-of-contents) | -| [top](#flags) | :grenada: | `:grenada:` | :georgia: | `:georgia:` | [top](#table-of-contents) | -| [top](#flags) | :french_guiana: | `:french_guiana:` | :guernsey: | `:guernsey:` | [top](#table-of-contents) | -| [top](#flags) | :ghana: | `:ghana:` | :gibraltar: | `:gibraltar:` | [top](#table-of-contents) | -| [top](#flags) | :greenland: | `:greenland:` | :gambia: | `:gambia:` | [top](#table-of-contents) | -| [top](#flags) | :guinea: | `:guinea:` | :guadeloupe: | `:guadeloupe:` | [top](#table-of-contents) | -| [top](#flags) | :equatorial_guinea: | `:equatorial_guinea:` | :greece: | `:greece:` | [top](#table-of-contents) | -| [top](#flags) | :south_georgia_south_sandwich_islands: | `:south_georgia_south_sandwich_islands:` | :guatemala: | `:guatemala:` | [top](#table-of-contents) | -| [top](#flags) | :guam: | `:guam:` | :guinea_bissau: | `:guinea_bissau:` | [top](#table-of-contents) | -| [top](#flags) | :guyana: | `:guyana:` | :hong_kong: | `:hong_kong:` | [top](#table-of-contents) | -| [top](#flags) | :heard_mcdonald_islands: | `:heard_mcdonald_islands:` | :honduras: | `:honduras:` | [top](#table-of-contents) | -| [top](#flags) | :croatia: | `:croatia:` | :haiti: | `:haiti:` | [top](#table-of-contents) | -| [top](#flags) | :hungary: | `:hungary:` | :canary_islands: | `:canary_islands:` | [top](#table-of-contents) | -| [top](#flags) | :indonesia: | `:indonesia:` | :ireland: | `:ireland:` | [top](#table-of-contents) | -| [top](#flags) | :israel: | `:israel:` | :isle_of_man: | `:isle_of_man:` | [top](#table-of-contents) | -| [top](#flags) | :india: | `:india:` | :british_indian_ocean_territory: | `:british_indian_ocean_territory:` | [top](#table-of-contents) | -| [top](#flags) | :iraq: | `:iraq:` | :iran: | `:iran:` | [top](#table-of-contents) | -| [top](#flags) | :iceland: | `:iceland:` | :it: | `:it:` | [top](#table-of-contents) | -| [top](#flags) | :jersey: | `:jersey:` | :jamaica: | `:jamaica:` | [top](#table-of-contents) | -| [top](#flags) | :jordan: | `:jordan:` | :jp: | `:jp:` | [top](#table-of-contents) | -| [top](#flags) | :kenya: | `:kenya:` | :kyrgyzstan: | `:kyrgyzstan:` | [top](#table-of-contents) | -| [top](#flags) | :cambodia: | `:cambodia:` | :kiribati: | `:kiribati:` | [top](#table-of-contents) | -| [top](#flags) | :comoros: | `:comoros:` | :st_kitts_nevis: | `:st_kitts_nevis:` | [top](#table-of-contents) | -| [top](#flags) | :north_korea: | `:north_korea:` | :kr: | `:kr:` | [top](#table-of-contents) | -| [top](#flags) | :kuwait: | `:kuwait:` | :cayman_islands: | `:cayman_islands:` | [top](#table-of-contents) | -| [top](#flags) | :kazakhstan: | `:kazakhstan:` | :laos: | `:laos:` | [top](#table-of-contents) | -| [top](#flags) | :lebanon: | `:lebanon:` | :st_lucia: | `:st_lucia:` | [top](#table-of-contents) | -| [top](#flags) | :liechtenstein: | `:liechtenstein:` | :sri_lanka: | `:sri_lanka:` | [top](#table-of-contents) | -| [top](#flags) | :liberia: | `:liberia:` | :lesotho: | `:lesotho:` | [top](#table-of-contents) | -| [top](#flags) | :lithuania: | `:lithuania:` | :luxembourg: | `:luxembourg:` | [top](#table-of-contents) | -| [top](#flags) | :latvia: | `:latvia:` | :libya: | `:libya:` | [top](#table-of-contents) | -| [top](#flags) | :morocco: | `:morocco:` | :monaco: | `:monaco:` | [top](#table-of-contents) | -| [top](#flags) | :moldova: | `:moldova:` | :montenegro: | `:montenegro:` | [top](#table-of-contents) | -| [top](#flags) | :st_martin: | `:st_martin:` | :madagascar: | `:madagascar:` | [top](#table-of-contents) | -| [top](#flags) | :marshall_islands: | `:marshall_islands:` | :macedonia: | `:macedonia:` | [top](#table-of-contents) | -| [top](#flags) | :mali: | `:mali:` | :myanmar: | `:myanmar:` | [top](#table-of-contents) | -| [top](#flags) | :mongolia: | `:mongolia:` | :macau: | `:macau:` | [top](#table-of-contents) | -| [top](#flags) | :northern_mariana_islands: | `:northern_mariana_islands:` | :martinique: | `:martinique:` | [top](#table-of-contents) | -| [top](#flags) | :mauritania: | `:mauritania:` | :montserrat: | `:montserrat:` | [top](#table-of-contents) | -| [top](#flags) | :malta: | `:malta:` | :mauritius: | `:mauritius:` | [top](#table-of-contents) | -| [top](#flags) | :maldives: | `:maldives:` | :malawi: | `:malawi:` | [top](#table-of-contents) | -| [top](#flags) | :mexico: | `:mexico:` | :malaysia: | `:malaysia:` | [top](#table-of-contents) | -| [top](#flags) | :mozambique: | `:mozambique:` | :namibia: | `:namibia:` | [top](#table-of-contents) | -| [top](#flags) | :new_caledonia: | `:new_caledonia:` | :niger: | `:niger:` | [top](#table-of-contents) | -| [top](#flags) | :norfolk_island: | `:norfolk_island:` | :nigeria: | `:nigeria:` | [top](#table-of-contents) | -| [top](#flags) | :nicaragua: | `:nicaragua:` | :netherlands: | `:netherlands:` | [top](#table-of-contents) | -| [top](#flags) | :norway: | `:norway:` | :nepal: | `:nepal:` | [top](#table-of-contents) | -| [top](#flags) | :nauru: | `:nauru:` | :niue: | `:niue:` | [top](#table-of-contents) | -| [top](#flags) | :new_zealand: | `:new_zealand:` | :oman: | `:oman:` | [top](#table-of-contents) | -| [top](#flags) | :panama: | `:panama:` | :peru: | `:peru:` | [top](#table-of-contents) | -| [top](#flags) | :french_polynesia: | `:french_polynesia:` | :papua_new_guinea: | `:papua_new_guinea:` | [top](#table-of-contents) | -| [top](#flags) | :philippines: | `:philippines:` | :pakistan: | `:pakistan:` | [top](#table-of-contents) | -| [top](#flags) | :poland: | `:poland:` | :st_pierre_miquelon: | `:st_pierre_miquelon:` | [top](#table-of-contents) | -| [top](#flags) | :pitcairn_islands: | `:pitcairn_islands:` | :puerto_rico: | `:puerto_rico:` | [top](#table-of-contents) | -| [top](#flags) | :palestinian_territories: | `:palestinian_territories:` | :portugal: | `:portugal:` | [top](#table-of-contents) | -| [top](#flags) | :palau: | `:palau:` | :paraguay: | `:paraguay:` | [top](#table-of-contents) | -| [top](#flags) | :qatar: | `:qatar:` | :reunion: | `:reunion:` | [top](#table-of-contents) | -| [top](#flags) | :romania: | `:romania:` | :serbia: | `:serbia:` | [top](#table-of-contents) | -| [top](#flags) | :ru: | `:ru:` | :rwanda: | `:rwanda:` | [top](#table-of-contents) | -| [top](#flags) | :saudi_arabia: | `:saudi_arabia:` | :solomon_islands: | `:solomon_islands:` | [top](#table-of-contents) | -| [top](#flags) | :seychelles: | `:seychelles:` | :sudan: | `:sudan:` | [top](#table-of-contents) | -| [top](#flags) | :sweden: | `:sweden:` | :singapore: | `:singapore:` | [top](#table-of-contents) | -| [top](#flags) | :st_helena: | `:st_helena:` | :slovenia: | `:slovenia:` | [top](#table-of-contents) | -| [top](#flags) | :svalbard_jan_mayen: | `:svalbard_jan_mayen:` | :slovakia: | `:slovakia:` | [top](#table-of-contents) | -| [top](#flags) | :sierra_leone: | `:sierra_leone:` | :san_marino: | `:san_marino:` | [top](#table-of-contents) | -| [top](#flags) | :senegal: | `:senegal:` | :somalia: | `:somalia:` | [top](#table-of-contents) | -| [top](#flags) | :suriname: | `:suriname:` | :south_sudan: | `:south_sudan:` | [top](#table-of-contents) | -| [top](#flags) | :sao_tome_principe: | `:sao_tome_principe:` | :el_salvador: | `:el_salvador:` | [top](#table-of-contents) | -| [top](#flags) | :sint_maarten: | `:sint_maarten:` | :syria: | `:syria:` | [top](#table-of-contents) | -| [top](#flags) | :swaziland: | `:swaziland:` | :tristan_da_cunha: | `:tristan_da_cunha:` | [top](#table-of-contents) | -| [top](#flags) | :turks_caicos_islands: | `:turks_caicos_islands:` | :chad: | `:chad:` | [top](#table-of-contents) | -| [top](#flags) | :french_southern_territories: | `:french_southern_territories:` | :togo: | `:togo:` | [top](#table-of-contents) | -| [top](#flags) | :thailand: | `:thailand:` | :tajikistan: | `:tajikistan:` | [top](#table-of-contents) | -| [top](#flags) | :tokelau: | `:tokelau:` | :timor_leste: | `:timor_leste:` | [top](#table-of-contents) | -| [top](#flags) | :turkmenistan: | `:turkmenistan:` | :tunisia: | `:tunisia:` | [top](#table-of-contents) | -| [top](#flags) | :tonga: | `:tonga:` | :tr: | `:tr:` | [top](#table-of-contents) | -| [top](#flags) | :trinidad_tobago: | `:trinidad_tobago:` | :tuvalu: | `:tuvalu:` | [top](#table-of-contents) | -| [top](#flags) | :taiwan: | `:taiwan:` | :tanzania: | `:tanzania:` | [top](#table-of-contents) | -| [top](#flags) | :ukraine: | `:ukraine:` | :uganda: | `:uganda:` | [top](#table-of-contents) | -| [top](#flags) | :us_outlying_islands: | `:us_outlying_islands:` | :united_nations: | `:united_nations:` | [top](#table-of-contents) | -| [top](#flags) | :us: | `:us:` | :uruguay: | `:uruguay:` | [top](#table-of-contents) | -| [top](#flags) | :uzbekistan: | `:uzbekistan:` | :vatican_city: | `:vatican_city:` | [top](#table-of-contents) | -| [top](#flags) | :st_vincent_grenadines: | `:st_vincent_grenadines:` | :venezuela: | `:venezuela:` | [top](#table-of-contents) | -| [top](#flags) | :british_virgin_islands: | `:british_virgin_islands:` | :us_virgin_islands: | `:us_virgin_islands:` | [top](#table-of-contents) | -| [top](#flags) | :vietnam: | `:vietnam:` | :vanuatu: | `:vanuatu:` | [top](#table-of-contents) | -| [top](#flags) | :wallis_futuna: | `:wallis_futuna:` | :samoa: | `:samoa:` | [top](#table-of-contents) | -| [top](#flags) | :kosovo: | `:kosovo:` | :yemen: | `:yemen:` | [top](#table-of-contents) | -| [top](#flags) | :mayotte: | `:mayotte:` | :south_africa: | `:south_africa:` | [top](#table-of-contents) | -| [top](#flags) | :zambia: | `:zambia:` | :zimbabwe: | `:zimbabwe:` | [top](#table-of-contents) | - -### Subdivision Flag - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#flags) | :england: | `:england:` | :scotland: | `:scotland:` | [top](#table-of-contents) | -| [top](#flags) | :wales: | `:wales:` | | | [top](#table-of-contents) | - -## GitHub Custom Emoji - -| | ico | shortcode | ico | shortcode | | -| - | :-: | - | :-: | - | - | -| [top](#github-custom-emoji) | :accessibility: | `:accessibility:` | :atom: | `:atom:` | [top](#table-of-contents) | -| [top](#github-custom-emoji) | :basecamp: | `:basecamp:` | :basecampy: | `:basecampy:` | [top](#table-of-contents) | -| [top](#github-custom-emoji) | :bowtie: | `:bowtie:` | :dependabot: | `:dependabot:` | [top](#table-of-contents) | -| [top](#github-custom-emoji) | :electron: | `:electron:` | :feelsgood: | `:feelsgood:` | [top](#table-of-contents) | -| [top](#github-custom-emoji) | :finnadie: | `:finnadie:` | :fishsticks: | `:fishsticks:` | [top](#table-of-contents) | -| [top](#github-custom-emoji) | :goberserk: | `:goberserk:` | :godmode: | `:godmode:` | [top](#table-of-contents) | -| [top](#github-custom-emoji) | :hurtrealbad: | `:hurtrealbad:` | :neckbeard: | `:neckbeard:` | [top](#table-of-contents) | -| [top](#github-custom-emoji) | :octocat: | `:octocat:` | :rage1: | `:rage1:` | [top](#table-of-contents) | -| [top](#github-custom-emoji) | :rage2: | `:rage2:` | :rage3: | `:rage3:` | [top](#table-of-contents) | -| [top](#github-custom-emoji) | :rage4: | `:rage4:` | :shipit: | `:shipit:` | [top](#table-of-contents) | -| [top](#github-custom-emoji) | :suspect: | `:suspect:` | :trollface: | `:trollface:` | [top](#table-of-contents) | diff --git a/docs/content/en/quick-reference/functions.md b/docs/content/en/quick-reference/functions.md deleted file mode 100644 index 72235d0f3..000000000 --- a/docs/content/en/quick-reference/functions.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Functions -description: A quick reference guide to Hugo's functions, grouped by namespace. Aliases, if any, appear in parentheses to the right of the function name. -categories: [] -keywords: [] ---- - -{{% quick-reference section="functions" %}} diff --git a/docs/content/en/quick-reference/glossary/_index.md b/docs/content/en/quick-reference/glossary/_index.md deleted file mode 100644 index 42894c4da..000000000 --- a/docs/content/en/quick-reference/glossary/_index.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: Glossary -description: Terms commonly used throughout the documentation. -categories: [] -keywords: [] -build: - render: always - list: always -cascade: - build: - render: never - list: local -layout: single -params: - hide_in_this_section: true -aliases: [/getting-started/glossary/] ---- - -{{% glossary %}} diff --git a/docs/content/en/quick-reference/glossary/action.md b/docs/content/en/quick-reference/glossary/action.md deleted file mode 100644 index ced877327..000000000 --- a/docs/content/en/quick-reference/glossary/action.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: action ---- - -See [_template action_](g). diff --git a/docs/content/en/quick-reference/glossary/archetype.md b/docs/content/en/quick-reference/glossary/archetype.md deleted file mode 100644 index 231089c56..000000000 --- a/docs/content/en/quick-reference/glossary/archetype.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: archetype -details: /content-management/archetypes ---- - -An _archetype_ is a template for new content. diff --git a/docs/content/en/quick-reference/glossary/argument.md b/docs/content/en/quick-reference/glossary/argument.md deleted file mode 100644 index 912951d2b..000000000 --- a/docs/content/en/quick-reference/glossary/argument.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: argument ---- - -An _argument_ is a [_scalar_](g), [_array_](g), [_slice_](g), [_map_](g), or [_object_](g) passed to a [_function_](g), [_method_](g), or [_shortcode_](g). diff --git a/docs/content/en/quick-reference/glossary/array.md b/docs/content/en/quick-reference/glossary/array.md deleted file mode 100644 index 0df45f212..000000000 --- a/docs/content/en/quick-reference/glossary/array.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: array -reference: https://go.dev/ref/spec#Array_types ---- - -An _array_ is a numbered sequence of [_elements_](g). Unlike Go's [_slice_](g) data type, an array has a fixed length. Elements within an array can be [_scalars_](g), slices, [_maps_](g), pages, or other arrays. diff --git a/docs/content/en/quick-reference/glossary/asset-pipeline.md b/docs/content/en/quick-reference/glossary/asset-pipeline.md deleted file mode 100644 index 5f3264a6e..000000000 --- a/docs/content/en/quick-reference/glossary/asset-pipeline.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: asset pipeline ---- - -An _asset pipeline_ is a system that automates and optimizes the handling of static assets like images, stylesheets, and JavaScript files. diff --git a/docs/content/en/quick-reference/glossary/bool.md b/docs/content/en/quick-reference/glossary/bool.md deleted file mode 100644 index a4f33b5b9..000000000 --- a/docs/content/en/quick-reference/glossary/bool.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: bool ---- - -See [_boolean_](g). diff --git a/docs/content/en/quick-reference/glossary/boolean.md b/docs/content/en/quick-reference/glossary/boolean.md deleted file mode 100644 index e727c40b0..000000000 --- a/docs/content/en/quick-reference/glossary/boolean.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: boolean ---- - -A _boolean_ is a data type with two possible values, either `true` or `false`. diff --git a/docs/content/en/quick-reference/glossary/branch-bundle.md b/docs/content/en/quick-reference/glossary/branch-bundle.md deleted file mode 100644 index d5688ba0b..000000000 --- a/docs/content/en/quick-reference/glossary/branch-bundle.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: branch bundle -reference: /content-management/page-bundles ---- - -A _branch bundle_ is a top-level content directory or any content directory containing an `_index.md` file. Analogous to a physical branch, a branch bundle may have descendants including [_leaf bundles_](g) and other branch bundles. A branch bundle may also contain [_page resources_](g) such as images. diff --git a/docs/content/en/quick-reference/glossary/build.md b/docs/content/en/quick-reference/glossary/build.md deleted file mode 100644 index 79b7ec74a..000000000 --- a/docs/content/en/quick-reference/glossary/build.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: build ---- - -To _build_ a site is to generate HTML files and assets such as images, CSS files, and JavaScript files. The build process includes rendering and resource transformations. diff --git a/docs/content/en/quick-reference/glossary/bundle.md b/docs/content/en/quick-reference/glossary/bundle.md deleted file mode 100644 index f30187811..000000000 --- a/docs/content/en/quick-reference/glossary/bundle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: bundle ---- - -See [page bundle](g). diff --git a/docs/content/en/quick-reference/glossary/cache.md b/docs/content/en/quick-reference/glossary/cache.md deleted file mode 100644 index a86068e4a..000000000 --- a/docs/content/en/quick-reference/glossary/cache.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: cache ---- - -A _cache_ is a software component that stores data so that future requests for the same data are faster. diff --git a/docs/content/en/quick-reference/glossary/chain.md b/docs/content/en/quick-reference/glossary/chain.md deleted file mode 100644 index bbbc14f49..000000000 --- a/docs/content/en/quick-reference/glossary/chain.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: chain ---- - -To _chain_ is to connect one or more [_identifiers_](g) with a dot. An identifier can represent a [_method_](g), [_object_](g), or [_field_](g). For example, `.Site.Params.author.name` or `.Date.UTC.Hour`. diff --git a/docs/content/en/quick-reference/glossary/cicd.md b/docs/content/en/quick-reference/glossary/cicd.md deleted file mode 100644 index 355097b1b..000000000 --- a/docs/content/en/quick-reference/glossary/cicd.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: CI/CD -params: - reference: https://en.wikipedia.org/wiki/CI/CD ---- - -The term _CI/CD_ is an abbreviation for Continuous Integration and Continuous Delivery or Continuous Deployment depending on context. diff --git a/docs/content/en/quick-reference/glossary/cjk.md b/docs/content/en/quick-reference/glossary/cjk.md deleted file mode 100644 index 05a294d44..000000000 --- a/docs/content/en/quick-reference/glossary/cjk.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: CJK ---- - -_CJK_ is a collective term for the Chinese, Japanese, and Korean languages. diff --git a/docs/content/en/quick-reference/glossary/cli.md b/docs/content/en/quick-reference/glossary/cli.md deleted file mode 100644 index 8f898e364..000000000 --- a/docs/content/en/quick-reference/glossary/cli.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: CLI ---- - -_CLI_ stands for command-line interface, a text-based method for interacting with computer programs or operating systems. diff --git a/docs/content/en/quick-reference/glossary/collection.md b/docs/content/en/quick-reference/glossary/collection.md deleted file mode 100644 index 30e1ef805..000000000 --- a/docs/content/en/quick-reference/glossary/collection.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: collection ---- - -A _collection_ is an [_array_](g), [_slice_](g), or [_map_](g). diff --git a/docs/content/en/quick-reference/glossary/content-adapter.md b/docs/content/en/quick-reference/glossary/content-adapter.md deleted file mode 100644 index 974e61dca..000000000 --- a/docs/content/en/quick-reference/glossary/content-adapter.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: content adapter -reference: /content-management/content-adapters ---- - -A _content adapter_ is a template that dynamically creates pages when building a site. For example, use a content adapter to create pages from a remote data source such as JSON, TOML, YAML, or XML. diff --git a/docs/content/en/quick-reference/glossary/content-format.md b/docs/content/en/quick-reference/glossary/content-format.md deleted file mode 100644 index ea459deb7..000000000 --- a/docs/content/en/quick-reference/glossary/content-format.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: content format -reference: /content-management/formats ---- - -A _content format_ is a markup language for creating content. Typically Markdown, but may also be HTML, AsciiDoc, Org, Pandoc, or reStructuredText. diff --git a/docs/content/en/quick-reference/glossary/content-type.md b/docs/content/en/quick-reference/glossary/content-type.md deleted file mode 100644 index b7ebc5be4..000000000 --- a/docs/content/en/quick-reference/glossary/content-type.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: content type -reference: /content-management/types ---- - -A _content type_ is a classification of content inferred from the top-level directory name or the `type` set in [front matter](g). Pages in the root of the `content` directory, including the home page, are of type "page". Accessed via `.Page.Type` in [_templates_](g). diff --git a/docs/content/en/quick-reference/glossary/content-view.md b/docs/content/en/quick-reference/glossary/content-view.md deleted file mode 100644 index 009981cb5..000000000 --- a/docs/content/en/quick-reference/glossary/content-view.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: content view -reference: /templates/content-view ---- - -A _content view_ is a template called with the [`Render`](/methods/page/render/) method on a `Page` object. diff --git a/docs/content/en/quick-reference/glossary/context.md b/docs/content/en/quick-reference/glossary/context.md deleted file mode 100644 index 75afc709d..000000000 --- a/docs/content/en/quick-reference/glossary/context.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: context -reference: /templates/introduction/#context ---- - -Represented by a dot (`.`) within a [_template action_](g), _context_ is the current location in a data structure. For example, while iterating over a [_collection_](g) of pages, the context within each iteration is the page's data structure. The context received by each template depends on template type and/or how it was called. diff --git a/docs/content/en/quick-reference/glossary/default-sort-order.md b/docs/content/en/quick-reference/glossary/default-sort-order.md deleted file mode 100644 index 9b981a7e9..000000000 --- a/docs/content/en/quick-reference/glossary/default-sort-order.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: default sort order ---- - -The _default sort order_ for [_page collections_](g), used when no other criteria are set, follows this priority: - - 1. [`weight`](/content-management/front-matter/#weight) (ascending) - 1. [`date`](/content-management/front-matter/#date) (descending) - 1. [`linkTitle`](/content-management/front-matter/#linktitle) falling back to [`title`](/content-management/front-matter/#title) (ascending) - 1. [logical path](g) (ascending) diff --git a/docs/content/en/quick-reference/glossary/duration.md b/docs/content/en/quick-reference/glossary/duration.md deleted file mode 100644 index 21fd3c832..000000000 --- a/docs/content/en/quick-reference/glossary/duration.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: duration ---- - -A _duration_ is a data type that represent a length of time, expressed using units such as seconds (represented by `s`), minutes (represented by `m`), and hours (represented by `h`). For example, `42s` means 42 seconds, `6m7s` means 6 minutes and 7 seconds, and `6h7m42s` means 6 hours, 7 minutes, and 42 seconds. diff --git a/docs/content/en/quick-reference/glossary/element.md b/docs/content/en/quick-reference/glossary/element.md deleted file mode 100644 index 39f5df656..000000000 --- a/docs/content/en/quick-reference/glossary/element.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: element ---- - -An _element_ is a member of a [_slice_](g) or [_array_](g). diff --git a/docs/content/en/quick-reference/glossary/embedded-template.md b/docs/content/en/quick-reference/glossary/embedded-template.md deleted file mode 100644 index 3a0871690..000000000 --- a/docs/content/en/quick-reference/glossary/embedded-template.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: embedded template ---- - -An _embedded template_ is a built-in component within the Hugo application. This includes features like [_partials_](g), [_shortcodes_](g), and [_render hooks_](g) that provide pre-defined structures or functionalities for creating website content. diff --git a/docs/content/en/quick-reference/glossary/environment.md b/docs/content/en/quick-reference/glossary/environment.md deleted file mode 100644 index ebba0ccdf..000000000 --- a/docs/content/en/quick-reference/glossary/environment.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: environment ---- - -Typically one of `development`, `staging`, or `production`, each _environment_ may exhibit different behavior depending on configuration and template logic. For example, in a production environment you might minify and fingerprint CSS, but that probably doesn't make sense in a development environment. - - When running the built-in development server with the `hugo server` command, the environment is set to `development`. When building your site with the `hugo` command, the environment is set to `production`. To override the environment value, use the `--environment` command line flag or the `HUGO_ENVIRONMENT` environment variable. - - To determine the current environment within a template, use the [`hugo.Environment`](/functions/hugo/environment/) function. diff --git a/docs/content/en/quick-reference/glossary/field.md b/docs/content/en/quick-reference/glossary/field.md deleted file mode 100644 index a32eb3a6b..000000000 --- a/docs/content/en/quick-reference/glossary/field.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: field ---- - -A _field_ is a predefined key-value pair in front matter such as `date` or `title`. diff --git a/docs/content/en/quick-reference/glossary/flag.md b/docs/content/en/quick-reference/glossary/flag.md deleted file mode 100644 index e7b6c5746..000000000 --- a/docs/content/en/quick-reference/glossary/flag.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: flag -reference: /commands/hugo ---- - -A _flag_ is an option passed to a command-line program, beginning with one or two hyphens. diff --git a/docs/content/en/quick-reference/glossary/float.md b/docs/content/en/quick-reference/glossary/float.md deleted file mode 100644 index 86f2c8ffb..000000000 --- a/docs/content/en/quick-reference/glossary/float.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: float -alias: true ---- - -See [floating point](g). diff --git a/docs/content/en/quick-reference/glossary/floating-point.md b/docs/content/en/quick-reference/glossary/floating-point.md deleted file mode 100644 index 38ba9f012..000000000 --- a/docs/content/en/quick-reference/glossary/floating-point.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: floating point ---- - -The term _floating point_ refers to a numeric data type with a fractional component. For example, `3.14159`. diff --git a/docs/content/en/quick-reference/glossary/fragment.md b/docs/content/en/quick-reference/glossary/fragment.md deleted file mode 100644 index 57ef1b4ef..000000000 --- a/docs/content/en/quick-reference/glossary/fragment.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: fragment ---- - -A _fragment_ is the final segment of a URL, beginning with a hash (`#`) mark, that references an `id` attribute of an HTML element on the page. diff --git a/docs/content/en/quick-reference/glossary/front-matter.md b/docs/content/en/quick-reference/glossary/front-matter.md deleted file mode 100644 index 5a3cd3040..000000000 --- a/docs/content/en/quick-reference/glossary/front-matter.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: front matter -reference: /content-management/front-matter ---- - -The term _front matter_ refers to the metadata at the beginning of each content page, separated from the content by format-specific delimiters. diff --git a/docs/content/en/quick-reference/glossary/function.md b/docs/content/en/quick-reference/glossary/function.md deleted file mode 100644 index a7da52cd2..000000000 --- a/docs/content/en/quick-reference/glossary/function.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: function -reference: /functions ---- - -Used within a [_template action_](g), a _function_ takes one or more [_arguments_](g) and returns a value. Unlike [_methods_](g), functions are not associated with an [_object_](g). diff --git a/docs/content/en/quick-reference/glossary/glob.md b/docs/content/en/quick-reference/glossary/glob.md deleted file mode 100644 index bb9c5a4d8..000000000 --- a/docs/content/en/quick-reference/glossary/glob.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: glob -reference: https://github.com/gobwas/glob?tab=readme-ov-file#example ---- - -A _glob_ is a pattern used to match file names and paths. It's a shorthand for specifying a set of files, making it easier to work with multiple files at once. diff --git a/docs/content/en/quick-reference/glossary/global-resource.md b/docs/content/en/quick-reference/glossary/global-resource.md deleted file mode 100644 index a4df65f67..000000000 --- a/docs/content/en/quick-reference/glossary/global-resource.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: global resource ---- - -A _global resource_ is file within the `assets` directory, or within any directory mounted to the `assets` directory. diff --git a/docs/content/en/quick-reference/glossary/headless-bundle.md b/docs/content/en/quick-reference/glossary/headless-bundle.md deleted file mode 100644 index ac7bf79c8..000000000 --- a/docs/content/en/quick-reference/glossary/headless-bundle.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: headless bundle -reference: /content-management/build-options/ ---- - -A _headless bundle_ is an unpublished [_leaf bundle_](g) or an unpublished [_branch bundle_](g) whose content and resources you can include in other pages. diff --git a/docs/content/en/quick-reference/glossary/i18n.md b/docs/content/en/quick-reference/glossary/i18n.md deleted file mode 100644 index 168828aa8..000000000 --- a/docs/content/en/quick-reference/glossary/i18n.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: i18n ---- - -See [_internationalization_](g). diff --git a/docs/content/en/quick-reference/glossary/iana.md b/docs/content/en/quick-reference/glossary/iana.md deleted file mode 100644 index 89497f76a..000000000 --- a/docs/content/en/quick-reference/glossary/iana.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: IANA -reference: https://www.iana.org/about ---- - -_IANA_ is an abbreviation for the Internet Assigned Numbers Authority, a non-profit organization that manages the allocation of global IP addresses, autonomous system numbers, DNS root zone, media types, and other Internet Protocol-related resources. diff --git a/docs/content/en/quick-reference/glossary/identifier.md b/docs/content/en/quick-reference/glossary/identifier.md deleted file mode 100644 index f53472fa7..000000000 --- a/docs/content/en/quick-reference/glossary/identifier.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: identifier ---- - -An _identifier_ is a string that represents a variable, method, object, or field. It must conform to Go's [language specification](https://go.dev/ref/spec#Identifiers), beginning with a letter or underscore, followed by zero or more letters, digits, or underscores. diff --git a/docs/content/en/quick-reference/glossary/int.md b/docs/content/en/quick-reference/glossary/int.md deleted file mode 100644 index 0896f1ce7..000000000 --- a/docs/content/en/quick-reference/glossary/int.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: int ---- - -See [_integer_](g). diff --git a/docs/content/en/quick-reference/glossary/integer.md b/docs/content/en/quick-reference/glossary/integer.md deleted file mode 100644 index 0af821300..000000000 --- a/docs/content/en/quick-reference/glossary/integer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: integer ---- - -An _integer_ is a numeric data type without a fractional component. For example, `42`. diff --git a/docs/content/en/quick-reference/glossary/internationalization.md b/docs/content/en/quick-reference/glossary/internationalization.md deleted file mode 100644 index 4e8b0199c..000000000 --- a/docs/content/en/quick-reference/glossary/internationalization.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: internationalization ---- - -The term _internationalization_ refers to software design and development efforts that enable [_localization_](g). diff --git a/docs/content/en/quick-reference/glossary/interpreted-string-literal.md b/docs/content/en/quick-reference/glossary/interpreted-string-literal.md deleted file mode 100644 index a5d2cd326..000000000 --- a/docs/content/en/quick-reference/glossary/interpreted-string-literal.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: interpreted string literal -reference: https://go.dev/ref/spec#String_literals ---- - -An _interpreted string literal_ is a character sequences between double quotes, as in "foo". Within the quotes, any character may appear except a newline and an unescaped double quote. The text between the quotes forms the value of the literal, with backslash escapes interpreted. diff --git a/docs/content/en/quick-reference/glossary/interval.md b/docs/content/en/quick-reference/glossary/interval.md deleted file mode 100644 index a7ed8a6cd..000000000 --- a/docs/content/en/quick-reference/glossary/interval.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: interval ---- - -An [_interval_](https://en.wikipedia.org/wiki/Interval_(mathematics)) is a range of numbers between two endpoints: closed, open, or half-open. - - - A _closed interval_, denoted by brackets, includes its endpoints. For example, [0, 1] is the interval where `0 <= x <= 1`. - - - An _open interval_, denoted by parentheses, excludes its endpoints. For example, (0, 1) is the interval where `0 < x < 1`. - - - A _half-open interval_ includes only one of its endpoints. For example, (0, 1] is the _left-open_ interval where `0 < x <= 1`, while [0, 1) is the _right-open_ interval where `0 <= x < 1`. diff --git a/docs/content/en/quick-reference/glossary/kind.md b/docs/content/en/quick-reference/glossary/kind.md deleted file mode 100644 index a214dfbf1..000000000 --- a/docs/content/en/quick-reference/glossary/kind.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: kind ---- - -See [_page kind_](g). diff --git a/docs/content/en/quick-reference/glossary/l10n.md b/docs/content/en/quick-reference/glossary/l10n.md deleted file mode 100644 index 013d0ab55..000000000 --- a/docs/content/en/quick-reference/glossary/l10n.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: l10n ---- - -See [_localization_](g). diff --git a/docs/content/en/quick-reference/glossary/layout.md b/docs/content/en/quick-reference/glossary/layout.md deleted file mode 100644 index a4e6b33f1..000000000 --- a/docs/content/en/quick-reference/glossary/layout.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: layout ---- - -See [_template_](g). diff --git a/docs/content/en/quick-reference/glossary/leaf-bundle.md b/docs/content/en/quick-reference/glossary/leaf-bundle.md deleted file mode 100644 index aa41384ee..000000000 --- a/docs/content/en/quick-reference/glossary/leaf-bundle.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: leaf bundle -reference: /content-management/page-bundles/ ---- - -A _leaf bundle_ is a directory that contains an `index.md` file and zero or more [_resources_](g). Analogous to a physical leaf, a leaf bundle is at the end of a [_branch bundle_](g). It has no descendants. diff --git a/docs/content/en/quick-reference/glossary/lexer.md b/docs/content/en/quick-reference/glossary/lexer.md deleted file mode 100644 index a46223148..000000000 --- a/docs/content/en/quick-reference/glossary/lexer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: lexer ---- - -A _lexer_ is a software component that identifies keywords, identifiers, operators, numbers, and other basic building blocks of a programming language within the input text. diff --git a/docs/content/en/quick-reference/glossary/list-page.md b/docs/content/en/quick-reference/glossary/list-page.md deleted file mode 100644 index b58ee4a35..000000000 --- a/docs/content/en/quick-reference/glossary/list-page.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: list page ---- - -A list page is any [_page kind_](g) that receives a page [_collection_](g) in [_context_](g). This includes the home page, [section pages](g), [taxonomy pages](g), and [term pages](g). diff --git a/docs/content/en/quick-reference/glossary/list-template.md b/docs/content/en/quick-reference/glossary/list-template.md deleted file mode 100644 index 9a12a275f..000000000 --- a/docs/content/en/quick-reference/glossary/list-template.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: list template ---- - -A _list template_ is any [_template_](g) that renders a [_list page_](g). This includes home, [_section_](g), [_taxonomy_](g), and [_term_](g) templates. diff --git a/docs/content/en/quick-reference/glossary/localization.md b/docs/content/en/quick-reference/glossary/localization.md deleted file mode 100644 index 01f450635..000000000 --- a/docs/content/en/quick-reference/glossary/localization.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: localization -reference: /content-management/multilingual/ ---- - -The term _localization_ refers to the process of adapting a site to meet language and regional requirements. This includes translations, date formats, number formats, currency formats, and collation order. diff --git a/docs/content/en/quick-reference/glossary/logical-path.md b/docs/content/en/quick-reference/glossary/logical-path.md deleted file mode 100644 index 2eeee751f..000000000 --- a/docs/content/en/quick-reference/glossary/logical-path.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: logical path -reference: /methods/page/path/#examples ---- - -A _logical path_ is a page or page resource identifier derived from the file path, excluding its extension and language identifier. This value is neither a file path nor a URL. Starting with a file path relative to the `content` directory, Hugo determines the logical path by stripping the file extension and language identifier, converting to lower case, then replacing spaces with hyphens. Path segments are separated with a slash (`/`). diff --git a/docs/content/en/quick-reference/glossary/map.md b/docs/content/en/quick-reference/glossary/map.md deleted file mode 100644 index b4605d20b..000000000 --- a/docs/content/en/quick-reference/glossary/map.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: map -reference: https://go.dev/ref/spec#Map_types ---- - -A _map_ is an unordered group of elements, each indexed by a unique key. diff --git a/docs/content/en/quick-reference/glossary/markdown-attribute.md b/docs/content/en/quick-reference/glossary/markdown-attribute.md deleted file mode 100644 index f5c57c728..000000000 --- a/docs/content/en/quick-reference/glossary/markdown-attribute.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Markdown attribute -reference: /content-management/markdown-attributes/ ---- - -A _Markdown attribute_ is a key-value pair attached to a Markdown element. These attributes are commonly used to add HTML attributes, like `class` and `id`, to the element when it's rendered into HTML. They provide a way to extend the basic Markdown syntax and add more semantic meaning or styling hooks to your content. diff --git a/docs/content/en/quick-reference/glossary/marshal.md b/docs/content/en/quick-reference/glossary/marshal.md deleted file mode 100644 index 3bb1623f5..000000000 --- a/docs/content/en/quick-reference/glossary/marshal.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: marshal -reference: /functions/transform/remarshal/ ---- - -To _marshal_ is to transform a data structure into a serialized object. For example, transforming a [_map_](g) into a JSON string. diff --git a/docs/content/en/quick-reference/glossary/media-type.md b/docs/content/en/quick-reference/glossary/media-type.md deleted file mode 100644 index 2994360ef..000000000 --- a/docs/content/en/quick-reference/glossary/media-type.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: media type -reference: /configuration/media-types/ ---- - -A _media type_ (formerly known as a MIME type) is a two-part identifier for file formats and transmitted content. For example, the media type for JSON data is `application/json`. diff --git a/docs/content/en/quick-reference/glossary/method.md b/docs/content/en/quick-reference/glossary/method.md deleted file mode 100644 index 634cd4b97..000000000 --- a/docs/content/en/quick-reference/glossary/method.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: method ---- - -Used within a [_template action_](g) and associated with an [_object_](g), a _method_ takes zero or more [_arguments_](g) and either returns a value or performs an action. For example, `IsHome` is a method on a `Page` object which returns `true` if the current page is the home page. See also [_function_](g). diff --git a/docs/content/en/quick-reference/glossary/module.md b/docs/content/en/quick-reference/glossary/module.md deleted file mode 100644 index 90c8ff80b..000000000 --- a/docs/content/en/quick-reference/glossary/module.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: module -reference: /hugo-modules/ ---- - -A _module_ is a packaged combination of [_archetypes_](g), assets, content, data, [_templates_](g), translation tables, static files, or configuration settings. A module may serve as the basis for a new site, or to augment an existing site. diff --git a/docs/content/en/quick-reference/glossary/node.md b/docs/content/en/quick-reference/glossary/node.md deleted file mode 100644 index b722e07c8..000000000 --- a/docs/content/en/quick-reference/glossary/node.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: node ---- - -A _node_ is a class of [_page kinds_](g) including `home`, `section`, `taxonomy`, and `term`. diff --git a/docs/content/en/quick-reference/glossary/noop.md b/docs/content/en/quick-reference/glossary/noop.md deleted file mode 100644 index bd159bb26..000000000 --- a/docs/content/en/quick-reference/glossary/noop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: noop ---- - -An abbreviated form of "no operation", a _noop_ is a statement that does nothing. diff --git a/docs/content/en/quick-reference/glossary/object.md b/docs/content/en/quick-reference/glossary/object.md deleted file mode 100644 index 216609d0d..000000000 --- a/docs/content/en/quick-reference/glossary/object.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: object ---- - -An _object_ is a data structure with or without associated [_methods_](g). diff --git a/docs/content/en/quick-reference/glossary/ordered-taxonomy.md b/docs/content/en/quick-reference/glossary/ordered-taxonomy.md deleted file mode 100644 index 9b6c1d92c..000000000 --- a/docs/content/en/quick-reference/glossary/ordered-taxonomy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: ordered taxonomy ---- - -Created by invoking the [`Alphabetical`](/methods/taxonomy/alphabetical/) or [`ByCount`](/methods/taxonomy/bycount/) method on a [`Taxonomy`](g) object, which is a [_map_](g), an _ordered taxonomy_ is a [_slice_](g), where each element is an object that contains the [_term_](g) and a slice of its [_weighted pages_](g). diff --git a/docs/content/en/quick-reference/glossary/output-format.md b/docs/content/en/quick-reference/glossary/output-format.md deleted file mode 100644 index df042d06b..000000000 --- a/docs/content/en/quick-reference/glossary/output-format.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: output format -reference: /configuration/output-formats/ ---- - -An _output format_ is a collection of settings that defines how Hugo renders a file when building a site. For example, `html`, `json`, and `rss` are built-in output formats. You can create multiple output formats and control their generation based on [page kind](g), or by enabling one or more output formats for specific pages. diff --git a/docs/content/en/quick-reference/glossary/page-bundle.md b/docs/content/en/quick-reference/glossary/page-bundle.md deleted file mode 100644 index af76da2aa..000000000 --- a/docs/content/en/quick-reference/glossary/page-bundle.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: page bundle -reference: /content-management/page-bundles/ ---- - -A _page bundle_ is a directory that encapsulates both content and associated [_resources_](g). There are two types of page bundles: [_leaf bundles_](g) and [_branch bundles_](g). diff --git a/docs/content/en/quick-reference/glossary/page-collection.md b/docs/content/en/quick-reference/glossary/page-collection.md deleted file mode 100644 index f078ecf27..000000000 --- a/docs/content/en/quick-reference/glossary/page-collection.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: page collection ---- - -A _page collection_ is a slice of `Page` objects. diff --git a/docs/content/en/quick-reference/glossary/page-kind.md b/docs/content/en/quick-reference/glossary/page-kind.md deleted file mode 100644 index ee47fe010..000000000 --- a/docs/content/en/quick-reference/glossary/page-kind.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: page kind -reference: /methods/page/kind/ ---- - -A _page kind_ is a classification of pages, one of `home`, `page`, `section`, `taxonomy`, or `term`. diff --git a/docs/content/en/quick-reference/glossary/page-resource.md b/docs/content/en/quick-reference/glossary/page-resource.md deleted file mode 100644 index dab119eb4..000000000 --- a/docs/content/en/quick-reference/glossary/page-resource.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: page resource ---- - -A _page resource_ is a file within a [_page bundle_](g). diff --git a/docs/content/en/quick-reference/glossary/pager.md b/docs/content/en/quick-reference/glossary/pager.md deleted file mode 100644 index f58874e4a..000000000 --- a/docs/content/en/quick-reference/glossary/pager.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: pager ---- - -Created during [_pagination_](g), a _pager_ contains a subset of a list page and navigation links to other pagers. diff --git a/docs/content/en/quick-reference/glossary/paginate.md b/docs/content/en/quick-reference/glossary/paginate.md deleted file mode 100644 index d2467e098..000000000 --- a/docs/content/en/quick-reference/glossary/paginate.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: paginate ---- - -To _paginate_ is to split a list page into two or more subsets. diff --git a/docs/content/en/quick-reference/glossary/pagination.md b/docs/content/en/quick-reference/glossary/pagination.md deleted file mode 100644 index 136341dbf..000000000 --- a/docs/content/en/quick-reference/glossary/pagination.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: pagination -reference: /templates/pagination ---- - -The term _pagination_ refers to the process of [_paginating_](g) a list page. diff --git a/docs/content/en/quick-reference/glossary/paginator.md b/docs/content/en/quick-reference/glossary/paginator.md deleted file mode 100644 index 4d197348d..000000000 --- a/docs/content/en/quick-reference/glossary/paginator.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: paginator ---- - -A _paginator_ is a collection of [_pagers_](g). diff --git a/docs/content/en/quick-reference/glossary/parameter.md b/docs/content/en/quick-reference/glossary/parameter.md deleted file mode 100644 index f1a45ea34..000000000 --- a/docs/content/en/quick-reference/glossary/parameter.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: parameter ---- - -A _parameter_ is typically a user-defined key-value pair at the site or page level, but may also refer to a configuration setting or an [_argument_](g). diff --git a/docs/content/en/quick-reference/glossary/partial.md b/docs/content/en/quick-reference/glossary/partial.md deleted file mode 100644 index a5dd5e51a..000000000 --- a/docs/content/en/quick-reference/glossary/partial.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: partial ---- - -A _partial_ is a [_template_](g) called from any other template including [_shortcodes_](g), [render hooks](g), and other partials. A partial either renders something or returns something. A partial can also call itself, for example, to [_walk_](g) a data structure. diff --git a/docs/content/en/quick-reference/glossary/permalink.md b/docs/content/en/quick-reference/glossary/permalink.md deleted file mode 100644 index 45651de83..000000000 --- a/docs/content/en/quick-reference/glossary/permalink.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: permalink ---- - -A _permalink_ is the absolute URL of a published resource or a rendered page, including scheme and host. diff --git a/docs/content/en/quick-reference/glossary/pipe.md b/docs/content/en/quick-reference/glossary/pipe.md deleted file mode 100644 index f587eeccf..000000000 --- a/docs/content/en/quick-reference/glossary/pipe.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: pipe ---- - -See [_pipeline_](g). diff --git a/docs/content/en/quick-reference/glossary/pipeline.md b/docs/content/en/quick-reference/glossary/pipeline.md deleted file mode 100644 index 6dab52578..000000000 --- a/docs/content/en/quick-reference/glossary/pipeline.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: pipeline ---- - -Within a [_template action_](g), a _pipeline_ is a possibly chained sequence of values, [_function_](g) calls, or [_method_](g) calls. Functions and methods in the pipeline may take multiple [_arguments_](g). - - A pipeline may be chained by separating a sequence of commands with pipeline characters (`|`). In a chained pipeline, the result of each command is passed as the last argument to the following command. The output of the final command in the pipeline is the value of the pipeline. diff --git a/docs/content/en/quick-reference/glossary/pretty-url.md b/docs/content/en/quick-reference/glossary/pretty-url.md deleted file mode 100644 index b15ef9f0f..000000000 --- a/docs/content/en/quick-reference/glossary/pretty-url.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: pretty URL ---- - -A _pretty URL_ is a URL that does not include a file extension. diff --git a/docs/content/en/quick-reference/glossary/primary-output-format.md b/docs/content/en/quick-reference/glossary/primary-output-format.md deleted file mode 100644 index c40c0176c..000000000 --- a/docs/content/en/quick-reference/glossary/primary-output-format.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: primary output format -details: /configuration/outputs/ ---- - -A _primary output format_ defines the default URL returned by the [`Permalink`] and [`RelPermalink`] methods for a given [page kind](g). It is specified as the first entry within the [outputs configuration] for that page kind. - -[`Permalink`]: /methods/page/permalink/ -[`RelPermalink`]: /methods/page/relpermalink/ -[outputs configuration]: /configuration/outputs/ diff --git a/docs/content/en/quick-reference/glossary/publish.md b/docs/content/en/quick-reference/glossary/publish.md deleted file mode 100644 index 743e87a3e..000000000 --- a/docs/content/en/quick-reference/glossary/publish.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: publish ---- - -See [_build_](g). diff --git a/docs/content/en/quick-reference/glossary/raw-string-literal.md b/docs/content/en/quick-reference/glossary/raw-string-literal.md deleted file mode 100644 index 7465c6837..000000000 --- a/docs/content/en/quick-reference/glossary/raw-string-literal.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: raw string literal -reference: https://go.dev/ref/spec#String_literals ---- - -A _raw string literal_ is a character sequences between backticks, as in \`bar\`. Within the backticks, any character may appear except a backtick. Backslashes have no special meaning and the string may contain newlines. Carriage return characters (`\r`) inside raw string literals are discarded from the raw string value. diff --git a/docs/content/en/quick-reference/glossary/regular-expression.md b/docs/content/en/quick-reference/glossary/regular-expression.md deleted file mode 100644 index 222f289d9..000000000 --- a/docs/content/en/quick-reference/glossary/regular-expression.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: regular expression -reference: ---- - -A _regular expression_, also known as a _regex_, is a sequence of characters that defines a search pattern. Use the [RE2 syntax] when defining regular expressions in your templates or site configuration. - - [RE2 syntax]: https://github.com/google/re2/wiki/syntax diff --git a/docs/content/en/quick-reference/glossary/regular-page.md b/docs/content/en/quick-reference/glossary/regular-page.md deleted file mode 100644 index 084408e57..000000000 --- a/docs/content/en/quick-reference/glossary/regular-page.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: regular page ---- - -A _regular page_ is a page with the "page" [_page kind_](g). See also [_section page_](g). diff --git a/docs/content/en/quick-reference/glossary/relative-permalink.md b/docs/content/en/quick-reference/glossary/relative-permalink.md deleted file mode 100644 index a272133a3..000000000 --- a/docs/content/en/quick-reference/glossary/relative-permalink.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: relative permalink ---- - -A _relative permalink_ is the host-relative URL of a published resource or a rendered page. diff --git a/docs/content/en/quick-reference/glossary/remote-resource.md b/docs/content/en/quick-reference/glossary/remote-resource.md deleted file mode 100644 index 163749763..000000000 --- a/docs/content/en/quick-reference/glossary/remote-resource.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: remote resource ---- - -A _remote resource_ is a file on a remote server, accessible via HTTP or HTTPS. diff --git a/docs/content/en/quick-reference/glossary/render-hook.md b/docs/content/en/quick-reference/glossary/render-hook.md deleted file mode 100644 index 2cdb98e05..000000000 --- a/docs/content/en/quick-reference/glossary/render-hook.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: render hook -reference: /render-hooks ---- - -A _render hook_ is a [_template_](g) that overrides standard Markdown rendering. diff --git a/docs/content/en/quick-reference/glossary/resource-type.md b/docs/content/en/quick-reference/glossary/resource-type.md deleted file mode 100644 index 9a2412fb9..000000000 --- a/docs/content/en/quick-reference/glossary/resource-type.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: resource type ---- - -A _resource type_ is the main type of a resource's [media type](/methods/resource/mediatype/). Content files such as Markdown, HTML, AsciiDoc, Pandoc, reStructuredText, and Emacs Org Mode have resource type `page`. Other resource types include `image`, `text`, `video`, and others. Retrieve the resource type using the [`ResourceType`](/methods/resource/resourcetype/) method on a `Resource` object. diff --git a/docs/content/en/quick-reference/glossary/resource.md b/docs/content/en/quick-reference/glossary/resource.md deleted file mode 100644 index 0864bc43d..000000000 --- a/docs/content/en/quick-reference/glossary/resource.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: resource ---- - -A _resource_ is any file consumed by the build process to augment or generate content, structure, behavior, or presentation. For example: images, videos, content snippets, CSS, Sass, JavaScript, and data. - - Hugo supports three types of resources: [_global resources_](g), [_page resources_](g), and [_remote resources_](g). diff --git a/docs/content/en/quick-reference/glossary/scalar.md b/docs/content/en/quick-reference/glossary/scalar.md deleted file mode 100644 index 63ae27d3f..000000000 --- a/docs/content/en/quick-reference/glossary/scalar.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: scalar ---- - -A _scalar_ is a single value, one of [_string_](g), [_integer_](g), [floating point](g), or [_boolean_](g). diff --git a/docs/content/en/quick-reference/glossary/scope.md b/docs/content/en/quick-reference/glossary/scope.md deleted file mode 100644 index 5a13312a6..000000000 --- a/docs/content/en/quick-reference/glossary/scope.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: scope ---- - -The term _scope_ refers to the specific region of code where a [_variable_](g) or [_object_](g) is accessible. For example, a variable initialized in one [template](g) is not available within another. diff --git a/docs/content/en/quick-reference/glossary/scratch-pad.md b/docs/content/en/quick-reference/glossary/scratch-pad.md deleted file mode 100644 index 4792ce12e..000000000 --- a/docs/content/en/quick-reference/glossary/scratch-pad.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: scratch pad ---- - -Conceptually, a _scratch pad_ is a [_map_](g) with [_methods_](g) to set, get, update, and delete values. Attach the data structure to a `Page` or `Site` object using the [`Store`](/methods/page/store/) method, or create a locally scoped scratch pad using the [`newScratch`](/functions/collections/newscratch/) function. diff --git a/docs/content/en/quick-reference/glossary/section-page.md b/docs/content/en/quick-reference/glossary/section-page.md deleted file mode 100644 index fd84ed5f2..000000000 --- a/docs/content/en/quick-reference/glossary/section-page.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: section page ---- - -A _section page_ is a page with the "section" [_page kind_](g). Typically a listing of [_regular pages_](g) and/or other section pages within the current [_section_](g). diff --git a/docs/content/en/quick-reference/glossary/section.md b/docs/content/en/quick-reference/glossary/section.md deleted file mode 100644 index 45d1203e0..000000000 --- a/docs/content/en/quick-reference/glossary/section.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: section ---- - -A _section_ is a top-level content directory or any content directory containing an `_index.md` file. diff --git a/docs/content/en/quick-reference/glossary/segment.md b/docs/content/en/quick-reference/glossary/segment.md deleted file mode 100644 index 5851a8825..000000000 --- a/docs/content/en/quick-reference/glossary/segment.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: segment ---- - -A _segment_ is a subset of a site, filtered by [_logical path_](g), language, [_page kind_](g), or [_output format_](g). diff --git a/docs/content/en/quick-reference/glossary/shortcode.md b/docs/content/en/quick-reference/glossary/shortcode.md deleted file mode 100644 index a6503ea63..000000000 --- a/docs/content/en/quick-reference/glossary/shortcode.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: shortcode -reference: /content-management/shortcodes ---- - -A _shortcode_ is a [_template_](g) invoked within markup, accepting any number of [_arguments_](g). They can be used with any [_content format_](g) to insert elements such as videos, images, and social media embeds into your content. diff --git a/docs/content/en/quick-reference/glossary/slice.md b/docs/content/en/quick-reference/glossary/slice.md deleted file mode 100644 index 5d83bed6b..000000000 --- a/docs/content/en/quick-reference/glossary/slice.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: slice -reference: https://go.dev/ref/spec#Slice_types ---- - -A _slice_ is a numbered sequence of elements. Unlike Go's [_array_](g) data type, slices are dynamically sized. [_Elements_](g) within a slice can be [_scalars_](g), [_arrays_](g), [_maps_](g), pages, or other slices. diff --git a/docs/content/en/quick-reference/glossary/string.md b/docs/content/en/quick-reference/glossary/string.md deleted file mode 100644 index 54ed4c1d1..000000000 --- a/docs/content/en/quick-reference/glossary/string.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: string ---- - -A _string_ is a sequence of bytes. For example, `"What is 6 times 7?"`. diff --git a/docs/content/en/quick-reference/glossary/taxonomic-weight.md b/docs/content/en/quick-reference/glossary/taxonomic-weight.md deleted file mode 100644 index 682bc056c..000000000 --- a/docs/content/en/quick-reference/glossary/taxonomic-weight.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: taxonomic weight -reference: content-management/taxonomies/#order-taxonomies ---- - -Defined in front matter and unique to each taxonomy, a _taxonomic weight_ is a [_weight_](g) that determines the sort order of page collections contained within a [`Taxonomy`](g) object. diff --git a/docs/content/en/quick-reference/glossary/taxonomy-object.md b/docs/content/en/quick-reference/glossary/taxonomy-object.md deleted file mode 100644 index 590490377..000000000 --- a/docs/content/en/quick-reference/glossary/taxonomy-object.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: taxonomy object ---- - -A _taxonomy object_ is a [_map_](g) of [_terms_](g) and the [weighted pages](g) associated with each term. diff --git a/docs/content/en/quick-reference/glossary/taxonomy-page.md b/docs/content/en/quick-reference/glossary/taxonomy-page.md deleted file mode 100644 index 6b62f0912..000000000 --- a/docs/content/en/quick-reference/glossary/taxonomy-page.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: taxonomy page ---- - -A _taxonomy page_ is a page with the "taxonomy" [_page kind_](g). Typically a listing of [_terms_](g) within a given [_taxonomy_](g). diff --git a/docs/content/en/quick-reference/glossary/taxonomy.md b/docs/content/en/quick-reference/glossary/taxonomy.md deleted file mode 100644 index ce35a634c..000000000 --- a/docs/content/en/quick-reference/glossary/taxonomy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: taxonomy -reference: /content-management/taxonomies ---- -A _taxonomy_ is a group of related [_terms_](g) used to classify content. For example, a "colors" taxonomy might include the terms "red", "green", and "blue". diff --git a/docs/content/en/quick-reference/glossary/template-action.md b/docs/content/en/quick-reference/glossary/template-action.md deleted file mode 100644 index e40c228fe..000000000 --- a/docs/content/en/quick-reference/glossary/template-action.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: template action -reference: https://pkg.go.dev/text/template#hdr-Actions ---- - -A data evaluation or control structure within a [_template_](g), delimited by `{{` and `}}`. diff --git a/docs/content/en/quick-reference/glossary/template.md b/docs/content/en/quick-reference/glossary/template.md deleted file mode 100644 index d442c0a22..000000000 --- a/docs/content/en/quick-reference/glossary/template.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: template -reference: /templates ---- - -A _template_ is a file with [_template actions_](g), located within the `layouts` directory of a project, theme, or module. diff --git a/docs/content/en/quick-reference/glossary/term-page.md b/docs/content/en/quick-reference/glossary/term-page.md deleted file mode 100644 index 78eabc11f..000000000 --- a/docs/content/en/quick-reference/glossary/term-page.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: term page ---- - -A _term page_ is a page with the "term" [_page kind_](g). Typically a listing of [_regular pages_](g) and [_section pages_](g) with a given [_term_](g). diff --git a/docs/content/en/quick-reference/glossary/term.md b/docs/content/en/quick-reference/glossary/term.md deleted file mode 100644 index cafd92585..000000000 --- a/docs/content/en/quick-reference/glossary/term.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: term -reference: /content-management/taxonomies ---- - -A _term_ is a member of a [_taxonomy_](g), used to classify content. diff --git a/docs/content/en/quick-reference/glossary/theme.md b/docs/content/en/quick-reference/glossary/theme.md deleted file mode 100644 index 84b9c684a..000000000 --- a/docs/content/en/quick-reference/glossary/theme.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: theme ---- - -A _theme_ is a packaged combination of [_archetypes_](g), assets, content, data, [_templates_](g), translation tables, static files, or configuration settings. A theme may serve as the basis for a new site, or to augment an existing site. diff --git a/docs/content/en/quick-reference/glossary/token.md b/docs/content/en/quick-reference/glossary/token.md deleted file mode 100644 index f8c12d570..000000000 --- a/docs/content/en/quick-reference/glossary/token.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: token ---- - -A _token_ is an identifier within a format string, beginning with a colon and replaced with a value when rendered. For example, use tokens in format strings for both [permalinks](/content-management/urls/#permalinks) and [dates](/functions/time/format/#localization). diff --git a/docs/content/en/quick-reference/glossary/type.md b/docs/content/en/quick-reference/glossary/type.md deleted file mode 100644 index dbc692e5a..000000000 --- a/docs/content/en/quick-reference/glossary/type.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: type ---- - -See [content type](g). diff --git a/docs/content/en/quick-reference/glossary/ugly-url.md b/docs/content/en/quick-reference/glossary/ugly-url.md deleted file mode 100644 index 4083f9378..000000000 --- a/docs/content/en/quick-reference/glossary/ugly-url.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: ugly URL ---- - -An _ugly URL_ is a URL that includes a file extension. diff --git a/docs/content/en/quick-reference/glossary/unmarshal.md b/docs/content/en/quick-reference/glossary/unmarshal.md deleted file mode 100644 index 0ed548a21..000000000 --- a/docs/content/en/quick-reference/glossary/unmarshal.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: unmarshal -reference: /functions/transform/unmarshal/ ---- - -To _unmarshal_ is to transform a serialized object into a data structure. For example, transforming a JSON file into a [_map_](g) that you can access within a template. diff --git a/docs/content/en/quick-reference/glossary/utc.md b/docs/content/en/quick-reference/glossary/utc.md deleted file mode 100644 index a4627be5a..000000000 --- a/docs/content/en/quick-reference/glossary/utc.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: UTC -reference: https://en.wikipedia.org/wiki/Coordinated_Universal_Time ---- - -_UTC_ is an abbreviation for Coordinated Universal Time, the primary time standard used worldwide to regulate clocks and time. It is the basis for civil time and time zones across the globe. diff --git a/docs/content/en/quick-reference/glossary/variable.md b/docs/content/en/quick-reference/glossary/variable.md deleted file mode 100644 index f8139a41f..000000000 --- a/docs/content/en/quick-reference/glossary/variable.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: variable ---- - -A _variable_ is a user-defined [_identifier_](g) prepended with a `$` symbol, representing a value of any data type, initialized or assigned within a [_template action_](g). For example, `$foo` and `$bar` are variables. diff --git a/docs/content/en/quick-reference/glossary/walk.md b/docs/content/en/quick-reference/glossary/walk.md deleted file mode 100644 index 09f9ecb20..000000000 --- a/docs/content/en/quick-reference/glossary/walk.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: walk ---- - -To _walk_ is to recursively traverse a nested data structure. For example, rendering a multilevel menu. diff --git a/docs/content/en/quick-reference/glossary/weight.md b/docs/content/en/quick-reference/glossary/weight.md deleted file mode 100644 index deb9e6dc0..000000000 --- a/docs/content/en/quick-reference/glossary/weight.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: weight ---- - -A _weight_ is a numeric value used to position an element within a sorted [collection](g). Assign weights using non-zero integers. Lighter items float to the top, while heavier items sink to the bottom. Unweighted or zero-weighted elements are placed at the end of the collection. Weights are typically assigned to pages, menu entries, languages, and output formats. diff --git a/docs/content/en/quick-reference/glossary/weighted-page.md b/docs/content/en/quick-reference/glossary/weighted-page.md deleted file mode 100644 index e6cf8d6db..000000000 --- a/docs/content/en/quick-reference/glossary/weighted-page.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: weighted page ---- - -Contained within a [_taxonomy object_](g), a _weighted page_ is a [_map_](g) with two [_elements_](g): a `Page` object, and its [_taxonomic weight_](g) as defined in front matter. Access the elements using the `Page` and `Weight` keys. diff --git a/docs/content/en/quick-reference/glossary/zero-time.md b/docs/content/en/quick-reference/glossary/zero-time.md deleted file mode 100644 index 9701f3912..000000000 --- a/docs/content/en/quick-reference/glossary/zero-time.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: zero time ---- - -The _zero time_ is January 1, 0001, 00:00:00 UTC. Formatted per [RFC3339](https://www.rfc-editor.org/rfc/rfc3339) the _zero time_ is 0001-01-01T00:00:00-00:00. diff --git a/docs/content/en/quick-reference/methods.md b/docs/content/en/quick-reference/methods.md deleted file mode 100644 index 524713c1f..000000000 --- a/docs/content/en/quick-reference/methods.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Methods -description: A quick reference guide to Hugo's methods, grouped by object. -categories: [] -keywords: [] ---- - -{{% quick-reference section="methods" %}} diff --git a/docs/content/en/quick-reference/page-collections.md b/docs/content/en/quick-reference/page-collections.md deleted file mode 100644 index 4c2387bbe..000000000 --- a/docs/content/en/quick-reference/page-collections.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: Page collections -description: A quick reference guide to Hugo's page collections. -categories: [] -keywords: [] ---- - -## Page - -Use these `Page` methods when rendering lists on [section pages](g), [taxonomy pages](g), [term pages](g), and the home page. - -{{% list-pages-in-section path=/methods/page filter=methods_page_page_collections filterType=include titlePrefix=PAGE. %}} - -## Site - -Use these `Site` methods when rendering lists on any page. - -{{% list-pages-in-section path=/methods/site filter=methods_site_page_collections filterType=include titlePrefix=SITE. %}} - -## Filter - -Use the [`where`] function to filter page collections. - -## Sort - -{{% glossary-term "default sort order" %}} - -Use these methods to sort page collections by different criteria. - -{{% list-pages-in-section path=/methods/pages filter=methods_pages_sort filterType=include titlePrefix=. titlePrefix=PAGES. %}} - -## Group - -Use these methods to group page collections. - -{{% list-pages-in-section path=/methods/pages filter=methods_pages_group filterType=include titlePrefix=. titlePrefix=PAGES. %}} - -[`where`]: /functions/collections/where/ diff --git a/docs/content/en/quick-reference/syntax-highlighting-styles.md b/docs/content/en/quick-reference/syntax-highlighting-styles.md deleted file mode 100644 index 14b313cbc..000000000 --- a/docs/content/en/quick-reference/syntax-highlighting-styles.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: Syntax highlighting styles -description: Highlight code examples using one of these styles. -categories: [] -keywords: [highlight] ---- - -## Overview - -Hugo provides several methods to add syntax highlighting to code examples: - -- Use the [`transform.Highlight`] function within your templates -- Use the [`highlight`] shortcode with any [content format](g) -- Use fenced code blocks with the Markdown content format - -Regardless of method, use any of the syntax highlighting styles below. - -Set the default syntax highlighting style in your site configuration: - -{{< code-toggle file=hugo >}} -[markup.highlight] -style = 'monokai' -{{< /code-toggle >}} - -See [configure Markup](/configuration/markup/#highlight). - -[`transform.Highlight`]: /functions/transform/highlight/ -[`highlight`]: /shortcodes/highlight/ -[fenced code blocks]: /content-management/syntax-highlighting/#fenced-code-blocks - -## Styles - -This gallery demonstrates the application of each syntax highlighting style with code examples written in different programming languages. - -{{% syntax-highlighting-styles %}} diff --git a/docs/content/en/render-hooks/_index.md b/docs/content/en/render-hooks/_index.md deleted file mode 100644 index a5e6203ff..000000000 --- a/docs/content/en/render-hooks/_index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Render hooks -description: Create render hooks to override the rendering of Markdown to HTML. -categories: [] -keywords: [] -weight: 10 -aliases: [/templates/render-hooks/] ---- diff --git a/docs/content/en/render-hooks/blockquotes.md b/docs/content/en/render-hooks/blockquotes.md deleted file mode 100755 index 7ba282a0a..000000000 --- a/docs/content/en/render-hooks/blockquotes.md +++ /dev/null @@ -1,184 +0,0 @@ ---- -title: Blockquote render hooks -linkTitle: Blockquotes -description: Create a blockquote render hook to override the rendering of Markdown blockquotes to HTML. -categories: [] -keywords: [] ---- - -{{< new-in 0.132.0 />}} - -## Context - -Blockquote render hook templates receive the following [context](g): - -AlertType -: (`string`) Applicable when [`Type`](#type) is `alert`, this is the alert type converted to lowercase. See the [alerts](#alerts) section below. - -AlertTitle -: {{< new-in 0.134.0 />}} -: (`template.HTML`) Applicable when [`Type`](#type) is `alert`, this is the alert title. See the [alerts](#alerts) section below. - -AlertSign -: {{< new-in 0.134.0 />}} -: (`string`) Applicable when [`Type`](#type) is `alert`, this is the alert sign. Typically used to indicate whether an alert is graphically foldable, this is one of `+`, `-`, or an empty string. See the [alerts](#alerts) section below. - -Attributes -: (`map`) The [Markdown attributes], available if you configure your site as follows: - - {{< code-toggle file=hugo >}} - [markup.goldmark.parser.attribute] - block = true - {{< /code-toggle >}} - -Ordinal -: (`int`) The zero-based ordinal of the blockquote on the page. - -Page -: (`page`) A reference to the current page. - -PageInner -: (`page`) A reference to a page nested via the [`RenderShortcodes`] method. [See details](#pageinner-details). - -Position -: (`string`) The position of the blockquote within the page content. - -Text -: (`template.HTML`) The blockquote text, excluding the first line if [`Type`](#type) is `alert`. See the [alerts](#alerts) section below. - -Type -: (`string`) The blockquote type. Returns `alert` if the blockquote has an alert designator, else `regular`. See the [alerts](#alerts) section below. - -## Examples - -In its default configuration, Hugo renders Markdown blockquotes according to the [CommonMark specification]. To create a render hook that does the same thing: - -```go-html-template {file="layouts/_default/_markup/render-blockquote.html" copy=true} -
    - {{ .Text }} -
    -``` - -To render a blockquote as an HTML `figure` element with an optional citation and caption: - -```go-html-template {file="layouts/_default/_markup/render-blockquote.html" copy=true} -
    -
    - {{ .Text }} -
    - {{ with .Attributes.caption }} -
    - {{ . | safeHTML }} -
    - {{ end }} -
    -``` - -Then in your markdown: - -```text -> Some text -{cite="https://gohugo.io" caption="Some caption"} -``` - -## Alerts - -Also known as _callouts_ or _admonitions_, alerts are blockquotes used to emphasize critical information. - -### Basic syntax - -With the basic Markdown syntax, the first line of each alert is an alert designator consisting of an exclamation point followed by the alert type, wrapped within brackets. For example: - -```text {file="content/example.md"} -> [!NOTE] -> Useful information that users should know, even when skimming content. - -> [!TIP] -> Helpful advice for doing things better or more easily. - -> [!IMPORTANT] -> Key information users need to know to achieve their goal. - -> [!WARNING] -> Urgent info that needs immediate user attention to avoid problems. - -> [!CAUTION] -> Advises about risks or negative outcomes of certain actions. -``` - -The basic syntax is compatible with [GitHub], [Obsidian], and [Typora]. - -### Extended syntax - -With the extended Markdown syntax, you may optionally include an alert sign and/or an alert title. The alert sign is one of `+` or `-`, typically used to indicate whether an alert is graphically foldable. For example: - -```text {file="content/example.md"} -> [!WARNING]+ Radiation hazard -> Do not approach or handle without protective gear. -``` - -The extended syntax is compatible with [Obsidian]. - -> [!note] -> The extended syntax is not compatible with GitHub or Typora. If you include an alert sign or an alert title, these applications render the Markdown as a blockquote. - -### Example - -This blockquote render hook renders a multilingual alert if an alert designator is present, otherwise it renders a blockquote according to the CommonMark specification. - -```go-html-template {file="layouts/_default/_markup/render-blockquote.html" copy=true} -{{ $emojis := dict - "caution" ":exclamation:" - "important" ":information_source:" - "note" ":information_source:" - "tip" ":bulb:" - "warning" ":information_source:" -}} - -{{ if eq .Type "alert" }} -
    -

    - {{ transform.Emojify (index $emojis .AlertType) }} - {{ with .AlertTitle }} - {{ . }} - {{ else }} - {{ or (i18n .AlertType) (title .AlertType) }} - {{ end }} -

    - {{ .Text }} -
    -{{ else }} -
    - {{ .Text }} -
    -{{ end }} -``` - -To override the label, create these entries in your i18n files: - -{{< code-toggle file=i18n/en.toml >}} -caution = 'Caution' -important = 'Important' -note = 'Note' -tip = 'Tip' -warning = 'Warning' -{{< /code-toggle >}} - -Although you can use one template with conditional logic as shown above, you can also create separate templates for each [`Type`](#type) of blockquote: - -```text -layouts/ -└── _default/ - └── _markup/ - ├── render-blockquote-alert.html - └── render-blockquote-regular.html -``` - -{{% include "/_common/render-hooks/pageinner.md" %}} - -[`RenderShortcodes`]: /methods/page/rendershortcodes -[CommonMark specification]: https://spec.commonmark.org/current/ -[GitHub]: https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts -[Markdown attributes]: /content-management/markdown-attributes/ -[Obsidian]: https://help.obsidian.md/Editing+and+formatting/Callouts -[Typora]: https://support.typora.io/Markdown-Reference/#callouts--github-style-alerts diff --git a/docs/content/en/render-hooks/code-blocks.md b/docs/content/en/render-hooks/code-blocks.md deleted file mode 100755 index d1a01e9b0..000000000 --- a/docs/content/en/render-hooks/code-blocks.md +++ /dev/null @@ -1,127 +0,0 @@ ---- -title: Code block render hooks -linkTitle: Code blocks -description: Create a code block render hook to override the rendering of Markdown code blocks to HTML. -categories: [] -keywords: [] ---- - -## Markdown - -This Markdown example contains a fenced code block: - -````text {file="content/example.md"} -```bash {class="my-class" id="my-codeblock" lineNos=inline tabWidth=2} -declare a=1 -echo "$a" -exit -``` -```` - -A fenced code block consists of: - -- A leading [code fence] -- An optional [info string] -- A code sample -- A trailing code fence - -In the previous example, the info string contains: - -- The language of the code sample (the first word) -- An optional space-delimited or comma-delimited list of attributes (everything within braces) - -The attributes in the info string can be generic attributes or highlighting options. - -In the example above, the _generic attributes_ are `class` and `id`. In the absence of special handling within a code block render hook, Hugo adds each generic attribute to the HTML element surrounding the rendered code block. Consistent with its content security model, Hugo removes HTML event attributes such as `onclick` and `onmouseover`. Generic attributes are typically global HTML attributes, but you may include custom attributes as well. - -In the example above, the _highlighting options_ are `lineNos` and `tabWidth`. Hugo uses the [Chroma] syntax highlighter to render the code sample. You can control the appearance of the rendered code by specifying one or more [highlighting options]. - -> [!note] -> Although `style` is a global HTML attribute, when used in an info string it is a highlighting option. - -## Context - -Code block render hook templates receive the following [context](g): - -Attributes -: (`map`) The generic attributes from the info string. - -Inner -: (`string`) The content between the leading and trailing code fences, excluding the info string. - -Options -: (`map`) The highlighting options from the info string. - -Ordinal -: (`int`) The zero-based ordinal of the code block on the page. - -Page -: (`page`) A reference to the current page. - -PageInner -: {{< new-in 0.125.0 />}} -: (`page`) A reference to a page nested via the [`RenderShortcodes`] method. [See details](#pageinner-details). - -Position -: (`text.Position`) The position of the code block within the page content. - -Type -: (`string`) The first word of the info string, typically the code language. - -## Examples - -In its default configuration, Hugo renders fenced code blocks by passing the code sample through the Chroma syntax highlighter and wrapping the result. To create a render hook that does the same thing: - -```go-html-template {file="layouts/_default/_markup/render-codeblock.html" copy=true} -{{ $result := transform.HighlightCodeBlock . }} -{{ $result.Wrapped }} -``` - -Although you can use one template with conditional logic to control the behavior on a per-language basis, you can also create language-specific templates. - -```text -layouts/ -└── _default/ - └── _markup/ - ├── render-codeblock-mermaid.html - ├── render-codeblock-python.html - └── render-codeblock.html -``` - -For example, to create a code block render hook to render [Mermaid] diagrams: - -```go-html-template {file="layouts/_default/_markup/render-codeblock-mermaid.html" copy=true} -
    -  {{ .Inner | htmlEscape | safeHTML }}
    -
    -{{ .Page.Store.Set "hasMermaid" true }} -``` - -Then include this snippet at the _bottom_ of your base template, before the closing `body` tag: - -```go-html-template {file="layouts/_default/baseof.html" copy=true} -{{ if .Store.Get "hasMermaid" }} - -{{ end }} -``` - -See the [diagrams] page for details. - -## Embedded - -Hugo includes an [embedded code block render hook] to render [GoAT diagrams]. - -{{% include "/_common/render-hooks/pageinner.md" %}} - -[`RenderShortcodes`]: /methods/page/rendershortcodes -[Chroma]: https://github.com/alecthomas/chroma/ -[code fence]: https://spec.commonmark.org/0.31.2/#code-fence -[diagrams]: /content-management/diagrams/#mermaid-diagrams -[embedded code block render hook]: {{% eturl render-codeblock-goat %}} -[GoAT diagrams]: /content-management/diagrams/#goat-diagrams-ascii -[highlighting options]: /functions/transform/highlight/#options -[info string]: https://spec.commonmark.org/0.31.2/#info-string -[Mermaid]: https://mermaid.js.org/ diff --git a/docs/content/en/render-hooks/headings.md b/docs/content/en/render-hooks/headings.md deleted file mode 100755 index 89868d478..000000000 --- a/docs/content/en/render-hooks/headings.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -title: Heading render hooks -linkTitle: Headings -description: Create a heading render hook to override the rendering of Markdown headings to HTML. -categories: [] -keywords: [] ---- - -## Context - -Heading render hook templates receive the following [context](g): - -Anchor -: (`string`) The `id` attribute of the heading element. - -Attributes -: (`map`) The [Markdown attributes], available if you configure your site as follows: - - {{< code-toggle file=hugo >}} - [markup.goldmark.parser.attribute] - title = true - {{< /code-toggle >}} - -Level -: (`int`) The heading level, 1 through 6. - -Page -: (`page`) A reference to the current page. - -PageInner -: {{< new-in 0.125.0 />}} -: (`page`) A reference to a page nested via the [`RenderShortcodes`] method. [See details](#pageinner-details). - -PlainText -: (`string`) The heading text as plain text. - -Text -: (`template.HTML`) The heading text. - -[Markdown attributes]: /content-management/markdown-attributes/ -[`RenderShortcodes`]: /methods/page/rendershortcodes - -## Examples - -In its default configuration, Hugo renders Markdown headings according to the [CommonMark specification] with the addition of automatic `id` attributes. To create a render hook that does the same thing: - -[CommonMark specification]: https://spec.commonmark.org/current/ - -```go-html-template {file="layouts/_default/_markup/render-heading.html" copy=true} - - {{- .Text -}} - -``` - -To add an anchor link to the right of each heading: - -```go-html-template {file="layouts/_default/_markup/render-heading.html" copy=true} - - {{ .Text }} - # - -``` - -{{% include "/_common/render-hooks/pageinner.md" %}} diff --git a/docs/content/en/render-hooks/images.md b/docs/content/en/render-hooks/images.md deleted file mode 100755 index a4faac672..000000000 --- a/docs/content/en/render-hooks/images.md +++ /dev/null @@ -1,138 +0,0 @@ ---- -title: Image render hooks -linkTitle: Images -description: Create an image render to hook override the rendering of Markdown images to HTML. -categories: [] -keywords: [] ---- - -## Markdown - -A Markdown image has three components: the image description, the image destination, and optionally the image title. - -```text -![white kitten](/images/kitten.jpg "A kitten!") - ------------ ------------------ --------- - description destination title -``` - -These components are passed into the render hook [context](g) as shown below. - -## Context - -Image render hook templates receive the following context: - -Attributes -: (`map`) The [Markdown attributes], available if you configure your site as follows: - - {{< code-toggle file=hugo >}} - [markup.goldmark.parser] - wrapStandAloneImageWithinParagraph = false - [markup.goldmark.parser.attribute] - block = true - {{< /code-toggle >}} - -Destination -: (`string`) The image destination. - -IsBlock -: (`bool`) Reports whether a standalone image is not wrapped within a paragraph element. - -Ordinal -: (`int`) The zero-based ordinal of the image on the page. - -Page -: (`page`) A reference to the current page. - -PageInner -: {{< new-in 0.125.0 />}} -: (`page`) A reference to a page nested via the [`RenderShortcodes`] method. [See details](#pageinner-details). - -PlainText -: (`string`) The image description as plain text. - -Text -: (`template.HTML`) The image description. - -Title -: (`string`) The image title. - -## Examples - -> [!note] -> With inline elements such as images and links, remove leading and trailing whitespace using the `{{‑ ‑}}` delimiter notation to prevent whitespace between adjacent inline elements and text. - -In its default configuration, Hugo renders Markdown images according to the [CommonMark specification]. To create a render hook that does the same thing: - -```go-html-template {file="layouts/_default/_markup/render-image.html" copy=true} -{{ . }} -{{- /* chomp trailing newline */ -}} -``` - -To render standalone images within `figure` elements: - -```go-html-template {file="layouts/_default/_markup/render-image.html" copy=true} -{{- if .IsBlock -}} -
    - {{ . }} - {{- with .Title }}
    {{ . }}
    {{ end -}} -
    -{{- else -}} - {{ . }} -{{- end -}} -``` - -Note that the above requires the following site configuration: - -{{< code-toggle file=hugo >}} -[markup.goldmark.parser] -wrapStandAloneImageWithinParagraph = false -{{< /code-toggle >}} - -## Default - -{{< new-in 0.123.0 />}} - -Hugo includes an [embedded image render hook] to resolve Markdown image destinations. Disabled by default, you can enable it in your site configuration: - -{{< code-toggle file=hugo >}} -[markup.goldmark.renderHooks.image] -enableDefault = true -{{< /code-toggle >}} - -A custom render hook, even when provided by a theme or module, will override the embedded render hook regardless of the configuration setting above. - -> [!note] -> The embedded image render hook is automatically enabled for multilingual single-host sites if [duplication of shared page resources] is disabled. This is the default configuration for multilingual single-host sites. - -The embedded image render hook resolves internal Markdown destinations by looking for a matching [page resource](g), falling back to a matching [global resource](g). Remote destinations are passed through, and the render hook will not throw an error or warning if unable to resolve a destination. - -You must place global resources in the `assets` directory. If you have placed your resources in the `static` directory, and you are unable or unwilling to move them, you must mount the `static` directory to the `assets` directory by including both of these entries in your site configuration: - -{{< code-toggle file=hugo >}} -[[module.mounts]] -source = 'assets' -target = 'assets' - -[[module.mounts]] -source = 'static' -target = 'assets' -{{< /code-toggle >}} - -Note that the embedded image render hook does not perform image processing. Its sole purpose is to resolve Markdown image destinations. - -{{% include "/_common/render-hooks/pageinner.md" %}} - -[`RenderShortcodes`]: /methods/page/rendershortcodes -[CommonMark specification]: https://spec.commonmark.org/current/ -[duplication of shared page resources]: /configuration/markup/#duplicateresourcefiles -[embedded image render hook]: {{% eturl render-image %}} -[Markdown attributes]: /content-management/markdown-attributes/ diff --git a/docs/content/en/render-hooks/introduction.md b/docs/content/en/render-hooks/introduction.md deleted file mode 100755 index 045d25c3d..000000000 --- a/docs/content/en/render-hooks/introduction.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -title: Introduction -description: An introduction to Hugo's render hooks. -categories: [] -keywords: [] -weight: 10 ---- - -When rendering Markdown to HTML, render hooks override the conversion. Each render hook is a template, with one template for each supported element type: - -- [Blockquotes](/render-hooks/blockquotes) -- [Code blocks](/render-hooks/code-blocks) -- [Headings](/render-hooks/headings) -- [Images](/render-hooks/images) -- [Links](/render-hooks/links) -- [Passthrough elements](/render-hooks/passthrough) -- [Tables](/render-hooks/tables) - -> [!note] -> Hugo supports multiple [content formats] including Markdown, HTML, AsciiDoc, Emacs Org Mode, Pandoc, and reStructuredText. -> -> The render hook capability is limited to Markdown. You cannot create render hooks for the other content formats. - -For example, consider this Markdown: - -```text -[Hugo](https://gohugo.io) - -![kitten](kitten.jpg) -``` - -Without link or image render hooks, the example above is rendered to: - -```html -

    Hugo

    -

    kitten

    -``` - -By creating link and image render hooks, you can alter the conversion from Markdown to HTML. For example: - -```html -

    Hugo

    -

    kitten

    -``` - -Each render hook is a template, with one template for each supported element type: - -```text -layouts/ -└── _default/ - └── _markup/ - ├── render-blockquote.html - ├── render-codeblock.html - ├── render-heading.html - ├── render-image.html - ├── render-link.html - ├── render-passthrough.html - └── render-table.html -``` - -The template lookup order allows you to create different render hooks for each page [type](g), [kind](g), language, and [output format](g). For example: - -```text -layouts/ -├── _default/ -│ └── _markup/ -│ ├── render-link.html -│ └── render-link.rss.xml -├── books/ -│ └── _markup/ -│ ├── render-link.html -│ └── render-link.rss.xml -└── films/ - └── _markup/ - ├── render-link.html - └── render-link.rss.xml -``` - -The remaining pages in this section describe each type of render hook, including examples and the context received by each template. - -[content formats]: /content-management/formats/ diff --git a/docs/content/en/render-hooks/links.md b/docs/content/en/render-hooks/links.md deleted file mode 100755 index 23f725eb7..000000000 --- a/docs/content/en/render-hooks/links.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -title: Link render hooks -linkTitle: Links -description: Create a link render hook to override the rendering of Markdown links to HTML. -categories: [] -keywords: [] ---- - -## Markdown - -A Markdown link has three components: the link text, the link destination, and optionally the link title. - -```text -[Post 1](/posts/post-1 "My first post") - ------ ------------- ------------- - text destination title -``` - -These components are passed into the render hook [context](g) as shown below. - -## Context - -Link render hook templates receive the following context: - -Destination -: (`string`) The link destination. - -Page -: (`page`) A reference to the current page. - -PageInner -: {{< new-in 0.125.0 />}} -: (`page`) A reference to a page nested via the [`RenderShortcodes`] method. [See details](#pageinner-details). - -PlainText -: (`string`) The link description as plain text. - -Text -: (`template.HTML`) The link description. - -Title -: (`string`) The link title. - -## Examples - -> [!note] -> With inline elements such as images and links, remove leading and trailing whitespace using the `{{‑ ‑}}` delimiter notation to prevent whitespace between adjacent inline elements and text. - -In its default configuration, Hugo renders Markdown links according to the [CommonMark specification]. To create a render hook that does the same thing: - -```go-html-template {file="layouts/_default/_markup/render-link.html" copy=true} - - {{- with .Text }}{{ . }}{{ end -}} - -{{- /* chomp trailing newline */ -}} -``` - -To include a `rel` attribute set to `external` for external links: - -```go-html-template {file="layouts/_default/_markup/render-link.html" copy=true} -{{- $u := urls.Parse .Destination -}} - - {{- with .Text }}{{ . }}{{ end -}} - -{{- /* chomp trailing newline */ -}} -``` - -## Default - -{{< new-in 0.123.0 />}} - -Hugo includes an [embedded link render hook] to resolve Markdown link destinations. Disabled by default, you can enable it in your site configuration: - -{{< code-toggle file=hugo >}} -[markup.goldmark.renderHooks.link] -enableDefault = true -{{< /code-toggle >}} - -A custom render hook, even when provided by a theme or module, will override the embedded render hook regardless of the configuration setting above. - -> [!note] -> The embedded link render hook is automatically enabled for multilingual single-host sites if [duplication of shared page resources] is disabled. This is the default configuration for multilingual single-host sites. - -The embedded link render hook resolves internal Markdown destinations by looking for a matching page, falling back to a matching [page resource](g), then falling back to a matching [global resource](g). Remote destinations are passed through, and the render hook will not throw an error or warning if unable to resolve a destination. - -You must place global resources in the `assets` directory. If you have placed your resources in the `static` directory, and you are unable or unwilling to move them, you must mount the `static` directory to the `assets` directory by including both of these entries in your site configuration: - -{{< code-toggle file=hugo >}} -[[module.mounts]] -source = 'assets' -target = 'assets' - -[[module.mounts]] -source = 'static' -target = 'assets' -{{< /code-toggle >}} - -{{% include "/_common/render-hooks/pageinner.md" %}} - -[`RenderShortcodes`]: /methods/page/rendershortcodes -[CommonMark specification]: https://spec.commonmark.org/current/ -[duplication of shared page resources]: /configuration/markup/#duplicateresourcefiles -[embedded link render hook]: {{% eturl render-link %}} diff --git a/docs/content/en/render-hooks/passthrough.md b/docs/content/en/render-hooks/passthrough.md deleted file mode 100755 index 356a030af..000000000 --- a/docs/content/en/render-hooks/passthrough.md +++ /dev/null @@ -1,124 +0,0 @@ ---- -title: Passthrough render hooks -linkTitle: Passthrough -description: Create a passthrough render hook to override the rendering of text snippets captured by the Goldmark Passthrough extension. -categories: [] -keywords: [] ---- - -{{< new-in 0.132.0 />}} - -## Overview - -Hugo uses [Goldmark] to render Markdown to HTML. Goldmark supports custom extensions to extend its core functionality. The [Passthrough] extension captures and preserves raw Markdown within delimited snippets of text, including the delimiters themselves. These are known as _passthrough elements_. - -[Goldmark]: https://github.com/yuin/goldmark -[Passthrough]: /configuration/markup/#passthrough - -Depending on your choice of delimiters, Hugo will classify a passthrough element as either _block_ or _inline_. Consider this contrived example: - -```text {file="content/example.md"} -This is a - -\[block\] - -passthrough element with opening and closing block delimiters. - -This is an \(inline\) passthrough element with opening and closing inline delimiters. -``` - -Update your site configuration to enable the Passthrough extension and define opening and closing delimiters for each passthrough element type, either `block` or `inline`. For example: - -{{< code-toggle file=hugo >}} -[markup.goldmark.extensions.passthrough] -enable = true -[markup.goldmark.extensions.passthrough.delimiters] -block = [['\[', '\]'], ['$$', '$$']] -inline = [['\(', '\)']] -{{< /code-toggle >}} - -In the example above there are two sets of `block` delimiters. You may use either one in your Markdown. - -The Passthrough extension is often used in conjunction with the MathJax or KaTeX display engine to render [mathematical expressions] written in the LaTeX markup language. - -[mathematical expressions]: /content-management/mathematics/ - -To enable custom rendering of passthrough elements, create a passthrough render hook. - -## Context - -Passthrough render hook templates receive the following [context](g): - -Attributes -: (`map`) The [Markdown attributes], available if you configure your site as follows: - - {{< code-toggle file=hugo >}} - [markup.goldmark.parser.attribute] - block = true - {{< /code-toggle >}} - - Hugo populates the `Attributes` map for _block_ passthrough elements. Markdown attributes are not applicable to _inline_ elements. - -Inner -: (`string`) The inner content of the passthrough element, excluding the delimiters. - -Ordinal -: (`int`) The zero-based ordinal of the passthrough element on the page. - -Page -: (`page`) A reference to the current page. - -PageInner -: (`page`) A reference to a page nested via the [`RenderShortcodes`] method. [See details](#pageinner-details). - -Position -: (`string`) The position of the passthrough element within the page content. - -Type -: (`string`) The passthrough element type, either `block` or `inline`. - -[Markdown attributes]: /content-management/markdown-attributes/ -[`RenderShortcodes`]: /methods/page/rendershortcodes - -## Example - -Instead of client-side JavaScript rendering of mathematical markup using MathJax or KaTeX, create a passthrough render hook which calls the [`transform.ToMath`] function. - -[`transform.ToMath`]: /functions/transform/tomath/ - -```go-html-template {file="layouts/_default/_markup/render-passthrough.html" copy=true} -{{- $opts := dict "output" "htmlAndMathml" "displayMode" (eq .Type "block") }} -{{- with try (transform.ToMath .Inner $opts) }} - {{- with .Err }} - {{- errorf "Unable to render mathematical markup to HTML using the transform.ToMath function. The KaTeX display engine threw the following error: %s: see %s." . $.Position }} - {{- else }} - {{- .Value }} - {{- $.Page.Store.Set "hasMath" true }} - {{- end }} -{{- end -}} -``` - -Then, in your base template, conditionally include the KaTeX CSS within the head element: - -```go-html-template {file="layouts/_default/baseof.html" copy=true} - - {{ $noop := .WordCount }} - {{ if .Page.Store.Get "hasMath" }} - - {{ end }} - -``` - -In the above, note the use of a [noop](g) statement to force content rendering before we check the value of `hasMath` with the `Store.Get` method. - -Although you can use one template with conditional logic as shown above, you can also create separate templates for each [`Type`](#type) of passthrough element: - -```text -layouts/ -└── _default/ - └── _markup/ - ├── render-passthrough-block.html - └── render-passthrough-inline.html -``` - -{{% include "/_common/render-hooks/pageinner.md" %}} diff --git a/docs/content/en/render-hooks/tables.md b/docs/content/en/render-hooks/tables.md deleted file mode 100755 index c7671aff4..000000000 --- a/docs/content/en/render-hooks/tables.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -title: Table render hooks -linkTitle: Tables -description: Create a table render hook to override the rendering of Markdown tables to HTML. -categories: [] -keywords: [] ---- - -{{< new-in 0.134.0 />}} - -## Context - -Table render hook templates receive the following [context](g): - -Attributes -: (`map`) The [Markdown attributes], available if you configure your site as follows: - - {{< code-toggle file=hugo >}} - [markup.goldmark.parser.attribute] - block = true - {{< /code-toggle >}} - -Ordinal -: (`int`) The zero-based ordinal of the table on the page. - -Page -: (`page`) A reference to the current page. - -PageInner -: (`page`) A reference to a page nested via the [`RenderShortcodes`] method. [See details](#pageinner-details). - -Position -: (`string`) The position of the table within the page content. - -THead -: (`slice`) A slice of table header rows, where each element is a slice of table cells. - -TBody -: (`slice`) A slice of table body rows, where each element is a slice of table cells. - -[Markdown attributes]: /content-management/markdown-attributes/ -[`RenderShortcodes`]: /methods/page/rendershortcodes - -## Table cells - -Each table cell within the slice of slices returned by the `THead` and `TBody` methods has the following fields: - -Alignment -: (`string`) The alignment of the text within the table cell, one of `left`, `center`, or `right`. - -Text -: (`template.HTML`) The text within the table cell. - -## Example - -In its default configuration, Hugo renders Markdown tables according to the [GitHub Flavored Markdown specification]. To create a render hook that does the same thing: - -[GitHub Flavored Markdown specification]: https://github.github.com/gfm/#tables-extension- - -```go-html-template {file="layouts/_default/_markup/render-table.html" copy=true} - - - {{- range .THead }} - - {{- range . }} - - {{- end }} - - {{- end }} - - - {{- range .TBody }} - - {{- range . }} - - {{- end }} - - {{- end }} - -
    - {{- .Text -}} -
    - {{- .Text -}} -
    -``` - -{{% include "/_common/render-hooks/pageinner.md" %}} diff --git a/docs/content/en/shortcodes/_index.md b/docs/content/en/shortcodes/_index.md deleted file mode 100644 index 826ee5796..000000000 --- a/docs/content/en/shortcodes/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Shortcodes -description: Insert elements such as videos, images, and social media embeds into your content using Hugo's embedded shortcodes. -categories: [] -keywords: [] -weight: 10 ---- diff --git a/docs/content/en/shortcodes/details.md b/docs/content/en/shortcodes/details.md deleted file mode 100755 index 94502ac1c..000000000 --- a/docs/content/en/shortcodes/details.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -title: Details shortcode -linkTitle: Details -description: Insert an HTML details element into your content using the details shortcode. -categories: [] -keywords: [] ---- - -{{< new-in 0.140.0 />}} - -> [!note] -> To override Hugo's embedded `details` shortcode, copy the [source code] to a file with the same name in the `layouts/shortcodes` directory. - -## Example - -With this markup: - -```text -{{}} -This is a **bold** word. -{{}} -``` - -Hugo renders this HTML: - -```html -
    - See the details -

    This is a bold word.

    -
    -``` - -Which looks like this in your browser: - -{{< details summary="See the details" >}} -This is a **bold** word. -{{< /details >}} - -## Arguments - -summary -: (`string`) The content of the child `summary` element rendered from Markdown to HTML. Default is `Details`. - -open -: (`bool`) Whether to initially display the content of the `details` element. Default is `false`. - -class -: (`string`) The `class` attribute of the `details` element. - -name -: (`string`) The `name` attribute of the `details` element. - -title -: (`string`) The `title` attribute of the `details` element. - -## Styling - -Use CSS to style the `details` element, the `summary` element, and the content itself. - -```css -/* target the details element */ -details { } - -/* target the summary element */ -details > summary { } - -/* target the children of the summary element */ -details > summary > * { } - -/* target the content */ -details > :not(summary) { } -``` - -[source code]: {{% eturl details %}} diff --git a/docs/content/en/shortcodes/figure.md b/docs/content/en/shortcodes/figure.md deleted file mode 100755 index 74af52fe7..000000000 --- a/docs/content/en/shortcodes/figure.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -title: Figure shortcode -linkTitle: Figure -description: Insert an HTML figure element into your content using the figure shortcode. -categories: [] -keywords: [] ---- - -> [!note] -> To override Hugo's embedded `figure` shortcode, copy the [source code] to a file with the same name in the `layouts/shortcodes` directory. - -## Example - -With this markup: - -```text -{{}} -``` - -Hugo renders this HTML: - -```html -
    - - A photograph of Zion National Park - -
    -

    Zion National Park

    -
    -
    -``` - -Which looks like this in your browser: - -{{< figure - src="/images/examples/zion-national-park.jpg" - alt="A photograph of Zion National Park" - link="https://www.nps.gov/zion/index.htm" - caption="Zion National Park" - class="ma0 w-75" ->}} - -## Arguments - -src -: (`string`) The `src` attribute of the `img` element. Typically this is a [page resource](g) or a [global resource](g). - -alt -: (`string`) The `alt` attribute of the `img` element. - -width -: (`int`) The `width` attribute of the `img` element. - -height -: (`int`) The `height` attribute of the `img` element. - -loading -: (`string`) The `loading` attribute of the `img` element. - -class -: (`string`) The `class` attribute of the `figure` element. - -link -: (`string`) The `href` attribute of the anchor element that wraps the `img` element. - -target -: (`string`) The `target` attribute of the anchor element that wraps the `img` element. - -rel -: (`rel`) The `rel` attribute of the anchor element that wraps the `img` element. - -title -: (`string`) Within the `figurecaption` element, the title is at the top, wrapped within an `h4` element. - -caption -: (`string`) Within the `figurecaption` element, the caption is at the bottom and may contain plain text or markdown. - -attr -: (`string`) Within the `figurecaption` element, the attribution appears next to the caption and may contain plain text or markdown. - -attrlink -: (`string`) The `href` attribute of the anchor element that wraps the attribution. - -## Image location - -The `figure` shortcode resolves internal Markdown destinations by looking for a matching [page resource](g), falling back to a matching [global resource](g). Remote destinations are passed through, and the render hook will not throw an error or warning if unable to resolve a destination. - -You must place global resources in the `assets` directory. If you have placed your resources in the `static` directory, and you are unable or unwilling to move them, you must mount the `static` directory to the `assets` directory by including both of these entries in your site configuration: - -{{< code-toggle file=hugo >}} -[[module.mounts]] -source = 'assets' -target = 'assets' - -[[module.mounts]] -source = 'static' -target = 'assets' -{{< /code-toggle >}} - -[source code]: {{% eturl figure %}} diff --git a/docs/content/en/shortcodes/gist.md b/docs/content/en/shortcodes/gist.md deleted file mode 100755 index fd2b468ab..000000000 --- a/docs/content/en/shortcodes/gist.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: Gist shortcode -linkTitle: Gist -description: Embed a GitHub Gist in your content using the gist shortcode. -categories: [] -keywords: [] -expiryDate: 2027-02-01 # deprecated 2025-02-01 in v0.143.0 ---- - -{{< deprecated-in 0.143.0 >}} -The `gist` shortcode was deprecated in version 0.143.0 and will be removed in a future release. To continue embedding GitHub Gists in your content, you'll need to create a custom shortcode: - -1. Create a new file: Create a file named `gist.html` within the `layouts/shortcodes` directory. -1. Copy the source code: Paste the [original source code]({{% eturl gist %}}) of the gist shortcode into the newly created `gist.html` file. - -This will allow you to maintain the functionality of embedding GitHub Gists in your content after the deprecation of the original shortcode. -{{< /deprecated-in >}} - -To display a GitHub gist with this URL: - -```text -https://gist.github.com/user/50a7482715eac222e230d1e64dd9a89b -``` - -Include this in your Markdown: - -```text -{{}} -``` - -To display a specific file within the gist: - -```text -{{}} -``` diff --git a/docs/content/en/shortcodes/highlight.md b/docs/content/en/shortcodes/highlight.md deleted file mode 100755 index 371a3d46e..000000000 --- a/docs/content/en/shortcodes/highlight.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -title: Highlight shortcode -linkTitle: Highlight -description: Insert syntax-highlighted code into your content using the highlight shortcode. -categories: [] -keywords: [highlight] ---- - -> [!note] -> To override Hugo's embedded `highlight` shortcode, copy the [source code] to a file with the same name in the `layouts/shortcodes` directory. - -> [!note] -> With the Markdown [content format], the `highlight` shortcode is rarely needed because, by default, Hugo automatically applies syntax highlighting to fenced code blocks. -> -> The primary use case for the `highlight` shortcode in Markdown is to apply syntax highlighting to inline code snippets. - -The `highlight` shortcode calls the [`transform.Highlight`] function which uses the [Chroma] syntax highlighter, supporting over 200 languages with more than 40 [highlighting styles]. - -## Arguments - -The `highlight` shortcode takes three arguments. - -```text -{{}} -CODE -{{}} -``` - -CODE -: (`string`) The code to highlight. - -LANG -: (`string`) The language of the code to highlight. Choose from one of the [supported languages]. This value is case-insensitive. - -OPTIONS -: (`string`) Zero or more space-separated key-value pairs wrapped in quotation marks. Set default values for each option in your [site configuration]. The key names are case-insensitive. - -## Example - -```text -{{}} -package main - -import "fmt" - -func main() { - for i := 0; i < 3; i++ { - fmt.Println("Value of i:", i) - } -} -{{}} -``` - -Hugo renders this to: - -{{< highlight go "linenos=inline, hl_Lines=3 6-8, noClasses=true" >}} -package main - -import "fmt" - -func main() { - for i := 0; i < 3; i++ { - fmt.Println("Value of i:", i) - } -} -{{< /highlight >}} - -You can also use the `highlight` shortcode for inline code snippets: - -```text -This is some {{}}fmt.Println("inline"){{}} code. -``` - -Hugo renders this to: - -This is some {{< highlight go "hl_inline=true, noClasses=true" >}}fmt.Println("inline"){{< /highlight >}} code. - -Given the verbosity of the example above, if you need to frequently highlight inline code snippets, create your own shortcode using a shorter name with preset options. - -```go-html-template {file="layouts/shortcodes/hl.html"} -{{ $code := .Inner | strings.TrimSpace }} -{{ $lang := or (.Get 0) "go" }} -{{ $opts := dict "hl_inline" true "noClasses" true }} -{{ transform.Highlight $code $lang $opts }} -``` - -```text -This is some {{}}fmt.Println("inline"){{}} code. -``` - -Hugo renders this to: - -This is some {{< hl >}}fmt.Println("inline"){{< /hl >}} code. - -## Options - -Pass the options when calling the shortcode. You can set their default values in your [site configuration]. - -{{% include "_common/syntax-highlighting-options.md" %}} - -[`transform.Highlight`]: /functions/transform/highlight/ -[Chroma]: https://github.com/alecthomas/chroma -[content format]: /content-management/formats/ -[highlighting styles]: /quick-reference/syntax-highlighting-styles/ -[site configuration]: /configuration/markup/#highlight -[source code]: {{% eturl highlight %}} -[supported languages]: /content-management/syntax-highlighting/#languages diff --git a/docs/content/en/shortcodes/instagram.md b/docs/content/en/shortcodes/instagram.md deleted file mode 100755 index 3256790c6..000000000 --- a/docs/content/en/shortcodes/instagram.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -title: Instagram shortcode -linkTitle: Instagram -description: Embed an Instagram post in your content using the instagram shortcode. -categories: [] -keywords: [] ---- - -> [!note] -> To override Hugo's embedded `instagram` shortcode, copy the [source code] to a file with the same name in the `layouts/shortcodes` directory. - -## Example - -To display an Instagram post with this URL: - -```text -https://www.instagram.com/p/CxOWiQNP2MO/ -``` - -Include this in your Markdown: - -```text -{{}} -``` - -Huge renders this to: - -{{< instagram CxOWiQNP2MO >}} - -## Privacy - -Adjust the relevant privacy settings in your site configuration. - -{{< code-toggle config=privacy.instagram />}} - -disable -: (`bool`) Whether to disable the shortcode. Default is `false`. - -simple -: (`bool`) Whether to enable simple mode for image card generation. If `true`, Hugo creates a static card without JavaScript. This mode only supports image cards, and the image is fetched directly from Instagram's servers. Default is `false`. - -[source code]: {{% eturl instagram %}} diff --git a/docs/content/en/shortcodes/param.md b/docs/content/en/shortcodes/param.md deleted file mode 100755 index 133b2322a..000000000 --- a/docs/content/en/shortcodes/param.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: Param shortcode -linkTitle: Param -description: Insert a parameter from front matter or site configuration into your content using the param shortcode. -categories: [] -keywords: [] ---- - -> [!note] -> To override Hugo's embedded `param` shortcode, copy the [source code] to a file with the same name in the `layouts/shortcodes` directory. - -The `param` shortcode renders a parameter from front matter, falling back to a site parameter of the same name. The shortcode throws an error if the parameter does not exist. - -```text {file="content/example.md"} ---- -title: Example -date: 2025-01-15T23:29:46-08:00 -params: - color: red - size: medium ---- - -We found a {{%/* param "color" */%}} shirt. -``` - -Hugo renders this to: - -```html -

    We found a red shirt.

    -``` - -Access nested values by [chaining](g) the [identifiers](g): - -```text -{{%/* param my.nested.param */%}} -``` - -[source code]: {{% eturl param %}} diff --git a/docs/content/en/shortcodes/qr.md b/docs/content/en/shortcodes/qr.md deleted file mode 100755 index 98d6cee4c..000000000 --- a/docs/content/en/shortcodes/qr.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -title: QR shortcode -linkTitle: QR -description: Insert a QR code into your content using the qr shortcode. -categories: [] -keywords: [] ---- - -{{< new-in 0.141.0 />}} - -> [!note] -> To override Hugo's embedded `qr` shortcode, copy the [source code] to a file with the same name in the `layouts/shortcodes` directory. - -The `qr` shortcode encodes the given text into a [QR code] using the specified options and renders the resulting image. - -Internally this shortcode calls the `images.QR` function. Please read the [related documentation] for implementation details and guidance. - -## Examples - -Use the self-closing syntax to pass the text as an argument: - -```text -{{}} -``` - -Or insert the text between the opening and closing tags: - -```text -{{}} -https://gohugo.io -{{}} -``` - -Both of the above produce this image: - -{{< qr text="https://gohugo.io" class="qrcode" targetDir="images/qr" />}} - -To create a QR code for a phone number: - -```text -{{}} -``` - -{{< qr text="tel:+12065550101" class="qrcode" targetDir="images/qr" />}} - -To create a QR code containing contact information in the [vCard] format: - -```text -{{}} -BEGIN:VCARD -VERSION:2.1 -N;CHARSET=UTF-8:Smith;John;R.;Dr.;PhD -FN;CHARSET=UTF-8:Dr. John R. Smith, PhD. -ORG;CHARSET=UTF-8:ABC Widgets -TITLE;CHARSET=UTF-8:Vice President Engineering -TEL;TYPE=WORK:+12065550101 -EMAIL;TYPE=WORK:jsmith@example.org -END:VCARD -{{}} -``` - -{{< qr level="low" scale=2 alt="QR code of vCard for John Smith" class="qrcode" targetDir="images/qr" >}} -BEGIN:VCARD -VERSION:2.1 -N;CHARSET=UTF-8:Smith;John;R.;Dr.;PhD -FN;CHARSET=UTF-8:Dr. John R. Smith, PhD. -ORG;CHARSET=UTF-8:ABC Widgets -TITLE;CHARSET=UTF-8:Vice President Engineering -TEL;TYPE=WORK:+12065550101 -EMAIL;TYPE=WORK:jsmith@example.org -END:VCARD -{{< /qr >}} - -## Arguments - -text -: (`string`) The text to encode, falling back to the text between the opening and closing shortcode tags. - -level -: (`string`) The error correction level to use when encoding the text, one of `low`, `medium`, `quartile`, or `high`. Default is `medium`. - -scale -: (`int`) The number of image pixels per QR code module. Must be greater than or equal to 2. Default is `4`. - -targetDir -: (`string`) The subdirectory within the [`publishDir`] where Hugo will place the generated image. - -alt -: (`string`) The `alt` attribute of the `img` element. - -class -: (`string`) The `class` attribute of the `img` element. - -id -: (`string`) The `id` attribute of the `img` element. - -loading -: (`string`) The `loading` attribute of the `img` element, either `eager` or `lazy`. - -title -: (`string`) The `title` attribute of the `img` element. - -[`publishDir`]: /configuration/all/#publishdir -[QR code]: https://en.wikipedia.org/wiki/QR_code -[related documentation]: /functions/images/qr/ -[source code]: {{% eturl qr %}} -[vCard]: https://en.wikipedia.org/wiki/VCard diff --git a/docs/content/en/shortcodes/ref.md b/docs/content/en/shortcodes/ref.md deleted file mode 100755 index a52c2bf6e..000000000 --- a/docs/content/en/shortcodes/ref.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -title: Ref shortcode -linkTitle: Ref -description: Insert a permalink to the given page reference using the ref shortcode. -categories: [] -keywords: [] ---- - -> [!note] -> To override Hugo's embedded `ref` shortcode, copy the [source code] to a file with the same name in the `layouts/shortcodes` directory. - -> [!note] -> When working with Markdown, this shortcode is obsolete. Instead, use a [link render hook] that resolves the link destination using the `GetPage` method on the `Page` object. You can either create your own, or simply enable the [embedded link render hook]. The embedded link render hook is automatically enabled for multilingual single-host projects. - -## Usage - -The `ref` shortcode accepts either a single positional argument (the path) or one or more named arguments, as listed below. - -## Arguments - -{{% include "_common/ref-and-relref-options.md" %}} - -## Examples - -The `ref` shortcode typically provides the destination for a Markdown link. - -> [!note] -> Always use [Markdown notation] notation when calling this shortcode. - -The following examples show the rendered output for a page on the English version of the site: - -```md -[Link A]({{%/* ref "/books/book-1" */%}}) - -[Link B]({{%/* ref path="/books/book-1" */%}}) - -[Link C]({{%/* ref path="/books/book-1" lang="de" */%}}) - -[Link D]({{%/* ref path="/books/book-1" lang="de" outputFormat="json" */%}}) -``` - -Rendered: - -```html -Link A - -Link B - -Link C - -Link D -``` - -## Error handling - -{{% include "_common/ref-and-relref-error-handling.md" %}} - -[content format]: /content-management/formats/ -[embedded link render hook]: /render-hooks/links/#default -[link render hook]: /render-hooks/links/ -[Markdown notation]: /content-management/shortcodes/#notation -[source code]: {{% eturl relref %}} diff --git a/docs/content/en/shortcodes/relref.md b/docs/content/en/shortcodes/relref.md deleted file mode 100755 index 219eae81a..000000000 --- a/docs/content/en/shortcodes/relref.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -title: Relref shortcode -linkTitle: Relref -description: Insert a relative permalink to the given page reference using the relref shortcode. -categories: [] -keywords: [] ---- - -> [!note] -> To override Hugo's embedded `relref` shortcode, copy the [source code] to a file with the same name in the `layouts/shortcodes` directory. - -> [!note] -> When working with Markdown, this shortcode is obsolete. Instead, use a [link render hook] that resolves the link destination using the `GetPage` method on the `Page` object. You can either create your own, or simply enable the [embedded link render hook]. The embedded link render hook is automatically enabled for multilingual single-host projects. - -## Usage - -The `relref` shortcode accepts either a single positional argument (the path) or one or more named arguments, as listed below. - -## Arguments - -{{% include "_common/ref-and-relref-options.md" %}} - -## Examples - -The `relref` shortcode typically provides the destination for a Markdown link. - -> [!note] -> Always use [Markdown notation] notation when calling this shortcode. - -The following examples show the rendered output for a page on the English version of the site: - -```md -[Link A]({{%/* ref "/books/book-1" */%}}) - -[Link B]({{%/* ref path="/books/book-1" */%}}) - -[Link C]({{%/* ref path="/books/book-1" lang="de" */%}}) - -[Link D]({{%/* ref path="/books/book-1" lang="de" outputFormat="json" */%}}) -``` - -Rendered: - -```html -Link A - -Link B - -Link C - -Link D -``` - -## Error handling - -{{% include "_common/ref-and-relref-error-handling.md" %}} - -[content format]: /content-management/formats/ -[embedded link render hook]: /render-hooks/links/#default -[link render hook]: /render-hooks/links/ -[Markdown notation]: /content-management/shortcodes/#notation -[source code]: {{% eturl relref %}} diff --git a/docs/content/en/shortcodes/vimeo.md b/docs/content/en/shortcodes/vimeo.md deleted file mode 100755 index 1164ce997..000000000 --- a/docs/content/en/shortcodes/vimeo.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -title: Vimeo shortcode -linkTitle: Vimeo -description: Embed a Vimeo video in your content using the vimeo shortcode. -categories: [] -keywords: [] ---- - -> [!note] -> To override Hugo's embedded `vimeo` shortcode, copy the [source code] to a file with the same name in the `layouts/shortcodes` directory. - -## Example - -To display a Vimeo video with this URL: - -```text -https://vimeo.com/channels/staffpicks/55073825 -``` - -Include this in your Markdown: - -```text -{{}} -``` - -Hugo renders this to: - -{{< vimeo 55073825 >}} - -## Arguments - -id -: (string) The video `id`. Optional if the `id` is provided as a positional argument as shown in the example above. - -allowFullScreen -: {{< new-in 0.146.0 />}} -: (`bool`) Whether the `iframe` element can activate full screen mode. Default is `true`. - -class -: (`string`) The `class` attribute of the wrapping `div` element. Adding one or more CSS classes disables inline styling. - -loading -: {{< new-in 0.146.0 />}} -: (`string`) The loading attribute of the `iframe` element, either `eager` or `lazy`. Default is `eager`. - -title -: (`string`) The `title` attribute of the `iframe` element. - -Here's an example using some of the available arguments: - -```text -{{}} -``` - -## Privacy - -Adjust the relevant privacy settings in your site configuration. - -{{< code-toggle config=privacy.vimeo />}} - -disable -: (`bool`) Whether to disable the shortcode. Default is `false`. - -enableDNT -: (`bool`) Whether to block the Vimeo player from tracking session data and analytics. Default is `false`. - -simple -: (`bool`) Whether to enable simple mode. If `true`, the video thumbnail is fetched from Vimeo and overlaid with a play button. Clicking the thumbnail opens the video in a new Vimeo tab. Default is `false`. - -The source code for the simple version of the shortcode is available [here]. - -[here]: {{% eturl vimeo_simple %}} -[source code]: {{% eturl vimeo %}} diff --git a/docs/content/en/shortcodes/x.md b/docs/content/en/shortcodes/x.md deleted file mode 100755 index f1eebdaf2..000000000 --- a/docs/content/en/shortcodes/x.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: X shortcode -linkTitle: X -description: Embed an X post in your content using the x shortcode. -categories: [] -keywords: [] ---- - -{{< new-in 0.141.0 />}} - -> [!note] -> To override Hugo's embedded `x` shortcode, copy the [source code] to a file with the same name in the `layouts/shortcodes` directory. - -## Example - -To display an X post with this URL: - -```txt -https://x.com/SanDiegoZoo/status/1453110110599868418 -``` - -Include this in your Markdown: - -```text -{{}} -``` - -Rendered: - -{{< x user="SanDiegoZoo" id="1453110110599868418" >}} - -## Privacy - -Adjust the relevant privacy settings in your site configuration. - -{{< code-toggle config=privacy.x />}} - -disable -: (`bool`) Whether to disable the shortcode. Default is `false`. - -enableDNT -: (`bool`) Whether to prevent X from using post and embedded page data for personalized suggestions and ads. Default is `false`. - -simple -: (`bool`) Whether to enable simple mode. If `true`, Hugo builds a static version of the of the post without JavaScript. Default is `false`. - -The source code for the simple version of the shortcode is available [here]. - -If you enable simple mode you may want to disable the hardcoded inline styles by setting `disableInlineCSS` to `true` in your site configuration. The default value for this setting is `false`. - -{{< code-toggle config=services.x />}} - -[here]: {{% eturl x_simple %}} -[source code]: {{% eturl x %}} diff --git a/docs/content/en/shortcodes/youtube.md b/docs/content/en/shortcodes/youtube.md deleted file mode 100755 index ed3cf0632..000000000 --- a/docs/content/en/shortcodes/youtube.md +++ /dev/null @@ -1,91 +0,0 @@ ---- -title: YouTube shortcode -linkTitle: YouTube -description: Embed a YouTube video in your content using the youtube shortcode. -categories: [] -keywords: [] ---- - -> [!note] -> To override Hugo's embedded `youtube` shortcode, copy the [source code] to a file with the same name in the `layouts/shortcodes` directory. - -## Example - -To display a YouTube video with this URL: - -```text -https://www.youtube.com/watch?v=0RKpf3rK57I -``` - -Include this in your Markdown: - -```texts -{{}} -``` - -Hugo renders this to: - -{{< youtube 0RKpf3rK57I >}} - -## Arguments - -id -: (`string`) The video `id`. Optional if the `id` is provided as a positional argument as shown in the example above. - -allowFullScreen -: {{< new-in 0.125.0 />}} -: (`bool`) Whether the `iframe` element can activate full screen mode. Default is `true`. - -autoplay -: {{< new-in 0.125.0 />}} -: (`bool`) Whether to automatically play the video. Forces `mute` to `true`. Default is `false`. - -class -: (`string`) The `class` attribute of the wrapping `div` element. When specified, removes the `style` attributes from the `iframe` element and its wrapping `div` element. - -controls -: {{< new-in 0.125.0 />}} -: (`bool`) Whether to display the video controls. Default is `true`. - -end -: {{< new-in 0.125.0 />}} -: (`int`) The time, measured in seconds from the start of the video, when the player should stop playing the video. - -loading -: {{< new-in 0.125.0 />}} -: (`string`) The loading attribute of the `iframe` element, either `eager` or `lazy`. Default is `eager`. - -loop -: {{< new-in 0.125.0 />}} -: (`bool`) Whether to indefinitely repeat the video. Ignores the `start` and `end` arguments after the first play. Default is `false`. - -mute -: {{< new-in 0.125.0 />}} -: (`bool`) Whether to mute the video. Always `true` when `autoplay` is `true`. Default is `false`. - -start -: {{< new-in 0.125.0 />}} -: (`int`) The time, measured in seconds from the start of the video, when the player should start playing the video. - -title -: (`string`) The `title` attribute of the `iframe` element. Defaults to "YouTube video". - -Here's an example using some of the available arguments: - -```text -{{}} -``` - -## Privacy - -Adjust the relevant privacy settings in your site configuration. - -{{< code-toggle config=privacy.youTube />}} - -disable -: (`bool`) Whether to disable the shortcode. Default is `false`. - -privacyEnhanced -: (`bool`) Whether to block YouTube from storing information about visitors on your website unless the user plays the embedded video. Default is `false`. - -[source code]: {{% eturl youtube %}} diff --git a/docs/content/en/showcase/1password-support/bio.md b/docs/content/en/showcase/1password-support/bio.md deleted file mode 100644 index 32d299bd4..000000000 --- a/docs/content/en/showcase/1password-support/bio.md +++ /dev/null @@ -1,3 +0,0 @@ -**1Password** is a password manager that keeps you safe online. It protects your secure information behind the one password only you know. - -The [1Password Support](https://support.1password.com/) website was built from scratch with **Hugo** and enhanced with **React** and **Elasticsearch** to give us the best of both worlds: The simplicity and performance of a static site, with the richness of a hosted web app. diff --git a/docs/content/en/showcase/1password-support/featured.png b/docs/content/en/showcase/1password-support/featured.png deleted file mode 100644 index 8e46495e6..000000000 Binary files a/docs/content/en/showcase/1password-support/featured.png and /dev/null differ diff --git a/docs/content/en/showcase/1password-support/index.md b/docs/content/en/showcase/1password-support/index.md deleted file mode 100644 index 54a30f849..000000000 --- a/docs/content/en/showcase/1password-support/index.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: 1Password Support -date: 2018-02-22 -description: 'Showcase: "Compiles 400 pages in five languages in the blink of an eye."' -siteURL: https://support.1password.com/ -byline: "[Mitch Cohen](https://github.com/mitchchn), Documentation Team Lead" -aliases: [/showcase/1password/] ---- - -At 1Password, we used to go through a different documentation platform every month: blog engines, ebooks, wikis, site generators written in Ruby and JavaScript. Each was inadequate in its own special way. Then we found **Hugo**. We made one last switch, and we're glad we did. - -### Not all static site generators are created equal - -Finding a tool that will make your customers, writers, designers, _and_ DevOps team happy is no easy task, but we managed it with Hugo: - -**Hugo is static**. We're a security company, so we swear by static sites and use them wherever possible. We feel much safer pointing customers at HTML files than at a complicated server which needs to be hardened. - -**Hugo is Go**. We love the Go programming language at 1Password, and we were delighted to learn that Hugo used the same Go template syntax that our designers and front-end developers had already mastered. - -**Hugo is FAST**. Our previous static site generator took nearly a minute to compile our (then much smaller) site. Developers might be used to this, but it wasn't cutting it for writers who wanted to see live previews of their work. Hugo did the same job in milliseconds, and to this day compiles 400 pages in five languages in the blink of an eye. - -**Hugo is flexible**. Thanks to Hugo's content and layout system, we were able to preserve our existing file and directory structure and port our entire production site in a few days. We could then create new content types that weren't possible before, like these snazzy [showcases](https://support.1password.com/explore/extension/). - -**Hugo is great for writers**. Our documentation team was already comfortable with Markdown and Git and could start creating content for Hugo with zero downtime. Once we added shortcodes, our writers were able to dress up articles with features like [platform boxes](https://support.1password.com/get-the-apps/) with just a bit of new syntax. - -**Hugo has an amazing developer community**. Hugo updates are frequent and filled to the brim with features and fixes. As we developed the multilingual version of our site, we submitted PRs for features we needed and were helped through the process by [@bep](https://github.com/bep) and others. - -**Hugo is simple to deploy**. Hugo has just the right amount of configuration options to fit into our build system without being too complicated. - -### Tech specs - -- [1Password Support](https://support.1password.com) uses Hugo with a custom theme. It shares styles and some template code with [1Password.com](https://1password.com), which we also moved to Hugo in 2016. -- Code and articles live in a private GitHub repository, which is deployed to a static content server using Git hooks. -- Writers build and preview the site on their computers and contribute content using pull requests. -- We use Hugo's [multilingual support](/content-management/multilingual/) to build the site in English, Spanish, French, Italian, German, and Russian. With the help of Hugo, 1Password Support became our very first site in multiple languages. -- Our [contact form](https://support.1password.com/contact) is a single-page React app. We were able to integrate it with Hugo seamlessly thanks to its support for static files. -- The one part of the support site which is not static is our search engine, which we developed with Elasticsearch and host on AWS. diff --git a/docs/content/en/showcase/_index.md b/docs/content/en/showcase/_index.md deleted file mode 100644 index e618e8104..000000000 --- a/docs/content/en/showcase/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Showcases -cascade: - build: - render: never - list: never ---- diff --git a/docs/content/en/showcase/_template/bio.md b/docs/content/en/showcase/_template/bio.md deleted file mode 100644 index 5ea389617..000000000 --- a/docs/content/en/showcase/_template/bio.md +++ /dev/null @@ -1,6 +0,0 @@ -Add some **general info** about Myshowcase here. - -The site is built by: - -- [Person 1](https://example.org) -- [Person 1](https://example.org) diff --git a/docs/content/en/showcase/_template/featured.png b/docs/content/en/showcase/_template/featured.png deleted file mode 100644 index 4f390132e..000000000 Binary files a/docs/content/en/showcase/_template/featured.png and /dev/null differ diff --git a/docs/content/en/showcase/_template/index.md b/docs/content/en/showcase/_template/index.md deleted file mode 100644 index 3103903e1..000000000 --- a/docs/content/en/showcase/_template/index.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: Myshowcase -date: -draft: true -description: A short description of this page. -# The URL to the site on the internet. -siteURL: https://gohugo.io/ -# Link to the site's Hugo source code if public and you can/want to share. -# Remove or leave blank if not needed/wanted. -siteSource: https://github.com/gohugoio/hugoDocs -# Add credit to the article author. Leave blank or remove if not needed/wanted. -byline: '[bep](https://github.com/bep), Hugo Lead' ---- - -To complete this showcase: - -1. Write the story about your site in this file. -1. Add a summary to the `bio.md` file in this directory. -1. Replace the `featured-template.png` with a screenshot of your site. You can rename it, but it must contain the word `featured`. -1. Create a new pull request in https://github.com/gohugoio/hugoDocs/pulls - -The content of this bundle explained: - -index.md -: The main content file. Fill in required front matter metadata and write your story. I does not have to be a novel. It can even be self-promotional, but it should include Hugo in some form. - -bio.md -: A short summary of the website. Site credits (who built it) fits nicely here. - -featured.png -: A reasonably sized screenshot of your website. It can be named anything, but the name must start with "featured". The sample image is `1500x750` (2:1 aspect ratio). diff --git a/docs/content/en/showcase/alora-labs/bio.md b/docs/content/en/showcase/alora-labs/bio.md deleted file mode 100644 index f14a90b75..000000000 --- a/docs/content/en/showcase/alora-labs/bio.md +++ /dev/null @@ -1,3 +0,0 @@ -**Alora Labs** is a product development consultancy headquartered in Toronto, Canada. - -We help companies build software and IoT products and were recently recognized as one of the [**top IoT development firms**](https://aloralabs.com/insights/alora-labs-receives-clutch-2021-top-iot-agency-award) in Toronto. diff --git a/docs/content/en/showcase/alora-labs/featured.png b/docs/content/en/showcase/alora-labs/featured.png deleted file mode 100644 index b8e1f302b..000000000 Binary files a/docs/content/en/showcase/alora-labs/featured.png and /dev/null differ diff --git a/docs/content/en/showcase/alora-labs/index.md b/docs/content/en/showcase/alora-labs/index.md deleted file mode 100644 index 5e676bad3..000000000 --- a/docs/content/en/showcase/alora-labs/index.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: Alora Labs -date: 2021-05-27 -description: 'Showcase: "Making performant websites accessible for everyone."' -siteURL: https://aloralabs.com/ -siteSource: https://github.com/aloralabs/homepage -aliases: [/showcase/aloralabs/] ---- - -At Alora Labs we always have an eye open for new tools and technology that we can utilize to the benefit of our customers or internal projects like our website. - -The previous iteration of our site was built with Jekyll, which served us well at first. However as time went on, we became frustrated with the number of dependencies we had to rely on, that would often break at the most inconvenient times. - -Hugo was a breath of fresh air in this regard, a single binary that works equally well on Windows as it did on macOS or Linux. We no longer need additional tools for image optimization, Sass compilation or JavaScript bundling. Everything just works, and with a substantial performance boost too. - -Hugo has become a favorite tool in the tool belt and the foundation for many client projects. We couldn't be happier with the switch and we are optimistic about recommending Hugo for many years to come. - -Thank you to the vibrant community and talented development team for all the hard work in making Hugo a success. As excellent as Hugo is now, we cannot wait to see what the release notes have in store for us next. diff --git a/docs/content/en/showcase/ampio-help/bio.md b/docs/content/en/showcase/ampio-help/bio.md deleted file mode 100644 index a08b26be7..000000000 --- a/docs/content/en/showcase/ampio-help/bio.md +++ /dev/null @@ -1,10 +0,0 @@ -__We are Ampio.__ We design and manufacture a building automation system that provides control, comfort, safety and reliability. Visit [our page](http://ampio.com/) to learn more about our solution! - -__Ampio Knowledge Base__ is a service built and maintained with Hugo. It is a self-service support platform for our customers and certified installers. It also contains a complete portfolio of our modules---building blocks of the Ampio building automation system. - -The site is built by: - -- [@mgetka](https://github.com/mgetka), developer -- [@SteynAnna](https://github.com/SteynAnna), maintainer - -and other members of the Ampio team responsible for content creation. diff --git a/docs/content/en/showcase/ampio-help/featured.png b/docs/content/en/showcase/ampio-help/featured.png deleted file mode 100644 index 07974e7f1..000000000 Binary files a/docs/content/en/showcase/ampio-help/featured.png and /dev/null differ diff --git a/docs/content/en/showcase/ampio-help/index.md b/docs/content/en/showcase/ampio-help/index.md deleted file mode 100644 index 462452cb1..000000000 --- a/docs/content/en/showcase/ampio-help/index.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -title: Ampio Knowledge Base -date: 2022-10-30 -description: "Knowledge base for the Ampio building automation system." -siteURL: https://help.ampio.com/ ---- - -As a company that specializes in highly customizable smart solutions for various industries, Ampio has accumulated a vast amount of knowledge throughout the years. We were on the lookout for a user-friendly platform to impart this knowledge to our clients and installers. Delivering a service that caters to both audiences, scattered around the globe with vastly divergent needs and expectations, was a challenge. - -On the one hand, we needed something that would let us educate a client with no technical knowledge about our system in a visually appealing way. - -On the other hand, our installers required technical drawings, offline manuals, and a deep dive into highly specialized subjects. - -Over and above that, we could not overlook the fact that our internal team of editors and maintainers of the Knowledge Base included non-programmers who had to be able to create content and navigate the architecture of the site just as well as those adept at coding. - -We started our journey with the following requirements: - - - Ease of contribution - - Efficient search capabilities - - The possibility of deployment to simple shared hosting - - Proper support for multilingualism - -## Dark ages of WordPress - -With the above-mentioned in mind, we built our first revision of the service in WordPress with a commercial knowledge base plugin. The initial requirements seemed not to be exorbitant, and yet we were surprised to see that only a few of the available solutions covered them. Especially, the case of multilingualism appeared to be particularly neglected across the available products. - -The WordPress-based products made big promises: pay some bucks, bootstrap the service in minutes, and forget about all the development troubles. And although those promises could possibly be deliverable on WordPress' end, it was definitely not true for anything more than the most generic deployments. In our case, we were dealing with more and more trade-offs. Plus, the solution was just slow on the simple shared hosting environment that we dedicated to the job. - -## Turning point - -The turning point was the introduction of a new key requirement---each document was to be downloadable in the PDF format. Such functionality was not available in the plugins we owned, nor did it look like any of the other existing WordPress plugins could fulfill our needs to a satisfactory degree. Nobody in our team was brave enough to add such a functionality to the current stack, so we decided to start from scratch. - -On top of that new development, we had to remember another one of our key requirements, namely, that mostly non-programmers were to be responsible for the service maintenance and content creation. Initially, we were leaning towards headless CMS-based solutions, but finally we made a bold move and decided to create a Git-managed Jamstack service and see what happens. - -## Hugo to the rescue - -Hugo was our first choice of SSG. The multilingualism support was the primary feature that convinced us. Later on, going through the documentation, we continued to discover new exciting features that we didn't even know we needed when we started. - -The rich functionalities of WordPress WYSIWYG editors soon turned out to be a curse. It became burdensome to maintain formatting consistency across documents prepared by multiple contributors. When we considered Markdown, we knew that it would give us a lot less flexibility. In our case, it proved to be a blessing in disguise---the constraints imposed by the notation ensured that each document was prepared in the same way. And in the cases where Markdown was not enough, Hugo shortcodes gave us all that we needed to get the results we anticipated. - -In terms of PDF generation, we utilized [custom output formats](/configuration/output-formats/) to produce intermediary document representations, which are consumed by our custom tool transforming them to TeX documents, which are finally used to produce PDF files. - -Custom output formats were also used to create search indexes. The search functionality is built on the brilliant [TNTSearch](https://github.com/teamtnt/tntsearch) library. The search queries and results are handled by PHP snippets embedded into static documents handled by Hugo. - -We even implemented a simple REST API generated by Hugo! We have yet to find something that cannot be achieved with this stack, while in WordPress-based solutions we were struggling with things as simple as defining custom document ordering in one of the categories list views. - -When talking about Hugo, we cannot forget about the speed. At the beginning we were not considering it a killer feature, but as our document base grew bigger, we appreciated it more and more. Dry-runs are not so common---most of the time we are working on one of the documents with cache already built during one of the previous Hugo runs. In such a scenario, Hugo rebuilds the site in about a second and we consider it a very good result. - -```text - | EN | PL --------------------+-----+------ - Pages | 483 | 486 - Paginator pages | 56 | 55 - Non-page files | 745 | 749 - Static files | 917 | 917 - Processed images | 487 | 490 - Aliases | 80 | 79 - Sitemaps | 2 | 1 - Cleaned | 0 | 0 - -Total in 1096 ms -``` - -## Adaptation among the contributors - -Very quickly it became apparent that our initial concerns about the adaptation of the workflow among contributors were grossly exaggerated. Markdown is fairly straightforward and did not cause any trouble for the contributors. - -We recommended that our colleagues use Visual Studio Code as a tool for content creation. The project's repository tracks project-scoped configuration of the editor, which includes a set of _tasks_ allowing to run a live server from the GUI level. This is very useful for those who are easily frightened when faced with the mighty terminal. - -The basic skills of the Git workflow were also easily acquired. At the end of the day, builds and deployments are fully managed by [CI/CD](g) processes, so the administration of the service drills down to reviewing and accepting merge requests in the Git frontend. As a side effect, we receive a full and clear history of contributions, which is well appreciated by our quality assurance auditors. - -We could even say that our experiment spread the love for Git among non-programmers in our organization! - -## Summary - -Hugo is the best! Definitely give it a try if you are ever faced with a challenge similar to ours. And do not give it a second thought if your service contributors are not too technically inclined---it might still turn out great! diff --git a/docs/content/en/showcase/bypasscensorship/bio.md b/docs/content/en/showcase/bypasscensorship/bio.md deleted file mode 100644 index a6c98f9ba..000000000 --- a/docs/content/en/showcase/bypasscensorship/bio.md +++ /dev/null @@ -1,6 +0,0 @@ -Bypass Censorship find and promote tools that provide Internet access to everyone. - -The site is built by: - -- [Leyla Avsar](https://www.leylaavsar.com/) (designer) -- [Fredrik Jonsson](https://xdeb.net/) (dev) diff --git a/docs/content/en/showcase/bypasscensorship/featured.png b/docs/content/en/showcase/bypasscensorship/featured.png deleted file mode 100644 index d6f429112..000000000 Binary files a/docs/content/en/showcase/bypasscensorship/featured.png and /dev/null differ diff --git a/docs/content/en/showcase/bypasscensorship/index.md b/docs/content/en/showcase/bypasscensorship/index.md deleted file mode 100644 index bd1a072c0..000000000 --- a/docs/content/en/showcase/bypasscensorship/index.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: Bypass Censorship -date: 2019-06-16 -description: 'Showcase: "Bypass Censorship find and promote tools that provide Internet access to everyone."' -siteURL: https://www.bypasscensorship.org/ -byline: "[Fredrik Jonsson](https://xdeb.net/), Web developer & Linux sysadmin" ---- - -The British Broadcasting Corporation (BBC) (UK), Deutsche Welle (DW) (Germany), France Médias Monde (FMM) (France), the U.S. Agency for Global Media (USAGM) (US) and the Open Technology Fund (OTF) (US) co-sponsor the Bypass Censorship website. - -Websites of international news agencies are often blocked in many countries. In order to connect people to these sites, Bypass Censorship feature and recommend tools in the following languages: English, French, Spanish, Arabic, Farsi, Chinese, and Russian. - -One of the tools is the Bypass Censorship Extension for Firefox and Chrome. The extension help direct people to mirrors of partners sites if they are being censored. - -The first version of the site was built in Drupal 8 but it was relaunched as a static site built with Hugo in 2019. - -Security, page load time and easy of hosting is the main reasons for switching to a static site. As the lead developer I had good experience with Hugo and was interested in exploring the multilingual features. - -It's a simply site, basically one page in seven languages. I had no problems getting Hugo to output what I wanted. Found the multilingual support straight forward and easy to work with. - -Thanks to the design by [Leyla Avsar](https://www.leylaavsar.com/) the site also looks good. I used the [Hugo Zen theme](https://github.com/frjo/hugo-theme-zen) with a few custom templates and the needed CSS. - -The editors can maintain content via [Forestry.io CMS](https://forestry.io/) or directly via Git. Forestry does unfortunately not have multilingual support. All the language versions are in one pile making it harder to find the right file to edit, but it works. diff --git a/docs/content/en/showcase/digitalgov/bio.md b/docs/content/en/showcase/digitalgov/bio.md deleted file mode 100644 index 70bb990b9..000000000 --- a/docs/content/en/showcase/digitalgov/bio.md +++ /dev/null @@ -1 +0,0 @@ -**Digital.gov** helps people in the U.S. government deliver better, more accessible digital services through publishing essential guidance, resources, tools, and online events that make it easier for people to design, build, and deliver essential services for the public. diff --git a/docs/content/en/showcase/digitalgov/featured.png b/docs/content/en/showcase/digitalgov/featured.png deleted file mode 100644 index 7d065dce9..000000000 Binary files a/docs/content/en/showcase/digitalgov/featured.png and /dev/null differ diff --git a/docs/content/en/showcase/digitalgov/index.md b/docs/content/en/showcase/digitalgov/index.md deleted file mode 100644 index 7f0584712..000000000 --- a/docs/content/en/showcase/digitalgov/index.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -title: Digital.gov -date: 2020-05-01 -description: 'Showcase: "Guidance on building better digital services in government."' -siteURL: https://digital.gov/ -siteSource: https://github.com/gsa/digitalgov.gov ---- - -For over a decade, Digital.gov has provided guidance, training, and community support to the people who are responsible for delivering digital services in the U.S. government. Essentially, it is a place where people can find examples of problems being solved in government, and get links to the tools and resources they need. - -Through collaboration in our communities of practice, Digital.gov is a window into the people who work in technology in government and the challenges they face making digital services stronger and more effective. [Read more about our site »](https://digital.gov/2019/12/19/a-new-digitalgov/) - -Digital.gov is built using the [U.S. Web Design System](https://designsystem.digital.gov/) (USWDS) and have followed the [design principles](https://designsystem.digital.gov/maturity-model/) in building out our new site: - -- **Start with real user needs** — We used human-centered design methods to inform our product decisions (like qualitative user research), and gathered feedback from real users. We also continually test our assumptions with small experiments. -- **Earn trust** —We recognize that trust has to be earned every time. We are including all [required links and content](https://digital.gov/resources/required-web-content-and-links/) on our site, clearly identifying as a government site, building with modern best practices, and using HTTPS. -- **Embrace accessibility** — [Accessibility](https://digital.gov/resources/intro-accessibility/) affects everybody, and we built it into every decision. We're continually working to conform to Section 508 requirements, use user experience best practices, and support a wide range of devices. -- **Promote continuity** — We started from shared solutions like USWDS and [Federalist](https://federalist.18f.gov/). We designed our site to clearly identify as a government site by including USWDS's .gov banner, common colors and patterns, and built with modern best practices. -- **Listen** — We actively collect user feedback and web metrics. We use the [Digital Analytics Program](https://digital.gov/services/dap/) (DAP) and analyze the data to discover actionable insights. We make small, incremental changes to continuously improve our website by listening to readers and learning from what we hear. - -_More on the [USWDS maturity model »](https://designsystem.digital.gov/maturity-model/)_ - -## Open tools - -We didn't start from scratch. We built and designed the Digital.gov using many of the open-source tools and services that we develop for government here in the [Technology Transformation Services](https://www.gsa.gov/tts/) (TTS). - -Using services that make it possible to design, build, and iterate quickly are essential to modern web design and development, which is why [Federalist](https://federalist.18f.gov/) and the [U.S. Web Design System](https://designsystem.digital.gov/) are such a great combination. - -**Why Hugo?** Well, with around `~3,000` files _(and growing)_ and `~9,000` built pages, we needed a site generator that could handle that volume with lightning fast speed. - -Hugo was the clear option. The [Federalist](https://federalist.18f.gov/) team quickly added it to their available site generators, and we were off. - -At the moment, it takes around `32 seconds` to build close to `~10,000` pages! - -Take a look: - -```text - - | EN --------------------+------- - Pages | 7973 - Paginator pages | 600 - Non-page files | 108 - Static files | 851 - Processed images | 0 - Aliases | 1381 - Sitemaps | 1 - Cleaned | 0 - -Built in 32.427 seconds -``` - -In addition to Hugo, we are proudly using a number of other tools and services, all built by government are free to use: - -- [Federalist](https://federalist.18f.gov/) -- [Search.gov](https://www.search.gov/) — A free, hosted search platform for federal websites. -- [Cloud.gov](https://www.cloud.gov/) — helps teams build, run, and authorize cloud-ready or legacy government systems quickly and cheaply. -- [Federal CrowdSource Mobile Testing Program](https://digital.gov/services/service_mobile-testing-program/) — Free mobile compatibility testing by feds, for feds. -- [Digital Analytics Program](https://digital.gov/services/dap/) (DAP) — A free analytics tool for measuring digital services in the federal government -- [Section508.gov](https://www.section508.gov/) and [PlainLanguage.gov](https://www.plainlanguage.gov/) resources -- [API.data.gov](https://api.data.gov/) — a free API management service for federal agencies -- [U.S. Digital Registry](https://digital.gov/services/u-s-digital-registry/) — A resource for confirming the official status of government social media accounts, mobile apps, and mobile websites. - -**Questions or feedback?** [Submit an issue](https://github.com/GSA/digitalgov.gov/issues) or send us an email to [digitalgov@gsa.gov](mailto:digitalgov@gsa.gov) :heart: diff --git a/docs/content/en/showcase/fireship/bio.md b/docs/content/en/showcase/fireship/bio.md deleted file mode 100644 index 2a5639aa7..000000000 --- a/docs/content/en/showcase/fireship/bio.md +++ /dev/null @@ -1,5 +0,0 @@ -**Fireship.io** is an ecosystem of detailed and practical resources for developers who want to build and ship high-quality apps. - -The site is built by: - -- [Jeff Delaney](https://fireship.io/contributors/jeff-delaney/) diff --git a/docs/content/en/showcase/fireship/featured.png b/docs/content/en/showcase/fireship/featured.png deleted file mode 100644 index 33d1a47c5..000000000 Binary files a/docs/content/en/showcase/fireship/featured.png and /dev/null differ diff --git a/docs/content/en/showcase/fireship/index.md b/docs/content/en/showcase/fireship/index.md deleted file mode 100644 index 454ee87d7..000000000 --- a/docs/content/en/showcase/fireship/index.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: fireship.io -date: 2019-02-02 -description: 'Showcase: "Hugo helps us create complex technical content that integrates engaging web components."' -siteURL: https://fireship.io -siteSource: https://github.com/fireship-io/fireship.io -byline: "[Jeff Delaney](https://github.com/codediodeio), Fireship.io Creator" ---- - -After careful consideration of JavaScript/JSX-based static site generators, it became clear that Hugo was the only tool capable of handling our project's complex demands. Not only do we have multiple content formats and taxonomies, but we often need to customize the experience at a more granular level. The problems Hugo has solved for us include: - -- **Render speed.** We know from past experience that JavaScript-based static site generators become very slow when you have thousands of pages and images. -- **Feature-rich.** Our site has a long list of specialized needs and Hugo somehow manages to cover every single use case. -- **Composability.** Hugo's partial and shortcode systems empower us to write DRY and maintainable templates. -- **Simplicity.** Hugo is easy to learn (even without Go experience) and doesn't burden us with brittle dependencies. - -The site is able to achieve Lighthouse performance scores of 95+, despite the fact that it is a fully interactive PWA that ships Angular and Firebase in the JS bundle. This is made possible by (1) prerendering content with Hugo and (2) lazily embedding native web components directly in the HTML and Markdown. We provide a [detailed explanation](https://youtu.be/gun8OiGtlNc) of the architecture on YouTube and can't imagine development without Hugo. diff --git a/docs/content/en/showcase/forestry/bio.md b/docs/content/en/showcase/forestry/bio.md deleted file mode 100644 index 23951a1c6..000000000 --- a/docs/content/en/showcase/forestry/bio.md +++ /dev/null @@ -1,3 +0,0 @@ -Forestry.io is a Git-backed CMS (content management system) for websites and web products built using static site generators such as Hugo. - -Forestry bridges the gap between developers and their teams, by making development fun and easy, while providing powerful content management for their teams. diff --git a/docs/content/en/showcase/forestry/featured.png b/docs/content/en/showcase/forestry/featured.png deleted file mode 100644 index 1ee315e78..000000000 Binary files a/docs/content/en/showcase/forestry/featured.png and /dev/null differ diff --git a/docs/content/en/showcase/forestry/index.md b/docs/content/en/showcase/forestry/index.md deleted file mode 100644 index 5b8872316..000000000 --- a/docs/content/en/showcase/forestry/index.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: Forestry.io -date: 2018-03-16 -description: 'Showcase: "Seeing Hugo in action is a whole different world of awesome."' -siteURL: https://forestry.io/ -siteSource: https://github.com/forestryio/forestry.io ---- - -It was clear from the get-go that we had to go with a static site generator. Static sites are secure, performant, and give you 100% flexibility. At [Forestry.io](https://forestry.io/) we provide Content Management Solutions for websites built with static site generators, so we might be a little biased. The only question: Which static site generator was the right choice for us? - -### Why Hugo? - -In our early research we looked at Ionic's [site](https://github.com/ionic-team/ionic) to get some inspiration. They used Jekyll to build their website. While Jekyll is a great generator, the build times for larger sites can be painfully slow. With more than 150 pages plus many custom configurations and add-ons, our website doesn't fall into the low-volume category anymore. Our developers want a smooth experience when working on the website and our content editors need the ability to preview content quickly. In short, we need our builds to be lightning fast. - -We knew Hugo was fast but we did [some additional benchmarking](https://forestry.io/blog/hugo-vs-jekyll-benchmark/) before making our decision. Seeing Hugo in action is a whole different world of awesome. Hugo takes less than one second to build our 150-page site! Take a look: - -```text - | EN -+------------------+-----+ - Pages | 141 - Paginator pages | 4 - Non-page files | 0 - Static files | 537 - Processed images | 0 - Aliases | 60 - Sitemaps | 1 - Cleaned | 0 - -Total in 739 ms -``` - -In fact, we liked Hugo so much that our wizard Chris made his workflow public and we started the open-source project [Create-Static-Site](https://github.com/forestryio/create-static-site). It's [a simple way to spin up sites](https://forestry.io/blog/up-and-running-with-hugo/) and set up a modern web development workflow with one line of code. Essentially it adds build configurations as a dependency for JS, CSS and Image Processing. - -Lastly, we want to take the opportunity to give some love to other amazing tools we used building our website. - -### What tools did we use? - -- Our Norwegian designer Nichlas is in love with [**Sketch**](https://www.sketchapp.com/). From what we hear it's a designer's dream come true. -- Some say our main graphic is [mesmerizing](https://x.com/hmncllctv/status/968907474664284160). Nichlas created it using [**3DS Max**](https://www.autodesk.com/products/3ds-max/overview). -- [**Hugo**](https://gohugo.io/) -- of course. -- Chris can't think of modern web development without [**Gulp**](https://gulpjs.com/) & [**Webpack**](https://webpack.js.org/). We used them to add additional build steps such as Browsersync, CSS, JS and SVG optimization. -- Speaking about adding steps to our build, our lives would be much harder without [**CircleCI**](https://circleci.com/) for continuous deployment and automated testing purposes. -- We can't stop raving about [**Algolia**](https://www.algolia.com/). Chris loves it and even wrote a tutorial on [how to implement Algolia](https://forestry.io/blog/search-with-algolia-in-hugo/) into static sites using Hugo's [custom output formats](/configuration/output-formats/). -- [**Cloudinary**](https://cloudinary.com/) is probably one of the easiest ways to get responsive images into your website. -- We might be a little biased on this one - We think [**Forestry.io**](https://forestry.io/) is a great way to add a content management system with a clean UI on top of your site without interrupting your experience as a developer. -- For hosting purposes we use the almighty [**AWS**](https://aws.amazon.com/). -- [**Formspree.io**](https://formspree.io/) is managing our support and enterprise requests. -- We also use browser cookies and JS to customize our user's experience and give it a more dynamic feel. diff --git a/docs/content/en/showcase/godot-tutorials/bio.md b/docs/content/en/showcase/godot-tutorials/bio.md deleted file mode 100644 index fd849f844..000000000 --- a/docs/content/en/showcase/godot-tutorials/bio.md +++ /dev/null @@ -1,7 +0,0 @@ -[Godot Tutorials](https://godottutorials.com) aims to teach beginners how to get up and running with basic game programming and game development skills. - -The website is built with the **Hugo Framework** alongside aws+cloudfront+lambda. - -The site is built by: - -- [Godot Tutorials](https://godottutorials.com) diff --git a/docs/content/en/showcase/godot-tutorials/featured.png b/docs/content/en/showcase/godot-tutorials/featured.png deleted file mode 100644 index fef13b996..000000000 Binary files a/docs/content/en/showcase/godot-tutorials/featured.png and /dev/null differ diff --git a/docs/content/en/showcase/godot-tutorials/index.md b/docs/content/en/showcase/godot-tutorials/index.md deleted file mode 100644 index fe4f9337e..000000000 --- a/docs/content/en/showcase/godot-tutorials/index.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: Godot Tutorials -date: 2021-01-07 -description: "Teaching game development skills with love." -siteURL: https://godottutorials.com -byline: "[Godot Tutorials](https://godottutorials.com), Web Developer & Game Programmer" ---- - -[Godot Tutorials](https://godottutorials.com) started as a way to teach beginners game programming and game development. -As I created videos, I ran into a problem; if I made a mistake with a YouTube video, it was difficult to correct errors. - -I discovered that blogging episodes and having articles that teach on top of my videos is a fantastic solution to my problem. - -As I researched blogging platforms, I came across two solutions; however, I chose [Hugo](https://gohugo.io) because it's built with Markdown in mind and simplified my workflow. - -In a sense, with [Hugo](https://gohugo.io) programmed the right way, I can focus **more time on planning, creating, and editing** -my videos and **less time maintaining and fixing** my website. diff --git a/docs/content/en/showcase/hapticmedia/bio.md b/docs/content/en/showcase/hapticmedia/bio.md deleted file mode 100644 index 4423edb70..000000000 --- a/docs/content/en/showcase/hapticmedia/bio.md +++ /dev/null @@ -1 +0,0 @@ -**Hapticmedia** provides interactive 3D configurators for eCommerce. diff --git a/docs/content/en/showcase/hapticmedia/featured.png b/docs/content/en/showcase/hapticmedia/featured.png deleted file mode 100644 index a47ea9c2c..000000000 Binary files a/docs/content/en/showcase/hapticmedia/featured.png and /dev/null differ diff --git a/docs/content/en/showcase/hapticmedia/index.md b/docs/content/en/showcase/hapticmedia/index.md deleted file mode 100644 index 52c3337bf..000000000 --- a/docs/content/en/showcase/hapticmedia/index.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: Hapticmedia Blog -date: 2019-10-01 -description: 'Showcase: "A simple, but powerful, multilingual blog."' -siteURL: https://hapticmedia.fr/blog/en/ -byline: "[Cyril Bonnet](https://github.com/monsieurnebo), Web Developer" ---- - -Our goal was to create a simple, effective and multilingual blog on [3D technology](https://hapticmedia.fr/blog/en/3d-technology/) that could be managed by a non-technical profile. - -## Why Hugo? - -Hugo addresses all these needs, coupled with [Forestry.io](https://forestry.io/) for its administration via a "turnkey" interface. We have attached particular importance to SEO, and therefore to the creation of an advanced taxonomy system. Thus, each author and tag has a dedicated page, listing the related posts. - -## What we liked - -- The **multilingual** content support, especially simple to setup. -- The **multiple environments** support (develop, staging, test, production, ...). -- Although a hard start with the Go language, the power of the **Hugo's templating**. -- The **partial layouts**, including the `internals` (e.g. social meta tags). -- The **build time**, unbeatable ⚡️⚡️⚡️. - -## Tools & workflow - -- We used the same design as **[our website](https://hapticmedia.fr/en/)**, recreated as a Hugo HTML template. -- **[Hugo](https://gohugo.io)** for the static website generator. -- **[CircleCI](https://circleci.com)** for continuous integration & deployment. -- **[AWS](https://aws.amazon.com/)** for web hosting. -- **[Forestry.io](https://forestry.io)** for the content management. - -**All of these tools allow our editor to manage the blog's content without having to worry about its technical aspect, which is managed by the developers.** diff --git a/docs/content/en/showcase/hartwell-insurance/bio.md b/docs/content/en/showcase/hartwell-insurance/bio.md deleted file mode 100644 index 4cded7beb..000000000 --- a/docs/content/en/showcase/hartwell-insurance/bio.md +++ /dev/null @@ -1,5 +0,0 @@ -Hartwell Insurance is an insurance company set up solely to service the Broker community. - -By combining **Hugo**, **Service Worker** and **Netlify**, we were able to achieve incredible global site performance. - -The site was built by [Tomango](https://www.tomango.co.uk) diff --git a/docs/content/en/showcase/hartwell-insurance/featured.png b/docs/content/en/showcase/hartwell-insurance/featured.png deleted file mode 100644 index ced251f98..000000000 Binary files a/docs/content/en/showcase/hartwell-insurance/featured.png and /dev/null differ diff --git a/docs/content/en/showcase/hartwell-insurance/hartwell-columns.png b/docs/content/en/showcase/hartwell-insurance/hartwell-columns.png deleted file mode 100644 index c9d36b67d..000000000 Binary files a/docs/content/en/showcase/hartwell-insurance/hartwell-columns.png and /dev/null differ diff --git a/docs/content/en/showcase/hartwell-insurance/hartwell-lighthouse.png b/docs/content/en/showcase/hartwell-insurance/hartwell-lighthouse.png deleted file mode 100644 index a882f01fd..000000000 Binary files a/docs/content/en/showcase/hartwell-insurance/hartwell-lighthouse.png and /dev/null differ diff --git a/docs/content/en/showcase/hartwell-insurance/hartwell-webpagetest.png b/docs/content/en/showcase/hartwell-insurance/hartwell-webpagetest.png deleted file mode 100644 index f60994ea1..000000000 Binary files a/docs/content/en/showcase/hartwell-insurance/hartwell-webpagetest.png and /dev/null differ diff --git a/docs/content/en/showcase/hartwell-insurance/index.md b/docs/content/en/showcase/hartwell-insurance/index.md deleted file mode 100644 index 07ee6182c..000000000 --- a/docs/content/en/showcase/hartwell-insurance/index.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -title: Hartwell Insurance -date: 2018-02-09 -description: 'Showcase: "Hugo + Netlify + PWA makes for a rapid website."' -siteURL: https://www.hartwell-insurance.com/ -byline: "[Trys Mudford](http://www.trysmudford.com), Lead Developer, Tomango" ---- - -We've just launched a shiny new website for [Hartwell Insurance](https://www.hartwell-insurance.com/). I'm really proud of it. It was tackled in a different way to most previous Tomango site builds, using some fancy new tools and some vintage web standards. - -It's a multi-page, single-page (!) website written in Hugo, a static site generator built with performance as a first-class feature. _I've outlined a load of benefits to Hugo & static sites [here](https://why-static.netlify.com/), in case you're interested._ - -> **In essence, a static site generator pre-renders the whole site into HTML files and serves them like it's 1995.** - -There's no Apache or Node backend that does compilation at runtime, it's all done at the build step. This means the server; Netlify in this case, only has to do one thing: serve files. Unsurprisingly, serving simple files is VERY quick. - -The starter point was the [Victor Hugo](https://github.com/netlify/victor-hugo) repository that Netlify have created. It let me dive in with Hugo, PostCSS, Browsersync and ES6 without setting up any tooling myself---always a win! - -I then took all the content from the design file and moved it into Markdown, putting shortcodes in where necessary. This site did need a number of custom shortcodes for the presentational elements like the expanding circles and full width backgrounds. But mostly it was just clean, semantic HTML with some CSS and JS enhancement thrown in. - -For example, this two column layout shown below. I used CSS Columns with a `break-after: always;` on the `

    `. No multi-wrapper or difficult-to-clear shortcodes, just clean HTML. - -![The multi-column setup on Hartwell Insurance](hartwell-columns.png) - -For the ripple effects on the section headings, I used JS to prepend a `` element then animated it with `RequestAnimationFrame`. It adds a nice bit of movement on the page. - -On the Hartwell Profitmaker section, I toyed with the idea of using Vue.js for the calculator, but after giving it some thought, I decided to code in Vanilla. The result, all of the site JS comes in at 3.2KB! - -The plan was to host with Netlify and therefore get access to Netlify Forms. It meant spending 0 minutes on getting a backend set up so I could focus fully on the frontend. - -Cache invalidation isn't normally something I spend all that much time thinking about when building a site. But as this site was going to be a Progressive Web App, invalidating files would be important to ensure the site didn't appear broken when we made changes. As I was using Victor-Hugo, I wasn't really sure how to best tackle this and sadly spent far too many hours wrangling with Webpack and Gulp files to try and get hashed file names working nicely. - -Then; while I was waiting for a haircut, I read a [Netlify blog post](https://www.netlify.com/blog/2017/02/23/better-living-through-caching/) on how they do cache invalidation with HTTP2 and it promptly blew my mind. - -When you request an asset, they send an ETag in the headers which is a hash of the file. There's also a header to tell the browser not to trust it's own cache (which sounds a little bit bonkers). - -So when you request the page, it opens a persistent HTTP2 connection up (so no new connections for file requests). When it gets to requesting that asset, the browser sends the ETag back to Netlify and they either return nothing if the ETag matches, or the new file with the new ETag. No `app.klfjlkdsfjdslkfjdslkfdsj.js` or `app.js?v=20180112`. Just a clean `app.js` with instant cache invalidation. Amazing. - -Finally, the [Service Worker](https://www.hartwell-insurance.com/sw.js) could be added. This turned out to be straightforward as the Netlify cache invalidation system solved most of the pain points. I went for a network-first, cache-fallback setup for both assets and HTML. This does mean flaky speeds are reliant on the page connection time, but given we're on HTTP2, I'm hoping the persistent connection and tiny ETag size will keep it quick. For online connections, every request is up to date and instantly live after any update. Offline connections fall back to every assets' last cached state. It seems to work really nicely, and there's no need for an update prompt if assets have changed. - ---- - -## The results - -The WebPageTest results are looking good. The speed index is 456, 10x smaller than the average Alexa top 300,000 score. - -![WebPageTest results](hartwell-webpagetest.png) - -[TestMySite.io](https://testmysite.io/5a7e1bb2df99531a23c9ad2f/hartwell-insurance.com) is return ~2ms time to first byte from the CDN edge nodes. Lighthouse audits are also very promising. There's still some improvement to be gained lazy-loading the images and inlining the CSS. I'm less excited about the [second suggestion](http://www.trysmudford.com/css-in-2017/), but I'll certainly look at some lazy-loading, especially as I'm already using `IntersectionObserver` for some animations. - -![Lighthouse results](hartwell-lighthouse.png) - -The most encouraging result is how quick the site is around the world. Most Tomango clients (and their customers) are pretty local and almost exclusively UK-based. We have a dedicated server in Surrey that serves our market pretty well. It did take me by surprise just how much slower a connection from the USA, Australia and Japan to our server was. They're waiting ~500ms just for the first byte, let alone downloading each asset. - -[Hartwell Insurance](https://www.hartwell-insurance.com/) are a US company so by putting them on our server, we'd be instantly hampering their local response times by literally seconds. This was one of the main reasons for going with Netlify. They provide global CDN hosting that's quick from anywhere in the world. - ---- - -This project was such a blast to develop, it's a real pleasure to put new technologies to good use in production, and to see real performance and usability benefits from them. Even using classic web methods of serving directories with files is fun when you've been using dynamic systems for a while---there's something really pure about it. - ---- - -_This was originally posted on [my website](http://www.trysmudford.com/perfomance-wins-with-hugo-and-netlify/)_ diff --git a/docs/content/en/showcase/keycdn/bio.md b/docs/content/en/showcase/keycdn/bio.md deleted file mode 100644 index 90f623dca..000000000 --- a/docs/content/en/showcase/keycdn/bio.md +++ /dev/null @@ -1 +0,0 @@ -[KeyCDN](https://www.keycdn.com) is a high performance content delivery network (CDN) offering many powerful features, including image processing that can transform and optimize images in real time. Our network offers global coverage to speed up content delivery and is capable of delivering entire static websites, like those built with Hugo, at the edge. diff --git a/docs/content/en/showcase/keycdn/featured.png b/docs/content/en/showcase/keycdn/featured.png deleted file mode 100644 index 46018a8f9..000000000 Binary files a/docs/content/en/showcase/keycdn/featured.png and /dev/null differ diff --git a/docs/content/en/showcase/keycdn/index.md b/docs/content/en/showcase/keycdn/index.md deleted file mode 100644 index c6972325f..000000000 --- a/docs/content/en/showcase/keycdn/index.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: KeyCDN -date: 2020-04-10 -description: 'Showcase: "Hugo has become an integral part of our stack."' -siteURL: https://www.keycdn.com ---- - -At KeyCDN one of our primary focuses is on performance. With speed being ingrained in our DNA we knew from the start that we must use a fast static website generator that could meet our requirements. When evaluating the right solution, Hugo met our requirements and we looked no further as it was the fastest and most flexible. - -## Why we chose Hugo - -Before our migration to Hugo our website was powered by a PHP-based website that had about 50 pages and a WordPress website that had over 500 posts between our blog and knowledge base. This became harder to maintain as time continued. We felt like we were losing the speed and flexibility that we require. To overcome this we knew we needed to convert our website to be static. This would allow our website to be faster and more secure as it could be delivered by all of our edge locations. - -It wasn't an easy task at the beginning, however, after evaluating Hugo and benchmarking it we knew we had found the ideal solution. Hugo was by far the fastest setup and offered an intuitive way to build our entire website exactly as needed. The Go-based templates, shortcodes, and configuration options made it easy to build a complex website. - -In the fall of 2018 we started the migration and within a couple short months we had built a custom static website with Hugo and migrated all content from our old systems. The simplicity and vast amount of functionality that Hugo offers made this process fast and left our entire team, including all of our writers and developers, happy with the migration. Since migrating to Hugo we haven't looked back. Hugo has become an integral part of our stack. We're grateful to all those who have contributed to make Hugo what it is today. - -## Technical overview - -Below is an overview of what we used with Hugo to build our website: - -- [KeyCDN](https://www.keycdn.com) uses a custom theme and is our primary hub for all style sheets and JavaScript. Our other websites, like [KeyCDN Tools](https://tools.keycdn.com), only import the required style sheets and JavaScript. -- We use [Gulp](https://gulpjs.com) in our build process for many tasks, such as combining, versioning, and compressing our style sheets as well as our JavaScript. -- Our search is powered by a custom solution that we've built. It allows our pages, blog, and knowledge base to be searched. It uses [Axios](https://github.com/axios/axios) to send a `POST` request containing the search query. An index file in JSON generated by Hugo is searched and the results are then returned. -- Our commenting system is also powered by a custom solution that we've built. It uses Axios to send a `GET` request containing the slug to pull the comment thread and a `POST` request containing the name, email address, and comment when submitting a comment. -- Our contact form is a simple HTML form, which uses Axios as well. -- Our writers use shortcodes to enhance the capability of Markdown. -- Our entire website is delivered through KeyCDN using a Pull Zone, which means all of our edge locations are delivering our website. diff --git a/docs/content/en/showcase/letsencrypt/bio.md b/docs/content/en/showcase/letsencrypt/bio.md deleted file mode 100644 index 62c547980..000000000 --- a/docs/content/en/showcase/letsencrypt/bio.md +++ /dev/null @@ -1 +0,0 @@ -Let's Encrypt is a free, automated, and open certificate authority (CA), run for the public's benefit. It is a service provided by the [Internet Security Research Group (ISRG)](https://www.abetterinternet.org/). diff --git a/docs/content/en/showcase/letsencrypt/featured.png b/docs/content/en/showcase/letsencrypt/featured.png deleted file mode 100644 index 9535d91bd..000000000 Binary files a/docs/content/en/showcase/letsencrypt/featured.png and /dev/null differ diff --git a/docs/content/en/showcase/letsencrypt/index.md b/docs/content/en/showcase/letsencrypt/index.md deleted file mode 100644 index 8696cb7ad..000000000 --- a/docs/content/en/showcase/letsencrypt/index.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: Let's Encrypt -date: 2018-03-13 -description: 'Showcase: "Lessons learned from taking letsencrypt.org to Hugo."' -siteURL: https://letsencrypt.org/ -siteSource: https://github.com/letsencrypt/website -byline: "[bep](https://github.com/bep), Hugo Lead" ---- - -The **Let's Encrypt website** has a common set of elements: A landing page and some other static info-pages, a document section, a blog, and a documentation section. Having it moved to Hugo was mostly motivated by a _simpler administration and Hugo's [multilingual support](/content-management/multilingual/)_. They already serve HTTPS to more than 60 million domains, and having the documentation available in more languages will increase that reach.[^1] - -{{< x user="letsencrypt" id="971755920639307777" >}} - -I helped them port the site from Jekyll to Hugo. There are usually very few surprises doing this. I know Hugo very well, but working on sites with a history usually comes up with something new. - -That site is bookmarked in many browsers, so preserving the URLs was a must. Hugo's URL handling is very flexible, but there was one challenge. The website has a mix of standard and what we in Hugo call _ugly URLs_ (`https://letsencrypt.org/2017/12/07/looking-forward-to-2018.html`). In Hugo this is handled automatically, and you can turn it on globally or per language. But before Hugo `0.33` you could not configure it for parts of your site. You could set it manually for the relevant pages in front matter -- which is how it was done in Jekyll -- but that would be hard to manage, especially when you start to introduce translations. So, in Hugo 0.33 I added support for _ugly URLs_ per section and also `url` set in front matter for list pages (`https://letsencrypt.org/blog/`). - -[^1]: The work on getting the content translated is in progress. diff --git a/docs/content/en/showcase/linode/bio.md b/docs/content/en/showcase/linode/bio.md deleted file mode 100644 index 52c7204c1..000000000 --- a/docs/content/en/showcase/linode/bio.md +++ /dev/null @@ -1,3 +0,0 @@ -**Linode** is a cloud hosting provider that offers high performance SSD Linux servers for your infrastructure needs. - -**Hugo** offers the documentation team incredible performance as we scale and continue providing quality Linux tutorials. diff --git a/docs/content/en/showcase/linode/featured.png b/docs/content/en/showcase/linode/featured.png deleted file mode 100644 index 8e517eacb..000000000 Binary files a/docs/content/en/showcase/linode/featured.png and /dev/null differ diff --git a/docs/content/en/showcase/linode/index.md b/docs/content/en/showcase/linode/index.md deleted file mode 100644 index f91c99d50..000000000 --- a/docs/content/en/showcase/linode/index.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Linode Docs -date: 2018-02-12 -description: 'Showcase: "Hugo allows us to build thousands of pages in seconds."' -siteURL: https://linode.com/docs/ -siteSource: https://github.com/linode/docs ---- - -The documentation team at Linode has been writing guides since 2009, with the goal of helping new and experienced Linux users find the best tools and get the most out of their systems. - -As our library grew into thousands of guides, we needed a fast static site generator with intuitive templating and the flexibility to extend Markdown without constantly writing HTML and CSS. - -Hugo solved a lot of our growing pains with features like shortcodes, customizable URLs, LiveReload, and more. We have already brought our site build time down from minutes to just a few seconds, and we are excited to see what future developments in Hugo will bring. - -Thank you to all the [Hugo contributors](https://github.com/gohugoio/hugo/graphs/contributors) and especially [@bep](https://github.com/bep) for helping us with the adoption of Hugo. diff --git a/docs/content/en/showcase/overmindstudios/bio.md b/docs/content/en/showcase/overmindstudios/bio.md deleted file mode 100644 index 080de69a0..000000000 --- a/docs/content/en/showcase/overmindstudios/bio.md +++ /dev/null @@ -1,5 +0,0 @@ -**Overmind Studios** is a visual effects studio headquartered in Southern Germany. - -The site is built by: - -- [Tobias Kummer](https://www.overmind-studios.de/about/) diff --git a/docs/content/en/showcase/overmindstudios/featured.png b/docs/content/en/showcase/overmindstudios/featured.png deleted file mode 100644 index c3eaaaf4c..000000000 Binary files a/docs/content/en/showcase/overmindstudios/featured.png and /dev/null differ diff --git a/docs/content/en/showcase/overmindstudios/index.md b/docs/content/en/showcase/overmindstudios/index.md deleted file mode 100644 index 00b09c5be..000000000 --- a/docs/content/en/showcase/overmindstudios/index.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: Overmind Studios -description: "A fresh start to make things easier in the future." -siteURL: https://www.overmind-studios.de/ -byline: "[tobkum](https://github.com/tobkum), Co-Founder Overmind Studios" ---- - -After many years of running our site on WordPress, we decided to switch to Hugo. - -WordPress is a great CMS for many people, but it has some downsides, especially for those who need a fast, secure, and customizable site. Plugins can become outdated, customization can be difficult, and bloat can slow down page loading times. - -Hugo is a static site generator that addresses many of these problems. It is fast to build and iterate, does not require PHP, is highly customizable, and is easy to learn and use. It is also secure, as it does not have a backend or MySQL database that can be hacked. - -We are very happy with our switch to Hugo. It is easy to update our site with new projects, and our Lighthouse score and loading times are both excellent. We now have more time to be creative instead of troubleshooting WordPress quirks and updates. diff --git a/docs/content/en/showcase/pharmaseal/bio.md b/docs/content/en/showcase/pharmaseal/bio.md deleted file mode 100644 index 7477f1c32..000000000 --- a/docs/content/en/showcase/pharmaseal/bio.md +++ /dev/null @@ -1,7 +0,0 @@ -PHARMASEAL began in 2016 with the purpose of disrupting the Clinical Trials Management market through continuous validation and integration - -We've been using **Hugo + Webpack + Netlify** to provide a scalable, modular design for the website, complete with Forestry building blocks to quickly be able to generate engagement pages. - -The site is built by: - -- [Roboto Studio](https://roboto.studio) diff --git a/docs/content/en/showcase/pharmaseal/featured-pharmaseal.png b/docs/content/en/showcase/pharmaseal/featured-pharmaseal.png deleted file mode 100644 index 4a64325b7..000000000 Binary files a/docs/content/en/showcase/pharmaseal/featured-pharmaseal.png and /dev/null differ diff --git a/docs/content/en/showcase/pharmaseal/index.md b/docs/content/en/showcase/pharmaseal/index.md deleted file mode 100644 index 3a8da2377..000000000 --- a/docs/content/en/showcase/pharmaseal/index.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: PHARMASEAL -date: 2019-04-29 -description: "Pharmaseal website developed using Hugo, Forestry, hosted and deployed by Netlify." -siteURL: https://pharmaseal.co/ -byline: "[Roboto Studio](https://roboto.studio), Jonathan Alford" ---- - -We wanted to shake the status quo with PHARMASEAL, opting for a fast and scalable website built with Hugo instead of slower monolithic systems the competitors were using. - -We had two goals: - -**Make it fast** - -We wanted to optimize the site as much as possible, so we opted for using Cloudinary, enabling us to take advantage of on-the-fly image manipulation, and thanks to the sheer speed of static sites, we achieved a perfect optimization score with Google audits. - -Because we're hosting the site through Netlify and our target audience is in America, we are taking advantage of Netlify edge (Their alternative to a CDN). We're talking blazing fast. - -**Make it easy** - -We're big fans of simplicity, and that's what we delivered with the Forestry building blocks. Every element on the site is built with building blocks in mind, allowing PHARMASEAL to generate multiple pages in the blink of an eye. - -PHARMASEAL have found Forestry CMS combined with HUGO to be so effective at producing fast, purpose driven pages, that we have worked with them to add even more blocks in a scalable, modular fashion. - -**TLDR:** We're blown away with HUGO, the sheer speed, scalability and deployment possibilities with Netlify is the 💣 diff --git a/docs/content/en/showcase/quiply-employee-communications-app/bio.md b/docs/content/en/showcase/quiply-employee-communications-app/bio.md deleted file mode 100644 index f79677a1a..000000000 --- a/docs/content/en/showcase/quiply-employee-communications-app/bio.md +++ /dev/null @@ -1,4 +0,0 @@ -**Quiply** is an employee communications app enabling mobile collaboration across an entire organization. -Our customers get their own branded app enabling them to communicate fast and effectively with all employees, also non-desk and shift workers. - -As the Quiply app's build process is based on **Gulp**, we have started to build our company and product website using **Gulp + Hugo** which is super-fast and gives us exactly the flexibility we need. diff --git a/docs/content/en/showcase/quiply-employee-communications-app/featured.png b/docs/content/en/showcase/quiply-employee-communications-app/featured.png deleted file mode 100644 index a4e9f046e..000000000 Binary files a/docs/content/en/showcase/quiply-employee-communications-app/featured.png and /dev/null differ diff --git a/docs/content/en/showcase/quiply-employee-communications-app/index.md b/docs/content/en/showcase/quiply-employee-communications-app/index.md deleted file mode 100644 index 2c8854a8d..000000000 --- a/docs/content/en/showcase/quiply-employee-communications-app/index.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Quiply Employee Communications App -date: 2018-02-13 -description: '"It became immediately clear that we would use Hugo going forward as it compiles super-fast, is intuitive to use, and offers all the features we need."' -siteURL: https://www.quiply.com -byline: "[Sebastian Schirmer](mailto:sebastian.schirmer@quiply.com), Quiply Co-Founder" ---- - -With the launch of our Employee Communications app Quiply we created a very simple and static one-page website to showcase our product. - -As our customer base and demand for marketing and communication started to grow, we needed a solution to easily grow and extend the contents of our web presence. As we do not have the need to serve dynamic content, we decided to use a static site generator. Amongst a couple of others, we tried Hugo and it became immediately clear that we'd use Hugo going forward as it compiles super-fast, is intuitive to use and offers all the features we need. - -Our website which we launched a couple of weeks ago is still growing and new content is being added constantly. By using Hugo, this can be easily done by content authors writing Markdown files without always having to touch HTML or CSS code. It is available in German only for the time being, an English version is in the works. - -Huge thanks to everyone involved in making Hugo a success. diff --git a/docs/content/en/showcase/tomango/bio.md b/docs/content/en/showcase/tomango/bio.md deleted file mode 100644 index c90e48163..000000000 --- a/docs/content/en/showcase/tomango/bio.md +++ /dev/null @@ -1,5 +0,0 @@ -We help ambitious businesses grow by getting more of the customers they want. - -Our new site runs quickly, anywhere in the world, regardless of internet connectivity. - -The site was built by [Tomango](https://www.tomango.co.uk) diff --git a/docs/content/en/showcase/tomango/featured.png b/docs/content/en/showcase/tomango/featured.png deleted file mode 100644 index d4b037e0f..000000000 Binary files a/docs/content/en/showcase/tomango/featured.png and /dev/null differ diff --git a/docs/content/en/showcase/tomango/index.md b/docs/content/en/showcase/tomango/index.md deleted file mode 100644 index d07abf92a..000000000 --- a/docs/content/en/showcase/tomango/index.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: Tomango -date: 2018-05-04 -description: 'Showcase: "Tomango site relaunch: Building our JAMstack site"' -siteURL: https://www.tomango.co.uk -siteSource: https://github.com/trys/tomango-2018 -byline: "[Trys Mudford](https://www.trysmudford.com), Lead Developer, Tomango" ---- - -Hugo is our static site generator (SSG) of choice. It's **really quick**. After using it on a number of [client projects](/showcase/hartwell-insurance/), it became clear that our new site _had_ to be built with Hugo. - -The big benefit of an SSG is how it moves all the heavy lifting to the build time. - -For example in WordPress, all the category pages are created at runtime, generating a lot of database queries. In Hugo, the paginated category pages are created at build time - so all the computational complexity is done once, and doesn't impact the user at all. - -Similarly, instead of running a live, or even a heavily cached Instagram feed that checked for new photos on page load, we used IFTTT to flip the feature to work performantly. I've [written about it](https://www.trysmudford.com/blog/making-the-static-dynamic-instagram-importer/) in detail on my blog but in essence: IFTTT sends a webhook to a Netlify Cloud Function every time a photo is uploaded. The function scrapes the photo and commits it to our GitHub repo which triggers a Hugo build on Netlify, deploying the site immediately! - -Shortcodes allow copy editors to continue using WordPress-esque features, Markdown keeps our developers happy, and our users don't have any of the database overheads. It's win-win! - ---- - -This is an extract from our [technical launch post](https://www.tomango.co.uk/thinks/tomango-progressive-web-app/). diff --git a/docs/content/en/templates/404.md b/docs/content/en/templates/404.md deleted file mode 100644 index 1a1a3c146..000000000 --- a/docs/content/en/templates/404.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: Custom 404 page -linkTitle: 404 templates -description: Create a template to render a 404 error page. -categories: [] -keywords: [] -weight: 190 ---- - -To render a 404 error page in the root of your site, create a 404 template in the root of the `layouts` directory. For example: - -```go-html-template {file="layouts/404.html"} -{{ define "main" }} -

    404 Not Found

    -

    The page you requested cannot be found.

    -

    - - Return to the home page - -

    -{{ end }} -``` - -For multilingual sites, add the language key to the file name: - -```text -layouts/ -├── 404.de.html -├── 404.en.html -└── 404.fr.html -``` - -Your production server redirects the browser to the 404 page when a page is not found. Capabilities and configuration vary by host. - -Host|Capabilities and configuration -:--|:-- -Amazon CloudFront|See [details](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/GeneratingCustomErrorResponses.html). -Amazon S3|See [details](https://docs.aws.amazon.com/AmazonS3/latest/userguide/CustomErrorDocSupport.html). -Apache|See [details](https://httpd.apache.org/docs/2.4/custom-error.html). -Azure Static Web Apps|See [details](https://learn.microsoft.com/en-us/azure/static-web-apps/configuration#response-overrides). -Azure Storage|See [details](https://learn.microsoft.com/en-us/azure/storage/blobs/storage-blob-static-website#setting-up-a-static-website). -Caddy|See [details](https://caddyserver.com/docs/caddyfile/directives/handle_errors). -Cloudflare Pages|See [details](https://developers.cloudflare.com/pages/configuration/serving-pages/#not-found-behavior). -DigitalOcean App Platform|See [details](https://docs.digitalocean.com/products/app-platform/how-to/manage-static-sites/#configure-a-static-site). -Firebase|See [details](https://firebase.google.com/docs/hosting/full-config#404). -GitHub Pages|Redirection to is automatic and not configurable. -GitLab Pages|See [details](https://docs.gitlab.com/ee/user/project/pages/introduction.html#custom-error-codes-pages). -NGINX|See [details](https://nginx.org/en/docs/http/ngx_http_core_module.html#error_page). -Netlify|See [details](https://docs.netlify.com/routing/redirects/redirect-options/). diff --git a/docs/content/en/templates/_index.md b/docs/content/en/templates/_index.md deleted file mode 100644 index ea293e8e1..000000000 --- a/docs/content/en/templates/_index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Templates -description: Create templates to render your content, resources, and data. -categories: [] -keywords: [] -weight: 10 -aliases: [/templates/overview/,/templates/content] ---- diff --git a/docs/content/en/templates/base.md b/docs/content/en/templates/base.md deleted file mode 100644 index bb6a25b1e..000000000 --- a/docs/content/en/templates/base.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -title: Base templates -description: The base and block construct allows you to define the outer shell of your master templates (i.e., the chrome of the page). -categories: [] -keywords: [] -weight: 40 -aliases: [/templates/blocks/,/templates/base-templates-and-blocks/] ---- - -The `block` keyword allows you to define the outer shell of your pages' one or more master template(s) and then fill in or override portions as necessary. - -{{< youtube QVOMCYitLEc >}} - -## Base template lookup order - -The base template lookup order closely follows that of the template it applies to (e.g. `_default/list.html`). - -See [Template Lookup Order](/templates/lookup-order/) for details and examples. - -## Define the base template - -The following defines a simple base template at `_default/baseof.html`. As a default template, it is the shell from which all your pages will be rendered unless you specify another `*baseof.html` closer to the beginning of the lookup order. - -```go-html-template {file="layouts/_default/baseof.html"} - - - - - {{ block "title" . }} - <!-- Blocks may include default content. --> - {{ .Site.Title }} - {{ end }} - - - - {{ block "main" . }} - - {{ end }} - {{ block "footer" . }} - - {{ end }} - - -``` - -## Override the base template - -The default list template will inherit all of the code defined above and can then implement its own `"main"` block from: - -```go-html-template {file="layouts/_default/list.html"} -{{ define "main" }} -

    Posts

    - {{ range .Pages }} -
    -

    {{ .Title }}

    - {{ .Content }} -
    - {{ end }} -{{ end }} -``` - -This replaces the contents of our (basically empty) `main` block with something useful for the list template. In this case, we didn't define a `title` block, so the contents from our base template remain unchanged in lists. - -> [!warning] -> Only [template comments] are allowed outside a block's `define` and `end` statements. Avoid placing any other text, including HTML comments, outside these boundaries. Doing so will cause rendering issues, potentially resulting in a blank page. See the example below. - -```go-html-template {file="layouts/_default/do-not-do-this.html"} -
    This div element broke your template.
    -{{ define "main" }} -

    {{ .Title }}

    - {{ .Content }} -{{ end }} - -``` - -The following shows how you can override both the `main` and `title` block areas from the base template with code unique to your default [single template]: - -```go-html-template {file="layouts/_default/single.html"} -{{ define "title" }} - - {{ .Title }} – {{ .Site.Title }} -{{ end }} -{{ define "main" }} -

    {{ .Title }}

    - {{ .Content }} -{{ end }} -``` - -[single template]: /templates/types/#single -[template comments]: /templates/introduction/#comments diff --git a/docs/content/en/templates/content-view.md b/docs/content/en/templates/content-view.md deleted file mode 100644 index f001e400e..000000000 --- a/docs/content/en/templates/content-view.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -title: Content view templates -description: Hugo can render alternative views of your content, useful in list and summary views. -categories: [] -keywords: [] -weight: 110 -aliases: [/templates/views/] ---- - -The following are common use cases for content views: - -- You want content of every type to be shown on the home page but only with limited [summary views][summaries]. -- You only want a bulleted list of your content in a [taxonomy template]. Views make this very straightforward by delegating the rendering of each different type of content to the content itself. - -## Create a content view - -To create a new view, create a template in each of your different content type directories with the view name. The following example contains an "li" view and a "summary" view for the `posts` and `project` content types. As you can see, these sit next to the [single template], `single.html`. You can even provide a specific view for a given type and continue to use the `_default/single.html` for the primary view. - -```txt -layouts/ -├── posts/ -│ ├── li.html -│ ├── single.html -│ └── summary.html -├── project/ -│ ├── li.html -│ └── single.html -└── summary.html -``` - -## Which template will be rendered? - -The following is the lookup order for content views ordered by specificity. - -1. `/layouts//.html` -1. `/layouts/
    /.html` -1. `/layouts/_default/.html` -1. `/themes//layouts//.html` -1. `/themes//layouts/
    /.html` -1. `/themes//layouts/_default/.html` - -## Example: content view inside a list - -### list.html - -In this example, `.Render` is passed into the template to call the [render function][render]. `.Render` is a special function that instructs content to render itself with the view template provided as the first argument. In this case, the template is going to render the `summary.html` view that follows: - -```go-html-template {file="layouts/_default/list.html"} -
    -
    -

    {{ .Title }}

    - {{ range .Pages }} - {{ .Render "summary" }} - {{ end }} -
    -
    -``` - -### summary.html - -Hugo passes the `Page` object to the following `summary.html` view template. - -```go-html-template {file="layouts/_default/summary.html"} - -``` - -### li.html - -Continuing on the previous example, we can change our render function to use a smaller `li.html` view by changing the argument in the call to the `.Render` function (i.e., `{{ .Render "li" }}`). - -```go-html-template {file="layouts/_default/li.html"} -
  • - {{ .LinkTitle }} -
    {{ .Date.Format "Mon, Jan 2, 2006" }}
    -
  • -``` - -[render]: /methods/page/render/ -[single template]: /templates/types/#single -[summaries]: /content-management/summaries/ -[taxonomy template]: /templates/types/#taxonomy diff --git a/docs/content/en/templates/embedded.md b/docs/content/en/templates/embedded.md deleted file mode 100644 index ecfd90514..000000000 --- a/docs/content/en/templates/embedded.md +++ /dev/null @@ -1,222 +0,0 @@ ---- -title: Embedded partial templates -description: Hugo provides embedded partial templates for common use cases. -categories: [] -keywords: [] -weight: 170 -aliases: [/templates/internal] ---- - -## Disqus - -> [!note] -> To override Hugo's embedded Disqus template, copy the [source code]({{% eturl disqus %}}) to a file with the same name in the `layouts/partials` directory, then call it from your templates using the [`partial`] function: -> -> `{{ partial "disqus.html" . }}` - -Hugo includes an embedded template for [Disqus], a popular commenting system for both static and dynamic websites. To effectively use Disqus, secure a Disqus "shortname" by [signing up] for the free service. - -To include the embedded template: - -```go-html-template -{{ template "_internal/disqus.html" . }} -``` - -### Configuration {#configuration-disqus} - -To use Hugo's Disqus template, first set up a single configuration value: - -{{< code-toggle file=hugo >}} -[services.disqus] -shortname = 'your-disqus-shortname' -{{}} - -Hugo's Disqus template accesses this value with: - -```go-html-template -{{ .Site.Config.Services.Disqus.Shortname }} -``` - -You can also set the following in the front matter for a given piece of content: - -- `disqus_identifier` -- `disqus_title` -- `disqus_url` - -### Privacy {#privacy-disqus} - -Adjust the relevant privacy settings in your site configuration. - -{{< code-toggle config=privacy.disqus />}} - -disable -: (`bool`) Whether to disable the template. Default is `false`. - -## Google Analytics - -> [!note] -> To override Hugo's embedded Google Analytics template, copy the [source code]({{% eturl google_analytics %}}) to a file with the same name in the `layouts/partials` directory, then call it from your templates using the [`partial`] function: -> -> `{{ partial "google_analytics.html" . }}` - -Hugo includes an embedded template supporting [Google Analytics 4]. - -To include the embedded template: - -```go-html-template -{{ template "_internal/google_analytics.html" . }} -``` - -### Configuration {#configuration-google-analytics} - -Provide your tracking ID in your configuration file: - -{{< code-toggle file=hugo >}} -[services.googleAnalytics] -id = "G-MEASUREMENT_ID" -{{}} - -To use this value in your own template, access the configured ID with `{{ site.Config.Services.GoogleAnalytics.ID }}`. - -### Privacy {#privacy-google-analytics} - -Adjust the relevant privacy settings in your site configuration. - -{{< code-toggle config=privacy.googleAnalytics />}} - -disable -: (`bool`) Whether to disable the template. Default is `false`. - -respectDoNotTrack -: (`bool`) Whether to respect the browser's "do not track" setting. Default is `false`. - -## Open Graph - -> [!note] -> To override Hugo's embedded Open Graph template, copy the [source code]({{% eturl opengraph %}}) to a file with the same name in the `layouts/partials` directory, then call it from your templates using the [`partial`] function: -> -> `{{ partial "opengraph.html" . }}` - -Hugo includes an embedded template for the [Open Graph protocol](https://ogp.me/), metadata that enables a page to become a rich object in a social graph. -This format is used for Facebook and some other sites. - -To include the embedded template: - -```go-html-template -{{ template "_internal/opengraph.html" . }} -``` - -### Configuration {#configuration-open-graph} - -Hugo's Open Graph template is configured using a mix of configuration settings and [front matter](/content-management/front-matter/) on individual pages. - -{{< code-toggle file=hugo >}} -[params] - description = 'Text about my cool site' - images = ['site-feature-image.jpg'] - title = 'My cool site' - [params.social] - facebook_admin = 'jsmith' -[taxonomies] - series = 'series' -{{}} - -{{< code-toggle file=content/blog/my-post.md fm=true >}} -title = "Post title" -description = "Text about this post" -date = 2024-03-08T08:18:11-08:00 -images = ["post-cover.png"] -audio = [] -videos = [] -series = [] -tags = [] -{{}} - -Hugo uses the page title and description for the title and description metadata. -The first 6 URLs from the `images` array are used for image metadata. -If [page bundles](/content-management/page-bundles/) are used and the `images` array is empty or undefined, images with file names matching `*feature*`, `*cover*`, or `*thumbnail*` are used for image metadata. - -Various optional metadata can also be set: - -- Date, published date, and last modified data are used to set the published time metadata if specified. -- `audio` and `videos` are URL arrays like `images` for the audio and video metadata tags, respectively. -- The first 6 `tags` on the page are used for the tags metadata. -- The `series` taxonomy is used to specify related "see also" pages by placing them in the same series. - -If using YouTube this will produce a og:video tag like ``. Use the `https://youtu.be/` format with YouTube videos (example: `https://youtu.be/qtIqKaDlqXo`). - -## Pagination - -See [details](/templates/pagination/). - -## Schema - -> [!note] -> To override Hugo's embedded Schema template, copy the [source code]({{% eturl schema %}}) to a file with the same name in the `layouts/partials` directory, then call it from your templates using the [`partial`] function: -> -> `{{ partial "schema.html" . }}` - -Hugo includes an embedded template to render [microdata] `meta` elements within the `head` element of your templates. - -To include the embedded template: - -```go-html-template -{{ template "_internal/schema.html" . }} -``` - -## X (Twitter) Cards - -> [!note] -> To override Hugo's embedded Twitter Cards template, copy the [source code]({{% eturl twitter_cards %}}) to a file with the same name in the `layouts/partials` directory, then call it from your templates using the [`partial`] function: -> -> `{{ partial "twitter_cards.html" . }}` - -Hugo includes an embedded template for [X (Twitter) Cards](https://developer.x.com/en/docs/twitter-for-websites/cards/overview/abouts-cards), -metadata used to attach rich media to Tweets linking to your site. - -To include the embedded template: - -```go-html-template -{{ template "_internal/twitter_cards.html" . }} -``` - -### Configuration {#configuration-x-cards} - -Hugo's X (Twitter) Card template is configured using a mix of configuration settings and [front-matter](/content-management/front-matter/) values on individual pages. - -{{< code-toggle file=hugo >}} -[params] - images = ["site-feature-image.jpg"] - description = "Text about my cool site" -{{}} - -{{< code-toggle file=content/blog/my-post.md fm=true >}} -title = "Post title" -description = "Text about this post" -images = ["post-cover.png"] -{{}} - -If [page bundles](/content-management/page-bundles/) are used and the `images` array is empty or undefined, images with file names matching `*feature*`, `*cover*`, or `*thumbnail*` are used for image metadata. -If no image resources with those names are found, the images defined in the [site config](/configuration/) are used instead. -If no images are found at all, then an image-less Twitter `summary` card is used instead of `summary_large_image`. - -Hugo uses the page title and description for the card's title and description fields. The page summary is used if no description is given. - -Set the value of `twitter:site` in your site configuration: - -{{< code-toggle file=hugo >}} -[params.social] -twitter = "GoHugoIO" -{{}} - -NOTE: The `@` will be added for you - -```html - -``` - -[`partial`]: /functions/partials/include/ -[Disqus]: https://disqus.com -[Google Analytics 4]: https://support.google.com/analytics/answer/10089681 -[microdata]: https://html.spec.whatwg.org/multipage/microdata.html#microdata -[signing up]: https://disqus.com/profile/signup/ diff --git a/docs/content/en/templates/home.md b/docs/content/en/templates/home.md deleted file mode 100644 index 937a4a5a8..000000000 --- a/docs/content/en/templates/home.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -title: Home page templates -description: The home page of a website is often formatted differently than the other pages. For this reason, Hugo makes it easy for you to define your new site's home page as a unique template. -categories: [] -keywords: [] -weight: 50 -aliases: [/layout/homepage/,/templates/homepage-template/,/templates/homepage/] ---- - -## Introduction - -A home page template is used to render your site's home page, and is the only template required for a single-page website. For example, the home page template below inherits the site's shell from the base template and renders the home page content, such as a list of other pages. - -```go-html-template {file="layouts/_default/home.html"} -{{ define "main" }} - {{ .Content }} - {{ range site.RegularPages }} -

    {{ .LinkTitle }}

    - {{ end }} -{{ end }} -``` - -{{% include "/_common/filter-sort-group.md" %}} - -## Lookup order - -Hugo's [template lookup order] determines the template path, allowing you to create unique templates for any page. - -> [!note] -> You must have thorough understanding of the template lookup order when creating templates. Template selection is based on template type, page kind, content type, section, language, and output format. - -## Content and front matter - -The home page template uses content and front matter from an `_index.md` file located in the root of your content directory. - -{{< code-toggle file=content/_index.md fm=true >}} ---- -title: The Home Page -date: 2025-01-30T03:36:57-08:00 -draft: false -params: - subtitle: The Subtitle ---- -{{< /code-toggle >}} - -The home page template below inherits the site's shell from the base template, renders the subtitle and content as defined in the `_index.md` file, then renders of list of the site's [regular pages](g). - -```go-html-template {file="layouts/_default/home.html"} -{{ define "main" }} -

    {{ .Params.Subtitle }}

    - {{ .Content }} - {{ range site.RegularPages }} -

    {{ .LinkTitle }}

    - {{ end }} -{{ end }} -``` - -[template lookup order]: /templates/lookup-order/#home-templates diff --git a/docs/content/en/templates/introduction.md b/docs/content/en/templates/introduction.md deleted file mode 100644 index 8898ee456..000000000 --- a/docs/content/en/templates/introduction.md +++ /dev/null @@ -1,535 +0,0 @@ ---- -title: Introduction to templating -linkTitle: Introduction -description: An introduction to Hugo's templating syntax. -categories: [] -keywords: [] -weight: 10 ---- - -{{% glossary-term template %}} - -Templates use [variables], [functions], and [methods] to transform your content, resources, and data into a published page. - -> [!note] -> Hugo uses Go's [text/template] and [html/template] packages. -> -> The text/template package implements data-driven templates for generating textual output, while the html/template package implements data-driven templates for generating HTML output safe against code injection. -> -> By default, Hugo uses the html/template package when rendering HTML files. - -For example, this HTML template initializes the `$v1` and `$v2` variables, then displays them and their product within an HTML paragraph. - -```go-html-template -{{ $v1 := 6 }} -{{ $v2 := 7 }} -

    The product of {{ $v1 }} and {{ $v2 }} is {{ mul $v1 $v2 }}.

    -``` - -While HTML templates are the most common, you can create templates for any [output format](g) including CSV, JSON, RSS, and plain text. - -## Context - -The most important concept to understand before creating a template is _context_, the data passed into each template. The data may be a simple value, or more commonly [objects](g) and associated [methods](g). - -For example, a template for a single page receives a `Page` object, and the `Page` object provides methods to return values or perform actions. - -### Current context - -Within a template, the dot (`.`) represents the current context. - -```go-html-template {file="layouts/_default/single.html"} -

    {{ .Title }}

    -``` - -In the example above the dot represents the `Page` object, and we call its [`Title`] method to return the title as defined in [front matter]. - -The current context may change within a template. For example, at the top of a template the context might be a `Page` object, but we rebind the context to another value or object within [`range`] or [`with`] blocks. - -```go-html-template {file="layouts/_default/single.html"} -

    {{ .Title }}

    - -{{ range slice "foo" "bar" }} -

    {{ . }}

    -{{ end }} - -{{ with "baz" }} -

    {{ . }}

    -{{ end }} -``` - -In the example above, the context changes as we `range` through the [slice](g) of values. In the first iteration the context is "foo", and in the second iteration the context is "bar". Inside of the `with` block the context is "baz". Hugo renders the above to: - -```html -

    My Page Title

    -

    foo

    -

    bar

    -

    baz

    -``` - -### Template context - -Within a `range` or `with` block you can access the context passed into the template by prepending a dollar sign (`$`) to the dot: - -```go-html-template {file="layouts/_default/single.html"} -{{ with "foo" }} -

    {{ $.Title }} - {{ . }}

    -{{ end }} -``` - -Hugo renders this to: - -```html -

    My Page Title - foo

    -``` - -> [!note] -> Make sure that you thoroughly understand the concept of _context_ before you continue reading. The most common templating errors made by new users relate to context. - -## Actions - -In the examples above the paired opening and closing braces represent the beginning and end of a template action, a data evaluation or control structure within a template. - -A template action may contain literal values ([boolean](g), [string](g), [integer](g), and [float](g)), variables, functions, and methods. - -```go-html-template {file="layouts/_default/single.html"} -{{ $convertToLower := true }} -{{ if $convertToLower }} -

    {{ strings.ToLower .Title }}

    -{{ end }} -``` - -In the example above: - -- `$convertToLower` is a variable -- `true` is a literal boolean value -- `strings.ToLower` is a function that converts all characters to lowercase -- `Title` is a method on a the `Page` object - -Hugo renders the above to: - -```html - - -

    my page title

    - -``` - -### Whitespace - -Notice the blank lines and indentation in the previous example? Although irrelevant in production when you typically minify the output, you can remove the adjacent whitespace by using template action delimiters with hyphens: - -```go-html-template {file="layouts/_default/single.html"} -{{- $convertToLower := true -}} -{{- if $convertToLower -}} -

    {{ strings.ToLower .Title }}

    -{{- end -}} -``` - -Hugo renders this to: - -```html -

    my page title

    -``` - -Whitespace includes spaces, horizontal tabs, carriage returns, and newlines. - -### Pipes - -Within a template action you may [pipe](g) a value to a function or method. The piped value becomes the final argument to the function or method. For example, these are equivalent: - -```go-html-template -{{ strings.ToLower "Hugo" }} → hugo -{{ "Hugo" | strings.ToLower }} → hugo -``` - -You can pipe the result of one function or method into another. For example, these are equivalent: - -```go-html-template -{{ strings.TrimSuffix "o" (strings.ToLower "Hugo") }} → hug -{{ "Hugo" | strings.ToLower | strings.TrimSuffix "o" }} → hug -``` - -These are also equivalent: - -```go-html-template -{{ mul 6 (add 2 5) }} → 42 -{{ 5 | add 2 | mul 6 }} → 42 -``` - -> [!note] -> Remember that the piped value becomes the final argument to the function or method to which you are piping. - -### Line splitting - -You can split a template action over two or more lines. For example, these are equivalent: - -```go-html-template -{{ $v := or $arg1 $arg2 }} - -{{ $v := or - $arg1 - $arg2 -}} -``` - -You can also split [raw string literals](g) over two or more lines. For example, these are equivalent: - -```go-html-template -{{ $msg := "This is line one.\nThis is line two." }} - -{{ $msg := `This is line one. -This is line two.` -}} -``` - -## Variables - -A variable is a user-defined [identifier](g) prepended with a dollar sign (`$`), representing a value of any data type, initialized or assigned within a template action. For example, `$foo` and `$bar` are variables. - -Variables may contain [scalars](g), [slices](g), [maps](g), or [objects](g). - -Use `:=` to initialize a variable, and use `=` to assign a value to a variable that has been previously initialized. For example: - -```go-html-template -{{ $total := 3 }} -{{ range slice 7 11 21 }} - {{ $total = add $total . }} -{{ end }} -{{ $total }} → 42 -``` - -Variables initialized inside of an `if`, `range`, or `with` block are scoped to the block. Variables initialized outside of these blocks are scoped to the template. - -With variables that represent a slice or map, use the [`index`] function to return the desired value. - -```go-html-template -{{ $slice := slice "foo" "bar" "baz" }} -{{ index $slice 2 }} → baz - -{{ $map := dict "a" "foo" "b" "bar" "c" "baz" }} -{{ index $map "c" }} → baz -``` - -> [!note] -> Slices and arrays are zero-based; element 0 is the first element. - -With variables that represent a map or object, [chain](g) identifiers to return the desired value or to access the desired method. - -```go-html-template -{{ $map := dict "a" "foo" "b" "bar" "c" "baz" }} -{{ $map.c }} → baz - -{{ $homePage := .Site.Home }} -{{ $homePage.Title }} → My Homepage -``` - -> [!note] -> As seen above, object and method names are capitalized. Although not required, to avoid confusion we recommend beginning variable and map key names with a lowercase letter or underscore. - -## Functions - -Used within a template action, a function takes one or more arguments and returns a value. Unlike methods, functions are not associated with an object. - -Go's text/template and html/template packages provide a small set of functions, operators, and statements for general use. See the [go-templates] section of the function documentation for details. - -Hugo provides hundreds of custom [functions] categorized by namespace. For example, the `strings` namespace includes these and other functions: - -Function|Alias -:--|:-- -[`strings.ToLower`](/functions/strings/tolower)|`lower` -[`strings.ToUpper`](/functions/strings/toupper)|`upper` -[`strings.Replace`](/functions/strings/replace)|`replace` - -As shown above, frequently used functions have an alias. Use aliases in your templates to reduce code length. - -When calling a function, separate the arguments from the function, and from each other, with a space. For example: - -```go-html-template -{{ $total := add 1 2 3 4 }} -``` - -## Methods - -Used within a template action and associated with an object, a method takes zero or more arguments and either returns a value or performs an action. - -The most commonly accessed objects are the [`Page`] and [`Site`] objects. This is a small sampling of the [methods] available to each object. - -Object|Method|Description -:--|:--|:-- -`Page`|[`Date`](methods/page/date/)|Returns the date of the given page. -`Page`|[`Params`](methods/page/params/)|Returns a map of custom parameters as defined in the front matter of the given page. -`Page`|[`Title`](methods/page/title/)|Returns the title of the given page. -`Site`|[`Data`](methods/site/data/)|Returns a data structure composed from the files in the `data` directory. -`Site`|[`Params`](methods/site/params/)|Returns a map of custom parameters as defined in the site configuration. -`Site`|[`Title`](methods/site/title/)|Returns the title as defined in the site configuration. - -Chain the method to its object with a dot (`.`) as shown below, remembering that the leading dot represents the [current context]. - -```go-html-template {file="layouts/_default/single.html"} -{{ .Site.Title }} → My Site Title -{{ .Page.Title }} → My Page Title -``` - -The context passed into most templates is a `Page` object, so this is equivalent to the previous example: - -```go-html-template {file="layouts/_default/single.html"} -{{ .Site.Title }} → My Site Title -{{ .Title }} → My Page Title -``` - -Some methods take an argument. Separate the argument from the method with a space. For example: - -```go-html-template {file="layouts/_default/single.html"} -{{ $page := .Page.GetPage "/books/les-miserables" }} -{{ $page.Title }} → Les Misérables -``` - -## Comments - -> [!note] -> Do not attempt to use HTML comment delimiters to comment out template code. -> -> Hugo strips HTML comments when rendering a page, but first evaluates any template code within the HTML comment delimiters. Depending on the template code within the HTML comment delimiters, this could cause unexpected results or fail the build. - -Template comments are similar to template actions. Paired opening and closing braces represent the beginning and end of a comment. For example: - -```text -{{/* This is an inline comment. */}} -{{- /* This is an inline comment with adjacent whitespace removed. */ -}} -``` - -Code within a comment is not parsed, executed, or displayed. Comments may be inline, as shown above, or in block form: - -```text -{{/* -This is a block comment. -*/}} - -{{- /* -This is a block comment with -adjacent whitespace removed. -*/ -}} -``` - -You may not nest one comment inside of another. - -To render an HTML comment, pass a string through the [`safeHTML`] template function. For example: - -```go-html-template -{{ "" | safeHTML }} -{{ printf "" .Site.Title | safeHTML }} -``` - -## Include - -Use the [`template`] function to include one or more of Hugo's [embedded templates]: - -```go-html-template -{{ template "_internal/google_analytics.html" . }} -{{ template "_internal/opengraph" . }} -{{ template "_internal/pagination.html" . }} -{{ template "_internal/schema.html" . }} -{{ template "_internal/twitter_cards.html" . }} -``` - -Use the [`partial`] or [`partialCached`] function to include one or more [partial templates]: - -```go-html-template -{{ partial "breadcrumbs.html" . }} -{{ partialCached "css.html" . }} -``` - -Create your partial templates in the layouts/partials directory. - -> [!note] -> In the examples above, note that we are passing the current context (the dot) to each of the templates. - -## Examples - -This limited set of contrived examples demonstrates some of concepts described above. Please see the [functions], [methods], and [templates] documentation for specific examples. - -### Conditional blocks - -See documentation for [`if`], [`else`], and [`end`]. - -```go-html-template -{{ $var := 42 }} -{{ if eq $var 6 }} - {{ print "var is 6" }} -{{ else if eq $var 7 }} - {{ print "var is 7" }} -{{ else if eq $var 42 }} - {{ print "var is 42" }} -{{ else }} - {{ print "var is something else" }} -{{ end }} -``` - -### Logical operators - -See documentation for [`and`] and [`or`]. - -```go-html-template -{{ $v1 := true }} -{{ $v2 := false }} -{{ $v3 := false }} -{{ $result := false }} - -{{ if and $v1 $v2 $v3 }} - {{ $result = true }} -{{ end }} -{{ $result }} → false - -{{ if or $v1 $v2 $v3 }} - {{ $result = true }} -{{ end }} -{{ $result }} → true -``` - -### Loops - -See documentation for [`range`], [`else`], and [`end`]. - -```go-html-template -{{ $s := slice "foo" "bar" "baz" }} -{{ range $s }} -

    {{ . }}

    -{{ else }} -

    The collection is empty

    -{{ end }} -``` - -Use the [`seq`] function to loop a specified number of times: - -```go-html-template -{{ $total := 0 }} -{{ range seq 4 }} - {{ $total = add $total . }} -{{ end }} -{{ $total }} → 10 -``` - -### Rebind context - -See documentation for [`with`], [`else`], and [`end`]. - -```go-html-template -{{ $var := "foo" }} -{{ with $var }} - {{ . }} → foo -{{ else }} - {{ print "var is falsy" }} -{{ end }} -``` - -To test multiple conditions: - -```go-html-template -{{ $v1 := 0 }} -{{ $v2 := 42 }} -{{ with $v1 }} - {{ . }} -{{ else with $v2 }} - {{ . }} → 42 -{{ else }} - {{ print "v1 and v2 are falsy" }} -{{ end }} -``` - -### Access site parameters - -See documentation for the [`Params`](/methods/site/params/) method on a `Site` object. - -With this site configuration: - -{{< code-toggle file=hugo >}} -title = 'ABC Widgets' -baseURL = 'https://example.org' -[params] - subtitle = 'The Best Widgets on Earth' - copyright-year = '2023' - [params.author] - email = 'jsmith@example.org' - name = 'John Smith' - [params.layouts] - rfc_1123 = 'Mon, 02 Jan 2006 15:04:05 MST' - rfc_3339 = '2006-01-02T15:04:05-07:00' -{{< /code-toggle >}} - -Access the custom site parameters by chaining the identifiers: - -```go-html-template -{{ .Site.Params.subtitle }} → The Best Widgets on Earth -{{ .Site.Params.author.name }} → John Smith - -{{ $layout := .Site.Params.layouts.rfc_1123 }} -{{ .Site.Lastmod.Format $layout }} → Tue, 17 Oct 2023 13:21:02 PDT -``` - -### Access page parameters - -See documentation for the [`Params`](/methods/page/params/) method on a `Page` object. - -By way of example, consider this front matter: - -{{< code-toggle file=content/annual-conference.md fm=true >}} -title = 'Annual conference' -date = 2023-10-17T15:11:37-07:00 -[params] -display_related = true -key-with-hyphens = 'must use index function' -[params.author] - email = 'jsmith@example.org' - name = 'John Smith' -{{< /code-toggle >}} - -The `title` and `date` fields are standard [front matter fields], while the other fields are user-defined. - -Access the custom fields by [chaining](g) the [identifiers](g) when needed: - -```go-html-template -{{ .Params.display_related }} → true -{{ .Params.author.email }} → jsmith@example.org -{{ .Params.author.name }} → John Smith -``` - -In the template example above, each of the keys is a valid identifier. For example, none of the keys contains a hyphen. To access a key that is not a valid identifier, use the [`index`] function: - -```go-html-template -{{ index .Params "key-with-hyphens" }} → must use index function -``` - -[`and`]: /functions/go-template/and -[`else`]: /functions/go-template/else/ -[`end`]: /functions/go-template/end/ -[`if`]: /functions/go-template/if/ -[`index`]: /functions/collections/indexfunction/ -[`index`]: /functions/collections/indexfunction/ -[`or`]: /functions/go-template/or -[`Page`]: /methods/page/ -[`partial`]: /functions/partials/include/ -[`partialCached`]: /functions/partials/includecached/ -[`range`]: /functions/go-template/range/ -[`range`]: /functions/go-template/range/ -[`safeHTML`]: /functions/safe/html -[`seq`]: /functions/collections/seq -[`Site`]: /methods/site/ -[`template`]: /functions/go-template/template/ -[`Title`]: /methods/page/title -[`with`]: /functions/go-template/with/ -[`with`]: /functions/go-template/with/ -[current context]: #current-context -[embedded templates]: /templates/embedded/ -[front matter]: /content-management/front-matter/ -[front matter fields]: /content-management/front-matter/#fields -[functions]: /functions/ -[functions]: /functions -[go-templates]: /functions/go-template/ -[html/template]: https://pkg.go.dev/html/template -[methods]: /methods/ -[methods]: /methods/ -[partial templates]: /templates/partial -[templates]: /templates/ -[text/template]: https://pkg.go.dev/text/template -[variables]: #variables diff --git a/docs/content/en/templates/lookup-order.md b/docs/content/en/templates/lookup-order.md deleted file mode 100644 index 518900797..000000000 --- a/docs/content/en/templates/lookup-order.md +++ /dev/null @@ -1,150 +0,0 @@ ---- -title: Template lookup order -linkTitle: Lookup order -description: Hugo uses the rules below to select a template for a given page, starting from the most specific. -categories: [] -keywords: [] -weight: 20 ---- - -## Lookup rules - -Hugo takes the parameters listed below into consideration when choosing a template for a given page. The templates are ordered by specificity. This should feel natural, but look at the table below for concrete examples of the different parameter variations. - -Kind -: The page `Kind` (the home page is one). See the example tables below per kind. This also determines if it is a **single page** (i.e. a regular content page. We then look for a template in `_default/single.html` for HTML) or a **list page** (section listings, home page, taxonomy lists, taxonomy terms. We then look for a template in `_default/list.html` for HTML). - -Layout -: Can be set in front matter. - -Output Format -: See [configure output formats](/configuration/output-formats/). An output format has both a `name` (e.g. `rss`, `amp`, `html`) and a `suffix` (e.g. `xml`, `html`). We prefer matches with both (e.g. `index.amp.html`), but look for less specific templates. - -Note that if the output format's Media Type has more than one suffix defined, only the first is considered. - -Language -: We will consider a language tag in the template name. If the site language is `fr`, `index.fr.amp.html` will win over `index.amp.html`, but `index.amp.html` will be chosen before `index.fr.html`. - -Type -: Is value of `type` if set in front matter, else it is the name of the root section (e.g. "blog"). It will always have a value, so if not set, the value is "page". - -Section -: Is relevant for `section`, `taxonomy` and `term` types. - -> [!note] -> Templates can live in either the project's or the themes' `layout` directories, and the most specific templates will be chosen. Hugo will interleave the lookups listed below, finding the most specific one either in the project or themes. - -## Target a template - -You cannot change the lookup order to target a content page, but you can change a content page to target a template. Specify `type`, `layout`, or both in front matter. - -Consider this content structure: - -```text -content/ -├── about.md -└── contact.md -``` - -Files in the root of the `content` directory have a [content type](g) of `page`. To render these pages with a unique template, create a matching subdirectory: - -```text -layouts/ -└── page/ - └── single.html -``` - -But the contact page probably has a form and requires a different template. In the front matter specify `layout`: - -{{< code-toggle file=content/contact.md fm=true >}} -title = 'Contact' -layout = 'contact' -{{< /code-toggle >}} - -Then create the template for the contact page: - -```text -layouts/ -└── page/ - └── contact.html <-- renders contact.md - └── single.html <-- renders about.md -``` - -As a content type, the word `page` is vague. Perhaps `miscellaneous` would be better. Add `type` to the front matter of each page: - -{{< code-toggle file=content/about.md fm=true >}} -title = 'About' -type = 'miscellaneous' -{{< /code-toggle >}} - -{{< code-toggle file=content/contact.md fm=true >}} -title = 'Contact' -type = 'miscellaneous' -layout = 'contact' -{{< /code-toggle >}} - -Now place the layouts in the corresponding directory: - -```text -layouts/ -└── miscellaneous/ - └── contact.html <-- renders contact.md - └── single.html <-- renders about.md -``` - -## Home templates - -These template paths are sorted by specificity in descending order. The least specific path is at the bottom of each list. - -{{< datatable-filtered "output" "layouts" "Kind == home" "Example" "OutputFormat" "Suffix" "Template Lookup Order" >}} - -## Single templates - -These template paths are sorted by specificity in descending order. The least specific path is at the bottom of each list. - -{{< datatable-filtered "output" "layouts" "Kind == page" "Example" "OutputFormat" "Suffix" "Template Lookup Order" >}} - -## Section templates - -These template paths are sorted by specificity in descending order. The least specific path is at the bottom of each list. - -{{< datatable-filtered "output" "layouts" "Kind == section" "Example" "OutputFormat" "Suffix" "Template Lookup Order" >}} - -## Taxonomy templates - -These template paths are sorted by specificity in descending order. The least specific path is at the bottom of each list. - -The examples below assume the following site configuration: - -{{< code-toggle file=hugo >}} -[taxonomies] -category = 'categories' -{{< /code-toggle >}} - -{{< datatable-filtered "output" "layouts" "Kind == taxonomy" "Example" "OutputFormat" "Suffix" "Template Lookup Order" >}} - -## Term templates - -These template paths are sorted by specificity in descending order. The least specific path is at the bottom of each list. - -The examples below assume the following site configuration: - -{{< code-toggle file=hugo >}} -[taxonomies] -category = 'categories' -{{< /code-toggle >}} - -{{< datatable-filtered "output" "layouts" "Kind == term" "Example" "OutputFormat" "Suffix" "Template Lookup Order" >}} - -## RSS templates - -These template paths are sorted by specificity in descending order. The least specific path is at the bottom of each list. - -The examples below assume the following site configuration: - -{{< code-toggle file=hugo >}} -[taxonomies] -category = 'categories' -{{< /code-toggle >}} - -{{< datatable-filtered "output" "layouts" "OutputFormat == rss" "Example" "OutputFormat" "Suffix" "Template Lookup Order" >}} diff --git a/docs/content/en/templates/menu.md b/docs/content/en/templates/menu.md deleted file mode 100644 index 4ff423255..000000000 --- a/docs/content/en/templates/menu.md +++ /dev/null @@ -1,127 +0,0 @@ ---- -title: Menu templates -description: Create templates to render one or more menus. -categories: [] -keywords: [] -weight: 150 -aliases: [/templates/menus/,/templates/menu-templates/] ---- - -## Overview - -After [defining menu entries], use [menu methods] to render a menu. - -Three factors determine how to render a menu: - -1. The method used to define the menu entries: [automatic], [in front matter], or [in site configuration] -1. The menu structure: flat or nested -1. The method used to [localize the menu entries]: site configuration or translation tables - -The example below handles every combination. - -## Example - -This partial template recursively "walks" a menu structure, rendering a localized, accessible nested list. - -```go-html-template {file="layouts/partials/menu.html" copy=true} -{{- $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 }} -``` - -Call the partial above, passing a menu ID and the current page in context. - -```go-html-template {file="layouts/_default/single.html"} -{{ partial "menu.html" (dict "menuID" "main" "page" .) }} -{{ partial "menu.html" (dict "menuID" "footer" "page" .) }} -``` - -## Page references - -Regardless of how you [define menu entries], an entry associated with a page has access to page context. - -This simplistic example renders a page parameter named `version` next to each entry's `name`. Code defensively using `with` or `if` to handle entries where (a) the entry points to an external resource, or (b) the `version` parameter is not defined. - -```go-html-template {file="layouts/_default/single.html"} -{{- range site.Menus.main }} - - {{ .Name }} - {{- with .Page }} - {{- with .Params.version -}} - ({{ . }}) - {{- end }} - {{- end }} - -{{- end }} -``` - -## Menu entry parameters - -When you define menu entries [in site configuration] or [in front matter], you can include a `params` key as shown in these examples: - -- [Menu entry defined in site configuration] -- [Menu entry defined in front matter] - -This simplistic example renders a `class` attribute for each anchor element. Code defensively using `with` or `if` to handle entries where `params.class` is not defined. - -```go-html-template {file="layouts/partials/menu.html"} -{{- range site.Menus.main }} - - {{ .Name }} - -{{- end }} -``` - -## Localize - -Hugo provides two methods to localize your menu entries. See [multilingual]. - -[automatic]: /content-management/menus/#define-automatically -[define menu entries]: /content-management/menus/ -[defining menu entries]: /content-management/menus/ -[in front matter]: /content-management/menus/#define-in-front-matter -[in site configuration]: /content-management/menus/#define-in-site-configuration -[localize the menu entries]: /content-management/multilingual/#menus -[menu entry defined in front matter]: /content-management/menus/#example -[menu entry defined in site configuration]: /configuration/menus -[menu methods]: /methods/menu/ -[multilingual]: /content-management/multilingual/#menus diff --git a/docs/content/en/templates/pagination.md b/docs/content/en/templates/pagination.md deleted file mode 100644 index 018ec9271..000000000 --- a/docs/content/en/templates/pagination.md +++ /dev/null @@ -1,241 +0,0 @@ ---- -title: Pagination -description: Split a list page into two or more subsets. -categories: [] -keywords: [] -weight: 160 -aliases: [/extras/pagination,/doc/pagination/] ---- - -Displaying a large page collection on a list page is not user-friendly: - -- A massive list can be intimidating and difficult to navigate. Users may get lost in the sheer volume of information. -- Large pages take longer to load, which can frustrate users and lead to them abandoning the site. -- Without any filtering or organization, finding a specific item becomes a tedious scrolling exercise. - -Improve usability by paginating `home`, `section`, `taxonomy`, and `term` pages. - -> [!note] -> The most common templating mistake related to pagination is invoking pagination more than once for a given list page. See the [caching](#caching) section below. - -## Terminology - -paginate -: To split a [list page](g) into two or more subsets. - -pagination -: The process of paginating a list page. - -pager -: Created during pagination, a pager contains a subset of a list page and navigation links to other pagers. - -paginator -: A collection of pagers. - -## Configuration - -See [configure pagination](/configuration/pagination). - -## Methods - -To paginate a `home`, `section`, `taxonomy`, or `term` page, invoke either of these methods on the `Page` object in the corresponding template: - -- [`Paginate`] -- [`Paginator`] - -The `Paginate` method is more flexible, allowing you to: - -- Paginate any page collection -- Filter, sort, and group the page collection -- Override the number of pages per pager as defined in your site configuration - -By comparison, the `Paginator` method paginates the page collection passed into the template, and you cannot override the number of pages per pager. - -## Examples - -To paginate a list page using the `Paginate` method: - -```go-html-template -{{ $pages := where site.RegularPages "Type" "posts" }} -{{ $paginator := .Paginate $pages.ByTitle 7 }} - -{{ range $paginator.Pages }} -

    {{ .LinkTitle }}

    -{{ end }} - -{{ template "_internal/pagination.html" . }} -``` - -In the example above, we: - -1. Build a page collection -1. Sort the page collection by title -1. Paginate the page collection, with 7 pages per pager -1. Range over the paginated page collection, rendering a link to each page -1. Call the embedded pagination template to create navigation links between pagers - -To paginate a list page using the `Paginator` method: - -```go-html-template -{{ range .Paginator.Pages }} -

    {{ .LinkTitle }}

    -{{ end }} - -{{ template "_internal/pagination.html" . }} -``` - -In the example above, we: - -1. Paginate the page collection passed into the template, with the default number of pages per pager -1. Range over the paginated page collection, rendering a link to each page -1. Call the embedded pagination template to create navigation links between pagers - -## Caching - -> [!note] -> The most common templating mistake related to pagination is invoking pagination more than once for a given list page. - -Regardless of pagination method, the initial invocation is cached and cannot be changed. If you invoke pagination more than once for a given list page, subsequent invocations use the cached result. This means that subsequent invocations will not behave as written. - -When paginating conditionally, do not use the `compare.Conditional` function due to its eager evaluation of arguments. Use an `if-else` construct instead. - -## Grouping - -Use pagination with any of the [grouping methods]. For example: - -```go-html-template -{{ $pages := where site.RegularPages "Type" "posts" }} -{{ $paginator := .Paginate ($pages.GroupByDate "Jan 2006") }} - -{{ range $paginator.PageGroups }} -

    {{ .Key }}

    - {{ range .Pages }} -

    {{ .LinkTitle }}

    - {{ end }} -{{ end }} - -{{ template "_internal/pagination.html" . }} -``` - -## Navigation - -As shown in the examples above, the easiest way to add navigation between pagers is with Hugo's embedded pagination template: - -```go-html-template -{{ template "_internal/pagination.html" . }} -``` - -The embedded pagination template has two formats: `default` and `terse`. The above is equivalent to: - -```go-html-template -{{ template "_internal/pagination.html" (dict "page" . "format" "default") }} -``` - -The `terse` format has fewer controls and page slots, consuming less space when styled as a horizontal list. To use the `terse` format: - -```go-html-template -{{ template "_internal/pagination.html" (dict "page" . "format" "terse") }} -``` - -> [!note] -> To override Hugo's embedded pagination template, copy the [source code] to a file with the same name in the `layouts/partials` directory, then call it from your templates using the [`partial`] function: -> -> `{{ partial "pagination.html" . }}` - -Create custom navigation components using any of the `Pager` methods: - -{{% list-pages-in-section path=/methods/pager %}} - -## Structure - -The example below depicts the published site structure when paginating a list page. - -With this content: - -```text -content/ -├── posts/ -│ ├── _index.md -│ ├── post-1.md -│ ├── post-2.md -│ ├── post-3.md -│ └── post-4.md -└── _index.md -``` - -And this site configuration: - -{{< code-toggle file=hugo >}} -[pagination] - disableAliases = false - pagerSize = 2 - path = 'page' -{{< /code-toggle >}} - -And this section template: - -```go-html-template -{{ range (.Paginate .Pages).Pages }} -

    {{ .LinkTitle }}

    -{{ end }} - -{{ template "_internal/pagination.html" . }} -``` - -The published site has this structure: - -```text -public/ -├── posts/ -│ ├── page/ -│ │ ├── 1/ -│ │ │ └── index.html <-- alias to public/posts/index.html -│ │ └── 2/ -│ │ └── index.html -│ ├── post-1/ -│ │ └── index.html -│ ├── post-2/ -│ │ └── index.html -│ ├── post-3/ -│ │ └── index.html -│ ├── post-4/ -│ │ └── index.html -│ └── index.html -└── index.html -``` - -To disable alias generation for the first pager, change your site configuration: - -{{< code-toggle file=hugo >}} -[pagination] - disableAliases = true - pagerSize = 2 - path = 'page' -{{< /code-toggle >}} - -Now the published site will have this structure: - -```text -public/ -├── posts/ -│ ├── page/ -│ │ └── 2/ -│ │ └── index.html -│ ├── post-1/ -│ │ └── index.html -│ ├── post-2/ -│ │ └── index.html -│ ├── post-3/ -│ │ └── index.html -│ ├── post-4/ -│ │ └── index.html -│ └── index.html -└── index.html -``` - -[`Paginate`]: /methods/page/paginate/ -[`Paginator`]: /methods/page/paginator/ -[`partial`]: /functions/partials/include/ -[grouping methods]: /quick-reference/page-collections/#group -[grouping methods]: /quick-reference/page-collections/#group -[source code]: {{% eturl pagination %}} diff --git a/docs/content/en/templates/partial.md b/docs/content/en/templates/partial.md deleted file mode 100644 index 7ff2d9594..000000000 --- a/docs/content/en/templates/partial.md +++ /dev/null @@ -1,158 +0,0 @@ ---- -title: Partial templates -description: Partials are smaller, context-aware components in your list and page templates that can be used economically to keep your templating DRY. -categories: [] -keywords: [] -weight: 100 -aliases: [/templates/partials/,/layout/chrome/] ---- - -{{< youtube pjS4pOLyB7c >}} - -## Use partials in your templates - -All partials for your Hugo project are located in a single `layouts/partials` directory. For better organization, you can create multiple subdirectories within `partials` as well: - -```txt -layouts/ -└── partials/ - ├── footer/ - │ ├── scripts.html - │ └── site-footer.html - ├── head/ - │ ├── favicons.html - │ ├── metadata.html - │ └── prerender.html - └── header/ - ├── site-header.html - └── site-nav.html -``` - -All partials are called within your templates using the following pattern: - -```go-html-template -{{ partial "/.html" . }} -``` - -> [!note] -> One of the most common mistakes with new Hugo users is failing to pass a context to the partial call. In the pattern above, note how "the dot" (`.`) is required as the second argument to give the partial context. You can read more about "the dot" in the [Hugo templating introduction](/templates/introduction/#context). - -> [!note] -> Do not include the word "baseof" when naming partial templates. The word "baseof" is reserved for base templates. - -As shown in the above example directory structure, you can nest your directories within `partials` for better source organization. You only need to call the nested partial's path relative to the `partials` directory: - -```go-html-template -{{ partial "header/site-header.html" . }} -{{ partial "footer/scripts.html" . }} -``` - -### Variable scoping - -The second argument in a partial call is the variable being passed down. The above examples are passing the dot (`.`), which tells the template receiving the partial to apply the current [context][context]. - -This means the partial will *only* be able to access those variables. The partial is isolated and cannot access the outer scope. From within the partial, `$.Var` is equivalent to `.Var`. - -## Returning a value from a partial - -In addition to outputting markup, partials can be used to return a value of any type. In order to return a value, a partial must include a lone `return` statement *at the end of the partial*. - -### Example GetFeatured - -```go-html-template -{{/* layouts/partials/GetFeatured.html */}} -{{ return first . (where site.RegularPages "Params.featured" true) }} -``` - -```go-html-template -{{/* layouts/index.html */}} -{{ range partial "GetFeatured.html" 5 }} - [...] -{{ end }} -``` - -### Example GetImage - -```go-html-template -{{/* layouts/partials/GetImage.html */}} -{{ $image := false }} -{{ with .Params.gallery }} - {{ $image = index . 0 }} -{{ end }} -{{ with .Params.image }} - {{ $image = . }} -{{ end }} -{{ return $image }} -``` - -```go-html-template -{{/* layouts/_default/single.html */}} -{{ with partial "GetImage.html" . }} - [...] -{{ end }} -``` - -> [!note] -> Only one `return` statement is allowed per partial file. - -## Inline partials - -You can also define partials inline in the template. But remember that template namespace is global, so you need to make sure that the names are unique to avoid conflicts. - -```go-html-template -Value: {{ partial "my-inline-partial.html" . }} - -{{ define "partials/my-inline-partial.html" }} -{{ $value := 32 }} -{{ return $value }} -{{ end }} -``` - -## Cached partials - -The `partialCached` template function provides significant performance gains for complex templates that don't need to be re-rendered on every invocation. See [details][partialcached]. - -## Examples - -### `header.html` - -The following `header.html` partial template is used for [spf13.com](https://spf13.com/): - -```go-html-template {file="layouts/partials/header.html"} - - - - - - {{ partial "meta.html" . }} - - - {{ .Title }} : spf13.com - - {{ if .RSSLink }}{{ end }} - - {{ partial "head_includes.html" . }} - -``` - -> [!note] -> The `header.html` example partial was built before the introduction of block templates to Hugo. Read more on [base templates and blocks](/templates/base/) for defining the outer chrome or shell of your master templates (i.e., your site's head, header, and footer). You can even combine blocks and partials for added flexibility. - -### `footer.html` - -The following `footer.html` partial template is used for [spf13.com](https://spf13.com/): - -```go-html-template {file="layouts/partials/footer.html"} -
    -
    -

    - © 2013-14 Steve Francia. - Some rights reserved; - please attribute properly and link back. -

    -
    -
    -``` - -[context]: /templates/introduction/ -[partialcached]: /functions/partials/includecached/ diff --git a/docs/content/en/templates/robots.md b/docs/content/en/templates/robots.md deleted file mode 100644 index 2d412d775..000000000 --- a/docs/content/en/templates/robots.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: robots.txt template -linkTitle: robots.txt templates -description: Hugo can generate a customized robots.txt in the same way as any other template. -categories: [] -keywords: [] -weight: 180 -aliases: [/extras/robots-txt/] ---- - -To generate a robots.txt file from a template, change the [site configuration]: - -{{< code-toggle file=hugo >}} -enableRobotsTXT = true -{{< /code-toggle >}} - -By default, Hugo generates robots.txt using an [embedded template]. - -```text -User-agent: * -``` - -Search engines that honor the Robots Exclusion Protocol will interpret this as permission to crawl everything on the site. - -## robots.txt template lookup order - -You may overwrite the internal template with a custom template. Hugo selects the template using this lookup order: - -1. `/layouts/robots.txt` -1. `/themes//layouts/robots.txt` - -## robots.txt template example - -```text {file="layouts/robots.txt"} -User-agent: * -{{ range .Pages }} -Disallow: {{ .RelPermalink }} -{{ end }} -``` - -This template creates a robots.txt file with a `Disallow` directive for each page on the site. Search engines that honor the Robots Exclusion Protocol will not crawl any page on the site. - -> [!note] -> To create a robots.txt file without using a template: -> -> 1. Set `enableRobotsTXT` to `false` in the site configuration. -> 1. Create a robots.txt file in the `static` directory. -> -> Remember that Hugo copies everything in the [`static` directory][static] to the root of `publishDir` (typically `public`) when you build your site. - -[embedded template]: {{% eturl robots %}} -[site configuration]: /configuration/ -[static]: /getting-started/directory-structure/ diff --git a/docs/content/en/templates/rss.md b/docs/content/en/templates/rss.md deleted file mode 100644 index f387a71e3..000000000 --- a/docs/content/en/templates/rss.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -title: RSS templates -description: Use the embedded RSS template, or create your own. -categories: [] -keywords: [] -weight: 140 ---- - -## Configuration - -By default, when you build your site, Hugo generates RSS feeds for home, section, taxonomy, and term pages. Control feed generation in your site configuration. For example, to generate feeds for home and section pages, but not for taxonomy and term pages: - -{{< code-toggle file=hugo >}} -[outputs] -home = ['html', 'rss'] -section = ['html', 'rss'] -taxonomy = ['html'] -term = ['html'] -{{< /code-toggle >}} - -To disable feed generation for all [page kinds](g): - -{{< code-toggle file=hugo >}} -disableKinds = ['rss'] -{{< /code-toggle >}} - -By default, the number of items in each feed is unlimited. Change this as needed in your site configuration: - -{{< code-toggle file=hugo >}} -[services.rss] -limit = 42 -{{< /code-toggle >}} - -Set `limit` to `-1` to generate an unlimited number of items per feed. - -The built-in RSS template will render the following values, if present, from your site configuration: - -{{< code-toggle file=hugo >}} -copyright = '© 2023 ABC Widgets, Inc.' -[params.author] -name = 'John Doe' -email = 'jdoe@example.org' -{{< /code-toggle >}} - -## Include feed reference - -To include a feed reference in the `head` element of your rendered pages, place this within the `head` element of your templates: - -```go-html-template -{{ with .OutputFormats.Get "rss" }} - {{ printf `` .Rel .MediaType.Type .Permalink site.Title | safeHTML }} -{{ end }} -``` - -Hugo will render this to: - -```html - -``` - -## Custom templates - -Override Hugo's [embedded RSS template] by creating one or more of your own, following the naming conventions as shown in the [template lookup order]. - -For example, to use different templates for home, section, taxonomy, and term pages: - -```text -layouts/ -└── _default/ - ├── home.rss.xml - ├── section.rss.xml - ├── taxonomy.rss.xml - └── term.rss.xml -``` - -RSS templates receive the `.Page` and `.Site` objects in context. - -[embedded RSS template]: {{% eturl rss %}} -[template lookup order]: /templates/lookup-order/#rss-templates diff --git a/docs/content/en/templates/section.md b/docs/content/en/templates/section.md deleted file mode 100644 index 8bc0f9dab..000000000 --- a/docs/content/en/templates/section.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -title: Section templates -description: Create a section template to list its members. -categories: [] -keywords: [] -weight: 70 -aliases: [/templates/sections/,/templates/section-templates/] ---- - -## Add content and front matter to section templates - -To effectively leverage section templates, you should first understand Hugo's [content organization](/content-management/organization/) and, specifically, the purpose of `_index.md` for adding content and front matter to section and other list pages. - -## Section template lookup order - -See [Template Lookup](/templates/lookup-order/). - -## Example: creating a default section template - -```go-html-template {file="layouts/_default/section.html"} -{{ define "main" }} -
    - {{ .Content }} - - {{ $pages := where site.RegularPages "Type" "posts" }} - {{ $paginator := .Paginate $pages }} - - {{ range $paginator.Pages }} -

    {{ .LinkTitle }}

    - {{ end }} - - {{ template "_internal/pagination.html" . }} -
    -{{ end }} -``` - -### Example: using `.Site.GetPage` - -The `.Site.GetPage` example that follows assumes the following project directory structure: - -```txt -. -└── content - ├── blog - │ ├── _index.md <-- title: My Hugo Blog - │ ├── post-1.md - │ ├── post-2.md - │ └── post-3.md - └── events - ├── event-1.md - └── event-2.md -``` - -`.Site.GetPage` will return `nil` if no `_index.md` page is found. Therefore, if `content/blog/_index.md` does not exist, the template will output the section name: - -```go-html-template -

    {{ with .Site.GetPage "/blog" }}{{ .Title }}{{ end }}

    -``` - -Since `blog` has a section index page with front matter at `content/blog/_index.md`, the above code will return the following result: - -```html -

    My Hugo Blog

    -``` - -If we try the same code with the `events` section, however, Hugo will default to the section title because there is no `content/events/_index.md` from which to pull content and front matter: - -```go-html-template -

    {{ with .Site.GetPage "/events" }}{{ .Title }}{{ end }}

    -``` - -Which then returns the following: - -```html -

    Events

    -``` - -[contentorg]: /content-management/organization/ -[lookup]: /templates/lookup-order/ -[`where`]: /functions/collections/where/ -[sections]: /content-management/sections/ diff --git a/docs/content/en/templates/shortcode.md b/docs/content/en/templates/shortcode.md deleted file mode 100644 index 3ed573651..000000000 --- a/docs/content/en/templates/shortcode.md +++ /dev/null @@ -1,338 +0,0 @@ ---- -title: Shortcode templates -description: Create custom shortcodes to simplify and standardize content creation. -categories: [] -keywords: [] -weight: 120 -aliases: [/templates/shortcode-templates/] ---- - -> [!note] -> Before creating custom shortcodes, please review the [shortcodes] page in the [content management] section. Understanding the usage details will help you design and create better templates. - -## Introduction - -Hugo provides [embedded shortcodes] for many common tasks, but you'll likely need to create your own for more specific needs. Some examples of custom shortcodes you might develop include: - -- Audio players -- Video players -- Image galleries -- Diagrams -- Maps -- Tables -- And many other custom elements - -## Directory structure - -Create shortcode templates within the `layouts/shortcodes` directory, either at its root or organized into subdirectories. - -```text -layouts/ -└── shortcodes/ - ├── diagrams/ - │ ├── kroki.html - │ └── plotly.html - ├── media/ - │ ├── audio.html - │ ├── gallery.html - │ └── video.html - ├── capture.html - ├── column.html - ├── include.html - └── row.html -``` - -When calling a shortcode in a subdirectory, specify its path relative to the `shortcode` directory, excluding the file extension. - -```text -{{}} -``` - -## Lookup order - -Hugo selects shortcode templates based on the shortcode name, the current output format, and the current language. The examples below are sorted by specificity in descending order. The least specific path is at the bottom of the list. - -Shortcode name|Output format|Language|Template path -:--|:--|:--|:-- -foo|html|en|`layouts/shortcodes/foo.en.html` -foo|html|en|`layouts/shortcodes/foo.html.html` -foo|html|en|`layouts/shortcodes/foo.html` -foo|html|en|`layouts/shortcodes/foo.html.en.html` - -Shortcode name|Output format|Language|Template path -:--|:--|:--|:-- -foo|json|en|`layouts/shortcodes/foo.en.json` -foo|json|en|`layouts/shortcodes/foo.json` -foo|json|en|`layouts/shortcodes/foo.json.json` -foo|json|en|`layouts/shortcodes/foo.json.en.json` - -## Methods - -Use these methods in your shortcode templates. Refer to each methods's documentation for details and examples. - -{{% list-pages-in-section path=/methods/shortcode %}} - -## Examples - -These examples range in complexity from simple to moderately advanced, with some simplified for clarity. - -### Insert year - -Create a shortcode to insert the current year: - -```go-html-template {file="layouts/shortcodes/year.html"} -{{- now.Format "2006" -}} -``` - -Then call the shortcode from within your markup: - -```text {file="content/example.md"} -This is {{}}, and look at how far we've come. -``` - -This shortcode can be used inline or as a block on its own line. If a shortcode might be used inline, remove the surrounding [whitespace] by using [template action](g) delimiters with hyphens. - -### Insert image - -This example assumes the following content structure, where `content/example/index.md` is a [page bundle](g) containing one or more [page resources](g). - -```text -content/ -├── example/ -│ ├── a.jpg -│ └── index.md -└── _index.md -``` - -Create a shortcode to capture an image as a page resource, resize it to the given width, convert it to the WebP format, and add an `alt` attribute: - -```go-html-template {file="layouts/shortcodes/image.html"} -{{- with .Page.Resources.Get (.Get "path") }} - {{- with .Process (printf "resize %dx wepb" ($.Get "width")) -}} - {{ $.Get - {{- end }} -{{- end -}} -``` - -Then call the shortcode from within your markup: - -```text {file="content/example/index.md"} -{{}} -``` - -The example above uses: - -- The [`with`] statement to rebind the [context](g) after each successful operation -- The [`Get`] method to retrieve arguments by name -- The `$` to access the template context - -> [!note] -> Make sure that you thoroughly understand the concept of context. The most common templating errors made by new users relate to context. -> -> Read more about context in the [introduction to templating]. - -### Insert image with error handling - -The previous example, while functional, silently fails if the image is missing, and does not gracefully exit if a required argument is missing. We'll add error handling to address these issues: - -```go-html-template {file="layouts/shortcodes/image.html"} -{{- with .Get "path" }} - {{- with $r := $.Page.Resources.Get ($.Get "path") }} - {{- with $.Get "width" }} - {{- with $r.Process (printf "resize %dx wepb" ($.Get "width" )) }} - {{- $alt := or ($.Get "alt") "" -}} - {{ $alt }} - {{- end }} - {{- else }} - {{- errorf "The %q shortcode requires a 'width' argument: see %s" $.Name $.Position }} - {{- end }} - {{- else }} - {{- warnf "The %q shortcode was unable to find %s: see %s" $.Name ($.Get "path") $.Position }} - {{- end }} -{{- else }} - {{- errorf "The %q shortcode requires a 'path' argument: see %s" .Name .Position }} -{{- end -}} -``` - -This template throws an error and gracefully fails the build if the author neglected to provide a `path` or `width` argument, and it emits a warning if it cannot find the image at the specified path. If the author does not provide an `alt` argument, the `alt` attribute is set to an empty string. - -The [`Name`] and [`Position`] methods provide helpful context for errors and warnings. For example, a missing `width` argument causes the shortcode to throw this error: - -```text -ERROR The "image" shortcode requires a 'width' argument: see "/home/user/project/content/example/index.md:7:1" -``` - -### Positional arguments - -Shortcode arguments can be [named or positional]. We used named arguments previously; let's explore positional arguments. Here's the named argument version of our example: - -```text {file="content/example/index.md"} -{{}} -``` - -Here's how to call it with positional arguments: - -```text {file="content/example/index.md"} -{{}} -``` - -Using the `Get` method with zero-indexed keys, we'll initialize variables with descriptive names in our template: - -```go-html-template {file="layouts/shortcodes/image.html"} -{{ $path := .Get 0 }} -{{ $width := .Get 1 }} -{{ $alt := .Get 2 }} -``` - -> [!note] -> Positional arguments work well for frequently used shortcodes with one or two arguments. Since you'll use them often, the argument order will be easy to remember. For less frequently used shortcodes, or those with more than two arguments, named arguments improve readability and reduce the chance of errors. - -### Named and positional arguments - -You can create a shortcode that will accept both named and positional arguments, but not at the same time. Use the [`IsNamedParams`] method to determine whether the shortcode call used named or positional arguments: - -```go-html-template {file="layouts/shortcodes/image.html"} -{{ $path := cond (.IsNamedParams) (.Get "path") (.Get 0) }} -{{ $width := cond (.IsNamedParams) (.Get "width") (.Get 1) }} -{{ $alt := cond (.IsNamedParams) (.Get "alt") (.Get 2) }} -``` - -This example uses the `cond` alias for the [`compare.Conditional`] function to get the argument by name if `IsNamedParams` returns `true`, otherwise get the argument by position. - -### Argument collection - -Use the [`Params`] method to access the arguments as a collection. - -When using named arguments, the `Params` method returns a map: - -```text {file="content/example/index.md"} -{{}} -``` - -```go-html-template {file="layouts/shortcodes/image.html"} -{{ .Params.path }} → a.jpg -{{ .Params.width }} → 300 -{{ .Params.alt }} → A white kitten -``` - - When using positional arguments, the `Params` method returns a slice: - -```text {file="content/example/index.md"} -{{}} -``` - -```go-html-template {file="layouts/shortcodes/image.html"} -{{ index .Params 0 }} → a.jpg -{{ index .Params 1 }} → 300 -{{ index .Params 1 }} → A white kitten -``` - -Combine the `Params` method with the [`collections.IsSet`] function to determine if a parameter is set, even if its value is falsy. - -### Inner content - -Extract the content enclosed within shortcode tags using the [`Inner`] method. This example demonstrates how to pass both content and a title to a shortcode. The shortcode then generates a `div` element containing an `h2` element (displaying the title) and the provided content. - -```text {file="content/example.md"} -{{}} -This is a **bold** word, and this is an _emphasized_ word. -{{}} -``` - -```go-html-template {file="layouts/shortcodes/contrived.html"} -
    -

    {{ .Get "title" }}

    - {{ .Inner | .Page.RenderString }} -
    -``` - -The preceding example called the shortcode using [standard notation], requiring us to process the inner content with the [`RenderString`] method to convert the Markdown to HTML. This conversion is unnecessary when calling a shortcode using [Markdown notation]. - -### Nesting - -The [`Parent`] method provides access to the parent shortcode context when the shortcode in question is called within the context of a parent shortcode. This provides an inheritance model. - -The following example is contrived but demonstrates the concept. Assume you have a `gallery` shortcode that expects one named `class` argument: - -```go-html-template {file="layouts/shortcodes/gallery.html"} -
    - {{ .Inner }} -
    -``` - -You also have an `img` shortcode with a single named `src` argument that you want to call inside of `gallery` and other shortcodes, so that the parent defines the context of each `img`: - -```go-html-template {file="layouts/shortcodes/img.html"} -{{ $src := .Get "src" }} -{{ with .Parent }} - -{{ else }} - -{{ end }} -``` - -You can then call your shortcode in your content as follows: - -```text {file="content/example.md"} -{{}} - {{}} - {{}} -{{}} -{{}} -``` - -This will output the following HTML. Note how the first two `img` shortcodes inherit the `class` value of `content-gallery` set with the call to the parent `gallery`, whereas the third `img` only uses `src`: - -```html - - -``` - -### Other examples - -For guidance, consider examining Hugo's embedded shortcodes. The source code, available on [GitHub], can provide a useful model. - -## Detection - -The [`HasShortcode`] method allows you to check if a specific shortcode has been called on a page. For example, consider a custom audio shortcode: - -```text {file="content/example.md"} -{{}} -``` - -You can use the `HasShortcode` method in your base template to conditionally load CSS if the audio shortcode was used on the page: - -```go-html-template {file="layouts/_default/baseof.html"} - - ... - {{ if .HasShortcode "audio" }} - - {{ end }} - ... - -``` - -[`collections.IsSet`]: /functions/collections/isset/ -[`compare.Conditional`]: /functions/compare/conditional/ -[`Get`]: /methods/shortcode/get/ -[`HasShortcode`]: /methods/page/hasshortcode/ -[`Inner`]: /methods/shortcode/inner/ -[`IsNamedParams`]: /methods/shortcode/isnamedparams/ -[`Name`]: /methods/shortcode/name/ -[`Params`]: /methods/shortcode/params/ -[`Parent`]: /methods/shortcode/parent/ -[`Position`]: /methods/shortcode/position/ -[`RenderString`]: /methods/page/renderstring/ -[`with`]: /functions/go-template/with/ -[content management]: /content-management/shortcodes/ -[embedded shortcodes]: /shortcodes/ -[GitHub]: https://github.com/gohugoio/hugo/tree/master/tpl/tplimpl/embedded/templates/_shortcodes -[introduction to templating]: /templates/introduction/ -[Markdown notation]: /content-management/shortcodes/#markdown-notation -[named or positional]: /content-management/shortcodes/#arguments -[shortcodes]: /content-management/shortcodes/ -[standard notation]: /content-management/shortcodes/#standard-notation -[whitespace]: /templates/introduction/#whitespace diff --git a/docs/content/en/templates/single.md b/docs/content/en/templates/single.md deleted file mode 100644 index 6f244ef10..000000000 --- a/docs/content/en/templates/single.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -title: Single templates -description: Create a single template to render a single page. -categories: [] -keywords: [] -weight: 60 -aliases: [/layout/content/,/templates/single-page-templates/] ---- - -The single template below inherits the site's shell from the [base template]. - -[base template]: /templates/types/ - -```go-html-template {file="layouts/_default/single.html"} -{{ define "main" }} -

    {{ .Title }}

    - {{ .Content }} -{{ end }} -``` - -Review the [template lookup order] to select a template path that provides the desired level of specificity. - -[template lookup order]: /templates/lookup-order/#single-templates - -The single template below inherits the site's shell from the base template, and renders the page title, creation date, content, and a list of associated terms in the "tags" taxonomy. - -```go-html-template {file="layouts/_default/single.html"} -{{ define "main" }} -
    -

    {{ .Title }}

    - {{ with .Date }} - {{ $dateMachine := . | time.Format "2006-01-02T15:04:05-07:00" }} - {{ $dateHuman := . | time.Format ":date_long" }} - - {{ end }} -
    - {{ .Content }} -
    - -
    -{{ end }} -``` diff --git a/docs/content/en/templates/sitemap.md b/docs/content/en/templates/sitemap.md deleted file mode 100644 index bf0850eef..000000000 --- a/docs/content/en/templates/sitemap.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -title: Sitemap templates -description: Hugo provides built-in sitemap templates. -categories: [] -keywords: [] -weight: 130 -aliases: [/layout/sitemap/,/templates/sitemap-template/] ---- - -## Overview - -Hugo's embedded sitemap templates conform to v0.9 of the [sitemap protocol]. - -With a monolingual project, Hugo generates a sitemap.xml file in the root of the [`publishDir`] using the [embedded sitemap template]. - -With a multilingual project, Hugo generates: - -- A sitemap.xml file in the root of each site (language) using the [embedded sitemap template] -- A sitemap.xml file in the root of the [`publishDir`] using the [embedded sitemapindex template] - -## Configuration - -See [configure sitemap](/configuration/sitemap). - -## Override default values - -Override the default values for a given page in front matter. - -{{< code-toggle file=news.md fm=true >}} -title = 'News' -[sitemap] - changefreq = 'weekly' - disable = true - priority = 0.8 -{{}} - -## Override built-in templates - -To override the built-in sitemap.xml template, create a new file in either of these locations: - -- `layouts/sitemap.xml` -- `layouts/_default/sitemap.xml` - -When ranging through the page collection, access the _change frequency_ and _priority_ with `.Sitemap.ChangeFreq` and `.Sitemap.Priority` respectively. - -To override the built-in sitemapindex.xml template, create a new file in either of these locations: - -- `layouts/sitemapindex.xml` -- `layouts/_default/sitemapindex.xml` - -## Disable sitemap generation - -You may disable sitemap generation in your site configuration: - -{{< code-toggle file=hugo >}} -disableKinds = ['sitemap'] -{{}} - -[`publishDir`]: /configuration/all/#publishdir -[embedded sitemap template]: {{% eturl sitemap %}} -[embedded sitemapindex template]: {{% eturl sitemapindex %}} -[sitemap protocol]: https://www.sitemaps.org/protocol.html diff --git a/docs/content/en/templates/taxonomy.md b/docs/content/en/templates/taxonomy.md deleted file mode 100644 index 96c93ec95..000000000 --- a/docs/content/en/templates/taxonomy.md +++ /dev/null @@ -1,164 +0,0 @@ ---- -title: Taxonomy templates -description: Create a taxonomy template to render a list of terms. -categories: [] -keywords: [] -weight: 80 -aliases: [/taxonomies/displaying/,/templates/terms/,/indexes/displaying/,/taxonomies/templates/,/indexes/ordering/, /templates/taxonomies/, /templates/taxonomy-templates/] ---- - -The [taxonomy](g) template below inherits the site's shell from the [base template], and renders a list of [terms](g) in the current taxonomy. - -[base template]: /templates/types/ - -```go-html-template {file="layouts/_default/taxonomy.html"} -{{ define "main" }} -

    {{ .Title }}

    - {{ .Content }} - {{ range .Pages }} -

    {{ .LinkTitle }}

    - {{ end }} -{{ end }} -``` - -Review the [template lookup order] to select a template path that provides the desired level of specificity. - -[template lookup order]: /templates/lookup-order/#taxonomy-templates - -In the example above, the taxonomy and term will be capitalized if their respective pages are not backed by files. You can disable this in your site configuration: - -{{< code-toggle file=hugo >}} -capitalizeListTitles = false -{{< /code-toggle >}} - -## Data object - -Use these methods on the `Data` object within a taxonomy template. - -Singular -: (`string`) Returns the singular name of the taxonomy. - -```go-html-template -{{ .Data.Singular }} → tag -``` - -Plural -: (`string`) Returns the plural name of the taxonomy. - -```go-html-template -{{ .Data.Plural }} → tags -``` - -Terms -: (`page.Taxonomy`) Returns the `Taxonomy` object, consisting of a map of terms and the [weighted pages](g) associated with each term. - -```go-html-template -{{ $taxonomyObject := .Data.Terms }} -``` - -Once we have the `Taxonomy` object, we can call any of its [methods], allowing us to sort alphabetically or by term count. - -[methods]: /methods/taxonomy/ - -## Sort alphabetically - -The taxonomy template below inherits the site's shell from the base template, and renders a list of terms in the current taxonomy. Hugo sorts the list alphabetically by term, and displays the number of pages associated with each term. - -```go-html-template {file="layouts/_default/taxonomy.html"} -{{ define "main" }} -

    {{ .Title }}

    - {{ .Content }} - {{ range .Data.Terms.Alphabetical }} -

    {{ .Page.LinkTitle }} ({{ .Count }})

    - {{ end }} -{{ end }} -``` - -## Sort by term count - -The taxonomy template below inherits the site's shell from the base template, and renders a list of terms in the current taxonomy. Hugo sorts the list by the number of pages associated with each term, and displays the number of pages associated with each term. - -```go-html-template {file="layouts/_default/taxonomy.html"} -{{ define "main" }} -

    {{ .Title }}

    - {{ .Content }} - {{ range .Data.Terms.ByCount }} -

    {{ .Page.LinkTitle }} ({{ .Count }})

    - {{ end }} -{{ end }} -``` - -## Include content links - -The [`Alphabetical`] and [`ByCount`] methods used in the previous examples return an [ordered taxonomy](g), so we can also list the content to which each term is assigned. - -[`Alphabetical`]: /methods/taxonomy/alphabetical/ -[`ByCount`]: /methods/taxonomy/bycount/ - -The taxonomy template below inherits the site's shell from the base template, and renders a list of terms in the current taxonomy. Hugo sorts the list by the number of pages associated with each term, displays the number of pages associated with each term, then lists the content to which each term is assigned. - -```go-html-template {file="layouts/_default/taxonomy.html"} -{{ define "main" }} -

    {{ .Title }}

    - {{ .Content }} - {{ range .Data.Terms.ByCount }} -

    {{ .Page.LinkTitle }} ({{ .Count }})

    - - {{ end }} -{{ end }} -``` - -## Display metadata - -Display metadata about each term by creating a corresponding branch bundle in the `content` directory. - -For example, create an "authors" taxonomy: - -{{< code-toggle file=hugo >}} -[taxonomies] -author = 'authors' -{{< /code-toggle >}} - -Then create content with one [branch bundle](g) for each term: - -```text -content/ -└── authors/ - ├── jsmith/ - │ ├── _index.md - │ └── portrait.jpg - └── rjones/ - ├── _index.md - └── portrait.jpg -``` - -Then add front matter to each term page: - -{{< code-toggle file=content/authors/jsmith/_index.md fm=true >}} -title = "John Smith" -affiliation = "University of Chicago" -{{< /code-toggle >}} - -Then create a taxonomy template specific to the "authors" taxonomy: - -```go-html-template {file="layouts/authors/taxonomy.html"} -{{ define "main" }} -

    {{ .Title }}

    - {{ .Content }} - {{ range .Data.Terms.Alphabetical }} -

    {{ .Page.LinkTitle }}

    -

    Affiliation: {{ .Page.Params.Affiliation }}

    - {{ with .Page.Resources.Get "portrait.jpg" }} - {{ with .Fill "100x100" }} - portrait - {{ end }} - {{ end }} - {{ end }} -{{ end }} -``` - -In the example above we list each author including their affiliation and portrait. diff --git a/docs/content/en/templates/term.md b/docs/content/en/templates/term.md deleted file mode 100644 index cf1097e86..000000000 --- a/docs/content/en/templates/term.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -title: Term templates -description: Create a term template to render a list of pages associated with the current term. -categories: [] -keywords: [] -weight: 90 ---- - -The [term](g) template below inherits the site's shell from the [base template], and renders a list of pages associated with the current term. - -[base template]: /templates/types/ - -```go-html-template {file="layouts/_default/term.html"} -{{ define "main" }} -

    {{ .Title }}

    - {{ .Content }} - {{ range .Pages }} -

    {{ .LinkTitle }}

    - {{ end }} -{{ end }} -``` - -Review the [template lookup order] to select a template path that provides the desired level of specificity. - -[template lookup order]: /templates/lookup-order/#taxonomy-templates - -In the example above, the term will be capitalized if its respective page is not backed by a file. You can disable this in your site configuration: - -{{< code-toggle file=hugo >}} -capitalizeListTitles = false -{{< /code-toggle >}} - -## Data object - -Use these methods on the `Data` object within a term template. - -Singular -: (`string`) Returns the singular name of the taxonomy. - -```go-html-template -{{ .Data.Singular }} → tag -``` - -Plural -: (`string`) Returns the plural name of the taxonomy. - -```go-html-template -{{ .Data.Plural }} → tags -``` - -Term -: (`string`) Returns the name of the term. - -```go-html-template -{{ .Data.Term }} → fiction -``` - -## Display metadata - -Display metadata about each term by creating a corresponding branch bundle in the `content` directory. - -For example, create an "authors" taxonomy: - -{{< code-toggle file=hugo >}} -[taxonomies] -author = 'authors' -{{< /code-toggle >}} - -Then create content with one [branch bundle](g) for each term: - -```text -content/ -└── authors/ - ├── jsmith/ - │ ├── _index.md - │ └── portrait.jpg - └── rjones/ - ├── _index.md - └── portrait.jpg -``` - -Then add front matter to each term page: - -{{< code-toggle file=content/authors/jsmith/_index.md fm=true >}} -title = "John Smith" -affiliation = "University of Chicago" -{{< /code-toggle >}} - -Then create a term template specific to the "authors" taxonomy: - -```go-html-template {file="layouts/authors/term.html"} -{{ define "main" }} -

    {{ .Title }}

    -

    Affiliation: {{ .Params.affiliation }}

    - {{ with .Resources.Get "portrait.jpg" }} - {{ with .Fill "100x100" }} - portrait - {{ end }} - {{ end }} - {{ .Content }} - {{ range .Pages }} -

    {{ .LinkTitle }}

    - {{ end }} -{{ end }} -``` - -In the example above we display the author with their affiliation and portrait, then a list of associated content. diff --git a/docs/content/en/templates/types.md b/docs/content/en/templates/types.md deleted file mode 100644 index b44d3eb47..000000000 --- a/docs/content/en/templates/types.md +++ /dev/null @@ -1,261 +0,0 @@ ---- -title: Template types -description: Create templates of different types to render your content, resources, and data. -categories: [] -keywords: [] -weight: 30 -aliases: ['/templates/lists/'] ---- - -## Structure - -Create templates in the `layouts` directory in the root of your project. - -Although your site may not require each of these templates, the example below is typical for a site of medium complexity. - -```text -layouts/ -├── _default/ -│ ├── _markup/ -│ │ ├── render-image.html <-- render hook -│ │ └── render-link.html <-- render hook -│ ├── baseof.html -│ ├── home.html -│ ├── section.html -│ ├── single.html -│ ├── taxonomy.html -│ └── term.html -├── articles/ -│ └── card.html <-- content view -├── partials/ -│ ├── footer.html -│ └── header.html -└── shortcodes/ - ├── audio.html - └── video.html -``` - -Hugo's [template lookup order] determines the template path, allowing you to create unique templates for any page. - -> [!note] -> You must have thorough understanding of the template lookup order when creating templates. Template selection is based on template type, page kind, content type, section, language, and output format. - -The purpose of each template type is described below. - -## Base - -Base templates reduce duplicate code by wrapping other templates within a shell. - -For example, the base template below calls the [partial] function to include partial templates for the `head`, `header`, and `footer` elements of each page, and it uses the [block] function to include `home`, `single`, `section`, `taxonomy`, and `term` templates within the `main` element of each page. - -```go-html-template {file="layouts/_default/baseof.html"} - - - - {{ partial "head.html" . }} - - -
    - {{ partial "header.html" . }} -
    -
    - {{ block "main" . }}{{ end }} -
    -
    - {{ partial "footer.html" . }} -
    - - -``` - -Learn more about [base templates](/templates/base/). - -## Home - -A home page template is used to render your site's home page, and is the only template required for a single-page website. For example, the home page template below inherits the site's shell from the base template and renders the home page content, such as a list of other pages. - -```go-html-template {file="layouts/_default/home.html"} -{{ define "main" }} - {{ .Content }} - {{ range site.RegularPages }} -

    {{ .LinkTitle }}

    - {{ end }} -{{ end }} -``` - -{{% include "/_common/filter-sort-group.md" %}} - -Learn more about [home page templates](/templates/home/). - -## Single - -A single template renders a single page. - -For example, the single template below inherits the site's shell from the base template, and renders the title and content of each page. - -```go-html-template {file="layouts/_default/single.html"} -{{ define "main" }} -

    {{ .Title }}

    - {{ .Content }} -{{ end }} -``` - -Learn more about [single templates](/templates/single/). - -## Section - -A section template typically renders a list of pages within a section. - -For example, the section template below inherits the site's shell from the base template, and renders a list of pages in the current section. - -```go-html-template {file="layouts/_default/section.html"} -{{ define "main" }} -

    {{ .Title }}

    - {{ .Content }} - {{ range .Pages }} -

    {{ .LinkTitle }}

    - {{ end }} -{{ end }} -``` - -{{% include "/_common/filter-sort-group.md" %}} - -Learn more about [section templates](/templates/section/). - -## Taxonomy - -A taxonomy template renders a list of terms in a [taxonomy](g). - -For example, the taxonomy template below inherits the site's shell from the base template, and renders a list of terms in the current taxonomy. - -```go-html-template {file="layouts/_default/taxonomy.html"} -{{ define "main" }} -

    {{ .Title }}

    - {{ .Content }} - {{ range .Pages }} -

    {{ .LinkTitle }}

    - {{ end }} -{{ end }} -``` - -{{% include "/_common/filter-sort-group.md" %}} - -Learn more about [taxonomy templates](/templates/taxonomy/). - -## Term - -A term template renders a list of pages associated with a [term](g). - -For example, the term template below inherits the site's shell from the base template, and renders a list of pages associated with the current term. - -```go-html-template {file="layouts/_default/term.html"} -{{ define "main" }} -

    {{ .Title }}

    - {{ .Content }} - {{ range .Pages }} -

    {{ .LinkTitle }}

    - {{ end }} -{{ end }} -``` - -{{% include "/_common/filter-sort-group.md" %}} - -Learn more about [term templates](/templates/term/). - -## Partial - -A partial template is typically used to render a component of your site, though you may also create partial templates that return values. - -> [!note] -> Unlike other template types, you cannot create partial templates to target a particular page kind, content type, section, language, or output format. Partial templates do not follow Hugo's [template lookup order]. - -For example, the partial template below renders copyright information. - -```go-html-template {file="layouts/partials/footer.html"} -

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

    -``` - -Learn more about [partial templates](/templates/partial/). - -## Content view - -A content view template is similar to a partial template, invoked by calling the [`Render`] method on a `Page` object. Unlike partial templates, content view templates: - -- Automatically inherit the context of the current page -- Follow a lookup order allowing you to target a given content type or section - -For example, the home template below inherits the site's shell from the base template, and renders a card component for each page within the "articles" section of your site. - -```go-html-template {file="layouts/_default/home.html"} -{{ define "main" }} - {{ .Content }} -
      - {{ range where site.RegularPages "Section" "articles" }} - {{ .Render "card" }} - {{ end }} -
    -{{ end }} -``` - -```go-html-template {file="layouts/articles/card.html"} -
    -

    {{ .LinkTitle }}

    - {{ .Summary }} -
    -``` - -Learn more about [content view templates](/templates/content-view/). - -## Render hook - -A render hook template overrides the conversion of Markdown to HTML. - -For example, the render hook template below adds a `rel` attribute to external links. - -```go-html-template {file="layouts/_default/_markup/render-link.html"} -{{- $u := urls.Parse .Destination -}} - - {{- with .Text }}{{ . }}{{ end -}} - -{{- /* chomp trailing newline */ -}} -``` - -Learn more about [render hook templates](/render-hooks/). - -## Shortcode - -A shortcode template is used to render a component of your site. Unlike partial templates, shortcode templates are called from content pages. - -For example, the shortcode template below renders an audio element from a [global resource](g). - -```go-html-template {file="layouts/shortcodes/audio.html"} -{{ with resources.Get (.Get "src") }} - -{{ end }} -``` - -Then call the shortcode from within markup: - -```text {file="content/example.md"} -{{}} -``` - -Learn more about [shortcode templates](/templates/shortcode/). - -## Other - -Use other specialized templates to create: - -- [Sitemaps](/templates/sitemap) -- [RSS feeds](/templates/rss/) -- [404 error pages](/templates/404/) -- [robots.txt files](/templates/robots/) - -[`Render`]: /methods/page/render/ -[block]: /functions/go-template/block/ -[partial]: /functions/partials/include/ -[template lookup order]: /templates/lookup-order/ -[template lookup order]: /templates/lookup-order/ diff --git a/docs/content/en/tools/_index.md b/docs/content/en/tools/_index.md deleted file mode 100644 index 3acc287ae..000000000 --- a/docs/content/en/tools/_index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Developer tools -description: Third-party tools to help you create and manage sites. -categories: [] -keywords: [] -weight: 10 ---- diff --git a/docs/content/en/tools/editors.md b/docs/content/en/tools/editors.md deleted file mode 100644 index c375fcba8..000000000 --- a/docs/content/en/tools/editors.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -title: Editor plugins -description: The Hugo community uses a wide range of tools and has developed plugins for some of the most popular text editors to help automate parts of your workflow. -categories: [] -keywords: [] -weight: 10 ---- - -## Visual Studio Code - -[Front Matter](https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-front-matter) -: Once you go for a static site, you need to think about how you are going to manage your articles. Front matter is a tool that helps you maintain the metadata/front matter of your articles like: creation date, modified date, slug, tile, SEO check, and more. - -[Hugo Helper](https://marketplace.visualstudio.com/items?itemName=rusnasonov.vscode-hugo) -: Hugo Helper is a plugin for Visual Studio Code that has some useful commands for Hugo. The source code can be found [here](https://github.com/rusnasonov/vscode-hugo). - -[Hugo Language and Syntax Support](https://marketplace.visualstudio.com/items?itemName=budparr.language-hugo-vscode) -: Hugo Language and Syntax Support is a Visual Studio Code plugin for Hugo syntax highlighting and snippets. The source code can be found [here](https://github.com/budparr/language-hugo-vscode). - -[Hugo Themer](https://marketplace.visualstudio.com/items?itemName=eliostruyf.vscode-hugo-themer) -: Hugo Themer is an extension to help you while developing themes. It allows you to easily navigate through your theme files. - -[Hugofy](https://marketplace.visualstudio.com/items?itemName=akmittal.hugofy) -: Hugofy is a plugin for Visual Studio Code to "make life easier" when developing with Hugo. The source code can be found [here](https://github.com/akmittal/hugofy-vscode). - -[Prettier Plugin for Go Templates](https://github.com/NiklasPor/prettier-plugin-go-template) -: Format Hugo templates using this [Prettier](https://prettier.io/) plugin. See [installation instructions](https://discourse.gohugo.io/t/38403). - -[Syntax Highlighting for Hugo Shortcodes](https://marketplace.visualstudio.com/items?itemName=kaellarkin.hugo-shortcode-syntax) -: This extension adds some syntax highlighting for Shortcodes, making visual identification of individual pieces easier. - -## Emacs - -[emacs-easy-hugo](https://github.com/masasam/emacs-easy-hugo) -: Emacs major mode for managing hugo blogs. Note that Hugo also supports [Org-mode][formats]. - -[ox-hugo.el](https://ox-hugo.scripter.co) -: Native Org-mode exporter that exports to Blackfriday Markdown with Hugo front-matter. `ox-hugo` supports two common Org blogging flows --- exporting multiple Org subtrees in a single file to multiple Hugo posts, and exporting a single Org file to a single Hugo post. It also leverages the Org tag and property inheritance features. See [*Why ox-hugo?*](https://ox-hugo.scripter.co/doc/why-ox-hugo/) for more. - -## Sublime Text - -[Hugofy](https://github.com/akmittal/Hugofy) -: Hugofy is a plugin for Sublime Text 3 to make life easier to use Hugo static site generator. - -[Hugo Snippets](https://packagecontrol.io/packages/Hugo%20Snippets) -: Hugo Snippets is a useful plugin for adding automatic snippets to Sublime Text 3. - -## Vim - -[Vim Hugo Helper]: https://github.com/robertbasic/vim-hugo-helper - -[Vim Hugo Helper] -: A small Vim plugin that facilitates authoring pages and blog posts with Hugo. - -[vim-hugo](https://github.com/phelipetls/vim-hugo) -: A Vim plugin with syntax highlighting for templates and a few other features. - -[formats]: /content-management/formats/ diff --git a/docs/content/en/tools/front-ends.md b/docs/content/en/tools/front-ends.md deleted file mode 100644 index 0c52a4687..000000000 --- a/docs/content/en/tools/front-ends.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: Front-end interfaces -linkTitle: Front-ends -description: Do you prefer a graphical user interface over a text editor? Give these front-ends a try. -categories: [] -keywords: [] -weight: 20 -aliases: [/tools/frontends/] ---- - -## Commercial - -[CloudCannon](https://cloudcannon.com/hugo-cms/) -: The intuitive Git-based CMS for your Hugo website. CloudCannon syncs changes from your Git repository and pushes content changes back, so your development and content teams are always in sync. Edit all of your content on the page with visual editing, build entire pages with reusable custom components and then publish confidently. - -[DatoCMS](https://www.datocms.com) -: DatoCMS is a fully customizable administrative area for your static websites. Use your favorite website generator, let your clients publish new content independently, and the host the site anywhere you like. - -[PubCrank](https://www.pubcrank.com/) -: PubCrank is a static site editor which lets you define templates for different front matter layouts in your site. This gives writers an easy-to-use visual interface to create and edit content while maintaining the guardrails that the developer has created. PubCrank is free for local editing. - -## Open-source - -[Decap CMS](https://decapcms.org/) -: Decap CMS is an open-source, serverless solution for managing Git based content in static sites, and it works on any platform that can host static sites. A [Hugo/Decap CMS starter](https://github.com/decaporg/one-click-hugo-cms) is available to get new projects running quickly. - -[Quiqr Desktop](https://quiqr.org/) -: Quiqr Desktop is a open-source, cross-platform, offline desktop CMS for Hugo with built-in Git functionality for deploying static sites to any hosting server. - -[Sveltia CMS](https://github.com/sveltia/sveltia-cms/) -: Sveltia CMS is a drop-in replacement for Decap CMS which is built from the ground up with powerful and performant modern UI library Svelte. Sveltia CMS incorporates i18n into every corner of the product, while striving to radically improve UX, performance and productivity. diff --git a/docs/content/en/tools/migrations.md b/docs/content/en/tools/migrations.md deleted file mode 100644 index 103e28b9e..000000000 --- a/docs/content/en/tools/migrations.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -title: Migrate to Hugo -linkTitle: Migrations -description: A list of community-developed tools for migrating from your existing static site generator or content management system to Hugo. -categories: [] -keywords: [] -weight: 40 -aliases: [/developer-tools/migrations/, /developer-tools/migrated/] ---- - -This section highlights some independently developed projects related to Hugo. These tools extend functionality or help you to get started. - -Take a look at this list of migration tools if you currently use other blogging tools like Jekyll or WordPress but intend to switch to Hugo instead. They'll help you export your content into Hugo-friendly formats. - -## Jekyll - -Alternatively, you can use the [Jekyll import command](/commands/hugo_import_jekyll/). - -[JekyllToHugo](https://github.com/fredrikloch/JekyllToHugo) -: A Small script for converting Jekyll blog posts to a Hugo site. - -[ConvertToHugo](https://github.com/coderzh/ConvertToHugo) -: Convert your blog from Jekyll to Hugo. - -## Octopress - -[octohug](https://github.com/codebrane/octohug) -: Octopress to Hugo migrator. - -## DokuWiki - -[dokuwiki-to-hugo](https://github.com/wgroeneveld/dokuwiki-to-hugo) -: Migrates your DokuWiki source pages from [DokuWiki syntax](https://www.dokuwiki.org/wiki:syntax) to Hugo Markdown syntax. Includes extras like the TODO plugin. Written with extensibility in mind using Python 3. Also generates a TOML header for each page. Designed to copy-paste the wiki directory into your `content` directory. - -## WordPress - -[wordpress-to-hugo-exporter](https://github.com/SchumacherFM/wordpress-to-hugo-exporter) -: A one-click WordPress plugin that converts all posts, pages, taxonomies, metadata, and settings to Markdown and YAML which can be dropped into Hugo. (Note: If you have trouble using this plugin, you can [export your site for Jekyll](https://wordpress.org/plugins/jekyll-exporter/) and use Hugo's built-in Jekyll converter listed above.) - -[blog2md](https://github.com/palaniraja/blog2md) -: Works with [exported xml](https://en.support.wordpress.com/export/) file of your free YOUR-TLD.wordpress.com website. It also saves approved comments to `YOUR-POST-NAME-comments.md` file along with posts. - -[wordhugopress](https://github.com/nantipov/wordhugopress) -: A small utility written in Java that exports the entire WordPress site from the database and resource (e.g., images) files stored locally or remotely. Therefore, migration from the backup files is possible. Supports merging multiple WordPress sites into a single Hugo site. - -[wp2hugo](https://github.com/ashishb/wp2hugo) -: A Go-based CLI tool to migrate WordPress website to Hugo while preserving original URLs, GUIDs (for feeds), image URLs, code highlights, table of contents, YouTube embeds, Google Maps embeds, and original WordPress navigation categories. - -## Medium - -[medium2md](https://github.com/gautamdhameja/medium-2-md) -: A simple Medium to Hugo exporter able to import stories in one command, including front matter. - -[medium-to-hugo](https://github.com/bgadrian/medium-to-hugo) -: A CLI tool written in Go to export medium posts into a Hugo-compatible Markdown format. Tags and images are included. All images will be downloaded locally and linked appropriately. - -## Tumblr - -[tumblr-importr](https://github.com/carlmjohnson/tumblr-importr) -: An importer that uses the Tumblr API to create a Hugo static site. - -[tumblr2hugomarkdown](https://github.com/Wysie/tumblr2hugomarkdown) -: Export all your Tumblr content to Hugo Markdown files with preserved original formatting. - -[Tumblr to Hugo](https://github.com/jipiboily/tumblr-to-hugo) -: A migration tool that converts each of your Tumblr posts to a content file with a proper title and path. It also generates a CSV file to help you set up URL redirects. - -## Drupal - -[drupal2hugo](https://github.com/danapsimer/drupal2hugo) -: Convert a Drupal site to Hugo. - -## Joomla - -[hugojoomla](https://github.com/davetcc/hugojoomla) -: This utility written in Java takes a Joomla database and converts all the content into Markdown files. It changes any URLs that are in Joomla's internal format and converts them to a suitable form. - -## Blogger - -[blogimport](https://github.com/natefinch/blogimport) -: A tool to import from Blogger posts to Hugo. - -[blogger-to-hugo](https://pypi.org/project/blogger-to-hugo/) -: Another tool to import Blogger posts to Hugo. It also downloads embedded images so they will be stored locally. - -[blog2md](https://github.com/palaniraja/blog2md) -: Works with [exported xml](https://support.google.com/blogger/answer/41387?hl=en) file of your YOUR-TLD.blogspot.com website. It also saves comments to `YOUR-POST-NAME-comments.md` file along with posts. - -[BloggerToHugo](https://github.com/huanlin/blogger-to-hugo) -: Yet another tool to import Blogger posts to Hugo. For Windows platform only, and .NET Framework 4.5 is required. See README.md before using this tool. - -## Contentful - -[contentful-hugo](https://github.com/ModiiMedia/contentful-hugo) -: A tool to create content-files for Hugo from content on [Contentful](https://www.contentful.com/). - -## BlogML - -[BlogML2Hugo](https://github.com/jijiechen/BlogML2Hugo) -: A tool that helps you convert BlogML xml file to Hugo Markdown files. Users need to take care of links to attachments and images by themselves. This helps the blogs that export BlogML files (e.g. BlogEngine.NET) transform to hugo sites easily. diff --git a/docs/content/en/tools/other.md b/docs/content/en/tools/other.md deleted file mode 100644 index 489d78506..000000000 --- a/docs/content/en/tools/other.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -title: Other community projects -linkTitle: Other projects -description: Some interesting projects developed by the Hugo community that don't quite fit into our other developer tool categories. -categories: [] -keywords: [] -weight: 50 ---- - -And for all the other community projects around Hugo: - -- [diego](https://github.com/ttybitnik/diego) - A CLI tool that integrates with Hugo to assist in importing and utilizing exported social media data from popular services on Hugo websites. -- [Emacs Easy Hugo](https://github.com/masasam/emacs-easy-hugo) - Emacs package for writing blog posts in Markdown or org-mode and building your site with Hugo. -- [Hugo SFTP Upload](https://github.com/thomasmey/HugoSftpUpload) - Sync the local build of your Hugo website with your remote web server via SFTP. -- [HugoPhotoSwipe](https://github.com/GjjvdBurg/HugoPhotoSwipe) - Make it easy to create image galleries using PhotoSwipe. -- [JAMStack Themes](https://jamstackthemes.dev/ssg/hugo/) - A collection of site themes filterable by static site generator and supported CMS to help build CMS-connected sites using Hugo (linking to Hugo-specific themes). -- [flickr-hugo-embed](https://github.com/nikhilm/flickr-hugo-embed) - Print shortcodes to embed a set of images from an album on Flickr into Hugo. -- [hugo-gallery](https://github.com/icecreammatt/hugo-gallery) - Create an image gallery for Hugo sites. -- [hugo-openapispec-shortcode](https://github.com/tenfourty/hugo-openapispec-shortcode) - A shortcode that allows you to include [Open API Spec](https://openapis.org) (formerly known as Swagger Spec) in a page. -- [plausible-hugo](https://github.com/divinerites/plausible-hugo) - Easy Hugo integration for Plausible Analytics, a simple, open-source, lightweight and privacy-friendly web analytics alternative to Google Analytics. diff --git a/docs/content/en/tools/search.md b/docs/content/en/tools/search.md deleted file mode 100644 index 2c392c75a..000000000 --- a/docs/content/en/tools/search.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: Search tools -linkTitle: Search -description: See some of the open-source and commercial search options for your newly created Hugo website. -categories: [] -keywords: [] -weight: 30 ---- - -A static website with a dynamic search function? Yes, Hugo provides an alternative to embeddable scripts from Google or other search engines for static websites. Hugo allows you to provide your visitors with a custom search function by indexing your content files directly. - -## Open-source - -[Pagefind](https://github.com/cloudcannon/pagefind) -: A fully static search library that aims to perform well on large sites, while using as little of your users' bandwidth as possible. - -[GitHub Gist for Hugo Workflow](https://gist.github.com/sebz/efddfc8fdcb6b480f567) -: This gist contains a simple workflow to create a search index for your static website. It uses a simple Grunt script to index all your content files and [lunr.js](https://lunrjs.com/) to serve the search results. - -[hugo-lunr](https://www.npmjs.com/package/hugo-lunr) -: A simple way to add site search to your static Hugo site using [lunr.js](https://lunrjs.com/). Hugo-lunr will create an index file of any HTML and Markdown documents in your Hugo project. - -[hugo-lunr-zh](https://www.npmjs.com/package/hugo-lunr-zh) -: A bit like Hugo-lunr, but Hugo-lunr-zh can help you separate the Chinese keywords. - -[GitHub Gist for Fuse.js integration](https://gist.github.com/eddiewebb/735feb48f50f0ddd65ae5606a1cb41ae) -: This gist demonstrates how to leverage Hugo's existing build time processing to generate a searchable JSON index used by [Fuse.js](https://fusejs.io/) on the client side. Although this gist uses Fuse.js for fuzzy matching, any client-side search tool capable of reading JSON indexes will work. Does not require npm, grunt, or other build-time tools except Hugo! - -[hugo-search-index](https://www.npmjs.com/package/hugo-search-index) -: A library containing Gulp tasks and a prebuilt browser script that implements search. Gulp generates a search index from project Markdown files. - -[hugofastsearch](https://gist.github.com/cmod/5410eae147e4318164258742dd053993) -: A usability and speed update to "GitHub Gist for Fuse.js integration" — global, keyboard-optimized search. - -[JS & Fuse.js tutorial](https://makewithhugo.com/add-search-to-a-hugo-site/) -: A simple client-side search solution, using FuseJS (does not require jQuery). - -[Hugo Lyra](https://github.com/paolomainardi/hugo-lyra) -: Hugo-Lyra is a JavaScript module to integrate [Lyra](https://github.com/LyraSearch/lyra) into a Hugo website. It contains the server-side part to generate the index and the client-side library (optional) to bootstrap the search engine easily. - -[INFINI Pizza for WebAssembly](https://github.com/infinilabs/pizza-docsearch) -: Pizza is a super-lightweight yet fully featured search engine written in Rust. You can quickly add offline search functionality to your Hugo website in just five minutes with only three lines of code. For a step-by-step guide on integrating it with Hugo, check out [this blog tutorial](https://dev.to/medcl/adding-search-functionality-to-a-hugo-static-site-based-on-infini-pizza-for-webassembly-4h5e). - -## Commercial - -[Algolia DocSearch](https://docsearch.algolia.com/) -: Algolia DocSearch is free for public technical documentation sites and easy to set up. For other use cases, [Algolia's Search API](https://www.algolia.com) makes it easy to deliver a great search experience in your apps and websites. Algolia Search provides hosted full-text, numerical, faceted, and geolocalized search. - -[Bonsai](https://www.bonsai.io) -: Bonsai is a fully-managed hosted Elasticsearch service that is fast, reliable, and simple to set up. Easily ingest your docs from Hugo into Elasticsearch following [this guide from the docs](https://bonsai.io/docs/hugo). - -[ExpertRec](https://www.expertrec.com/) -: ExpertRec is a hosted search-as-a-service solution that is fast and scalable. Set-up and integration is extremely easy and takes only a few minutes. The search settings can be modified without coding using a dashboard. diff --git a/docs/content/en/troubleshooting/_index.md b/docs/content/en/troubleshooting/_index.md deleted file mode 100644 index fd51b4fc8..000000000 --- a/docs/content/en/troubleshooting/_index.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: Troubleshooting -description: Use these techniques when troubleshooting your site. -categories: [] -keywords: [] -weight: 10 -aliases: [/templates/template-debugging/] ---- diff --git a/docs/content/en/troubleshooting/audit/index.md b/docs/content/en/troubleshooting/audit/index.md deleted file mode 100644 index 2efad55e3..000000000 --- a/docs/content/en/troubleshooting/audit/index.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: Site audit -linkTitle: Audit -description: Run this audit before deploying your production site. -categories: [] -keywords: [] ---- - -There are several conditions that can produce errors in your published site which are not detected during the build. Run this audit before your final build. - -```text {copy=true} -HUGO_MINIFY_TDEWOLFF_HTML_KEEPCOMMENTS=true HUGO_ENABLEMISSINGTRANSLATIONPLACEHOLDERS=true hugo && grep -inorE "<\!-- raw HTML omitted -->|ZgotmplZ|\[i18n\]|\(\)|(<nil>)|hahahugo" public/ -``` - -_Tested with GNU Bash 5.1 and GNU grep 3.7._ - -## Example output - -![site audit terminal output](screen-capture.png) - -## Explanation - -### Environment variables - -`HUGO_MINIFY_TDEWOLFF_HTML_KEEPCOMMENTS=true` -: Retain HTML comments even if minification is enabled. This takes precedence over `minify.tdewolff.html.keepComments` in the site configuration. If you minify without keeping HTML comments when performing this audit, you will not be able to detect when raw HTML has been omitted. - -`HUGO_ENABLEMISSINGTRANSLATIONPLACEHOLDERS=true` -: Show a placeholder instead of the default value or an empty string if a translation is missing. This takes precedence over `enableMissingTranslationPlaceholders` in the site configuration. - -### Grep options - -`-i, --ignore-case` -: Ignore case distinctions in patterns and input data, so that characters that differ only in case match each other. - -`-n, --line-number` -: Prefix each line of output with the 1-based line number within its input file. - -`-o, --only-matching` -: Print only the matched (non-empty) parts of a matching line, with each such part on a separate output line. - -`-r, --recursive` -: Read all files under each directory, recursively, following symbolic links only if they are on the command line. - -`-E, --extended-regexp` -: Interpret PATTERNS as extended regular expressions. - -### Patterns - -`` -: By default, Hugo strips raw HTML from your Markdown prior to rendering, and leaves this HTML comment in its place. - -`ZgotmplZ` -: ZgotmplZ is a special value that indicates that unsafe content reached a CSS or URL context at runtime. See [details]. - -[details]: https://pkg.go.dev/html/template - -`[i18n]` -: This is the placeholder produced instead of the default value or an empty string if a translation is missing. - -`()` -: This string will appear in the rendered HTML when passing a nil value to the `printf` function. - -`(<nil>)` -: Same as above when the value returned from the `printf` function has not been passed through `safeHTML`. - -`HAHAHUGO` -: Under certain conditions a rendered shortcode may include all or a portion of the string HAHAHUGOSHORTCODE in either uppercase or lowercase. This is difficult to detect in all circumstances, but a case-insensitive search of the output for `HAHAHUGO` is likely to catch the majority of cases without producing false positives. diff --git a/docs/content/en/troubleshooting/audit/screen-capture.png b/docs/content/en/troubleshooting/audit/screen-capture.png deleted file mode 100644 index 221abfff0..000000000 Binary files a/docs/content/en/troubleshooting/audit/screen-capture.png and /dev/null differ diff --git a/docs/content/en/troubleshooting/deprecation.md b/docs/content/en/troubleshooting/deprecation.md deleted file mode 100644 index f2e5259a6..000000000 --- a/docs/content/en/troubleshooting/deprecation.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -title: Deprecation -description: The Hugo project follows a formal and consistent process to deprecate functions, methods, and configuration settings. -categories: [] -keywords: [] ---- - -When a project _deprecates_ something, they are telling its users: - -1. Don't use Thing One anymore. -1. Use Thing Two instead. -1. We're going to remove Thing One at some point in the future. - -[reasons for deprecation]: https://en.wikipedia.org/wiki/Deprecation - -Common [reasons for deprecation]: - -- A feature has been replaced by a more powerful alternative. -- A feature contains a design flaw. -- A feature is considered extraneous, and will be removed in the future in order to simplify the system as a whole. -- A future version of the software will make major structural changes, making it impossible or impractical to support older features. -- Standardization or increased consistency in naming. -- A feature that once was available only independently is now combined with its co-feature. - -After the project team deprecates something in code, Hugo will: - -1. Log an INFO message for 3 minor releases[^1] -1. Log a WARN message for another 12 minor releases -1. Log an ERROR message and fail the build thereafter - -The project team will: - -1. On the deprecation date, update the documentation with a note describing the deprecation and any relevant alternatives. -1. Remove the code six or more minor releases after Hugo begins logging ERROR messages and failing the build. At that point, Hugo will throw an error, but the error message will no longer mention the deprecation. -1. Remove the corresponding documentation two years after the deprecation date. - -To see the INFO messages, you must use the `--logLevel` command line flag: - -```text -hugo --logLevel info -``` - -To limit the output to deprecation notices: - -```text -hugo --logLevel info | grep deprecate -``` - -Run the above command every time you upgrade Hugo. - -[^1]: For example, v0.1.1 => v0.2.0 is a minor release. diff --git a/docs/content/en/troubleshooting/faq.md b/docs/content/en/troubleshooting/faq.md deleted file mode 100644 index 6992af5d3..000000000 --- a/docs/content/en/troubleshooting/faq.md +++ /dev/null @@ -1,113 +0,0 @@ ---- -title: Frequently asked questions -linkTitle: FAQs -description: These questions are frequently asked by new users. -categories: [] -keywords: [] ---- - -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. - -These are just a few of the questions most frequently asked by new users. - -An error message indicates that a feature is not available. Why? -: - {{% include "/_common/installation/01-editions.md" %}} - - When you attempt to use a feature that is not available in the edition that you installed, Hugo throws this error: - - ```go-html-template - this feature is not available in this edition of Hugo - ``` - - To resolve, install a different edition based on the feature table above. See the [installation] section for details. - -Why do I see "Page Not Found" when visiting the home page? -: In the `content/_index.md` file: - - - Is `draft` set to `true`? - - Is the `date` in the future? - - Is the `publishDate` in the future? - - Is the `expiryDate` in the past? - - If the answer to any of these questions is yes, either change the field values, or use one of these command line flags: `--buildDrafts`, `--buildFuture`, or `--buildExpired`. - -Why is a given page not published? -: In the `content/section/page.md` file, or in the `content/section/page/index.md` file: - - - Is `draft` set to `true`? - - Is the `date` in the future? - - Is the `publishDate` in the future? - - Is the `expiryDate` in the past? - - If the answer to any of these questions is yes, either change the field values, or use one of these command line flags: `--buildDrafts`, `--buildFuture`, or `--buildExpired`. - -Why can't I see any of a page's descendants? -: You may have an `index.md` file instead of an `_index.md` file. See [details](/content-management/page-bundles/). - -What is the difference between an `index.md` file and an `_index.md` file? -: A directory with an `index.md file` is a [leaf bundle](g). A directory with an `_index.md` file is a [branch bundle](g). See [details](/content-management/page-bundles/). - -Why is my partial template not rendered as expected? -: You may have neglected to pass the required [context](g) when calling the partial. For example: - - ```go-html-template - {{/* incorrect */}} - {{ partial "_internal/pagination.html" }} - - {{/* correct */}} - {{ partial "_internal/pagination.html" . }} - ``` - -In a template, what's the difference between `:=` and `=` when assigning values to variables? -: Use `:=` to initialize a variable, and use `=` to assign a value to a variable that has been previously initialized. See [details](https://pkg.go.dev/text/template#hdr-Variables). - -When I paginate a list page, why is the page collection not filtered as specified? -: You are probably invoking the [`Paginate`] or [`Paginator`] method more than once on the same page. See [details](/templates/pagination/). - -Why are there two ways to call a shortcode? -: Use the `{{%/* shortcode */%}}` notation if the shortcode template, or the content between the opening and closing shortcode tags, contains Markdown. Otherwise use the\ -`{{}}` notation. See [details](/content-management/shortcodes/#notation). - -Can I use environment variables to control configuration? -: Yes. See [details](/configuration/introduction/#environment-variables). - -Why am I seeing inconsistent output from one build to the next? -: The most common causes are page collisions (publishing two pages to the same path) and the effects of concurrency. Use the `--printPathWarnings` command line flag to check for page collisions, and create a topic on the [forum] if you suspect concurrency problems. - -Why isn't Hugo's development server detecting file changes? -: In its default configuration, Hugo's file watcher may not be able detect file changes when: - - - Running Hugo within Windows Subsystem for Linux (WSL/WSL2) with project files on a Windows partition - - Running Hugo locally with project files on a removable drive - - Running Hugo locally with project files on a storage server accessed via the NFS, SMB, or CIFS protocols - - In these cases, instead of monitoring native file system events, use the `--poll` command line flag. For example, to poll the project files every 700 milliseconds, use `--poll 700ms`. - -Why is my page Scratch or Store missing a value? -: The [`Scratch`] and [`Store`] methods on a `Page` object allow you to create a [scratch pad](g) on the given page to store and manipulate data. Values are often set within a shortcode, a partial template called by a shortcode, or by a Markdown render hook. In all three cases, the scratch pad values are not determinate until Hugo renders the page content. - - If you need to access a scratch pad value from a parent template, and the parent template has not yet rendered the page content, you can trigger content rendering by assigning the returned value to a [noop](g) variable: - - ```go-html-template - {{ $noop := .Content }} - {{ .Store.Get "mykey" }} - ``` - - You can trigger content rendering with other methods as well. See next FAQ. - -Which page methods trigger content rendering? -: The following methods on a `Page` object trigger content rendering: `Content`, `ContentWithoutSummary`, `FuzzyWordCount`, `Len`, `Plain`, `PlainWords`, `ReadingTime`, `Summary`, `Truncated`, and `WordCount`. - -> [!note] -> For other questions please visit the [forum]. 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. - -[`Paginate`]: /methods/page/paginate/ -[`Paginator`]: /methods/page/paginator/ -[`Scratch`]: /methods/page/scratch -[`Store`]: /methods/page/store -[forum]: https://discourse.gohugo.io -[forum]: https://discourse.gohugo.io -[installation]: /installation/ -[requesting help]: https://discourse.gohugo.io/t/requesting-help/9132 -[requesting help]: https://discourse.gohugo.io/t/requesting-help/9132 diff --git a/docs/content/en/troubleshooting/inspection.md b/docs/content/en/troubleshooting/inspection.md deleted file mode 100644 index ea3c097f9..000000000 --- a/docs/content/en/troubleshooting/inspection.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -title: Data inspection -linkTitle: Inspection -description: Use template functions to inspect values and data structures. -categories: [] -keywords: [] ---- - -Use the [`debug.Dump`] function to inspect a data structure: - -```go-html-template -
    {{ debug.Dump .Params }}
    -``` - -```text -{ - "date": "2023-11-10T15:10:42-08:00", - "draft": false, - "iscjklanguage": false, - "lastmod": "2023-11-10T15:10:42-08:00", - "publishdate": "2023-11-10T15:10:42-08:00", - "tags": [ - "foo", - "bar" - ], - "title": "My first post" -} -``` - -Use the [`printf`] function (render) or [`warnf`] function (log to console) to inspect simple data structures. The layout string below displays both value and data type. - -```go-html-template -{{ $value := 42 }} -{{ printf "%[1]v (%[1]T)" $value }} → 42 (int) -``` - -{{< new-in 0.146.0 />}} - -Use the [`templates.Current`] function to visually mark template execution boundaries or to display the template call stack. - -[`debug.Dump`]: /functions/debug/dump/ -[`printf`]: /functions/fmt/printf/ -[`warnf`]: /functions/fmt/warnf/ -[`templates.Current`]: /functions/templates/current/ diff --git a/docs/content/en/troubleshooting/logging.md b/docs/content/en/troubleshooting/logging.md deleted file mode 100644 index 0cd25d1ae..000000000 --- a/docs/content/en/troubleshooting/logging.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: Logging -description: Enable logging to inspect events while building your site. -categories: [] -keywords: [] ---- - -## Command line - -Enable console logging with the `--logLevel` command line flag. - -Hugo has four logging levels: - -error -: Display error messages only. - - ```sh - hugo --logLevel error - ``` - -warn -: Display warning and error messages. - - ```sh - hugo --logLevel warn - ``` - -info -: Display information, warning, and error messages. - - ```sh - hugo --logLevel info - ``` - -debug -: Display debug, information, warning, and error messages. - - ```sh - hugo --logLevel debug - ``` - -> [!note] -> If you do not specify a logging level with the `--logLevel` flag, warnings and errors are always displayed. - -## Template functions - -You can also use template functions to print warnings or errors to the console. These functions are typically used to report data validation errors, missing files, etc. - -{{% list-pages-in-section path=/functions/fmt filter=functions_fmt_logging filterType=include %}} - -## LiveReload - -To log Hugo's LiveReload requests in your browser, add this query string to the URL when running Hugo's development server: - -```text -debug=LR-verbose -``` - -For example: - -```text -http://localhost:1313/?debug=LR-verbose -``` - -Then monitor the reload requests in your browser's dev tools console. Make sure the dev tools "preserve log" option is enabled. diff --git a/docs/content/en/troubleshooting/performance.md b/docs/content/en/troubleshooting/performance.md deleted file mode 100644 index e366eba81..000000000 --- a/docs/content/en/troubleshooting/performance.md +++ /dev/null @@ -1,104 +0,0 @@ ---- -title: Performance -description: Tools and suggestions for evaluating and improving performance. -categories: [] -keywords: [] -aliases: [/troubleshooting/build-performance/] ---- - -## Virus scanning - -Virus scanners are an essential component of system protection, but the performance impact can be severe for applications like Hugo that frequently read and write to disk. For example, with Microsoft Defender Antivirus, build times for some sites may increase by 400% or more. - -Before building a site, your virus scanner has already evaluated the files in your project directory. Scanning them again while building the site is superfluous. To improve performance, add Hugo's executable to your virus scanner's process exclusion list. - -For example, with Microsoft Defender Antivirus: - -**Start** > **Settings** > **Privacy & security** > **Windows Security** > **Open Windows Security** > **Virus & threat protection** > **Manage settings** > **Add or remove exclusions** > **Add an exclusion** > **Process** - -Then type `hugo.exe` add press the **Add** button. - -> [!note] -> Virus scanning exclusions are common, but use caution when changing these settings. See the [Microsoft Defender Antivirus documentation] for details. - -Other virus scanners have similar exclusion mechanisms. See their respective documentation. - -## Template metrics - -Hugo is fast, but inefficient templates impede performance. Enable template metrics to determine which templates take the most time, and to identify caching opportunities: - -```sh -hugo --templateMetrics --templateMetricsHints -``` - -The result will look something like this: - -```text -Template Metrics: - - cumulative average maximum cache percent cached total - duration duration duration potential cached count count template - ---------- -------- -------- --------- ------- ------ ----- -------- - 36.037476822s 135.990478ms 225.765245ms 11 0 0 265 partials/head.html - 35.920040902s 164.018451ms 233.475072ms 0 0 0 219 articles/single.html - 34.163268129s 128.917992ms 224.816751ms 23 0 0 265 partials/head/meta/opengraph.html - 1.041227437s 3.92916ms 186.303376ms 47 0 0 265 partials/head/meta/schema.html - 805.628827ms 27.780304ms 114.678523ms 0 0 0 29 _default/list.html - 624.08354ms 15.221549ms 108.420729ms 8 0 0 41 partials/utilities/render-page-collection.html - 545.968801ms 775.523µs 105.045775ms 0 0 0 704 _default/summary.html - 334.680981ms 1.262947ms 127.412027ms 100 0 0 265 partials/head/js.html - 272.763205ms 2.050851ms 24.371757ms 0 0 0 133 _default/_markup/render-codeblock.html - 230.490038ms 8.865001ms 177.4615ms 0 0 0 26 shortcodes/template.html - 176.921913ms 176.921913ms 176.921913ms 0 0 0 1 examples.tmpl - 163.951469ms 14.904679ms 70.267953ms 0 0 0 11 articles/list.html - 153.07021ms 577.623µs 73.593597ms 100 0 0 265 partials/head/init.html - 150.910984ms 150.910984ms 150.910984ms 0 0 0 1 _default/single.html - 146.785804ms 146.785804ms 146.785804ms 0 0 0 1 _default/contact.html - 115.364617ms 115.364617ms 115.364617ms 0 0 0 1 authors/term.html - 87.392071ms 329.781µs 10.687132ms 100 0 0 265 partials/head/css.html - 86.803122ms 86.803122ms 86.803122ms 0 0 0 1 _default/home.html -``` - -From left to right, the columns represent: - -cumulative duration -: The cumulative time spent executing the template. - -average duration -: The average time spent executing the template. - -maximum duration -: The maximum time spent executing the template. - -cache potential -: Displayed as a percentage, any partial template with a 100% cache potential should be called with the [`partialCached`] function instead of the [`partial`] function. See the [caching](#caching) section below. - -percent cached -: The number of times the rendered templated was cached divided by the number of times the template was executed. - -cached count -: The number of times the rendered templated was cached. - -total count -: The number of times the template was executed. - -template -: The path to the template, relative to the `layouts` directory. - -> [!note] -> Hugo builds pages in parallel where multiple pages are generated simultaneously. Because of this parallelism, the sum of "cumulative duration" values is usually greater than the actual time it takes to build a site. - -## Caching - -Some partial templates such as sidebars or menus are executed many times during a site build. Depending on the content within the partial template and the desired output, the template may benefit from caching to reduce the number of executions. The [`partialCached`] template function provides caching capabilities for partial templates. - -> [!note] -> Note that you can create cached variants of each partial by passing additional arguments to `partialCached` beyond the initial context. See the `partialCached` documentation for more details. - -## Timers - -Use the `debug.Timer` function to determine execution time for a block of code, useful for finding performance bottlenecks in templates. See [details](/functions/debug/timer/). - -[`partial`]: /functions/partials/include/ -[`partialCached`]: /functions/partials/includecached/ -[Microsoft Defender Antivirus documentation]: https://support.microsoft.com/en-us/topic/how-to-add-a-file-type-or-process-exclusion-to-windows-security-e524cbc2-3975-63c2-f9d1-7c2eb5331e53 diff --git a/docs/content/extras/aliases.md b/docs/content/extras/aliases.md new file mode 100644 index 000000000..00f014ca0 --- /dev/null +++ b/docs/content/extras/aliases.md @@ -0,0 +1,103 @@ +--- +aliases: +- /doc/redirects/ +- /doc/alias/ +- /doc/aliases/ +lastmod: 2015-12-23 +date: 2013-07-09 +menu: + main: + parent: extras +next: /extras/analytics +prev: /taxonomies/methods +title: Aliases +--- + +For people migrating existing published content to Hugo, there's a good chance you need a mechanism to handle redirecting old URLs. + +Luckily, redirects can be handled easily with _aliases_ in Hugo. + +## Example + +Given a post on your current Hugo site, with a path of: + +``content/posts/my-awesome-blog-post.md`` + +... you create an "aliases" section in the frontmatter of your post, and add previous paths to that. + +### TOML frontmatter + +```toml ++++ + ... +aliases = [ + "/posts/my-original-url/", + "/2010/01/01/even-earlier-url.html" +] + ... ++++ +``` + +### YAML frontmatter + +```yaml +--- + ... +aliases: + - /posts/my-original-url/ + - /2010/01/01/even-earlier-url.html + ... +--- +``` + +Now when you visit any of the locations specified in aliases, _assuming the same site domain_, you'll be redirected to the page they are specified on. + +## Important Behaviors + +1. *Hugo makes no assumptions about aliases. They also don't change based +on your UglyURLs setting. You need to provide absolute path to your webroot +and the complete filename or directory.* + +2. *Aliases are rendered prior to any content and will be overwritten by +any content with the same location.* + +## Multilingual example + +On [multilingual sites]({{< relref "content/multilingual.md" >}}), each translation of a post can have unique aliases. To use the same alias across multiple languages, prefix it with the language code. + +In `/posts/my-new-post.es.md`: + +```yaml +--- +aliases: + - /es/posts/my-original-post/ +--- +``` + +## How Hugo Aliases Work + +When aliases are specified, Hugo creates a physical folder structure to match the alias entry, and, an html file specifying the canonical URL for the page, and a redirect target. + +Assuming a baseURL of `mysite.tld`, the contents of the html file will look something like: + +```html + + + + http://mysite.tld/posts/my-original-url + + + + + +``` + +The `http-equiv="refresh"` line is what performs the redirect, in 0 seconds in this case. + +## Customizing + +You may customize this alias page by creating an alias.html template in the +layouts folder of your site. In this case, the data passed to the template is + +* Permalink - the link to the page being aliased +* Page - the Page data for the page being aliased diff --git a/docs/content/extras/analytics.md b/docs/content/extras/analytics.md new file mode 100644 index 000000000..31f44c7bf --- /dev/null +++ b/docs/content/extras/analytics.md @@ -0,0 +1,28 @@ +--- +date: 2016-02-06 +linktitle: Analytics +menu: + main: + parent: extras +next: /extras/builders +prev: /extras/aliases +title: Analytics in Hugo +--- + +Hugo ships with prebuilt internal templates for Google Analytics tracking, including both synchronous and asynchronous tracking codes. + +## Configuring Google Analytics + +Provide your tracking id in your configuration file, e.g. config.yaml. + + googleAnalytics = "UA-123-45" + +## Example + +Include the internal template in your templates like so: + + {{ template "_internal/google_analytics.html" . }} + +For async include the async template: + + {{ template "_internal/google_analytics_async.html" . }} diff --git a/docs/content/extras/builders.md b/docs/content/extras/builders.md new file mode 100644 index 000000000..707eff306 --- /dev/null +++ b/docs/content/extras/builders.md @@ -0,0 +1,56 @@ +--- +lastmod: 2015-12-24 +date: 2014-05-26 +linktitle: Builders +menu: + main: + parent: extras +next: /extras/comments +prev: /extras/analytics +title: Hugo Builders +--- + +Hugo provides the functionality to quickly get a site, theme or page +started. + + +## New Site + +Want to get a site built quickly? + +{{< nohighlight >}}$ hugo new site path/to/site +{{< /nohighlight >}} + +Hugo will create all the needed directories and files to get started +quickly. + +Hugo will only touch the files and create the directories (in the right +places), [configuration](/overview/configuration/) and content are up to +you... but luckily we have builders for content (see below). + +## New Theme + +Want to design a new theme? + + $ hugo new theme THEME_NAME + +Run from your working directory, this will create a new theme with all +the needed files in your themes directory. Hugo will provide you with a +license and theme.toml file with most of the work done for you. + +Follow the [Theme Creation Guide](/themes/creation/) once the builder is +done. + +## New Content + +You will use this builder the most of all. Every time you want to create +a new piece of content, the content builder will get you started right. + +Leveraging [content archetypes](/content/archetypes/) the content builder +will not only insert the current date and appropriate metadata, but it +will pre-populate values based on the content type. + + $ hugo new relative/path/to/content + +This assumes it is being run from your working directory and the content +path starts from your content directory. Now, Hugo watches your content directory by default and rebuilds your entire website if any change occurs. diff --git a/docs/content/extras/comments.md b/docs/content/extras/comments.md new file mode 100644 index 000000000..5ecb6ba20 --- /dev/null +++ b/docs/content/extras/comments.md @@ -0,0 +1,99 @@ +--- +lastmod: 2015-08-04 +date: 2014-05-26 +linktitle: Comments +menu: + main: + parent: extras +next: /extras/crossreferences +prev: /extras/builders +title: Comments in Hugo +--- + +As Hugo is a static site generator, the content produced is static and doesn’t interact with the users. The most common interaction people ask for is comment capability. + +Hugo ships with support for [Disqus](https://disqus.com/), a third-party service that provides comment and community capabilities to website via JavaScript. + +Your theme may already support Disqus, but even it if doesn’t, it is easy to add. + +# Disqus Support + +## Adding Disqus to a template + +Hugo comes with all the code you would need to include load Disqus. Simply include the following line where you want your comments to appear: + + {{ template "_internal/disqus.html" . }} + +## Configuring Disqus + +That template requires you to set a single value in your site config file, e.g. config.yaml. + + disqusShortname = "XYW" + +Additionally, you can optionally set the following in the front matter +for a given piece of content: + + * **disqus_identifier** + * **disqus_title** + * **disqus_url** + +## Conditional Loading of Disqus Comments + +Users have noticed that enabling Disqus comments when running the Hugo web server on localhost causes the creation of unwanted discussions on the associated Disqus account. In order to prevent this, a slightly tweaked partial template is required. So, rather than using the built-in `"_internal/disqus.html"` template referenced above, create a template in your `partials` folder that looks like this: + +```html +
    + + +comments powered by Disqus +``` + +Notice that there is a simple `if` statement that detects when you are running on localhost and skips the initialization of the Disqus comment injection. + +Now, reference the partial template from your page template: + + {{ partial "disqus.html" . }} + + +# Alternatives + +A few alternatives exist to [Disqus](https://disqus.com/): + +* [txtpen](https://txtpen.com) +* [Discourse](http://www.discourse.org) +* [IntenseDebate](http://intensedebate.com/) +* [Livefyre](http://www.adobe.com/marketing-cloud/enterprise-content-management/ugc-content-platform.html) +* [Muut](http://muut.com/) +* [多说](http://duoshuo.com/) ([Duoshuo](http://duoshuo.com/), popular in China) +* [isso](http://posativ.org/isso/) (Self-hosted, Python) +* [Kaiju](https://github.com/spf13/kaiju) + +## Kaiju + +[Kaiju](https://github.com/spf13/kaiju) is an open-source project started by [spf13](http://spf13.com/) (Hugo’s author) to bring easy and fast real time discussions to the web. + +Written using Go, Socket.io and MongoDB, it is very fast and easy to deploy. + +It is in early development but shows promise. If you have interest, please help by contributing whether via a pull request, an issue or even just a tweet. Everything helps. + +## txtpen + +[txtpen](https://txtpen.com) adds highlighting an in-line commenting similar to Medium to your Hugo blog. + +## Discourse + +Additionally, you may recognize [Discourse](http://www.discourse.org) as the system that powers the [Hugo Discussion Forum](https://discourse.gohugo.io). + diff --git a/docs/content/extras/crossreferences.md b/docs/content/extras/crossreferences.md new file mode 100644 index 000000000..9f3e7ef23 --- /dev/null +++ b/docs/content/extras/crossreferences.md @@ -0,0 +1,153 @@ +--- +lastmod: 2015-12-23 +date: 2014-11-25 +menu: + main: + parent: extras +next: /extras/robots-txt +prev: /extras/comments +title: Cross-References +toc: true +--- + +Hugo makes it easy to link documents together with the `ref` and `relref` shortcodes. These shortcodes are also used to safely provide links to headings inside of your content, whether across documents or within a document. The only difference between `ref` and `relref` is whether the resulting URL is absolute (`http://1.com/about/`) or relative (`/about/`). + +## Using `ref` and `relref` + +```django +{{}} +{{}} +{{}} +{{}} +{{}} +{{}} +``` + +The single parameter to `ref` is a string with a content _document name_ (`about.md`), an in-document _anchor_ (`#who`), or both (`about.md#who`). + +### Document Names + +The _document name_ is the name of a document including the format extension; this may be just the filename, or the relative path from the `content/` directory. With a document `content/blog/post.md`, either format will produce the same result. + + {{}} ⇒ `/blog/post/` + {{}} ⇒ `/blog/post/` + +If you have multiple sections with the same filename, you should only use the relative path format, because the behaviour is _undefined_. So, if I also have a document `link/post.md`, the output of `ref` is unknown for `post.md`. + + {{}} ⇒ `/blog/post/` + {{}} ⇒ `/blog/post/` (maybe) + {{}} ⇒ `/link/post/` (maybe) + {{}} ⇒ `/link/post/` + +A relative document name must *not* begin with a slash (`/`). + + {{}} ⇒ `""` + +### Anchors + +When an _anchor_ is provided by itself, the current page’s unique identifier will be appended; when an _anchor_ is provided with a document name, the found page's unique identifier will be appended. + + {{}} ⇒ `#who:9decaf7` + {{}} ⇒ `/blog/post/#who:badcafe` + +More information about document unique identifiers and headings can be found [below]({{< ref "#hugo-heading-anchors" >}}). + +### Examples + +* `{{}}` ⇒ `http://1.com/blog/post/` +* `{{}}` ⇒ `http://1.com/blog/post/#tldr:caffebad` +* `{{}}` ⇒ `/blog/post/` +* `{{}}` ⇒ `/blog/post/#tldr:caffebad` +* `{{}}` ⇒ `#tldr:badcaffe` +* `{{}}` ⇒ `#tldr:badcaffe` + +## Hugo Heading Anchors + +When using Markdown document types, Hugo generates heading anchors automatically. The generated anchor for this section is `hugo-heading-anchors`. Because the heading anchors are generated automatically, Hugo takes some effort to ensure that heading anchors are unique both inside a document and across the entire site. + +Ensuring heading uniqueness across the site is accomplished with a unique identifier for each document based on its path. Unless a document is renamed or moved between sections *in the filesystem*, the unique identifier for the document will not change: `blog/post.md` will always have a unique identifier of `81df004c333b392d34a49fd3a91ba720`. + +`ref` and `relref` were added so you can make these reference links without having to know the document’s unique identifier. (The links in document tables of contents are automatically up-to-date with this value.) + + {{}} + /extras/crossreferences/#hugo-heading-anchors:77cd9ea530577debf4ce0f28c8dca242 + +> What follows is a deeper discussion of *why* and *how* Hugo generates heading anchors. It is not necessary to know this to use `ref` and `relref`, but it may be useful in understanding how some anchors may not match your expectations. + +### How to Generate a Heading Anchor + +Convert the text of the heading to lowercase. + + Hugo: A Fast & Modern Static Web Engine + hugo: a fast & modern static web engine + +Replace anything that isn't an ASCII letter (`a-z`) or number (`0-9`) with a dash (`-`). + + hugo: a fast & modern static web engine + hugo--a-fast---modern-static-web-engine + +Get rid of extra dashes. + + hugo--a-fast---modern-static-web-engine + hugo-a-fast-modern-static-web-engine + +You have just converting the text of a heading to a suitable anchor. If your document has unique heading text, all of the anchors will be unique, too. + +#### Specifying Heading Anchors + +You can also tell Hugo to use a particular heading anchor. + + # Hugo: A Fast & Modern Static Web Engine {#hugo-main} + +Hugo will use `hugo-main` as the heading anchor. + +### What About Duplicate Heading Anchors? + +The technique outlined above works well enough, but some documents have headings with identical text, like the [shortcodes](/extras/shortcodes/) page—there are three headings with the text "Example". You can specify heading anchors manually: + + ### Example {#example-1} + ### Example {#example-2} + ### Example {#example-3} + +It’s easy to forget to do that all the time, and Hugo is smart enough to do it for you. It just adds `-x` to the end of each heading it has already seen. + +* `### Example` ⇒ `example` +* `### Example` ⇒ `example-1` +* `### Example` ⇒ `example-2` + +Sometimes it's a little harder, but Hugo can recover from those, too, by adding more suffixes: + +* `# Heading` ⇒ `heading` +* `# Heading 1` ⇒ `heading-1` +* `# Heading` ⇒ `heading-1-1` +* `# Heading` ⇒ `heading-1-2` +* `# Heading 1` ⇒ `heading-2` + +This can even affect specified heading anchors that come after a generated heading anchor. + +* `# My Heading` ⇒ `my-heading` +* `# My Heading {#my-heading}` ⇒ `my-heading-1` + +> This particular collision and override is unfortunate, but unavoidable because Hugo processes each heading for collision detection as it sees it during conversion. + +This technique works well for documents rendered on individual pages, like blog posts. What about on Hugo list pages? + +### Unique Heading Anchors in Lists + +Hugo converts each document from Markdown independently. it doesn’t know that `blog/post.md` has an "Example" heading that will collide with the "Example" heading in `blog/post2.md`. Even if it did know this, the addition of `blog/post3.md` should not cause the anchors for the headings in the other blog posts to change. + +Enter the document’s unique identifier. To prevent this sort of collision on +list pages, Hugo always appends the document's to a generated heading anchor. +So, the "Example" heading in `blog/post.md` actually turns into +`#example:81df004…`, and the "Example" heading in `blog/post2.md` actually +turns into `#example:8cf1599…`. All you have to know is the heading anchor that +was generated, not the document identifier; `ref` and `relref` take care of the +rest for you. + + Post Example + Post Example + + [Post Two Example]({{}}) + Post Two Example + +Now you know. diff --git a/docs/content/extras/datadrivencontent.md b/docs/content/extras/datadrivencontent.md new file mode 100644 index 000000000..e5c6c9130 --- /dev/null +++ b/docs/content/extras/datadrivencontent.md @@ -0,0 +1,142 @@ +--- +aliases: +- /doc/datadrivencontent/ +lastmod: 2016-03-03 +date: 2015-02-14 +menu: + main: + parent: extras +next: /extras/gitinfo +prev: /extras/datafiles +title: Data-driven Content +toc: true +--- + +Data-driven content with a static site generator? Yes, it is possible! + +In addition to the [data files](/extras/datafiles/) feature, we have also +implemented the feature "Data-driven Content", which lets you load +any [JSON](http://www.json.org/) or +[CSV](http://en.wikipedia.org/wiki/Comma-separated_values) file +from nearly any resource. + +"Data-driven Content" currently consists of two functions, `getJSON` +and `getCSV`, which are available in **all template files**. + +## Implementation details + +### Calling the functions with an URL + +In any HTML template or Markdown document, call the functions like this: + + {{ $dataJ := getJSON "url" }} + {{ $dataC := getCSV "separator" "url" }} + +or, if you use a prefix or postfix for the URL, the functions +accept [variadic arguments](http://en.wikipedia.org/wiki/Variadic_function): + + {{ $dataJ := getJSON "url prefix" "arg1" "arg2" "arg n" }} + {{ $dataC := getCSV "separator" "url prefix" "arg1" "arg2" "arg n" }} + +The separator for `getCSV` must be put in the first position and can only +be one character long. + +All passed arguments will be joined to the final URL; for example: + + {{ $urlPre := "https://api.github.com" }} + {{ $gistJ := getJSON $urlPre "/users/GITHUB_USERNAME/gists" }} + +will resolve internally to: + + {{ $gistJ := getJSON "https://api.github.com/users/GITHUB_USERNAME/gists" }} + +Finally, you can range over an array. This example will output the +first 5 gists for a GitHub user: + +
      + {{ $urlPre := "https://api.github.com" }} + {{ $gistJ := getJSON $urlPre "/users/GITHUB_USERNAME/gists" }} + {{ range first 5 $gistJ }} + {{ if .public }} +
    • {{ .description }}
    • + {{ end }} + {{ end }} +
    + + +### Example for CSV files + +For `getCSV`, the one-character long separator must be placed in the +first position followed by the URL. + + + + + + + + + + + {{ $url := "http://a-big-corp.com/finance/employee-salaries.csv" }} + {{ $sep := "," }} + {{ range $i, $r := getCSV $sep $url }} + + + + + + {{ end }} + +
    NamePositionSalary
    {{ index $r 0 }}{{ index $r 1 }}{{ index $r 2 }}
    + +The expression `{{index $r number}}` must be used to output the nth-column from +the current row. + +### Caching of URLs + +Each downloaded URL will be cached in the default folder `$TMPDIR/hugo_cache/`. +The variable `$TMPDIR` will be resolved to your system-dependent +temporary directory. + +With the command-line flag `--cacheDir`, you can specify any folder on +your system as a caching directory. + +You can also set `cacheDir` in the main configuration file. + +If you don't like caching at all, you can fully disable caching with the +command line flag `--ignoreCache`. + +### Authentication when using REST URLs + +Currently, you can only use those authentication methods that can +be put into an URL. [OAuth](http://en.wikipedia.org/wiki/OAuth) or +other authentication methods are not implemented. + +### Loading local files + +To load local files with the two functions `getJSON` and `getCSV`, the +source files must reside within Hugo's working directory. The file +extension does not matter but the content does. + +It applies the same output logic as in the topic: *Calling the functions with an URL*. + +## LiveReload + +There is no chance to trigger a [LiveReload](/extras/livereload/) when +the content of an URL changes. However, when a local JSON/CSV file changes, +then a LiveReload will be triggered of course. Symlinks are not supported. + +**URLs and LiveReload**: If you change any local file and the LiveReload +is triggered, Hugo will either read the URL content from the cache or, if +you have disabled the cache, Hugo will re-download the content. +This can create huge traffic and you may also reach API limits quickly. + +As downloading of content takes a while, Hugo stops processing +your Markdown files until the content has been downloaded. + +## Examples + +- Photo gallery JSON powered: [https://github.com/pcdummy/hugo-lightslider-example](https://github.com/pcdummy/hugo-lightslider-example) +- GitHub Starred Repositories [in a posts](https://github.com/SchumacherFM/blog-cs/blob/master/content%2Fposts%2Fgithub-starred.md) with the related [short code](https://github.com/SchumacherFM/blog-cs/blob/master/layouts%2Fshortcodes%2FghStarred.html). +- More? Please tell us! diff --git a/docs/content/extras/datafiles.md b/docs/content/extras/datafiles.md new file mode 100644 index 000000000..dc7e8059c --- /dev/null +++ b/docs/content/extras/datafiles.md @@ -0,0 +1,106 @@ +--- +aliases: +- /doc/datafiles/ +lastmod: 2015-08-04 +date: 2015-01-22 +menu: + main: + parent: extras +next: /extras/datadrivencontent +prev: /extras/robots-txt +title: Data Files +--- + +In addition to the [built-in variables](/templates/variables/) available from Hugo, you can specify your own custom data that can be accessed via templates or shortcodes. + +Hugo supports loading data from [YAML](http://yaml.org/), [JSON](http://www.json.org/), and [TOML](https://github.com/toml-lang/toml) files located in the `data` directory. + +**It even works with [LiveReload](/extras/livereload/).** + +Data Files can also be used in [themes](/themes/overview/), but note: If the same `key` is used in both the main data folder and in the theme's data folder, the main one will win. So, for theme authors, for theme specific data items that shouldn't be overridden, it can be wise to prefix the folder structure with a namespace, e.g. `mytheme/data/mytheme/somekey/...`. To check if any such duplicate exists, run hugo with the `-v` flag, e.g. `hugo -v`. + +## The Data Folder + +The `data` folder is where you can store additional data for Hugo to use when generating your site. Data files aren't used to generate standalone pages - rather they're meant to supplement the content files. This feature can extend the content in case your frontmatter would grow immensely. Or perhaps you want to show a larger dataset in a template (see example below). In both cases it's a good idea to outsource the data in their own file. + +These files must be YAML, JSON or TOML files (using either the `.yml`, `.yaml`, `.json` or `toml` extension) and the data will be accessible as a `map` in `.Site.Data`. + +**The keys in this map will be a dot chained set of _path_, _filename_ and _key_ in file (if applicable).** + +This is best explained with an example: + +## Example: Jaco Pastorius' Solo Discography + +[Jaco Pastorius](http://en.wikipedia.org/wiki/Jaco_Pastorius_discography) was a great bass player, but his solo discography is short enough to use as an example. [John Patitucci](http://en.wikipedia.org/wiki/John_Patitucci) is another bass giant. + +The example below is a bit constructed, but it illustrates the flexibility of Data Files. It uses TOML as file format. + +Given the files: + +* `data/jazz/bass/jacopastorius.toml` +* `data/jazz/bass/johnpatitucci.toml` + +`jacopastorius.toml` contains the content below, `johnpatitucci.toml` contains a similar list: + +``` +discography = [ +"1974 – Modern American Music … Period! The Criteria Sessions", +"1974 – Jaco", +"1976 - Jaco Pastorius", +"1981 - Word of Mouth", +"1981 - The Birthday Concert (released in 1995)", +"1982 - Twins I & II (released in 1999)", +"1983 - Invitation", +"1986 - Broadway Blues (released in 1998)", +"1986 - Honestly Solo Live (released in 1990)", +"1986 - Live In Italy (released in 1991)", +"1986 - Heavy'n Jazz (released in 1992)", +"1991 - Live In New York City, Volumes 1-7.", +"1999 - Rare Collection (compilation)", +"2003 - Punk Jazz: The Jaco Pastorius Anthology (compilation)", +"2007 - The Essential Jaco Pastorius (compilation)" +] +``` + +The list of bass players can be accessed via `.Site.Data.jazz.bass`, a single bass player by adding the filename without the suffix, e.g. `.Site.Data.jazz.bass.jacopastorius`. + +You can now render the list of recordings for all the bass players in a template: + +``` +{{ range $.Site.Data.jazz.bass }} + {{ partial "artist.html" . }} +{{ end }} +``` + +And then in `partial/artist.html`: + +``` +
      +{{ range .discography }} +
    • {{ . }}
    • +{{ end }} +
    +``` + +Discover a new favourite bass player? Just add another TOML-file. + +## Example: Accessing named values in a Data File + +Assuming you have the following YAML structure to your `User0123.yml` Data File located directly in `data/` + +``` +Name: User0123 +"Short Description": "He is a **jolly good** fellow." +Achievements: + - "Can create a Key, Value list from Data File" + - "Learns Hugo" + - "Reads documentation" +``` + +To render the `Short Description` in your `layout` File following code is required. + +``` +
    Short Description of {{.Site.Data.User0123.Name}}:

    {{ index .Site.Data.User0123 "Short Description" | markdownify }}

    +``` + +Note the use of the `markdownify` template function. This will send the description through the Blackfriday Markdown rendering engine. diff --git a/docs/content/extras/gitinfo.md b/docs/content/extras/gitinfo.md new file mode 100644 index 000000000..d267e0e3f --- /dev/null +++ b/docs/content/extras/gitinfo.md @@ -0,0 +1,54 @@ +--- +aliases: +- /doc/gitinfo/ +lastmod: 2016-12-11 +date: 2016-12-11 +menu: + main: + parent: extras +next: /extras/livereload +prev: /extras/datadrivencontent +title: GitInfo +--- + +Hugo provides a way to integrate Git data into your site. + + +## Prerequisites + +1. The Hugo site must be in a Git-enabled directory. +1. The Git executable must be installed and in your system `PATH`. +1. Enable the GitInfo feature in Hugo by using `--enableGitInfo` on the command + line or by setting `enableGitInfo` to `true` in your site configuration. + +## The GitInfo Object + +The `GitInfo` object contains the following fields: + +AbbreviatedHash +: abbreviated commit hash, e.g. `866cbcc` + +AuthorName +: author name, respecting `.mailmap` + +AuthorEmail +: author email address, respecting `.mailmap` + +AuthorDate +: the author date + +Hash +: commit hash, e.g. `866cbccdab588b9908887ffd3b4f2667e94090c3` + +Subject +: commit message subject, e.g. `tpl: Add custom index function` + + +## Other Considerations + +The Git integrations should be fairly performant, but it does add some time to the build, which depends somewhat on the Git history size. + +The accuracy of data depends on the underlying local git respository. If the local repository is a *shallow clone*, then any file that hasn't been modified in the truncated history will default to data in the oldest commit. In particular, if the respository has been cloned using `--depth=1` then every file will the exact same `GitInfo` data -- that of the only commit in the repository. + +In particular, many CI/CD systems such as [travis-ci.org](https://travis-ci.org) default to a clone depth of 50 which is unlikely to be deep enough. You can explicitly add back the missing history using using `git fetch --unshallow` or [make the initial checkout deeper](https://docs.travis-ci.com/user/customizing-the-build#Git-Clone-Depth). + diff --git a/docs/content/extras/highlighting.md b/docs/content/extras/highlighting.md new file mode 100644 index 000000000..601373deb --- /dev/null +++ b/docs/content/extras/highlighting.md @@ -0,0 +1,197 @@ +--- +aliases: +- /extras/highlight/ +lastmod: 2015-10-27 +date: 2013-07-01 +menu: + main: + parent: extras +next: /extras/toc +prev: /extras/shortcodes +title: Syntax Highlighting +toc: true +--- + +Hugo provides the ability for you to highlight source code in _two different ways_ — either pre-processed server side from your content, or to defer the processing to the client side, using a JavaScript library. + +**The advantage of server side** is that it doesn’t depend on a JavaScript library and consequently works very well when read from an RSS feed. + +**The advantage of client side** is that it doesn’t cost anything when building your site and some of the highlighting scripts available cover more languages than Pygments does. + +## Server-side + +For the pre-processed approach, highlighting is performed by an external Python-based program called [Pygments](http://pygments.org/) and is triggered via an embedded Hugo shortcode (see example below). If Pygments is absent from the path, it will silently simply pass the content along unhighlighted. + +### Pygments + +If you have never worked with Pygments before, here is a brief primer: + ++ Install Python from [python.org](https://www.python.org/downloads/). Version 2.7.x is already sufficient. ++ Run `pip install Pygments` in order to install Pygments. Once installed, Pygments gives you a command `pygmentize`. Make sure it sits in your PATH, otherwise Hugo cannot find it. + +On Debian and Ubuntu systems, you may also install Pygments by running `sudo apt-get install python3-pygments`. + +Hugo gives you two options that you can set with the variable `pygmentsuseclasses` (default `false`) in `config.toml` (or `config.yaml`). + +1. Color-codes for highlighting keywords are directly inserted if `pygmentsuseclasses = false` (default). See in the example below. The color-codes depend on your choice of the `pygmentsstyle` (default `"monokai"`). You can explore the different color styles on [pygments.org](http://pygments.org/) after inserting some example code. +2. If you choose `pygmentsuseclasses = true`, Hugo includes class names in your code instead of color-codes. For class-names to be meaningful, you need to include a `.css`-file in your website representing your color-scheme. You can either generate this `.css`-files according to this [description](http://pygments.org/docs/cmdline/) or download the standard ones from the [GitHub pygments-css repository](https://github.com/richleland/pygments-css). + +### Usage + +Highlighting is carried out via the in-built shortcode `highlight`. `highlight` takes exactly one required parameter of language, and requires a closing shortcode. Note that `highlight` is _not_ used for client-side javascript highlighting. + +### Example + +``` +{{}} +
    +
    +

    {{ .Title }}

    + {{ range .Data.Pages }} + {{ .Render "summary"}} + {{ end }} +
    +
    +{{}} +``` + +### Example Output + +``` +<section id="main"> + <div> + <h1 id="title">{{ .Title }}</h1> + {{ range .Data.Pages }} + {{ .Render "summary"}} + {{ end }} + </div> +</section> +``` + +### Options + +Options to control highlighting can be added as a quoted, comma separated key-value list as the second argument in the shortcode. The example below will highlight as language `go` with inline line numbers, with line number 2 and 3 highlighted. + +``` +{{}} +var a string +var b string +var c string +var d string +{{}} +``` + +Supported keywords: `style`, `encoding`, `noclasses`, `hl_lines`, `linenos`. Note that `style` and `noclasses` will override the similar setting in the global config. + +The keywords are the same you would using with Pygments from the command line, see the [Pygments doc](http://pygments.org/docs/) for more info. + +### Code fences + +It is also possible to add syntax highlighting with GitHub flavoured code fences. To enable this, set the `PygmentsCodeFences` to `true` in Hugo's configuration file. + +```` +``` html +
    +
    +

    {{ .Title }}

    + {{ range .Data.Pages }} + {{ .Render "summary"}} + {{ end }} +
    +
    +``` +```` + +### Disclaimers + + * Pygments is relatively slow and _causes a performance hit when building your site_, but Hugo has been designed to cache the results to disk. + * The caching can be turned off by setting the `--ignoreCache` flag to `true`. + * Languages available depends on your Pygments installation. + +## Client-side + +Alternatively, code highlighting can be done in client-side JavaScript. + +Client-side syntax highlighting is very simple to add. You'll need to pick +a library and a corresponding theme. Some popular libraries are: + +- [Highlight.js] +- [Prism] +- [Rainbow] +- [Syntax Highlighter] +- [Google Prettify] + +### Highlight.js example + +This example uses the popular [Highlight.js] library, hosted by [Yandex], a popular Russian search engine. + +In your `./layouts/partials/` (or `./layouts/chrome/`) folder, depending on your specific theme, there will be a snippet that will be included in every generated HTML page, such as `header.html` or `header.includes.html`. Simply add the css and js to initialize [Highlight.js]: + +~~~ + + + +~~~ + +### Prism example + +Prism is another popular highlighter library, used on some major sites. Similar to Highlight.js, you simply load `prism.css` in your `` via whatever Hugo partial template is creating that part of your pages, like so: + +```html +... + +... +``` + +... and add `prism.js` near the bottom of your `` tag, again in whatever Hugo partial template is appropriate for your site or theme. + +```html +... + +... + +``` + +In this example, the local paths indicate that your own copy of these files are being added to the site, typically under `./static/`. + +### Using Client-side highlighting + +To use client-side highlighting, most of these javascript libraries expect your code to be wrapped in semantically correct `` tags, with the language expressed in a class attribute on the `` tag, such as `class="language-abc"`, where the `abc` is the code the highlighter script uses to represent that language. + +The script would be looking for classes like `language-go`, `language-html`, or `language-css`. If you look at the page's source, it would be marked up like so: + +~~~html +
    +
    +body {
    +  font-family: "Noto Sans", sans-serif;
    +}
    +
    +
    +~~~ + +The markup in your content pages (e.g. `my-css-tutorial.md`) needs to look like the following, with the name of the language to be highlighted entered directly after the first "fence", in a fenced code block: + +
    ~~~css
    +body {
    +  font-family: "Noto Sans", sans-serif;
    +}
    +~~~
    + +When passed through the highlighter script, it would yield something like this output when viewed on your rendered page: + +~~~css +body { + font-family: "Noto Sans", sans-serif; +} +~~~ + +Please see individual libraries' documentation for how to implement each of the JavaScript-based libraries. + +[Prism]: http://prismjs.com +[Highlight.js]: http://highlightjs.org/ +[Rainbow]: http://craig.is/making/rainbows +[Syntax Highlighter]: http://alexgorbatchev.com/SyntaxHighlighter/ +[Google Prettify]: https://github.com/google/code-prettify +[Yandex]: http://yandex.ru/ + diff --git a/docs/content/extras/livereload.md b/docs/content/extras/livereload.md new file mode 100644 index 000000000..cb4047636 --- /dev/null +++ b/docs/content/extras/livereload.md @@ -0,0 +1,74 @@ +--- +lastmod: 2016-08-09 +date: 2014-05-26 +menu: + main: + parent: extras +next: /extras/menus +prev: /extras/gitinfo +title: LiveReload +--- + +Hugo may not be the first static site generator to utilize LiveReload +technology, but it’s the first to do it right. + +The combination of Hugo’s insane build speed and LiveReload make +crafting your content pure joy. Virtually instantly after you hit save +your rebuilt content will appear in your browser. + +## Using LiveReload + +Hugo comes with LiveReload built in. There are no additional packages to +install. A common way to use Hugo while developing a site is to have +Hugo run a server and watch for changes: + +{{< nohighlight >}}$ hugo server +{{< /nohighlight >}} + +This will run a full functioning web server while simultaneously +watching your file system for additions, deletions or changes within +your: + + * static files + * content + * data files + * layouts + * current theme + * configuration files + +Whenever anything changes, Hugo will rebuild the site while continuing to serve +the content. As soon as the build is finished, it will tell the +browser and silently reload the page. Because most Hugo builds are so +fast they are barely noticeable, you merely need to glance at your open +browser and you will see the change, already there. + +This means that keeping the site open on a second monitor (or another +half of your current monitor) allows you to see exactly what your +content looks like, without even leaving your text editor. + +## Disabling Watch + +If for some reason you don't want the Hugo server's watch functionality, +just do: + +{{< nohighlight >}}$ hugo server --watch=false +{{< /nohighlight >}} + +## Disabling LiveReload + +LiveReload works by injecting JavaScript into the pages Hugo generates, +which creates a connection from the browser web socket client to the +Hugo web socket server. + +Awesome for development, but not something you would want to do in +production. Since many people use `hugo server` in production to +instantly display any updated content, we’ve made it easy to disable the +LiveReload functionality: + +{{< nohighlight >}}$ hugo server --disableLiveReload +{{< /nohighlight >}} + +## Notes + +You must have a closing `` tag for LiveReload to work. +Hugo injects the LiveReload ` + +Extract the value from the field `data-id` and pass it to the shortcode: + + {{}} + +### Instagram + +If you'd like to embed photo from [Instagram](https://www.instagram.com/), all you need is photo ID from the URL, e. g.: + +* https://www.instagram.com/p/BMokmydjG-M/ + +Pass it to the shortcode: + + {{}} + +Optionally, hide caption: + + {{}} + +## Creating your own shortcodes + +To create a shortcode, place a template in the layouts/shortcodes directory. The +template name will be the name of the shortcode. + +In creating a shortcode, you can choose if the shortcode will use _positional +parameters_, or _named parameters_, or _both_. A good rule of thumb is that if a +shortcode has a single required value in the case of the `youtube` example below, +then positional works very well. For more complex layouts with optional +parameters, named parameters work best. Allowing both types of parameters is +useful for complex layouts where you want to set default values that can be +overridden. + +**Inside the template** + +To access a parameter by position, the `.Get` method can be used: + + {{ .Get 0 }} + +To access a parameter by name, the `.Get` method should be utilized: + + {{ .Get "class" }} + +`with` is great when the output depends on a parameter being set: + + {{ with .Get "class"}} class="{{.}}"{{ end }} + +`.Get` can also be used to check if a parameter has been provided. This is +most helpful when the condition depends on either one value or another... +or both: + + {{ or .Get "title" | .Get "alt" | if }} alt="{{ with .Get "alt"}}{{.}}{{else}}{{.Get "title"}}{{end}}"{{ end }} + +If a closing shortcode is used, the variable `.Inner` will be populated with all +of the content between the opening and closing shortcodes. If a closing +shortcode is required, you can check the length of `.Inner` and provide a warning +to the user. + +A shortcode with `.Inner` content can be used without the inline content, and without the closing shortcode, by using the self-closing syntax: + + {{}} + +The variable `.Params` contains the list of parameters in case you need to do +more complicated things than `.Get`. It is sometimes useful to provide a +flexible shortcode that can take named or positional parameters. To meet this +need, Hugo shortcodes have a `.IsNamedParams` boolean available that can be used +such as `{{ if .IsNamedParams }}...{{ else }}...{{ end }}`. See the +`Single Flexible Example` below for an example. + +You can also use the variable `.Page` to access all the normal [Page Variables](/templates/variables/). + +A shortcodes can be nested. In a nested shortcode you can access the parent shortcode context with `.Parent`. This can be very useful for inheritance of common shortcode parameters from the root. + +## Single Positional Example: youtube + + {{}} + +Would load the template /layouts/shortcodes/youtube.html + +
    + +
    + +This would be rendered as: + +
    + +
    + +## Single Named Example: image with caption + + {{}} + +Would load the template /layouts/shortcodes/img.html + + +
    + {{ with .Get "link"}}{{ end }} + + {{ if .Get "link"}}{{ end }} + {{ if or (or (.Get "title") (.Get "caption")) (.Get "attr")}} +
    {{ if isset .Params "title" }} +

    {{ .Get "title" }}

    {{ end }} + {{ if or (.Get "caption") (.Get "attr")}}

    + {{ .Get "caption" }} + {{ with .Get "attrlink"}} {{ end }} + {{ .Get "attr" }} + {{ if .Get "attrlink"}} {{ end }} +

    {{ end }} +
    + {{ end }} +
    + + +Would be rendered as: + +
    + +
    +

    Steve Francia

    +
    +
    + +## Single Flexible Example: vimeo with defaults + + {{}} + {{}} + +Would load the template /layouts/shortcodes/vimeo.html + + {{ if .IsNamedParams }} +
    + +
    + {{ else }} +
    + +
    + {{ end }} + +Would be rendered as: + +
    + +
    +
    + +
    + +## Paired Example: Highlight +*Hugo already ships with the `highlight` shortcode* + + {{}} + + This HTML + + {{}} + +The template for this utilizes the following code (already included in Hugo) + + {{ .Get 0 | highlight .Inner }} + +And will be rendered as: + +
    <html>
    +        <body> This HTML </body>
    +    </html>
    +    
    + +Please notice that this template makes use of a Hugo-specific template function +called `highlight` which uses Pygments to add the highlighting code. + +## Simple Single-word Example: Year + +Let's assume you would like to have a shortcode to be replaced by the current year in your Markdown content files, for a license or copyright statement. Calling a shortcode like this: + + {{}} + +... would load your one-line template ``/layouts/shortcodes/year.html``, which contains: + + {{ .Page.Now.Year }} + +More shortcode examples can be found at [spf13.com](https://github.com/spf13/spf13.com/tree/master/layouts/shortcodes). diff --git a/docs/content/extras/toc.md b/docs/content/extras/toc.md new file mode 100644 index 000000000..56e093ba7 --- /dev/null +++ b/docs/content/extras/toc.md @@ -0,0 +1,37 @@ +--- +lastmod: 2015-01-27 +date: 2013-07-09 +menu: + main: + parent: extras +next: /extras/localfiles +prev: /extras/highlighting +title: Table of Contents +--- + +Hugo will automatically parse the Markdown for your content and create +a Table of Contents you can use to guide readers to the sections within +your content. + +## Usage + +Simply create content like you normally would with the appropriate +headers. + +Hugo will take this Markdown and create a table of contents stored in the +[content variable](/layout/variables/) `.TableOfContents` + + +## Template Example + +This is example code of a [single.html template](/layout/content/). + + {{ partial "header.html" . }} +
    + {{ .TableOfContents }} +
    +

    {{ .Title }}

    + {{ .Content }} + {{ partial "footer.html" . }} + + diff --git a/docs/content/extras/urls.md b/docs/content/extras/urls.md new file mode 100644 index 000000000..487e685f3 --- /dev/null +++ b/docs/content/extras/urls.md @@ -0,0 +1,70 @@ +--- +aliases: +- /doc/urls/ +lastmod: 2016-05-07 +date: 2014-01-03 +menu: + main: + parent: extras +next: /community/mailing-list +notoc: true +prev: /extras/localfiles +title: URLs +--- + +## Pretty URLs + +By default, Hugo creates content with 'pretty' URLs. For example, +content created at `/content/extras/urls.md` will be rendered at +`/public/extras/urls/index.html`, thus accessible from the browser +at http://example.com/extras/urls/. No non-standard server-side +configuration is required for these pretty URLs to work. + +If you would like to have what we call "ugly URLs", +e.g. http://example.com/extras/urls.html, you are in luck. +Hugo supports the ability to create your entire site with ugly URLs. +Simply add `uglyurls = true` to your site-wide `config.toml`, +or use the `--uglyURLs=true` flag on the command line. + +If you want a specific piece of content to have an exact URL, you can +specify this in the front matter under the `url` key. See [Content +Organization](/content/organization/) for more details. + +## Canonicalization + +By default, all relative URLs encountered in the input are left unmodified, +e.g. `/css/foo.css` would stay as `/css/foo.css`, +i.e. `canonifyURLs` defaults to `false`. + +By setting `canonifyURLs` to `true`, all relative URLs would instead +be *canonicalized* using `baseURL`. For example, assuming you have +`baseURL = http://yoursite.example.com/` defined in the site-wide +`config.toml`, the relative URL `/css/foo.css` would be turned into +the absolute URL `http://yoursite.example.com/css/foo.css`. + +Benefits of canonicalization include fixing all URLs to be absolute, which may +aid with some parsing tasks. Note though that all real browsers handle this +client-side without issues. + +Benefits of non-canonicalization include being able to have resource inclusion +be scheme-relative, so that http vs https can be decided based on how this +page was retrieved. + +> Note: In the May 2014 release of Hugo v0.11, the default value of `canonifyURLs` was switched from `true` to `false`, which we think is the better default and should continue to be the case going forward. So, please verify and adjust your website accordingly if you are upgrading from v0.10 or older versions. + +To find out the current value of `canonifyURLs` for your website, you may use the handy `hugo config` command added in v0.13: + + hugo config | grep -i canon + +Or, if you are on Windows and do not have `grep` installed: + + hugo config | FINDSTR /I canon + +## Relative URLs + +By default, all relative URLs are left unchanged by Hugo, +which can be problematic when you want to make your site browsable from a local file system. + +Setting `relativeURLs` to `true` in the site configuration will cause Hugo to rewrite all relative URLs to be relative to the current content. + +For example, if the `/post/first/` page contained a link with a relative URL of `/about/`, Hugo would rewrite that URL to `../../about/`. diff --git a/docs/content/meta/license.md b/docs/content/meta/license.md new file mode 100644 index 000000000..a16923bfb --- /dev/null +++ b/docs/content/meta/license.md @@ -0,0 +1,211 @@ +--- +aliases: +- /doc/license/ +- /license/ +- /meta/license/ +lastmod: 2015-11-25 +date: 2013-07-01 +menu: + main: + parent: about +title: License +weight: 50 +--- + +Hugo v0.15 and later are released under the Apache 2.0 license. +Earlier releases were under the Simple Public License. + +Apache License +============== + +_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. diff --git a/docs/content/meta/roadmap.md b/docs/content/meta/roadmap.md new file mode 100644 index 000000000..8a0a914be --- /dev/null +++ b/docs/content/meta/roadmap.md @@ -0,0 +1,33 @@ +--- +aliases: +- /doc/roadmap/ +- /meta/roadmap/ +lastmod: 2015-02-16 +date: 2013-07-01 +menu: + main: + parent: about +notoc: true +title: Hugo Roadmap +weight: 20 +--- + +In no particular order, here is what we are working on: + + * Intelligently related posts ([#98][]) + * Even easier deployment to S3, SSH, GitHub, rsync. Give the [tools section](https://gohugo.io/tools/#deployment) a shot or read one of the related tutorials. + * Import from other website systems. There are already existing [migration tools](https://gohugo.io/tools/#migration) but they don't cover all major platforms. + * An interactive web based editor (See https://discourse.gohugo.io/t/web-based-editor/155) + * Additional [themes](https://github.com/gohugoio/hugoThemes) (always on-going, contributions welcome!) + * Dynamic image resizing via shortcodes ([#1014][]) + * Native support for additional content formats (AsciiDoc [#1435][], reST [#1436][]) + * And, last but not least, ***Your best ideas***! + +[#100]: https://github.com/gohugoio/hugo/issues/100 "hugo import from wordpress · Issue #100 · gohugoio/hugo" +[#101]: https://github.com/gohugoio/hugo/issues/101 "hugo import from jekyll · Issue #101 · gohugoio/hugo" +[#1435]: https://github.com/gohugoio/hugo/issues/1435 "Add support for native Go implementation of AsciiDoc · Issue #1435 · gohugoio/hugo" +[#1436]: https://github.com/gohugoio/hugo/issues/1436 "Add support for native Go implementation of reStructuredText (reST) · Issue #1436 · gohugoio/hugo" +[#1014]: https://github.com/gohugoio/hugo/issues/1014 "Image Resizing and Cropping · Issue #1014 · gohugoio/hugo" +[#98]: https://github.com/gohugoio/hugo/issues/98 "Add support for related content · Issue #98 · gohugoio/hugo" + +> Feel free to [contribute]({{< relref "tutorials/how-to-contribute-to-hugo.md" >}}) or open a [new issue](https://github.com/gohugoio/hugo/issues/new) if you have an idea for a new feature.) diff --git a/docs/content/overview/configuration.md b/docs/content/overview/configuration.md new file mode 100644 index 000000000..7d05570b7 --- /dev/null +++ b/docs/content/overview/configuration.md @@ -0,0 +1,458 @@ +--- +aliases: +- /doc/configuration/ +lastmod: 2016-09-17 +date: 2013-07-01 +linktitle: Configuration +menu: + main: + parent: getting started +next: /overview/source-directory +toc: true +prev: /overview/usage +title: Configuring Hugo +weight: 40 +--- +The directory structure of a Hugo web site—or more precisely, +of the source files containing its content and templates—provide +most of the configuration information that Hugo needs. +Therefore, in essence, +many web sites wouldn't actually need a configuration file. +This is because Hugo is designed to recognize certain typical usage patterns +(and it expects them, by default). + +Nevertheless, Hugo does search for a configuration file bearing +a particular name in the root of your web site's source directory. +First, it looks for a `./config.toml` file. +If that's not present, it will seek a `./config.yaml` file, +followed by a `./config.json` file. + +In this `config` file for your web site, +you can include precise directions to Hugo regarding +how it should render your site, as well as define its menus, +and set various other site-wide parameters. + +Another way that web site configuration can be accomplished is through +operating system environment variables. +For instance, the following command will work on Unix-like systems—it +sets a web site's title: +```bash +$ env HUGO_TITLE="Some Title" hugo +``` +(**Note:** all such environment variable names must be prefixed with +HUGO_.) + +## Examples + +Following is a typical example of a YAML configuration file. +Three periods end the document: + +```yaml +--- +baseURL: "http://yoursite.example.com/" +... +``` +Following is an example TOML configuration file with some default values. +The values under `[params]` will populate the `.Site.Params` variable +for use in templates: + +```toml +contentDir = "content" +layoutDir = "layouts" +publishDir = "public" +buildDrafts = false +baseURL = "http://yoursite.example.com/" +canonifyURLs = true + +[taxonomies] + category = "categories" + tag = "tags" + +[params] + description = "Tesla's Awesome Hugo Site" + author = "Nikola Tesla" +``` +Here is a YAML configuration file which sets a few more options: + +```yaml +--- +baseURL: "http://yoursite.example.com/" +title: "Yoyodyne Widget Blogging" +footnoteReturnLinkContents: "↩" +permalinks: + post: /:year/:month/:title/ +params: + Subtitle: "Spinning the cogs in the widgets" + AuthorName: "John Doe" + GitHubUser: "spf13" + ListOfFoo: + - "foo1" + - "foo2" + SidebarRecentLimit: 5 +... +``` +## Configuration variables + +Following is a list of Hugo-defined variables you can configure, +along with their current, default values: + + --- + archetypeDir: "archetypes" + # hostname (and path) to the root, e.g. http://spf13.com/ + baseURL: "" + # include content marked as draft + buildDrafts: false + # include content with publishdate in the future + buildFuture: false + # include content already expired + buildExpired: false + # enable this to make all relative URLs relative to content root. Note that this does not affect absolute URLs. + relativeURLs: false + canonifyURLs: false + # config file (default is path/config.yaml|json|toml) + config: "config.toml" + contentDir: "content" + dataDir: "data" + defaultExtension: "html" + defaultLayout: "post" + # Missing translations will default to this content language + defaultContentLanguage: "en" + # Renders the default content language in subdir, e.g. /en/. The root directory / will redirect to /en/ + defaultContentLanguageInSubdir: false + # The below example will disable all page types and will render nothing. + disableKinds = ["page", "home", "section", "taxonomy", "taxonomyTerm", "RSS", "sitemap", "robotsTXT", "404"] + disableLiveReload: false + # Do not build RSS files + disableRSS: false + # Do not build Sitemap file + disableSitemap: false + # Enable GitInfo feature + enableGitInfo: false + # Build robots.txt file + enableRobotsTXT: false + # Do not render 404 page + disable404: false + # Do not inject generator meta tag on homepage + disableHugoGeneratorInject: false + # Enable Emoji emoticons support for page content. + # See www.emoji-cheat-sheet.com + enableEmoji: false + # Show a placeholder instead of the default value or an empty string if a translation is missing + enableMissingTranslationPlaceholders: false + footnoteAnchorPrefix: "" + footnoteReturnLinkContents: "" + # google analytics tracking id + googleAnalytics: "" + languageCode: "" + layoutDir: "layouts" + # Enable Logging + log: false + # Log File path (if set, logging enabled automatically) + logFile: "" + # "yaml", "toml", "json" + metaDataFormat: "toml" + # Edit new content with this editor, if provided + newContentEditor: "" + # Don't sync permission mode of files + noChmod: false + # Don't sync modification time of files + noTimes: false + paginate: 10 + paginatePath: "page" + permalinks: + # Pluralize titles in lists using inflect + pluralizeListTitles: true + # Preserve special characters in taxonomy names ("Gérard Depardieu" vs "Gerard Depardieu") + preserveTaxonomyNames: false + # filesystem path to write files to + publishDir: "public" + # enables syntax guessing for code fences without specified language + pygmentsCodeFencesGuessSyntax: false + # color-codes for highlighting derived from this style + pygmentsStyle: "monokai" + # true: use pygments-css or false: color-codes directly + pygmentsUseClasses: false + # maximum number of items in the RSS feed + rssLimit: 15 + # default sitemap configuration map + sitemap: + # filesystem path to read files relative from + source: "" + staticDir: "static" + # display memory and timing of different steps of the program + stepAnalysis: false + # theme to use (located by default in /themes/THEMENAME/) + themesDir: "themes" + theme: "" + title: "" + # if true, use /filename.html instead of /filename/ + uglyURLs: false + # Do not make the url/path to lowercase + disablePathToLower: false + # if true, auto-detect Chinese/Japanese/Korean Languages in the content. (.Summary and .WordCount can work properly in CJKLanguage) + hasCJKLanguage: false + # verbose output + verbose: false + # verbose logging + verboseLog: false + # watch filesystem for changes and recreate as needed + watch: true + --- + +## Ignore various files when rendering + +The following statement inside `./config.toml` will cause Hugo to ignore files +ending with `.foo` and `.boo` when rendering: + +```toml +ignoreFiles = [ "\\.foo$", "\\.boo$" ] +``` +The above is a list of regular expressions. +Note that the backslash (`\`) character is escaped, to keep TOML happy. + +## Configure Blackfriday rendering + +[Blackfriday](https://github.com/russross/blackfriday) is Hugo's +[Markdown](http://daringfireball.net/projects/markdown/) +rendering engine. + +In the main, Hugo typically configures Blackfriday with a sane set of defaults. +These defaults should fit most use cases, reasonably well. + +However, if you have unusual needs with respect to Markdown, +Hugo exposes some of its Blackfriday behavior options for you to alter. +The following table lists these Hugo options, +paired with the corresponding flags from Blackfriday's source code (for the latter, see +[html.go](https://github.com/russross/blackfriday/blob/master/html.go) and +[markdown.go](https://github.com/russross/blackfriday/blob/master/markdown.go)): + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FlagDefaultBlackfriday flag
    taskListstrue
    + Purpose: + false turns off GitHub-style automatic task/TODO + list generation. +
    smartypantstrueHTML_USE_SMARTYPANTS
    + Purpose: + false disables smart punctuation substitutions + including smart quotes, smart dashes, smart fractions, etc. + If true, it may be fine-tuned with the + angledQuotes, + fractions, + smartDashes and + latexDashes flags (see below). +
    angledQuotesfalseHTML_SMARTYPANTS_ANGLED_QUOTES
    + Purpose: + true enables smart, angled double quotes.
    + + Example: + "Hugo" renders to + «Hugo» instead of “Hugo”. + +
    fractionstrueHTML_SMARTYPANTS_FRACTIONS
    + Purpose: + false disables smart fractions.
    + + Example: + 5/12 renders to + 512 + (<sup>5</sup>&frasl;<sub>12</sub>).
    + Caveat: + Even with fractions = false, + Blackfriday still converts + 1/2, 1/4 and 3/4 respectively to + ½ (&frac12;), + ¼ (&frac14;) and + ¾ (&frac34;), + but only these three.
    +
    smartDashestrueHTML_SMARTYPANTS_DASHES
    + Purpose: + false disables smart dashes; i.e., the conversion + of multiple hyphens into en dash or em dash. + If true, its behavior can be modified with the + latexDashes flag below. +
    latexDashestrueHTML_SMARTYPANTS_LATEX_DASHES
    + Purpose: + false disables LaTeX-style smart dashes and + selects conventional smart dashes. Assuming + smartDashes (above), if this is: +
      +
    • + true, then + -- is translated into “–” + (&ndash;), whereas + --- is translated into “—” + (&mdash;). +
    • +
    • + false, then + -- is translated into “—” + (&mdash;), whereas a + spaced single hyphen between two words + is translated into an en dash—e.g., + 12 June - 3 July becomes + 12 June &ndash; 3 July. +
    • +
    +
    hrefTargetBlankfalseHTML_HREF_TARGET_BLANK
    + Purpose: + true opens external links in a new window or tab. +
    plainIDAnchorstrue + FootnoteAnchorPrefix and + HeaderIDSuffix +
    + Purpose: + true renders any header and footnote IDs + without the document ID.
    + + Example: + renders #my-header instead of + #my-header:bec3ed8ba720b9073ab75abcf3ba5d97. + +
    extensions[]EXTENSION_*
    + Purpose: + Enable one or more of Blackfriday's Markdown extensions + (if they aren't Hugo defaults).
    + + Example:   + Include "hardLineBreak" + in the list to enable Blackfriday's + EXTENSION_HARD_LINE_BREAK. + +
    extensionsmask[]EXTENSION_*
    + Purpose: + Disable one or more of Blackfriday's Markdown extensions + (if they are Hugo defaults).
    + + Example:   + Include "autoHeaderIds" + in the list to disable Blackfriday's + EXTENSION_AUTO_HEADER_IDS. + +
    + +**Notes** + +* These flags are **case sensitive** (as of Hugo v0.15)! +* These flags must be grouped under the `blackfriday` key +and can be set on **both the site level and the page level**. +Any setting on a page will override the site setting +there. For example: + + + + + + + + + + + + + + +
    TOMLYAML
    +
    [blackfriday]
    +  angledQuotes = true
    +  fractions = false
    +  plainIDAnchors = true
    +  extensions = ["hardLineBreak"]
    +
    +
    +
    blackfriday:
    +  angledQuotes: true
    +  fractions: false
    +  plainIDAnchors: true
    +  extensions:
    +    - hardLineBreak
    +
    +
    diff --git a/docs/content/overview/installing.md b/docs/content/overview/installing.md new file mode 100644 index 000000000..fb7315c19 --- /dev/null +++ b/docs/content/overview/installing.md @@ -0,0 +1,149 @@ + +--- +aliases: +- /doc/installing/ +lastmod: 2016-01-04 +date: 2013-07-01 +menu: + main: + parent: getting started +next: /overview/usage +prev: /overview/quickstart +title: Installing Hugo +weight: 20 +--- + +Hugo is written in [Go][] with support for multiple platforms. + +The latest release can be found at [Hugo Releases](https://github.com/gohugoio/hugo/releases). +We currently provide pre-built binaries for + Windows, + Linux, + FreeBSD +and  OS X (Darwin) +for x64, i386 and ARM architectures. + +Hugo may also be compiled from source wherever the Go compiler tool chain can run, e.g. for other operating systems including DragonFly BSD, OpenBSD, Plan 9 and Solaris. See http://golang.org/doc/install/source for the full set of supported combinations of target operating systems and compilation architectures. + +## Installing Hugo (binary) + +Installation is very easy. Simply download the appropriate version for your +platform from [Hugo Releases](https://github.com/gohugoio/hugo/releases). +Once downloaded it can be run from anywhere. You don't need to install +it into a global location. This works well for shared hosts and other systems +where you don't have a privileged account. + +Ideally, you should install it somewhere in your `PATH` for easy use. +`/usr/local/bin` is the most probable location. + +On macOS, if you have [Homebrew](http://brew.sh/), installation is even +easier: just run `brew install hugo`. + +For a more detailed explanation follow the corresponding installation guides: + +- [Installation on macOS]({{< relref "tutorials/installing-on-mac.md" >}}) +- [Installation on Windows]({{< relref "tutorials/installing-on-windows.md" >}}) + +### Installing Pygments (optional) + +The Hugo executable has one *optional* external dependency for source code highlighting (Pygments). + +If you want to have source code highlighting using the [highlight shortcode](/extras/highlighting/), +you need to install the Python-based Pygments program. The procedure is outlined on the [Pygments home page](http://pygments.org/). + +## Upgrading Hugo + +Upgrading Hugo is as easy as downloading and replacing the executable you’ve +placed in your `PATH`. + +On macOS, if you have [Homebrew](http://brew.sh/), upgrading is even +easier: just run `brew upgrade hugo`. + +## Installing Hugo on Linux from native packages + +### Arch Linux + +You can install Hugo from the [Arch user repository](https://aur.archlinux.org/) on Arch Linux or derivatives such as Manjaro. + + sudo pacman -S yaourt + yaourt -S hugo + +Be aware that Hugo is built from source. This means that additional tools like [Git](https://git-scm.com/) and [Go](https://golang.org/doc/install) will be installed as well. + +### Debian and Ubuntu + +Hugo has been included in Debian and Ubuntu since 2016, and thus installing Hugo is as simple as: + + sudo apt install hugo + +Pros: + +* Native Debian/Ubuntu package maintained by Debian Developers +* Pre-installed bash completion script and man pages for best interactive experience + +Cons: + +* Might not be the latest version, especially if you are using an older stable version (e.g., Ubuntu 16.04 LTS). + Until backports and PPA are available, you may consider installing the Hugo snap package to get the latest version of Hugo, as described below. + +### Fedora, CentOS and Red Hat + +* https://copr.fedorainfracloud.org/coprs/daftaupe/hugo/ : updated as soon as possible after the official Hugo release. +* https://copr.fedorainfracloud.org/coprs/spf13/Hugo/ (updated to Hugo v0.16) + +See also [this discussion](https://discourse.gohugo.io/t/solved-fedora-copr-repository-out-of-service/2491). + +## Alternate Installation Methods + +### Snap Package + +In any of the [Linux distributions that support snaps](http://snapcraft.io/docs/core/install): + + snap install hugo + +> Note: Hugo-as-a-snap can write only inside the user’s `$HOME` directory—and gvfs-mounted directories owned by the user—because of Snaps’ confinement and security model. +> More information is also available [in this related GitHub issue](https://github.com/gohugoio/hugo/issues/3143). + +### Docker Image (unofficial) + +[Docker Hugo](https://hub.docker.com/r/felicianotech/docker-hugo/) is a Docker image that can be used for local development but more importantly, can be easily used for continuous integration builds of your Hugo site on [CircleCI](https://circleci.com/) or [Travis CI](https://travis-ci.org/). Source available on [GitHub](https://github.com/felicianotech/docker-hugo). + +## Installing from source + +### Prerequisite tools for downloading and building source code + +* [Git](http://git-scm.com/) +* [Go][] 1.8+ +* [govendor][] + +### Vendored Dependencies + +Hugo uses [govendor][] to vendor dependencies, but we don't commit the vendored packages themselves to the Hugo git repository. +Therefore, a simple `go get` is not supported since `go get` is not vendor-aware. +You **must use govendor** to fetch Hugo's dependencies. + +### Fetch from GitHub + + The commands below assume that you have [Go](https://golang.org/dl/) installed with your `$GOPATH` set. + + go get github.com/kardianos/govendor + govendor get github.com/gohugoio/hugo + +`govendor get` will fetch Hugo and all its dependent libraries to +`$GOPATH/src/github.com/gohugoio/hugo`, and compile everything into a final `hugo` +(or `hugo.exe`) executable, which you will find sitting inside +`$GOPATH/go/bin/`, all ready to go! + +*Windows users: where you see the `$HOME` environment variable above, replace it with `%USERPROFILE%`.* + + +*Note: For syntax highlighting using the [highlight shortcode](/extras/highlighting/), +you need to install the Python-based [Pygments](http://pygments.org/) program.* + +## Contributing + +Please see the [contributing guide](/doc/contributing/) if you are interested in +working with the Hugo source or contributing to the project in any way. + +[Go]: http://golang.org/ +[govendor]: https://github.com/kardianos/govendor diff --git a/docs/content/overview/introduction.md b/docs/content/overview/introduction.md new file mode 100644 index 000000000..4ae8ccaa7 --- /dev/null +++ b/docs/content/overview/introduction.md @@ -0,0 +1,197 @@ +--- +lastmod: 2016-08-14 +date: 2013-07-01 +linktitle: Introduction +menu: + main: + parent: getting started +next: /overview/quickstart +title: Introduction to Hugo +weight: 5 +--- + +## What is Hugo? + +Hugo is a general-purpose website framework. Technically speaking, Hugo is +a static site generator. Unlike other systems which dynamically build a page +every time a visitor requests one, Hugo does the building when you create +your content. Since websites are viewed far more often than they are +edited, Hugo is optimized for website viewing while providing a great +writing experience. + +Sites built with Hugo are extremely fast and very secure. Hugo sites can +be hosted anywhere, including [Heroku][], [GoDaddy][], [DreamHost][], +[GitHub Pages][], [Netlify][], [Surge][], [Aerobatic][], [Firebase Hosting][], +[Google Cloud Storage][], [Amazon S3][] and [CloudFront][], and work well +with CDNs. Hugo sites run without dependencies on expensive runtimes +like Ruby, Python or PHP and without dependencies on any databases. + +[Heroku]: https://www.heroku.com/ +[GoDaddy]: https://www.godaddy.com/ +[DreamHost]: http://www.dreamhost.com/ +[GitLab]: https://about.gitlab.com +[GitHub Pages]: https://pages.github.com/ +[Aerobatic]: https://www.aerobatic.com/ +[Firebase Hosting]: https://firebase.google.com/docs/hosting/ +[Google Cloud Storage]: http://cloud.google.com/storage/ +[Amazon S3]: http://aws.amazon.com/s3/ +[CloudFront]: http://aws.amazon.com/cloudfront/ "Amazon CloudFront" +[Surge]: https://surge.sh +[Netlify]: https://www.netlify.com + +We think of Hugo as the ideal website creation tool. With nearly instant +build times and the ability to rebuild whenever a change is made, Hugo +provides a very fast feedback loop. This is essential when you are +designing websites, but also very useful when creating content. + +## What makes Hugo different? + +Web site generators render content into HTML files. Most are "dynamic +site generators." That means the HTTP +server (which is the program running on your website that the user's +browser talks to) runs the generator to create a new HTML file +each and every time a user wants to view a page. + +Creating the page dynamically means that the computer hosting +the HTTP server has to have enough memory and CPU to effectively run +the generator around the clock. If not, then the user has to wait +in a queue for the page to be generated. + +Nobody wants users to wait longer than needed, so the dynamic site +generators programmed their systems to cache the HTML files. When +a file is cached, a copy of it is temporarily stored on the computer. +It is much faster for the HTTP server to send that copy the next time +the page is requested than it is to generate it from scratch. + +Hugo takes caching a step further. All HTML files are rendered on your +computer. You can review the files before you copy them to the computer +hosting the HTTP server. Since the HTML files aren't generated dynamically, +we say that Hugo is a "static site generator." + +Not running a web site generator on your HTTP server has many benefits. +The most noticeable is performance - HTTP servers are very good at +sending files. So good that you can effectively serve the same number +of pages with a fraction of the memory and CPU needed for a dynamic site. + +Hugo has two components to help you build and test your web site. The +one that you'll probably use most often is the built-in HTTP server. +When you run `hugo server`, Hugo renders all of your content into +HTML files and then runs an HTTP server on your computer so that you +can see what the pages look like. + +The second component is used when you're ready to publish your web +site to the computer running your website. Running Hugo without any +actions will rebuild your entire web site using the `baseURL` setting +from your site's configuration file. That's required to have your page +links work properly with most hosting companies. + +## How fast is Hugo? + +{{% youtube CdiDYZ51a2o %}} + +## What does Hugo do? + +In technical terms, Hugo takes a source directory of files and +templates and uses these as input to create a complete website. + +Hugo boasts the following features: + +### General + + * Extremely fast build times (~1 ms per page) + * Completely cross platform: Runs on  macOS,  Linux,  Windows, and more! + * Easy [installation](/overview/installing/) + * Render changes [on the fly](/overview/usage/) with [LiveReload](/extras/livereload/) as you develop + * Complete theme support + * Host your site anywhere + +### Organization + + * Straightforward [organization](/content/organization/) + * Support for [website sections](/content/sections/) + * Completely customizable [URLs](/extras/urls/) + * Support for configurable [taxonomies](/taxonomies/overview/) which includes categories and tags. Create your own custom organization of content + * Ability to [sort content](/content/ordering/) as you desire + * Automatic [table of contents](/extras/toc/) generation + * Dynamic menu creation + * [Pretty URLs](/extras/urls/) support + * [Permalink](/extras/permalinks/) pattern support + * [Aliases](/extras/aliases/) (redirects) + +### Content + + * Native support for content written in [Markdown](/content/example/) + * Support for other languages through _external helpers_, see [supported formats](/content/supported-formats) + * Support for TOML, YAML and JSON metadata in [frontmatter](/content/front-matter/) + * Completely [customizable homepage](/layout/homepage/) + * Support for multiple [content types](/content/types/) + * Automatic and user defined [summaries](/content/summaries/) + * [Shortcodes](/extras/shortcodes/) to enable rich content inside of Markdown + * ["Minutes to Read"](/layout/variables/) functionality + * ["Wordcount"](/layout/variables/) functionality + +### Additional Features + + * Integrated [Disqus](https://disqus.com/) comment support + * Integrated [Google Analytics](https://google-analytics.com/) support + * Automatic [RSS](/layout/rss/) creation + * Support for [Go](http://golang.org/pkg/html/template/), [Amber](https://github.com/eknkc/amber) and [Ace](https://github.com/yosssi/ace) HTML templates + * Syntax [highlighting](/extras/highlighting/) powered by [Pygments](http://pygments.org/) + +See what's coming next in the [roadmap](/meta/roadmap/). + +## Who should use Hugo? + +Hugo is for people that prefer writing in a text editor over +a browser. + +Hugo is for people who want to hand code their own website without +worrying about setting up complicated runtimes, dependencies and +databases. + +Hugo is for people building a blog, company site, portfolio, tumblog, +documentation, single page site or a site with thousands of +pages. + +## Why did you write Hugo? + +I wrote Hugo ultimately for a few reasons. First, I was disappointed with +WordPress, my then website solution. With it, I couldn't create +content as efficiently as I wanted to. +It rendered slowly. It required me to be online to write +posts: plus its constant security updates and the horror stories of people's +hacked blogs! I hated how content for it was written only in HTML, instead of the much +simpler Markdown. Overall, I felt like WordPress got in my way +much more than it helped me. It kept +me from writing great content. + +I looked at the existing static site generators +like [Jekyll][], [Middleman][] and [Nanoc][]. +All had complicated installation dependencies and took far longer to render +my blog with its hundreds of posts than I felt was acceptable. I wanted +a framework to be able to give me rapid feedback while making changes to the +templates, and the 5+-minute render times were just too slow. In general, +they were also very blog minded and didn't have the ability to provide +other content types and flexible URLs. + +[Jekyll]: http://jekyllrb.com/ +[Middleman]: https://middlemanapp.com/ +[Nanoc]: http://nanoc.ws/ + +I wanted to develop a fast and full-featured website framework without any +dependencies. The [Go language][] seemed to have all the features I needed +in a language. I began developing Hugo in Go and fell in love with the +language. I hope you will enjoy using Hugo (and contributing to it) as much +as I have writing it. + +—Steve Francia (@spf13) + +[Go language]: http://golang.org/ "The Go Programming Language" + +## Next Steps + + * [Install Hugo](/overview/installing/) + * [Quick start](/overview/quickstart/) + * [Join the Mailing List](/community/mailing-list/) + * [Star us on GitHub](https://github.com/gohugoio/hugo) + * [Discussion Forum](https://discourse.gohugo.io/) diff --git a/docs/content/overview/quickstart.md b/docs/content/overview/quickstart.md new file mode 100644 index 000000000..7340a4957 --- /dev/null +++ b/docs/content/overview/quickstart.md @@ -0,0 +1,573 @@ +--- +lastmod: 2016-10-20 +date: 2013-07-01 +linktitle: Quickstart +menu: + main: + parent: getting started +next: /overview/installing +prev: /overview/introduction +title: Hugo Quickstart Guide +weight: 10 +--- + +Building a bookshelf +--- + +In this quickstart, we will build an online bookshelf that will list books and their reviews. + +> _Note: This quickstart depends on features introduced in Hugo v0.15. If you have an earlier version of Hugo, you will need to [upgrade](/overview/installing/) before proceeding._ + +{{% youtube w7Ft2ymGmfc %}} + +## Step 1. Install Hugo + +Go to [Hugo Releases](https://github.com/gohugoio/hugo/releases) and download the +appropriate version for your OS and architecture. + +Save the main executable as `hugo` (or `hugo.exe` on Windows) somewhere in your `PATH` as we will be using it in the next step. + +More complete instructions are available +at [Installing Hugo]({{< relref "overview/installing.md" >}}). + +If you're on Windows, this quickstart will assume +you're using [Git Bash](https://git-for-windows.github.io/) +(also known as Git for Windows). +Thus all commands will begin with the Bash prompt character (which is `$`). + +Once `hugo` is installed, make sure to run the `help` command to verify `hugo` installation. Below you can see part of the `help` command output for brevity. + +```bash +$ hugo help +``` +``` +hugo 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 http://gohugo.io/. +``` + +You can check `hugo` version using the command shown below. + +```bash +$ hugo version +``` +``` +Hugo Static Site Generator v0.15 BuildDate: 2015-11-26T11:59:00+05:30 +``` + +## Step 2. Scaffold bookshelf hugo site + +Hugo has commands that allows us to quickly scaffold a Hugo managed website. Navigate to a convenient location on your filesystem and create a new Hugo site `bookshelf` by executing the following command. + +```bash +$ hugo new site bookshelf +``` + +Change directory to `bookshelf` and you will see the following directory layout. + +```bash +$ tree -a +``` +``` +. +|-- archetypes +|-- config.toml +|-- content +|-- data +|-- layouts +|-- static +`-- themes + +6 directories, 1 file +``` + +As mentioned in the command output, `bookshelf` directory has 6 sub-directories and 1 file. Let's look at each of them one by one. + +* **archetypes**: You can create new content files in Hugo using the `hugo new` command. When you run that command, it adds few configuration properties to the post like date and title. [Archetype]({{< relref "content/archetypes.md" >}}) allows you to define your own configuration properties that will be added to the post front matter whenever `hugo new` command is used. + +* **config.toml**: Every website should have a configuration file at the root. By default, the configuration file uses `TOML` format but you can also use `YAML` or `JSON` formats as well. [TOML](https://github.com/toml-lang/toml) is minimal configuration file format that's easy to read due to obvious semantics. The configuration settings mentioned in the `config.toml` are applied to the full site. These configuration settings include `baseURL` and `title` of the website. + +* **content**: This is where you will store content of the website. Inside content, you will create sub-directories for different sections. Let's suppose your website has three sections -- `blog`, `article`, and `tutorial` then you will have three different directories for each of them inside the `content` directory. The name of the section i.e. `blog`, `article`, or `tutorial` will be used by Hugo to apply a specific layout applicable to that section. + +* **data**: This directory is used to store configuration files that can be +used by Hugo when generating your website. +You can write these files in YAML, JSON, or TOML format. + +* **layouts**: The content inside this directory is used to specify how your content will be converted into the static website. + +* **static**: This directory is used to store all the static content that your website will need like images, CSS, JavaScript or other static content. + +* **themes**: This is where you will create a theme for your site to use. Themes provide the layout and templates that renders content. There's a wide variety of open-source themes available to download and use but you can also create your own if you prefer. + +## Step 3. Add content + +Let's now add a post to our `bookshelf`. We will use the `hugo new` command to add a post. In January, I read [Good To Great](http://www.amazon.com/Good-Great-Some-Companies-Others/dp/0066620996/) book so we will start with creating a post for it. **Make sure you are inside the `bookshelf` directory.** + +```bash +$ hugo new post/good-to-great.md +``` +``` +/Users/shekhargulati/bookshelf/content/post/good-to-great.md created +``` + +The above command will create a new directory `post` +inside the `bookshelf/content` directory +and create `good-to-great.md` file inside it. + +```bash +$ tree -a content +``` +``` +content +`-- post + `-- good-to-great.md + +1 directory, 1 file +``` + +The content inside the `good-to-great.md` file looks as shown below. + +``` ++++ +date = "2016-02-14T16:11:58+05:30" +draft = true +title = "good to great" + ++++ +``` + +The content inside `+++` is the TOML configuration for the post. +This configuration is called **front matter**. +It enables you to define post configuration along with its content. +By default, each post will have the three configuration properties shown above. + +* **date** specifies the date and time at which post was created. +* **draft** specifies that post is not ready for publication yet so it will not be in the generated site. +* **title** specifies title for the post. + +Let's add a small review for **Good to Great** book. + +``` ++++ +date = "2016-02-14T16:11:58+05:30" +draft = true +title = "Good to Great Book Review" + ++++ + +I read **Good to Great in January 2016**. An awesome read sharing detailed analysis on how good companies became great. +``` + +## Step 4. Serve content + +Hugo has an inbuilt server that can serve your website content so that you can preview it. You can also use the inbuilt Hugo server in production. To serve content, execute the following command inside the `bookshelf` directory. + +```bash +$ hugo server +``` +``` +0 of 1 draft rendered +0 future content +0 pages created +0 paginator pages created +0 tags created +0 categories created +in 9 ms +Watching for changes in /Users/shekhargulati/bookshelf/{data,content,layouts,static} +Serving pages from memory +Web Server is available at http://localhost:1313/ (bind address 127.0.0.1) +Press Ctrl+C to stop +``` + +This will start the server on port `1313`. +You can view your blog at http://localhost:1313/. +When you go to the link, you will see nothing. +There are couple of reasons for that: + +1. As you can see in the `hugo server` command output, Hugo didn't render the draft. Hugo will only render drafts if you pass the `buildDrafts` flag to the `hugo server` command. +2. We have not specified how Markdown content should be rendered. We have to specify a theme that Hugo can use. We will do that in the next step. + +To render drafts, re-run the server with command shown below. + +```bash +$ hugo server --buildDrafts +``` +``` +1 of 1 draft rendered +0 future content +1 pages created +0 paginator pages created +0 tags created +0 categories created +in 6 ms +Watching for changes in /Users/shekhargulati/bookshelf/{data,content,layouts,static} +Serving pages from memory +Web Server is available at http://localhost:1313/ (bind address 127.0.0.1) +Press Ctrl+C to stop +``` + +If you go to [http://localhost:1313/](http://localhost:1313/), +you still will not see anything as we have not specified a theme that Hugo should use. + +## Step 5. Add theme + +Themes provide the layout and templates that will be used by Hugo to render your website. There are a lot of Open-source themes available at [https://themes.gohugo.io/](https://themes.gohugo.io/) that you can use. + +> **Hugo currently doesn’t ship with a `default` theme, allowing the user to pick whichever theme best suits their project.** + +Themes should be added in the `themes` directory inside the repository root. + +```bash +$ cd themes +``` +Now, you can clone one or more themes inside the `themes` directory. +We will use the `robust` theme, +but at a commit (in its history) that works with this quickstart. + +```bash +$ git clone https://github.com/dim0627/hugo_theme_robust.git +$ (cd hugo_theme_robust; git checkout b8ce466) +``` + +Leave the themes folder. + +```bash +$ cd .. +``` + + +Start the server again. + +```bash +$ hugo server --theme=hugo_theme_robust --buildDrafts +``` +``` +1 of 1 draft rendered +0 future content +1 pages created +2 paginator pages created +0 tags created +0 categories created +in 10 ms +Watching for changes in /Users/shekhargulati/bookshelf/{data,content,layouts,static,themes} +Serving pages from memory +Web Server is available at http://localhost:1313/ (bind address 127.0.0.1) +Press Ctrl+C to stop +``` + +> *Note: If Hugo doesn't find the specified theme in the `themes` directory, +it will throw an exception as shown below.* +``` +FATAL: 2016/02/14 Unable to find theme Directory: /Users/shekhargulati/bookshelf/themes/robust +``` + +To view your website, you can go to http://localhost:1313/. You will see as shown below. + +![](/img/quickstart/bookshelf-robust-theme.png) + +Let's understand the layout of the theme. A theme consists of the following: + +* **theme.toml** is the theme configuration file that gives information +about the theme like name and description of theme, +author details, and theme license. + +* **images** directory contains two images -- `screenshot.png` and `tn.png`. `screenshot.png` is the image of the list view and `tn.png` is the single post view. + +* **layouts** directory contains different views for different content types. +Every content type should have two files `single.html` and `list.html`. +`single.html` is used for rendering a single piece of content. +`list.html` is used to view a list of content items. +For example, you will use `list.html` to view all the posts +that have the `programming` tag. + +* **static** directory stores all the static assets used by the template. +Static assets could be JavaScript libraries like jQuery or CSS styles or images, +or any other static content. +This directory will be copied into the final site when rendered. + +## Step 6. Use multiple themes + +You can very easily test different layouts by switching between different themes. +Let's suppose we want to try out the `bleak` theme. +We clone the `bleak` theme inside the `bookshelf/themes` directory. + +```bash +$ git clone https://github.com/Zenithar/hugo-theme-bleak.git +``` + +Restart the server using `hugo-theme-bleak` as shown below. + +```bash +$ hugo server --theme=hugo-theme-bleak --buildDrafts +``` + +Now, the website will use the `bleak` theme +and will be rendered differently as shown below. + +![](/img/quickstart/bookshelf-bleak-theme.png) + +## Step 7. Update config.toml and live reloading in action + +Restart the server with the `robust` theme, as we will use it in this quickstart. + +```bash +$ hugo server --theme=hugo_theme_robust --buildDrafts +``` + +The website uses the dummy values specified in `bookshelf/config.toml`. +Let's update the configuration. + +```toml +baseURL = "http://example.org/" +languageCode = "en-us" +title = "Shekhar Gulati Book Reviews" + +[Params] + Author = "Shekhar Gulati" +``` + +Hugo has inbuilt support for live reloading. +So, as soon as you save your changes it will apply the change +and reload the web page. You will see the changes shown below. + +![](/img/quickstart/bookshelf-updated-config.png) + +The same is reflected in the Hugo server logs as well. +As soon as you changed the configuration file, +Hugo applied those changes to the affected pages. + +``` +Config file changed: /Users/shekhargulati/bookshelf/config.toml +1 of 1 draft rendered +0 future content +1 pages created +2 paginator pages created +0 tags created +0 categories created +in 11 ms +``` + +## Step 8. Customize robust theme + +The `robust` theme is a good start towards our online bookshelf but we want to +customize it a bit to meet the look and feel required for the bookshelf. +Hugo makes it very easy to customize themes. +You can also create your themes but we will not do that today. +If you want to create your own theme, then you should refer to +the [Hugo documentation]({{< relref "themes/creation.md" >}}). + +The first change that we have to make is to use a different default image +instead of the one used in the theme. +The theme's default image used in both the list and single view page resides +inside `themes/hugo_theme_robust/static/images/default.jpg`. +We can easily override it by creating a simple directory structure +inside the repository's `static` directory. + +Create an images directory inside the `bookshelf/static` directory +and copy an image with name `default.jpg` inside it. +We will use the default image shown below. + +![](/img/quickstart/default.jpg) + +Hugo will sync the changes and reload the website to use new image as shown below. + +![](/img/quickstart/bookshelf-new-default-image.png) + +Now, we need to change the layout of the index page so that only images are shown instead of the text. The index.html inside the layouts directory of the theme refer to partial `li` that renders the list view shown below. + +```html + +``` + +Create a new file li.html inside the `bookshelf/layouts/_default` directory. Copy the content shown below into the li.html. We have removed details of the book so that only image is shown. + +```html + +``` + +Now, the website will be rendered as shown below. + +![](/img/quickstart/bookshelf-only-picture.png) + +Next, we want to remove information related to theme from the footer. +So, create a new directory `partials` inside `bookshelf/layouts`. +There, create a new file `default_foot.html` with the content copied +from the theme's `layouts/partials/default_foot.html`. +Replace the footer section with the one shown below. + +```html +
    +

    {{ with .Site.Copyright | safeHTML }}{{ . }}{{ else }}© {{ $.Site.LastChange.Year }} {{ if isset $.Site.Params "Author" }}{{ $.Site.Params.Author }}{{ else }}{{ .Site.Title }}{{ end }}{{ end }}

    +

    Powered by Hugo,

    +
    +``` + +We also have to remove the sidebar on the right. +Copy the `index.html` from the theme's `layouts` directory to +the `bookshelf/layouts` directory. +Remove the section related to the sidebar from the HTML: + +```html +
    + {{ partial "sidebar.html" . }} +
    +``` + +So far we are using the default image but we would like to use the book image so that we can relate to the book. Every book review will define a configuration setting in its front matter. Update the `good-to-great.md` as shown below. + + +``` ++++ +date = "2016-02-14T16:11:58+05:30" +draft = true +title = "Good to Great Book Review" +image = "good-to-great.jpg" ++++ + +I read **Good to Great in January 2016**. An awesome read sharing detailed analysis on how good companies became great. Although this book is about how companies became great but we could apply a lot of the learnings on ourselves. Concepts like level 5 leader, hedgehog concept, the stockdale paradox are equally applicable to individuals. +``` + +Grab a (legal) image from somewhere, name it `good-to-great.jpg`, +and place it in the `bookshelf/static/images` directory. + + +After adding few more books to our shelf, the shelf appears as shown below. +These are a few of the books that I have read within the last year. + +![](/img/quickstart/bookshelf.png) + + +## Step 9. Make posts public + +So far all the posts that we have written are in draft status. +To make a draft public, you can either run a command +or manually change the draft status in the post to `false`. + +```bash +$ hugo undraft content/post/good-to-great.md +``` + +Now, you can start the server without the `buildDrafts` option. + +``` +$ hugo server --theme=hugo_theme_robust +``` + +## Step 10. Integrate Disqus + +Disqus allows you to integrate comments in your static blog. To enable Disqus, you just have to set `disqusShortname` in the config.toml as shown below. + +``` +[Params] + Author = "Shekhar Gulati" + disqusShortname = +``` + +Now, commenting will be enabled in your blog. + +![](/img/quickstart/bookshelf-disqus.png) + +## Step 11. Generate website + +To generate Hugo website source you can use +to deploy your website on GitHub pages, +first edit `bookshelf/config.toml`, changing the `baseURL` line to: + +``` +baseURL = "https://.github.io/bookshelf/" +``` + +Then type the following command. + +```bash +$ hugo --theme=hugo_theme_robust +``` +``` +0 draft content +0 future content +5 pages created +2 paginator pages created +0 tags created +0 categories created +in 17 ms +``` + +After you run the `hugo` command, a `bookshelf/public` directory +will be created containing the generated website source. + +BTW (in case you tried), +the website isn't properly accessible via the `file:///` protocol. + +## Step 12. Deploy bookshelf on GitHub pages + +Let's version control your bookshelf: + +```bash +$ git init +$ echo "/public/" >> .gitignore +$ echo "/themes/" >> .gitignore +$ git add --all +$ git commit -m "Initial commit" +``` + +Now the Git repositories under `bookshelf/themes` +won't conflict with your `bookshelf` repository, +and neither will a Git repository in `bookshelf/public`. + +Create a new repository on GitHub named `bookshelf` (without a README). +Once that's done, create a new Git repository on your local system +in `bookshelf/public` and add remote: + +```bash +$ cd public +$ git init +$ git remote add origin git@github.com:/bookshelf.git +``` + +There, create and check out a new branch `gh-pages`. + +```bash +$ git checkout -b gh-pages +Switched to a new branch 'gh-pages' +``` + +Add all the files (within `bookshelf/public`) to the index, +commit them, and push the changes to GitHub. + +```bash +$ git add --all +$ git commit -m "bookshelf added" +$ git push -f origin gh-pages +``` + +In couple of minutes, your website will be live +at `https://.github.io/bookshelf/`. + +Anytime, you can regenerate your site with: + +```bash +$ (cd ..; hugo --theme=hugo_theme_robust) +$ git add --all +$ git commit -m "" +$ git push -f origin gh-pages +``` + +---- + +This quickstart was originally written by [Shekhar Gulati](https://twitter.com/shekhargulati) in his [52 Technologies in 2016](https://github.com/shekhargulati/52-technologies-in-2016) blog series. diff --git a/docs/content/overview/source-directory.md b/docs/content/overview/source-directory.md new file mode 100644 index 000000000..2d4ce10f4 --- /dev/null +++ b/docs/content/overview/source-directory.md @@ -0,0 +1,126 @@ +--- +aliases: +- /doc/source-directory/ +lastmod: 2015-02-09 +date: 2013-07-01 +menu: + main: + parent: getting started +next: /content/organization +notoc: true +prev: /overview/configuration +title: Source Organization +weight: 50 +--- + +Hugo takes a single directory and uses it as the input for creating a complete +website. + + +The top level of a source directory will typically have the following elements: + + ▸ archetypes/ + ▸ content/ + ▸ data/ + ▸ i18n/ + ▸ layouts/ + ▸ static/ + ▸ themes/ + config.toml + +Learn more about the different directories and what their purpose is: + +* [config]({{< relref "overview/configuration.md" >}}) +* [data]({{< relref "extras/datafiles.md" >}}) +* [i18n]({{< relref "content/multilingual.md#translation-of-strings" >}}) +* [archetypes]({{< relref "content/archetypes.md" >}}) +* [content]({{< relref "content/organization.md" >}}) +* [layouts]({{< relref "templates/overview.md" >}}) +* [static]({{< relref "themes/creation.md#static" >}}) +* [themes]({{< relref "themes/overview.md" >}}) + + +## Example + +An example directory may look like: + + . + ├── config.toml + ├── archetypes + | └── default.md + ├── content + | ├── post + | | ├── firstpost.md + | | └── secondpost.md + | └── quote + | | ├── first.md + | | └── second.md + ├── data + ├── i18n + ├── layouts + | ├── _default + | | ├── single.html + | | └── list.html + | ├── partials + | | ├── header.html + | | └── footer.html + | ├── taxonomy + | | ├── category.html + | | ├── post.html + | | ├── quote.html + | | └── tag.html + | ├── post + | | ├── li.html + | | ├── single.html + | | └── summary.html + | ├── quote + | | ├── li.html + | | ├── single.html + | | └── summary.html + | ├── shortcodes + | | ├── img.html + | | ├── vimeo.html + | | └── youtube.html + | ├── index.html + | └── sitemap.xml + ├── themes + | ├── hyde + | └── doc + └── static + ├── css + └── js + +This directory structure tells us a lot about this site: + +1. The website intends to have two different types of content: *posts* and *quotes*. +2. It will also apply two different taxonomies to that content: *categories* and *tags*. +3. It will be displaying content in 3 different views: a list, a summary and a full page view. + +## Content for home page and other list pages + +Since Hugo 0.18, "everything" is a `Page` that can have content and metadata, like `.Params`, attached to it -- and share the same set of [page variables](/templates/variables/). + +To add content and frontmatter to the home page, a section, a taxonomy or a taxonomy terms listing, add a markdown file with the base name `_index` on the relevant place on the file system. + +For the default Markdown content, the filename will be `_index.md`. + +Se the example directory tree below. + +**Note that you don't have to create `_index` file for every section, taxonomy and similar, a default page will be created if not present, but with no content and default values for `.Title` etc.** + +```bash +└── content + ├── _index.md + ├── categories + │   ├── _index.md + │   └── photo + │   └── _index.md + ├── post + │   ├── _index.md + │   └── firstpost.md + └── tags + ├── _index.md + └── hugo + └── _index.md +``` + diff --git a/docs/content/overview/usage.md b/docs/content/overview/usage.md new file mode 100644 index 000000000..d7c7f7772 --- /dev/null +++ b/docs/content/overview/usage.md @@ -0,0 +1,224 @@ +--- +aliases: +- /doc/usage/ +lastmod: 2016-08-19 +date: 2013-07-01 +menu: + main: + parent: getting started +next: /overview/configuration +notoc: true +prev: /overview/installing +title: Using Hugo +weight: 30 +--- + +Make sure Hugo is in your `PATH` (or provide a path to it). Test this by: + +{{< nohighlight >}}$ hugo help +hugo 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 http://gohugo.io/. + +Usage: + hugo [flags] + hugo [command] + +Available Commands: + benchmark Benchmark Hugo by building a site a number of times. + check Contains some verification checks + config Print the site configuration + convert Convert your content to different formats + env Print Hugo version and environment info + gen A collection of several useful generators. + help Help about any command + import Import your site from others. + list Listing out various types of content + new Create new content for your site + server A high performance webserver + undraft Undraft changes the content's draft status from 'True' to 'False' + version Print the version number of Hugo + +Flags: + -b, --baseURL string hostname (and path) to the root, e.g. http://spf13.com/ + -D, --buildDrafts include content marked as draft + -E, --buildExpired include expired content + -F, --buildFuture include content with publishdate in the future + --cacheDir string filesystem path to cache directory. Defaults: $TMPDIR/hugo_cache/ + --canonifyURLs if true, all relative URLs will be canonicalized using baseURL + --cleanDestinationDir remove files from destination not found in static directories + --config string config file (default is path/config.yaml|json|toml) + -c, --contentDir string filesystem path to content directory + -d, --destination string filesystem path to write files to + --disable404 do not render 404 page + --disableKinds stringSlice disable different kind of pages (home, RSS etc.) + --disableRSS do not build RSS files + --disableSitemap do not build Sitemap file + --enableGitInfo add Git revision, date and author info to the pages + --forceSyncStatic copy all files when static is changed. + -h, --help help for hugo + --i18n-warnings print missing translations + --ignoreCache ignores the cache directory + -l, --layoutDir string filesystem path to layout directory + --log enable Logging + --logFile string log File path (if set, logging enabled automatically) + --noChmod don't sync permission mode of files + --noTimes don't sync modification time of files + --pluralizeListTitles pluralize titles in lists using inflect (default true) + --preserveTaxonomyNames preserve taxonomy names as written ("Gérard Depardieu" vs "gerard-depardieu") + --quiet build in quiet mode + --renderToMemory render to memory (only useful for benchmark testing) + -s, --source string filesystem path to read files relative from + --stepAnalysis display memory and timing of different steps of the program + -t, --theme string theme to use (located in /themes/THEMENAME/) + --themesDir string filesystem path to themes directory + --uglyURLs if true, use /filename.html instead of /filename/ + -v, --verbose verbose output + --verboseLog verbose logging + -w, --watch watch filesystem for changes and recreate as needed + +Use "hugo [command] --help" for more information about a command. +{{< /nohighlight >}} + +## Common Usage Example + +The most common use is probably to run `hugo` with your current directory being the input directory: + +{{< nohighlight >}}$ hugo +0 draft content +0 future content +99 pages created +0 paginator pages created +16 tags created +0 groups created +in 120 ms +{{< /nohighlight >}} + +This generates your web site to the `public/` directory, +ready to be deployed to your web server. + + +## Instant feedback as you develop your web site + +If you are working on things and want to see the changes immediately, by default +Hugo will watch the filesystem for changes, and rebuild your site as soon as a file is saved: + +{{< nohighlight >}}$ hugo -s ~/Code/hugo/docs +0 draft content +0 future content +99 pages created +0 paginator pages created +16 tags created +0 groups created +in 120 ms +Watching for changes in /Users/spf13/Code/hugo/docs/content +Press Ctrl+C to stop +{{< /nohighlight >}} + +Hugo can even run a server and create a site preview at the same time! +Hugo implements [LiveReload](/extras/livereload/) technology to automatically +reload any open pages in all JavaScript-enabled browsers, including mobile. +This is the easiest and most common way to develop a Hugo web site: + +{{< nohighlight >}}$ hugo server -ws ~/Code/hugo/docs +0 draft content +0 future content +99 pages created +0 paginator pages created +16 tags created +0 groups created +in 120 ms +Watching for changes in /Users/spf13/Code/hugo/docs/content +Serving pages from /Users/spf13/Code/hugo/docs/public +Web Server is available at http://localhost:1313/ +Press Ctrl+C to stop +{{< /nohighlight >}} + + +## Deploying your web site + +After running `hugo server` for local web development, +you need to do a final `hugo` run +**without the `server` part of the command** +to rebuild your site. +You may then **deploy your site** by copying the `public/` directory +(by FTP, SFTP, WebDAV, Rsync, `git push`, etc.) +to your production web server. + +Since Hugo generates a static website, your site can be hosted anywhere, +including [Heroku][], [GoDaddy][], [DreamHost][], [GitHub Pages][], +[Amazon S3][] with [CloudFront][], [Firebase Hosting][], +or any other cheap (or even free) static web hosting service. + +[Apache][], [nginx][], [IIS][]... Any web server software would do! + +[Apache]: http://httpd.apache.org/ "Apache HTTP Server" +[nginx]: http://nginx.org/ +[IIS]: http://www.iis.net/ +[Heroku]: https://www.heroku.com/ +[GoDaddy]: https://www.godaddy.com/ +[DreamHost]: http://www.dreamhost.com/ +[GitHub Pages]: https://pages.github.com/ +[GitLab]: https://about.gitlab.com +[Amazon S3]: http://aws.amazon.com/s3/ +[CloudFront]: http://aws.amazon.com/cloudfront/ "Amazon CloudFront" +[Firebase Hosting]: https://firebase.google.com/docs/hosting/ + +### A note about deployment + +Running `hugo` *does not* remove generated files before building. This means that you should delete your `public/` directory (or the directory you specified with `-d`/`--destination`) before running the `hugo` command, or you run the risk of the wrong files (e.g. drafts and/or future posts) being left in the generated site. + +An easy way to work around this is to use different directories for development and production. + +To start a server that builds draft content (helpful for editing), you can specify a different destination: the `dev/` dir. + +{{< nohighlight >}}$ hugo server -wDs ~/Code/hugo/docs -d dev +{{< /nohighlight >}} + +When the content is ready for publishing, use the default `public/` dir: + +{{< nohighlight >}}$ hugo -s ~/Code/hugo/docs +{{< /nohighlight >}} + +This prevents content you're not yet ready to share +from accidentally becoming available. + +### Alternatively, serve your web site with Hugo! + +Yes, that's right! Because Hugo is so blazingly fast both in web site creation +*and* in web serving (thanks to its concurrent and multi-threaded design and +its Go heritage), some users actually prefer using Hugo itself to serve their +web site *on their production server*! + +No other web server software (Apache, nginx, IIS...) is necessary. + +Here is the command: + +{{< nohighlight >}}$ hugo server --baseURL=http://yoursite.org/ \ + --port=80 \ + --appendPort=false \ + --bind=87.245.198.50 +{{< /nohighlight >}} + +Note the `bind` option, +which is the interface to which the server will bind +(defaults to `127.0.0.1`: +fine for most development use cases). +Some hosts, such as Amazon Web Services, +run NAT (network address translation); +sometimes it can be hard to figure out the actual IP address. +Using `--bind=0.0.0.0` will bind to all interfaces. + +This way, you may actually deploy just the source files, +and Hugo on your server will generate the resulting web site +on-the-fly and serve them at the same time. + +You may optionally add `--disableLiveReload=true` if you do not want +the JavaScript code for LiveReload to be added to your web pages. + +Interested? Here are some great tutorials contributed by Hugo users: + +* [hugo, syncthing](http://fredix.xyz/2014/10/hugo-syncthing/) (French) by Frédéric Logier (@fredix) diff --git a/docs/content/release-notes/0.20.3-relnotes.md b/docs/content/release-notes/0.20.3-relnotes.md new file mode 100644 index 000000000..d62740abc --- /dev/null +++ b/docs/content/release-notes/0.20.3-relnotes.md @@ -0,0 +1,23 @@ + +--- +date: 2017-04-24 +title: 0.20.3 +--- + + + +This is a bug-fix release with one important fix. But it also adds some harness around [GoReleaser](https://github.com/goreleaser/goreleaser) to automate the Hugo release process. Big thanks to [@caarlos0](https://github.com/caarlos0) for great and super-fast support fixing issues along the way. + +Hugo now has: + +* 16619+ [stars](https://github.com/gohugoio/hugo/stargazers) +* 458+ [contributors](https://github.com/gohugoio/hugo/graphs/contributors) +* 156+ [themes](http://themes.gohugo.io/) + +## Enhancement + +* Automate the Hugo release process [550eba64](https://github.com/gohugoio/hugo/commit/550eba64705725eb54fdb1042e0fb4dbf6f29fd0) [@bep](https://github.com/bep) [#3358](https://github.com/gohugoio/hugo/issues/3358) + +## Fix + +* Fix handling of zero-length files [9bf5c381](https://github.com/gohugoio/hugo/commit/9bf5c381b6b3e69d4d8dbfd7a40074ac44792bbf) [@bep](https://github.com/bep) [#3355](https://github.com/gohugoio/hugo/issues/3355) diff --git a/docs/content/release-notes/0.20.4-relnotes.md b/docs/content/release-notes/0.20.4-relnotes.md new file mode 100644 index 000000000..0065945fb --- /dev/null +++ b/docs/content/release-notes/0.20.4-relnotes.md @@ -0,0 +1,28 @@ + +--- +date: 2017-04-24T21:12:31+01:00 +title: 0.20.4 +--- + + + +This is the second bug-fix release of the day, fixing a couple of issues related to the new release scripts. + + +Hugo now has: + +* 16626+ [stars](https://github.com/gohugoio/hugo/stargazers) +* 457+ [contributors](https://github.com/gohugoio/hugo/graphs/contributors) +* 156+ [themes](http://themes.gohugo.io/) + +## Fixes + +* Fix statically linked binaries [275bcf56](https://github.com/gohugoio/hugo/commit/275bcf566c7cb72367d4423cf4810319311ff680) [@munnerz](https://github.com/munnerz) [#3382](https://github.com/gohugoio/hugo/issues/3382) +* Filename change in Hugo 0.20.3 binaries [#3385](https://github.com/gohugoio/hugo/issues/3385) +* Fix version calculation [cb3c6b6f](https://github.com/gohugoio/hugo/commit/cb3c6b6f7670f85189a4a3637e7132901d1ed6e9) [@bep](https://github.com/bep) + + + + + + diff --git a/docs/content/release-notes/0.20.5-relnotes.md b/docs/content/release-notes/0.20.5-relnotes.md new file mode 100644 index 000000000..15ecbc232 --- /dev/null +++ b/docs/content/release-notes/0.20.5-relnotes.md @@ -0,0 +1,9 @@ + +--- +date: 2017-04-25 +title: 0.20.5 +--- + + + +This is a bug-fix release which fixes the version number of `0.20.4` (which wrongly shows up as `0.21-DEV`) ([#3388](https://github.com/gohugoio/hugo/issues/3388)). diff --git a/docs/content/release-notes/0.20.6-relnotes.md b/docs/content/release-notes/0.20.6-relnotes.md new file mode 100644 index 000000000..b3c94b3dd --- /dev/null +++ b/docs/content/release-notes/0.20.6-relnotes.md @@ -0,0 +1,26 @@ + +--- +date: 2017-04-27 +title: 0.20.6 +--- + + + +There have been some [shouting on discuss.gohugo.io](https://discourse.gohugo.io/t/index-md-is-generated-in-subfolder-index-index-html-hugo-0-20/6338/15) about some broken sites after the release of Hugo `0.20`. This release reintroduces the old behaviour, making `/my-blog-post/index.md` work as expected. + +Hugo now has: + +* 16675+ [stars](https://github.com/gohugoio/hugo/stargazers) +* 456+ [contributors](https://github.com/gohugoio/hugo/graphs/contributors) +* 156+ [themes](http://themes.gohugo.io/) + +## Fixes + +* Avoid index.md in /index/index.html [#3396](https://github.com/gohugoio/hugo/issues/3396) +* Make missing GitInfo a WARNING [b30ca4be](https://github.com/gohugoio/hugo/commit/b30ca4bec811dbc17e9fd05925544db2b75e0e49) [@bep](https://github.com/bep) [#3376](https://github.com/gohugoio/hugo/issues/3376) +* Fix some of the fpm fields for deb [3bd1d057](https://github.com/gohugoio/hugo/commit/3bd1d0571d5f2f6bf0dc8f90a8adf2dbfcb2fdfd) [@anthonyfok](https://github.com/anthonyfok) + + + + + diff --git a/docs/content/release-notes/0.21-relnotes.md b/docs/content/release-notes/0.21-relnotes.md new file mode 100644 index 000000000..2f393d20f --- /dev/null +++ b/docs/content/release-notes/0.21-relnotes.md @@ -0,0 +1,106 @@ + +--- +date: 2017-05-22 +title: 0.21 +--- + + Hugo `0.21` brings full support for shortcodes per [Output Format](https://gohugo.io/extras/output-formats/) ([#3220](https://github.com/gohugoio/hugo/issues/3220)), the last vital piece of that puzzle. This is especially useful for `Google AMP` with its many custom media tags. + +This release represents **126 contributions by 29 contributors** to the main Hugo code base. Since last main release Hugo has **gained 850 stars and 7 additional themes**. + +Hugo now has: + +* 17156+ [stars](https://github.com/gohugoio/hugo/stargazers) +* 457+ [contributors](https://github.com/gohugoio/hugo/graphs/contributors) +* 163+ [themes](http://themes.gohugo.io/) + +[@bep](https://github.com/bep) leads the Hugo development with a significant amount of contributions, but also a big shoutout to [@moorereason](https://github.com/moorereason), [@bogem](https://github.com/bogem), and [@munnerz](https://github.com/munnerz) for their ongoing contributions. And as always a big thanks to [@digitalcraftsman](https://github.com/digitalcraftsman) for his relentless work on keeping the documentation and the themes site in pristine condition. + +## Other Highlights + +On a more technical side, [@moorereason](https://github.com/moorereason) and [@bep](https://github.com/bep) have introduced namespaces for Hugo's many template funcs ([#3042](https://github.com/gohugoio/hugo/issues/3042) ). There are so many now, and adding more into that big pile would be a sure path toward losing control. Now they are nicely categorised into namespaces with its own tests and examples, with an API that the documentation site can use to make sure it is correct and up-to-date. + +## Notes + +* The deprecated `.Extension`, `.Now` and `.TargetPath` will now `ERROR` [544f0a63](https://github.com/gohugoio/hugo/commit/544f0a6394b0e085d355e8217fc5bb3d96c12a98) [@bep](https://github.com/bep) +* The config settings and flags `disable404`, `disableRSS`, `disableSitemap`, `disableRobotsTXT` are now deprecated. Use `disableKinds`. [5794a265](https://github.com/gohugoio/hugo/commit/5794a265b41ffdeebfd8485eecf65cf4088d49d6) [@bep](https://github.com/bep) [#3345](https://github.com/gohugoio/hugo/issues/3345) + +## Enhancements + +### Templates + +* Log a WARNING on wrong usage of `IsSet` [38661c17](https://github.com/gohugoio/hugo/commit/38661c17bb8c31c9f31ee18f8eba5e3bfddd5574) [@moorereason](https://github.com/moorereason) [#3092](https://github.com/gohugoio/hugo/issues/3092) +* Add support for ellipsed paginator navigator, making paginators with lots of pages more compact [b6ea492b](https://github.com/gohugoio/hugo/commit/b6ea492b7a6325d04d44eeb00a990a3a0e29e0c0) [@bep](https://github.com/bep) [#3466](https://github.com/gohugoio/hugo/issues/3466) +* Add support for interfaces to `intersect` [f1c29b01](https://github.com/gohugoio/hugo/commit/f1c29b017bbd88e701cd5151dd186e868672ef89) [@moorereason](https://github.com/moorereason) [#1952](https://github.com/gohugoio/hugo/issues/1952) +* Add `NumFmt` function [93b3b138](https://github.com/gohugoio/hugo/commit/93b3b1386714999d716e03b131f77234248f1724) [@moorereason](https://github.com/moorereason) [#1444](https://github.com/gohugoio/hugo/issues/1444) +* Add template function namespaces [#3418](https://github.com/gohugoio/hugo/issues/3418) [#3042](https://github.com/gohugoio/hugo/issues/3042) [@moorereason](https://github.com/moorereason) [@bep](https://github.com/bep) +* Add translation links to the default sitemap template [90d3fbf1](https://github.com/gohugoio/hugo/commit/90d3fbf1da93a279cfe994a226ae82cf5441deab) [@rayjolt](https://github.com/rayjolt) [#2569](https://github.com/gohugoio/hugo/issues/2569) +* Allow text partials in HTML templates and the other way around [1cf29200](https://github.com/gohugoio/hugo/commit/1cf29200b4bb0a9c006155ec76759b7f4b1ad925) [@bep](https://github.com/bep) [#3273](https://github.com/gohugoio/hugo/issues/3273) + +### Output + +* Refactor site rendering with an "output format context". In this release, this is used for shortcode handling only, but this paves the way for future niceness [1e4d082c](https://github.com/gohugoio/hugo/commit/1e4d082cf5b92fedbc60b1b4f0e9d1ee6ec45e33) [@bep](https://github.com/bep) [#3397](https://github.com/gohugoio/hugo/issues/3397) [2bcbf104](https://github.com/gohugoio/hugo/commit/2bcbf104006e0ec03be4fd500f2519301d460f8c) [@bep](https://github.com/bep) [#3220](https://github.com/gohugoio/hugo/issues/3220) + + +### Core + +* Handle `shortcode` per `Output Format` [af72db80](https://github.com/gohugoio/hugo/commit/af72db806f2c1c0bf1dfe5832275c41eeba89906) [@bep](https://github.com/bep) [#3220](https://github.com/gohugoio/hugo/issues/3220) +* Improve shortcode error message [58d9cbd3](https://github.com/gohugoio/hugo/commit/58d9cbd31bcf7c296a39860fd7e566d10faaff28) [@bep](https://github.com/bep) +* Avoid `index.md` in `/index/index.html` [fea4fd86](https://github.com/gohugoio/hugo/commit/fea4fd86a324bf9679df23f8289887d91b42e919) [@bep](https://github.com/bep) [#3396](https://github.com/gohugoio/hugo/issues/3396) +* Make missing `GitInfo` a `WARNING` [5ad2f176](https://github.com/gohugoio/hugo/commit/5ad2f17693a9860be76ef8089c8728d2b59d6b04) [@bep](https://github.com/bep) [#3376](https://github.com/gohugoio/hugo/issues/3376) +* Prevent decoding `pageParam` in common cases [e98f885b](https://github.com/gohugoio/hugo/commit/e98f885b8af27f5473a89d31d0b1f02e61e8a5ec) [@bogem](https://github.com/bogem) +* Ignore non-source files on partial rebuild [b5b6e81c](https://github.com/gohugoio/hugo/commit/b5b6e81c0269abf9b0f4bc6a127744a25344e5c6) [@xofyarg](https://github.com/xofyarg) [#3325](https://github.com/gohugoio/hugo/issues/3325) +* Log `WARNING` only on unknown `/data` files [ab692e73](https://github.com/gohugoio/hugo/commit/ab692e73dea3ddfe979c88ee236cc394e47e82f1) [@bep](https://github.com/bep) [#3361](https://github.com/gohugoio/hugo/issues/3361) +* Avoid processing the same notify event twice [3b677594](https://github.com/gohugoio/hugo/commit/3b67759495c9268c30e6ba2d8c7e3b75d52d2960) [@bep](https://github.com/bep) +* Only show `rssURI` deprecation `WARNING` if it is actually set [cfd3af8e](https://github.com/gohugoio/hugo/commit/cfd3af8e691119461effa4385251b9d3818e2291) [@bep](https://github.com/bep) [#3319](https://github.com/gohugoio/hugo/issues/3319) + +### Docs + +* Add documentation on slug translation [635b3bb4](https://github.com/gohugoio/hugo/commit/635b3bb4eb873978c7d52e6c0cb85da0c4d25299) [@xavib](https://github.com/xavib) +* Replace `cdn.mathjax.org` with `cdnjs.cloudflare.com` [4b637ac0](https://github.com/gohugoio/hugo/commit/4b637ac041d17b22187f5ccd0f65461f0065aaa9) [@takuti](https://github.com/takuti) +* Add notes about some output format behaviour [162d3a58](https://github.com/gohugoio/hugo/commit/162d3a586d36cabf6376a76b096fd8b6414487ae) [@jpatters](https://github.com/jpatters) +* Add `txtpen` as alternative commenting service [7cdc244a](https://github.com/gohugoio/hugo/commit/7cdc244a72de4c08edc0008e37aec83d945dccdf) [@rickyhan](https://github.com/rickyhan) + +### Other + +* Embed `Page` in `WeightedPage` [ebf677a5](https://github.com/gohugoio/hugo/commit/ebf677a58360126d8b9a1e98d086aa4279f53181) [@bep](https://github.com/bep) [#3435](https://github.com/gohugoio/hugo/issues/3435) +* Improve the detection of untranslated strings [a40d1f6e](https://github.com/gohugoio/hugo/commit/a40d1f6ed2aedddc99725658993258cd557640ed) [@bogem](https://github.com/bogem) [#2607](https://github.com/gohugoio/hugo/issues/2607) +* Make first letter of the Hugo commands flags' usage lowercase [f0f69d03](https://github.com/gohugoio/hugo/commit/f0f69d03c551acb8ac2eeedaad579cf0b596f9ef) [@bogem](https://github.com/bogem) +* Import `Octopress` image tag in `Jekyll importer` [5f3ad1c3](https://github.com/gohugoio/hugo/commit/5f3ad1c31985450fab8d6772e9cbfcb57cf5cc53) [@buynov](https://github.com/buynov) + +## Fixes + +### Templates + +* Do not lower case template names [6d2ea0f7](https://github.com/gohugoio/hugo/commit/6d2ea0f7d7e8a54b8edfc36e52ff74266c30dc27) [@bep](https://github.com/bep) [#3333](https://github.com/gohugoio/hugo/issues/3333) + +### Output + +* Fix output format mixup in example [10287263](https://github.com/gohugoio/hugo/commit/10287263f529181d3169668b044cb84e2e3b049a) [@bep](https://github.com/bep) [#3481](https://github.com/gohugoio/hugo/issues/3481) +* Fix base theme vs project base template logic [077005e5](https://github.com/gohugoio/hugo/commit/077005e514b1ed50d84ceb90c7c72f184cb04521) [@bep](https://github.com/bep) [#3323](https://github.com/gohugoio/hugo/issues/3323) + +### Core +* Render `404` in default language only [154e18dd](https://github.com/gohugoio/hugo/commit/154e18ddb9ad205055d5bd4827c87f3f0daf499f) [@mitchchn](https://github.com/mitchchn) [#3075](https://github.com/gohugoio/hugo/issues/3075) +* Fix `RSSLink` vs `RSS` `Output Format` [e682fcc6](https://github.com/gohugoio/hugo/commit/e682fcc62233b47cf5bdcaf598ac0657ef089471) [@bep](https://github.com/bep) [#3450](https://github.com/gohugoio/hugo/issues/3450) +* Add default config for `ignoreFiles`, making that option work when running in server mode [42f4ce15](https://github.com/gohugoio/hugo/commit/42f4ce15a9d68053da36f9efcf7a7d975cc59559) [@chaseadamsio](https://github.com/chaseadamsio) +* Fix output formats override when no outputs definition given [6e2f2dd8](https://github.com/gohugoio/hugo/commit/6e2f2dd8d3ca61c92a2ee8824fbf05cadef08425) [@bep](https://github.com/bep) [#3447](https://github.com/gohugoio/hugo/issues/3447) +* Fix handling of zero-length files [0e87b18b](https://github.com/gohugoio/hugo/commit/0e87b18b66d2c8ba9e2abc429630cb03f5b093d6) [@bep](https://github.com/bep) [#3355](https://github.com/gohugoio/hugo/issues/3355) +* Must recreate `Paginator` on live-reload [45c74526](https://github.com/gohugoio/hugo/commit/45c74526686f6a2afa02bcee767d837d6b9dd028) [@bep](https://github.com/bep) [#3315](https://github.com/gohugoio/hugo/issues/3315) + +### Docs + +* Fix incorrect path in `templates/list` [27e88154](https://github.com/gohugoio/hugo/commit/27e88154af2dd9af6d0523d6e67b612e6336f91c) [@MunifTanjim](https://github.com/MunifTanjim) +* Fixed incorrect specification of directory structure [a28fbca6](https://github.com/gohugoio/hugo/commit/a28fbca6dcfa80b6541f5ef6c8c12cd1804ae9ed) [@TejasQ](https://github.com/TejasQ) +* Fix `bash` command in `tutorials/github-pages-blog` [c9976155](https://github.com/gohugoio/hugo/commit/c99761555c014e4d041438d5d7e53a6cbaee4492) [@hansott](https://github.com/hansott) +* Fix `.Data.Pages` range in example [b5e32eb6](https://github.com/gohugoio/hugo/commit/b5e32eb60993b4656918af2c959ae217a68c461e) [@hxlnt](https://github.com/hxlnt) + +### Other + +* Fix data race in live-reload close, avoiding some rare panics [355736ec](https://github.com/gohugoio/hugo/commit/355736ec357c81dfb2eb6851ee019d407090c5ec) [@bep](https://github.com/bep) [#2625](https://github.com/gohugoio/hugo/issues/2625) +* Skip `.git` directories in file scan [94b5be67](https://github.com/gohugoio/hugo/commit/94b5be67fc73b87d114d94a7bb1a33ab997f30f1) [@bogem](https://github.com/bogem) [#3468](https://github.com/gohugoio/hugo/issues/3468) + + + + + + diff --git a/docs/content/release-notes/0.22-relnotes.md b/docs/content/release-notes/0.22-relnotes.md new file mode 100644 index 000000000..f5250062e --- /dev/null +++ b/docs/content/release-notes/0.22-relnotes.md @@ -0,0 +1,85 @@ + +--- +date: 2017-06-12 +title: 0.22 +--- + + +Hugo `0.22` brings **nested sections**, by popular demand and a long sought after feature ([#465](https://github.com/gohugoio/hugo/issues/465)). We are still low on documentation for this great feature, but [@bep](https://github.com/bep) has been kind enough to accompany his implementation with a [demo site](http://hugotest.bep.is/). + +This release represents **58 contributions by 10 contributors** to the main Hugo code base. Since last release Hugo has **gained 420 stars and 2 additional themes.** + +[@bep](https://github.com/bep) still leads the Hugo development with his witty Norwegian humor, and once again contributed a significant amount of additions. But also a big shoutout to [@bogem](https://github.com/bogem), [@moorereason](https://github.com/moorereason), and [@onedrawingperday](https://github.com/onedrawingperday) for their ongoing contributions. And as always big thanks to [@digitalcraftsman](https://github.com/digitalcraftsman) for his relentless work on keeping the documentation and the themes site in pristine condition. + +Hugo now has: + +* 17576+ [stars](https://github.com/gohugoio/hugo/stargazers) +* 455+ [contributors](https://github.com/gohugoio/hugo/graphs/contributors) +* 165+ [themes](http://themes.gohugo.io/) + +## Other Highlights + +`.Site.GetPage` can now also be used to get regular pages ([#2844](https://github.com/gohugoio/hugo/issues/2844)): + +```go +{{ (.Site.GetPage "page" "blog" "mypost.md" ).Title }} +``` + +Also, considerable work has been put into writing automated benchmark tests for the site builds, and we're happy to report that although this release comes with fundamental structural changes, this version is -- in general -- even faster than the previous. It’s quite a challenge to consistently add significant new functionality and simultaneously maintain the stellar performance Hugo is famous for. + + + +## Notes + +`.Site.Sections` is replaced. We have reworked how sections work in Hugo, they can now be nested and are no longer taxonomies. If you use the old collection, you should get detailed upgrade instructions in the log when you run `hugo`. For more information, see this [demo site](http://hugotest.bep.is/). + +## Enhancements + +### Templates + +* Add `uint` support to `In` [b82cd82f](https://github.com/gohugoio/hugo/commit/b82cd82f1198a371ed94bda7faafe22813f4cb29) [@moorereason](https://github.com/moorereason) +* Support interfaces in `union` [204c3a9e](https://github.com/gohugoio/hugo/commit/204c3a9e32fcf6617ede978e35d3e2e89a5b491c) [@moorereason](https://github.com/moorereason) [#3411](https://github.com/gohugoio/hugo/issues/3411) +* Add `uniq` function [e28d9aa4](https://github.com/gohugoio/hugo/commit/e28d9aa42c3429d22fe254e69e4605aaf1e684f3) [@adiabatic](https://github.com/adiabatic) +* Handle `template.HTML` and friends in `ToInt` [4113693a](https://github.com/gohugoio/hugo/commit/4113693ac1b275f3a40aa5c248269340ef9b57f6) [@moorereason](https://github.com/moorereason) [#3308](https://github.com/gohugoio/hugo/issues/3308) + + +### Core + +* Make the `RSS feed` use the date for the node it represents [f1da5a15](https://github.com/gohugoio/hugo/commit/f1da5a15a37666ee59350d6600a8c14c1383f5bc) [@bep](https://github.com/bep) [#2708](https://github.com/gohugoio/hugo/issues/2708) +* Enable `nested sections` [b3968939](https://github.com/gohugoio/hugo/commit/b39689393ccb8434d9a57658a64b77568c718e99) [@bep](https://github.com/bep) [#465](https://github.com/gohugoio/hugo/issues/465) +* Add test for "no 404" in `sitemap` [8aaec644](https://github.com/gohugoio/hugo/commit/8aaec644a90d09bd7f079d35d382f76bb4ed35db) [@bep](https://github.com/bep) [#3563](https://github.com/gohugoio/hugo/issues/3563) +* Support regular pages in `.Site.GetPage` [e0c2e798](https://github.com/gohugoio/hugo/commit/e0c2e798201f75ae6e9a81a7442355288c2d141b) [@bep](https://github.com/bep) [#2844](https://github.com/gohugoio/hugo/issues/2844) +[#3082](https://github.com/gohugoio/hugo/issues/3082) + +### Performance +* Add site building benchmarks [8930e259](https://github.com/gohugoio/hugo/commit/8930e259d78cba4041b550cc51a7f40bc91d7c20) [@bep](https://github.com/bep) [#3535](https://github.com/gohugoio/hugo/issues/3535) +* Add a cache to `GetPage` which makes it much faster [50d11138](https://github.com/gohugoio/hugo/commit/50d11138f3e18b545c15fadf52f7b0b744bf3e7c) [@bep](https://github.com/bep) +* Speed up `GetPage` [fbb78b89](https://github.com/gohugoio/hugo/commit/fbb78b89df8ccef8f0ab26af00aa45d35c1ee2cf) [@bep](https://github.com/bep) [#3503](https://github.com/gohugoio/hugo/issues/3503) +* Add BenchmarkFrontmatterTags [3d9c4f51](https://github.com/gohugoio/hugo/commit/3d9c4f513b0443648d7e88995e351df1739646d2) [@bep](https://github.com/bep) [#3464](https://github.com/gohugoio/hugo/issues/3464) +* Add `benchSite.sh` to make it easy to run Hugo performance benchmarks [d74452cf](https://github.com/gohugoio/hugo/commit/d74452cfe8f69a85ec83e05481e16bebf199a5cb) [@bep](https://github.com/bep) +* Cache language config [4aff2b6e](https://github.com/gohugoio/hugo/commit/4aff2b6e7409a308f30cff1825fec02991e0d56a) [@bep](https://github.com/bep) +* Temporarily revert to BurntSushi for `TOML` front matter handling; it is currently much faster [0907a5c1](https://github.com/gohugoio/hugo/commit/0907a5c1c293755e6bf297246f07888448d81f8b) [@bep](https://github.com/bep) [#3541](https://github.com/gohugoio/hugo/issues/3541) [#3464](https://github.com/gohugoio/hugo/issues/3464) +* Add a simple partitioned lazy cache [87203139](https://github.com/gohugoio/hugo/commit/87203139c38e0b992c96d7b8a23c7730649c68e5) [@bep](https://github.com/bep) + +### Other + +* Add `noindex` tag to HTML generated by Hugo aliases [d5ab7f08](https://github.com/gohugoio/hugo/commit/d5ab7f087d967b30e7de7d789e6ad3091b42f1f7) [@onedrawingperday](https://github.com/onedrawingperday) +* Update Go versions [bde807bd](https://github.com/gohugoio/hugo/commit/bde807bd1e560fb4cc765c0fc22132db7f8a0801) [@bep](https://github.com/bep) +* Remove the `rlimit` tweaking on `macOS` [bcd32f10](https://github.com/gohugoio/hugo/commit/bcd32f1086c8c604fb22a7496924e41cc46b1605) [@bep](https://github.com/bep) [#3512](https://github.com/gohugoio/hugo/issues/3512) + +### Docs +* Rewrite “Archetypes” article [@davidturnbull](https://github.com/davidturnbull) [#3543](https://github.com/gohugoio/hugo/pull/3543/) +* Remove Unmaintaned Frontends from Tools. [f41f7282](https://github.com/gohugoio/hugo/commit/f41f72822251c9a31031fd5b3dda585c57c8b028) [@onedrawingperday](https://github.com/onedrawingperday) + +## Fixes + +### Core +* Improve `live-reload` on directory structure changes making removal of directories or pasting new content directories into  `/content` just work [fe901b81](https://github.com/gohugoio/hugo/commit/fe901b81191860b60e6fcb29f8ebf87baef2ee79) [@bep](https://github.com/bep) [#3570](https://github.com/gohugoio/hugo/issues/3570) +* Respect `disableKinds=["sitemap"]` [69d92dc4](https://github.com/gohugoio/hugo/commit/69d92dc49cb8ab9276ab013d427ba2d9aaf9135d) [@bep](https://github.com/bep) [#3544](https://github.com/gohugoio/hugo/issues/3544) +* Fix `disablePathToLower` regression [5be04486](https://github.com/gohugoio/hugo/commit/5be0448635fdf5fe6b1ee673e869f2b9baf1a5c6) [@bep](https://github.com/bep) [#3374](https://github.com/gohugoio/hugo/issues/3374) +* Fix `ref`/`relref` issue with duplicate base filenames [612f6e3a](https://github.com/gohugoio/hugo/commit/612f6e3afe0510c31f70f3621f3dc8ba609dade4) [@bep](https://github.com/bep) [#2507](https://github.com/gohugoio/hugo/issues/2507) + +### Docs + +* Fix parameter name in `YouTube` shortcode section [37e37877](https://github.com/gohugoio/hugo/commit/37e378773fbc127863f2b7a389d5ce3a14674c73) [@zivbk1](https://github.com/zivbk1) + diff --git a/docs/content/release-notes/0.22.1-relnotes.md b/docs/content/release-notes/0.22.1-relnotes.md new file mode 100644 index 000000000..c0a7dd453 --- /dev/null +++ b/docs/content/release-notes/0.22.1-relnotes.md @@ -0,0 +1,40 @@ + +--- +date: 2017-06-13 +title: 0.22.1 +--- + + + +Hugo `0.22.1` fixes a couple of issues reported after the [0.22 release](https://github.com/gohugoio/hugo/releases/tag/v0.22) Monday. Most importantly a fix for detecting regular subfolders below the root-sections. + +Also, we forgot to adapt the `permalink settings` with support for nested sections, which made that feature less useful than it could be. + +With this release you can configure **permalinks with sections** like this: + +**First level only:** + +```toml +[permalinks] +blog = ":section/:title" +``` + +**Nested (all levels):** + +```toml +[permalinks] +blog = ":sections/:title" +``` +## Fixes + +* Fix section logic for root folders with subfolders [a30023f5](https://github.com/gohugoio/hugo/commit/a30023f5cbafd06034807255181a5b7b17f3c25f) [@bep](https://github.com/bep) [#3586](https://github.com/gohugoio/hugo/issues/3586) +* Support sub-sections in permalink settings [1f26420d](https://github.com/gohugoio/hugo/commit/1f26420d392a5ab4c7b7fe1911c0268b45d01ab8) [@bep](https://github.com/bep) [#3580](https://github.com/gohugoio/hugo/issues/3580) +* Adjust rlimit to 64000 [ff54b6bd](https://github.com/gohugoio/hugo/commit/ff54b6bddcefab45339d8dc2b13776b92bdc04b9) [@bep](https://github.com/bep) [#3582](https://github.com/gohugoio/hugo/issues/3582) +* Make error on setting rlimit a warning only [629e1439](https://github.com/gohugoio/hugo/commit/629e1439e819a7118ae483381d4634f16d3474dd) [@bep](https://github.com/bep) [#3582](https://github.com/gohugoio/hugo/issues/3582) +* Revert: Remove the rlimit tweaking on macOS" [26aa06a3](https://github.com/gohugoio/hugo/commit/26aa06a3db57ab7134a900d641fa2976f7971520) [@bep](https://github.com/bep) [#3582](https://github.com/gohugoio/hugo/issues/3582) + + + + + + diff --git a/docs/content/release-notes/0.23-relnotes.md b/docs/content/release-notes/0.23-relnotes.md new file mode 100644 index 000000000..aa940d4b9 --- /dev/null +++ b/docs/content/release-notes/0.23-relnotes.md @@ -0,0 +1,52 @@ + +--- +date: 2017-06-16 +title: 0.23 +--- + + +Hugo `0.23` is mainly a release that handles all the small changes needed to get Hugo moved to a GitHub organisation: [gohugoio](https://github.com/gohugoio), but it also contains a couple of important fixes that makes this an update worth-while for all. + +Hugo now has: + +* 17739+ [stars](https://github.com/gohugoio/hugo/stargazers) +* 494+ [contributors](https://github.com/gohugoio/hugo/graphs/contributors) +* 165+ [themes](http://themes.gohugo.io/) + +## Fixes + +* Fix handling of duplicate footnotes [a9e551a1](https://github.com/gohugoio/hugo/commit/a9e551a100e60a603210ee083103dd73369d6a98) [@bep](https://github.com/bep) [#1912](https://github.com/gohugoio/hugo/issues/1912) +* Add support for spaces in project folder for `GitInfo` #3533 #3552 + +## GitHub organisation related changes + +* Update layout references to gohugoio/hugo [66d4850b](https://github.com/gohugoio/hugo/commit/66d4850b89db293dc58e828de784037f06c6c8dc) [@bep](https://github.com/bep) +* Update content references to gohugoio/hugo [715ff1f8](https://github.com/gohugoio/hugo/commit/715ff1f87406edf27738c8c0f52fe185fa974ee8) [@bep](https://github.com/bep) +* Add note on updates for rpm-based distros [52a0cea6](https://github.com/gohugoio/hugo/commit/52a0cea65de7b75ae1662abe3dec36fca3604617) [@daftaupe](https://github.com/daftaupe) +* Update logo link in README [ccb8300d](https://github.com/gohugoio/hugo/commit/ccb8300d380636d75a39f4133284eb0109e836c3) [@bep](https://github.com/bep) +* Remove docs building from CI builds [214dbdfb](https://github.com/gohugoio/hugo/commit/214dbdfb6f016d21415bc1ed511a37a084238878) [@bep](https://github.com/bep) +* Adjust docs path [729be807](https://github.com/gohugoio/hugo/commit/729be8074bddb58c9111f32c55cc769e49cd0d5a) [@bep](https://github.com/bep) +* Add docs as submodule [6cee0dfe](https://github.com/gohugoio/hugo/commit/6cee0dfe53899d433afc3c173a87d56265904cb0) [@bep](https://github.com/bep) +* Update Gitter link in README [fbb25014](https://github.com/gohugoio/hugo/commit/fbb25014e1306ce7127d53e5fc4fc49867790336) [@bep](https://github.com/bep) +* Change Windows build badge link, take #3 [86543d6a](https://github.com/gohugoio/hugo/commit/86543d6a50251b40540ebd0b851d45eb99d017c7) [@bep](https://github.com/bep) +* Update Windows build link [e6ae32a0](https://github.com/gohugoio/hugo/commit/e6ae32a0ba75b9894418227e87391defbb1b3b49) [@bep](https://github.com/bep) +* Update links in CONTRIBUTING.md due to the org transition [95386544](https://github.com/gohugoio/hugo/commit/95386544e858949a2baa414f395f30aaf66a6257) [@digitalcraftsman](https://github.com/digitalcraftsman) +* Update source path in Dockerfile due to the org transition [7b99fb9f](https://github.com/gohugoio/hugo/commit/7b99fb9f1ca8381457afe9d8e953a388b8ada182) [@digitalcraftsman](https://github.com/digitalcraftsman) +* Update clone folder in appveyor.yml due to the org transition [d531d17b](https://github.com/gohugoio/hugo/commit/d531d17b3be0b14faf4934611e01ac3289e37835) [@digitalcraftsman](https://github.com/digitalcraftsman) +* Update import path in snapcraft.yaml due to the org transition [9266bf9d](https://github.com/gohugoio/hugo/commit/9266bf9d4c24592b875a7f6b92f761b4cea40879) [@digitalcraftsman](https://github.com/digitalcraftsman) +* Run gofmt to get imports in line vs gohugoio/hugo [873a6f18](https://github.com/gohugoio/hugo/commit/873a6f18851bcda79d562ff6c02e1109e8e31a88) [@bep](https://github.com/bep) +* Update Makefile vs gohugoio/hugo [f503d76a](https://github.com/gohugoio/hugo/commit/f503d76a3b2719bbb65ab9df5595d0dbc871fae9) [@bep](https://github.com/bep) +* Update README to point to gohugoio/hugo [93643860](https://github.com/gohugoio/hugo/commit/93643860c9db10c6c32176b17cc83f1c317279bd) [@bep](https://github.com/bep) +* Update examples to point to gohugoio/hugo [db46bcf8](https://github.com/gohugoio/hugo/commit/db46bcf82d060656d4bc731550e63ec9cf8576f2) [@bep](https://github.com/bep) +* Update textual references in Go source to point to gohugoio/hugo [c17ad675](https://github.com/gohugoio/hugo/commit/c17ad675e8fcdb2db40fc50816b8f016bc14294c) [@bep](https://github.com/bep) +* Update import paths to gohugoio/hugo [d8717cd4](https://github.com/gohugoio/hugo/commit/d8717cd4c74e80ea8e20adead9321412a2d76022) [@bep](https://github.com/bep) + + + + + + + + + + diff --git a/docs/content/release-notes/0.24-relnotes.md b/docs/content/release-notes/0.24-relnotes.md new file mode 100644 index 000000000..2f4c87912 --- /dev/null +++ b/docs/content/release-notes/0.24-relnotes.md @@ -0,0 +1,72 @@ + +--- +date: 2017-06-21 +title: 0.24 +--- + + +This is **The Revival of the Archetypes!** + +> "A feature that could be the name of the next Indiana Jones movie deserves its own release," says [@bep](https://github.com/bep). + +Hugo now handles the **archetype files as Go templates**. This means that the issues with sorting and lost comments are long gone. This also means that you will have to supply all values, including title and date. But this also opens up a lot of new windows. + +A fictional example for the section `newsletter` and the archetype file `archetypes/newsletter.md`: + +```markdown +--- +title: "{{ replace .TranslationBaseName "-" " " | title }}" +date: {{ .Date }} +draft: true +--- + +**Insert Lead paragraph here.** + + + +## New Cool Posts + +{{ range first 10 ( where .Site.RegularPages "Type" "cool" ) }} +* {{ .Title }} +{{ end }} +``` + +And then create a new post with: + +```bash +hugo new newsletter/the-latest-cool.stuff.md +``` + +**Note:** the site will only be built if the `.Site` is in use in the archetype file, and this can be time consuming for big sites. + +**Hot Tip:** If you set the `newContentEditor` configuration variable to an editor on your `PATH`, the newly created article will be opened. + +The above _newsletter type archetype_ illustrates the possibilities: The full Hugo `.Site` and all of Hugo's template funcs can be used in the archetype file. + +**Also, Hugo now supports archetype files for all content formats, not just markdown.** + +Hugo now has: + +* 17839+ [stars](https://github.com/gohugoio/hugo/stargazers) +* 493+ [contributors](https://github.com/gohugoio/hugo/graphs/contributors) +* 166+ [themes](http://themes.gohugo.io/) + +## Notes + +Archetype files now need to be complete, including `title` and `date`. + +## Enhancements + +* Support extension-less media types. The motivation behind this change is to support Netlify's `_redirects` files, so we can generate server-side redirects for the Hugo docs site. See [this commit](https://github.com/gohugoio/hugoDocs/commit/c1ab9894e8292e0a74c43bbca2263b1fb3840f9e) to see how we configured that. [0f40e1fa](https://github.com/gohugoio/hugo/commit/0f40e1fadfca2276f65adefa6d7d5d63aef9160a) [@bep](https://github.com/bep) [#3614](https://github.com/gohugoio/hugo/issues/3614) +* Add `disableAliases` [516e6c6d](https://github.com/gohugoio/hugo/commit/516e6c6dc5733cdaf985317d58eedbc6ec0ef2f7) [@bep](https://github.com/bep) [#3613](https://github.com/gohugoio/hugo/issues/3613) +* Support non-md files as archetype files [19f2e729](https://github.com/gohugoio/hugo/commit/19f2e729135af700c5d4aa06e7b3540e6d4847fd) [@bep](https://github.com/bep) [#3597](https://github.com/gohugoio/hugo/issues/3597) [#3618](https://github.com/gohugoio/hugo/issues/3618) +* Identify extension-less text types as text [c43b512b](https://github.com/gohugoio/hugo/commit/c43b512b4700f76ac77f12d632bb030c3a241393) [@bep](https://github.com/bep) [#3614](https://github.com/gohugoio/hugo/issues/3614) +* Add `.Site` to the archetype templates [662e12f3](https://github.com/gohugoio/hugo/commit/662e12f348a638a6fcc92a416ee7f7c2a7ef8792) [@bep](https://github.com/bep) [#1629](https://github.com/gohugoio/hugo/issues/1629) +* Use archetype template as-is as a Go template [422057f6](https://github.com/gohugoio/hugo/commit/422057f60709696bbbd1c38c9ead2bf114d47e31) [@bep](https://github.com/bep) [#452](https://github.com/gohugoio/hugo/issues/452) [#1629](https://github.com/gohugoio/hugo/issues/1629) +* Update links to new discuss URL [4aa12390](https://github.com/gohugoio/hugo/commit/4aa1239070bb9d4324d3582f3e809b702a59d3ac) [@bep](https://github.com/bep) + +## Fixes + +* Fix error handling for `JSON` front matter [fb53987a](https://github.com/gohugoio/hugo/commit/fb53987a4ff2acb9da8dec6ec7b11924d37352ce) [@bep](https://github.com/bep) [#3610](https://github.com/gohugoio/hugo/issues/3610) +* Fix handling of quoted brackets in `JSON` front matter [3183b9a2](https://github.com/gohugoio/hugo/commit/3183b9a29d8adac962fbc73f79b04542f4c4c55d) [@bep](https://github.com/bep) [#3511](https://github.com/gohugoio/hugo/issues/3511) + diff --git a/docs/content/release-notes/0.24.1-relnotes.md b/docs/content/release-notes/0.24.1-relnotes.md new file mode 100644 index 000000000..438c5c433 --- /dev/null +++ b/docs/content/release-notes/0.24.1-relnotes.md @@ -0,0 +1,23 @@ + +--- +date: 2017-06-24 +title: 0.24.1 +--- + + + +This release fixes some important **archetype-related regressions** from the recent Hugo 0.24-release. + +## Fixes + +* Fix archetype regression when no archetype file [4294dd8d](https://github.com/gohugoio/hugo/commit/4294dd8d9d22bd8107b7904d5389967da1f83f27) [@bep](https://github.com/bep) [#3626](https://github.com/gohugoio/hugo/issues/3626) +* Preserve shortcodes in archetype templates [b63e4ee1](https://github.com/gohugoio/hugo/commit/b63e4ee198c875b73a6a9af6bb809589785ed589) [@bep](https://github.com/bep) [#3623](https://github.com/gohugoio/hugo/issues/3623) +* Fix handling of timezones with positive UTC offset (e.g., +0800) in TOML [0744f81e](https://github.com/gohugoio/hugo/commit/0744f81ec00bb8888f59d6c8b5f57096e07e70b1) [@bep](https://github.com/bep) [#3628](https://github.com/gohugoio/hugo/issues/3628) + +## Enhancements + +* Create default archetype on new site [bfa336d9](https://github.com/gohugoio/hugo/commit/bfa336d96173377b9bbe2298dbd101f6a718c174) [@bep](https://github.com/bep) [#3626](https://github.com/gohugoio/hugo/issues/3626) + + + + diff --git a/docs/content/release-notes/0.25-relnotes.md b/docs/content/release-notes/0.25-relnotes.md new file mode 100644 index 000000000..8f4e47227 --- /dev/null +++ b/docs/content/release-notes/0.25-relnotes.md @@ -0,0 +1,78 @@ + +--- +date: 2017-07-07 +title: 0.25 +--- + + Hugo `0.25` is the **Kinder Surprise**: It automatically opens the page you're working on in the browser, it adds full `AND` and `OR` support in page queries, and you can now have templates per language. + +![Hugo Open on Save](https://cdn-standard5.discourse.org/uploads/gohugo/optimized/2X/6/622088d4a8eacaf62bbbaa27dab19d789e10fe09_1_690x345.gif "Hugo Open on Save") + +If you start with `hugo server --navigateToChanged`, Hugo will navigate to the relevant page on save (see animated GIF). This is extremely useful for site-wide edits. Another very useful feature in this version is the added support for `AND` (`intersect`) and `OR` (`union`) filters when combined with `where`. + +Example: + +```go +{{ $pages := where .Site.RegularPages "Type" "not in" (slice "page" "about") }} +{{ $pages := $pages | union (where .Site.RegularPages "Params.pinned" true) }} +{{ $pages := $pages | intersect (where .Site.RegularPages "Params.images" "!=" nil) }} +``` + +The above fetches regular pages not of `page` or `about` type unless they are pinned. And finally, we exclude all pages with no `images` set in Page params. + +This release represents **36 contributions by 12 contributors** to the main Hugo code base. [@bep](https://github.com/bep) still leads the Hugo development with his witty Norwegian humor, and once again contributed a significant amount of additions. But also a big shoutout to [@yihui](https://github.com/yihui), [@anthonyfok](https://github.com/anthonyfok), and [@kropp](https://github.com/kropp) for their ongoing contributions. And as always a big thanks to [@digitalcraftsman](https://github.com/digitalcraftsman) for his relentless work on keeping the documentation and the themes site in pristine condition. + +Hugo now has: + +* 18209+ [stars](https://github.com/gohugoio/hugo/stargazers) +* 455+ [contributors](https://github.com/gohugoio/hugo/graphs/contributors) +* 168+ [themes](http://themes.gohugo.io/) + +## Enhancements + +### Templates + +* Add `Pages` support to `intersect` (`AND`) and `union`(`ÒR`). This makes the `where` template func even more powerful. [ccdd08d5](https://github.com/gohugoio/hugo/commit/ccdd08d57ab64441e93d6861ae126b5faacdb92f) [@bep](https://github.com/bep) [#3174](https://github.com/gohugoio/hugo/issues/3174) +* Add `math.Log` function. This is very handy for creating tag clouds. [34c56677](https://github.com/gohugoio/hugo/commit/34c566773a1364077e1397daece85b22948dc721) [@artem-sidorenko](https://github.com/artem-sidorenko) +* Add `WebP` images support [8431c8d3](https://github.com/gohugoio/hugo/commit/8431c8d39d878c18c6b5463d9091a953608df10b) [@bep](https://github.com/bep) [#3529](https://github.com/gohugoio/hugo/issues/3529) +* Only show post's own keywords in schema.org [da72805a](https://github.com/gohugoio/hugo/commit/da72805a4304a57362e8e79a01cc145767b027c5) [@brunoamaral](https://github.com/brunoamaral) [#2635](https://github.com/gohugoio/hugo/issues/2635)[#2646](https://github.com/gohugoio/hugo/issues/2646) +* Simplify the `Disqus` template a little bit (#3655) [eccb0647](https://github.com/gohugoio/hugo/commit/eccb0647821e9db20ba9800da1b4861807cc5205) [@yihui](https://github.com/yihui) +* Improve the built-in Disqus template (#3639) [2e1e4934](https://github.com/gohugoio/hugo/commit/2e1e4934b60ce8081a7f3a79191ed204f3098481) [@yihui](https://github.com/yihui) + +### Output + +* Support templates per site/language. This is for both regular templates and shortcode templates. [aa6b1b9b](https://github.com/gohugoio/hugo/commit/aa6b1b9be7c9d7322333893b642aaf8c7a5f2c2e) [@bep](https://github.com/bep) [#3360](https://github.com/gohugoio/hugo/issues/3360) + +### Core + +* Extend the sections API [a1d260b4](https://github.com/gohugoio/hugo/commit/a1d260b41a6673adef679ec4e262c5f390432cf5) [@bep](https://github.com/bep) [#3591](https://github.com/gohugoio/hugo/issues/3591) +* Make `.Site.Sections` return the top level sections [dd9b1baa](https://github.com/gohugoio/hugo/commit/dd9b1baab0cb860a3eb32fd9043bac18cab3f9f0) [@bep](https://github.com/bep) [#3591](https://github.com/gohugoio/hugo/issues/3591) +* Render `404.html` for all languages [41805dca](https://github.com/gohugoio/hugo/commit/41805dca9e40e9b0952e04d06074e6fc91140495) [@mitchchn](https://github.com/mitchchn) [#3598](https://github.com/gohugoio/hugo/issues/3598) + +### Other + +* Support human-readable `YAML` boolean values in `undraft` [1039356e](https://github.com/gohugoio/hugo/commit/1039356edf747f044c989a5bc0e85d792341ed5d) [@kropp](https://github.com/kropp) +* `hugo import jekyll` support nested `_posts` directories [7ee1f25e](https://github.com/gohugoio/hugo/commit/7ee1f25e9ef3be8f99c171e8e7982f4f82c13e16) [@coderzh](https://github.com/coderzh) [#1890](https://github.com/gohugoio/hugo/issues/1890)[#1911](https://github.com/gohugoio/hugo/issues/1911) +* Update `Dockerfile` and add Docker optimizations [118f8f7c](https://github.com/gohugoio/hugo/commit/118f8f7cf22d756d8a894ff93551974a806f2155) [@ellerbrock](https://github.com/ellerbrock) +* Add Blackfriday `joinLines` extension support (#3574) [a5440496](https://github.com/gohugoio/hugo/commit/a54404968a4b36579797f2e7ff7f5eada94866d9) [@choueric](https://github.com/choueric) +* add `--initial-header-level=2` to rst2html (#3528) [bfce30d8](https://github.com/gohugoio/hugo/commit/bfce30d85972c27c27e8a2caac9db6315f813298) [@frankbraun](https://github.com/frankbraun) +* Support open "current content page" in browser [c825a731](https://github.com/gohugoio/hugo/commit/c825a7312131b4afa67ee90d593640dee3525d98) [@bep](https://github.com/bep) [#3643](https://github.com/gohugoio/hugo/issues/3643) +* Make `--navigateToChanged` more robust on Windows [30e14cc3](https://github.com/gohugoio/hugo/commit/30e14cc31678ddc204b082ab362f86b6b8063881) [@anthonyfok](https://github.com/anthonyfok) [#3645](https://github.com/gohugoio/hugo/issues/3645) +* Remove the docs submodule [31393f60](https://github.com/gohugoio/hugo/commit/31393f6024416ea1b2e61d1080dfd7104df36eda) [@bep](https://github.com/bep) [#3647](https://github.com/gohugoio/hugo/issues/3647) +* Use `example.com` as homepage for new theme [aff1ac32](https://github.com/gohugoio/hugo/commit/aff1ac3235b6c075d01f7237addf44fecdd36d82) [@anthonyfok](https://github.com/anthonyfok) + +## Fixes + +### Templates + +* Fix `in` function for JSON arrays [d12cf5a2](https://github.com/gohugoio/hugo/commit/d12cf5a25df00fa16c59f0b2ae282187a398214c) [@bep](https://github.com/bep) [#1468](https://github.com/gohugoio/hugo/issues/1468) + +### Other + +* Fix handling of `JSON` front matter with escaped quotes [e10e51a0](https://github.com/gohugoio/hugo/commit/e10e51a00827b9fdc1bee51439fef05afc529831) [@bep](https://github.com/bep) [#3661](https://github.com/gohugoio/hugo/issues/3661) +* Fix typo in code comment [56d82aa0](https://github.com/gohugoio/hugo/commit/56d82aa025f4d2edb1dc6315132cd7ab52df649a) [@dvic](https://github.com/dvic) + + + + + diff --git a/docs/content/release-notes/_index.md b/docs/content/release-notes/_index.md new file mode 100644 index 000000000..3b934c69d --- /dev/null +++ b/docs/content/release-notes/_index.md @@ -0,0 +1,8 @@ +--- +date: 2017-04-17 +aliases: +- /doc/release-notes/ +- /meta/release-notes/ +title: Release Notes +weight: 10 +--- diff --git a/docs/content/release-notes/release-notes.md b/docs/content/release-notes/release-notes.md new file mode 100644 index 000000000..7464de2ef --- /dev/null +++ b/docs/content/release-notes/release-notes.md @@ -0,0 +1,959 @@ +--- +aliases: +- /doc/release-notes/ +- /meta/release-notes/ +date: 2017-04-16 +title: Older Release Notes +--- +# **0.20.2** April 16th 2017 + +Hugo `0.20.2` adds support for plain text partials included into `HTML` templates. This was a side-effect of the big new [Custom Output Format](https://gohugo.io/extras/output-formats/) feature in `0.20`, and while the change was intentional and there was an ongoing discussion about fixing it in {{< gh 3273 >}}, it did break some themes. There were valid workarounds for these themes, but we might as well get it right. + +The most obvious use case for this is inline `CSS` styles, which you now can do without having to name your partials with a `html` suffix. + +A simple example: + +In `layouts/partials/mystyles.css`: + +```css +body { + background-color: {{ .Param "colors.main" }} +} +``` + +Then in `config.toml` (note that by using the `.Param` lookup func, we can override the color in a page's front matter if we want): + +```toml +[params] +[params.colors] +main = "green" +text = "blue" +``` + +And then in `layouts/partials/head.html` (or the partial used to include the head section into your layout): + +```html + + + +``` + +Of course, `0.20` also made it super-easy to create external `CSS` stylesheets based on your site and page configuration. A simple example: + +Add "CSS" to your home page's `outputs` list, create the template `/layouts/index.css` using Go template syntax for the dynamic parts, and then include it into your `HTML` template with: + +```html +{{ with .OutputFormats.Get "css" }} + +{{ end }}` +``` + + +# **0.20.1** April 13th 2017 +Hugo `0.20.1` is a bug fix release, fixing some important regressions introduced in `0.20` a couple of days ago: + +* Fix logic for base template in work dir vs in the theme {{< gh 3323 >}} +* camelCased templates (partials, shortcodes etc.) not found {{< gh 3333 >}} +* Live-reload fails with `_index.md` with paginator {{< gh 3315 >}} +* `rssURI` WARNING always shown {{< gh 3319 >}} + +See the [full list](https://github.com/gohugoio/hugo/milestone/16?closed=1). + +# **0.20** April 10th 2017 + +Hugo `0.20` introduces the powerful and long sought after feature [Custom Output Formats]({{< ref "extras/output-formats.md" >}}); Hugo isn't just that "static HTML with an added RSS feed" anymore. *Say hello* to calendars, e-book formats, Google AMP, and JSON search indexes, to name a few ({{< gh 2828 >}}). + +This release represents **over 180 contributions by over 30 contributors** to the main Hugo code base. Since last release Hugo has **gained 1100 stars, 20 new contributors and 5 additional themes.** + +Hugo now has: + +- 16300+ stars +- 495+ contributors +- 156+ themes + +{{< gh "@bep" >}} still leads the Hugo development with his witty Norwegian humor, and once again contributed a significant amount of additions. Also a big shoutout to {{< gh "@digitalcraftsman" >}} for his relentless work on keeping the documentation and the themes site in pristine condition, and {{< gh "@moorereason" >}} and {{< gh "@bogem" >}} for their ongoing contributions. + +## Other Highlights + +{{< gh "@bogem" >}} has also contributed TOML as an alternative and much simpler format for language/i18n files ({{< gh 3200 >}}). A feature you will appreciate when you start to work on larger translations. + +Also, there have been some important updates in the Emacs Org-mode handling: {{< gh "@chaseadamsio" >}} has fixed the newline-handling ({{< gh 3126 >}}) and {{< gh "@clockoon" >}} has added basic footnote support. + +Worth mentioning is also the ongoing work that {{< gh "@rdwatters" >}} and {{< gh "@budparr" >}} is doing to re-do the [gohugo.io](https://gohugo.io/) site, including a total restructuring and partial rewrite of the documentation. It is getting close to finished, and it looks fantastic! + +## Notes +* `RSS` description in the built-in template is changed from full `.Content` to `.Summary`. This is a somewhat breaking change, but is what most people expect from their RSS feeds. If you want full content, please provide your own RSS template. +* The deprecated `.RSSlink` is now removed. Use `.RSSLink`. +* `RSSUri` is deprecated and will be removed in a future Hugo version, replace it with an output format definition. +* The deprecated `.Site.GetParam` is now removed, use `.Site.Param`. +* Hugo does no longer append missing trailing slash to `baseURL` set as a command line parameter, making it consistent with how it behaves from site config. {{< gh 3262 >}} + +## Enhancements + +* Hugo `0.20` is built with Go 1.8.1. +* Add `.Site.Params.mainSections` that defaults to the section with the most pages. Plan is to get themes to use this instead of the hardcoded `blog` in `where` clauses. {{< gh 3206 >}} +* File extension is now configurable. {{< gh 320 >}} +* Impove `markdownify` template function performance. {{< gh 3292 >}} +* Add taxonomy terms' pages to `.Data.Pages` {{< gh 2826 >}} +* Change `RSS` description from full `.Content` to `.Summary`. +* Ignore "." dirs in `hugo --cleanDestinationDir` {{< gh 3202 >}} +* Allow `jekyll import` to accept both `2006-01-02` and `2006-1-2` date format {{< gh 2738 >}} +* Raise the default `rssLimit` {{< gh 3145 >}} +* Unify section list vs single template lookup order {{< gh 3116 >}} +* Allow `apply` to be used with the built-in Go template funcs `print`, `printf` and `println`. {{< gh 3139 >}} + +## Fixes +* Fix deadlock in `getJSON` {{< gh 3211 >}} +* Make sure empty terms pages are created. {{< gh 2977 >}} +* Fix base template lookup order for sections {{< gh 2995 >}} +* `URL` fixes: + * Fix pagination URLs with `baseURL` with sub-root and `canonifyUrls=false` {{< gh 1252 >}} + * Fix pagination URL for resources with "." in name {{< gh 2110 >}} {{< gh 2374 >}} {{< gh 1885 >}} + * Handle taxonomy names with period {{< gh 3169 >}} + * Handle `uglyURLs` ambiguity in `Permalink` {{< gh 3102 >}} + * Fix `Permalink` for language-roots wrong when `uglyURLs` is `true` {{< gh 3179 >}} + * Fix misc case issues for `URLs` {{< gh 1641 >}} + * Fix for taxonomies URLs when `uglyUrls=true` {{< gh 1989 >}} + * Fix empty `RSSLink` for list pages with content page. {{< gh 3131 >}} +* Correctly identify regular pages on the form "my_index_page.md" {{< gh 3234 >}} +* `Exit -1` on `ERROR` in global logger {{< gh 3239 >}} +* Document hugo `help command` {{< gh 2349 >}} +* Fix internal `Hugo` version handling for bug fix releases. {{< gh 3025 >}} +* Only return `RSSLink` for pages that actually have a RSS feed. {{< gh 1302 >}} + + +# **0.19** February 27th 2017 + +We're happy to announce the first release of Hugo in 2017. + +This release represents **over 180 contributions by over 50 contributors** to the main Hugo code base. Since last release Hugo has **gained 1450 stars, 35 new contributors and 15 additional themes.** + +Hugo now has: + +- 15200+ stars +- 470+ contributors +- 151+ themes + +Furthermore, Hugo has its own Twitter account ([@gohugoio](https://twitter.com/gohugoio)) where we share bite-sized news and themes from the Hugo community. + +{{< gh "@bep" >}} leads the Hugo development and once again contributed a significant amount of additions. Also a big shoutout to {{< gh "@chaseadamsio" >}} for the Emacs Org-mode support, {{< gh "@digitalcraftsman" >}} for his relentless work on keeping the documentation and the themes site in pristine condition, {{< gh "@fj" >}}for his work on revising the `params` handling in Hugo, and {{< gh "@moorereason" >}} and {{< gh "@bogem" >}} for their ongoing contributions. + +## Highlights +Hugo `0.19` brings native Emacs Org-mode content support ({{}}), big thanks to {{< gh "@chaseadamsio" >}}. + +Also, a considerably amount of work have been put into cleaning up the Hugo source code, in an issue titled [Refactor the globals out of site build](https://github.com/gohugoio/hugo/issues/2701). This is not immediately visible to the Hugo end user, but will speed up future development. + +Hugo `0.18` was bringing full-parallel page rendering, so workarounds depending on rendering order did not work anymore, and pages with duplicate target paths (common examples would be `/index.md` or `/about/index.md`) would now conflict with the home page or the section listing. + +With Hugo `0.19`, you can control this behaviour by turning off page types you do not want ({{}}). In its most extreme case, if you put the below setting in your `config.toml`, you will get **nothing!**: + +``` +disableKinds = ["page", "home", "section", "taxonomy", "taxonomyTerm", "RSS", "sitemap", "robotsTXT", "404"] +``` +## Other New Features + +* Add ability to sort pages by frontmatter parameters, enabling easy custom "top 10" page lists. {{}} +* Add `truncate` template function {{}} +* Add `now` function, which replaces the now deprecated `.Now` {{}} +* Make RSS item limit configurable {{}} + +## Enhancements +* Enhance `.Param` to permit arbitrarily nested parameter references {{}} +* Use `Page.Params` more consistently when adding metadata {{}} +* The `sectionPagesMenu` feature ("Section menu for the lazy blogger") is now integrated with the section content pages. {{}} +* Hugo `0.19` is compiled with Go 1.8! +* Make template funcs like `findRE` and friends more liberal in what argument types they accept {{}} {{}} +* Improve generation of OpenGraph date tags {{}} + +## Notes + +* `sourceRelativeLinks` is now deprecated and will be removed in Hugo `0.21` if no one is stepping up to the plate and fixes and maintains this feature. {{}} + +## Fixes +* Fix `.Site.LastChange` on sites where the default sort order is not chronological. {{}} +* Fix regression of `.Truncated` evaluation in manual summaries. {{}} +* Fix `preserveTaxonomyNames` regression {{}} +* Fix issue with taxonomies when only some have content page {{}} +* Fix instagram shortcode panic on invalid ID {{}} +* Fix subtle data race in `getJSON` {{}} +* Fix deadlock in cached partials {{}} +* Avoid double-encoding of paginator URLs {{}} +* Allow tilde in URLs {{}} +* Fix `.Site.Pages` handling on live reloads {{}} +* `UniqueID` now correctly uses the fill file path from the content root to calculate the hash, and is finally ... unique! +* Discard current language based on `.Lang()`, go get translations correct for paginated pages. {{}} +* Fix infinite loop in template AST handling for recursive templates {{}} +* Fix issue with watching when config loading fails {{}} +* Correctly flush the imageConfig on live-reload {{}} +* Fix parsing of TOML arrays in frontmatter {{}} + +## Docs +* Add tutorial "How to use Google Firebase to host a Hugo site" {{}} +* Improve documentation for menu rendering {{}} +* Revise GitHub Pages deployment tutorial {{}} + +# **0.18.1** December 30th 2016 + +Hugo 0.18.1 is a bug fix release fixing some issues introduced in Hugo 0.18: + +* Fix 32-bit binaries {{}} +* Fix issues with `preserveTaxonomyNames` {{}} +* Fix `.URL` for taxonomy pages when `uglyURLs=true` {{}} +* Fix `IsTranslated` and `Translations` for node pages {{}} +* Make template error messages more verbose {{}} + +# **0.18.0** December 19th 2016 + +Today, we're excited to release the much-anticipated Hugo 0.18! + +We're heading towards the end of the year 2016, and we can look back on three releases and a steady growing community around the project. +This release includes **over 220 contributions by nearly 50 contributors** to the main codebase. +Since the last release, Hugo has **gained 1750 stars and 27 additional themes**. + +Hugo now has: + +- 13750+ stars +- 408+ contributors +- 137+ themes + +{{< gh "@bep" >}} once again took the lead of Hugo and contributed a significant amount of additions. +Also a big shoutout to {{< gh "@digitalcraftsman" >}} for his relentless work on keeping the documentation and the themes site in pristine condition, +and also a big thanks to {{< gh "@moorereason" >}} and {{< gh "@bogem" >}} for their contributions. + +We wish you all a Merry Christmas and a Happy New Year.
    +*The Hugo team* + +## Highlights + +The primary new feature in Hugo 0.18 is that every piece of content is now a `Page` ({{}}). +This means that every page, including the home page, can have a content file with frontmatter. +Not only is this a much simpler model to understand, it is also faster and paved the way for several important new features: + +* Enable proper titles for Nodes {{}} +* Sitemap.xml should include nodes, as well as pages {{}} +* Document homepage content workaround {{}} +* Allow home page to be easily authored in markdown {{}} +* Minimalist website with homepage as content {{}} + +Hugo again continues its trend of each release being faster than the last. It's quite a challenge to consistently add significant new functionality and simultaneously dramatically improve performance. Running [this benchmark]( https://github.com/bep/hugo-benchmark) with [these sites](https://github.com/bep/hugo-benchmark/tree/master/sites) (renders to memory) shows about 60% reduction in time spent and 30% reduction in memory usage compared to Hugo 0.17. + +## Other New Features + +* Every `Page` now has a `Kind` property. Since everything is a `Page` now, the `Kind` is used to differentiate different kinds of pages. + Possible values are `page`, `home`, `section`, `taxonomy`, and `taxonomyTerm`. + (Internally, we also define `RSS`, `sitemap`, `robotsTXT`, and `404`, but those have no practical use for end users at the moment since they are not included in any collections.) +* Add a `GitInfo` object to `Page` if `enableGitInfo` is set. It then also sets `Lastmod` for the given `Page` to the author date provided by Git. {{}} +* Implement support for alias templates {{}} +* New template functions: + * Add `imageConfig` function {{}} + * Add `sha256` function {{}} + * Add `partialCached` template function {{}} +* Add shortcode to display Instagram images {{}} +* Add `noChmod` option to disable perm sync {{}} +* Add `quiet` build mode {{}} + +## Notices + +* `.Site.Pages` will now contain *several kinds of pages*, including regular pages, sections, taxonomies, and the home page. + If you want a specific kind of page, you can filter it with `where` and `Kind`. + `.Site.RegularPages` is a shortcut to the page collection you have been used to getting. +* `RSSlink` is now deprecated. Use `RSSLink` instead. + Note that in Hugo 0.17 both of them existed, so there is a fifty-fifty chance you will not have to do anything + (if you use a theme, the chance is close to 0), and `RSSlink` will still work for two Hugo versions. + +## Fixes + +* Revise the `base` template lookup logic so it now better matches the behavior of regular templates, making it easier to override the master templates from the theme {{}} +* Add workaround for `block` template crash. + Block templates are very useful, but there is a bug in Go 1.6 and 1.7 which makes the template rendering crash if you use the block template in more complex scenarios. + This is fixed in the upcoming Go 1.8, but Hugo adds a temporary workaround in Hugo 0.18. {{}} +* All the `Params` configurations are now case insensitive {{}} {{}} {{}} +* Make RawContent raw again {{}} +* Fix archetype title and date handling {{}} +* Fix TOML archetype parsing in `hugo new` {{}} +* Fix page sorting when weight is zero {{}} +* Fix page names that contain dot {{}} +* Fix RSS Title regression {{}} +* Handle ToC before handling shortcodes {{}} +* Only watch relevant themes dir {{}} +* Hugo new content creates TOML slices with closing bracket on new line {{}} + + +## Improvements + +* Add page information to error logging in rendering {{}} +* Deprecate `RSSlink` in favor of `RSSLink` +* Make benchmark command more useful {{}} +* Consolidate the `Param` methods {{}} +* Allow to set cache dir in config file +* Performance improvements: + * Avoid repeated Viper loads of `sectionPagesMenu` {{}} + * Avoid reading from Viper for path and URL funcs {{}} + * Add `partialCached` template function. This can be a significant performance boost if you have complex partials that does not need to be rerendered for every page. {{}} + +## Documentation Updates + +* Update roadmap {{}} +* Update multilingual example {{}} +* Add a "Deployment with rsync" tutorial page {{}} +* Refactor `/docs` to use the `block` keyword {{}} + + +## **0.17.0** October 7th 2016 + +Hugo is going global with our 0.17 release. We put a lot of thought into how we could extend Hugo +to support multilingual websites with the most simple and elegant experience. Hugo's multilingual +capabilities rival the best web and documentation software, but Hugo's experience is unmatched. +If you have a single language website, the simple Hugo experience you already love is unchanged. +Adding additional languages to your website is simple and straightforward. Hugo has been completely +internally rewritten to be multilingual aware with translation and internationalization features +embedded throughout Hugo. + +Hugo continues its trend of each release being faster than the last. It's quite a challenge to consistently add +significant new functionality and simultaneously dramatically improve performance. {{}} has made it +his personal mission to apply the Go mantra of "Enable more. Do less" to Hugo. Hugo's consistent improvement +is a testament to his brilliance and his dedication to his craft. Hugo additionally benefits from the +performance improvements from the Go team in the Go 1.7 release. + +This release represents **over 300 contributions by over 70 contributors** to +the main Hugo code base. Since last release Hugo has **gained 2000 stars, 50 new +contributors and 20 additional themes.** + +Hugo now has: + +* 12,000 stars on GitHub +* 370+ contributors +* 110+ themes + +{{}} continues to lead the project with the lionshare of contributions +and reviews. A special thanks to {{}} and {{}} for their +considerable work on multilingual support. + +A big welcome to newcomers {{}}, {{}} and +{{}} for their critical contributions. + +### Highlights + +**Multilingual Support:** +Hugo now supports multiple languages side-by-side. A single site can now have multiple languages rendered with +full support for translation and i18n. + +**Performance:** +Hugo is faster than ever! Hugo 0.17 is not only our fastest release, it's also the most efficient. +Hugo 0.17 is **nearly twice as fast as Hugo 0.16** and uses about 10% less memory. +This means that the same site will build in nearly half the time it took with Hugo 0.16. +For the first time Hugo sites are averaging well under 1ms per rendered content. + +**Docs overhaul:** +This release really focused on improving the documentation. [Gohugo.io](http://gohugo.io) is +more accurate and complete than ever. + +**Support for macOS Sierra** + +### New Features +* Multilingual support {{}} +* Allow content expiration {{}} +* New templates functions: + * `querify` function to generate query strings inside templates {{}} + * `htmlEscape` and `htmlUnescape` template functions {{}} + * `time` converts a timestamp string into a time.Time structure {{}} + +### Enhancements + +* Render the shortcodes as late as possible {{}} +* Remove unneeded casts in page.getParam {{}} +* Automatic page date fallback {{}} +* Enable safeHTMLAttr {{}} +* Add TODO list support for markdown {{}} +* Make absURL and relURL accept any type {{}} +* Suppress 'missing static' error {{}} +* Make summary, wordcount etc. more efficient {{}} +* Better error reporting in `hugo convert` {{}} +* Reproducible builds thanks to govendor {{}} + +### Fixes + +* Fix shortcode in markdown headers {{}} +* Explicitly bind livereload to hugo server port {{}} +* Fix Emojify for certain text patterns {{}} +* Normalize file name to NFC {{}} +* Ignore emacs temp files {{}} +* Handle symlink change event {{}} +* Fix panic when using URLize {{}} +* `hugo import jekyll`: Fixed target path location check {{}} +* Return all errors from casting in templates {{}} +* Fix paginator counter on x86-32 {{}} +* Fix half-broken self-closing shortcodes {{}} + +**** + +## **0.16.0** June 6th 2016 + +Hugo 0.16 is our best and biggest release ever. The Hugo community has +outdone itself with continued performance improvements, +[beautiful themes](http://themes.gohugo.io) for all types of sites from project +sites to documentation to blogs to portfolios, and increased stability. + +This release represents **over 550 contributions by over 110 contributors** to +the main Hugo code base. Since last release Hugo has **gained 3500 stars, 90 +contributors and 23 additional themes.** + +This release celebrates 3 years since {{< gh "@spf13" >}} wrote the first lines +of Hugo. During those 3 years Hugo has accomplished some major milestones +including... + +* 10,000+ stars on GitHub +* 320+ contributors +* 90+ themes +* 1000s of happy websites +* Many subprojects like {{< gh "@spf13/cobra">}}, {{< gh "@spf13/viper">}} and + {{< gh "@spf13/afero">}} which have experienced broad usage across the Go + ecosystem. + +{{< gh "@bep" >}} led the development of Hugo for the 3rd consecutive release +with nearly half of the contributions to 0.16 in addition to his considerable +contributions as lead maintainer. {{< gh "@anthonyfok" >}}, {{< gh +"@DigitalCraftsman" >}}, {{< gh "@MooreReason" >}} all made significant +contributions. A special thanks to {{< gh "@abourget " >}} for his considerable +work on multilingual support. Due to its broad impact we wanted to spend more +time testing it and it will be included in Hugo's next release. + +### Highlights + +**Partial Builds:** Prior to this release Hugo would always reread and rebuild +the entire site. This release introduces support for reactive site building +while watching (`hugo server`). Hugo will watch the filesystem for changes and +only re-read the changed files. Depending on the files change Hugo will +intelligently re-render only the needed portion of the site. Performance gains +depend on the operation performed and size of the site. In our testing build +times decreased anywhere from 10% to 99%. + +**Template Improvements:** Template improvements continue to be a mainstay of each Hugo release. Hugo 0.16 adds support for the new `block` keyword introduced in Go 1.6 -- think base templates with default sections -- as well as many new template functions. + +**Polish:** As Hugo matures releases will inevitably contain fewer huge new features. This release represents hundreds of small improvements across ever facet of Hugo which will make for a much better experience for all of our users. Worth mentioning here is the curious bug where live reloading didn't work in some editors on OS X, including the popular TextMate 2. This is now fixed. Oh, and now any error will exit with an error code, a big thing for automated deployments. + +### New Features +* Support reading configuration variables from the OS environment {{}} +* Add emoji support {{}} +* Add `themesDir` option to configuration {{}} +* Add support for Go 1.6 `block` keyword in templates {{}} +* Partial static sync {{}} +* Source file based relative linking (a la GitHub) {{}} +* Add `ByLastmod` sort function to pages. {{}} +* New templates functions: + * `readFile` {{}} + * `countwords` and `countrunes` {{}} + * `default` {{}} + * `hasPrefix` {{}} + * `humanize` {{}} + * `jsonify` {{}} + * `md5` and `sha1` {{}} + * `replaceRE` {{}} + * `findRE` {{}} + * `shuffle` {{}} + * `slice` {{}} + * `plainify` {{}} + +### Enhancements + +* Hugo now exits with error code on any error. This is a big thing for + automated deployments. {{}} +* Print error when `/index.html` is zero-length {{}} +* Enable dirname and filename bash autocompletion for more flags {{}} +* Improve error handling in commands {{}} +* Add sanity checks for `hugo import jekyll` {{}} +* Add description to `Page.Params` {{}} +* Add async version of Google Analytics internal template {{}} +* Add autostart option to YouTube shortcode {{}} +* Set Date and Lastmod for main home page {{}} +* Allow URL with extension in frontmatter {{}} +* Add list support in Scratch {{}} +* Add file option to gist shortcode {{}} +* Add config layout and content directory CLI options {{}} +* Add boolean value comparison to `where` template function {{}} +* Do not write to to cache when `ignoreCache` is set {{}} +* Add option to disable rendering of 404 page {{}} +* Mercurial is no longer needed to build Hugo {{}} +* Do not create `robots.txt` by default {{}} +* Disable syntax guessing for PygmentsCodeFences by default. To enable syntax + guessing again, add the following to your config file: + `PygmentsCodeFencesGuessSyntax = true` {{}} +* Make `ByCount` sort consistently {{}} +* Add `Scratch` to shortcode {{}} +* Add support for symbolic links for content, layout, static, theme {{}} +* Add '+' as one of the valid characters in URLs specified in the front matter + {{}} +* Make alias redirect output URLs relative when `RelativeURLs = true` {{}} +* Hugo injects meta generator tag on homepage if missing {{}} + +### Fixes +* Fix file change watcher for TextMate 2 and friends on OS X {{}} +* Make dynamic reloading of config file reliable on all platform {{}} +* Hugo now works on Linux/arm64 {{}} +* `plainIDAnchors` now defaults to `true` {{}} +* Win32 and ARM builds fixed {{}} +* Copy static dir files without theme's static dir {{}} +* Make `noTimes` command flag work {{}} +* Change most global CLI flags into local ones {{}} +* Remove transformation of menu URLs {{}} +* Do not fail on unknown Jekyll file {{}} +* Use absolute path when editing with editor {{}} +* Fix hugo server "Watching for changes" path display {{}} +* Do not strip special characters out of URLs {{}} +* Fix `RSSLink` when uglyURLs are enabled {{}} +* Get BaseURL from viper in server mode {{}} +* Fix shortcode handling in RST {{}} +* Use default sitemap configuration for homepage {{}} +* Exit if specific port is unavailable in server mode {{}} +* Fix regression in "section menus for lazy blogger" {{}} + +**** + +## **0.15.0** November 25, 2015 + +The v0.15.0 Hugo release brings a lot of polish to Hugo. Exactly 6 months after +the 0.14 release, Hugo has seen massive growth and changes. Most notably, this +is Hugo's first release under the Apache 2.0 license. With this license change +we hope to expand the great community around Hugo and make it easier for our +many users to contribute. This release represents over **377 contributions by +87 contributors** to the main Hugo repo and hundreds of improvements to the +libraries Hugo uses. Hugo also launched a [new theme +showcase](http://themes.gohugo.io) and participated in +[Hacktoberfest](https://hacktoberfest.digitalocean.com). + +Hugo now has: + +* 6700 (+2700) stars on GitHub +* 235 (+75) contributors +* 65 (+30) themes + + +**Template Improvements:** This release takes Hugo to a new level of speed and +usability. Considerable work has been done adding features and performance to +the template system which now has full support of Ace, Amber and Go Templates. + +**Hugo Import:** Have a Jekyll site, but dreaming of porting it to Hugo? This +release introduces a new `hugo import jekyll`command that makes this easier +than ever. + +**Performance Improvements:** Just when you thought Hugo couldn't get any faster, +Hugo continues to improve in speed while adding features. Notably Hugo 0.15 +introduces the ability to render and serve directly from memory resulting in +30%+ lower render times. + +Huge thanks to all who participated in this release. A special thanks to +{{< gh "@bep" >}} who led the development of Hugo this release again, +{{< gh "@anthonyfok" >}}, +{{< gh "@eparis" >}}, +{{< gh "@tatsushid" >}} and +{{< gh "@DigitalCraftsman" >}}. + + +### New features +* new `hugo import jekyll` command. {{< gh 1469 >}} +* The new `Param` convenience method on `Page` and `Node` can be used to get the most specific parameter value for a given key. {{< gh 1462 >}} +* Several new information elements have been added to `Page` and `Node`: + * `RuneCount`: The number of [runes](http://blog.golang.org/strings) in the content, excluding any whitespace. This may be a good alternative to `.WordCount` for Japanese and other CJK languages where a word-split by spaces makes no sense. {{< gh 1266 >}} + * `RawContent`: Raw Markdown as a string. One use case may be of embedding remarkjs.com slides. + * `IsHome`: tells the truth about whether you're on the home page or not. + +### Improvements +* `hugo server` now builds ~30%+ faster by rendering to memory instead of disk. To get the old behavior, start the server with `--renderToDisk=true`. +* Hugo now supports dynamic reloading of the config file when watching. +* We now use a custom-built `LazyFileReader` for reading file contents, which means we don't read media files in `/content` into memory anymore -- and file reading is now performed in parallel on multicore PCs. {{< gh 1181 >}} +* Hugo is now built with `Go 1.5` which, among many other improvements, have fixed the last known data race in Hugo. {{< gh 917 >}} +* Paginator now also supports page groups. {{< gh 1274 >}} +* Markdown improvements: + * Hugo now supports GitHub-flavoured markdown code fences for highlighting for `md`-files (Blackfriday rendered markdown) and `mmark` files (MMark rendered markdown). {{< gh 362 1258 >}} + * Several new Blackfriday options are added: + * Option to disable Blackfriday's `Smartypants`. + * Option for Blackfriday to open links in a new window/tab. {{< gh 1220 >}} + * Option to disable Blackfriday's LaTeX style dashes {{< gh 1231 >}} + * Definition lists extension support. +* `Scratch` now has built-in `map` support. +* We now fall back to `link title` for the default page sort. {{< gh 1299 >}} +* Some notable new configuration options: + * `IgnoreFiles` can be set with a list of Regular Expressions that matches files to be ignored during build. {{< gh 1189 >}} + * `PreserveTaxonomyNames`, when set to `true`, will preserve what you type as the taxonomy name both in the folders created and the taxonomy `key`, but it will be normalized for the URL. {{< gh 1180 >}} +* `hugo gen` can now generate man files, bash auto complete and markdown documentation +* Hugo will now make suggestions when a command is mistyped +* Shortcodes now have a boolean `.IsNamedParams` property. {{< gh 1597 >}} + +### New Template Features +* All template engines: + * The new `dict` function that could be used to pass maps into a template. {{< gh 1463 >}} + * The new `pluralize` and `singularize` template funcs. + * The new `base64Decode` and `base64Encode` template funcs. + * The `sort` template func now accepts field/key chaining arguments and pointer values. {{< gh 1330 >}} + * Several fixes for `slicestr` and `substr`, most importantly, they now have full `utf-8`-support. {{< gh 1190 1333 1347 >}} + * The new `last` template function allows the user to select the last `N` items of a slice. {{< gh 1148 >}} + * The new `after` func allows the user to select the items after the `Nth` item. {{< gh 1200 >}} + * Add `time.Time` type support to the `where`, `ge`, `gt`, `le`, and `lt` template functions. + * It is now possible to use constructs like `where Values ".Param.key" nil` to filter pages that doesn't have a particular parameter. {{< gh 1232 >}} + * `getJSON`/`getCSV`: Add retry on invalid content. {{< gh 1166 >}} + * The new `readDir` func lists local files. {{< gh 1204 >}} + * The new `safeJS` function allows the embedding of content into JavaScript contexts in Go templates. + * Get the main site RSS link from any page by accessing the `.Site.RSSLink` property. {{< gh 1566 >}} +* Ace templates: + * Base templates now also works in themes. {{< gh 1215 >}}. + * And now also on Windows. {{< gh 1178 >}} +* Full support for Amber templates including all template functions. +* A built-in template for Google Analytics. {{< gh 1505 >}} +* Hugo is now shipped with new built-in shortcodes: {{< gh 1576 >}} + * `youtube` for YouTube videos + * `vimeo` for Vimeo videos + * `gist` for GitHub gists + * `tweet` for Twitter Tweets + * `speakerdeck` for Speakerdeck slides + + +### Bugfixes +* Fix data races in page sorting and page reversal. These operations are now also cached. {{< gh 1293 >}} +* `page.HasMenuCurrent()` and `node.HasMenuCurrent()` now work correctly in multi-level nested menus. +* Support `Fish and Chips` style section titles. Previously, this would end up as `Fish And Chips`. Now, the first character is made toupper, but the rest are preserved as-is. {{< gh 1176 >}} +* Hugo now removes superfluous p-tags around shortcodes. {{< gh 1148 >}} + +### Notices +* `hugo server` will watch by default now. +* Some fields and methods were deprecated in `0.14`. These are now removed, so the error message isn't as friendly if you still use the old values. So please change: + * `getJson` to `getJSON`, `getCsv` to `getCSV`, `safeHtml` to + `safeHTML`, `safeCss` to `safeCSS`, `safeUrl` to `safeURL`, `Url` to `URL`, + `UrlPath` to `URLPath`, `BaseUrl` to `BaseURL`, `Recent` to `Pages`. + +### Known Issues + +Using the Hugo v0.15 32-bit Windows or ARM binary, running `hugo server` would crash or hang due to a [memory alignment issue](https://golang.org/pkg/sync/atomic/#pkg-note-BUG) in [Afero](https://github.com/spf13/afero). The bug was discovered shortly after the v0.15.0 release and has since been [fixed](https://github.com/spf13/afero/pull/23) by {{< gh "@tpng" >}}. If you encounter this bug, you may either compile Hugo v0.16-DEV from source, or use the following solution/workaround: + +* **64-bit Windows users: Please use [hugo_0.15_windows_amd64.zip](https://github.com/gohugoio/hugo/releases/download/v0.15/hugo_0.15_windows_amd64.zip)** (amd64 == x86-64). It is only the 32-bit hugo_0.15_windows_386.zip that crashes/hangs (see {{< gh 1621 >}} and {{< gh 1628 >}}). +* **32-bit Windows and ARM users: Please run `hugo server --renderToDisk` as a workaround** until Hugo v0.16 is released (see [“hugo server” returns runtime error on armhf](https://discourse.gohugo.io/t/hugo-server-returns-runtime-error-on-armhf/2293) and {{< gh 1716 >}}). + +---- + +## **0.14.0** May 25, 2015 + +The v0.14.0 Hugo release brings of the most demanded features to Hugo. The +foundation of Hugo is stabilizing nicely and a lot of polish has been added. +We’ve expanded support for additional content types with support for AsciiDoc, +Restructured Text, HTML and Markdown. Some of these types depend on external +libraries as there does not currently exist native support in Go. We’ve tried +to make the experience as seamless as possible. Look for more improvements here +in upcoming releases. + +A lot of work has been done to improve the user experience, with extra polish +to the Windows experience. Hugo errors are more helpful overall and Hugo now +can detect if it’s being run in Windows Explorer and provide additional +instructions to run it via the command prompt. + +The Hugo community continues to grow. Hugo has over 4000 stars on github, 165 +contributors, 35 themes and 1000s of happy users. It is now the 5th most +popular static site generator (by Stars) and has the 3rd largest contributor +community. + +This release represents over **240 contributions by 36 contributors** to the main +Hugo codebase. + +Big shout out to {{< gh "@bep" >}} who led the development of Hugo +this release, {{< gh "@anthonyfok" >}}, +{{< gh "@eparis" >}}, +{{< gh "@SchumacherFM" >}}, +{{< gh "@RickCogley" >}} & +{{< gh "@mdhender" >}} for their significant contributions +and {{< gh "@tatsushid" >}} for his continuous improvements +to the templates. Also a big thanks to all the theme creators. 11 new themes +have been added since last release and the [hugoThemes repo now has previews of +all of +them](https://github.com/gohugoio/hugoThemes/blob/master/README.md#theme-list). + +Hugo also depends on a lot of other great projects. A big thanks to all of our dependencies including: +[cobra](https://github.com/spf13/cobra), +[viper](https://github.com/spf13/viper), +[blackfriday](https://github.com/russross/blackfriday), +[pflag](https://github.com/spf13/pflag), +[HugoThemes](https://github.com/gohugoio/hugothemes), +[BurntSushi](https://github.com/BurntSushi/toml), +[goYaml](https://github.com/go-yaml/yaml/tree/v2), and the Go standard library. + +## New features +* Support for all file types in content directory. + * If dedicated file type handler isn’t found it will be copied to the destination. +* Add `AsciiDoc` support using external helpers. +* Add experimental support for [`Mmark`](https://github.com/miekg/mmark) markdown processor +* Bash autocomplete support via `genautocomplete` command +* Add section menu support for a [Section Menu for "the Lazy Blogger"]({{< relref "extras/menus.md#section-menu-for-the-lazy-blogger" >}}) +* Add support for `Ace` base templates +* Adding `RelativeURLs = true` to site config will now make all the relative URLs relative to the content root. +* New template functions: + * `getenv` + * The string functions `substr` and `slicestr` + * `seq`, a sequence generator very similar to its Gnu counterpart + * `absURL` and `relURL`, both of which takes the `BaseURL` setting into account + +## Improvements +* Highlighting with `Pygments` is now cached to disk -- expect a major speed boost if you use it! +* More Pygments highlighting options, including `line numbers` +* Show help information to Windows users who try to double click on `hugo.exe`. +* Add `bind` flag to `hugo server` to set the interface to which the server will bind +* Add support for `canonifyURLs` in `srcset` +* Add shortcode support for HTML (content) files +* Allow the same `shortcode` to be used with or without inline content +* Configurable RSS output filename + +## Bugfixes +* Fix panic with paginator and zero pages in result set. +* Fix crossrefs on Windows. +* Fix `eq` and `ne` template functions when used with a raw number combined with the result of `add`, `sub` etc. +* Fix paginator with uglyURLs +* Fix {{< gh 998 >}}, supporting UTF8 characters in Permalinks. + +## Notices +* To get variable and function names in line with the rest of the Go community, + a set of variable and function names has been deprecated: These will still + work in 0.14, but will be removed in 0.15. What to do should be obvious by + the build log; `getJson` to `getJSON`, `getCsv` to `getCSV`, `safeHtml` to + `safeHTML`, `safeCss` to `safeCSS`, `safeUrl` to `safeURL`, `Url` to `URL`, + `UrlPath` to `URLPath`, `BaseUrl` to `BaseURL`, `Recent` to `Pages`, + `Indexes` to `Taxonomies`. + + +---- + +## **0.13.0** Feb 21, 2015 + +The v0.13.0 release is the largest Hugo release to date. The release introduced +some long sought after features (pagination, sequencing, data loading, tons of +template improvements) as well as major internal improvements. In addition to +the code changes, the Hugo community has grown significantly and now has over +3000 stars on github, 134 contributors, 24 themes and 1000s of happy users. + +This release represents **448 contributions by 65 contributors** + +A special shout out to {{< gh "@bep" >}} and +{{< gh "@anthonyfok" >}} for their new role as Hugo +maintainers and their tremendous contributions this release. + +### New major features +* Support for [data files](/extras/datafiles/) in [YAML](http://yaml.org/), + [JSON](http://www.json.org/), or [TOML](https://github.com/toml-lang/toml) + located in the `data` directory ({{< gh 885 >}}) +* Support for [dynamic content](/extras/dynamiccontent/) by loading JSON & CSV + from remote sources via GetJson and GetCsv in short codes or other layout + files ({{< gh 748 >}}) +* [Pagination support](/extras/pagination/) for home page, sections and + taxonomies ({{< gh 750 >}}) +* Universal sequencing support + * A new, generic Next/Prev functionality is added to all lists of pages + (sections, taxonomies, etc.) + * Add in-section [Next/Prev](/templates/variables/) content pointers +* `Scratch` -- [a "scratchpad"](/extras/scratch) for your node- and page-scoped + variables +* [Cross Reference](/extras/crossreferences/) support to easily link documents + together with the ref and relref shortcodes. +* [Ace](http://ace.yoss.si/) template engine support ({{< gh 541 >}}) +* A new [shortcode](/extras/shortcodes/) token of `{{}}` (raw HTML) + alongside the existing `{{%/* */%}}` (Markdown) +* A top level `Hugo` variable (on Page & Node) is added with various build + information +* Several new ways to order and group content: + * `ByPublishDate` + * `GroupByPublishDate(format, order)` + * `GroupByParam(key, order)` + * `GroupByParamDate(key, format, order)` +* Hugo has undergone a major refactoring, with a new handler system and a + generic file system. This sounds and is technical, but will pave the way for + new features and make Hugo even speedier + +### Notable enhancements to existing features + +* The [shortcode](/extras/shortcodes/) handling is rewritten for speed and + better error messages. +* Several improvements to the [template functions](/templates/functions/): + * `where` is now even more powerful and accepts SQL-like syntax with the + operators `==`, `eq`; `!=`, `<>`, `ne`; `>=`, `ge`; `>`, `gt`; `<=`, + `le`; `<`, `lt`; `in`, `not in` + * `where` template function now also accepts dot chaining key argument + (e.g. `"Params.foo.bar"`) +* New template functions: + * `apply` + * `chomp` + * `delimit` + * `sort` + * `markdownify` + * `in` and `intersect` + * `trim` + * `replace` + * `dateFormat` +* Several [configurable improvements related to Markdown + rendering](/overview/configuration/#configure-blackfriday-rendering:a66b35d20295cb764719ac8bd35837ec): + * Configuration of footnote rendering + * Optional support for smart angled quotes, e.g. `"Hugo"` → «Hugo» + * Enable descriptive header IDs +* URLs in XML output is now correctly canonified ({{< gh 725 728 >}}, and part + of {{< gh 789 >}}) + +### Other improvements + +* Internal change to use byte buffer pool significantly lowering memory usage + and providing measurable performance improvements overall +* Changes to docs: + * A new [Troubleshooting](/troubleshooting/overview/) section is added + * It's now searchable through Google Custom Search ({{< gh 753 >}}) + * Some new great tutorials: + * [Automated deployments with + Wercker](/tutorials/automated-deployments/) + * [Creating a new theme](/tutorials/creating-a-new-theme/) +* [`hugo new`](/content/archetypes/) now copies the content in addition to the front matter +* Improved unit test coverage +* Fixed a lot of Windows-related path issues +* Improved error messages for template and rendering errors +* Enabled soft LiveReload of CSS and images ({{< gh 490 >}}) +* Various fixes in RSS feed generation ({{< gh 789 >}}) +* `HasMenuCurrent` and `IsMenuCurrent` is now supported on Nodes +* A bunch of [bug fixes](https://github.com/gohugoio/hugo/commits/master) + +---- + +## **0.12.0** Sept 1, 2014 + +A lot has happened since Hugo v0.11.0 was released. Most of the work has been +focused on polishing the theme engine and adding critical functionality to the +templates. + +This release represents over 90 code commits from 28 different contributors. + + * 10 [new themes](https://github.com/gohugoio/hugoThemes) created by the community + * Fully themable [Partials](/templates/partials/) + * [404 template](/templates/404/) support in themes + * [Shortcode](/extras/shortcodes/) support in themes + * [Views](/templates/views/) support in themes + * Inner [shortcode](/extras/shortcodes/) content now treated as Markdown + * Support for header ids in Markdown (# Header {#myid}) + * [Where](/templates/list/) template function to filter lists of content, taxonomies, etc. + * [GroupBy](/templates/list/) & [GroupByDate](/templates/list/) methods to group pages + * Taxonomy [pages list](/taxonomies/methods/) now sortable, filterable, limitable & groupable + * General cleanup to taxonomies & documentation to make it more clear and consistent + * [Showcase](/showcase/) returned and has been expanded + * Pretty links now always have trailing slashes + * [BaseUrl](/overview/configuration/) can now include a subdirectory + * Better feedback about draft & future post rendering + * A variety of improvements to [the website](http://gohugo.io/) + +---- + +## **0.11.0** May 28, 2014 + +This release represents over 110 code commits from 29 different contributors. + + * Considerably faster... about 3 - 4x faster on average + * [LiveReload](/extras/livereload/). Hugo will automatically reload the browser when the build is complete + * Theme engine w/[Theme Repository](https://github.com/gohugoio/hugoThemes) + * [Menu system](/extras/menus/) with support for active page + * [Builders](/extras/builders/) to quickly create a new site, content or theme + * [XML sitemap](/templates/sitemap/) generation + * [Integrated Disqus](/extras/comments/) support + * Streamlined [template organization](/templates/overview/) + * [Brand new docs site](http://gohugo.io/) + * Support for publishDate which allows for posts to be dated in the future + * More [sort](/content/ordering/) options + * Logging support + * Much better error handling + * More informative verbose output + * Renamed Indexes > [Taxonomies](/taxonomies/overview/) + * Renamed Chrome > [Partials](/templates/partials/) + +---- + +## **0.10.0** March 1, 2014 + +This release represents over 110 code commits from 29 different contributors. + + * [Syntax highlighting](/extras/highlighting/) powered by pygments (**slow**) + * Ability to [sort content](/content/ordering/) many more ways + * Automatic [table of contents](/extras/toc/) generation + * Support for Unicode URLs, aliases and indexes + * Configurable per-section [permalink](/extras/permalinks/) pattern support + * Support for [paired shortcodes](/extras/shortcodes/) + * Shipping with some [shortcodes](/extras/shortcodes/) (highlight & figure) + * Adding [canonify](/extras/urls/) option to keep urls relative + * A bunch of [additional template functions](/layout/functions/) + * Watching very large sites now works on Mac + * RSS generation improved. Limited to 50 items by default, can limit further in [template](/layout/rss/) + * Boolean params now supported in [frontmatter](/content/front-matter/) + * Launched website [showcase](/showcase/). Show off your own hugo site! + * A bunch of [bug fixes](https://github.com/gohugoio/hugo/commits/master) + +---- + +## **0.9.0** November 15, 2013 + +This release represents over 220 code commits from 22 different contributors. + + * New [command based interface](/overview/usage/) similar to git (`hugo server -s ./`) + * Amber template support + * [Aliases](/extras/aliases/) (redirects) + * Support for top level pages (in addition to homepage) + * Complete overhaul of the documentation site + * Full Windows support + * Better index support including [ordering by content weight](/content/ordering/) + * Add params to site config, available in .Site.Params from templates + * Friendlier json support + * Support for html & xml content (with frontmatter support) + * Support for [summary](/content/summaries/) content divider (<!--more-->) + * HTML in [summary](/content/summaries/) (when using divider) + * Added ["Minutes to Read"](/layout/variables/) functionality + * Support for a custom 404 page + * Cleanup of how content organization is handled + * Loads of unit and performance tests + * Integration with travis ci + * Static directory now watched and copied on any addition or modification + * Support for relative permalinks + * Fixed watching being triggered multiple times for the same event + * Watch now ignores temp files (as created by Vim) + * Configurable number of posts on [homepage](/layout/homepage/) + * [Front matter](/content/front-matter/) supports multiple types (int, string, date, float) + * Indexes can now use a default template + * Addition of truncated bool to content to determine if should show 'more' link + * Support for [linkTitles](/layout/variables/) + * Better handling of most errors with directions on how to resolve + * Support for more date / time formats + * Support for go 1.2 + * Support for `first` in templates + +---- + +## **0.8.0** August 2, 2013 + +This release represents over 65 code commits from 6 different contributors. + + * Added support for pretty urls (filename/index.html vs filename.html) + * Hugo supports a destination directory + * Will efficiently sync content in static to destination directory + * Cleaned up options.. now with support for short and long options + * Added support for TOML + * Added support for YAML + * Added support for Previous & Next + * Added support for indexes for the indexes + * Better Windows compatibility + * Support for series + * Adding verbose output + * Loads of bugfixes + +---- + +## **0.7.0** July 4, 2013 + * Hugo now includes a simple server + * First public release + +---- + +## **0.6.0** July 2, 2013 + * Hugo includes an example documentation site which it builds + +---- + +## **0.5.0** June 25, 2013 + * Hugo is quite usable and able to build spf13.com diff --git a/docs/content/showcase/2626info.md b/docs/content/showcase/2626info.md new file mode 100644 index 000000000..3797860c0 --- /dev/null +++ b/docs/content/showcase/2626info.md @@ -0,0 +1,13 @@ +--- +date: 2016-05-03T17:20:59+09:00 +description: "Personal blog of Masashi Tsuru" +license: "" +licenseLink: "" +sitelink: http://2626.info/ +tags: +- personal +- blog +thumbnail: /img/2626info-tn.png +title: 2626.info +--- + diff --git a/docs/content/showcase/antzucaro.md b/docs/content/showcase/antzucaro.md new file mode 100644 index 000000000..36ec43245 --- /dev/null +++ b/docs/content/showcase/antzucaro.md @@ -0,0 +1,16 @@ +--- +lastmod: 2015-01-27 +date: 2014-02-03T20:00:00Z +description: Ant Zucaro's Blog +license: GPL +licenseLink: "" +sitelink: http://antzucaro.com/ +sourceLink: https://github.com/antzucaro/az.com +tags: +- personal +- blog +- foundation +thumbnail: /img/antzucaro-tn.jpg +title: Ant Zucaro +--- + diff --git a/docs/content/showcase/appernetic.md b/docs/content/showcase/appernetic.md new file mode 100644 index 000000000..53d2d8f77 --- /dev/null +++ b/docs/content/showcase/appernetic.md @@ -0,0 +1,15 @@ +--- +lastmod: 2016-04-26 +date: 2016-04-26T13:33:33Z +description: Appernetic.io blog +license: MIT +licenseLink: https://github.com/appernetic/hugo-bootstrap-premium/blob/master/LICENSE.md +sitelink: https://blog.appernetic.io/ +sourceLink: https://github.com/appernetic/hugo-bootstrap-premium +tags: +- company +- blog +- bootstrap +thumbnail: /img/apperneticioblog.png +title: Appernetic +--- diff --git a/docs/content/showcase/arresteddevops.md b/docs/content/showcase/arresteddevops.md new file mode 100644 index 000000000..b520b5667 --- /dev/null +++ b/docs/content/showcase/arresteddevops.md @@ -0,0 +1,14 @@ +--- +lastmod: 2015-11-20 +date: 2015-11-20T01:46:33-06:00 +description: "Arrested DevOps is a podcast focusing on trends in the DevOps space" +license: "apache2" +licenseLink: "https://github.com/arresteddevops/ado-hugo/blob/master/LICENSE.md" +sitelink: https://www.arresteddevops.com/ +sourceLink: https://github.com/arresteddevops/ado-hugo +tags: +- podcast +- bootstrap +thumbnail: /img/arresteddevops-tn.png +title: arresteddevops +--- diff --git a/docs/content/showcase/asc.md b/docs/content/showcase/asc.md new file mode 100644 index 000000000..bc9424820 --- /dev/null +++ b/docs/content/showcase/asc.md @@ -0,0 +1,15 @@ +--- +lastmod: 2015-01-27 +date: 2014-01-22T07:32:00Z +description: "" +license: CC-BY-SA +licenseLink: "" +sitelink: http://andrewcodispoti.com/ +sourceLink: https://gitlab.com/acodispo/andrewcodispoti-com +tags: +- personal +- bootstrap +thumbnail: /img/asc-tn.jpg +title: Andrew S Codispoti +--- + diff --git a/docs/content/showcase/astrochili.md b/docs/content/showcase/astrochili.md new file mode 100644 index 000000000..fcf1a4217 --- /dev/null +++ b/docs/content/showcase/astrochili.md @@ -0,0 +1,14 @@ +--- +date: 2016-07-31T11:22:32+03:00 +description: "Personal website" +license: "" +licenseLink: "" +sitelink: http://romansilin.com/ +sourceLink: https://github.com/astrochili/astrochili.github.io +tags: +- personal +- blog +thumbnail: /img/astrochili-tn.png +title: Roman Silin +--- + diff --git a/docs/content/showcase/aydoscom.md b/docs/content/showcase/aydoscom.md new file mode 100644 index 000000000..3239d4467 --- /dev/null +++ b/docs/content/showcase/aydoscom.md @@ -0,0 +1,12 @@ +--- +date: 2016-04-23T13:23:00Z +description: "web applications" +license: "" +licenseLink: "" +sitelink: https://aydos.com/ +tags: +- web applications +thumbnail: /img/aydoscom.png +title: aydos.com +--- + diff --git a/docs/content/showcase/balaramadurai.net.md b/docs/content/showcase/balaramadurai.net.md new file mode 100644 index 000000000..ca4a7c71e --- /dev/null +++ b/docs/content/showcase/balaramadurai.net.md @@ -0,0 +1,12 @@ +--- +date: 2016-11-17T12:27:18+05:30 +description: "Dr. Bala Ramadurai, Professor & Consultant in Innovation, Design Thinking and Tech Forecasting" +license: "" +licenseLink: "" +sitelink: http://balaramadurai.net +tags: +- personal +- blog +thumbnail: /img/balaramadurai-net-tn.jpg +title: Dr. Bala Ramadurai | Professor & Consultant in Innovation, Design Thinking and Tech Forecasting +--- diff --git a/docs/content/showcase/barricade.md b/docs/content/showcase/barricade.md new file mode 100644 index 000000000..6316e085a --- /dev/null +++ b/docs/content/showcase/barricade.md @@ -0,0 +1,13 @@ +--- +date: 2016-04-15T14:14:28+01:00 +description: "Barricade is an early warning system against hackers." +license: "" +licenseLink: "" +sitelink: https://barricade.io +tags: +- company +- security +thumbnail: /img/barricade-tn.png +title: Barricade +--- + diff --git a/docs/content/showcase/bepsays.md b/docs/content/showcase/bepsays.md new file mode 100644 index 000000000..c6c749f11 --- /dev/null +++ b/docs/content/showcase/bepsays.md @@ -0,0 +1,15 @@ +--- +lastmod: 2015-05-26 +date: 2013-11-01T07:32:00Z +description: "bep's blog" +license: "" +licenseLink: "" +sitelink: http://bepsays.com/ +sourceLink: "https://github.com/bep/bepsays.com" +tags: +- personal +- blog +thumbnail: /img/bepsays-tn.png +title: bepsays.com +--- + diff --git a/docs/content/showcase/bharathpalavalli.com.md b/docs/content/showcase/bharathpalavalli.com.md new file mode 100644 index 000000000..79d60dcf3 --- /dev/null +++ b/docs/content/showcase/bharathpalavalli.com.md @@ -0,0 +1,14 @@ +--- +date: 2017-03-15T07:32:00Z +description: "Bharath M. Palavalli" +license: "" +licenseLink: "" +sitelink: http://bharathpalavalli.com +sourceLink: https://github.com/bmp/bharathmp-hugo/tree/gh-pages +tags: +- personal +- website +thumbnail: /img/bharathpalavalli-tn.png +title: bharathpalavalli.com +--- + diff --git a/docs/content/showcase/bugtrackers.io.md b/docs/content/showcase/bugtrackers.io.md new file mode 100644 index 000000000..d3a61489c --- /dev/null +++ b/docs/content/showcase/bugtrackers.io.md @@ -0,0 +1,13 @@ +--- +lastmod: 2015-10-27 +date: 2015-10-27T09:02:00Z +description: bugtrackers.io provides stories of digital crafters. It shows people behind bits, pixels and bug reports. bugtrackers.io is your resource for web development. +sitelink: https://www.bugtrackers.io/ +tags: +- blog +- community +- interviews +thumbnail: /img/bugtrackersio-tn.jpg +title: bugtrackers.io +--- + diff --git a/docs/content/showcase/bullion-investor.md b/docs/content/showcase/bullion-investor.md new file mode 100644 index 000000000..262dcfb53 --- /dev/null +++ b/docs/content/showcase/bullion-investor.md @@ -0,0 +1,22 @@ +--- +date: 2017-02-11T10:30:00+02:00 +description: "German language GOLD REPORT. Tips, tricks, news & glossary on coins, bullion bars and precious-metals investments." +sitelink: https://www.bullion-investor.com/report/ +tags: +- 'gold price' +- blog +- coins +- deutschland +- finance +- germany +- gold +- investment +- news +- numismatics +- palladium +- platinum +- report +- silver +thumbnail: /img/bullion-investor-com.png +title: GOLDREPORT — Gold, Silver & Numismatics News +--- \ No newline at end of file diff --git a/docs/content/showcase/camunda-blog.md b/docs/content/showcase/camunda-blog.md new file mode 100644 index 000000000..7e7f76427 --- /dev/null +++ b/docs/content/showcase/camunda-blog.md @@ -0,0 +1,15 @@ +--- +lastmod: 2015-11-30 +date: 2015-12-01T04:20:00Z +description: "Camunda BPM Team Blog" +license: "Apache 2.0" +licenseLink: "https://github.com/camunda/blog.camunda.org#license" +sitelink: http://blog.camunda.org/ +sourceLink: https://github.com/camunda/blog.camunda.org +tags: +- company +- blog +thumbnail: /img/camunda-blog.png +title: Camunda Blog +--- + diff --git a/docs/content/showcase/camunda-docs.md b/docs/content/showcase/camunda-docs.md new file mode 100644 index 000000000..8d97d1492 --- /dev/null +++ b/docs/content/showcase/camunda-docs.md @@ -0,0 +1,15 @@ +--- +lastmod: 2015-11-30 +date: 2015-12-01T04:20:00Z +description: "Camunda BPM Documentation" +license: MIT +licenseLink: "https://github.com/camunda/camunda-docs-theme#licence" +sitelink: http://docs.camunda.org/ +sourceLink: https://github.com/camunda/camunda-docs-theme +tags: +- company +- documentation +thumbnail: /img/camunda-docs.png +title: Camunda Docs +--- + diff --git a/docs/content/showcase/carnivorousplants.md b/docs/content/showcase/carnivorousplants.md new file mode 100644 index 000000000..30f3b3756 --- /dev/null +++ b/docs/content/showcase/carnivorousplants.md @@ -0,0 +1,13 @@ +--- +date: 2017-05-13T11:00:00Z +description: "Tools, guides, and interactive resources for growers of all carnivorous plant species." +license: "" +licenseLink: "" +sitelink: https://www.carnivorousplants.co.uk/ +tags: +- blog +- education +thumbnail: /img/carnivorousplants-tn.png +title: CarnivorousPlants.co.uk +--- + diff --git a/docs/content/showcase/cdnoverview.md b/docs/content/showcase/cdnoverview.md new file mode 100644 index 000000000..d48788ec6 --- /dev/null +++ b/docs/content/showcase/cdnoverview.md @@ -0,0 +1,14 @@ +--- +date: 2016-02-17T15:19:08+01:00 +description: "Overview and comparison of CDNs, their features and prices" +license: "" +licenseLink: "" +sitelink: https://www.cdnoverview.com/ +tags: +- bootstrap +- portfolio +- tech +thumbnail: /img/cdnoverview-tn.png +title: cdnoverview.com +--- + diff --git a/docs/content/showcase/chinese-grammar.md b/docs/content/showcase/chinese-grammar.md new file mode 100644 index 000000000..3e8bc3563 --- /dev/null +++ b/docs/content/showcase/chinese-grammar.md @@ -0,0 +1,12 @@ +--- +lastmod: 2015-08-21 +date: 2015-08-23 +description: Chinese grammar lessons +sitelink: https://www.chineseboost.com/grammar/ +sourceLink: https://github.com/hughgrigg/chineseboost-articles +tags: +- learning +- education +thumbnail: /img/chinese-grammar-tn.png +title: Chinese Grammar +--- diff --git a/docs/content/showcase/chingli.md b/docs/content/showcase/chingli.md new file mode 100644 index 000000000..ae2d4a4db --- /dev/null +++ b/docs/content/showcase/chingli.md @@ -0,0 +1,12 @@ +--- +lastmod: 2015-12-19 +date: 2014-08-26T11:20:02-04:00 +description: "chingli’s personal blog" +sitelink: http://www.chingli.com/ +tags: +- personal +- blog +thumbnail: /img/chingli-tn.jpg +title: 青砾 (chingli) +--- + diff --git a/docs/content/showcase/chipsncookies.md b/docs/content/showcase/chipsncookies.md new file mode 100644 index 000000000..ff3128d81 --- /dev/null +++ b/docs/content/showcase/chipsncookies.md @@ -0,0 +1,15 @@ +--- +lastmod: 2016-04-14 +date: 2015-07-08T14:02:16+02:00 +description: "personal blog and portfolio of Samuel Debruyn" +license: "" +licenseLink: "" +sitelink: https://chipsncookies.com +sourceLink: https://github.com/SamuelDebruyn/chipsncookies-site +tags: +- personal +- blog +thumbnail: /img/chipsncookies-tn.png +title: Chips 'n' Cookies +--- + diff --git a/docs/content/showcase/christianmendoza.md b/docs/content/showcase/christianmendoza.md new file mode 100644 index 000000000..6b36a53d8 --- /dev/null +++ b/docs/content/showcase/christianmendoza.md @@ -0,0 +1,14 @@ +--- +lastmod: 2017-06-30 +date: 2016-10-16T13:30:47-04:00 +description: "Personal site" +license: "" +licenseLink: "" +sitelink: https://www.christianmendoza.me/ +sourceLink: https://github.com/christianmendoza/christianmendoza.me +tags: +- personal +- profile +thumbnail: /img/christianmendoza-tn.jpg +title: christianmendoza.me +--- diff --git a/docs/content/showcase/cinegyopen.md b/docs/content/showcase/cinegyopen.md new file mode 100644 index 000000000..592df2612 --- /dev/null +++ b/docs/content/showcase/cinegyopen.md @@ -0,0 +1,12 @@ +--- +date: 2016-09-05T23:23:18+02:00 +description: "Cinegy Open documentation project" +license: "" +licenseLink: "" +sitelink: https://open.cinegy.com/ +sourceLink: +tags: +- documentation +thumbnail: /img/cinegyopen-tn.png +title: Cinegy Open +--- \ No newline at end of file diff --git a/docs/content/showcase/clearhaus.md b/docs/content/showcase/clearhaus.md new file mode 100644 index 000000000..9408ec8e6 --- /dev/null +++ b/docs/content/showcase/clearhaus.md @@ -0,0 +1,14 @@ +--- +date: 2016-10-21T12:37:00+02:00 +description: "Online Acquiring · Accept Visa & MasterCard within 1-3 days" +license: "" +licenseLink: "" +sitelink: https://www.clearhaus.com/ +tags: + - company + - fintech + - payments + - acquirer +thumbnail: /img/clearhaus-tn.png +title: Clearhaus +--- diff --git a/docs/content/showcase/cloudshark.md b/docs/content/showcase/cloudshark.md new file mode 100644 index 000000000..6a8b3fa4f --- /dev/null +++ b/docs/content/showcase/cloudshark.md @@ -0,0 +1,15 @@ +--- +lastmod: 2015-01-27 +date: 2014-03-27T09:45:00Z +description: CloudShark Appliance homepage and documentation +license: "" +licenseLink: "" +sitelink: https://appliance.cloudshark.org/ +tags: +- company +- documentation +- foundation +thumbnail: /img/cloudshark-tn.jpg +title: CloudShark +--- + diff --git a/docs/content/showcase/coding-journal.md b/docs/content/showcase/coding-journal.md new file mode 100644 index 000000000..78e4b38d2 --- /dev/null +++ b/docs/content/showcase/coding-journal.md @@ -0,0 +1,14 @@ +--- +lastmod: 2015-12-23 +date: 2015-12-23T11:42:00+01:00 +description: blog, portfolio +license: "" +licenseLink: "" +sitelink: http://blog.kulman.sk/ +sourceLink: https://github.com/igorkulman/coding-journal +tags: +- blog +- portfolio +thumbnail: /img/codingjournal-tn.png +title: Coding Journal +--- diff --git a/docs/content/showcase/consequently.md b/docs/content/showcase/consequently.md new file mode 100644 index 000000000..3469129f9 --- /dev/null +++ b/docs/content/showcase/consequently.md @@ -0,0 +1,16 @@ +--- +lastmod: 2015-02-11 +date: 2015-02-11T13:21:27+11:00 +description: "consequently.org, Greg Restall's personal website" +license: "" +licenseLink: "" +sitelink: http://consequently.org +sourceLink: "https://github.com/consequently/consequently-hugo" +tags: +- academic +- blog +- kube +thumbnail: /img/consequently.jpg +title: consequently.org +--- + diff --git a/docs/content/showcase/ctlcompiled.md b/docs/content/showcase/ctlcompiled.md new file mode 100644 index 000000000..18179cf5b --- /dev/null +++ b/docs/content/showcase/ctlcompiled.md @@ -0,0 +1,14 @@ +--- +date: 2016-07-25T13:45:09-04:00 +description: "CompilED is a collection of reflections and comments by the software developers at Columbia’s Center for Teaching and Learning (CTL). These views are rooted in our professional and personal experiences developing educational technology." +license: "Creative Commons Attribution-ShareAlike 3.0 United States." +licenseLink: "https://creativecommons.org/licenses/by-sa/3.0/us/" +sitelink: https://compiled.ctl.columbia.edu/ +tags: +- edtech +- technology +- blog +thumbnail: /img/ctlcompiled-tn.png +title: CompilED at CTL +--- + diff --git a/docs/content/showcase/danmux.md b/docs/content/showcase/danmux.md new file mode 100644 index 000000000..1ae4589d1 --- /dev/null +++ b/docs/content/showcase/danmux.md @@ -0,0 +1,15 @@ +--- +lastmod: 2014-08-26 +date: 2014-08-26T10:55:23-04:00 +description: "" +license: MIT +licenseLink: "" +sitelink: http://danmux.com/ +sourceLink: https://github.com/danmux/danmux-hugo +tags: +- personal +- blog +thumbnail: /img/danmux-tn.jpg +title: Danmux +--- + diff --git a/docs/content/showcase/datapipelinearchitect.md b/docs/content/showcase/datapipelinearchitect.md new file mode 100644 index 000000000..a74eb835e --- /dev/null +++ b/docs/content/showcase/datapipelinearchitect.md @@ -0,0 +1,13 @@ +--- +date: 2016-02-04T18:30:07-05:00 +description: "Professional website at datapipelinearchitect.com" +sitelink: http://datapipelinearchitect.com/ +tags: +- company +- tech +- blog +- website +thumbnail: /img/datapipelinearchitect-tn.jpg +title: Data Pipeline Architect +--- + diff --git a/docs/content/showcase/davidepetilli.md b/docs/content/showcase/davidepetilli.md new file mode 100644 index 000000000..6b69eee0f --- /dev/null +++ b/docs/content/showcase/davidepetilli.md @@ -0,0 +1,16 @@ +--- +lastmod: 2015-05-25 +date: 2013-11-01T07:32:00Z +description: "" +license: MIT +licenseLink: "" +sitelink: http://davidepetilli.com +tags: +- personal +- photography +- portfolio +- blog +thumbnail: /img/davidepetilli-tn.jpg +title: Davide Petilli +--- + diff --git a/docs/content/showcase/davidrallen.md b/docs/content/showcase/davidrallen.md new file mode 100644 index 000000000..06e802fd5 --- /dev/null +++ b/docs/content/showcase/davidrallen.md @@ -0,0 +1,15 @@ +--- +date: 2016-02-03T11:49:07-05:00 +description: "Personal website for David Allen" +license: "MIT" +licenseLink: "https://opensource.org/licenses/MIT" +sitelink: http://davidrallen.com/ +sourceLink: https://github.com/doctorallen/davidrallen.com +tags: +- personal +- blog +- tech +thumbnail: /img/davidrallen-tn.png +title: David Allen +--- + diff --git a/docs/content/showcase/davidyates.md b/docs/content/showcase/davidyates.md new file mode 100644 index 000000000..0d6063ad4 --- /dev/null +++ b/docs/content/showcase/davidyates.md @@ -0,0 +1,13 @@ +--- +date: 2016-09-10T07:07:39+02:00 +description: "David Yates" +license: "" +licenseLink: "" +sitelink: https://davidyat.es/ +tags: +- personal +- blog +thumbnail: /img/davidyates-tn.png +title: David Yates +--- + diff --git a/docs/content/showcase/dbzman-online.md b/docs/content/showcase/dbzman-online.md new file mode 100644 index 000000000..f30d71491 --- /dev/null +++ b/docs/content/showcase/dbzman-online.md @@ -0,0 +1,15 @@ +--- +date: 2017-01-02T07:32:00Z +description: "" +license: "" +licenseLink: "" +sitelink: http://dbzman-online.eu/ +sourceLink: https://github.com/Dbzman/dbzman-online.eu +tags: +- personal +- blog +- portfolio +thumbnail: /img/dbzman-online-tn.png +title: Dbzman-Online +--- + diff --git a/docs/content/showcase/devmonk.md b/docs/content/showcase/devmonk.md new file mode 100644 index 000000000..00a1a7572 --- /dev/null +++ b/docs/content/showcase/devmonk.md @@ -0,0 +1,15 @@ +--- +lastmod: 2014-08-26 +date: 2014-08-26T11:31:02-04:00 +description: "" +license: "" +licenseLink: "" +sitelink: https://devmonk.com/ +sourceLink: https://github.com/peteraba/devmonk.com +tags: +- educational +- video +thumbnail: /img/devmonk-tn.jpg +title: devmonk +--- + diff --git a/docs/content/showcase/dmitriid.com.md b/docs/content/showcase/dmitriid.com.md new file mode 100644 index 000000000..2a5e308ba --- /dev/null +++ b/docs/content/showcase/dmitriid.com.md @@ -0,0 +1,14 @@ +--- +date: 2016-10-19T16:52:28+02:00 +description: "Personal blog" +license: "CC BY-NC 4.0" +licenseLink: "" +sitelink: http://dmitriid.com/ +sourceLink: https://github.com/dmitriid/dmitriid.com +tags: +- personal +- blog +thumbnail: /img/dmitriid.com.png +title: dmitriid.com +--- + diff --git a/docs/content/showcase/emilyhorsman.com.md b/docs/content/showcase/emilyhorsman.com.md new file mode 100644 index 000000000..8e792f716 --- /dev/null +++ b/docs/content/showcase/emilyhorsman.com.md @@ -0,0 +1,13 @@ +--- +lastmod: 2016-01-09 +date: 2016-01-09T01:00:10Z +description: Personal homepage +sitelink: https://emilyhorsman.com/ +sourceLink: https://github.com/emilyhorsman/buttercup +tags: +- personal +- blog +thumbnail: /img/emilyhorsman.com-tn.jpg +title: emilyhorsman.com +--- + diff --git a/docs/content/showcase/enjoyablerecipes.md b/docs/content/showcase/enjoyablerecipes.md new file mode 100644 index 000000000..7b97e6293 --- /dev/null +++ b/docs/content/showcase/enjoyablerecipes.md @@ -0,0 +1,16 @@ +--- +date: 2017-03-07T07:32:00Z +description: "Indian Recipes Blog" +license: "" +licenseLink: "" +sitelink: https://enjoyable.recipes/ +sourceLink: https://github.com/shubhojyoti/enjoyablerecipes-hugo +tags: +- personal +- blog +- recipes +- zurb-foundation +thumbnail: /img/enjoyablerecipes-tn.png +title: Enjoyable Recipes +--- + diff --git a/docs/content/showcase/esaezgil.md b/docs/content/showcase/esaezgil.md new file mode 100644 index 000000000..f1ae53b20 --- /dev/null +++ b/docs/content/showcase/esaezgil.md @@ -0,0 +1,16 @@ +--- +date: 2017-04-02T07:32:00Z +description: "Personal blog of Enrique Saez Gil - esaezgil" +license: "" +licenseLink: "" +sitelink: https://esaezgil.com +sourceLink: https://github.com/esaezgil/esaezgil.github.io +tags: +- personal +- technical blog +- software +- open source +thumbnail: /img/esaezgil_com-tn.png +title: esaezgil.com +--- + diff --git a/docs/content/showcase/esolia-com.md b/docs/content/showcase/esolia-com.md new file mode 100644 index 000000000..58a482f51 --- /dev/null +++ b/docs/content/showcase/esolia-com.md @@ -0,0 +1,16 @@ +--- +lastmod: 2015-08-08 +date: 2015-07-07T04:32:00Z +description: Tokyo IT service provider eSolia Inc's Hugo-powered website. +license: MIT +licenseLink: "" +sitelink: http://esolia.com/ +sourceLink: https://github.com/eSolia/eSolia +tags: +- company +- esolia +- rickcogley +- japan +thumbnail: /img/esolia_com-tn.png +title: eSolia.com +--- diff --git a/docs/content/showcase/esolia-pro.md b/docs/content/showcase/esolia-pro.md new file mode 100644 index 000000000..24b06b1f8 --- /dev/null +++ b/docs/content/showcase/esolia-pro.md @@ -0,0 +1,16 @@ +--- +lastmod: 2015-08-08 +date: 2015-07-07T04:32:00Z +description: Tokyo IT service provider eSolia Inc's eSolia.pro blog site, powered by Hugo. +license: MIT +licenseLink: "" +sitelink: http://esolia.pro/ +sourceLink: https://github.com/eSolia/eSolia.pro +tags: +- company +- esolia +- rickcogley +- japan +thumbnail: /img/esolia_pro-tn.png +title: eSolia.pro +--- diff --git a/docs/content/showcase/eurie.md b/docs/content/showcase/eurie.md new file mode 100644 index 000000000..0ab5966f7 --- /dev/null +++ b/docs/content/showcase/eurie.md @@ -0,0 +1,11 @@ +--- +date: 2016-10-15T23:30:59+09:00 +description: "User guide for euire Desk" +license: "" +licenseLink: "" +sitelink: https://docs.eurie.io +tags: +- documentation +thumbnail: /img/docs.eurie.io-tn.png +title: eurie Desk docs +--- diff --git a/docs/content/showcase/fale.md b/docs/content/showcase/fale.md new file mode 100644 index 000000000..449c1cc22 --- /dev/null +++ b/docs/content/showcase/fale.md @@ -0,0 +1,15 @@ +--- +lastmod: 2016-03-01 +date: 2016-03-01T12:38:00Z +description: Fabio Alessandro Locati personal blog. +license: AGPLv3 +licenseLink: "" +sitelink: http://fale.io/ +sourceLink: https://github.com/fale/fale.io +tags: +- personal +- blog +thumbnail: /img/fale-tn.png +title: fale.io +--- + diff --git a/docs/content/showcase/firstnameclub.md b/docs/content/showcase/firstnameclub.md new file mode 100644 index 000000000..573cdda0a --- /dev/null +++ b/docs/content/showcase/firstnameclub.md @@ -0,0 +1,13 @@ +--- +date: 2017-03-31T22:36:12+03:00 +description: "Multilingual website. Size 20GB, build time 25 mins. Cloudflare, s3, bootstrap 4" +license: "" +licenseLink: "" +sitelink: https://firstname.club +sourceLink: +tags: +- data +- website +thumbnail: /img/firstnameclub.png +title: firstname club +--- diff --git a/docs/content/showcase/fixatom.md b/docs/content/showcase/fixatom.md new file mode 100644 index 000000000..8ba1978d1 --- /dev/null +++ b/docs/content/showcase/fixatom.md @@ -0,0 +1,13 @@ +--- +date: 2016-10-17T16:19:24+08:00 +description: "a personal blog" +license: "" +licenseLink: "" +sitelink: https://fixatom.com/ +tags: +- personal +- blog +thumbnail: /img/fixatom-tn.png +title: Atom +--- + diff --git a/docs/content/showcase/furqansoftware.md b/docs/content/showcase/furqansoftware.md new file mode 100644 index 000000000..4d377e642 --- /dev/null +++ b/docs/content/showcase/furqansoftware.md @@ -0,0 +1,12 @@ +--- +date: 2017-05-24T06:19:40Z +description: "Official company website" +sitelink: https://furqansoftware.com/ +tags: +- company +- website +- blog +- tech +thumbnail: /img/furqansoftware-tn.png +title: Furqan Software +--- diff --git a/docs/content/showcase/fxsitecompat.md b/docs/content/showcase/fxsitecompat.md new file mode 100644 index 000000000..393b097d9 --- /dev/null +++ b/docs/content/showcase/fxsitecompat.md @@ -0,0 +1,16 @@ +--- +lastmod: 2015-09-08 +date: 2014-08-26T19:40:00-04:00 +description: Multilingual, community documentation and blog site +license: "" +licenseLink: "" +sitelink: https://www.fxsitecompat.com/ +sourceLink: https://github.com/fxsitecompat/www.fxsitecompat.com +tags: +- community +- documentation +- translation +thumbnail: /img/fxsitecompat-tn.png +title: Firefox Site Compatibility +--- + diff --git a/docs/content/showcase/gntech.md b/docs/content/showcase/gntech.md new file mode 100644 index 000000000..3d199e931 --- /dev/null +++ b/docs/content/showcase/gntech.md @@ -0,0 +1,15 @@ +--- +date: 2016-02-13T21:47:27+01:00 +description: "Projects, music and drawings, the personal web page of Gustav Näslund" +license: "" +licenseLink: "" +sitelink: http://www.gntech.se/ +tags: +- personal +- blog +- projects +- music +- drawings +thumbnail: /img/gntech-tn.png +title: gntech.se +--- diff --git a/docs/content/showcase/gogb.md b/docs/content/showcase/gogb.md new file mode 100644 index 000000000..d104741ba --- /dev/null +++ b/docs/content/showcase/gogb.md @@ -0,0 +1,13 @@ +--- +lastmod: 2015-05-25 +date: 2013-11-01T07:32:00Z +description: "" +license: MIT +licenseLink: "" +sitelink: http://getgb.io +tags: +- project +thumbnail: /img/gogb-tn.jpg +title: GoGB +--- + diff --git a/docs/content/showcase/goin5minutes.md b/docs/content/showcase/goin5minutes.md new file mode 100644 index 000000000..ae0c61da3 --- /dev/null +++ b/docs/content/showcase/goin5minutes.md @@ -0,0 +1,13 @@ +--- +lastmod: 2015-11-20 +date: 2015-11-20T20:46:00Z +description: "Code for Go in 5 Minutes Screencasts" +license: "Apache License 2.0" +licenseLink: "https://github.com/arschles/go-in-5-minutes/blob/master/LICENSE" +sitelink: http://www.goin5minutes.com/ +sourceLink: https://github.com/arschles/go-in-5-minutes/tree/master/www +tags: +- screencasts +thumbnail: /img/goin5minutes-tn.png +title: Go in 5 minutes +--- \ No newline at end of file diff --git a/docs/content/showcase/h10n.me.md b/docs/content/showcase/h10n.me.md new file mode 100644 index 000000000..0576edb1f --- /dev/null +++ b/docs/content/showcase/h10n.me.md @@ -0,0 +1,11 @@ +--- +date: 2016-03-05T14:30:21+01:00 +description: "Personal profile page of Horst Gutmann" +sitelink: http://h10n.me/ +sourceLink: https://github.com/zerok/h10n.me +tags: +- personal +- profile +thumbnail: /img/h10n.me-tn.png +title: h10n.me +--- diff --git a/docs/content/showcase/heimatverein-niederjosbach.md b/docs/content/showcase/heimatverein-niederjosbach.md new file mode 100644 index 000000000..d063e9c50 --- /dev/null +++ b/docs/content/showcase/heimatverein-niederjosbach.md @@ -0,0 +1,14 @@ +--- +date: 2017-01-02T07:32:00Z +description: "" +license: "" +licenseLink: "" +sitelink: http://heimatverein-niederjosbach.de/ +sourceLink: https://github.com/Dbzman/heimatverein-niederjosbach.de +tags: +- association +- gallery +thumbnail: /img/heimatverein-niederjosbach-tn.png +title: Niederjosbacher Heimat- und Geschichtsverein 2007 e.V. +--- + diff --git a/docs/content/showcase/horeaporutiu.md b/docs/content/showcase/horeaporutiu.md new file mode 100644 index 000000000..e48d3de0b --- /dev/null +++ b/docs/content/showcase/horeaporutiu.md @@ -0,0 +1,13 @@ +--- +date: 2017-06-22T07:32:00Z +description: "" +license: "" +licenseLink: "" +sitelink: https://horeaporutiu.github.io/ +sourceLink: https://github.com/horeaporutiu/hugo-horeaporutiu +tags: +- personal +- blog +thumbnail: /img/horeaporutiu-tn.jpg +title: horeaporutiu.github.io +--- diff --git a/docs/content/showcase/hugo.md b/docs/content/showcase/hugo.md new file mode 100644 index 000000000..54c9ce09a --- /dev/null +++ b/docs/content/showcase/hugo.md @@ -0,0 +1,15 @@ +--- +lastmod: 2015-01-27 +date: 2013-07-01T07:32:00Z +description: This site +license: Simpl +licenseLink: "" +sitelink: http://gohugo.io/ +sourceLink: https://github.com/gohugoio/hugo/tree/master/docs +tags: +- documentation +- bootstrap +thumbnail: /img/hugo-tn.jpg +title: Hugo +--- + diff --git a/docs/content/showcase/invincible.md b/docs/content/showcase/invincible.md new file mode 100644 index 000000000..2030e93fb --- /dev/null +++ b/docs/content/showcase/invincible.md @@ -0,0 +1,15 @@ +--- +date: 2017-07-01T01:11:00Z +description: "The Invincible: The tale of a disillusioned prince" +license: "Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License" +licenseLink: "https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode" +sitelink: https://invincible.site/ +sourceLink: https://github.com/shazic/the_invincible +tags: +- personal +- blog +- graphic novel +thumbnail: /img/invincible-tn.jpg +title: Invincible +--- + diff --git a/docs/content/showcase/invision.md b/docs/content/showcase/invision.md new file mode 100644 index 000000000..baf1fc463 --- /dev/null +++ b/docs/content/showcase/invision.md @@ -0,0 +1,13 @@ +--- +date: 2017-01-28T12:04:00Z +description: InVision Engineering Blog +license: "" +licenseLink: "" +sitelink: http://engineering.invisionapp.com +tags: +- company +- blog +- engineering +thumbnail: /img/invision-tn.png +title: InVision Engineering Blog +--- diff --git a/docs/content/showcase/jamescampbell.md b/docs/content/showcase/jamescampbell.md new file mode 100644 index 000000000..de7fa62a2 --- /dev/null +++ b/docs/content/showcase/jamescampbell.md @@ -0,0 +1,15 @@ +--- +date: 2016-06-06T14:42:59-04:00 +description: "" +license: "" +licenseLink: "" +sitelink: https://jamescampbell.us/ +sourceLink: https://github.com/jamesacampbell/causes-and-effects-hugo +tags: +- personal +- blog +thumbnail: /img/jamescampbell-tn.png +title: jamescampbell.us +--- + + diff --git a/docs/content/showcase/jorgennilsson.md b/docs/content/showcase/jorgennilsson.md new file mode 100644 index 000000000..7821eea7d --- /dev/null +++ b/docs/content/showcase/jorgennilsson.md @@ -0,0 +1,12 @@ +--- +date: 2016-02-11T23:41:27+01:00 +description: "Personal web site and blog of digital director Jorgen Nilsson" +license: "" +licenseLink: "" +sitelink: http://jorgennilsson.com/ +tags: +- personal +- blog +thumbnail: /img/jorgennilsson-tn.png +title: jorgennilsson.com +--- diff --git a/docs/content/showcase/kieranhealy.md b/docs/content/showcase/kieranhealy.md new file mode 100644 index 000000000..f4f32989c --- /dev/null +++ b/docs/content/showcase/kieranhealy.md @@ -0,0 +1,16 @@ +--- +lastmod: 2015-01-27 +date: 2014-02-27T20:35:00Z +description: Kieran Healy's Website +license: "" +licenseLink: "" +sitelink: http://kieranhealy.org/ +sourceLink: https://github.com/kjhealy/kieranhealy.hugo +tags: +- personal +- blog +- academic +thumbnail: /img/kjhealy-tn.jpg +title: Kieran Healy +--- + diff --git a/docs/content/showcase/klingt-net.md b/docs/content/showcase/klingt-net.md new file mode 100644 index 000000000..369e44cdb --- /dev/null +++ b/docs/content/showcase/klingt-net.md @@ -0,0 +1,15 @@ +--- +date: 2016-05-15T23:18:16+02:00 +description: "klingt.net is the personal homepage of Andreas Linz." +license: "" +licenseLink: "" +sitelink: https://klingt.net/ +sourceLink: https://github.com/klingtnet/klingt.net +tags: +- personal +- blog +- programming +thumbnail: /img/klingt-net-tn.png +title: klingt net +--- + diff --git a/docs/content/showcase/launchcode5.md b/docs/content/showcase/launchcode5.md new file mode 100644 index 000000000..99bea2cfb --- /dev/null +++ b/docs/content/showcase/launchcode5.md @@ -0,0 +1,14 @@ +--- +lastmod: 2015-01-27 +date: 2014-11-01T07:32:00Z +description: Corporate Site for Launchcode Software Studios +license: Copyright Launchcode Software Studios +licenseLink: "" +sitelink: http://www.launchcode5.com/ +sourceLink: https://github.com/Launchcode5/launchcode5.com +tags: +- bootstrap +thumbnail: /img/launchcode-tn.jpg +title: Launchcode Software Studios +--- + diff --git a/docs/content/showcase/leepenney.md b/docs/content/showcase/leepenney.md new file mode 100644 index 000000000..db8bfb45b --- /dev/null +++ b/docs/content/showcase/leepenney.md @@ -0,0 +1,14 @@ +--- +lastmod: 2016-01-24 +date: 2016-01-24T16:10:00Z +description: Site of author Lee Penney +license: MIT +licenseLink: "" +sitelink: http://leepenney.com/ +tags: +- personal +- website +thumbnail: /img/leepenney-tn.jpg +title: Lee Penney +--- + diff --git a/docs/content/showcase/leowkahman.md b/docs/content/showcase/leowkahman.md new file mode 100644 index 000000000..a7ce50418 --- /dev/null +++ b/docs/content/showcase/leowkahman.md @@ -0,0 +1,12 @@ +--- +date: 2016-07-24T00:00:00+08:00 +description: "Leow Kah Man - Tech Blog" +license: "" +licenseLink: "" +sitelink: https://www.leowkahman.com/ +tags: +- personal +- blog +thumbnail: /img/leowkahman-tn.png +title: Leow Kah Man - Tech Blog +--- \ No newline at end of file diff --git a/docs/content/showcase/lk4d4.darth.io.md b/docs/content/showcase/lk4d4.darth.io.md new file mode 100644 index 000000000..a35773380 --- /dev/null +++ b/docs/content/showcase/lk4d4.darth.io.md @@ -0,0 +1,15 @@ +--- +lastmod: 2014-08-26 +date: 2014-08-25T18:59:30-04:00 +description: Alexandr Morozov +license: "" +licenseLink: "" +sitelink: http://lk4d4.darth.io/ +sourceLink: https://github.com/LK4D4/lk4d4.darth.io +tags: +- personal +- blog +thumbnail: /img/lk4d4-tn.jpg +title: lk4d4.darth.io +--- + diff --git a/docs/content/showcase/losslesslife.md b/docs/content/showcase/losslesslife.md new file mode 100644 index 000000000..5e7158daa --- /dev/null +++ b/docs/content/showcase/losslesslife.md @@ -0,0 +1,15 @@ +--- +lastmod: 2015-11-15 +date: 2015-11-15T11:20:00-08:00 +description: "Losslesslife covers everything high-end and audiophile headphones. Headphones and headphone amplifiers, accessories, how-to guides, and reviews." +sitelink: http://pages.losslesslife.com/ +tags: +- headphones +- electronics +- technical +- reviews +- education +- audiophile +thumbnail: /img/losslesslife-tn.png +title: LosslessLife +--- diff --git a/docs/content/showcase/lucumt.info.md b/docs/content/showcase/lucumt.info.md new file mode 100644 index 000000000..0f66b294f --- /dev/null +++ b/docs/content/showcase/lucumt.info.md @@ -0,0 +1,14 @@ +--- +date: 2017-03-12T13:14:14+08:00 +description: "Personal blog of Rosen Lu" +license: "" +licenseLink: "" +sitelink: http://lucumt.info/posts +sourceLink: https://github.com/lucumt/ghblog +tags: +- personal +- blog +thumbnail: /img/lucumt.info.png +title: 飞狐的部落格 +--- + diff --git a/docs/content/showcase/mariosanchez.md b/docs/content/showcase/mariosanchez.md new file mode 100644 index 000000000..99285f871 --- /dev/null +++ b/docs/content/showcase/mariosanchez.md @@ -0,0 +1,14 @@ +--- +lastmod: 2015-06-20 +date: 2015-06-20 +description: "" +license: "" +licenseLink: "" +sitelink: http://mariosanchez.org/ +sourceLink: https://github.com/mariobox/Hugo-Source +tags: +- personal +- blog +thumbnail: /img/mariosanchez-tn.jpg +title: mariosanchez.org +--- diff --git a/docs/content/showcase/mayan-edms.md b/docs/content/showcase/mayan-edms.md new file mode 100644 index 000000000..656a6790c --- /dev/null +++ b/docs/content/showcase/mayan-edms.md @@ -0,0 +1,14 @@ +--- +date: 2016-06-13T19:38:56-04:00 +description: "Free Open Source Document Management System" +license: "Apache 2.0" +licenseLink: "" +sitelink: http://www.mayan-edms.com/ +sourceLink: https://gitlab.com/mayan-edms/website +tags: +- paperless +- floss +thumbnail: /img/mayan-edms-tn.png +title: Mayan EDMS +--- + diff --git a/docs/content/showcase/michaelwhatcott.md b/docs/content/showcase/michaelwhatcott.md new file mode 100644 index 000000000..9c5bcd07f --- /dev/null +++ b/docs/content/showcase/michaelwhatcott.md @@ -0,0 +1,15 @@ +--- +lastmod: 2014-08-26 +date: 2014-08-26T11:47:11-04:00 +description: "" +license: Simpl-2.0 +licenseLink: "" +sitelink: http://michaelwhatcott.com/ +sourceLink: https://bitbucket.org/mdwhatcott/michaelwhatcott.com-boilerplate/src +tags: +- personal +- blog +thumbnail: /img/michaelwhatcott-tn.jpg +title: michaelwhatcott +--- + diff --git a/docs/content/showcase/mongodb-eng-journal.md b/docs/content/showcase/mongodb-eng-journal.md new file mode 100644 index 000000000..9dc5ea0ad --- /dev/null +++ b/docs/content/showcase/mongodb-eng-journal.md @@ -0,0 +1,13 @@ +--- +date: 2016-03-09T14:14:16-05:00 +description: "The MongoDB Engineering Journal -- for builders, by builders." +license: "" +licenseLink: "" +sitelink: http://engineering.mongodb.com/ +tags: +- engineering +- blog +thumbnail: /img/mongodb-eng-tn.png +title: The Mongodb Engineering Journal +--- + diff --git a/docs/content/showcase/mtbhomer.md b/docs/content/showcase/mtbhomer.md new file mode 100644 index 000000000..a98c59621 --- /dev/null +++ b/docs/content/showcase/mtbhomer.md @@ -0,0 +1,14 @@ +--- +date: 2016-06-19T11:38:39+02:00 +description: "Personal website Martijn ten Bhömer" +license: "" +licenseLink: "" +sitelink: https://www.mtbhomer.com/ +sourceLink: https://github.com/mtbhomer/web-portfolio +tags: +- personal +- portfolio +- design +thumbnail: /img/mtbhomer-tn.png +title: mtbhomer.com +--- diff --git a/docs/content/showcase/myearworms.md b/docs/content/showcase/myearworms.md new file mode 100644 index 000000000..17d0549f3 --- /dev/null +++ b/docs/content/showcase/myearworms.md @@ -0,0 +1,14 @@ +--- +date: 2017-03-08T07:32:00Z +description: "All the songs that get stuck in my head" +license: "" +licenseLink: "" +sitelink: https://myearworms.com/ +sourceLink: https://github.com/jaydreyer/myearworms +tags: +- personal +- blog +- music +thumbnail: /img/myearworms-tn.jpg +title: My Earworms +--- diff --git a/docs/content/showcase/neavey.net.md b/docs/content/showcase/neavey.net.md new file mode 100644 index 000000000..443d2b670 --- /dev/null +++ b/docs/content/showcase/neavey.net.md @@ -0,0 +1,14 @@ +--- +date: 2016-12-21T21:19:46Z +description: "Adventure Miles - A personal travel blog." +license: "MIT" +licenseLink: "https://raw.githubusercontent.com/patrickn/neaveynet-hugo/master/LICENSE.md" +sitelink: http://neavey.net/ +sourceLink: https://github.com/patrickn/neaveynet-hugo +tags: +- personal +- travel +- blog +thumbnail: /img/neavey-tn.jpg +title: neavey.net +--- diff --git a/docs/content/showcase/nickoneill.md b/docs/content/showcase/nickoneill.md new file mode 100644 index 000000000..405a9f945 --- /dev/null +++ b/docs/content/showcase/nickoneill.md @@ -0,0 +1,15 @@ +--- +lastmod: 2014-08-26 +date: 2014-08-26T12:15:48-04:00 +description: "" +license: "" +licenseLink: "" +sitelink: http://blog.nickoneill.name/ +sourceLink: https://github.com/nickoneill/blog.nickoneill.name +tags: +- personal +- blog +thumbnail: /img/nickoneill-tn.jpg +title: authenticgeek +--- + diff --git a/docs/content/showcase/ninjaducks.in.md b/docs/content/showcase/ninjaducks.in.md new file mode 100644 index 000000000..46be436fc --- /dev/null +++ b/docs/content/showcase/ninjaducks.in.md @@ -0,0 +1,14 @@ +--- +lastmod: 2015-10-24 +date: 2015-10-24T14:06:00+05:30 +description: Personal blog +license: "" +licenseLink: "" +sitelink: http://ninjaducks.in +sourceLink: https://github.com/shivanshuag/shivanshuag.github.io/tree/new +tags: +- personal +- blog +thumbnail: /img/ninjaducks-tn.png +title: ninjaducks.in +--- diff --git a/docs/content/showcase/ninya.io.md b/docs/content/showcase/ninya.io.md new file mode 100644 index 000000000..a7ac4fe99 --- /dev/null +++ b/docs/content/showcase/ninya.io.md @@ -0,0 +1,15 @@ +--- +lastmod: 2014-08-26 +date: 2014-08-26T09:47:00-04:00 +description: "" +license: "" +licenseLink: "" +sitelink: http://blog.ninya.io/ +sourceLink: https://github.com/ninya-io/ninya-io.github.io/tree/dev +tags: +- project +- blog +thumbnail: /img/ninya-tn.jpg +title: ninya.io +--- + diff --git a/docs/content/showcase/nodesk.md b/docs/content/showcase/nodesk.md new file mode 100644 index 000000000..8656c7ec0 --- /dev/null +++ b/docs/content/showcase/nodesk.md @@ -0,0 +1,11 @@ +--- +lastmod: 2015-08-26 +date: 2015-08-26T22:33:00Z +description: "NoDesk | All Things Digital Nomad" +sitelink: http://nodesk.co/ +tags: +- digital nomad +- web +thumbnail: /img/nodesk-tn.png +title: nodesk.co +--- diff --git a/docs/content/showcase/novelist-xyz.md b/docs/content/showcase/novelist-xyz.md new file mode 100644 index 000000000..e0b59fb41 --- /dev/null +++ b/docs/content/showcase/novelist-xyz.md @@ -0,0 +1,13 @@ +--- +date: 2016-05-22T17:54:51+08:00 +description: "Peter Y. Chuang - Novelist, Short Story Writer" +license: "" +licenseLink: "" +sitelink: https://novelist.xyz +sourceLink: https://github.com/peterychuang/peterychuang.github.io/tree/source +tags: +- personal +- blog +thumbnail: /img/novelist-xyz.png +title: Peter Y. Chuang +--- diff --git a/docs/content/showcase/npf.md b/docs/content/showcase/npf.md new file mode 100644 index 000000000..b11f6cbc4 --- /dev/null +++ b/docs/content/showcase/npf.md @@ -0,0 +1,15 @@ +--- +lastmod: 2015-01-27 +date: 2014-08-21T12:21:18-04:00 +description: "" +license: "" +licenseLink: "" +sitelink: http://npf.io/ +sourceLink: https://github.com/natefinch/npf +tags: +- personal +- blog +thumbnail: /img/npf-tn.jpg +title: npf.io +--- + diff --git a/docs/content/showcase/nutspubcrawl.md b/docs/content/showcase/nutspubcrawl.md new file mode 100644 index 000000000..bb5ddf6d8 --- /dev/null +++ b/docs/content/showcase/nutspubcrawl.md @@ -0,0 +1,15 @@ +--- +date: 2017-06-03T07:32:00Z +description: "" +license: "" +licenseLink: "" +sitelink: https://nutspubcrawl.com/ +sourceLink: https://github.com/jeremielondon/nuts-hugo +tags: +- company +- multilingual +- London +thumbnail: /img/nutspubcrawl.jpg +title: Nutspubcrawl.com +--- + diff --git a/docs/content/showcase/ocul-maps.md b/docs/content/showcase/ocul-maps.md new file mode 100644 index 000000000..516bda399 --- /dev/null +++ b/docs/content/showcase/ocul-maps.md @@ -0,0 +1,14 @@ +--- +date: 2017-05-09T11:04:33-04:00 +description: "The Ontario Council of University Libraries Historical Topographic Maps Digitization Project" +license: "" +licenseLink: "" +sitelink: http://ocul.on.ca/topomaps +sourcelink: https://github.com/scholarsportal/historical-topos +tags: +- education +- project +- multilingual +thumbnail: /img/ocul-maps.png +title: OCUL topographic maps +--- diff --git a/docs/content/showcase/petanikode.md b/docs/content/showcase/petanikode.md new file mode 100644 index 000000000..7ebd51b9f --- /dev/null +++ b/docs/content/showcase/petanikode.md @@ -0,0 +1,12 @@ +--- +date: 2017-01-10T07:32:00Z +description: "Programmer Pengguna Linux" +license: "" +licenseLink: "" +sitelink: http://petanikode.com/ +tags: +- programming +- blog +thumbnail: /img/petanikode.png +title: Petani Kode +--- diff --git a/docs/content/showcase/peteraba.md b/docs/content/showcase/peteraba.md new file mode 100644 index 000000000..c00373e54 --- /dev/null +++ b/docs/content/showcase/peteraba.md @@ -0,0 +1,15 @@ +--- +lastmod: 2014-08-26 +date: 2014-08-26T11:30:57-04:00 +description: "" +license: "" +licenseLink: "" +sitelink: https://peteraba.com/ +sourceLink: https://github.com/peteraba/peteraba.com +tags: +- personal +- blog +thumbnail: /img/peteraba-tn.jpg +title: peteraba +--- + diff --git a/docs/content/showcase/picturingjordan.md b/docs/content/showcase/picturingjordan.md new file mode 100644 index 000000000..c0005312a --- /dev/null +++ b/docs/content/showcase/picturingjordan.md @@ -0,0 +1,14 @@ +--- +date: 2017-01-22T17:32:00Z +description: "Sharing Jordan with the world — one picture at a time." +license: "Creative Commons Attribution 4.0 International" +licenseLink: https://creativecommons.org/licenses/by-sa/4.0/ +sitelink: https://picturingjordan.com/ +sourceLink: https://github.com/alanorth/picturingjordan.com +tags: +- personal +- blog +thumbnail: /img/picturingjordan-tn.png +title: picturingjordan.com +--- + diff --git a/docs/content/showcase/promotive.md b/docs/content/showcase/promotive.md new file mode 100644 index 000000000..45e99d1a1 --- /dev/null +++ b/docs/content/showcase/promotive.md @@ -0,0 +1,15 @@ +--- +date: 2017-02-21T12:26:26+01:00 +description: "Corporate website a event management agency" +license: "" +licenseLink: "" +sitelink: https://promotive.es +tags: +- company +- corporate +- spanish +- event management +- bootstrap +thumbnail: /img/promotive.png +title: Promotive +--- diff --git a/docs/content/showcase/rahulrai.md b/docs/content/showcase/rahulrai.md new file mode 100644 index 000000000..1970ad683 --- /dev/null +++ b/docs/content/showcase/rahulrai.md @@ -0,0 +1,13 @@ +--- +date: 2016-10-04T21:01:18+01:00 +description: "My Take on Cloud" +license: "" +licenseLink: "" +sitelink: https://rahulrai.in +sourceLink: https://github.com/moonytheloony/Blog-Web +tags: +- personal +- blog +thumbnail: /img/rahulrai_in-tn.png +title: My Take on Cloud +--- diff --git a/docs/content/showcase/rakutentech.md b/docs/content/showcase/rakutentech.md new file mode 100644 index 000000000..b78b3e8b1 --- /dev/null +++ b/docs/content/showcase/rakutentech.md @@ -0,0 +1,14 @@ +--- +lastmod: 2016-01-20 +date: 2016-01-20T07:32:00Z +description: "" +license: MIT +licenseLink: "" +sitelink: http://techblog.rakuten.co.jp/ +tags: +- company +- blog +thumbnail: /img/rakutentech-tn.png +title: Rakuten Tech Blog +--- + diff --git a/docs/content/showcase/rdegges.md b/docs/content/showcase/rdegges.md new file mode 100644 index 000000000..2d7589b60 --- /dev/null +++ b/docs/content/showcase/rdegges.md @@ -0,0 +1,14 @@ +--- +date: 2016-08-05T15:33:56-07:00 +description: "The personal website of Randall Degges." +license: "Unlicense" +licenseLink: "http://unlicense.org/" +sitelink: https://www.rdegges.com/ +sourceLink: https://github.com/rdegges/rdegges-www +tags: +- personal +- blog +thumbnail: /img/rdegges-tn.png +title: Randall Degges +--- + diff --git a/docs/content/showcase/readtext.md b/docs/content/showcase/readtext.md new file mode 100644 index 000000000..330d89ca2 --- /dev/null +++ b/docs/content/showcase/readtext.md @@ -0,0 +1,12 @@ +--- +lastmod: 2015-11-16 +date: 2015-11-16T08:36:00Z +description: Restored text files +sitelink: http://readtext.org/ +tags: +- textfiles +- reading +thumbnail: /img/readtext-tn.png +title: ReadText +--- + diff --git a/docs/content/showcase/richardsumilang.md b/docs/content/showcase/richardsumilang.md new file mode 100644 index 000000000..8d4e97d09 --- /dev/null +++ b/docs/content/showcase/richardsumilang.md @@ -0,0 +1,17 @@ +--- +lastmod: 2015-09-08 +date: 2015-09-07T00:12:00-07:00 +description: "Personal website dedicated to electronics, programming, and reviews." +license: "MIT" +licenseLink: "https://opensource.org/licenses/MIT" +sitelink: http://richardsumilang.com/ +sourceLink: https://github.com/richardsumilang-blog +tags: +- personal +- blog +- technical +- electronics +- reviews +thumbnail: /img/richardsumilang-tn.png +title: Richard Sumilang - Top Secret Labs +--- diff --git a/docs/content/showcase/rick-cogley-info.md b/docs/content/showcase/rick-cogley-info.md new file mode 100644 index 000000000..997a45757 --- /dev/null +++ b/docs/content/showcase/rick-cogley-info.md @@ -0,0 +1,16 @@ +--- +lastmod: 2016-02-01 +date: 2015-05-20T04:32:00Z +description: Rick Cogley's personal site, powered by Hugo. +license: MIT +licenseLink: "" +sitelink: http://rick.cogley.info/ +sourceLink: https://github.com/RickCogley/RCC-Hugo2015 +tags: +- personal +- blog +- rickcogley +- japan +thumbnail: /img/rick_cogley_info-tn.jpg +title: rick.cogley.info +--- diff --git a/docs/content/showcase/ridingbytes.md b/docs/content/showcase/ridingbytes.md new file mode 100644 index 000000000..29fbb2ecf --- /dev/null +++ b/docs/content/showcase/ridingbytes.md @@ -0,0 +1,16 @@ +--- +lastmod: 2015-10-01 +date: 2015-09-27T00:00:00Z +description: Official Company Website +license: "" +licenseLink: "" +sitelink: http://ridingbytes.com/ +tags: +- company +- website +- blog +- tech +thumbnail: /img/ridingbytes-tn.png +title: RIDING BYTES +--- + diff --git a/docs/content/showcase/robertbasic.md b/docs/content/showcase/robertbasic.md new file mode 100644 index 000000000..237fce837 --- /dev/null +++ b/docs/content/showcase/robertbasic.md @@ -0,0 +1,15 @@ +--- +lastmod: 2016-03-26 +date: 2016-03-26T17:47:40+01:00 +description: "Robert Basic is a web developer from Serbia." +license: "" +licenseLink: "" +sitelink: http://robertbasic.com/ +sourceLink: https://github.com/robertbasic/robertbasic.com-hugo +tags: +- personal +- blog +thumbnail: /img/robertbasic-tn.png +title: Robert Basic's blog +--- + diff --git a/docs/content/showcase/sanjay-saxena.md b/docs/content/showcase/sanjay-saxena.md new file mode 100644 index 000000000..df869414d --- /dev/null +++ b/docs/content/showcase/sanjay-saxena.md @@ -0,0 +1,14 @@ +--- +date: 2017-04-13T07:32:00Z +description: "" +license: "" +licenseLink: "" +sitelink: https://sanjay-saxena.github.io +sourceLink: https://github.com/sanjay-saxena/sanjay-saxena-hugo +tags: +- personal +- blog +thumbnail: /img/sanjay-saxena-tn.png +title: sanjay-saxena.github.io +--- + diff --git a/docs/content/showcase/scottcwilson.md b/docs/content/showcase/scottcwilson.md new file mode 100644 index 000000000..9d07cf123 --- /dev/null +++ b/docs/content/showcase/scottcwilson.md @@ -0,0 +1,15 @@ +--- +lastmod: 2015-08-08 +date: 2015-07-21T10:00:00Z +description: Personal portfolio, created with Hugo +license: MIT +licenseLink: "" +sitelink: http://scottcwilson.github.io/ +sourceLink: https://github.com/scottcwilson/hugosite +tags: +- personal +- blog +thumbnail: /img/scottcwilson-tn.png +title: scottcwilson.com +--- + diff --git a/docs/content/showcase/shapeshed.md b/docs/content/showcase/shapeshed.md new file mode 100644 index 000000000..8e8908d00 --- /dev/null +++ b/docs/content/showcase/shapeshed.md @@ -0,0 +1,14 @@ +--- +date: 2016-10-10T07:32:00Z +description: Personal blog. +license: "" +licenseLink: "" +sitelink: http://shapeshed.com/ +sourceLink: https://github.com/shapeshed/shapeshed.com +tags: +- personal +- blog +thumbnail: /img/shapeshed-tn.png +title: shapeshed.com +--- + diff --git a/docs/content/showcase/shelan.md b/docs/content/showcase/shelan.md new file mode 100644 index 000000000..230b7b04f --- /dev/null +++ b/docs/content/showcase/shelan.md @@ -0,0 +1,14 @@ +--- +lastmod: 2016-02-07 +date: 2016-02-07T10:30:00Z +description: Shelan's Blog +license: MIT +licenseLink: "" +sitelink: http://shelan.org/ +sourceLink: https://github.com/shelan/my-hugo-site +tags: +- personal +- blog +thumbnail: /img/shelan-tn.png +title: shelan.org +--- \ No newline at end of file diff --git a/docs/content/showcase/siba.md b/docs/content/showcase/siba.md new file mode 100644 index 000000000..21f6f2a5f --- /dev/null +++ b/docs/content/showcase/siba.md @@ -0,0 +1,16 @@ +--- +date: 2017-08-01T08:12:00Z +description: "All-in-One Intelligent Bot for Business" +license: "" +licenseLink: "" +sitelink: http://siba.ai/ +tags: +- corporate +- saas +- software +- chatbot +- blog +thumbnail: /img/siba-tn.png +title: Siba Chatbot +--- + diff --git a/docs/content/showcase/silvergeko.md b/docs/content/showcase/silvergeko.md new file mode 100644 index 000000000..3e0105f30 --- /dev/null +++ b/docs/content/showcase/silvergeko.md @@ -0,0 +1,12 @@ +--- +date: 2016-03-28T14:16:59+02:00 +description: "Custom theme of small italian software house" +license: "" +licenseLink: "" +sitelink: http://silvergeko.it/ +tags: +- profesional +thumbnail: /img/silvergeko.jpg +title: Silvergeko +--- + diff --git a/docs/content/showcase/softinio.md b/docs/content/showcase/softinio.md new file mode 100644 index 000000000..fdbd5f364 --- /dev/null +++ b/docs/content/showcase/softinio.md @@ -0,0 +1,15 @@ +--- +lastmod: 2016-03-11 +date: 2015-11-29T07:16:53-05:00 +description: Salar Rahmanian Blog +license: MIT +licenseLink: https://raw.githubusercontent.com/softinio/softinio.com/master/LICENSE +sitelink: http://www.softinio.com/ +sourceLink: https://github.com/softinio/softinio.com +tags: +- personal +- technical +- blog +thumbnail: /img/softinio-tn.png +title: Salar Rahmanian +--- diff --git a/docs/content/showcase/spf13.md b/docs/content/showcase/spf13.md new file mode 100644 index 000000000..e0b524255 --- /dev/null +++ b/docs/content/showcase/spf13.md @@ -0,0 +1,15 @@ +--- +lastmod: 2015-01-27 +date: 2013-07-01T07:32:00Z +description: The first Hugo powered website. +license: MIT +licenseLink: "" +sitelink: http://spf13.com/ +sourceLink: https://github.com/spf13/spf13.com +tags: +- personal +- blog +thumbnail: /img/spf13-tn.jpg +title: spf13.com +--- + diff --git a/docs/content/showcase/steambap.md b/docs/content/showcase/steambap.md new file mode 100644 index 000000000..439c12e8b --- /dev/null +++ b/docs/content/showcase/steambap.md @@ -0,0 +1,13 @@ +--- +date: 2016-07-28T07:32:00Z +description: Weilin's blog +license: MIT +licenseLink: "" +sitelink: http://weilinshi.org/ +sourceLink: https://github.com/steambap/weilinshi.org +tags: +- personal +- blog +thumbnail: /img/steambap.png +title: weilinshi +--- diff --git a/docs/content/showcase/stefano.chiodino.md b/docs/content/showcase/stefano.chiodino.md new file mode 100644 index 000000000..5880eb9ed --- /dev/null +++ b/docs/content/showcase/stefano.chiodino.md @@ -0,0 +1,14 @@ +--- +date: 2016-05-21T21:25:25+01:00 +description: "Personal site + blog" +license: "" +licenseLink: "" +sitelink: https://stefano.chiodino.uk/ +sourceLink: https://github.com/Draga/go-web +tags: +- personal +- blog +thumbnail: /img/stefano.chiodino-tn.jpg +title: stefano.chiodino.uk +--- + diff --git a/docs/content/showcase/stou.md b/docs/content/showcase/stou.md new file mode 100644 index 000000000..ddd4b313b --- /dev/null +++ b/docs/content/showcase/stou.md @@ -0,0 +1,15 @@ +--- +lastmod: 2015-01-27 +date: 2014-11-23T01:28:16+07:00 +description: "Rasmus Stougaard" +license: "" +licenseLink: "" +sitelink: http://stou.dk/ +sourceLink: "https://github.com/stou/stou.github.io" +tags: +- personal +- blog +thumbnail: /img/stou-tn.png +title: stou.dk +--- + diff --git a/docs/content/showcase/szymonkatra.md b/docs/content/showcase/szymonkatra.md new file mode 100644 index 000000000..1be52b94c --- /dev/null +++ b/docs/content/showcase/szymonkatra.md @@ -0,0 +1,15 @@ +--- +lastmod: 2015-07-10 +date: 2015-07-10T16:15:00+01:00 +description: Szymon Katra +license: "" +licenseLink: "" +sitelink: http://szymonkatra.github.io/ +sourceLink: https://github.com/SzymonKatra/SzymonKatra.github.io/tree/master/hugo_project +tags: +- personal +- blog +thumbnail: /img/szymonkatra-tn.png +title: szymonkatra.github.io +--- + diff --git a/docs/content/showcase/techmadeplain.md b/docs/content/showcase/techmadeplain.md new file mode 100644 index 000000000..25712751d --- /dev/null +++ b/docs/content/showcase/techmadeplain.md @@ -0,0 +1,14 @@ +--- +lastmod: 2015-01-27 +date: 2014-05-22T19:54:00Z +description: Tech Coaching site +license: "" +licenseLink: "" +sitelink: http://techmadeplain.com/ +tags: +- personal +- blog +thumbnail: /img/techmadeplain-tn.jpg +title: Tech Made Plain +--- + diff --git a/docs/content/showcase/tendermint.md b/docs/content/showcase/tendermint.md new file mode 100644 index 000000000..d95e3c090 --- /dev/null +++ b/docs/content/showcase/tendermint.md @@ -0,0 +1,14 @@ +--- +lastmod: 2014-08-26 +date: 2014-08-26T09:34:42-04:00 +description: "" +license: "" +licenseLink: "" +sitelink: http://tendermint.com/ +sourceLink: https://github.com/tendermint/tendermint.github.io +tags: +- project +thumbnail: /img/tendermint-tn.jpg +title: tendermint +--- + diff --git a/docs/content/showcase/thecodeking.md b/docs/content/showcase/thecodeking.md new file mode 100644 index 000000000..86786d736 --- /dev/null +++ b/docs/content/showcase/thecodeking.md @@ -0,0 +1,13 @@ +--- +date: 2016-10-09T23:06:16+01:00 +description: "Personal site for articles and projects." +license: "" +licenseLink: "" +sitelink: http://thecodeking.co.uk +tags: +- personal +- blog +thumbnail: /img/thecodeking-tn.jpg +title: thecodeking +--- + diff --git a/docs/content/showcase/thehome.md b/docs/content/showcase/thehome.md new file mode 100644 index 000000000..1db5706f3 --- /dev/null +++ b/docs/content/showcase/thehome.md @@ -0,0 +1,15 @@ +--- +lastmod: 2015-08-08 +date: 2014-12-27T20:00:00+07:00 +description: "Tom Helmer Hansen" +license: "" +licenseLink: "" +sitelink: http://www.thehome.dk/ +sourceLink: "https://github.com/tomhelmer/website-source" +tags: +- personal +- blog +thumbnail: /img/thehome-tn.png +title: thehome.dk +--- + diff --git a/docs/content/showcase/thislittleduck.md b/docs/content/showcase/thislittleduck.md new file mode 100644 index 000000000..295b9cd32 --- /dev/null +++ b/docs/content/showcase/thislittleduck.md @@ -0,0 +1,11 @@ +--- +date: 2017-06-04T21:30:00+10:00 +description: "Software company website" +sitelink: https://thislittleduck.com +tags: +- business +- portfolio +thumbnail: /img/thislittleduck-tn.png +title: This Little Duck +--- + diff --git a/docs/content/showcase/tibobeijen.nl.md b/docs/content/showcase/tibobeijen.nl.md new file mode 100644 index 000000000..37db6491b --- /dev/null +++ b/docs/content/showcase/tibobeijen.nl.md @@ -0,0 +1,15 @@ +--- +date: 2017-03-18T11:30:00Z +description: "Blog of programmer Tibo Beijen" +license: "" +licenseLink: "" +sitelink: https://www.tibobeijen.nl/ +sourceLink: https://github.com/TBeijen/tbnl-hugo +tags: +- personal +- programming +- blog +thumbnail: /img/tibobeijen-nl-tn.png +title: tibobeijen.nl +--- + diff --git a/docs/content/showcase/ttsreader.md b/docs/content/showcase/ttsreader.md new file mode 100644 index 000000000..90f02763b --- /dev/null +++ b/docs/content/showcase/ttsreader.md @@ -0,0 +1,15 @@ +--- +date: 2017-05-19T07:32:00Z +description: "Online Text to Speech App and Content" +license: "" +licenseLink: "" +sitelink: http://ttsreader.com/ +tags: +- text to speech +- app +- blog +- showcase +- company +thumbnail: /img/ttsreader-tn.png +title: TTSReader +--- diff --git a/docs/content/showcase/tutorialonfly.md b/docs/content/showcase/tutorialonfly.md new file mode 100644 index 000000000..7c975aaa9 --- /dev/null +++ b/docs/content/showcase/tutorialonfly.md @@ -0,0 +1,15 @@ +--- +lastmod: 2016-04-15 +date: 2016-04-15T01:28:16+08:00 +description: "Tutorialonfly.com provide free tutorials in gitbook version online and pdf,epub,mobi etc for offline read, it's a great website built on hugo, thanks spf13 who created hugo" +license: "" +licenseLink: "" +sitelink: https://tutorialonfly.com/ +tags: +- tutorials +- free +- ebook download +- fast +thumbnail: /img/tutorialonfly-tn.jpg +title: Tutorialonfly +--- \ No newline at end of file diff --git a/docs/content/showcase/tutswiki.md b/docs/content/showcase/tutswiki.md new file mode 100644 index 000000000..72bd65844 --- /dev/null +++ b/docs/content/showcase/tutswiki.md @@ -0,0 +1,19 @@ +--- +lastmod: 2017-05-23 +date: 2017-05-23T07:33:00Z +description: "Collaborative tutorials for the internet" +license: "" +licenseLink: "" +sitelink: https://tutswiki.com/ +sourceLink: https://github.com/TutsWiki/source +tags: +- tutorial +- wiki +- computer science +- programming +- documentation +- books +thumbnail: /img/tutswiki-tn.jpg +title: TutsWiki.com +--- + diff --git a/docs/content/showcase/ucsb.md b/docs/content/showcase/ucsb.md new file mode 100644 index 000000000..9f9ff77e7 --- /dev/null +++ b/docs/content/showcase/ucsb.md @@ -0,0 +1,14 @@ +--- +lastmod: 2014-11-25 +date: 2014-08-26T14:12:55-04:00 +description: "" +license: "" +licenseLink: "" +sitelink: http://philosophy.ucsb.edu/ +sourceLink: https://github.com/ucsbphil/philweb +tags: +- education +thumbnail: /img/ucsb-tn.jpg +title: ucsb +--- + diff --git a/docs/content/showcase/upbeat.md b/docs/content/showcase/upbeat.md new file mode 100644 index 000000000..7c5b0fe57 --- /dev/null +++ b/docs/content/showcase/upbeat.md @@ -0,0 +1,13 @@ +--- +date: 2016-06-12T11:57:53+02:00 +description: "Blog by Rocchi Cesare" +license: "" +licenseLink: "" +sitelink: http://upbeat.it/ +sourceLink: +tags: +- personal +- blog +thumbnail: /img/upbeat.png +title: upbeat +--- diff --git a/docs/content/showcase/vamp.md b/docs/content/showcase/vamp.md new file mode 100644 index 000000000..a2591dbb8 --- /dev/null +++ b/docs/content/showcase/vamp.md @@ -0,0 +1,16 @@ +--- +lastmod: 2015-08-08 +date: 2014-06-26T15:45:00Z +description: Vamp.io microservices platform homepage and documentation +license: "Apache 2" +licenseLink: "" +sitelink: http://vamp.io/ +tags: +- tech +- documentation +- company +- api +thumbnail: /img/vamp_landingpage-tn.png +title: Vamp.io +--- + diff --git a/docs/content/showcase/viglug.org.md b/docs/content/showcase/viglug.org.md new file mode 100644 index 000000000..a9e78290a --- /dev/null +++ b/docs/content/showcase/viglug.org.md @@ -0,0 +1,15 @@ +--- +lastmod: 2016-03-01 +date: 2016-03-01T11:20:00Z +description: ViGLug.org is the website of the Linux User Group of Milan Est (Italy) +license: "AGPLv3" +licenseLink: "" +sitelink: http://viglug.org/ +tags: +- tech +- linux +- user group +thumbnail: /img/viglug-tn.png +title: Viglug.org +--- + diff --git a/docs/content/showcase/vurt.co.md b/docs/content/showcase/vurt.co.md new file mode 100644 index 000000000..6da6aa09d --- /dev/null +++ b/docs/content/showcase/vurt.co.md @@ -0,0 +1,15 @@ +--- +lastmod: 2014-08-26 +date: 2014-08-26T12:09:39-04:00 +description: "" +license: "" +licenseLink: "" +sitelink: http://vurt.co.uk/ +sourceLink: https://github.com/gilesp/vurtcouk +tags: +- personal +- blog +thumbnail: /img/vurt.co-tn.jpg +title: vurt.co.uk +--- + diff --git a/docs/content/showcase/worldtowriters.md b/docs/content/showcase/worldtowriters.md new file mode 100644 index 000000000..d28fc3e43 --- /dev/null +++ b/docs/content/showcase/worldtowriters.md @@ -0,0 +1,14 @@ +--- +lastmod: 2017-05-12 +date: 2017-05-12T22:00:00Z +description: "A translation service agency site" +license: MIT +licenseLink: "" +sitelink: https://worldtowriters.com +tags: +- company +- translation +thumbnail: /img/worldtowriters-com.jpg +title: World to Writers translation site +--- + diff --git a/docs/content/showcase/yslow-rules.md b/docs/content/showcase/yslow-rules.md new file mode 100644 index 000000000..ccc84bfbb --- /dev/null +++ b/docs/content/showcase/yslow-rules.md @@ -0,0 +1,16 @@ +--- +lastmod: 2015-08-08 +date: 2014-04-07T10:45:00Z +description: Community project of YSlow rules translations +license: MIT License +licenseLink: https://raw.github.com/checkmyws/yslow-rules/master/LICENSE +sitelink: http://checkmyws.github.io/yslow-rules/ +sourceLink: https://github.com/checkmyws/yslow-rules +tags: +- community +- documentation +- translation +thumbnail: /img/yslow-rules-tn.png +title: YSlow Rules +--- + diff --git a/docs/content/showcase/ysqi.md b/docs/content/showcase/ysqi.md new file mode 100644 index 000000000..56bf06bd1 --- /dev/null +++ b/docs/content/showcase/ysqi.md @@ -0,0 +1,13 @@ +--- +date: 2016-03-25T09:05:49+08:00 +description: "虞双齐个人博客" +license: "MIT" +licenseLink: "https://opensource.org/licenses/MIT" +sitelink: https://www.yushuangqi.com/ +sourceLink: https://github.com/ysqi/yushuangqi.com/ +tags: +- personal +- blog +thumbnail: /img/ysqi-blog.png +title: yushuangqi-blog +--- diff --git a/docs/content/showcase/yulinling.net.md b/docs/content/showcase/yulinling.net.md new file mode 100644 index 000000000..b5a6559f1 --- /dev/null +++ b/docs/content/showcase/yulinling.net.md @@ -0,0 +1,14 @@ +--- +lastmod: 2015-12-19 +date: 2015-09-09T21:42:00-04:00 +description: Multilingual, blog +license: "" +licenseLink: "" +sitelink: https://yulinling.net/ +sourceLink: https://bitbucket.org/lynxiayel/yulinling_source_public +tags: +- blog +- documentation +thumbnail: /img/yulinling-tn.jpg +title: 语林灵 (Yulinling) +--- diff --git a/docs/content/taxonomies/displaying.md b/docs/content/taxonomies/displaying.md new file mode 100644 index 000000000..43dd48ba8 --- /dev/null +++ b/docs/content/taxonomies/displaying.md @@ -0,0 +1,137 @@ +--- +aliases: +- /indexes/displaying/ +lastmod: 2016-06-29 +date: 2013-07-01 +linktitle: Displaying +menu: + main: + parent: taxonomy +next: /taxonomies/templates +prev: /taxonomies/usage +title: Displaying Taxonomies +weight: 20 +toc: true +--- + +There are four common ways you can display the data in your +taxonomies in addition to the automatic taxonomy pages created by hugo +using the [list templates](/templates/list/): + +1. For a given piece of content, you can list the terms attached +2. For a given piece of content, you can list other content with the same + term +3. You can list all terms for a taxonomy +4. You can list all taxonomies (with their terms) + +## 1. Displaying taxonomy terms assigned to this content + +Within your content templates, you may wish to display +the taxonomies that that piece of content is assigned to. + +Because we are leveraging the front matter system to +define taxonomies for content, the taxonomies assigned to +each content piece are located in the usual place +(.Params.`plural`). + +### Example + +
      + {{ range .Params.tags }} +
    • {{ . }}
    • + {{ end }} +
    + +If you want to list taxonomies inline, you will have to take +care of optional plural ending in the title (if multiple taxonomies), +as well as commas. Let's say we have a taxonomy "directors" such as +`directors: [ "Joel Coen", "Ethan Coen" ]` in the TOML-format front matter. +To list such taxonomy use the following: + +### Example + + {{ if .Params.directors }} + Director{{ if gt (len .Params.directors) 1 }}s{{ end }}: + {{ range $index, $director := .Params.directors }}{{ if gt $index 0 }}, {{ end }}{{ . }}{{ end }} + {{ end }} + +Alternatively, you may use the [delimit]({{< relref "templates/functions.md#delimit" >}}) +template function as a shortcut if the taxonomies should just be listed +with a separator. See {{< gh 2143 >}} on GitHub for discussion. + +## 2. Listing content with the same taxonomy term + +First, you may be asking why you would use this. If you are using a +taxonomy for something like a series of posts, this is exactly how you +would do it. It’s also an quick and dirty way to show some related +content. + + +### Example + + + +## 3. Listing all content in a given taxonomy + +This would be very useful in a sidebar as “featured content”. You could +even have different sections of “featured content” by assigning +different terms to the content. + +### Example + + + + +## 4. Rendering a Site's Taxonomies + +If you wish to display the list of all keys for a taxonomy, you can find retrieve +them from the `.Site` variable which is available on every page. + +This may take the form of a tag cloud, a menu or simply a list. + +The following example displays all tag keys: + +### Example + +
      + {{ range $name, $taxonomy := .Site.Taxonomies.tags }} +
    • {{ $name }}
    • + {{ end }} +
    + +### Complete Example +This example will list all taxonomies, each of their keys and all the content assigned to each key. + +
    +
      + {{ range $taxonomyname, $taxonomy := .Site.Taxonomies }} +
    • {{ $taxonomyname }} +
        + {{ range $key, $value := $taxonomy }} +
      • {{ $key }}
      • + + {{ end }} +
      +
    • + {{ end }} +
    +
    diff --git a/docs/content/taxonomies/methods.md b/docs/content/taxonomies/methods.md new file mode 100644 index 000000000..c5b9e755b --- /dev/null +++ b/docs/content/taxonomies/methods.md @@ -0,0 +1,69 @@ +--- +lastmod: 2015-12-23 +date: 2014-05-26 +linktitle: Structure & Methods +menu: + main: + parent: taxonomy +next: /extras/aliases +prev: /taxonomies/ordering +title: Using Taxonomies +weight: 75 +--- + +Hugo makes a set of values and methods available on the various Taxonomy structures. + +## Taxonomy Methods + +A Taxonomy is a `map[string]WeightedPages`. + +.Get(term) +: Returns the WeightedPages for a term. + +.Count(term) +: The number of pieces of content assigned to this term. + +.Alphabetical +: Returns an OrderedTaxonomy (slice) ordered by Term. + +.ByCount +: Returns an OrderedTaxonomy (slice) ordered by number of entries. + +## OrderedTaxonomy + +Since Maps are unordered, an OrderedTaxonomy is a special structure that has a defined order. + +```go +[]struct { + Name string + WeightedPages WeightedPages +} +``` + +Each element of the slice has: + +.Term +: The Term used. + +.WeightedPages +: A slice of Weighted Pages. + +.Count +: The number of pieces of content assigned to this term. + +.Pages +: All Pages assigned to this term. All [list methods](/templates/list/) are available to this. + +## WeightedPages + +WeightedPages is simply a slice of WeightedPage. + +```go +type WeightedPages []WeightedPage +``` + +.Count(term) +: The number of pieces of content assigned to this term. + +.Pages +: Returns a slice of pages, which then can be ordered using any of the [list methods](/templates/list/). diff --git a/docs/content/taxonomies/ordering.md b/docs/content/taxonomies/ordering.md new file mode 100644 index 000000000..ac86bc69d --- /dev/null +++ b/docs/content/taxonomies/ordering.md @@ -0,0 +1,80 @@ +--- +aliases: +- /indexes/ordering/ +lastmod: 2015-12-23 +date: 2013-07-01 +linktitle: Ordering +menu: + main: + identifier: Ordering Taxonomies + parent: taxonomy +next: /taxonomies/methods +prev: /taxonomies/templates +title: Ordering Taxonomies +weight: 60 +toc: true +--- + +Hugo provides the ability to both: + + 1. Order the way the keys for a taxonomy are displayed + 2. Order the way taxonomyed content appears + + +## Ordering Taxonomies +Taxonomies can be ordered by either alphabetical key or by the number of content pieces assigned to that key. + +### Order Alphabetically Example + +
      + {{ $data := .Data }} + {{ range $key, $value := .Data.Taxonomy.Alphabetical }} +
    • {{ $value.Name }} {{ $value.Count }}
    • + {{ end }} +
    + +### Order by Popularity Example + +
      + {{ $data := .Data }} + {{ range $key, $value := .Data.Taxonomy.ByCount }} +
    • {{ $value.Name }} {{ $value.Count }}
    • + {{ end }} +
    + + +[See Also Taxonomy Lists]({{< relref "templates/list.md" >}}) + +## Ordering Content within Taxonomies + +Hugo uses both **Date** and **Weight** to order content within taxonomies. + +Each piece of content in Hugo can optionally be assigned a date. +It can also be assigned a weight for each taxonomy it is assigned to. + +When iterating over content within taxonomies the default sort is first by weight then by date. This means that if the weights for two pieces of content are the same, than the more recent content will be displayed first. The default weight for any piece of content is 0. + +### Assigning Weight + +Content can be assigned weight for each taxonomy that it's assigned to. + +```toml ++++ +tags = [ "a", "b", "c" ] +tags_weight = 22 +categories = ["d"] +title = "foo" +categories_weight = 44 ++++ +Front Matter with weighted tags and categories +``` + +The convention is `taxonomyname_weight`. + +In the above example, this piece of content has a weight of 22 which applies to the sorting when rendering the pages assigned to the "a", "b" and "c" values of the 'tag' taxonomy. + +It has also been assigned the weight of 44 when rendering the 'd' category. + +With this the same piece of content can appear in different positions in different taxonomies. + +Currently taxonomies only support the default ordering of content which is weight -> date. diff --git a/docs/content/taxonomies/overview.md b/docs/content/taxonomies/overview.md new file mode 100644 index 000000000..a01e63980 --- /dev/null +++ b/docs/content/taxonomies/overview.md @@ -0,0 +1,95 @@ +--- +aliases: +- /indexes/overview/ +- /doc/indexes/ +- /extras/indexes +lastmod: 2015-08-04 +date: 2013-07-01 +linktitle: Overview +menu: + main: + identifier: taxonomy overview + parent: taxonomy +next: /taxonomies/usage +prev: /templates/404 +title: Taxonomy Overview +weight: 10 +--- + +Hugo includes support for user-defined groupings of content called +taxonomies.[^1] Taxonomies give us a way to classify our content so we can +demonstrate relationships in a variety of logical ways. + +[^1]: Taxonomies were called *indexes* in Hugo prior to v0.11. + +The default taxonomies for Hugo are *tags* and *categories*. These +taxonomies are common to many website systems (e.g. WordPress, Drupal, +Jekyll). Unlike all of those systems, Hugo makes it trivial to customize +the taxonomies you will be using for your site however you wish. Another +good use for taxonomies is to group a set of posts into a series. Other +common uses would include *categories*, *tags*, *groups*, *series* and many +more. + +When taxonomies are used (and templates are provided), Hugo will +automatically create pages listing all of the taxonomies, their terms +and all of the content attached to those terms. + +## Definitions + +**Taxonomy:** A categorization that can be used to classify content + +**Term:** A key within that taxonomy + +**Value:** A piece of content assigned to that Term + +## Example + +For example, if I was writing about movies, I may want the following +taxonomies: + +* Actors +* Directors +* Studios +* Genre +* Year +* Awards + +I would then specify in each movie’s front-matter the specific terms for +each of those taxonomies. Hugo would then automatically create pages for +each Actor, Director, Studio, Genre, Year and Award listing all of the +Movies that matched that specific Actor, Director, etc. + + +### Taxonomy Organization + +Let’s use an example to demonstrate the different labels in action. +From the perspective of the taxonomy, it could be visualized as: + + Actor <- Taxonomy + Bruce Willis <- Term + The Six Sense <- Content + Unbreakable <- Content + Moonrise Kingdom <- Content + Samuel L. Jackson <- Term + Unbreakable <- Content + The Avengers <- Content + xXx <- Content + +From the perspective of the content, it would appear differently, though +the data and labels used are the same: + + Unbreakable <- Content + Actors <- Taxonomy + Bruce Willis <- Term + Samuel L. Jackson <- Term + Director <- Taxonomy + M. Night Shyamalan <- Term + ... + Moonrise Kingdom <- Content + Actors <- Taxonomy + Bruce Willis <- Term + Bill Murray <- Term + Director <- Taxonomy + Wes Anderson <- Term + ... + diff --git a/docs/content/taxonomies/templates.md b/docs/content/taxonomies/templates.md new file mode 100644 index 000000000..fdb6cd530 --- /dev/null +++ b/docs/content/taxonomies/templates.md @@ -0,0 +1,40 @@ +--- +aliases: +- /indexes/templates/ +lastmod: 2014-05-29 +date: 2013-07-01 +linktitle: Templates +menu: + main: + parent: taxonomy +next: /taxonomies/ordering +prev: /templates/displaying +title: Taxonomy Templates +weight: 30 +--- + +Taxonomy templates should be placed in the folder `layouts/taxonomy`. +When Taxonomy term template is provided for a Taxonomy, a section is rendered for it at `/SINGULAR/`. (eg. `/tag/` or `/category/`) + +There are two different templates that the use of taxonomies will require you to provide: + +### All content attached to taxonomy + +A [taxonomy terms template](/templates/terms/) is a template which has access to all the full Taxonomy structure. +This Template is commonly used to generate the list of terms for a given template. + + +#### layouts/taxonomy/SINGULAR.terms.html + +For example: `tag.terms.html`, `category.terms.html`, or your custom Taxonomy: `actor.terms.html` + +### All content attached to term + +A [list template](/templates/list/) is used to automatically generate pages for each unique term found. + + +#### layouts/taxonomy/SINGULAR.html + +For example: `tag.html`, `category.html`, or your custom Taxonomy: `actor.html` + +Terms are rendered at `SINGULAR/TERM/`. (eg. `/tag/book/` or `/category/news/`) diff --git a/docs/content/taxonomies/usage.md b/docs/content/taxonomies/usage.md new file mode 100644 index 000000000..9c5dc2189 --- /dev/null +++ b/docs/content/taxonomies/usage.md @@ -0,0 +1,109 @@ +--- +lastmod: 2015-12-23 +date: 2014-05-26 +linktitle: Usage +toc: true +menu: + main: + parent: taxonomy +next: /taxonomies/displaying +prev: /taxonomies/overview +title: Using Taxonomies +weight: 15 +--- + +## Defining taxonomies for a site + +Taxonomies must be defined in the site configuration before they can be +used throughout the site. You need to provide both the plural and +singular labels for each taxonomy. + +Here is an example configuration in TOML and YAML +that specifies three taxonomies (the default two, plus `series`). + +Notice the format is singular key = "plural value" for TOML, +or singular key: "plural value" for YAML: + + + + + + + + + + + + + +
    config.toml excerpt:config.yaml excerpt:
    [taxonomies]
    +tag = "tags"
    +category = "categories"
    +series = "series"
    +
    taxonomies:
    +  tag: "tags"
    +  category: "categories"
    +  series: "series"
    +
    + +## Assigning taxonomy values to content + +Once an taxonomy is defined at the site level, any piece of content +can be assigned to it regardless of content type or section. + +Assigning content to an taxonomy is done in the front matter. +Simply create a variable with the *plural* name of the taxonomy +and assign all terms you want to apply to this content. + +## Preserving taxonomy values + +By default, taxonomy names are hyphenated, lower-cased and normalized, and then +fixed and titleized on the archive page. + +However, if you want to have a taxonomy value with special characters +such as `Gérard Depardieu` instead of `Gerard Depardieu`, +you need to set the `preserveTaxonomyNames` [site configuration](/overview/configuration/) variable to `true`. +Hugo will then preserve special characters in taxonomy values +but will still titleize the values for titles and normalize them in URLs. + +Note that if you use `preserveTaxonomyNames` and intend to manually construct URLs to the archive pages, +you will need to pass the taxonomy values through the `urlize` template function. + +## Front Matter Example (in TOML) + +```toml ++++ +title = "Hugo: A fast and flexible static site generator" +tags = [ "Development", "Go", "fast", "Blogging" ] +categories = [ "Development" ] +series = [ "Go Web Dev" ] +slug = "hugo" +project_url = "https://github.com/gohugoio/hugo" ++++ +``` + +## Front Matter Example (in JSON) + +```json +{ + "title": "Hugo: A fast and flexible static site generator", + "tags": [ + "Development", + "Go", + "fast", + "Blogging" + ], + "categories" : [ + "Development" + ], + "series" : [ + "Go Web Dev" + ], + "slug": "hugo", + "project_url": "https://github.com/gohugoio/hugo" +} +``` + +## Add content file with frontmatter + +See [Source Organization]({{< relref "overview/source-directory.md#content-for-home-page-and-other-list-pages" >}}). diff --git a/docs/content/templates/404.md b/docs/content/templates/404.md new file mode 100644 index 000000000..0dc2e2c9c --- /dev/null +++ b/docs/content/templates/404.md @@ -0,0 +1,55 @@ +--- +aliases: +- /layout/404/ +lastmod: 2015-12-30 +date: 2013-08-21 +linktitle: "Custom 404 page" +menu: + main: + parent: layout +next: /taxonomies/overview +notoc: true +next: /templates/debugging +prev: /templates/sitemap +title: 404.html Templates +weight: 100 +--- + +When using Hugo with [GitHub Pages](http://pages.github.com/), you can provide +your own template for a [custom 404 error page](https://help.github.com/articles/custom-404-pages/) +by creating a 404.html template file in your `/layouts` folder. +When Hugo generates your site, the `404.html` file will be placed in the root. + +404 pages will have all the regular [page +variables](/layout/variables/) available to use in the templates. + +In addition to the standard page variables, the 404 page has access to +all site content accessible from `.Data.Pages`. + + ▾ layouts/ + 404.html + +## 404.html + +This is a basic example of a 404.html template: + + {{ partial "header.html" . }} + {{ partial "subheader.html" . }} + +
    +
    +

    {{ .Title }}

    +
    +
    + + {{ partial "footer.html" . }} + +### Automatic Loading + +Your 404.html file can be set to load automatically when a visitor enters a mistaken URL path, dependent upon the web serving environment you are using. For example: + +* _GitHub Pages_ - it's automatic. +* _Apache_ - one way is to specify `ErrorDocument 404 /404.html` in an `.htaccess` file in the root of your site. +* _Nginx_ - you might specify `error_page 404 = /404.html;` in your `nginx.conf` file. +* _Amazon AWS S3_ - when setting a bucket up for static web serving, you can specify the error file. +* _Caddy Server_ - using `errors { 404 /404.html }`. [Details here](https://caddyserver.com/docs/errors) diff --git a/docs/content/templates/ace.md b/docs/content/templates/ace.md new file mode 100644 index 000000000..1bd85796a --- /dev/null +++ b/docs/content/templates/ace.md @@ -0,0 +1,58 @@ +--- +aliases: +- /doc/templates/ace +- /layout/templates/ace +- /layout/ace/ +lastmod: 2015-08-04 +date: 2014-04-20 +linktitle: Ace templates +menu: + main: + parent: layout +next: /templates/functions +prev: /templates/go-templates +title: Ace Templates +weight: 17 +--- + +In addition to [Go templates](/templates/go-templates) and [Amber](/templates/amber) templates, Hugo supports the powerful Ace templates. + +For template documentation, follow the links from the [Ace project](https://github.com/yosssi/ace). + +* Ace templates must be named with the ace-suffix, e.g. `list.ace` +* It's possible to use both Go templates and Ace templates side-by-side, and include one into the other +* Full Go template syntax support, including all the useful helper funcs +* Partials can be included both with the Ace and the Go template syntax: + * `= include partials/foo.html .`[^ace-theme] + * `{{ partial "foo" . }}` + + +One noticeable difference between Ace and the others is the inheritance support through [base and inner templates](https://github.com/yosssi/ace/tree/master/examples/base_inner_template). + +In Hugo the base template will be chosen with the same ruleset as for [Go templates](/templates/blocks/). + + +.: +index.ace + +./blog: +single.ace +baseof.ace + +./_default: +baseof.ace list.ace single.ace single-baseof.ace +``` + +Some examples for the layout files above: + +* Home page: `./index.ace` + `./_default/baseof.ace` +* Single page in the `blog` section: `./blog/single.ace` + `./blog/baseof.ace` +* Single page in another section: `./_default/single.ace` + `./_default/single-baseof.ace` +* Taxonomy page in any section: `./_default/list.ace` + `./_default/baseof.ace` + +**Note:** In most cases one `baseof.ace` in `_default` will suffice. +**Note:** An Ace template without a reference to a base section, e.g. `= content`, will be handled as a standalone template. + + +[^ace-theme]: Note that the `html` suffix is needed, even if the filename is suffixed `ace`. This does not work from inside a theme, see [issue 763](https://github.com/gohugoio/hugo/issues/763). + diff --git a/docs/content/templates/amber.md b/docs/content/templates/amber.md new file mode 100644 index 000000000..2ba84353b --- /dev/null +++ b/docs/content/templates/amber.md @@ -0,0 +1,27 @@ +--- +aliases: +- /doc/templates/amber +- /layout/templates/amber +- /layout/amber/ +lastmod: 2015-11-05 +date: 2015-07-20 +linktitle: Amber templates +menu: + main: + parent: layout +next: /templates/functions +prev: /templates/go-templates +title: Amber Templates +weight: 18 +--- + +Amber templates are another template type which Hugo supports, in addition to [Go templates](/templates/go-templates) and [Ace templates]({{< relref "templates/ace.md" >}}) templates. + +For template documentation, follow the links from the [Amber project](https://github.com/eknkc/amber) + +* Amber templates must be named with the amber-suffix, e.g. `list.amber` +* Partials in Amber or HTML can be included with the Amber template syntax: + * `import ../partials/test.html ` + * `import ../partials/test_a.amber ` + + diff --git a/docs/content/templates/blocks.md b/docs/content/templates/blocks.md new file mode 100644 index 000000000..f80a3d908 --- /dev/null +++ b/docs/content/templates/blocks.md @@ -0,0 +1,110 @@ +--- +date: 2016-03-29T21:26:20-05:00 +menu: + main: + parent: layout +prev: /templates/views/ +next: /templates/partials/ +title: Block Templates +weight: 80 +--- + +The `block` keyword in Go templates allows you to define the outer shell of your pages one or more master template(s), filling in or overriding portions as necessary. + +## Base template lookup + +In version `0.20` Hugo introduced custom [Output Formats]({{< relref "extras/output-formats.md" >}}), all of which can have their own templates that also can use a base template if needed. + +This introduced two new terms relevant in the lookup of the templates, the media type's `Suffix` and the output format's `Name`. + +Given the above, Hugo tries to use the most specific base tamplate it finds: + +1. /layouts/_current-path_/_template-name_-baseof.[output-format].[suffix], e.g. list-baseof.amp.html. +1. /layouts/_current-path_/_template-name_-baseof.[suffix], e.g. list-baseof.html. +2. /layouts/_current-path_/baseof.[output-format].[suffix], e.g baseof.amp.html +2. /layouts/_current-path_/baseof.[suffix], e.g baseof.html +3. /layouts/_default/_template-name_-baseof.[output-format].[suffix] e.g. list-baseof.amp.html. +3. /layouts/_default/_template-name_-baseof.[suffix], e.g. list-baseof.html. +4. /layouts/_default/baseof.[output-format].[suffix] +4. /layouts/_default/baseof.[suffix] + +For each of the steps above, it will first look in the project, then, if theme is set, in the theme's layouts folder. Hugo picks the first base template found. + +As an example, with a site using the theme `exampletheme`, when rendering the section list for the section `post` for the output format `Calendar`. Hugo picks the `section/post.calendar.ics` as the template and this template has a `define` section that indicates it needs a base template. This is then the lookup order: + +1. `/layouts/section/post-baseof.calendar.ics` +1. `/layouts/section/post-baseof.ics` +2. `/themes/exampletheme/layouts/section/post-baseof.calendar.ics` +2. `/themes/exampletheme/layouts/section/post-baseof.ics` +3. `/layouts/section/baseof.calendar.ics` +3. `/layouts/section/baseof.ics` +4. `/themes/exampletheme/layouts/section/baseof.calendar.ics` +4. `/themes/exampletheme/layouts/section/baseof.ics` +5. `/layouts/_default/post-baseof.calendar.ics` +5. `/layouts/_default/post-baseof.ics` +6. `/themes/exampletheme/layouts/_default/post-baseof.calendar.ics` +6. `/themes/exampletheme/layouts/_default/post-baseof.ics` +7. `/layouts/_default/baseof.calendar.ics` +7. `/layouts/_default/baseof.ics` +8. `/themes/exampletheme/layouts/_default/baseof.calendar.ics` +8. `/themes/exampletheme/layouts/_default/baseof.ics` + + +## Define the base template + +Let's define a simple base template (`_default/baseof.html`), a shell from which all our pages will start. + +```html + + + + + {{ block "title" . }} + <!-- Blocks may include default content. --> + {{ .Site.Title }} + {{ end }} + + + + + {{ block "main" . }} + + {{ end }} + + + + +``` + +## Overriding the base + +Your [default list template]({{< relref "templates/list.md" >}}) (`_default/list.html`) will inherit all of the code defined in the base template. It could then implement its own "main" block from the base template above like so: + +```html + +{{ define "main" }} +

    Posts

    + {{ range .Data.Pages }} +
    +

    {{ .Title }}

    + {{ .Content }} +
    + {{ end }} +{{ end }} +``` + +This replaces the contents of our (basically empty) "main" block with something useful for the list template. In this case, we didn't define a "title" block so the contents from our base template remain unchanged in lists. + +In our [default single template]({{< relref "templates/content.md" >}}) (`_default/single.html`), let's implement both blocks: + +```html +{{ define "title" }} + {{ .Title }} – {{ .Site.Title }} +{{ end }} +{{ define "main" }} +

    {{ .Title }}

    + {{ .Content }} +{{ end }} +``` + +This overrides both block areas from the base template with code unique to our single template. diff --git a/docs/content/templates/content.md b/docs/content/templates/content.md new file mode 100644 index 000000000..24d1782ad --- /dev/null +++ b/docs/content/templates/content.md @@ -0,0 +1,167 @@ +--- +aliases: +- /layout/content/ +lastmod: 2015-05-22 +date: 2013-07-01 +linktitle: Single Content +menu: + main: + parent: layout +next: /templates/list +prev: /templates/variables +title: Single Content Template +weight: 30 +toc: true +--- + +The primary view of content in Hugo is the single view. Hugo, for every +Markdown file provided, will render it with a single template. + + +## Which Template will be rendered? +Hugo uses a set of rules to figure out which template to use when +rendering a specific page. + +Hugo will use the following prioritized list. If a file isn’t present, +then the next one in the list will be used. This enables you to craft +specific layouts when you want to without creating more templates +than necessary. For most sites, only the `_default` file at the end of +the list will be needed. + +Users can specify the `type` and `layout` in the [front-matter](/content/front-matter/). `Section` +is determined based on the content file’s location. If `type` is provided, +it will be used instead of `section`. + +### Single Page + +* /layouts/`TYPE`/`LAYOUT`.html +* /layouts/`SECTION`/`LAYOUT`.html +* /layouts/`TYPE`/single.html +* /layouts/`SECTION`/single.html +* /layouts/_default/single.html +* /themes/`THEME`/layouts/`TYPE`/`LAYOUT`.html +* /themes/`THEME`/layouts/`SECTION`/`LAYOUT`.html +* /themes/`THEME`/layouts/`TYPE`/single.html +* /themes/`THEME`/layouts/`SECTION`/single.html +* /themes/`THEME`/layouts/_default/single.html + +## Example Single Template File + +Content pages are of the type "page" and have all the [page +variables](/layout/variables/) and [site +variables](/templates/variables/) available to use in the templates. + +In the following examples we have created two different content types as well as +a default content type. + +The default content template to be used in the event that a specific +template has not been provided for that type. The default type works the +same as the other types, but the directory must be called "\_default". + + ▾ layouts/ + ▾ _default/ + single.html + ▾ post/ + single.html + ▾ project/ + single.html + + +### post/single.html +This content template is used for [spf13.com](http://spf13.com/). +It makes use of [partial templates](/templates/partials/) + + {{ partial "header.html" . }} + {{ partial "subheader.html" . }} + {{ $baseURL := .Site.BaseURL }} + +
    +

    {{ .Title }}

    +
    +
    + {{ .Content }} +
    +
    +
    + + + + {{ partial "disqus.html" . }} + {{ partial "footer.html" . }} + + +### project/single.html +This content template is used for [spf13.com](http://spf13.com/). +It makes use of [partial templates](/templates/partials/) + + + {{ partial "header.html" . }} + {{ partial "subheader.html" . }} + {{ $baseURL := .Site.BaseURL }} + +
    +

    {{ .Title }}

    +
    +
    + {{ .Content }} +
    +
    +
    + + + + {{if isset .Params "project_url" }} + + {{ end }} + + {{ partial "footer.html" . }} + +Notice how the project/single.html template uses an additional parameter unique +to this template. This doesn't need to be defined ahead of time. If the key is +present in the front matter than it can be used in the template. To +easily generate new content of this type with these keys ready use +[content archetypes](/content/archetypes/). diff --git a/docs/content/templates/debugging.md b/docs/content/templates/debugging.md new file mode 100644 index 000000000..ae348f533 --- /dev/null +++ b/docs/content/templates/debugging.md @@ -0,0 +1,62 @@ +--- +aliases: +- /doc/debugging/ +- /layout/debugging/ +lastmod: 2015-05-25 +date: 2015-05-22 +linktitle: Debugging +menu: + main: + parent: layout +prev: /templates/404 +title: Template Debugging +weight: 110 +--- + + +# Template Debugging + +Here are some snippets you can add to your template to answer some common questions. +These snippets use the `printf` function available in all Go templates. This function is +an alias to the Go function, [fmt.Printf](http://golang.org/pkg/fmt/). + + +### What variables are available in this context? + +You can use the template syntax, `$.`, to get the top-level template context +from anywhere in your template. This will print out all the values under, `.Site`. + + {{ printf "%#v" $.Site }} + +This will print out the value of `.Permalink`: + + {{ printf "%#v" .Permalink }} + +This will print out a list of all the variables scoped to the current context +(a.k.a. The dot, "`.`"). + + {{ printf "%#v" . }} + +When writing a [Homepage](/templates/homepage), what does one of the pages +you're looping through look like? + +``` +{{ range .Data.Pages }} + {{/* The context, ".", is now a Page */}} + {{ printf "%#v" . }} +{{ end }} +``` + +### Why do I have no variables defined? + +Check that you are passing variables in the `partial` function. For example + +``` +{{ partial "header" }} +``` + +will render the header partial, but the header partial will not have access to any variables. You need to pass variables explicitly. For example: + +``` +{{ partial "header" . }} +``` diff --git a/docs/content/templates/functions.md b/docs/content/templates/functions.md new file mode 100644 index 000000000..99fb56d68 --- /dev/null +++ b/docs/content/templates/functions.md @@ -0,0 +1,1094 @@ +--- +aliases: +- /layout/functions/ +lastmod: 2015-09-20 +date: 2013-07-01 +linktitle: Functions +toc: true +menu: + main: + parent: layout +next: /templates/variables +prev: /templates/go-templates +title: Hugo Template Functions +weight: 20 +--- + +Hugo uses the excellent Go html/template library for its template engine. +It is an extremely lightweight engine that provides a very small amount of +logic. In our experience, it is just the right amount of logic to be able +to create a good static website. + +Go templates are lightweight but extensible. Hugo has added the following +functions to the basic template logic. + +(Go itself supplies built-in functions, including comparison operators +and other basic tools; these are listed in the +[Go template documentation](http://golang.org/pkg/text/template/#hdr-Functions).) + +## General + +### default +Checks whether a given value is set and returns a default value if it is not. +"Set" in this context means non-zero for numeric types and times; +non-zero length for strings, arrays, slices, and maps; +any boolean or struct value; or non-nil for any other types. + +e.g. + + {{ index .Params "font" | default "Roboto" }} → default is "Roboto" + {{ default "Roboto" (index .Params "font") }} → default is "Roboto" + +### delimit +Loops through any array, slice or map and returns a string of all the values separated by the delimiter. There is an optional third parameter that lets you choose a different delimiter to go between the last two values. +Maps will be sorted by the keys, and only a slice of the values will be returned, keeping a consistent output order. + +Works on [lists](/templates/list/), [taxonomies](/taxonomies/displaying/), [terms](/templates/terms/), [groups](/templates/list/) + +e.g. + + // Front matter + +++ + tags: [ "tag1", "tag2", "tag3" ] + +++ + + // Used anywhere in a template + Tags: {{ delimit .Params.tags ", " }} + + // Outputs Tags: tag1, tag2, tag3 + + // Example with the optional "last" parameter + Tags: {{ delimit .Params.tags ", " " and " }} + + // Outputs Tags: tag1, tag2 and tag3 + +### dict +Creates a dictionary `(map[string, interface{})`, expects parameters added in value:object fashion. +Invalid combinations like keys that are not strings or uneven number of parameters, will result in an exception thrown. +Useful for passing maps to partials when adding to a template. + +e.g. Pass into "foo.html" a map with the keys "important, content" + + {{$important := .Site.Params.SomethingImportant }} + {{range .Site.Params.Bar}} + {{partial "foo" (dict "content" . "important" $important)}} + {{end}} + +"foo.html" + + Important {{.important}} + {{.content}} + +or create a map on the fly to pass into + + {{partial "foo" (dict "important" "Smiles" "content" "You should do more")}} + + + +### slice + +`slice` allows you to create an array (`[]interface{}`) of all arguments that you pass to this function. + +One use case is the concatenation of elements in combination with `delimit`: + +```html +{{ delimit (slice "foo" "bar" "buzz") ", " }} + +``` + + +### shuffle + +`shuffle` returns a random permutation of a given array or slice, e.g. + +```html +{{ shuffle (seq 1 5) }} + + +{{ shuffle (slice "foo" "bar" "buzz") }} + +``` + +### echoParam +Prints a parameter if it is set. + +e.g. `{{ echoParam .Params "project_url" }}` + + +### eq +Returns true if the parameters are equal. + +e.g. + + {{ if eq .Section "blog" }}current{{ end }} + + +### first +Slices an array to only the first _N_ elements. + +Works on [lists](/templates/list/), [taxonomies](/taxonomies/displaying/), [terms](/templates/terms/), [groups](/templates/list/) + +e.g. + + {{ range first 10 .Data.Pages }} + {{ .Render "summary" }} + {{ end }} + + +### jsonify +Encodes a given object to JSON. + +e.g. + + {{ dict "title" .Title "content" .Plain | jsonify }} + +### last +Slices an array to only the last _N_ elements. + +Works on [lists](/templates/list/), [taxonomies](/taxonomies/displaying/), [terms](/templates/terms/), [groups](/templates/list/) + +e.g. + + {{ range last 10 .Data.Pages }} + {{ .Render "summary" }} + {{ end }} + +### after +Slices an array to only the items after the Nth item. Use this in combination +with `first` to use both halves of an array split at item _N_. + +Works on [lists](/templates/list/), [taxonomies](/taxonomies/displaying/), [terms](/templates/terms/), [groups](/templates/list/) + +e.g. + + {{ range after 10 .Data.Pages }} + {{ .Render "title" }} + {{ end }} + +### getenv +Returns the value of an environment variable. + +Takes a string containing the name of the variable as input. Returns +an empty string if the variable is not set, otherwise returns the +value of the variable. Note that in Unix-like environments, the +variable must also be exported in order to be seen by `hugo`. + +e.g. + + {{ getenv "HOME" }} + + +### in +Checks if an element is in an array (or slice) and returns a boolean. +The elements supported are strings, integers and floats (only float64 will match as expected). +In addition, it can also check if a substring exists in a string. + +e.g. + + {{ if in .Params.tags "Git" }}Follow me on GitHub!{{ end }} + +or + + {{ if in "this string contains a substring" "substring" }}Substring found!{{ end }} + + +### intersect +Given two arrays (or slices), this function will return the common elements in the arrays. +The elements supported are strings, integers and floats (only float64). + +A useful example of this functionality is a 'similar posts' block. +Create a list of links to posts where any of the tags in the current post match any tags in other posts. + +e.g. + +
      + {{ $page_link := .Permalink }} + {{ $tags := .Params.tags }} + {{ range .Site.Pages }} + {{ $page := . }} + {{ $has_common_tags := intersect $tags .Params.tags | len | lt 0 }} + {{ if and $has_common_tags (ne $page_link $page.Permalink) }} +
    • {{ $page.Title }}
    • + {{ end }} + {{ end }} +
    + + +### union +Given two arrays (or slices) A and B, this function will return a new array that contains the elements or objects that belong to either A or to B or to both. The elements supported are strings, integers and floats (only float64). + +``` +{{ union (slice 1 2 3) (slice 3 4 5) }} + + +{{ union (slice 1 2 3) nil }} + + +{{ union nil (slice 1 2 3) }} + + +{{ union nil nil }} + +``` + +### isset +Returns true if the parameter is set. +Takes either a slice, array or channel and an index or a map and a key as input. + +e.g. `{{ if isset .Params "project_url" }} {{ index .Params "project_url" }}{{ end }}` + +### seq + +Creates a sequence of integers. It's named and used as GNU's seq. + +Some examples: + +* `3` => `1, 2, 3` +* `1 2 4` => `1, 3` +* `-3` => `-1, -2, -3` +* `1 4` => `1, 2, 3, 4` +* `1 -2` => `1, 0, -1, -2` + +### sort +Sorts maps, arrays and slices, returning a sorted slice. +A sorted array of map values will be returned, with the keys eliminated. +There are two optional arguments, which are `sortByField` and `sortAsc`. +If left blank, sort will sort by keys (for maps) in ascending order. + +Works on [lists](/templates/list/), [taxonomies](/taxonomies/displaying/), [terms](/templates/terms/), [groups](/templates/list/) + +e.g. + + // Front matter + +++ + tags: [ "tag3", "tag1", "tag2" ] + +++ + + // Site config + +++ + [params.authors] + [params.authors.Derek] + "firstName" = "Derek" + "lastName" = "Perkins" + [params.authors.Joe] + "firstName" = "Joe" + "lastName" = "Bergevin" + [params.authors.Tanner] + "firstName" = "Tanner" + "lastName" = "Linsley" + +++ + + // Use default sort options - sort by key / ascending + Tags: {{ range sort .Params.tags }}{{ . }} {{ end }} + + // Outputs Tags: tag1 tag2 tag3 + + // Sort by value / descending + Tags: {{ range sort .Params.tags "value" "desc" }}{{ . }} {{ end }} + + // Outputs Tags: tag3 tag2 tag1 + + // Use default sort options - sort by value / descending + Authors: {{ range sort .Site.Params.authors }}{{ .firstName }} {{ end }} + + // Outputs Authors: Derek Joe Tanner + + // Use default sort options - sort by value / descending + Authors: {{ range sort .Site.Params.authors "lastName" "desc" }}{{ .lastName }} {{ end }} + + // Outputs Authors: Perkins Linsley Bergevin + + +### where +Filters an array to only elements containing a matching value for a given field. + +Works on [lists](/templates/list/), [taxonomies](/taxonomies/displaying/), [terms](/templates/terms/), [groups](/templates/list/) + +e.g. + + {{ range where .Data.Pages "Section" "post" }} + {{ .Content }} + {{ end }} + +It can be used with dot chaining second argument to refer a nested element of a value. + +e.g. + + // Front matter on some pages + +++ + series: golang + +++ + + {{ range where .Site.Pages "Params.series" "golang" }} + {{ .Content }} + {{ end }} + +It can also be used with an operator like `!=`, `>=`, `in` etc. Without an operator (like above), `where` compares a given field with a matching value in a way like `=` is specified. + +e.g. + + {{ range where .Data.Pages "Section" "!=" "post" }} + {{ .Content }} + {{ end }} + +Following operators are now available + +- `=`, `==`, `eq`: True if a given field value equals a matching value +- `!=`, `<>`, `ne`: True if a given field value doesn't equal a matching value +- `>=`, `ge`: True if a given field value is greater than or equal to a matching value +- `>`, `gt`: True if a given field value is greater than a matching value +- `<=`, `le`: True if a given field value is lesser than or equal to a matching value +- `<`, `lt`: True if a given field value is lesser than a matching value +- `in`: True if a given field value is included in a matching value. A matching value must be an array or a slice +- `not in`: True if a given field value isn't included in a matching value. A matching value must be an array or a slice +- `intersect`: True if a given field value that is a slice / array of strings or integers contains elements in common with the matching value. It follows the same rules as the intersect function. + +*`intersect` operator, e.g.:* + + {{ range where .Site.Pages ".Params.tags" "intersect" .Params.tags }} + {{ if ne .Permalink $.Permalink }} + {{ .Render "summary" }} + {{ end }} + {{ end }} + +*`where` and `first` can be stacked, e.g.:* + + {{ range first 5 (where .Data.Pages "Section" "post") }} + {{ .Content }} + {{ end }} + +### Unset field +Filter only work for set fields. To check whether a field is set or exist, use operand `nil`. + +This can be useful to filter a small amount of pages from a large pool. Instead of set field on all pages, you can set field on required pages only. + +Only following operators are available for `nil` + +- `=`, `==`, `eq`: True if the given field is not set. +- `!=`, `<>`, `ne`: True if the given field is set. + +e.g. + + {{ range where .Data.Pages ".Params.specialpost" "!=" nil }} + {{ .Content }} + {{ end }} + + +### uniq + +Takes in a slice or array and returns a slice with subsequent duplicate elements removed. + + {{ uniq (slice 1 2 3 2) }} + {{ slice 1 2 3 2 | uniq }} + + +## Files + +### readDir + +Gets a directory listing from a directory relative to the current project working dir. + +So, If the project working dir has a single file named `README.txt`: + +`{{ range (readDir ".") }}{{ .Name }}{{ end }}` → "README.txt" + +### readFile +Reads a file from disk and converts it into a string. Note that the filename must be relative to the current project working dir. + So, if you have a file with the name `README.txt` in the root of your project with the content `Hugo Rocks!`: + + `{{readFile "README.txt"}}` → `"Hugo Rocks!"` + +### imageConfig +Parses the image and returns the height, width and color model. + +e.g. +``` +{{ with (imageConfig "favicon.ico") }} +favicon.ico: {{.Width}} x {{.Height}} +{{ end }} +``` + +## Math + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FunctionDescriptionExample
    addAdds two integers.{{add 1 2}} → 3
    divDivides two integers.{{div 6 3}} → 2
    math.LogNatural logarithm of one float.{{math.Log 1.0}} → 0
    modModulus of two integers.{{mod 15 3}} → 0
    modBoolBoolean of modulus of two integers. true if modulus is 0.{{modBool 15 3}} → true
    mulMultiplies two integers.{{mul 2 3}} → 6
    subSubtracts two integers.{{sub 3 2}} → 1
    + +## Numbers + +### int + +Creates an `int`. + +e.g. + +* `{{ int "123" }}` → 123 + +### lang.NumFmt + +`NumFmt` formats a number with the given precision using the *decimal*, +*grouping*, and *negative* options. The `options` parameter is a +string consisting of ` `. The default +`options` value is `- . ,`. + +Note that numbers are rounded up at 5 or greater. +So, with precision set to 0, 1.5 becomes `2`, and 1.4 becomes `1`. + +``` +{{ lang.NumFmt 2 12345.6789 }} → 12,345.68 +{{ lang.NumFmt 2 12345.6789 "- , ." }} → 12.345,68 +{{ lang.NumFmt 0 -12345.6789 "- . ," }} → -12,346 +{{ lang.NumFmt 6 -12345.6789 "- ." }} → -12345.678900 +{{ -98765.4321 | lang.NumFmt 2 }} → -98,765.43 +``` + +## Strings + +### printf + +Format a string using the standard `fmt.Sprintf` function. See [the go +doc](https://golang.org/pkg/fmt/) for reference. +A +e.g., `{{ i18n ( printf "combined_%s" $var ) }}` or `{{ printf "formatted %.2f" 3.1416 }}` + +### chomp +Removes any trailing newline characters. Useful in a pipeline to remove newlines added by other processing (including `markdownify`). + +e.g., `{{chomp "

    Blockhead

    \n"}}` → `"

    Blockhead

    "` + + +### dateFormat +Converts the textual representation of the datetime into the other form or returns it of Go `time.Time` type value. +These are formatted with the layout string. + +e.g. `{{ dateFormat "Monday, Jan 2, 2006" "2015-01-21" }}` → "Wednesday, Jan 21, 2015" + + +### emojify + +Runs the string through the Emoji emoticons processor. The result will be declared as "safe" so Go templates will not filter it. + +See the [Emoji cheat sheet](http://www.emoji-cheat-sheet.com/) for available emoticons. + +e.g. `{{ "I :heart: Hugo" | emojify }}` + +### highlight +Takes a string of code and a language, uses Pygments to return the syntax highlighted code in HTML. +Used in the [highlight shortcode](/extras/highlighting/). + +### htmlEscape +HtmlEscape returns the given string with the critical reserved HTML codes escaped, +such that `&` becomes `&` and so on. It escapes only: `<`, `>`, `&`, `'` and `"`. + +Bear in mind that, unless content is passed to `safeHTML`, output strings are escaped +usually by the processor anyway. + +e.g. +`{{ htmlEscape "Hugo & Caddy > Wordpress & Apache" }} → "Hugo & Caddy > Wordpress & Apache"` + +### htmlUnescape +HtmlUnescape returns the given string with html escape codes un-escaped. This +un-escapes more codes than `htmlEscape` escapes, including `#` codes and pre-UTF8 +escapes for accented characters. It defers completely to the Go `html.UnescapeString` +function, so functionality is consistent with that codebase. + +Remember to pass the output of this to `safeHTML` if fully unescaped characters +are desired, or the output will be escaped again as normal. + +e.g. +`{{ htmlUnescape "Hugo & Caddy > Wordpress & Apache" }} → "Hugo & Caddy > Wordpress & Apache"` + +### humanize +Humanize returns the humanized version of an argument with the first letter capitalized. +If the input is either an int64 value or the string representation of an integer, humanize returns the number with the proper ordinal appended. + +e.g. +``` +{{humanize "my-first-post"}} → "My first post" +{{humanize "myCamelPost"}} → "My camel post" +{{humanize "52"}} → "52nd" +{{humanize 103}} → "103rd" +``` + + +### lower +Converts all characters in string to lowercase. + +e.g. `{{lower "BatMan"}}` → "batman" + + +### markdownify + +Runs the string through the Markdown processor. The result will be declared as "safe" so Go templates will not filter it. + +e.g. `{{ .Title | markdownify }}` + +### plainify + +Strips any HTML and returns the plain text version. + +e.g. `{{ "BatMan" | plainify }}` → "BatMan" + +### pluralize +Pluralize the given word with a set of common English pluralization rules. + +e.g. `{{ "cat" | pluralize }}` → "cats" + +### findRE +Returns a list of strings that match the regular expression. By default all matches will be included. The number of matches can be limited with an optional third parameter. + +The example below returns a list of all second level headers (`

    `) in the content: + + {{ findRE "(.|\n)*?

    " .Content }} + +We can limit the number of matches in that list with a third parameter. Let's say we want to have at most one match (or none if no substring matched): + + {{ findRE "(.|\n)*?

    " .Content 1 }} + + +`findRE` allows us to build an automatically generated table of contents that could be used for a simple scrollspy: + + {{ $headers := findRE "(.|\n)*?" .Content }} + + {{ if ge (len $headers) 1 }} + + {{ end }} + +First, we try to find all second-level headers and generate a list if at least one header was found. `plainify` strips the HTML and `urlize` converts the header into a valid URL. + +### replace +Replaces all occurrences of the search string with the replacement string. + +e.g. `{{ replace "Batman and Robin" "Robin" "Catwoman" }}` → "Batman and Catwoman" + + +### replaceRE +Replaces all occurrences of a regular expression with the replacement pattern. + +e.g. `{{ replaceRE "^https?://([^/]+).*" "$1" "http://gohugo.io/docs" }}` → "gohugo.io" +e.g. `{{ "http://gohugo.io/docs" | replaceRE "^https?://([^/]+).*" "$1" }}` → "gohugo.io" + + +### safeHTML +Declares the provided string as a "safe" HTML document fragment +so Go html/template will not filter it. It should not be used +for HTML from a third-party, or HTML with unclosed tags or comments. + +Example: Given a site-wide `config.toml` that contains this line: + + copyright = "© 2015 Jane Doe. Some rights reserved." + +`{{ .Site.Copyright | safeHTML }}` would then output: + +> © 2015 Jane Doe. Some rights reserved. + +However, without the `safeHTML` function, html/template assumes +`.Site.Copyright` to be unsafe, escaping all HTML tags, +rendering the whole string as plain-text like this: + +
    +

    © 2015 Jane Doe. <a href="http://creativecommons.org/licenses/by/4.0/">Some rights reserved</a>.

    +
    + +### safeHTMLAttr +Declares the provided string as a "safe" HTML attribute +from a trusted source, for example, ` dir="ltr"`, +so Go html/template will not filter it. + +Example: Given a site-wide `config.toml` that contains this menu entry: + + [[menu.main]] + name = "IRC: #golang at freenode" + url = "irc://irc.freenode.net/#golang" + +* `` ⇒ `` (Bad!) +* `` ⇒ `` (Good!) + +### safeCSS +Declares the provided string as a known "safe" CSS string +so Go html/templates will not filter it. +"Safe" means CSS content that matches any of: + +1. The CSS3 stylesheet production, such as `p { color: purple }`. +2. The CSS3 rule production, such as `a[href=~"https:"].foo#bar`. +3. CSS3 declaration productions, such as `color: red; margin: 2px`. +4. The CSS3 value production, such as `rgba(0, 0, 255, 127)`. + +Example: Given `style = "color: red;"` defined in the front matter of your `.md` file: + +* `

    ` ⇒ `

    ` (Good!) +* `

    ` ⇒ `

    ` (Bad!) + +Note: "ZgotmplZ" is a special value that indicates that unsafe content reached a +CSS or URL context. + +### safeJS + +Declares the provided string as a known "safe" Javascript string so Go +html/templates will not escape it. "Safe" means the string encapsulates a known +safe EcmaScript5 Expression, for example, `(x + y * z())`. Template authors +are responsible for ensuring that typed expressions do not break the intended +precedence and that there is no statement/expression ambiguity as when passing +an expression like `{ foo:bar() }\n['foo']()`, which is both a valid Expression +and a valid Program with a very different meaning. + +Example: Given `hash = "619c16f"` defined in the front matter of your `.md` file: + +* `` ⇒ `` (Good!) +* `` ⇒ `` (Bad!) + +### singularize +Singularize the given word with a set of common English singularization rules. + +e.g. `{{ "cats" | singularize }}` → "cat" + +### slicestr + +Slicing in `slicestr` is done by specifying a half-open range with two indices, `start` and `end`. +For example, 1 and 4 creates a slice including elements 1 through 3. +The `end` index can be omitted; it defaults to the string's length. + +e.g. + +* `{{slicestr "BatMan" 3}}` → "Man" +* `{{slicestr "BatMan" 0 3}}` → "Bat" + +### truncate + +Truncate a text to a max length without cutting words or leaving unclosed HTML tags. Since Go templates are HTML-aware, truncate will handle normal strings vs HTML strings intelligently. It's important to note that if you have a raw string that contains HTML tags that you want treated as HTML, you will need to convert the string to HTML using the safeHTML template function before sending the value to truncate; otherwise, the HTML tags will be escaped by truncate. + +e.g. + +* `{{ "this is a text" | truncate 10 " ..." }}` → `this is a ...` +* `{{ "Keep my HTML" | safeHTML | truncate 10 }}` → `Keep my …` +* `{{ "With [Markdown](#markdown) inside." | markdownify | truncate 10 }}` → `With
    Markdown …` + +### split + +Split a string into substrings separated by a delimiter. + +e.g. + +* `{{split "tag1,tag2,tag3" "," }}` → ["tag1" "tag2" "tag3"] + +### string + +Creates a `string`. + +e.g. + +* `{{string "BatMan"}}` → "BatMan" + +### substr + +Extracts parts of a string, beginning at the character at the specified +position, and returns the specified number of characters. + +It normally takes two parameters: `start` and `length`. +It can also take one parameter: `start`, i.e. `length` is omitted, in which case +the substring starting from start until the end of the string will be returned. + +To extract characters from the end of the string, use a negative start number. + +In addition, borrowing from the extended behavior described at http://php.net/substr, +if `length` is given and is negative, then that many characters will be omitted from +the end of string. + +e.g. + +* `{{substr "BatMan" 0 -3}}` → "Bat" +* `{{substr "BatMan" 3 3}}` → "Man" + +### hasPrefix + +HasPrefix tests whether a string begins with prefix. + +* `{{ hasPrefix "Hugo" "Hu" }}` → true + +### title +Converts all characters in string to titlecase. + +e.g. `{{title "BatMan"}}` → "Batman" + + +### trim +Returns a slice of the string with all leading and trailing characters contained in cutset removed. + +e.g. `{{ trim "++Batman--" "+-" }}` → "Batman" + + +### upper +Converts all characters in string to uppercase. + +e.g. `{{upper "BatMan"}}` → "BATMAN" + + +### countwords + +`countwords` tries to convert the passed content to a string and counts each word +in it. The template functions works similar to [.WordCount]({{< relref "templates/variables.md#page-variables" >}}). + +```html +{{ "Hugo is a static site generator." | countwords }} + +``` + + +### countrunes + +Alternatively to counting all words , `countrunes` determines the number of runes in the content and excludes any whitespace. This can become useful if you have to deal with +CJK-like languages. + +```html +{{ "Hello, 世界" | countrunes }} + +``` + +### md5 + +`md5` hashes the given input and returns its MD5 checksum. + +```html +{{ md5 "Hello world, gophers!" }} + +``` + +This can be useful if you want to use Gravatar for generating a unique avatar: + +```html + +``` + + +### sha1 + +`sha1` hashes the given input and returns its SHA1 checksum. + +```html +{{ sha1 "Hello world, gophers!" }} + +``` + + +### sha256 + +`sha256` hashes the given input and returns its SHA256 checksum. + +```html +{{ sha256 "Hello world, gophers!" }} + +``` + + +## Internationalization + +### i18n + +This translates a piece of content based on your `i18n/en-US.yaml` +(and friends) files. You can use the [go-i18n](https://github.com/nicksnyder/go-i18n) tools to manage your translations. The translations can exist in both the theme and at the root of your repository. + +e.g.: `{{ i18n "translation_id" }}` + +For more information about string translations, see [Translation of strings]({{< relref "content/multilingual.md#translation-of-strings">}}). + +### T + +`T` is an alias to `i18n`. E.g. `{{ T "translation_id" }}`. + +## Times + +### time + +`time` converts a timestamp string into a [`time.Time`](https://godoc.org/time#Time) structure so you can access its fields. E.g. + +* `{{ time "2016-05-28" }}` → "2016-05-28T00:00:00Z" +* `{{ (time "2016-05-28").YearDay }}` → 149 +* `{{ mul 1000 (time "2016-05-28T10:30:00.00+10:00").Unix }}` → 1464395400000 (Unix time in milliseconds) + +### now + +`now` returns the current local time as a [`time.Time`](https://godoc.org/time#Time). + +## URLs +### absLangURL, relLangURL +These are similar to the `absURL` and `relURL` relatives below, but will add the correct language prefix when the site is configured with more than one language. + +So for a site `baseURL` set to `http://mysite.com/hugo/` and the current language is `en`: + +* `{{ "blog/" | absLangURL }}` → "http://mysite.com/hugo/en/blog/" +* `{{ "blog/" | relLangURL }}` → "/hugo/en/blog/" + +### absURL, relURL + +Both `absURL` and `relURL` considers the configured value of `baseURL`, so given a `baseURL` set to `http://mysite.com/hugo/`: + +* `{{ "mystyle.css" | absURL }}` → "http://mysite.com/hugo/mystyle.css" +* `{{ "mystyle.css" | relURL }}` → "/hugo/mystyle.css" +* `{{ "http://gohugo.io/" | relURL }}` → "http://gohugo.io/" +* `{{ "http://gohugo.io/" | absURL }}` → "http://gohugo.io/" + +The last two examples may look funky, but is useful if you, say, have a list of images, some of them hosted externally, some locally: + +``` + +``` + +The above also exploits the fact that the Go template parser JSON-encodes objects inside `script` tags. + + + +**Note:** These functions are smart about missing slashes, but will not add one to the end if not present. + + +### ref, relref +Looks up a content page by relative path or logical name to return the permalink (`ref`) or relative permalink (`relref`). Requires a `Page` object (usually satisfied with `.`). Used in the [`ref` and `relref` shortcodes]({{% ref "extras/crossreferences.md" %}}). + +e.g. {{ ref . "about.md" }} + +### safeURL +Declares the provided string as a "safe" URL or URL substring (see [RFC 3986][]). +A URL like `javascript:checkThatFormNotEditedBeforeLeavingPage()` from a trusted +source should go in the page, but by default dynamic `javascript:` URLs are +filtered out since they are a frequently exploited injection vector. + +[RFC 3986]: http://tools.ietf.org/html/rfc3986 + +Without `safeURL`, only the URI schemes `http:`, `https:` and `mailto:` +are considered safe by Go. If any other URI schemes, e.g. `irc:` and +`javascript:`, are detected, the whole URL would be replaced with +`#ZgotmplZ`. This is to "defang" any potential attack in the URL, +rendering it useless. + +Example: Given a site-wide `config.toml` that contains this menu entry: + + [[menu.main]] + name = "IRC: #golang at freenode" + url = "irc://irc.freenode.net/#golang" + +The following template: + + + +would produce `
  • IRC: #golang at freenode
  • ` +for the `irc://…` URL. + +To fix this, add ` | safeURL` after `.URL` on the 3rd line, like this: + +
  • {{ .Name }}
  • + +With this change, we finally get `
  • IRC: #golang at freenode
  • ` +as intended. + + +### urlize +Takes a string and sanitizes it for usage in URLs, converts spaces to "-". + +e.g. `{{ . }}` + + +### querify + +Takes a set of key-value pairs and returns a [query string](https://en.wikipedia.org/wiki/Query_string) that can be appended to a URL. E.g. + + Search + +will be rendered as + + Search + + +## Content Views + +### Render +Takes a view to render the content with. The view is an alternate layout, and should be a file name that points to a template in one of the locations specified in the documentation for [Content Views](/templates/views). + +This function is only available on a piece of content, and in list context. + +This example could render a piece of content using the content view located at `/layouts/_default/summary.html`: + + {{ range .Data.Pages }} + {{ .Render "summary"}} + {{ end }} + + + +## Advanced + +### apply + +Given a map, array, or slice, returns a new slice with a function applied over it. Expects at least three parameters, depending on the function being applied. The first parameter is the sequence to operate on; the second is the name of the function as a string, which must be in the Hugo function map (generally, it is these functions documented here). After that, the parameters to the applied function are provided, with the string `"."` standing in for each element of the sequence the function is to be applied against. An example is in order: + + +++ + names: [ "Derek Perkins", "Joe Bergevin", "Tanner Linsley" ] + +++ + + {{ apply .Params.names "urlize" "." }} → [ "derek-perkins", "joe-bergevin", "tanner-linsley" ] + +This is roughly equivalent to: + + {{ range .Params.names }}{{ . | urlize }}{{ end }} + +However, it isn’t possible to provide the output of a range to the `delimit` function, so you need to `apply` it. A more complete example should explain this. Let's say you have two partials for displaying tag links in a post, "post/tag/list.html" and "post/tag/link.html", as shown below. + + + {{ with .Params.tags }} +
    + Tags: + {{ $len := len . }} + {{ if eq $len 1 }} + {{ partial "post/tag/link" (index . 0) }} + {{ else }} + {{ $last := sub $len 1 }} + {{ range first $last . }} + {{ partial "post/tag/link" . }}, + {{ end }} + {{ partial "post/tag/link" (index . $last) }} + {{ end }} +
    + {{ end }} + + + + + +This works, but the complexity of "post/tag/list.html" is fairly high; the Hugo template needs to perform special behaviour for the case where there’s only one tag, and it has to treat the last tag as special. Additionally, the tag list will be rendered something like "Tags: tag1 , tag2 , tag3" because of the way that the HTML is generated and it is interpreted by a browser. + +This is Hugo. We have a better way. If this were your "post/tag/list.html" instead, all of those problems are fixed automatically (this first version separates all of the operations for ease of reading; the combined version will be shown after the explanation). + + + {{ with .Params.tags }} +
    + Tags: + {{ $sort := sort . }} + {{ $links := apply $sort "partial" "post/tag/link" "." }} + {{ $clean := apply $links "chomp" "." }} + {{ delimit $clean ", " }} +
    + {{ end }} + +In this version, we are now sorting the tags, converting them to links with "post/tag/link.html", cleaning off stray newlines, and joining them together in a delimited list for presentation. That can also be written as: + + + {{ with .Params.tags }} +
    + Tags: + {{ delimit (apply (apply (sort .) "partial" "post/tag/link" ".") "chomp" ".") ", " }} +
    + {{ end }} + +`apply` does not work when receiving the sequence as an argument through a pipeline. + +*** + +### base64Encode and base64Decode + +`base64Encode` and `base64Decode` let you easily decode content with a base64 encoding and vice versa through pipes. Let's take a look at an example: + + + {{ "Hello world" | base64Encode }} + + + {{ "SGVsbG8gd29ybGQ=" | base64Decode }} + + +You can also pass other datatypes as argument to the template function which tries +to convert them. Now we use an integer instead of a string: + + + {{ 42 | base64Encode | base64Decode }} + + +**Tip:** Using base64 to decode and encode becomes really powerful if we have to handle +responses of APIs. + + {{ $resp := getJSON "https://api.github.com/repos/gohugoio/hugo/readme" }} + {{ $resp.content | base64Decode | markdownify }} + +The response of the GitHub API contains the base64-encoded version of the [README.md](https://github.com/gohugoio/hugo/blob/master/README.md) in the Hugo repository. Now we can decode it and parse the Markdown. The final output will look similar to the rendered version on GitHub. + +*** + +### partialCached + +See [Template Partials]({{< relref "templates/partials.md#cached-partials" >}}) for an explanation of the `partialCached` template function. + + +## .Site.GetPage +Every `Page` has a `Kind` attribute that shows what kind of page it is. While this attribute can be used to list pages of a certain `kind` using `where`, often it can be useful to fetch a single page by its path. + +`GetPage` looks up an index page of a given `Kind` and `path`. This method may support regular pages in the future, but currently it is a convenient way of getting the index pages, such as the home page or a section, from a template: + + {{ with .Site.GetPage "section" "blog" }}{{ .Title }}{{ end }} + +This method wil return `nil` when no page could be found, so the above will not print anything if the blog section isn't found. + +The valid page kinds are: *home, section, taxonomy and taxonomyTerm.* diff --git a/docs/content/templates/go-templates.md b/docs/content/templates/go-templates.md new file mode 100644 index 000000000..bb7c71606 --- /dev/null +++ b/docs/content/templates/go-templates.md @@ -0,0 +1,443 @@ +--- +aliases: +- /layout/go-templates/ +- /layouts/go-templates/ +lastmod: 2015-11-30 +date: 2013-07-01 +menu: + main: + parent: layout +next: /templates/ace +prev: /templates/overview +title: Go Template Primer +weight: 15 +toc: true +--- + +Hugo uses the excellent [Go][] [html/template][gohtmltemplate] library for +its template engine. It is an extremely lightweight engine that provides a very +small amount of logic. In our experience it is just the right amount of +logic to be able to create a good static website. If you have used other +template systems from different languages or frameworks, you will find a lot of +similarities in Go templates. + +This document is a brief primer on using Go templates. The [Go docs][gohtmltemplate] +go into more depth and cover features that aren't mentioned here. + +## Introduction to Go Templates + +Go templates provide an extremely simple template language. It adheres to the +belief that only the most basic of logic belongs in the template or view layer. +One consequence of this simplicity is that Go templates parse very quickly. + +A unique characteristic of Go templates is they are content aware. Variables and +content will be sanitized depending on the context of where they are used. More +details can be found in the [Go docs][gohtmltemplate]. + +## Basic Syntax + +Go lang templates are HTML files with the addition of variables and +functions. + +**Go variables and functions are accessible within {{ }}** + +Accessing a predefined variable "foo": + + {{ foo }} + +**Parameters are separated using spaces** + +Calling the `add` function with input of 1, 2: + + {{ add 1 2 }} + +**Methods and fields are accessed via dot notation** + +Accessing the Page Parameter "bar" + + {{ .Params.bar }} + +**Parentheses can be used to group items together** + + {{ if or (isset .Params "alt") (isset .Params "caption") }} Caption {{ end }} + + +## Variables + +Each Go template has a struct (object) made available to it. In Hugo, each +template is passed page struct. More details are available on the +[variables](/layout/variables/) page. + +A variable is accessed by referencing the variable name. + + {{ .Title }} + +Variables can also be defined and referenced. + + {{ $address := "123 Main St."}} + {{ $address }} + + +## Functions + +Go template ships with a few functions which provide basic functionality. The Go +template system also provides a mechanism for applications to extend the +available functions with their own. [Hugo template +functions](/layout/functions/) provide some additional functionality we believe +are useful for building websites. Functions are called by using their name +followed by the required parameters separated by spaces. Template +functions cannot be added without recompiling Hugo. + +**Example 1: Adding numbers** + + {{ add 1 2 }} + +**Example 2: Comparing numbers** + + {{ lt 1 2 }} + +(There are more boolean operators, detailed in the +[template documentation](http://golang.org/pkg/text/template/#hdr-Functions).) + +## Includes + +When including another template, you will pass to it the data it will be +able to access. To pass along the current context, please remember to +include a trailing dot. The templates location will always be starting at +the /layout/ directory within Hugo. + +**Example:** + + {{ template "partials/header.html" . }} + +And, starting with Hugo v0.12, you may also use the `partial` call +for [partial templates](/templates/partials/): + + {{ partial "header.html" . }} + + +## Logic + +Go templates provide the most basic iteration and conditional logic. + +### Iteration + +Just like in Go, the Go templates make heavy use of `range` to iterate over +a map, array or slice. The following are different examples of how to use +range. + +**Example 1: Using Context** + + {{ range array }} + {{ . }} + {{ end }} + +**Example 2: Declaring value variable name** + + {{range $element := array}} + {{ $element }} + {{ end }} + +**Example 2: Declaring key and value variable name** + + {{range $index, $element := array}} + {{ $index }} + {{ $element }} + {{ end }} + +### Conditionals + +`if`, `else`, `with`, `or` & `and` provide the framework for handling conditional +logic in Go Templates. Like `range`, each statement is closed with `end`. + +Go Templates treat the following values as false: + +* false +* 0 +* any array, slice, map, or string of length zero + +**Example 1: `if`** + + {{ if isset .Params "title" }}

    {{ index .Params "title" }}

    {{ end }} + +**Example 2: `if` … `else`** + + {{ if isset .Params "alt" }} + {{ index .Params "alt" }} + {{else}} + {{ index .Params "caption" }} + {{ end }} + +**Example 3: `and` & `or`** + + {{ if and (or (isset .Params "title") (isset .Params "caption")) (isset .Params "attr")}} + +**Example 4: `with`** + +An alternative way of writing "`if`" and then referencing the same value +is to use "`with`" instead. `with` rebinds the context `.` within its scope, +and skips the block if the variable is absent. + +The first example above could be simplified as: + + {{ with .Params.title }}

    {{ . }}

    {{ end }} + +**Example 5: `if` … `else if`** + + {{ if isset .Params "alt" }} + {{ index .Params "alt" }} + {{ else if isset .Params "caption" }} + {{ index .Params "caption" }} + {{ end }} + +## Pipes + +One of the most powerful components of Go templates is the ability to +stack actions one after another. This is done by using pipes. Borrowed +from Unix pipes, the concept is simple, each pipeline's output becomes the +input of the following pipe. + +Because of the very simple syntax of Go templates, the pipe is essential +to being able to chain together function calls. One limitation of the +pipes is that they only can work with a single value and that value +becomes the last parameter of the next pipeline. + +A few simple examples should help convey how to use the pipe. + +**Example 1:** + + {{ shuffle (seq 1 5) }} + +is the same as + + {{ (seq 1 5) | shuffle }} + +**Example 2:** + + {{ index .Params "disqus_url" | html }} + +Access the page parameter called "disqus_url" and escape the HTML. + +The `index` function is a [Go][] built-in, and you can read about it [here][gostdlibpkgtexttemplate]. `index`: + +> ...returns the result of indexing its first argument by the following arguments. Thus "index x 1 2 3" is, in Go syntax, `x[1][2][3]`. Each indexed item must be a map, slice, or array. + +**Example 3:** + + {{ if or (or (isset .Params "title") (isset .Params "caption")) (isset .Params "attr") }} + Stuff Here + {{ end }} + +Could be rewritten as + + {{ if isset .Params "caption" | or isset .Params "title" | or isset .Params "attr" }} + Stuff Here + {{ end }} + +### Internet Explorer conditional comments using Pipes + +By default, Go Templates remove HTML comments from output. This has the unfortunate side effect of removing Internet Explorer conditional comments. As a workaround, use something like this: + + {{ "" | safeHTML }} + +Alternatively, use the backtick (`` ` ``) to quote the IE conditional comments, avoiding the tedious task of escaping every double quotes (`"`) inside, as demonstrated in the [examples](http://golang.org/pkg/text/template/#hdr-Examples) in the Go text/template documentation, e.g.: + +``` +{{ `` | safeHTML }} +``` + +## Context (a.k.a. the dot) + +The most easily overlooked concept to understand about Go templates is that `{{ . }}` +always refers to the current context. In the top level of your template, this +will be the data set made available to it. Inside of a iteration, however, it will have +the value of the current item. When inside of a loop, the context has changed: +`{{ . }}` will no longer refer to the data available to the entire page. If you need +to +access this from within the loop, you will likely want to do one of the following: + +1. Set it to a variable instead of depending on the context. For example: + + {{ $title := .Site.Title }} + {{ range .Params.tags }} +
  • + {{ . }} + - {{ $title }} +
  • + {{ end }} + + Notice how once we have entered the loop the value of `{{ . }}` has changed. We + have defined a variable outside of the loop so we have access to it from within + the loop. + +2. Use `$.` to access the global context from anywhere. + Here is an equivalent example: + + {{ range .Params.tags }} +
  • + {{ . }} + - {{ $.Site.Title }} +
  • + {{ end }} + + This is because `$`, a special variable, is set to the starting value + of `.` the dot by default, + a [documented feature](http://golang.org/pkg/text/template/#hdr-Variables) + of Go text/template. Very handy, eh? + + > However, this little magic would cease to work if someone were to + > mischievously redefine `$`, e.g. `{{ $ := .Site }}`. + > *(No, don't do it!)* + > You may, of course, recover from this mischief by using `{{ $ := . }}` + > in a global context to reset `$` to its default value. + +## Whitespace + +Go 1.6 includes the ability to trim the whitespace from either side of a Go tag by including a hyphen (`-`) and space immediately beside the corresponding `{{` or `}}` delimiter. + +For instance, the following Go template: + +```html +
    + {{ .Title }} +
    +``` + +will include the newlines and horizontal tab in its HTML output: + +```html +
    + Hello, World! +
    +``` + +whereas using + +```html +
    + {{- .Title -}} +
    +``` + +in that case will output simply `
    Hello, World!
    `. + +Go considers the following characters as whitespace: space, horizontal tab, carriage return and newline. + +# Hugo Parameters + +Hugo provides the option of passing values to the template language +through the site configuration (for sitewide values), or through the meta +data of each specific piece of content. You can define any values of any +type (supported by your front matter/config format) and use them however +you want to inside of your templates. + + +## Using Content (page) Parameters + +In each piece of content, you can provide variables to be used by the +templates. This happens in the [front matter](/content/front-matter/). + +An example of this is used in this documentation site. Most of the pages +benefit from having the table of contents provided. Sometimes the TOC just +doesn't make a lot of sense. We've defined a variable in our front matter +of some pages to turn off the TOC from being displayed. + +Here is the example front matter: + +``` +--- +title: "Permalinks" +lastmod: 2015-11-30 +date: "2013-11-18" +aliases: + - "/doc/permalinks/" +groups: ["extras"] +groups_weight: 30 +notoc: true +--- +``` + +Here is the corresponding code inside of the template: + + {{ if not .Params.notoc }} +
    + {{ .TableOfContents }} +
    + {{ end }} + + + +## Using Site (config) Parameters +In your top-level configuration file (e.g., `config.yaml`) you can define site +parameters, which are values which will be available to you in partials. + +For instance, you might declare: + +```yaml +params: + CopyrightHTML: "Copyright © 2013 John Doe. All Rights Reserved." + TwitterUser: "spf13" + SidebarRecentLimit: 5 +``` + +Within a footer layout, you might then declare a `
    ` which is only +provided if the `CopyrightHTML` parameter is provided, and if it is given, +you would declare it to be HTML-safe, so that the HTML entity is not escaped +again. This would let you easily update just your top-level config file each +January 1st, instead of hunting through your templates. + +``` +{{if .Site.Params.CopyrightHTML}}
    +
    {{.Site.Params.CopyrightHTML | safeHTML}}
    +
    {{end}} +``` + +An alternative way of writing the "`if`" and then referencing the same value +is to use "`with`" instead. With rebinds the context `.` within its scope, +and skips the block if the variable is absent: + +``` +{{with .Site.Params.TwitterUser}}{{end}} +``` + +Finally, if you want to pull "magic constants" out of your layouts, you can do +so, such as in this example: + +``` + +``` + +# Template example: Show only upcoming events + +Go allows you to do more than what's shown here. Using Hugo's +[`where`](/templates/functions/#where) function and Go built-ins, we can list +only the items from `content/events/` whose date (set in the front matter) is in +the future: + +

    Upcoming Events

    +
      + {{ range where .Data.Pages.ByDate "Section" "events" }} + {{ if ge .Date.Unix .Now.Unix }} +
    • {{ .Type | title }} — + {{ .Title }} + on + {{ .Date.Format "2 January at 3:04pm" }} + at {{ .Params.place }} +
    • + {{ end }} + {{ end }} + +[go]: http://golang.org/ +[gohtmltemplate]: http://golang.org/pkg/html/template/ +[gostdlibpkgtexttemplate]: http://golang.org/pkg/text/template/ diff --git a/docs/content/templates/homepage.md b/docs/content/templates/homepage.md new file mode 100644 index 000000000..c5cebd219 --- /dev/null +++ b/docs/content/templates/homepage.md @@ -0,0 +1,81 @@ +--- +aliases: +- /layout/homepage/ +lastmod: 2015-08-04 +date: 2013-07-01 +menu: + main: + parent: layout +next: /templates/terms +notoc: true +prev: /templates/list +title: Homepage +weight: 50 +--- + +The home page of a website is often formatted differently than the other +pages. In Hugo you can define your own homepage template. + +Homepage is a `Page` and have all the [page +variables](/templates/variables/) and [site +variables](/templates/variables/) available to use in the templates. + +*This is the only required template for building a site and useful when +bootstrapping a new site and template. It is also the only required +template when using a single page site.* + +In addition to the standard page variables, the homepage has access to +all site content accessible from `.Data.Pages`. Details on how to use the +list of pages can be found in the [Lists Template](/templates/list/). + +Note that a home page can also have a content file with frontmatter, see [Source Organization]({{< relref "overview/source-directory.md#content-for-home-page-and-other-list-pages" >}}). + +## Which Template will be rendered? +Hugo uses a set of rules to figure out which template to use when +rendering a specific page. + +Hugo will use the following prioritized list. If a file isn’t present, +then the next one in the list will be used. This enables you to craft +specific layouts when you want to without creating more templates +than necessary. For most sites, only the \_default file at the end of +the list will be needed. + +* /layouts/index.html +* /layouts/\_default/list.html +* /layouts/\_default/single.html +* /themes/`THEME`/layouts/index.html +* /themes/`THEME`/layouts/\_default/list.html +* /themes/`THEME`/layouts/\_default/single.html + +## Example index.html +This content template is used for [spf13.com](http://spf13.com/). + +It makes use of [partial templates](/templates/partials/) and uses a similar approach as a [List](/templates/list/). + + + + + + + {{ partial "meta.html" . }} + + + {{ .Site.Title }} + + + + {{ partial "head_includes.html" . }} + + + + {{ partial "subheader.html" . }} + +
      +
      + {{ range first 10 .Data.Pages }} + {{ .Render "summary"}} + {{ end }} +
      +
      + + {{ partial "footer.html" . }} diff --git a/docs/content/templates/list.md b/docs/content/templates/list.md new file mode 100644 index 000000000..22d3123ac --- /dev/null +++ b/docs/content/templates/list.md @@ -0,0 +1,437 @@ +--- +aliases: +- /layout/indexes/ +lastmod: 2015-08-04 +date: 2013-07-01 +linktitle: List of Content +menu: + main: + parent: layout +next: /templates/homepage +prev: /templates/content +title: Content List Template +weight: 40 +toc: true +--- + +A list template is any template that will be used to render multiple pieces of +content in a single HTML page (with the exception of the [homepage](/layout/homepage/) which has a +dedicated template). + +We are using the term list in its truest sense, a sequential arrangement +of material, especially in alphabetical or numerical order. Hugo uses +list templates to render anyplace where content is being listed such as +taxonomies and sections. + +## Which Template will be rendered? + +Hugo uses a set of rules to figure out which template to use when +rendering a specific page. + +Hugo will use the following prioritized list. If a file isn’t present, +then the next one in the list will be used. This enables you to craft +specific layouts when you want to without creating more templates +than necessary. For most sites only the \_default file at the end of +the list will be needed. + + +### Section Lists + +A Section will be rendered at /`SECTION`/ (e.g. http://spf13.com/project/) + +* /layouts/section/`SECTION`.html +* /layouts/`SECTION`/list.html +* /layouts/\_default/section.html +* /layouts/\_default/list.html +* /themes/`THEME`/layouts/section/`SECTION`.html +* /themes/`THEME`/layouts/`SECTION`/list.html +* /themes/`THEME`/layouts/\_default/section.html +* /themes/`THEME`/layouts/\_default/list.html + +Note that a sections list page can also have a content file with frontmatter, see [Source Organization]({{< relref "overview/source-directory.md#content-for-home-page-and-other-list-pages" >}}). + +### Taxonomy Lists + +A Taxonomy will be rendered at /`PLURAL`/`TERM`/ (e.g. http://spf13.com/topics/golang/) from: + +* /layouts/taxonomy/`SINGULAR`.html (e.g. `/layouts/taxonomy/topic.html`) +* /layouts/\_default/taxonomy.html +* /layouts/\_default/list.html +* /themes/`THEME`/layouts/taxonomy/`SINGULAR`.html +* /themes/`THEME`/layouts/\_default/taxonomy.html +* /themes/`THEME`/layouts/\_default/list.html + +Note that a taxonomy list page can also have a content file with frontmatter, see [Source Organization]({{< relref "overview/source-directory.md#content-for-home-page-and-other-list-pages" >}}). + +### Section RSS + +A Section’s RSS will be rendered at /`SECTION`/index.xml (e.g. http://spf13.com/project/index.xml) + +*Hugo ships with its own [RSS 2.0][] template. In most cases this will +be sufficient, and an RSS template will not need to be provided by the +user.* + +Hugo provides the ability for you to define any RSS type you wish, and +can have different RSS files for each section and taxonomy. + +* /layouts/section/`SECTION`.rss.xml +* /layouts/\_default/rss.xml +* /themes/`THEME`/layouts/section/`SECTION`.rss.xml +* /themes/`THEME`/layouts/\_default/rss.xml + +### Taxonomy RSS + +A Taxonomy’s RSS will be rendered at /`PLURAL`/`TERM`/index.xml (e.g. http://spf13.com/topics/golang/index.xml) + +*Hugo ships with its own [RSS 2.0][] template. In most cases this will +be sufficient, and an RSS template will not need to be provided by the +user.* + +Hugo provides the ability for you to define any RSS type you wish, and +can have different RSS files for each section and taxonomy. + +* /layouts/taxonomy/`SINGULAR`.rss.xml +* /layouts/\_default/rss.xml +* /themes/`THEME`/layouts/taxonomy/`SINGULAR`.rss.xml +* /themes/`THEME`/layouts/\_default/rss.xml + + +## Variables + +A list page is a `Page` and have all the [page variables](/templates/variables/) +and [site variables](/templates/variables/) available to use in the templates. + +Taxonomy pages will additionally have: + +**.Data.`Singular`** The taxonomy itself.
      + +## Example List Template Pages + +### Example section template (post.html) +This content template is used for [spf13.com](http://spf13.com/). +It makes use of [partial templates](/templates/partials/). All examples use a +[view](/templates/views/) called either "li" or "summary" which this example site +defined. + + {{ partial "header.html" . }} + {{ partial "subheader.html" . }} + +
      +
      +

      {{ .Title }}

      +
        + {{ range .Data.Pages }} + {{ .Render "li"}} + {{ end }} +
      +
      +
      + + {{ partial "footer.html" . }} + +### Example taxonomy template (tag.html) +This content template is used for [spf13.com](http://spf13.com/). +It makes use of [partial templates](/templates/partials/). All examples use a +[view](/templates/views/) called either "li" or "summary" which this example site +defined. + + {{ partial "header.html" . }} + {{ partial "subheader.html" . }} + +
      +
      +

      {{ .Title }}

      + {{ range .Data.Pages }} + {{ .Render "summary"}} + {{ end }} +
      +
      + + {{ partial "footer.html" . }} + +## Ordering Content + +In the case of Hugo, each list will render the content based on metadata provided in the [front +matter](/content/front-matter/). See [ordering content](/content/ordering/) for more information. + +Here are a variety of different ways you can order the content items in +your list templates: + +### Order by Weight -> Date (default) + + {{ range .Data.Pages }} +
    • + {{ .Title }} +
      {{ .Date.Format "Mon, Jan 2, 2006" }}
      +
    • + {{ end }} + +### Order by Weight -> Date + + {{ range .Data.Pages.ByWeight }} +
    • + {{ .Title }} +
      {{ .Date.Format "Mon, Jan 2, 2006" }}
      +
    • + {{ end }} + +### Order by Date + + {{ range .Data.Pages.ByDate }} +
    • + {{ .Title }} +
      {{ .Date.Format "Mon, Jan 2, 2006" }}
      +
    • + {{ end }} + +### Order by PublishDate + + {{ range .Data.Pages.ByPublishDate }} +
    • + {{ .Title }} +
      {{ .PublishDate.Format "Mon, Jan 2, 2006" }}
      +
    • + {{ end }} + +### Order by ExpiryDate + + {{ range .Data.Pages.ByExpiryDate }} +
    • + {{ .Title }} +
      {{ .ExpiryDate.Format "Mon, Jan 2, 2006" }}
      +
    • + {{ end }} + +### Order by Lastmod + + {{ range .Data.Pages.ByLastmod }} +
    • + {{ .Title }} +
      {{ .Date.Format "Mon, Jan 2, 2006" }}
      +
    • + {{ end }} + +### Order by Length + + {{ range .Data.Pages.ByLength }} +
    • + {{ .Title }} +
      {{ .Date.Format "Mon, Jan 2, 2006" }}
      +
    • + {{ end }} + + +### Order by Title + + {{ range .Data.Pages.ByTitle }} +
    • + {{ .Title }} +
      {{ .Date.Format "Mon, Jan 2, 2006" }}
      +
    • + {{ end }} + +### Order by LinkTitle + + {{ range .Data.Pages.ByLinkTitle }} +
    • + {{ .LinkTitle }} +
      {{ .Date.Format "Mon, Jan 2, 2006" }}
      +
    • + {{ end }} + +### Order by Parameter + +Order based on the specified frontmatter parameter. Pages without that +parameter will use the site's `.Site.Params` default. If the parameter is not +found at all in some entries, those entries will appear together at the end +of the ordering. + +The below example sorts a list of posts by their rating. + + {{ range (.Data.Pages.ByParam "rating") }} + + {{ end }} + +If the frontmatter field of interest is nested beneath another field, you can +also get it: + + {{ range (.Data.Pages.ByParam "author.last_name") }} + + {{ end }} + +### Reverse Order +Can be applied to any of the above. Using Date for an example. + + {{ range .Data.Pages.ByDate.Reverse }} +
    • + {{ .Title }} +
      {{ .Date.Format "Mon, Jan 2, 2006" }}
      +
    • + {{ end }} + +## Grouping Content + +Hugo provides some grouping functions for list pages. You can use them to +group pages by Section, Type, Date etc. + +Here are a variety of different ways you can group the content items in +your list templates: + +### Grouping by Page field + + {{ range .Data.Pages.GroupBy "Section" }} +

      {{ .Key }}

      +
        + {{ range .Pages }} +
      • + {{ .Title }} +
        {{ .Date.Format "Mon, Jan 2, 2006" }}
        +
      • + {{ end }} +
      + {{ end }} + +### Grouping by Page date + + {{ range .Data.Pages.GroupByDate "2006-01" }} +

      {{ .Key }}

      +
        + {{ range .Pages }} +
      • + {{ .Title }} +
        {{ .Date.Format "Mon, Jan 2, 2006" }}
        +
      • + {{ end }} +
      + {{ end }} + +### Grouping by Page publish date + + {{ range .Data.Pages.GroupByPublishDate "2006-01" }} +

      {{ .Key }}

      +
        + {{ range .Pages }} +
      • + {{ .Title }} +
        {{ .PublishDate.Format "Mon, Jan 2, 2006" }}
        +
      • + {{ end }} +
      + {{ end }} + +### Grouping by Page param + + {{ range .Data.Pages.GroupByParam "param_key" }} +

      {{ .Key }}

      +
        + {{ range .Pages }} +
      • + {{ .Title }} +
        {{ .Date.Format "Mon, Jan 2, 2006" }}
        +
      • + {{ end }} +
      + {{ end }} + +### Grouping by Page param in date format + + {{ range .Data.Pages.GroupByParamDate "param_key" "2006-01" }} +

      {{ .Key }}

      +
        + {{ range .Pages }} +
      • + {{ .Title }} +
        {{ .Date.Format "Mon, Jan 2, 2006" }}
        +
      • + {{ end }} +
      + {{ end }} + +### Reversing Key Order + +The ordering of the groups is performed by keys in alphanumeric order (A–Z, +1–100) and in reverse chronological order (newest first) for dates. + +While these are logical defaults, they are not always the desired order. There +are two different syntaxes to change the order; they both work the same way, so +it’s really just a matter of preference. + +#### Reverse method + + {{ range (.Data.Pages.GroupBy "Section").Reverse }} + ... + + {{ range (.Data.Pages.GroupByDate "2006-01").Reverse }} + ... + + +#### Providing the (alternate) direction + + {{ range .Data.Pages.GroupByDate "2006-01" "asc" }} + ... + + {{ range .Data.Pages.GroupBy "Section" "desc" }} + ... + +### Ordering Pages within Group + +Because Grouping returns a key and a slice of pages, all of the ordering methods listed above are available. + +In this example, I’ve ordered the groups in chronological order and the content +within each group in alphabetical order by title. + + {{ range .Data.Pages.GroupByDate "2006-01" "asc" }} +

      {{ .Key }}

      +
        + {{ range .Pages.ByTitle }} +
      • + {{ .Title }} +
        {{ .Date.Format "Mon, Jan 2, 2006" }}
        +
      • + {{ end }} +
      + {{ end }} + +## Filtering & Limiting Content + +Sometimes you only want to list a subset of the available content. A common +request is to only display “Posts” on the homepage. Using the `where` function, +you can do just that. + +### `first` + +`first` works like the `limit` keyword in SQL. It reduces the array to only the +first _N_ elements. It takes the array and number of elements as input. + + {{ range first 10 .Data.Pages }} + {{ .Render "summary" }} + {{ end }} + +### `where` + +`where` works in a similar manner to the `where` keyword in SQL. It selects all +elements of the slice that match the provided field and value. It takes three +arguments: 'array or slice of maps or structs', 'key or field name' and 'match +value'. + + {{ range where .Data.Pages "Section" "post" }} + {{ .Content }} + {{ end }} + +### `first` & `where` Together + +Using both together can be very powerful. + + {{ range first 5 (where .Data.Pages "Section" "post") }} + {{ .Content }} + {{ end }} + +If `where` or `first` receives invalid input or a field name that doesn’t exist, +it will return an error and stop site generation. + +These are both template functions and work on not only +[lists](/templates/list/), but [taxonomies](/taxonomies/displaying/), +[terms](/templates/terms/) and [groups](/templates/list/). + + +[RSS 2.0]: http://cyber.law.harvard.edu/rss/rss.html "RSS 2.0 Specification" diff --git a/docs/content/templates/overview.md b/docs/content/templates/overview.md new file mode 100644 index 000000000..3b41a6641 --- /dev/null +++ b/docs/content/templates/overview.md @@ -0,0 +1,76 @@ +--- +aliases: +- /doc/templates/ +- /layout/templates/ +- /layout/overview/ +lastmod: 2015-05-22 +date: 2013-07-01 +linktitle: Overview +menu: + main: + parent: layout +next: /templates/go-templates +prev: /themes/creation +title: Hugo Templates +weight: 10 +toc: true +--- + +Hugo uses the excellent Go html/template library for its template engine. +It is an extremely lightweight engine that provides a very small amount of +logic. In our experience it is just the right amount of logic to be able +to create a good static website. + +While Hugo has a number of different template roles, most complete +websites can be built using just a small number of template files. +Please don’t be afraid of the variety of different template roles. They +enable Hugo to build very complicated sites. Most sites will only +need to create a [/layouts/\_default/single.html](/templates/content/) & [/layouts/\_default/list.html](/templates/list/) + +If you are new to Go's templates, the [Go Template Primer](/layout/go-templates/) +is a great place to start. + +If you are familiar with Go’s templates, Hugo provides some [additional +template functions](/templates/functions/) and [variables](/templates/variables/) you will want to be familiar +with. + +## Primary Template roles + +There are 3 primary kinds of templates that Hugo works with. + +### [Single](/templates/content/) +Render a single piece of content + +### [List](/templates/list/) +Page that list multiple pieces of content + +### [Homepage](/templates/homepage/) +The homepage of your site + +## Supporting Template Roles (optional) + +Hugo also has additional kinds of templates all of which are optional + +### [Partial Templates](/templates/partials/) +Common page parts to be included in the above mentioned templates + +### [Content Views](/templates/views/) +Different ways of rendering a (single) content type + +### [Taxonomy Terms](/templates/terms/) +A list of the terms used for a specific taxonomy, e.g. a Tag cloud + +## Other Templates (generally unnecessary) + +### [RSS](/templates/rss/) +Used to render all rss documents + +### [Sitemap](/templates/sitemap/) +Used to render the XML sitemap + +### [404](/templates/404/) +This template will create a 404.html page used when hosting on GitHub Pages + +### [Alias](/extras/aliases/#customizing) +This template will override the default page used to create aliases of pages. + diff --git a/docs/content/templates/partials.md b/docs/content/templates/partials.md new file mode 100644 index 000000000..46c2c2400 --- /dev/null +++ b/docs/content/templates/partials.md @@ -0,0 +1,146 @@ +--- +aliases: +- /layout/chrome/ +lastmod: 2016-01-01 +date: 2013-07-01 +menu: + main: + parent: layout +next: /templates/rss +prev: /templates/blocks/ +title: Partial Templates +weight: 80 +toc: true +--- + +In practice, it's very convenient to split out common template portions into a +partial template that can be included anywhere. As you create the rest of your +templates, you will include templates from the ``/layouts/partials` directory +or from arbitrary subdirectories like `/layouts/partials/post/tag`. + +Partials are especially important for themes as it gives users an opportunity +to overwrite just a small part of your theme, while maintaining future compatibility. + +Theme developers may want to include a few partials with empty HTML +files in the theme just so end users have an easy place to inject their +customized content. + +I've found it helpful to include a header and footer template in +partials so I can include those in all the full page layouts. There is +nothing special about header.html and footer.html other than they seem +like good names to use for inclusion in your other templates. + + ▾ layouts/ + ▾ partials/ + header.html + footer.html + +## Partial vs Template + +Version v0.12 of Hugo introduced the `partial` call inside the template system. +This is a change to the way partials were handled previously inside the +template system. In earlier versions, Hugo didn’t treat partials specially, and +you could include a partial template with the `template` call in the standard +template language. + +With the addition of the theme system in v0.11, it became apparent that a theme +& override-aware partial was needed. + +When using Hugo v0.12 and above, please use the `partial` call (and leave out +the “partial/” path). The old approach would still work, but wouldn’t benefit from +the ability to have users override the partial theme file with local layouts. + +## Example header.html +This header template is used for [spf13.com](http://spf13.com/): + + + + + + + {{ partial "meta.html" . }} + + + {{ .Title }} : spf13.com + + {{ if .RSSLink }}{{ end }} + + {{ partial "head_includes.html" . }} + + + +## Example footer.html +This footer template is used for [spf13.com](http://spf13.com/): + + + + + + +To reference a partial template stored in a subfolder, e.g. `/layouts/partials/post/tag/list.html`, call it this way: + + {{ partial "post/tag/list" . }} + +Note that the subdirectories you create under /layouts/partials can be named whatever you like. + +For more examples of referencing these templates, see +[single content templates](/templates/content/), +[list templates](/templates/list/) and +[homepage templates](/templates/homepage/). + + +## Variable scoping + +As you might have noticed, `partial` calls receive two parameters. + +1. The first is the name of the partial and determines the file +location to be read. +2. The second is the variables to be passed down to the partial. + +This means that the partial will _only_ be able to access those variables. It is +isolated and has no access to the outer scope. From within the +partial, `$.Var` is equivalent to `.Var` + +## Cached Partials + +The `partialCached` template function can offer significant performance gains +for complex templates that don't need to be rerendered upon every invocation. +The simplest usage is as follows: + + {{ partialCached "footer.html" . }} + +You can also pass additional parameters to `partialCached` to create *variants* of the cached partial. +For example, say you have a complex partial that should be identical when rendered for pages within the same section. +You could use a variant based upon section so that the partial is only rendered once per section: + + {{ partialCached "footer.html" . .Section }} + +If you need to pass additional parameters to create unique variants, +you can pass as many variant parameters as you need: + + {{ partialCached "footer.html" . .Params.country .Params.province }} + +Note that the variant parameters are not made available to the underlying partial template. +They are only use to create a unique cache key. diff --git a/docs/content/templates/rss.md b/docs/content/templates/rss.md new file mode 100644 index 000000000..10f2a8535 --- /dev/null +++ b/docs/content/templates/rss.md @@ -0,0 +1,130 @@ +--- +aliases: +- /layout/rss/ +lastmod: 2015-08-04 +date: 2015-05-19 +linktitle: RSS +menu: + main: + parent: layout +next: /templates/sitemap +notoc: one +prev: /templates/partials +title: RSS (feed) Templates +weight: 90 +toc: true +--- + +Like all other templates, you can use a single RSS template to generate all of your RSS feeds, or you can create a specific template for each individual feed. + +*Unlike other Hugo templates*, Hugo ships with its own [RSS 2.0 template](#the-embedded-rss-xml:eceb479b7b3b2077408a2878a29e1320). In most cases this will be sufficient, and an RSS template will not need to be provided by the user. But you can provide an rss template if you like, as you can see in the next section. + +RSS pages are of the type `Page` and have all the [page variables](/layout/variables/) available to use in the templates. + +## Which Template will be rendered? +Hugo uses a set of rules to figure out which template to use when rendering a specific page. + +Hugo will use the following prioritized list. If a file isn’t present, then the next one in the list will be used. This enables you to craft specific layouts when you want to without creating more templates than necessary. For most sites only the `\_default` file at the end of the list will be needed. + +### Main RSS + +* /layouts/rss.xml +* /layouts/\_default/rss.xml +* [Embedded rss.xml](#the-embedded-rss-xml:eceb479b7b3b2077408a2878a29e1320) + +### Section RSS + +* /layouts/section/`SECTION`.rss.xml +* /layouts/\_default/rss.xml +* /themes/`THEME`/layouts/section/`SECTION`.rss.xml +* /themes/`THEME`/layouts/\_default/rss.xml +* [Embedded rss.xml](#the-embedded-rss-xml:eceb479b7b3b2077408a2878a29e1320) + +### Taxonomy RSS + +* /layouts/taxonomy/`SINGULAR`.rss.xml +* /layouts/\_default/rss.xml +* /themes/`THEME`/layouts/taxonomy/`SINGULAR`.rss.xml +* /themes/`THEME`/layouts/\_default/rss.xml +* [Embedded rss.xml](#the-embedded-rss-xml:eceb479b7b3b2077408a2878a29e1320) + +### Taxonomy Terms RSS + +* /layouts/taxonomy/`SINGULAR`.terms.rss.xml +* /layouts/\_default/rss.xml +* /themes/`THEME`/layouts/taxonomy/`SINGULAR`.terms.rss.xml +* /themes/`THEME`/layouts/\_default/rss.xml +* [Embedded rss.xml](#the-embedded-rss-xml:eceb479b7b3b2077408a2878a29e1320) + + +## Configuring RSS + +If the following values are specified in the site’s config file (`config.toml`), then they will be included in the RSS output. Example values are provided. + + languageCode = "en-us" + copyright = "This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License." + + [author] + name = "My Name Here" + email = "sample@domain.tld" + +### Limiting the Number of Items + +By default, the RSS feed is limited to **15** items. +You may override the default by using the `rssLimit` [site configuration variable](/overview/configuration/). + +## The Embedded rss.xml +This is the default RSS template that ships with Hugo. It adheres to the [RSS 2.0 Specification][RSS 2.0]. + + + + {{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }} + {{ .Permalink }} + Recent content {{ if ne .Title .Site.Title }}{{ with .Title }}in {{.}} {{ end }}{{ end }}on {{ .Site.Title }} + Hugo -- gohugo.io{{ with .Site.LanguageCode }} + {{.}}{{end}}{{ with .Site.Author.email }} + {{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}{{end}}{{ with .Site.Author.email }} + {{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}{{end}}{{ with .Site.Copyright }} + {{.}}{{end}}{{ if not .Date.IsZero }} + {{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}{{ end }} + + {{ range .Data.Pages }} + + {{ .Title }} + {{ .Permalink }} + {{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }} + {{ with .Site.Author.email }}{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}{{end}} + {{ .Permalink }} + {{ .Summary | html }} + + {{ end }} + + + +**Important**: _Hugo will automatically add the following header line to this file on render… please don't include this in the template as it's not valid HTML._ + +~~~css + +~~~ + +## Referencing your RSS Feed in `` + +In your `header.html` template, you can specify your RSS feed in your `` tag like this: + +~~~html +{{ if .RSSLink }} + +{{ end }} +~~~ + +... with the autodiscovery link specified by the line with `rel="alternate"`. + +The `.RSSLink` will render the appropriate RSS feed URL for the section, whether it's everything, posts in a section, or a taxonomy. + +**N.b.**, if you reference your RSS link, be sure to specify the mime type with `type="application/rss+xml"`. + +~~~html +{{ .SomeText }} +~~~ + +[RSS 2.0]: http://cyber.law.harvard.edu/rss/rss.html "RSS 2.0 Specification" diff --git a/docs/content/templates/sitemap.md b/docs/content/templates/sitemap.md new file mode 100644 index 000000000..c893f3155 --- /dev/null +++ b/docs/content/templates/sitemap.md @@ -0,0 +1,75 @@ +--- +aliases: +- /layout/sitemap/ +lastmod: 2015-12-10 +date: 2014-05-07 +linktitle: Sitemap +menu: + main: + parent: layout +next: /templates/404 +notoc: true +prev: /templates/rss +title: Sitemap Template +weight: 95 +--- + +A single Sitemap template is used to generate the `sitemap.xml` file. +Hugo automatically comes with this template file. **No work is needed on +the users' part unless they want to customize `sitemap.xml`.** + +A sitemap is a `Page` and have all the [page +variables](/layout/variables/) available to use in this template +along with Sitemap-specific ones: + +**.Sitemap.ChangeFreq** The page change frequency
      +**.Sitemap.Priority** The priority of the page
      +**.Sitemap.Filename** The sitemap filename
      + +If provided, Hugo will use `/layouts/sitemap.xml` instead of the internal +one. + +## Hugo’s sitemap.xml + +This template uses the version 0.9 of the [Sitemap +Protocol](http://www.sitemaps.org/protocol.html) with Google's [hreflang +attributes](https://support.google.com/webmasters/answer/2620865?hl=en&topic=2370587&ctx=topic) +for linking to translated content. + + + {{ range .Data.Pages }} + + {{ .Permalink }}{{ if not .Lastmod.IsZero }} + {{ safeHTML ( .Lastmod.Format "2006-01-02T15:04:05-07:00" ) }}{{ end }}{{ with .Sitemap.ChangeFreq }} + {{ . }}{{ end }}{{ if ge .Sitemap.Priority 0.0 }} + {{ .Sitemap.Priority }}{{ end }}{{ if .IsTranslated }}{{ range .Translations }} + {{ end }} + {{ end }} + + {{ end }} + + +***Important:** Hugo will automatically add the following header line to this file +on render. Please don't include this in the template as it's not valid HTML.* + + + +## Configuring sitemap.xml + +Defaults for ``, `` and `filename` values can be set in the site's config file, e.g.: + + [sitemap] + changefreq = "monthly" + priority = 0.5 + filename = "sitemap.xml" + +The same fields can be specified in an individual page's front matter in order to override the value for that page. diff --git a/docs/content/templates/terms.md b/docs/content/templates/terms.md new file mode 100644 index 000000000..83414ceed --- /dev/null +++ b/docs/content/templates/terms.md @@ -0,0 +1,176 @@ +--- +aliases: +- /indexes/lists/ +- /doc/indexes/ +- /extras/indexes +lastmod: 2015-09-15 +date: 2014-05-21 +linktitle: Taxonomy Terms +menu: + main: + parent: layout +next: /templates/views +prev: /templates/homepage +title: Taxonomy Terms Template +weight: 60 +toc: true +--- + +A unique template is needed to create a list of the terms for a given +taxonomy. This is different from the [list template](/templates/list/) +as that template is a list of content, whereas this is a list of meta data. + +Note that a taxonomy terms page can also have a content file with frontmatter, see [Source Organization]({{< relref "overview/source-directory.md#content-for-home-page-and-other-list-pages" >}}). + +## Which Template will be rendered? +Hugo uses a set of rules to figure out which template to use when +rendering a specific page. + +A Taxonomy Terms List will be rendered at /`PLURAL`/ +(e.g. http://spf13.com/topics/) +from the following prioritized list: + +* /layouts/taxonomy/`SINGULAR`.terms.html (e.g. `/layouts/taxonomy/topic.terms.html`) +* /layouts/\_default/terms.html + +If a file isn’t present, +then the next one in the list will be used. This enables you to craft +specific layouts when you want to without creating more templates +than necessary. For most sites, only the `_default` file at the end of +the list will be needed. + +If that neither file is found in either the /layouts or /theme/layouts +directory, then Hugo will not render the taxonomy terms pages. It is also +common for people to render taxonomy terms lists on other pages such as +the homepage or the sidebar (such as a tag cloud) and not have a +dedicated page for the terms. + + +## Variables + +Taxonomy Terms pages are of the type `Page` and have all the +[page variables](/templates/variables/) and +[site variables](/templates/variables/) +available to use in the templates. + +Taxonomy Terms pages will additionally have: + +* **.Data.Singular** The singular name of the taxonomy +* **.Data.Plural** The plural name of the taxonomy +* **.Data.Pages** (or as **.Pages**) The taxonomy Terms index pages +* **.Data.Terms** The taxonomy itself +* **.Data.Terms.Alphabetical** The Terms alphabetized +* **.Data.Terms.ByCount** The Terms ordered by popularity + +The last two can also be reversed: **.Data.Terms.Alphabetical.Reverse**, **.Data.Terms.ByCount.Reverse**. + +### Example terms.html files + +This content template is used for [spf13.com](http://spf13.com/). +It makes use of [partial templates](/templates/partials/). The list of taxonomy +templates cannot use a [content view](/templates/views/) as they don't display the content, but +rather information about the content. + +This particular template lists all of the Tags used on +[spf13.com](http://spf13.com/) and provides a count for the number of pieces of +content tagged with each tag. + +`.Data.Terms` is a map of terms ⇒ [contents] + + {{ partial "header.html" . }} + {{ partial "subheader.html" . }} + +
      +
      +

      {{ .Title }}

      + +
        + {{ $data := .Data }} + {{ range $key, $value := .Data.Terms }} +
      • {{ $key }} {{ len $value }}
      • + {{ end }} +
      +
      +
      + + {{ partial "footer.html" . }} + + +Another example listing the content for each term (ordered by Date): + + {{ partial "header.html" . }} + {{ partial "subheader.html" . }} + +
      +
      +

      {{ .Title }}

      + + {{ $data := .Data }} + {{ range $key,$value := .Data.Terms.ByCount }} +

      {{ $value.Name }} {{ $value.Count }}

      +
        + {{ range $value.Pages.ByDate }} +
      • {{ .Title }}
      • + {{ end }} +
      + {{ end }} +
      +
      + + {{ partial "footer.html" . }} + + +## Ordering + +Hugo can order the term meta data in two different ways. It can be ordered: + +* by the number of contents assigned to that key, or +* alphabetically. + +### Example terms.html file (alphabetical) + + {{ partial "header.html" . }} + {{ partial "subheader.html" . }} + +
      +
      +

      {{ .Title }}

      +
        + {{ $data := .Data }} + {{ range $key, $value := .Data.Terms.Alphabetical }} +
      • {{ $value.Name }} {{ $value.Count }}
      • + {{ end }} +
      +
      +
      + {{ partial "footer.html" . }} + +### Example terms.html file (ordered by popularity) + + {{ partial "header.html" . }} + {{ partial "subheader.html" . }} + +
      +
      +

      {{ .Title }}

      +
        + {{ $data := .Data }} + {{ range $key, $value := .Data.Terms.ByCount }} +
      • {{ $value.Name }} {{ $value.Count }}
      • + {{ end }} +
      +
      +
      + + {{ partial "footer.html" . }} + +Hugo can also order and paginate the term index pages in all the normal ways. + +### Example terms.html snippet (paginated and ordered by date) + +

      {{ .Title }}

      +
        + {{ range .Paginator.Pages.ByDate.Reverse }} +
      • {{ .Title }} {{ $.Data.Terms.Count .Data.Term }}
      • + {{ end }} +
      diff --git a/docs/content/templates/variables.md b/docs/content/templates/variables.md new file mode 100644 index 000000000..962297156 --- /dev/null +++ b/docs/content/templates/variables.md @@ -0,0 +1,237 @@ +--- +aliases: +- /doc/variables/ +- /layout/variables/ +lastmod: 2015-12-08 +date: 2013-07-01 +linktitle: Variables +menu: + main: + parent: layout +next: /templates/content +prev: /templates/functions +title: Template Variables +weight: 20 +toc: true +--- + +Hugo makes a set of values available to the templates. Go templates are context based. The following +are available in the context for the templates. + +## Page Variables + +The following is a list of most of the accessible variables which can be +defined for a piece of content. Many of these will be defined in the front +matter, content or derived from file location. + +**See also:** [Scratch](/extras/scratch) for page-scoped writable variables. + + + +**.Content** The content itself, defined below the front matter.
      +**.Data** The data specific to this type of page.
      +**.Date** The date the page is associated with.
      +**.Description** The description for the page.
      +**.Draft** A boolean, `true` if the content is marked as a draft in the front matter.
      +**.ExpiryDate** The date where the content is scheduled to expire on.
      +**.FuzzyWordCount** The approximate number of words in the content.
      +**.Hugo** See [Hugo Variables]({{< relref "#hugo-variables" >}}) below.
      +**.IsHome** True if this is the home page.
      +**.IsNode** Always false for regular content pages.
      +**.IsPage** Always true for regular content pages.
      +**.IsTranslated** Whether there are any translations to display.
      +**.Keywords** The meta keywords for this content.
      +**.Kind** What *kind* of page is this: is one of *page, home, section, taxonomy or taxonomyTerm.* There are also *RSS, sitemap, robotsTXT and 404*, but these will only available during rendering of that kind of page, and not available in any of the `Pages` collections.
      +**.Lang** Language taken from the language extension notation.
      +**.Language** A language object that points to this the language's definition in the site config.
      +**.Lastmod** The date the content was last modified.
      +**.LinkTitle** Access when creating links to this content. Will use `linktitle` if set in front matter, else `title`.
      +**.Next** Pointer to the following content (based on pub date).
      +**.NextInSection** Pointer to the following content within the same section (based on pub date)
      +**.Pages** a collection of associated pages. This will be nil for regular content pages. This is an alias for **.Data.Pages**.
      +**.Permalink** The Permanent link for this page.
      +**.Prev** Pointer to the previous content (based on pub date).
      +**.PrevInSection** Pointer to the previous content within the same section (based on pub date). For example, `{{if .PrevInSection}}{{.PrevInSection.Permalink}}{{end}}`.
      +**.PublishDate** The date the content is published on.
      +**.RSSLink** Link to the taxonomies' RSS link.
      +**.RawContent** Raw Markdown content without the metadata header. Useful with [remarkjs.com](http://remarkjs.com)
      +**.ReadingTime** The estimated time it takes to read the content in minutes.
      +**.Ref** Returns the permalink for a given reference. Example: `.Ref "sample.md"`. See [cross-references]({{% ref "extras/crossreferences.md" %}}). Does not handle in-page fragments correctly.
      +**.RelPermalink** The Relative permanent link for this page.
      +**.RelRef** Returns the relative permalink for a given reference. Example: `RelRef "sample.md"`. See [cross-references]({{% ref "extras/crossreferences.md" %}}). Does not handle in-page fragments.
      +**.Section** The [section](/content/sections/) this content belongs to.
      +**.Site** See [Site Variables]({{< relref "#site-variables" >}}) below.
      +**.Summary** A generated summary of the content for easily showing a snippet in a summary view. Note that the breakpoint can be set manually by inserting <!--more--> at the appropriate place in the content page. See [Summaries](/content/summaries/) for more details.
      +**.TableOfContents** The rendered table of contents for this content.
      +**.Title** The title for this page.
      +**.Translations** A list of translated versions of the current page. See [Multilingual]({{< relref "content/multilingual.md" >}}) for more info.
      +**.Truncated** A boolean, `true` if the `.Summary` is truncated. Useful for showing a "Read more..." link only if necessary. See [Summaries](/content/summaries/) for more details.
      +**.Type** The content [type](/content/types/) (e.g. post).
      +**.URL** The relative URL for this page. Note that if `URL` is set directly in frontmatter, that URL is returned as-is.
      +**.UniqueID** The MD5-checksum of the content file's path
      +**.Weight** Assigned weight (in the front matter) to this content, used in sorting.
      +**.WordCount** The number of words in the content.
      + +## Page Params + +Any other value defined in the front matter, including taxonomies, will be made available under `.Params`. +For example, the *tags* and *categories* taxonomies are accessed with: + +* **.Params.tags** +* **.Params.categories** + +**All Params are only accessible using all lowercase characters.** + +This is particularly useful for the introduction of user defined fields in content files. For example, a Hugo website on book reviews could have in the front matter of /content/review/book01.md + + --- + ... + affiliatelink: "http://www.my-book-link.here" + recommendedby: "my Mother" + --- + +Which would then be accessible to a template at `/themes/yourtheme/layouts/review/single.html` through `.Params.affiliatelink` and `.Params.recommendedby`, respectively. Two common situations where these could be introduced are as a value of a certain attribute (like `href=""` below) or by itself to be displayed. Sample syntaxes include: + +

      Buy this book

      +

      It was recommended by {{ .Params.recommendedby }}.

      + +which would render + +

      Buy this book

      +

      It was recommended by my Mother.

      + +**See also:** [Archetypes]({{% ref "content/archetypes.md" %}}) for consistency of `Params` across pieces of content. + +### Param method + +In Hugo you can declare params both for the site and the individual page. A +common use case is to have a general value for the site and a more specific +value for some of the pages (i.e. a header image): + +``` +{{ $.Param "header_image" }} +``` + +The `.Param` method provides a way to resolve a single value whether it's +in a page parameter or a site parameter. + +When frontmatter contains nested fields, like: + +``` +--- +author: + given_name: John + family_name: Feminella + display_name: John Feminella +--- +``` + +then `.Param` can access them by concatenating the field names together with a +dot: + +``` +{{ $.Param "author.display_name" }} +``` + +If your frontmatter contains a top-level key that is ambiguous with a nested +key, as in the following case, + +``` +--- +favorites.flavor: vanilla +favorites: + flavor: chocolate +--- +``` + +then the top-level key will be preferred. In the previous example, this + +``` +{{ $.Param "favorites.flavor" }} +``` + +will print `vanilla`, not `chocolate`. + +### Taxonomy Terms Page Variables + +[Taxonomy Terms](/templates/terms/) pages are of the type `Page` and have the following additional variables. These are available in `layouts/_defaults/terms.html` for example. + +**.Data.Singular** The singular name of the taxonomy
      +**.Data.Plural** The plural name of the taxonomy
      +**.Data.Pages** the list of pages in this taxonomy
      +**.Data.Terms** The taxonomy itself
      +**.Data.Terms.Alphabetical** The Terms alphabetized
      +**.Data.Terms.ByCount** The Terms ordered by popularity
      + +The last two can also be reversed: **.Data.Terms.Alphabetical.Reverse**, **.Data.Terms.ByCount.Reverse**. + +### Taxonomies elsewhere + +The **.Site.Taxonomies** variable holds all taxonomies defines site-wide. It is a map of the taxonomy name to a list of its values. For example: "tags" -> ["tag1", "tag2", "tag3"]. Each value, though, is not a string but rather a [Taxonomy variable](#the-taxonomy-variable). + +#### The Taxonomy variable + +The Taxonomy variable, available as **.Site.Taxonomies.tags** for example, contains the list of tags (values) and, for each of those, their corresponding content pages. + +## Site Variables + +Also available is `.Site` which has the following: + +**.Site.BaseURL** The base URL for the site as defined in the site configuration file.
      +**.Site.RSSLink** The URL for the site RSS.
      +**.Site.Taxonomies** The [taxonomies](/taxonomies/usage/) for the entire site. Replaces the now-obsolete `.Site.Indexes` since v0.11. Also see section [Taxonomies elsewhere](#taxonomies-elsewhere).
      +**.Site.Pages** Array of all content ordered by Date, newest first. `.Site.Pages` replaced `.Site.Recent`, which is no longer supported. This array contains only the pages in the current language.
      +**.Site.AllPages** Array of all pages regardless of their translation.
      +**.Site.Params** A container holding the values from the `params` section of your site configuration file. For example, a TOML config file might look like this: + + baseURL = "http://yoursite.example.com/" + + [params] + description = "Tesla's Awesome Hugo Site" + author = "Nikola Tesla" +**.Site.Sections** Top level directories of the site.
      +**.Site.Files** All of the source files of the site.
      +**.Site.Menus** All of the menus in the site.
      +**.Site.Title** A string representing the title of the site.
      +**.Site.Author** A map of the authors as defined in the site configuration.
      +**.Site.LanguageCode** A string representing the language as defined in the site configuration. This is mostly used to populate the RSS feeds with the right language code.
      +**.Site.DisqusShortname** A string representing the shortname of the Disqus shortcode as defined in the site configuration.
      +**.Site.GoogleAnalytics** A string representing your tracking code for Google Analytics as defined in the site configuration.
      +**.Site.Copyright** A string representing the copyright of your web site as defined in the site configuration.
      +**.Site.LastChange** A string representing the date/time of the most recent change to your site, based on the [`date` variable]({{< ref "content/front-matter.md#required-variables" >}}) in the front matter of your content pages.
      +**.Site.Permalinks** A string to override the default permalink format. Defined in the site configuration.
      +**.Site.BuildDrafts** A boolean (Default: false) to indicate whether to build drafts. Defined in the site configuration.
      +**.Site.Data** Custom data, see [Data Files](/extras/datafiles/).
      +**.Site.IsMultiLingual** Whether there are more than one language in this site.
      See [Multilingual]({{< relref "content/multilingual.md" >}}) for more info.
      +**.Site.Language** This indicates which language you are currently rendering the website for. This is an object with the attributes set in your language definition in your site config.
      +**.Site.Language.Lang** The language code of the current locale, e.g. `en`.
      +**.Site.Language.Weight** The weight that defines the order in the `.Site.Languages` list.
      +**.Site.Language.LanguageName** The full language name, e.g. `English`.
      +**.Site.LanguagePrefix** This can be used to prefix theURLs with whats needed to point to the correct language. It will even work when only one language defined. See also the functions [absLangURL and relLangURL]({{< relref "templates/functions.md#abslangurl-rellangurl" >}}).
      +**.Site.Languages** An ordered list (ordered by defined weight) of languages.
      +**.Site.RegularPages** A shortcut to the *regular page* collection. Equivalent to `where .Site.Pages "Kind" "page"`.
      + +## File Variables + +The `.File` variable gives you additional information of a page. + +> **Note:** `.File` is only accessible on *Pages* that has a content page attached to it. + +Available are the following attributes: + +**.File.Path** The original relative path of the page, e.g. `content/posts/foo.en.md`
      +**.File.LogicalName** The name of the content file that represents a page, e.g. `foo.en.md`
      +**.File.TranslationBaseName** The filename without extension or optional language identifier, e.g. `foo`
      +**.File.BaseFileName** The filename without extension, e.g. `foo.en`
      +**.File.Ext** or **.File.Extension** The file extension of the content file, e.g. `md`
      +**.File.Lang** The language associated with the given file if [Multilingual]({{< relref "content/multilingual.md" >}}) is enabled, e.g. `en`
      +**.File.Dir** Given the path `content/posts/dir1/dir2/`, the relative directory path of the content file will be returned, e.g. `posts/dir1/dir2/`
      + +## Hugo Variables + +Also available is `.Hugo` which has the following: + +**.Hugo.Generator** Meta tag for the version of Hugo that generated the site. Highly recommended to be included by default in all theme headers so we can start to track the usage and popularity of Hugo. Unlike other variables it outputs a **complete** HTML tag, e.g. ``
      +**.Hugo.Version** The current version of the Hugo binary you are using e.g. `0.13-DEV`
      +**.Hugo.CommitHash** The git commit hash of the current Hugo binary e.g. `0e8bed9ccffba0df554728b46c5bbf6d78ae5247`
      +**.Hugo.BuildDate** The compile date of the current Hugo binary formatted with RFC 3339 e.g. `2002-10-02T10:00:00-05:00`
      diff --git a/docs/content/templates/views.md b/docs/content/templates/views.md new file mode 100644 index 000000000..c8426ec11 --- /dev/null +++ b/docs/content/templates/views.md @@ -0,0 +1,129 @@ +--- +aliases: +- /templates/views/ +lastmod: 2015-05-22 +date: 2013-07-01 +menu: + main: + parent: layout +next: /templates/blocks +prev: /templates/terms +title: Content Views +weight: 70 +toc: true +--- + +In addition to the [single content template](/templates/content/), Hugo can render alternative views of +your content. These are especially useful in [list templates](/templates/list/). + +For example you may want content of every type to be shown on the +homepage, but only a summary view of it there. Perhaps on a taxonomy +list page you would only want a bulleted list of your content. Views +make this very straightforward by delegating the rendering of each +different type of content to the content itself. + + +## Creating a content view + +To create a new view, simply create a template in each of your different +content type directories with the view name. In the following example, we +have created a "li" view and a "summary" view for our two content types +of post and project. As you can see, these sit next to the [single +content view](/templates/content/) template "single.html". You can even +provide a specific view for a given type and continue to use the +\_default/single.html for the primary view. + + ▾ layouts/ + ▾ post/ + li.html + single.html + summary.html + ▾ project/ + li.html + single.html + summary.html + +Hugo also has support for a default content template to be used in the event +that a specific template has not been provided for that type. The default type +works the same as the other types, but the directory must be called "_default". +Content views can also be defined in the "_default" directory. + + + ▾ layouts/ + ▾ _default/ + li.html + single.html + summary.html + + +## Which Template will be rendered? +Hugo uses a set of rules to figure out which template to use when +rendering a specific page. + +Hugo will use the following prioritized list. If a file isn’t present, +then the next one in the list will be used. This enables you to craft +specific layouts when you want to without creating more templates +than necessary. For most sites only the \_default file at the end of +the list will be needed. + +* /layouts/`TYPE`/`VIEW`.html +* /layouts/\_default/`VIEW`.html +* /themes/`THEME`/layouts/`TYPE`/`VIEW`.html +* /themes/`THEME`/layouts/\_default/`view`.html + + +## Example using views + +### rendering view inside of a list + +Using the summary view (defined below) inside of a ([list +templates](/templates/list/)). + +
      +
      +

      {{ .Title }}

      + {{ range .Data.Pages }} + {{ .Render "summary"}} + {{ end }} +
      +
      + +In the above example, you will notice that we have called `.Render` and passed in +which view to render the content with. `.Render` is a special function available on +a content which tells the content to render itself with the provided view template. +In this example, we are not using the li view. To use this we would +change the render line to `{{ .Render "li" }}`. + + +### li.html + +Hugo will pass the entire page object to the view template. See [page +variables](/templates/variables/) for a complete list. + +This content template is used for [spf13.com](http://spf13.com/). + +
    • + {{ .Title }} +
      {{ .Date.Format "Mon, Jan 2, 2006" }}
      +
    • + +### summary.html + +Hugo will pass the entire page object to the view template. See [page +variables](/templates/variables/) for a complete list. + +This content template is used for [spf13.com](http://spf13.com/). + +
      +
      +

      {{ .Title }}

      + +
      + + {{ .Summary }} + +
      + + diff --git a/docs/content/themes/creation.md b/docs/content/themes/creation.md new file mode 100644 index 000000000..5f7a2e53b --- /dev/null +++ b/docs/content/themes/creation.md @@ -0,0 +1,78 @@ +--- +lastmod: 2015-08-04 +date: 2014-05-12T10:09:17Z +menu: + main: + parent: themes +next: /templates/overview +prev: /themes/customizing +title: Creating a Theme +weight: 50 +--- + +Hugo has the ability to create a new theme in your themes directory for you +using the `hugo new` command. + +`hugo new theme [name]` + +This command will initialize all of the files and directories a basic theme +would need. Hugo themes are written in the Go template language. If you are new +to Go, the [Go template primer](/layout/go-templates/) will help you get started. + +## Theme Components + +A theme consists of templates and static assets such as javascript and css +files. Themes can also optionally provide [archetypes](/content/archetypes/) +which are archetypal content types used by the `hugo new` command. + +### Layouts + +Hugo is built around the concept that things should be as simple as possible. +Fundamentally website content is displayed in two different ways, a single +piece of content and a list of content items. With Hugo a theme layout starts +with the defaults. As additional layouts are defined they are used for the +content type or section they apply to. This keeps layouts simple, but permits +a large amount of flexibility. + +### Single Content + +The default single file layout is located at `layouts/_default/single.html`. + +### List of Contents + +The default list file layout is located at `layouts/_default/list.html`. + +### Partial Templates + +Theme creators should liberally use [partial templates](/templates/partials/) +throughout their theme files. Not only is a good DRY practice to include shared +code, but partials are a special template type that enables the themes end user +to be able to overwrite just a small piece of a file or inject code into the +theme from their local /layouts. These partial templates are perfect for easy +injection into the theme with minimal maintenance to ensure future +compatibility. + +### Static + +Everything in the static directory will be copied directly into the final site +when rendered. No structure is provided here to enable complete freedom. It is +common to organize the static content into: + + /css + /js + /img + +The actual structure is entirely up to you, the theme creator, on how you would like to organize your files. + + +### Archetypes + +If your theme makes use of specific keys in the front matter, it is a good idea +to provide an archetype for each content type you have. Archetypes follow the +[guidelines provided](/content/archetypes/). + + +### Generator meta tag + +With a growing community around Hugo we recommend theme creators to include the [Generator meta tag]({{< relref "templates/variables.md#hugo-variables" >}}) with `.Hugo.Generator` in the `` of your HTML code. The output might looks like `` and helps us to analyse the usage and popularity of Hugo. + diff --git a/docs/content/themes/customizing.md b/docs/content/themes/customizing.md new file mode 100644 index 000000000..f381106db --- /dev/null +++ b/docs/content/themes/customizing.md @@ -0,0 +1,56 @@ +--- +lastmod: 2015-08-04 +date: 2014-05-12T10:09:34Z +menu: + main: + parent: themes +next: /themes/creation +prev: /themes/usage +title: Customizing a Theme +weight: 40 +toc: true +--- + +_The following are key concepts for Hugo site customization. Hugo permits you to **supplement or override** any theme template or static file, with files in your working directory._ + +_When you use a theme cloned from its git repository, you do not edit the theme's files directly. Rather, you override them as per the following:_ + +## Replace Static Files + +For including a different file than what the theme ships with. For example, if you would like to use a more recent version of jQuery than what the theme happens to include, simply place an identically-named file in the same relative location but in your working directory. + +For example, if the theme has jQuery 1.6 in: + + /themes/themename/static/js/jquery.min.js + +... you would simply place your file in the same relative path, but in the root of your working folder: + + /static/js/jquery.min.js + +## Replace a single template file + +Anytime Hugo looks for a matching template, it will first check the working directory before looking in the theme directory. If you would like to modify a template, simply create that template in your local `layouts` directory. + +In the [template documentation](/templates/overview/) _each different template type explains the rules it uses to determine which template to use_. Read and understand these rules carefully. + +This is especially helpful when the theme creator used [partial templates](/templates/partials/). These partial templates are perfect for easy injection into the theme with minimal maintenance to ensure future compatibility. + +For example: + + /themes/themename/layouts/_default/single.html + +... would be overridden by: + + /layouts/_default/single.html + +**Warning**: This only works for templates that Hugo "knows about" (that follow its convention for folder structure and naming). If the theme imports template files in a creatively-named directory, Hugo won’t know to look for the local `/layouts` first. + +## Replace an archetype + +If the archetype that ships with the theme for a given content type (or all content types) doesn’t fit with how you are using the theme, feel free to copy it to your `/archetypes` directory and make modifications as you see fit. + +## Beware of the default + +**Default** is a very powerful force in Hugo, especially as it pertains to overwriting theme files. If a default is located in the local archetype directory or `/layouts/_default/` directory, it will be used instead of any of the similar files in the theme. + +It is usually better to override specific files rather than using the default in your working directory. diff --git a/docs/content/themes/installing.md b/docs/content/themes/installing.md new file mode 100644 index 000000000..a70f50287 --- /dev/null +++ b/docs/content/themes/installing.md @@ -0,0 +1,46 @@ +--- +lastmod: 2015-10-10 +date: 2014-05-12T10:09:49Z +menu: + main: + parent: themes +next: /themes/usage +prev: /themes/overview +title: Installing Themes +weight: 20 +--- + +Community-contributed [Hugo themes](http://themes.gohugo.io/), showcased +at [themes.gohugo.io](//themes.gohugo.io/), are hosted in a centralized +GitHub repository. The [Hugo Themes Repo](https://github.com/gohugoio/hugoThemes) +itself at [github.com/gohugoio/hugoThemes](https://github.com/gohugoio/hugoThemes) is +really a meta repository which contains pointers to set of contributed themes. + +## Installing all themes + +If you would like to install all of the available Hugo themes, simply +clone the entire repository from within your working directory. Depending +on your internet connection the download of all themes might take a while. + +> **NOTE:** Make sure you've installed [Git](https://git-scm.com/) on your computer. +Otherwise you will not be able to clone the theme repositories. + +```bash +$ git clone --depth 1 --recursive https://github.com/gohugoio/hugoThemes.git themes +``` +Before you use a theme, remove the .git folder in that theme's root folder. Otherwise, this will cause problem if you deploy using Git. + +## Installing a specific theme + +Switch into the `themes` directory and download a theme by replacing `URL_TO_THEME` +with the URL of the theme repository, e.g. `https://github.com/spf13/hyde`: + + $ cd themes + $ git clone URL_TO_THEME + +Alternatively, you can download the theme as `.zip` file and extract it in the +`themes` directory. + +**NOTE:** Please have a look at the `README.md` file that is shipped with all themes. +It might contain further instructions that are required to setup the theme, e.g. copying +an example configuration file. diff --git a/docs/content/themes/overview.md b/docs/content/themes/overview.md new file mode 100644 index 000000000..94e7259fd --- /dev/null +++ b/docs/content/themes/overview.md @@ -0,0 +1,32 @@ +--- +lastmod: 2015-10-10 +date: 2014-05-12T10:03:52Z +menu: + main: + parent: themes +next: /themes/installing +prev: /content/example +title: Themes Overview +weight: 10 +--- + +Hugo provides a robust theming system which is simple, yet capable of producing +even the most complicated websites. + +The Hugo community has created [a wide variety of beautiful themes](//themes.gohugo.io/), as demoed at [themes.gohugo.io](//themes.gohugo.io/), +ready for using in your own site. + +Hugo themes have been designed to be the perfect balance between +simplicity and functionality. Hugo themes are powered by the excellent +Go template library. If you are new to Go templates, see our [primer on +Go templates](/templates/go-templates/). + +Hugo themes support all modern features you come to expect. They are +structured in such a way to eliminate code duplication. Themes are also +designed to be very easy to customize while retaining the ability to +maintain upgradeability as the upstream theme changes. + +Hugo currently doesn’t ship with a “default” theme, allowing the user to +pick whichever theme best suits their project. + +We hope you will find Hugo themes perfect for your site. diff --git a/docs/content/themes/usage.md b/docs/content/themes/usage.md new file mode 100644 index 000000000..5b1d11c02 --- /dev/null +++ b/docs/content/themes/usage.md @@ -0,0 +1,27 @@ +--- +lastmod: 2015-01-27 +date: 2014-05-12T10:09:27Z +menu: + main: + parent: themes +next: /themes/customizing +prev: /themes/installing +title: Using a Theme +weight: 30 +--- + +Please make certain you have installed the themes you want to use in the +`/themes` directory. + +To use a theme for a site, execute Hugo with the following parameter: + + hugo -t ThemeName + +or add this line to your site configuration: + + theme: "ThemeName" + +The *ThemeName* must match the name of the directory inside `/themes`. + +Hugo will then apply the theme first, then apply anything that is in the local +directory. To learn more, go to [customizing themes](/themes/customizing/). diff --git a/docs/content/tools/_index.md b/docs/content/tools/_index.md new file mode 100644 index 000000000..d5543f045 --- /dev/null +++ b/docs/content/tools/_index.md @@ -0,0 +1,144 @@ +--- +date: 2015-09-12T10:40:31+02:00 +title: Tools +weight: 120 +--- + +This section highlights some projects around Hugo that are independently developed. These tools try to extend the functionality of our static site generator or help you to get started. + +> **Note:** Do you know or maintain a similar project around Hugo? Feel free to open a [pull request](https://github.com/gohugoio/hugo/pulls) on GitHub if you think it should be added. + + +## Migration + +Take a look at this list of migration tools if you currently use other blogging tools like Jekyll or WordPress but intend to switch to Hugo instead. They'll take care to export +your content into Hugo-friendly formats. + +### Jekyll + +Alternatively, you can follow the manual [migration guide]({{< relref "tutorials/migrate-from-jekyll.md" >}}) or use the new [Jekyll import command]({{< relref "commands/hugo_import_jekyll.md" >}}). + +- [JekyllToHugo](https://github.com/SenjinDarashiva/JekyllToHugo) - A Small script for converting Jekyll blog posts to a Hugo site. +- [ConvertToHugo](https://github.com/coderzh/ConvertToHugo) - Convert your blog from Jekyll to Hugo. + +### Ghost + +- [ghostToHugo](https://github.com/jbarone/ghostToHugo) - Convert Ghost blog posts and export them to Hugo. + +### Octopress + +- [octohug](https://github.com/codebrane/octohug) - Octopress to Hugo migrator. + +### DokuWiki + +- [dokuwiki-to-hugo](https://github.com/wgroeneveld/dokuwiki-to-hugo) - Migrates your dokuwiki source pages from [DokuWiki syntax](https://www.dokuwiki.org/wiki:syntax) to Hugo Markdown syntax. Includes extra's like the TODO plugin. Written with extensibility in mind using python 3. Also generates a TOML header for each page. Designed to copypaste the wiki directory into your /content directory. + +### WordPress + +- [wordpress-to-hugo-exporter](https://github.com/SchumacherFM/wordpress-to-hugo-exporter) - A one-click WordPress plugin that converts all posts, pages, taxonomies, metadata, and settings to Markdown and YAML which can be dropped into Hugo. (Note: If you have trouble using this plugin, you can [export your site for Jekyll](https://wordpress.org/plugins/jekyll-exporter/) and use Hugo's built in Jekyll converter listed above.) + +### Tumblr + +- [tumblr-importr](https://github.com/carlmjohnson/tumblr-importr) - An importer that uses the Tumblr API to create a Hugo static site. +- [tumblr2hugomarkdown](https://github.com/Wysie/tumblr2hugomarkdown) - Export all your Tumblr content to Hugo Markdown files with preserved original formatting. +- [Tumblr to Hugo](https://github.com/jipiboily/tumblr-to-hugo) - A migration tool that converts each of your Tumblr posts to a content file with a proper title and path. Furthermore, "Tumblr to Hugo" creates a CSV file with the original URL and the new path on Hugo, to help you setup the redirections. + +### Drupal + +- [drupal2hugo](https://github.com/danapsimer/drupal2hugo) - Convert a Drupal site to Hugo. + +### Joomla + +- [hugojoomla](https://github.com/davetcc/hugojoomla) - This utility written in Java takes a Joomla database and converts all the content into Markdown files. It changes any URLs that are in Joomla's internal format and converts them to a suitable form. + +### Blogger + +- [blogimport](https://github.com/natefinch/blogimport) - A tool to import from Blogger posts to Hugo. + +### Contentful + +- [contentful2hugo](https://github.com/ArnoNuyts/contentful2hugo) - A tool to create content-files for Hugo from content on [Contentful](https://www.contentful.com/). + +---- + +## Deployment + +If you don't want to use [Wercker for automated deployments]({{< relref "tutorials/automated-deployments.md" >}}), give these tools a try to get your content to the public: + +- [hugomac](https://github.com/nickoneill/hugomac) - Hugomac is an OS X menubar app to publish your blog directly to Amazon S3. No command line is needed. +- [hugo-lambda](https://github.com/ryansb/hugo-lambda) - A wrapper around the Hugo static site generator to have it run in AWS Lambda whenever new (Markdown or other) content is uploaded. +- [hugodeploy](https://github.com/mindok/hugodeploy) - Simple SFTP deployment tool for static websites (e.g. created by Hugo) with optional minification. +- [webhook](https://github.com/adnanh/webhook) - Run build and deployment scripts (e.g. hugo) on incoming webhooks +- [Hugo SFTP Upload](https://github.com/thomasmey/HugoSftpUpload) - Sync the local build of your Hugo website with your remote webserver via SFTP. + +---- + +## Frontends + +Do you prefer an graphical user interface over a text editor? Then give these frontends a try: + +- [enwrite](https://github.com/zzamboni/enwrite) - Evernote-powered statically-generated blogs and websites. Now posting to your blog or updating your website is as easy as writing a new note in Evernote! +- [caddy-hugo](https://github.com/hacdias/caddy-hugo) - This is an add-on for [Caddy](https://caddyserver.com/) which wants to deliver a good UI to edit the content of the website. +- [Lipi](https://github.com/SohanChy/Lipi) - A native GUI frontend written in Java to manage your Hugo websites. + +---- + +## Editor plugins + +If you still want to use an editor, look at these plugins to automate your workflow: + +### Sublime Text + +- [Hugofy](https://github.com/akmittal/Hugofy) - Hugofy is a plugin for Sublime Text 3 to make life easier to use Hugo static site generator. + +### Visual Studio Code + +- [Hugofy](https://marketplace.visualstudio.com/items?itemName=akmittal.hugofy) - Hugofy is a plugin for Visual Studio Code to make life easier to use Hugo static site generator. The source code can be found [here](https://github.com/akmittal/hugofy-vscode). + +### Emacs + +- [easy-hugo](https://github.com/masasam/emacs-easy-hugo) - Major mode & tools for Hugo. +- [hugo.el](https://github.com/yewton/hugo.el) - Some helper functions for creating a Website with Hugo. + +### Vim + +- [Vim Hugo Helper](https://github.com/robertbasic/vim-hugo-helper) - A small Vim plugin to help me with writing posts with Hugo. + +### Atom + +- [Hugofy](https://atom.io/packages/hugofy) - A Hugo Static Website Generator package for Atom + +---- + +## Search + +A static site with a dynamic search function? Yes. Alternatively to embeddable scripts from Google or other search engines you can provide your visitors a custom search by indexing your content files directly. + +- [Hugoidx](https://github.com/blevesearch/hugoidx) is an experimental application to create a search index. It's build on top of [Bleve](http://www.blevesearch.com/). +- This [GitHub Gist](https://gist.github.com/sebz/efddfc8fdcb6b480f567) contains simple workflow to create a search index for your static site. It uses a simple Grunt script to index all your content files and [lunr.js](http://lunrjs.com/) to serve the search results. +- [hugo-lunr](https://www.npmjs.com/package/hugo-lunr) - A simple way to add site search to your static Hugo site using [lunr.js](http://lunrjs.com/). Hugo-lunr will create an index file of any html and markdown documents in your Hugo project. + +---- + +## Commercial Services + +- [Algolia](https://www.algolia.com/)'s Search API makes it easy to deliver a great search experience in your apps & websites. Algolia Search provides hosted full-text, numerical, faceted and geolocalized search. + +- [Appernetic.io](https://appernetic.io) is a Hugo Static Site Generator as a Service that is easy to use for non-technical users. +Features: inline PageDown editor, visual tree view, image upload and digital asset management with Cloudinary, site preview, continuous integration with GitHub, atomic deploy and hosting, Git and Hugo integration, autosave, custom domain, project syncing, theme cloning and management. Developers have complete control over the source code and can manage it with GitHub's deceptively simple workflow. + +- [Netlify.com](https://www.netlify.com), builds, deploy & hosts your static site or app (Hugo, Jekyll etc). Build, deploy and host your static site or app with a drag and drop interface and automatic deploys from GitHub or Bitbucket. +Features: global CDN, atomic deploys, ultra fast DNS, instant cache invalidation, high availability, automated hosting, Git integration, form submission hooks, authentication providers, custom domain. Developers have complete control over the source code and can manage it with GitHub's or Bitbuckets deceptively simple workflow. + +- [Forestry.io](https://forestry.io/) - A simple CMS for Jekyll and Hugo sites with support for GitHub, GitLab and Bitbucket. Every time an update is made via the CMS, Forestry will commit changes back to your repo and will compile/deploy your site (S3, GitHub Pages, FTP, etc). + +---- + +## Other + +And for all the other small things around Hugo: + +- [hugo-gallery](https://github.com/icecreammatt/hugo-gallery) lets you create an image gallery for Hugo sites. +- [flickr-hugo-embed](https://github.com/nikhilm/flickr-hugo-embed) prints shortcodes to embed a set of images from an album on Flickr into Hugo. +- [hugo-openapispec-shortcode](https://github.com/tenfourty/hugo-openapispec-shortcode) A shortcode which allows you to include [Open API Spec](https://openapis.org) (formerly known as Swagger Spec) in a page. +- [HugoPhotoSwipe](https://github.com/GjjvdBurg/HugoPhotoSwipe) makes it easy to create image galleries using PhotoSwipe. diff --git a/docs/content/troubleshooting/categories-with-accented-characters.md b/docs/content/troubleshooting/categories-with-accented-characters.md new file mode 100644 index 000000000..3a6dba82d --- /dev/null +++ b/docs/content/troubleshooting/categories-with-accented-characters.md @@ -0,0 +1,50 @@ +--- +lastmod: 2015-01-08 +date: 2015-01-08T16:32:00-07:00 +menu: + main: + parent: troubleshooting +title: Accented Categories +weight: 10 +--- + +## Trouble: Categories with accented characters + +One of my categories is named "Le-carré," but the link ends up being generated like this: + + categories/le-carr%C3%A9 + +And not working. Is there an easy fix for this that I'm overlooking? + + +## Solution + +Mac OS X user? If so, you are likely a victim of HFS Plus file system's insistence to store the "é" (U+00E9) character in Normal Form Decomposed (NFD) mode, i.e. as "e" + " ́" (U+0065 U+0301). + +`le-carr%C3%A9` is actually correct, `%C3%A9` being the UTF-8 version of U+00E9 as expected by the web server. Problem is, OS X turns [U+00E9] into [U+0065 U+0301], and thus `le-carr%C3%A9` no longer works. Instead, only `le-carre%CC%81` ending with `e%CC%81` would match that [U+0065 U+0301] at the end. + +This is unique to OS X. The rest of the world does not do this, and most certainly not your web server which is most likely running Linux. This is not a Hugo-specific problem either. Other people have been bitten by this when they have accented characters in their HTML files. + +Nor is this problem specific to Latin scripts. Japanese Mac users often run into the same issue, e.g. with `だ` decomposing into `た` and .[^1] + +Rsync 3.x to the rescue! From [an answer posted on Server Fault](http://serverfault.com/questions/397420/converting-utf-8-nfd-filenames-to-utf-8-nfc-in-either-rsync-or-afpd): + +> You can use rsync's `--iconv` option to convert between UTF-8 NFC & NFD, at least if you're on a Mac. There is a special `utf-8-mac` character set that stands for UTF-8 NFD. So to copy files from your Mac to your web server, you'd need to run something like: +> +> `rsync -a --iconv=utf-8-mac,utf-8 localdir/ mywebserver:remotedir/` +> +> This will convert all the local filenames from UTF-8 NFD to UTF-8 NFC on the remote server. The files' contents won't be affected. + +Please make sure you have the latest version rsync 3.x installed. The rsync that ships with OS X (even the latest 10.10 Yosemite) is the horribly old at version 2.6.9 protocol version 29. The `--iconv` flag is new in rsync 3.x. + +### References + +* https://discourse.gohugo.io/t/categories-with-accented-characters/505 +* [Converting UTF-8 NFD filenames to UTF-8 NFC, in either rsync or afpd](http://serverfault.com/questions/397420/converting-utf-8-nfd-filenames-to-utf-8-nfc-in-either-rsync-or-afpd) (Server Fault) +* http://wiki.apache.org/subversion/NonNormalizingUnicodeCompositionAwareness +* https://en.wikipedia.org/wiki/Unicode_equivalence#Example +* http://zaiste.net/2012/07/brand_new_rsync_for_osx/ +* https://gogo244.wordpress.com/2014/09/17/drived-me-crazy-convert-utf-8-mac-to-utf-8/ + + +[^1]: As explained in the Japanese Perl Users article [Encode::UTF8Mac makes you happy while handling file names on MacOSX](http://perl-users.jp/articles/advent-calendar/2010/english/24). diff --git a/docs/content/troubleshooting/overview.md b/docs/content/troubleshooting/overview.md new file mode 100644 index 000000000..5f5ec19dc --- /dev/null +++ b/docs/content/troubleshooting/overview.md @@ -0,0 +1,36 @@ +--- +lastmod: 2015-01-27 +date: 2015-01-18T02:41:52-07:00 +menu: + main: + parent: troubleshooting +title: Troubleshooting Overview +weight: 0 +--- + +Got stuck? Worry not! Chances are other users have encountered +the exact same problem as you have, brought it up for +[discussion](https://discourse.gohugo.io/), and have likely found a solution +through the collective wisdom of our vibrant Hugo community! + +Here are some examples: + +* [`hugo new` aborts with cryptic EOF error](/troubleshooting/strange-eof-error/) (affects v0.12 and lower) +* [Categories with accented characters inaccessible](/troubleshooting/categories-with-accented-characters/) (affects Mac OS X users) +* [My CSS files aren't loaded!](https://discourse.gohugo.io/t/deployment-workflow/90/15) +* [How do I include an image gallery on my website?](https://discourse.gohugo.io/t/image-gallery/594) +* ... And a lot more! + +{{% youtube c8fJIRNChmU %}} + +Indeed, you may find many questions and solutions +to problems in our [discussion forum](https://discourse.gohugo.io/), +and you may find the [support](https://discourse.gohugo.io/category/support) +and [tips & tricks](https://discourse.gohugo.io/category/tips-tricks) +categories particularly helpful. + +Can't find anything? Please write on the forum and post your questions +and comments! Sometimes, your feedback may lead to the discovery of +existing bugs in the code or in the documentation, and may even spur +the interest of adding new features to the next Hugo version, improving +Hugo for everybody! (Thank you!) See you on the forum! diff --git a/docs/content/troubleshooting/strange-eof-error.md b/docs/content/troubleshooting/strange-eof-error.md new file mode 100644 index 000000000..559b1e7d2 --- /dev/null +++ b/docs/content/troubleshooting/strange-eof-error.md @@ -0,0 +1,43 @@ +--- +lastmod: 2015-01-17 +date: 2015-01-08T16:11:23-07:00 +menu: + main: + parent: troubleshooting +title: Strange EOF error +weight: 5 +--- + +## Trouble: `hugo new` aborts with cryptic EOF error + +> I'm running into an issue where I cannot get archetypes working, when running `hugo new showcase/test.md`, for example, I see an `EOF` error thrown by Hugo. +> +> I have set up this test repository to show exactly what I've done, but it is essentially a vanilla installation of Hugo. https://github.com/polds/hugo-archetypes-test +> +> When in that repository, using Hugo v0.12 to run `hugo new -v showcase/test.md`, I see the following output: +> +> INFO: 2015/01/04 Using config file: /private/tmp/test/config.toml +> INFO: 2015/01/04 attempting to create showcase/test.md of showcase +> INFO: 2015/01/04 curpath: /private/tmp/test/archetypes/showcase.md +> ERROR: 2015/01/04 EOF +> +> Is there something that I am blatantly missing? + +## Solution + +Thank you for reporting this issue. The solution is to add a final newline (i.e. EOL) to the end of your default.md archetype file of your theme. More discussions happened on the forum here: + +* https://discourse.gohugo.io/t/archetypes-not-properly-working-in-0-12/544 +* https://discourse.gohugo.io/t/eol-f-in-archetype-files/554 + +Due to popular demand, Hugo's parser has been enhanced to +accommodate archetype files without final EOL, +thanks to the great work by [@tatsushid](https://github.com/tatsushid), +in the upcoming v0.13 release, + +Until then, for us running the stable v0.12 release, please remember to add the final EOL diligently. + +## References + +* https://github.com/gohugoio/hugo/issues/776 + diff --git a/docs/content/tutorials/automated-deployments.md b/docs/content/tutorials/automated-deployments.md new file mode 100644 index 000000000..9f6d5c628 --- /dev/null +++ b/docs/content/tutorials/automated-deployments.md @@ -0,0 +1,275 @@ +--- +authors: +- Arjen Schwarz +- Samuel Debruyn +lastmod: 2017-02-26 +date: 2015-01-12 +linktitle: Automated deployments +toc: true +menu: + main: + parent: tutorials +next: /tutorials/creating-a-new-theme +prev: /community/contributing +title: Automated deployments with Wercker +weight: 10 +--- + +# Automated deployments with Wercker + +In this tutorial we will set up a basic Hugo project and then configure a free tool called Wercker to automatically deploy the generated site any time we add an article. We will deploy it to GitHub pages as that is easiest to set up, but you will see that we can use anything. This tutorial takes you through every step of the process, complete with screenshots and is fairly long. + +The assumptions made for this tutorial are that you know how to use git for version control, and have a GitHub account. In case you are unfamiliar with these, in their [help section](https://help.github.com/articles/set-up-git/) GitHub has an explanation of how to install and use git and you can easily sign up for a free GitHub account as well. + +## Creating a basic Hugo site + +There are already [pages](http://gohugo.io/overview/quickstart/) dedicated to describing how to set up a Hugo site so we will only go through the most basic steps required to get a site up and running before we dive into the Wercker configuration. All the work for setting up the project is done using the command line, and kept as simple as possible. + +Create the new site using the `hugo new site` command, and we move into it. + +```bash +hugo new site hugo-wercker-example +cd hugo-wercker-example +``` + +Add the herring-cove theme by cloning it into the theme directory using the following commands. + +```bash +mkdir themes +cd themes +git clone https://github.com/spf13/herring-cove.git +``` + +Cloning the project like this will conflict with our own version control, so we remove the external git configuration. + +```bash +rm -rf herring-cove/.git +``` + +Let's add a quick **about** page. + +```bash +hugo new about.md +``` + +Now we'll edit contents/about.md to ensure it's no longer a draft and add some text to it. + +```bash +hugo undraft content/about.md +``` + +Once completed it's a good idea to do a quick check if everything is working by running + +```bash +hugo server --theme=herring-cove +``` + +If everything is fine, you should be able to see something similar to the image below when you go to localhost:1313 in your browser. + +![][1] + +[1]: /img/tutorials/automated-deployments/creating-a-basic-hugo-site.png + +## Setting up version control + +Adding git to our project is done by running the `git init` command from the root directory of the project. + +```bash +git init +``` + +Running `git status` at this point will show you p entries: the **config.toml** file, the **themes** directory, the **contents** directory, and the **public** directory. We don't want the **public** directory version controlled however, as we will use wercker to generate that later on. Therefore, we'll add a gitignore file that will exclude this using the following command. + +```bash +echo "/public" >> .gitignore +``` + +As we currently have no static files outside of the theme directory, Wercker might complain when we try to build the site later on. To prevent this, we simply have to add any file to the static folder. To keep it simple for now we'll add a robots.txt file that will give all search engines full access to the site when it's up. + +```bash +echo "User-agent: *\nDisallow:" > static/robots.txt +``` + +After this we can add everything to the repository. + +```bash +git commit -a -m "Initial commit" +``` + +## Adding the project to GitHub + +First we'll create a new repository. You can do this by clicking on the **+** sign at the top right, or by going to https://github.com/new + +We then choose a name for the project (**hugo-wercker-example**). When clicking on create repository GitHub displays the commands for adding an existing project to the site. The commands shown below are the ones used for this site, if you're following along you will need to use the ones shown by GitHub. Once we've run those commands the project is in GitHub and we can move on to setting up the Wercker configuration. + +```bash +git remote add origin git@github.com:YourUsername/hugo-wercker-example.git +git push -u origin master +``` + +![][2] + +[2]: /img/tutorials/automated-deployments/adding-the-project-to-github.png + +## Welcome to wercker + +Let's start by setting up an account for Wercker. To do so we'll go to http://wercker.com and click on the **Sign up** button. + +![][3] + +[3]: /img/tutorials/automated-deployments/wercker-sign-up.png + +## Register + +To make life easier for ourselves, we will then register using GitHub. If you don't have a GitHub account, or don't want to use it for your account, you can of course register with a username and password as well. + +![][4] + +[4]: /img/tutorials/automated-deployments/wercker-sign-up-page.png + +## Connect GitHub/Bitbucket + +After you are registered, you will need to link your GitHub and/or Bitbucket account to Wercker. You do this by going to your profile settings, and then "Git connections" If you registered using GitHub it will most likely look like the image below. To connect a missing service, simply click on the connect button which will then send you to either GitHub or Bitbucket where you might need to log in and approve their access to your account. + +![][5] + +[5]: /img/tutorials/automated-deployments/wercker-git-connections.png + +## Add your project + +Now that we've got all the preliminaries out of the way, it's time to set up our application. For this we click on the **+ Create** button next to Applications. Create a new application, and choose to use GitHub. + +![][6] + +[6]: /img/tutorials/automated-deployments/wercker-add-app.png + +## Select a repository + +Clicking this will make Wercker show you all the repositories you have on GitHub, but you can easily filter them as well. So we search for our repository, select it, and then click on "Use selected repo". + +![][7] + +[7]: /img/tutorials/automated-deployments/wercker-select-repository.png + +## Configure access + +As Wercker doesn't access to check out your private projects by default, it will ask you what you want to do. When your project is public, as needs to be the case if you wish to use GitHub Pages, the top choice is recommended. When you use this it will simply check out the code in the same way anybody visiting the project on GitHub can do. + +![][8] + +[8]: /img/tutorials/automated-deployments/wercker-access.png + +## Public or not + +This is a personal choice; you can make an app public so that everyone can see more details about it. This doesn't give you any real benefits either way in general, although as part of the tutorial I have of course made this app public so you can see it in action [yourself](https://app.wercker.com/#applications/5586dcbdaf7de9c51b02b0d5). + +![][9] + +[9]: /img/tutorials/automated-deployments/public-or-not.png + +## Wercker.yml + +Choose Default for your programming language. Wercker will now attempt to create an initial *wercker.yml* file for you. Or rather, it will create the code you can copy into it yourself. Because there is nothing special about our project according to Wercker, we will simply get the `debian` box. So what we do now is create a *wercker.yml* file in the local root of our project that contains the provided configuration, and after we finish setting up the app we will expand this file to make it actually do something. + +![][10] + +[10]: /img/tutorials/automated-deployments/werckeryml.png + +## And we've got an app + +The application is added now, and Wercker will be offering you the chance to trigger a build. As we haven't pushed up the **wercker.yml** file however, we will politely decline this option. Wercker has automatically added a build pipeline to your application. + +## Adding build step + +And now we're going to add the build step to the build pipeline. First, we go to the "Registry" action in the top menu and then search for "hugo build". Find the **Hugo-Build** task by Arjen and select it. + +![][11] + +[11]: /img/tutorials/automated-deployments/wercker-search.png + +## Using Hugo-Build + +Inside the details of this step you will see how to use it. At the top is a summary for very basic usage, but when scrolling down you go through the README of the step which will usually contain more details about the advanced options available and a full example of using the step. + +We're not going to use any of the advanced features in this tutorial, so we'll return to our project and add the details we need to our wercker.yml file so that it looks like below: + +```yaml +box: debian +build: + steps: + - install-packages: + packages: git + - script: + name: download theme + code: | + $(git clone https://github.com/spf13/herring-cove ./themes/herring-cove) + - arjen/hugo-build: + version: "0.14" + theme: herring-cove + flags: --buildDrafts=true +``` + +As you can see, we have two steps in the build pipeline. The first step downloads the theme, and the second step runs arjen's hugo-build step. To use a different theme, you can replace the link to the herring-cove source with another theme's repository - just make sure the name of the folder you download the theme to (./themes/your-theme-name) matches the theme name you tell arjen/hugo-build to use (theme: your-theme-name). Now we'll test that it all works as it should by pushing up our wercker.yml file to GitHub and seeing the magic at work. + +```bash +git commit -a -m "Add wercker.yml" +git push origin master +``` + +Once completed a nice tick should have appeared in front of your first build, and if you want you can look at the details by clicking on it. However, we're not done yet as we still need to deploy it to GitHub Pages. + +![][12] + +[12]: /img/tutorials/automated-deployments/using-hugo-build.png + +## Adding a deploy pipeline + +In order to deploy to GitHub Pages we need to add a deploy pipeline. + +1. First, go to your Wercker application's page. Go to the "Workflows" tab and click on "Add new pipeline." Name it whatever you want; "deploy-production" or "deploy" works fine. For your YML Pipeline name, type in "deploy" without quotes. Leave the hook type as "Default" and hit the Create button. + +2. Now you need to link the deploy pipeline to your build pipeline. In the workflow editor, click on the + next to your build pipeline and add the deploy pipeline you've just made. Now the deploy pipeline will be run automatically whenever the build pipeline is completed successfully. + +![][13] + +[13]: /img/tutorials/automated-deployments/adding-a-deploy-pipeline.png + +## Adding a deploy step + +Next, we need to add a step to our deploy pipeline that will deploy the Hugo-built website to your GitHub pages repository. Once again searching through the Steps registry, we find that the most popular step is the **lukevevier/gh-pages** step so we add the configuration for that to our wercker.yml file. Additionally, we need to ensure that the box we run on has git and ssh installed. We can do this using the **install-packages** command, which then turns the wercker.yml file into this: + +```yaml +box: debian +build: + steps: + - arjen/hugo-build: + version: "0.14" + theme: herring-cove + flags: --buildDrafts=true +deploy: + steps: + - install-packages: + packages: git ssh-client + - lukevivier/gh-pages@0.2.1: + token: $GIT_TOKEN + domain: hugo-wercker.ig.nore.me + basedir: public +``` + +How does the GitHub Pages configuration work? We've selected a couple of things, first the domain we want to use for the site. Configuring this here will ensure that GitHub Pages is aware of the domain you want to use. + +Secondly we've configured the basedir to **public**, this is the directory that will be used as the website on GitHub Pages. + +And lastly, you can see here that this has a **$GIT_TOKEN** variable. This is used for pushing our changes up to GitHub and we will need to configure this before we can do that. To do this, go to your application page and click on the "Environment" tab. Under Application Environment Variables, put **$GIT_TOKEN** for the Key. Now you'll need to create an access token in GitHub. How to do that is described on a [GitHub help page](https://help.github.com/articles/creating-an-access-token-for-command-line-use/). Copy and paste the access token you generated from GitHub into the Value box. **Make sure you check Protected** and then hit Add. + +![][14] + +[14]: /img/tutorials/automated-deployments/adding-a-deploy-step.png + +With the deploy step configured in Wercker, we can push the updated wercker.yml file to GitHub and it will create the GitHub pages site for us. The example site we used here is accessible under hugo-wercker.ig.nore.me + +## Conclusion + +From now on, any time you want to put a new post on your blog all you need to do is push your new page to GitHub and the rest will happen automatically. The source code for the example site used here is available on [GitHub](https://github.com/ArjenSchwarz/hugo-wercker-example), as is the [Hugo Build step](https://github.com/ArjenSchwarz/wercker-step-hugo-build) itself. + +If you want to see an example of how you can deploy to S3 instead of GitHub pages, take a look at [Wercker's documentation](http://devcenter.wercker.com/docs/deploy/s3.html) about how to set that up. diff --git a/docs/content/tutorials/create-a-multilingual-site.md b/docs/content/tutorials/create-a-multilingual-site.md new file mode 100644 index 000000000..8a2dd960e --- /dev/null +++ b/docs/content/tutorials/create-a-multilingual-site.md @@ -0,0 +1,168 @@ +--- +author: "Rick Cogley" +lastmod: 2015-12-24 +date: 2015-07-08 +linktitle: Multilingual Site +menu: + main: + parent: tutorials +prev: /tutorials/migrate-from-jekyll +title: Create a Multilingual Site +weight: 10 +--- + +> **Note:** Since v0.17 Hugo has built-in support for the creation of multilingual website. [Read more about it]({{< relref "content/multilingual.md" >}}). + +## Introduction + +Hugo allows you to create a multilingual site from its built-in tools. This tutorial will show one way to do it, and assumes: + +* You already know the basics about creating a Hugo site +* You have a separate domain name for each language +* You'll use `/data` files for some translation strings +* You'll use single, combined `layout` and `static` folders +* You'll use a subfolder for each language under `content` and `public` + +## Site Configs + +Create your site configs in the root of your repository, for example for an English and Japanese site. + +**English Config `config_en.toml`**: + +~~~toml +baseURL = "http://acme.com/" +title = "Acme Inc." +contentDir = "content/en" +publishDir = "public/en" + +[params] + locale = "en-US" +~~~ + +**Japanese Config `config_ja.toml`**: + +~~~toml +baseURL = "http://acme.jp/" +title = "有限会社アクミー" +contentDir = "content/ja" +publishDir = "public/ja" + +[params] + locale = "ja-JP" +~~~ + +If you had more domains and languages, you would just create more config files. The standard `config.toml` is what Hugo will run as a default, but since we're creating language-specific ones, you'll need to specify each config file when running `hugo server` or just `hugo` before deploying. + +## Prep Translation Strings in `/data` + +Create `.yaml` (or `.json` or `.toml`) files for each language, under `/data/translations`. + +**English Strings `en-US.yaml`**: + +~~~yaml +topSlogan: Acme Inc. +topSubslogan: You'll love us +... +~~~ + +**Japanese Strings `ja-JP.yaml`**: + +~~~yaml +topSlogan: 有限会社アクミー +topSubslogan: キット勝つぞ +... +~~~ + +In some cases, where there is more complex formatting within the strings you want to show, it might be better to employ some conditional logic in your template, to display a block of html per language. + +## Reference Strings in templates + +Now you can reference the strings in your templates. One way is to do it like in this `layouts/index.html`, leveraging the fact that you have the locale set: + +~~~html + + +... + + + {{ if eq .Site.Params.locale "en-US" }}{{ if .IsHome }}Welcome to {{ end }}{{ end }}{{ .Title }}{{ if eq .Site.Params.locale "ja-JP" }}{{ if .IsHome }}へようこそ{{ end }}{{ end }}{{ if ne .Title .Site.Title }} : {{ .Site.Title }}{{ end }} + ... + + +
      +

      {{ ( index $.Site.Data.translations $.Site.Params.locale ).topSlogan }}

      +

      {{ ( index $.Site.Data.translations $.Site.Params.locale ).topSubslogan }}

      +
      + + +~~~ + +The above shows both techniques, using an `if eq` and `else if eq` to check the locale, and using `index` to pull strings from the data file that matches the locale set in the site's config file. + +## Customize Dates + +At the time of this writing, Golang does not yet have support for internationalized locales, but if you do some work, you can simulate it. For example, if you want to use French month names, you can add a data file like ``data/mois.yaml`` with this content: + +~~~toml +1: "janvier" +2: "février" +3: "mars" +4: "avril" +5: "mai" +6: "juin" +7: "juillet" +8: "août" +9: "septembre" +10: "octobre" +11: "novembre" +12: "décembre" +~~~ + +... then index the non-English date names in your templates like so: + +~~~html + +~~~ + +This technique extracts the day, month and year by specifying ``.Date.Day``, ``.Date.Month``, and ``.Date.Year``, and uses the month number as a key, when indexing the month name data file. + +## Create Multilingual Content + +Now you can create markdown content in your languages, in the `content/en` and `content/ja` folders. The frontmatter stays the same on the key side, but the values would be set in each of the languages. + +## Run Hugo Server or Deploy Commands + +Once you have things set up, you can run `hugo server` or `hugo` before deploying. You can create scripts to do it, or as shell functions. Here are sample basic `zsh` functions: + +**Live Reload with `hugo server`**: + +~~~shell +function hugoserver-com { + cd /Users/me/dev/mainsite + hugo server --buildDrafts --verbose --source="/Users/me/dev/mainsite" --config="/Users/me/dev/mainsite/config_en.toml" --port=1377 +} +function hugoserver-jp { + cd /Users/me/dev/mainsite + hugo server --buildDrafts --verbose --source="/Users/me/dev/mainsite" --config="/Users/me/dev/mainsite/config_ja.toml" --port=1399 +} +~~~ + +**Deploy with `hugo` and `rsync`**: + +~~~shell +function hugodeploy-acmecom { + rm -rf /tmp/acme.com + hugo --config="/Users/me/dev/mainsite/config_en.toml" -s /Users/me/dev/mainsite/ -d /tmp/acme.com + rsync -avze "ssh -p 22" --delete /tmp/acme.com/ me@mywebhost.com:/home/me/webapps/acme_com_site +} + +function hugodeploy-acmejp { + rm -rf /tmp/acme.jp + hugo --config="/Users/me/dev/mainsite/config_ja.toml" -s /Users/me/dev/mainsite/ -d /tmp/acme.jp + rsync -avze "ssh -p 22" --delete /tmp/acme.jp/ me@mywebhost.com:/home/me/webapps/acme_jp_site +} +~~~ + +Adjust to fit your situation, setting dns, your webserver config, and other settings as appropriate. diff --git a/docs/content/tutorials/creating-a-new-theme.md b/docs/content/tutorials/creating-a-new-theme.md new file mode 100644 index 000000000..a0b95b5e6 --- /dev/null +++ b/docs/content/tutorials/creating-a-new-theme.md @@ -0,0 +1,1717 @@ +--- +author: "Michael Henderson" +lastmod: 2016-09-01 +date: 2015-11-26 +linktitle: Creating a New Theme +menu: + main: + parent: tutorials +next: /tutorials/github-pages-blog +prev: /tutorials/automated-deployments +title: Creating a New Theme +weight: 10 +--- +## Introduction + +This tutorial will show you how to create a simple theme in Hugo. + +I'll introduce Hugo's use of templates, +and explain how to organize them into a theme. +The theme will grow, minimizing effort while meeting evolving needs. +To promote this focus, and to keep everything simple, I'll omit CSS styling. + +We'll start by creating a tiny, blog-like web site. +We'll implement this blog with just one — quite basic — template. +Then we'll add an About page, and a few articles. +Overall, this web site (along with what you learn here) +will provide a good basis for you to continue working with Hugo in the future. +By making small variations, +you'll be able to create many different kinds of web sites. + +I will assume you're comfortable with HTML, Markdown formatting, +and the Bash command line (possibly using [Git for +Windows](https://git-for-windows.github.io/)). + +A few symbols might call for explanation: in this tutorial, +the commands you'll enter will be preceded by a `$` prompt — +and their output will follow. +`vi` means to open your editor; then `:wq` means to save the file. +Sometimes I'll add comments to explain a point — these start with `#`. +So, for example: +```bash +# this is a comment +$ echo this is a command +this is a command + +# edit the file +$ vi foo.md ++++ +date = "2040-01-18" +title = "creating a new theme" + ++++ +Bah! Humbug! +:wq + +# show it +$ cat foo.md ++++ +date = "2040-01-18" +title = "creating a new theme" + ++++ +Bah! Humbug! +``` +## Definitions + +Three concepts: + +1. _Non-content_ files; +1. _Templates_ (as Hugo defines them); and +1. _Front-matter_ + +are essential for creating your first Hugo theme, +as well as your first Hugo website. +### Non-Content + +The source files of a web site (destined to be rendered by Hugo) +are divided into two kinds: + +1. The files containing its textual content (and nothing else — +except Hugo front-matter: see below, and Markdown styling); and +1. All other files. (These contain ***no*** textual content — ideally.) + +Temporarily, let's affix the adjective _non-content_ +to the latter kind of source files. + +Non-content files are responsible for your web site's look and feel. +(Follow these article links from [Bop +Design](https://www.bopdesign.com/bop-blog/2013/11/what-is-the-look-and-feel-of-a-website-and-why-its-important/) +and +[Wikipedia](https://en.wikipedia.org/w/index.php?title=Look_and_feel&oldid=731052704) +if you wish for more information.) +They comprise its images, its CSS (for the sizes, colors and fonts), +its JavaScript (for the actions and reactions), and its Hugo templates +(which contain the rules Hugo uses to transform your content into HTML). + +Given these files, Hugo will render a static web site — +informed by your content — +which contains the above images, HTML, CSS and JavaScript, +ready to be served to visitors. + +Actually, a few of your invariant textual snippets +could reside in non-content files as well. +However, because someone might reuse your theme (eventually), +preferably you should keep those textual snippets in their own content files. +#### Where + +Regarding where to create your non-content files, you have two options. +The simplest is the `./layouts/` and `./static/` filesystem trees. +If you choose this way, +then you needn't worry about configuring Hugo to find them. +Invariably, these are the first two places Hugo seeks for templates +(as well as images, CSS and JavaScript); +so in that case, it's guaranteed to find all your non-content files. + +The second option is to create them in a filesystem tree +located somewhere under the `./themes/` directory. +If you choose that way, +then you must always tell Hugo where to search for them — +that's extra work, though. So, why bother? +#### Theme + +Well — the difference between creating your non-content files under +`./layouts/` and `./static/` and creating them under `./themes/` +is admittedly very subtle. +Non-content files created under `./layouts/` and `./static/` +cannot be customized without editing them directly. +On the other hand, non-content files created under `./themes/` +can be customized, in another way. That way is both conventional +(for Hugo web sites) and non-destructive. Therefore, +creating your non-content files under `./themes/` +makes it easier for other people to use them. + +The rest of this tutorial will call a set of non-content files a ***theme*** +if they comprise a filesystem tree rooted anywhere under the +`./themes/` directory. + +Note that you can use this tutorial to create your set of non-content files +under `./layouts/` and `./static/` if you wish. The only difference is that +you wouldn't need to edit your web site's configuration file +in order to select a theme. +### Home + +The home page, or landing page, +is the first page that many visitors to a web site will see. +Often this is `/index.html`, located at the root URL of the web site. +Since Hugo writes files into the `./public/` tree, +your home page will reside in file `./public/index.html`. +### Configure + +When Hugo runs, it first looks for an overall configuration file, +in order to read its settings, and applies them to the entire web site. +These settings override Hugo's default values. + +The file can be in TOML, YAML, or JSON format. +I prefer TOML for my configuration files. +If you prefer JSON or YAML, you'll need to translate my examples. +You'll also need to change the basename, since Hugo uses its extension +to determine how to process it. + +Hugo translates Markdown files into HTML. +By default, Hugo searches for Markdown files in the `./content/` tree +and template files under the `./themes/` directory. +It will render HTML files to the `./public/` tree. +You can override any of these defaults by specifying alternative locations +in the configuration file. +### Template + +_Templates_ direct Hugo in rendering content into HTML; +they bridge content and presentation. + +Rules in template files determine which content is published and where, +and precisely how it will be rendered into HTML files. +Templates also guide your web site's presentation +by specifying the CSS styling to use. + +Hugo uses its knowledge of each piece of content +to seek a template file to use in rendering it. +If it can't find a template that matches the content, it will zoom out, +one conceptual level; it will then resume the search from there. +It will continue to do so, till it finds a matching template, +or runs out of templates to try. +Its last resort is your web site's default template, +which could conceivably be missing. If it finds no suitable template, +it simply forgoes rendering that piece of content. + +It's important to note that _front-matter_ (see next) +can influence Hugo's template file selection process. +### Content + +Content is stored in text files which contain two sections. +The first is called _front-matter_: this is information about the content. +The second contains Markdown-formatted text, +destined for conversion to HTML format. +#### Front-Matter + +The _front-matter_ is meta-information describing the content. +Like the web site's configuration file, it can be written in the +TOML, YAML, or JSON formats. +Unlike the configuration file, Hugo doesn't use the file's extension +to determine the format. +Instead, it looks for markers in the file which signal this. +TOML is surrounded by "`+++`" and YAML by "`---`", but +JSON is enclosed in curly braces. I prefer to use TOML. +You'll need to translate my examples if you prefer YAML or JSON. + +Hugo informs its chosen template files with the front-matter information +before rendering the content in HTML. +#### Markdown + +Content is written in Markdown format, which makes it easy to create. +Hugo runs the content through a Markdown engine to transform it into HTML, +which it then renders to the output file. +### Template Kinds + +Here I'll discuss three kinds of Hugo templates: +_Single_, _List_, and _Partial_. +All these kinds take one or more pieces of content as input, +and transform the pieces, based on commands in the template. +#### Single + +A _Single_ template is used to render one piece of content. +For example, an article or a post is a single piece of content; +thus, it uses a Single template. +#### List + +A _List_ template renders a group of related content items. +This could be a summary of recent postings, +or all of the articles in a category. +List templates can contain multiple groups (or categories). + +The home page template is a special kind of List template. +This is because Hugo assumes that your home page will act as a portal +to all of the remaining content on your web site. +#### Partial + +A _Partial_ template is a template that's incapable of producing a web page, +by itself. To include a Partial template in your web site, +another template must call it, using the `partial` command. + +Partial templates are very handy for rolling up common behavior. +For example, you might want the same banner to appear on all +of your web site's pages — so, rather than copy your banner's text +into multiple content files, +as well as the other information relevant to your banner +into multiple template files (both Single and List), +you can instead create just one content file and one Partial template. +That way, whenever you decide to change the banner, you can do so +by editing one file only (or maybe two). +## Site + +Let's let Hugo help you create your new web site. +The `hugo new site` command will generate a skeleton — +it will give you a basic directory structure, along with +a usable configuration file: +```bash +$ cd /tmp/ + +$ hugo new site mySite + +$ cd mySite/ + +$ ls -l +total 8 +drwxr-xr-x 2 {user} {group} 68 {date} archetypes +-rw-r--r-- 1 {user} {group} 107 {date} config.toml +drwxr-xr-x 2 {user} {group} 68 {date} content +drwxr-xr-x 2 {user} {group} 68 {date} data +drwxr-xr-x 2 {user} {group} 68 {date} layouts +drwxr-xr-x 2 {user} {group} 68 {date} static +drwxr-xr-x 2 {user} {group} 68 {date} themes +``` +Take a look in the `./content/` and `./themes/` directories to confirm +they are empty. + +The other directories +(`./archetypes/`, `./data/`, `./layouts/` and `./static/`) +are used for customizing a named theme. +That's a topic for a different tutorial, so please ignore them for now. +### Render + +Running the `hugo` command with no options will read +all of the available content and render the HTML files. Also, it will copy +all the static files (that's everything besides content). +Since we have an empty web site, Hugo won't be doing much. +However, generally speaking, Hugo does this very quickly: +```bash +$ hugo --verbose +INFO: {date} {source} Using config file: /tmp/mySite/config.toml +WARN: {date} {source} No theme set +INFO: {date} {source} /tmp/mySite/static/ is the only static directory available to sync from +INFO: {date} {source} syncing static files to /tmp/mySite/public/ +Started building site +WARN: {date} {source} Unable to locate layout for homepage: [index.html _default/list.html] +WARN: {date} {source} "/" is rendered empty +============================================================= +Your rendered home page is blank: /index.html is zero-length + * Did you specify a theme on the command-line or in your + "config.toml" file? (Current theme: "") +============================================================= +WARN: {date} {source} Unable to locate layout for 404 page: [404.html] +WARN: {date} {source} "404.html" is rendered empty +0 draft content +0 future content +0 expired content +0 pages created +0 non-page files copied +0 paginator pages created +0 tags created +0 categories created +in 4 ms +``` +The "`--verbose`" flag gives extra information that will be helpful +whenever we are developing a template. +Every line of the output starting with "INFO:" or "WARN:" is present +because we used that flag. The lines that start with "WARN:" +are warning messages. We'll go over them later. + +We can verify that the command worked by looking at the directory again: +```bash +$ ls -l +total 8 +drwxr-xr-x 2 {user} {group} 68 {date} archetypes +-rw-r--r-- 1 {user} {group} 107 {date} config.toml +drwxr-xr-x 2 {user} {group} 68 {date} content +drwxr-xr-x 2 {user} {group} 68 {date} data +drwxr-xr-x 2 {user} {group} 68 {date} layouts +drwxr-xr-x 6 {user} {group} 204 {date} public +drwxr-xr-x 2 {user} {group} 68 {date} static +drwxr-xr-x 2 {user} {group} 68 {date} themes +``` +See that new `./public/` directory? +Hugo placed all its rendered content there. +When you're ready to publish your web site, that's the place to start. +For now, though, let's just confirm we have the files we expect +for a web site with no content: +```bash +$ ls -l public/ +total 16 +-rw-r--r-- 1 {user} {group} 0 {date} 404.html +-rw-r--r-- 1 {user} {group} 0 {date} index.html +-rw-r--r-- 1 {user} {group} 511 {date} index.xml +-rw-r--r-- 1 {user} {group} 210 {date} sitemap.xml +``` +Hugo rendered two XML files and some empty HTML files. +The XML files are used for RSS feeds. Hugo has an opinion about what +those feeds should contain, so it populated those files. +Hugo has no opinion on the look or content of your web site, +so it left those files empty. + +If you look back at the output from the `hugo server` command, +you'll notice that Hugo said: +```bash +0 pages created +``` +That's because Hugo doesn't count the home page, the 404 error page, +or the RSS feed files as pages. +### Serve + +Let's verify you can run the built-in web server — +that'll shorten your development cycle, dramatically. +Start it, by running the `hugo server` command. +If successful, you'll see output similar to the following: +```bash +$ hugo server --verbose +INFO: {date} {source} Using config file: /tmp/mySite/config.toml +WARN: {date} {source} No theme set +INFO: {date} {source} /tmp/mySite/static/ is the only static directory available to sync from +INFO: {date} {source} syncing static files to / +WARN: {date} {source} Unable to locate layout for homepage: [index.html _default/list.html] +WARN: {date} {source} "/" is rendered empty +============================================================= +Your rendered home page is blank: /index.html is zero-length + * Did you specify a theme on the command-line or in your + "config.toml" file? (Current theme: "") +============================================================= +WARN: {date} {source} Unable to locate layout for 404 page: [404.html] +WARN: {date} {source} "404.html" is rendered empty +0 draft content +0 future content +0 expired content +0 pages created +0 non-page files copied +0 paginator pages created +0 tags created +0 categories created +in 3 ms +Watching for changes in /tmp/mySite/{data,content,layouts,static} +Serving pages from memory +Web Server is available at http://localhost:1313/ (bind address 127.0.0.1) +Press Ctrl+C to stop +``` +Connect to the listed URL (it's on the line that begins with +`Web Server is available`). If everything's working correctly, +you should get a page that shows nothing. +### Warnings + +Let's go back and look at some of those warnings again: +```bash +WARN: {date} {source} Unable to locate layout for 404 page: [404.html] +WARN: {date} {source} Unable to locate layout for homepage: [index.html _default/list.html] +``` +The 404 warning is easy to explain — it's because we haven't created +the template file `layouts/404.html`. Hugo uses this to render an HTML file +which serves "page not found" errors. However, +the 404 page is a topic for a separate tutorial. + +Regarding the home page warning: the first layout Hugo looked for was +`layouts/index.html`. Note that Hugo uses this file for the home page only. + +It's good that Hugo lists the files it seeks, when +we give it the verbose flag. For the home page, these files are +`layouts/index.html` and `layouts/_default/list.html`. +Later, we'll cover some rules which explain these paths +(including their basenames). For now, just remember that +Hugo couldn't find a template to use for the home page, and it said so. + +All right! So, now — after these few steps — you have a working +installation, and a web site foundation you can build upon. +All that's left is to add some content, as well as a theme to display it. +## Theme + +Hugo doesn't ship with a default theme. However, a large number of themes +are easily available: for example, at +[hugoThemes](https://github.com/gohugoio/hugoThemes). +Also, Hugo comes with a command to generate them. + +We're going to generate a new theme called Zafta. +The goal of this tutorial is simply to show you how to create +(in a theme) the minimal files Hugo needs in order to display your content. +Therefore, the theme will exclude CSS — +it'll be functional, not beautiful. + +Every theme has its own opinions on content and layout. For example, this +Zafta theme prefers the Type "article" over the Types "blog" or "post." +Strong opinions make for simpler templates, but unconventional opinions +make themes tougher for other users. So when you develop a theme, you should +consider the value of adopting the terms used by themes similar to yours. +### Skeleton + +Let's press Ctrl+C and use the `hugo new theme` command +to generate the skeleton of a theme. The result is a directory structure +containing empty files for you to fill out: +```bash +$ hugo new theme zafta + +$ find themes -type f | xargs ls -l +-rw-r--r-- 1 {user} {group} 8 {date} themes/zafta/archetypes/default.md +-rw-r--r-- 1 {user} {group} 0 {date} themes/zafta/layouts/404.html +-rw-r--r-- 1 {user} {group} 0 {date} themes/zafta/layouts/_default/list.html +-rw-r--r-- 1 {user} {group} 0 {date} themes/zafta/layouts/_default/single.html +-rw-r--r-- 1 {user} {group} 0 {date} themes/zafta/layouts/index.html +-rw-r--r-- 1 {user} {group} 0 {date} themes/zafta/layouts/partials/footer.html +-rw-r--r-- 1 {user} {group} 0 {date} themes/zafta/layouts/partials/header.html +-rw-r--r-- 1 {user} {group} 1081 {date} themes/zafta/LICENSE.md +-rw-r--r-- 1 {user} {group} 450 {date} themes/zafta/theme.toml +``` +The skeleton includes templates (files ending in `.html`), a license file, +a description of your theme (`theme.toml`), and a default archetype file. + +When you're developing a real theme, please remember to fill out files +`theme.toml` and `LICENSE.md`. They're optional, but if you're going to +distribute your theme, it tells the world who to praise (or blame). +It's also important to declare your choice of license, so people will know +whether (or where) they can use your theme. + +Note that the skeleton theme's template files are empty. Don't worry; +we'll change that shortly: +```bash +$ find themes/zafta -name '*.html' | xargs ls -l +-rw-r--r-- 1 {user} {group} 0 {date} themes/zafta/layouts/404.html +-rw-r--r-- 1 {user} {group} 0 {date} themes/zafta/layouts/_default/list.html +-rw-r--r-- 1 {user} {group} 0 {date} themes/zafta/layouts/_default/single.html +-rw-r--r-- 1 {user} {group} 0 {date} themes/zafta/layouts/index.html +-rw-r--r-- 1 {user} {group} 0 {date} themes/zafta/layouts/partials/footer.html +-rw-r--r-- 1 {user} {group} 0 {date} themes/zafta/layouts/partials/header.html +``` +### Select + +Now that we've created a theme we can work with, it's a good idea +to add its name to the configuration file. This is optional, because +it's possible to add "-t zafta" to all your commands. +I like to put it in the configuration file because I like +shorter command lines. If you don't put it in the configuration file, +or specify it on the command line, sometimes you won't get the template +you're expecting. + +So, let's edit your configuration file to add the theme name: +```toml +$ vi config.toml +theme = "zafta" +baseURL = "http://example.org/" +title = "My New Hugo Site" +languageCode = "en-us" +:wq +``` +### Themed Render + +Now that we have a theme (albeit empty), let's render the web site again: +```bash +$ hugo --verbose +INFO: {date} {source} Using config file: /tmp/mySite/config.toml +INFO: {date} {source} using a UnionFS for static directory comprised of: +INFO: {date} {source} Base: /tmp/mySite/themes/zafta/static +INFO: {date} {source} Overlay: /tmp/mySite/static/ +INFO: {date} {source} syncing static files to /tmp/mySite/public/ +Started building site +WARN: {date} {source} "/" is rendered empty +============================================================= +Your rendered home page is blank: /index.html is zero-length + * Did you specify a theme on the command-line or in your + "config.toml" file? (Current theme: "zafta") +============================================================= +WARN: {date} {source} "404.html" is rendered empty +0 draft content +0 future content +0 expired content +0 pages created +0 non-page files copied +0 paginator pages created +0 tags created +0 categories created +in 4 ms +``` +Did you notice the output is different? +Two previous warning messages have disappeared, which contained the words +"Unable to locate layout" for your home page and the 404 page. +And, a new informational message tells us Hugo is accessing your theme's tree +(`./themes/zafta/`). + +Let's check the `./public/` directory to see what Hugo rendered: +```bash +$ ls -l public/ +total 16 +-rw-r--r-- 1 {user} {group} 0 {date} 404.html +drwxr-xr-x 2 {user} {group} 68 {date} css +-rw-r--r-- 1 {user} {group} 0 {date} index.html +-rw-r--r-- 1 {user} {group} 511 {date} index.xml +drwxr-xr-x 2 {user} {group} 68 {date} js +-rw-r--r-- 1 {user} {group} 210 {date} sitemap.xml +``` +It's similar to what we had before, without a theme. +We'd expect so, since all your theme's templates are empty. But notice: +in `./public/`, Hugo created the `css/` and `js/` directories. +That's because Hugo found them in your theme's `static/` directory: +```bash +$ ls -l themes/zafta/static/ +total 0 +drwxr-xr-x 2 {user} {group} 68 {date} css +drwxr-xr-x 2 {user} {group} 68 {date} js +``` +#### Home + +In a Hugo web site, each kind of page is informed (primarily) by just one +of the many different kinds of templates available; +yet the home page is special, because it gets its own kind of template, +and its own template file. + +Hugo uses template file `layouts/index.html` to render the home page's HTML. +Although Hugo's documentation may state that this file is the home page's +only required template, Hugo's earlier warning message showed it actually +looks for two different templates: +```bash +WARN: {date} {source} Unable to locate layout for homepage: [index.html _default/list.html] +``` +#### Empty + +When Hugo generated your theme, it included an empty home page template. +Whenever Hugo renders your web site, it seeks that same template and uses it +to render the HTML for the home page. Currently, the template file is empty, +so the output HTML file is empty, too. Whenever we add rules to that template, +Hugo will use them in rendering the home page: +```bash +$ find * -name index.html | xargs ls -l +-rw-r--r-- 1 {user} {group} 0 {date} public/index.html +-rw-r--r-- 1 {user} {group} 0 {date} themes/zafta/layouts/index.html +``` +As we'll see later, Hugo follows this same pattern for all its templates. +## Static Files + +Hugo does two things when it renders your web site. +Besides using templates to transform your content into HTML, +it also incorporates your static files. Hugo's rule is simple: +unlike with templates and content, static files aren't transformed. +Hugo copies them over, exactly as they are. + +Hugo assumes that your web site will use both CSS and JavaScript, +so it generates some directories in your theme to hold them. +Remember opinions? Well, Hugo's opinion is that you'll store your CSS +in directory `static/css/`, and your JavaScript in directory `static/js/`. +If you don't like that, you can relocate these directories +or change their names (as long as they remain in your theme's `static/` tree), +or delete them completely. +Hugo is nice enough to offer its opinion; yet it still behaves nicely, +if you disagree: +```bash +$ find themes/zafta/* -type d | xargs ls -dl +drwxr-xr-x 3 {user} {group} 102 {date} themes/zafta/archetypes +drwxr-xr-x 6 {user} {group} 204 {date} themes/zafta/layouts +drwxr-xr-x 4 {user} {group} 136 {date} themes/zafta/layouts/_default +drwxr-xr-x 4 {user} {group} 136 {date} themes/zafta/layouts/partials +drwxr-xr-x 4 {user} {group} 136 {date} themes/zafta/static +drwxr-xr-x 2 {user} {group} 68 {date} themes/zafta/static/css +drwxr-xr-x 2 {user} {group} 68 {date} themes/zafta/static/js +``` +## Theme Development + +Generally (using any kind of software), working on a theme means +changing your files, serving your web site again, and then verifying +the resulting improvements in your browser. +With Hugo, this way of working is quite easy: + +- First purge the `./public/` tree. (This is optional but useful, +if you want to start with a clean slate.) +- Run the built-in Hugo web server. +- Open your web site in a browser — and then: + +1. Edit your theme; +1. Glance at your browser window to see your changes; and +1. Repeat. + +I'll throw in one more opinion: ***never*** directly edit a theme on a live +web site. Instead, always develop ***using a copy***. First, make some changes +to your theme and test them. Afterwards, **when you've got them working,** +copy them to your web site. For added safety, use a tool like Git to keep +some revision history of your content, and of your theme. Believe me: +it's too easy to lose your changes, and your mind! + +Check out the main Hugo web site for information about using Git with Hugo. +### Purge + +When rendering your web site, Hugo will create new files in the `./public/` +tree and update existing ones. But it won't delete files that are +no longer used. For example, files previously rendered with +(what is now) the wrong basename, or in the wrong directory, will remain. +Later, if you leave them, they'll likely confuse you. +Cleaning out your `./public/` files prior to rendering can help. + +When Hugo is running in web server mode (as of version 0.15), +it doesn't actually write the files. Instead, +it keeps all the rendered files in memory. So, you can "clean" up +your files simply by stopping and restarting the web server. +### Serve +#### Watch + +Hugo's watch functionality monitors the relevant content, theme and +(overriding) site trees for filesystem changes, +and renders your web site again automatically, when changes are detected. + +By default, watch is +enabled when in web server mode (`hugo server`), +but disabled for the web site renderer (`hugo`). + +In some use cases, +Hugo's web site renderer should continue running and watch — simply +type `hugo --watch` on the command line. + +Sometimes with Docker containers (and Heroku slugs), +the site sources may live on a read-only filesystem. +In that scenario, it makes no sense +for Hugo's web server to watch for file changes — so +use `hugo server --watch=false`. +#### Reload + +Hugo's built in web server includes +[LiveReload](/extras/livereload/) functionality. When any page is updated +in the filesystem, the web browser is told to refresh its currently-open tabs +from your web site. Usually, this happens faster than you can say, +"Wow, that's totally amazing!" +### Workflow + +Again, +I recommend you use the following commands as the basis for your workflow: +```bash +# purge old files. Hugo will recreate the public directory +$ rm -rf public/ + +# run Hugo in watch mode with LiveReload; +# when you're done, stop the web server +$ hugo server --verbose +Press Ctrl+C to stop +``` +Below is some sample output showing Hugo detecting a change in the home page +template. (Actually, the change is the edit we're about to do.) Once it's +rendered again, the web browser automatically reloads the page. + +(As I said above — it's amazing:) +```bash +$ rm -rf public/ + +$ hugo server --verbose +INFO: {date} {source} Using config file: /tmp/mySite/config.toml +INFO: {date} {source} using a UnionFS for static directory comprised of: +INFO: {date} {source} Base: /tmp/mySite/themes/zafta/static +INFO: {date} {source} Overlay: /tmp/mySite/static/ +INFO: {date} {source} syncing static files to / +Started building site +WARN: {date} {source} "/" is rendered empty +============================================================= +Your rendered home page is blank: /index.html is zero-length + * Did you specify a theme on the command-line or in your + "config.toml" file? (Current theme: "") +============================================================= +WARN: {date} {source} "404.html" is rendered empty +0 draft content +0 future content +0 expired content +0 pages created +0 non-page files copied +0 paginator pages created +0 tags created +0 categories created +in 4 ms +Watching for changes in /tmp/mySite/{data,content,layouts,static,themes} +Serving pages from memory +Web Server is available at http://localhost:1313/ (bind address 127.0.0.1) +Press Ctrl+C to stop +INFO: {date} {source} Received System Events: ["/tmp/mySite/themes/zafta/layouts/index.html": WRITE] + +Change detected, rebuilding site +{date} +Template changed /tmp/mySite/themes/zafta/layouts/index.html +WARN: {date} {source} "404.html" is rendered empty +0 draft content +0 future content +0 expired content +0 pages created +0 non-page files copied +0 paginator pages created +0 tags created +0 categories created +in 3 ms +``` +## Home Template + +The home page is one of the few special pages Hugo renders automatically. +As mentioned earlier, it looks in your theme's `layouts/` tree for one +of two files: + +1. `index.html` +1. `_default/list.html` + +We could edit the default template, but a good design principle is to edit +the most specific template available. That's not a hard-and-fast rule +(in fact, in this tutorial, we'll break it a few times), +but it's a good generalization. +### Static + +Right now, your home page is empty because you've added no content, +and because its template includes no logic. Let's change that by adding +some text to your home page template (`layouts/index.html`): +```html +$ vi themes/zafta/layouts/index.html + + + +

      Hugo says hello!

      + + +:wq +``` +Let's press Ctrl+C and render the web site, and then verify the results: +```html +$ rm -rf public/ + +$ hugo --verbose +INFO: {date} {source} Using config file: /tmp/mySite/config.toml +INFO: {date} {source} using a UnionFS for static directory comprised of: +INFO: {date} {source} Base: /tmp/mySite/themes/zafta/static +INFO: {date} {source} Overlay: /tmp/mySite/static/ +INFO: {date} {source} syncing static files to /tmp/mySite/public/ +Started building site +WARN: {date} {source} "404.html" is rendered empty +0 draft content +0 future content +0 expired content +0 pages created +0 non-page files copied +0 paginator pages created +0 tags created +0 categories created +in 4 ms + +$ ls -l public/index.html +-rw-r--r-- 1 {user} {group} 72 {date} public/index.html + +$ cat public/index.html + + + +

      Hugo says hello!

      + + +``` +### Dynamic + +A ***dynamic*** home page? Because Hugo is a _static web site_ generator, +the word _dynamic_ seems odd, doesn't it? But this means arranging for your +home page to reflect the content in your web site automatically, +each time Hugo renders it. + +To accomplish that, later we'll add an iterator to your home page template. +## Article + +Now that Hugo is successfully rendering your home page with static content, +let's add more pages to your web site. We'll display some new articles +as a list on your home page; and we'll display each article +on its own page, too. + +Hugo has a command to generate an entry skeleton for new content, +just as it does for web sites and themes: +```bash +$ hugo --verbose new article/First.md +INFO: {date} {source} Using config file: /tmp/mySite/config.toml +INFO: {date} {source} attempting to create article/First.md of article +INFO: {date} {source} curpath: /tmp/mySite/themes/zafta/archetypes/default.md +INFO: {date} {source} creating /tmp/mySite/content/article/First.md +/tmp/mySite/content/article/First.md created + +$ ls -l content/article/ +total 8 +-rw-r--r-- 1 {user} {group} 61 {date} First.md +``` +Let's generate a second article, while we're here: +```bash +$ hugo --verbose new article/Second.md +INFO: {date} {source} Using config file: /tmp/mySite/config.toml +INFO: {date} {source} attempting to create article/Second.md of article +INFO: {date} {source} curpath: /tmp/mySite/themes/zafta/archetypes/default.md +INFO: {date} {source} creating /tmp/mySite/content/article/Second.md +/tmp/mySite/content/article/Second.md created + +$ ls -l content/article/ +total 16 +-rw-r--r-- 1 {user} {group} 61 {date} First.md +-rw-r--r-- 1 {user} {group} 62 {date} Second.md +``` +Let's edit both those articles. Be careful to preserve their front-matter, +but append some text to their bodies, as follows: +```bash +$ vi content/article/First.md +In vel ligula tortor. Aliquam erat volutpat. +Pellentesque at felis eu quam tincidunt dignissim. +Nulla facilisi. + +Pellentesque tempus nisi et interdum convallis. +In quam ante, vulputate at massa et, rutrum +gravida dui. Phasellus tristique libero at ex. +:wq + +$ vi content/article/Second.md +Fusce lacus magna, maximus nec sapien eu, +porta efficitur neque. Aliquam erat volutpat. +Vestibulum enim nibh, posuere eu diam nec, +varius sagittis turpis. + +Praesent quis sapien egestas mauris accumsan +pulvinar. Ut mattis gravida venenatis. Vivamus +lobortis risus id nisi rutrum, at iaculis. +:wq +``` +So, for example, `./content/article/Second.md` becomes: +```toml +$ cat content/article/Second.md ++++ +date = "2040-01-18T21:08:08-06:00" +title = "Second" + ++++ +Fusce lacus magna, maximus nec sapien eu, +porta efficitur neque. Aliquam erat volutpat. +Vestibulum enim nibh, posuere eu diam nec, +varius sagittis turpis. + +Praesent quis sapien egestas mauris accumsan +pulvinar. Ut mattis gravida venenatis. Vivamus +lobortis risus id nisi rutrum, at iaculis. +``` +Let's render the web site, and then verify the results: +```bash +$ rm -rf public/ + +$ hugo --verbose +INFO: {date} {source} Using config file: /tmp/mySite/config.toml +INFO: {date} {source} using a UnionFS for static directory comprised of: +INFO: {date} {source} Base: /tmp/mySite/themes/zafta/static +INFO: {date} {source} Overlay: /tmp/mySite/static/ +INFO: {date} {source} syncing static files to /tmp/mySite/public/ +Started building site +INFO: {date} {source} found taxonomies: map[string]string{"tag":"tags", "category":"categories"} +WARN: {date} {source} "article" is rendered empty +WARN: {date} {source} "article/Second.html" is rendered empty +WARN: {date} {source} "article/First.html" is rendered empty +WARN: {date} {source} "404.html" is rendered empty +0 draft content +0 future content +0 expired content +2 pages created +0 non-page files copied +0 paginator pages created +0 tags created +0 categories created +in 7 ms +``` +The output says Hugo rendered ("created") two pages. +Those pages are your new articles: +```bash +$ find public -type f -name '*.html' | xargs ls -l +-rw-r--r-- 1 {user} {group} 0 {date} public/404.html +-rw-r--r-- 1 {user} {group} 0 {date} public/article/First/index.html +-rw-r--r-- 1 {user} {group} 0 {date} public/article/index.html +-rw-r--r-- 1 {user} {group} 0 {date} public/article/Second/index.html +-rw-r--r-- 1 {user} {group} 72 {date} public/index.html +``` +The new pages are empty, because Hugo rendered their HTML from empty +template files. The home page doesn't show us the new content, either: +```html +$ cat public/index.html + + + +

      Hugo says hello!

      + + +``` +So, we have to edit the templates, in order to pick up the articles. +### Single & List + +Here again I'll discuss three kinds of Hugo templates. One kind is +the home page template we edited previously; it's applicable only to +the home page. Another kind is Single templates, which render output for +just one content file. The third kind are List templates, which group +multiple pieces of content before rendering output. + +It's important to note that, generally, List templates +(except the home page template) are named `list.html`; +and Single templates are named `single.html`. + +Hugo also has three other kinds of templates: +Partials, _Content Views_, and _Terms_. +We'll give examples of some Partial templates; but otherwise, +we won't go into much detail about these. +### Home + +You'll want your home page to list the articles you just created. +So, let's alter its template file (`layouts/index.html`) to show them. +Hugo runs each template's logic whenever it renders that template's web page +(of course): +```html +$ vi themes/zafta/layouts/index.html + + + + {{- range first 10 .Data.Pages }} +

      {{ .Title }}

      + {{- end }} + + +:wq +``` +#### Engine + +Hugo uses the [Go language's template +engine](https://gohugo.io/templates/go-templates/). +That engine scans your template files for commands enclosed between +"{{" and "}}" (these are doubled, curly braces — affectionately +known as "mustaches"). + +BTW, a hyphen, if placed immediately after an opening mustache, or +immediately before a closing one, will prevent extraneous newlines. +(This can make Hugo's output look better, when viewed as text.) + +So, the mustache commands in your newly-altered template are: + +1.  `range ...` +1.  `.Permalink` +1.  `.Title` +1.  `end` + +The `range` command is an iterator. We're using it to go through the latest +ten pages. (Hugo characterizes some of its HTML output files as "pages," +but not all — see above.) + +Looping through the list of data pages will consider each such HTML file +that Hugo renders (or rather — to speak more precisely — each +such HTML file that Hugo currently calculates it _will_ render). + +It's helpful to remember that Hugo sets some variables, such as `.Data`, quite +early in its overall processing. Hugo loads information from every content +file into that variable, and gives all the templates a chance to process that +variable's contents, before actually rendering any HTML output files. + +`.Permalink` supplies the URL which links to that article's page, and +`.Title` supplies the value of its "title" variable. Hugo obtains this +from the front-matter in the article's Markdown file. + +Automatically, the pages are considered in descending order of the generation +times of their Markdown files (actually, based on the value of the "date" +variable in their front-matter) so that the latest is first (naturally). + +The `end` command signals the end of the range iterator. The engine +loops back to the top of the iterator, whenever it finds `end.` +Everything between `range` and `end` is reevaluated, +each time the engine goes through the iterator. + +For the present template, this means that the titles of your latest +ten pages (or however many exist, if that's less) become the +[textContent](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent) +of an equivalent number of copies Hugo makes, of your level-four +subheading tags (and anchor tags). `.Permalink` enables these to link +to the actual articles. + +Let's render your web site, and then verify the results: +```html +$ rm -rf public/ + +$ hugo --verbose +INFO: {date} {source} Using config file: /tmp/mySite/config.toml +INFO: {date} {source} using a UnionFS for static directory comprised of: +INFO: {date} {source} Base: /tmp/mySite/themes/zafta/static +INFO: {date} {source} Overlay: /tmp/mySite/static/ +INFO: {date} {source} syncing static files to /tmp/mySite/public/ +Started building site +INFO: {date} {source} found taxonomies: map[string]string{"tag":"tags", "category":"categories"} +WARN: {date} {source} "article" is rendered empty +WARN: {date} {source} "article/Second.html" is rendered empty +WARN: {date} {source} "article/First.html" is rendered empty +WARN: {date} {source} "404.html" is rendered empty +0 draft content +0 future content +0 expired content +2 pages created +0 non-page files copied +0 paginator pages created +0 tags created +0 categories created +in 7 ms + +$ find public -type f -name '*.html' | xargs ls -l +-rw-r--r-- 1 {user} {group} 0 {date} public/404.html +-rw-r--r-- 1 {user} {group} 0 {date} public/article/First/index.html +-rw-r--r-- 1 {user} {group} 0 {date} public/article/index.html +-rw-r--r-- 1 {user} {group} 0 {date} public/article/Second/index.html +-rw-r--r-- 1 {user} {group} 232 {date} public/index.html + +$ cat public/index.html + + + +

      Second

      +

      First

      + + +``` +### All Done + +Congratulations! Your home page shows the titles of your two articles, along +with the links to them. The articles themselves are still empty. But, +let's take a moment to appreciate what we've done, so far! + +Your home page template (`layouts/index.html`) now renders output dynamically. +Believe it or not, by inserting the range command inside those doubled +curly braces, you've learned everything you need to know — +essentially — about developing a theme. + +All that's left is understanding which of your templates renders each content +file, and becoming more familiar with the commands for the template engine. +## More + +Well — if things were so simple, this tutorial would be much shorter! + +Some things are still useful to learn, because they'll make creating new +templates _much_ easier — so, I'll cover them, now. +### Base URL + +While developing and testing your theme, did you notice that the links in the +rendered `./public/index.html` file use the full "baseURL" from your +`./config.toml` file? That's because those files are intended to be deployed +to your web server. + +Whenever you test your theme, you start Hugo in web server mode +(with `hugo server`) and connect to it with your web browser. +That command is smart enough to replace the "baseURL" with +`http://localhost:1313` on the fly, so that the links automatically +work for you. + +That's another reason why we recommend testing with the built-in web server. +### Content + +The articles you've been working with are in your `./content/article/` +directory. That means their _Section_ (as far as templates are concerned) +is "article". Unless we do something unusual in their front-matter, their +_Type_ is also "article". +#### Search + +Hugo uses the Section and Type to find a template file for every piece of +content it renders. Hugo first will seek a template file in subdirectories of +`layouts/` that match its Section or Type name (i.e., in `layouts/SECTION/` +or `layouts/TYPE/`). If it can't find a file there, then it will look in the +`layouts/_default/` directory. Other documentation covers some twists about +categories and tags, but we won't use those in this tutorial. Therefore, +we can assume that Hugo will try first `layouts/article/single.html`, then +`layouts/_default/single.html`. + +Now that we know the search rule, let's see what's available: +```bash +$ find themes/zafta -name single.html | xargs ls -l +-rw-r--r-- 1 {user} {group} 0 {date} themes/zafta/layouts/_default/single.html +``` +If you look back at the articles Hugo has rendered, you can see that +they were empty. Now we can see that this is because Hugo sought +`layouts/article/single.html` but couldn't find it, and template +`layouts/_default/single.html` was empty. Therefore, the rendered article +file was empty, too. + +So, we could either create a new template, `layouts/article/single.html`, +or edit the default one. +#### Default Single + +Since we know of no other content Types, let's start by editing the default +template file, `layouts/_default/single.html`. + +As we mentioned earlier, you always should edit (or create) the most +specific template first, in order to avoid accidentally changing how other +content is displayed. However, we're breaking that rule intentionally, +just so we can explore how the default is used. + +Remember, any content — for which we don't create a specific template +— will end up using this default template. That can be good or bad. +Bad, because I know we'll be adding different Types of content, and we'll +eventually undo some of the changes we've made. Good, because then we'll be +able to see some results immediately. It's also good to create the default +template first, because with it, we can start to develop the basic layout +for the web site. + +As we add more content Types, we'll refactor this file and move its logic +around. Hugo makes this fairly painless, so we'll accept the cost and proceed. + +Please see Hugo's documentation on template rendering, for all the details on +determining which template to use. And, as the documentation mentions, if +your web site is a single-page application (SPA), you can delete all the +other templates and work with just the default Single one. By itself, +that fact provides a refreshing amount of joy. + +Let's edit the default template file (`layouts/_default/single.html`): +```html +$ vi themes/zafta/layouts/_default/single.html + + + + {{ .Title }} + + +

      {{ .Title }}

      +
      {{ .Date.Format "Mon, Jan 2, 2006" }}
      + {{ .Content }} +

      Home

      + + +:wq +``` +#### Verify + +Let's render the web site, and verify the results: +```bash +$ rm -rf public/ + +$ hugo --verbose +INFO: {date} {source} Using config file: /tmp/mySite/config.toml +INFO: {date} {source} using a UnionFS for static directory comprised of: +INFO: {date} {source} Base: /tmp/mySite/themes/zafta/static +INFO: {date} {source} Overlay: /tmp/mySite/static/ +INFO: {date} {source} syncing static files to /tmp/mySite/public/ +Started building site +INFO: {date} {source} found taxonomies: map[string]string{"tag":"tags", "category":"categories"} +WARN: {date} {source} "article" is rendered empty +WARN: {date} {source} "404.html" is rendered empty +0 draft content +0 future content +0 expired content +2 pages created +0 non-page files copied +0 paginator pages created +0 tags created +0 categories created +in 7 ms + +$ find public -type f -name '*.html' | xargs ls -l +-rw-r--r-- 1 {user} {group} 0 {date} public/404.html +-rw-r--r-- 1 {user} {group} 473 {date} public/article/First/index.html +-rw-r--r-- 1 {user} {group} 0 {date} public/article/index.html +-rw-r--r-- 1 {user} {group} 514 {date} public/article/Second/index.html +-rw-r--r-- 1 {user} {group} 232 {date} public/index.html +``` +Note that although Hugo rendered a file, to list your articles: +`./public/article/index.html`, the file is empty, because we don't have +a template for it. (However: see next.) The other HTML files contain your +content, as we can see below: +```html +$ cat public/article/First/index.html + + + + First + + +

      First

      +
      Wed, Jan 18, 2040
      +

      In vel ligula tortor. Aliquam erat volutpat. +Pellentesque at felis eu quam tincidunt dignissim. +Nulla facilisi.

      + +

      Pellentesque tempus nisi et interdum convallis. +In quam ante, vulputate at massa et, rutrum +gravida dui. Phasellus tristique libero at ex.

      + +

      Home

      + + + +$ cat public/article/Second/index.html + + + + Second + + +

      Second

      +
      Wed, Jan 18, 2040
      +

      Fusce lacus magna, maximus nec sapien eu, +porta efficitur neque. Aliquam erat volutpat. +Vestibulum enim nibh, posuere eu diam nec, +varius sagittis turpis.

      + +

      Praesent quis sapien egestas mauris accumsan +pulvinar. Ut mattis gravida venenatis. Vivamus +lobortis risus id nisi rutrum, at iaculis.

      + +

      Home

      + + +``` +Again, notice that your rendered article files have content. +You can run `hugo server` and use your browser to confirm this. +You should see your home page, and it should contain the titles of both +articles. Each title should be a link to its respective article. + +Each article should be displayed fully on its own page. And at the bottom of +each article, you should see a link which takes you back to your home page. +### Article List + +Your home page still lists your most recent articles. However — +remember, from above, that I mentioned an empty file, +`./public/article/index.html`? +Let's make that show a list of ***all*** of your articles +(not just the latest ten). + +We need to decide which template to edit. Key to this, is that +individual pages always come from Single templates. On the other hand, +only List templates are capable of rendering pages which display collections +(or lists) of other pages. + +Because the new page will show a listing, we should select a List template. +Let's take a quick look to see which List templates are available already: +```bash +$ find themes/zafta -name list.html | xargs ls -l +-rw-r--r-- 1 {user} {group} 0 {date} themes/zafta/layouts/_default/list.html +``` +So, just as before with the single articles, so again now with the list of +articles, we must decide: whether to edit `layouts/_default/list.html`, +or to create `layouts/article/list.html`. +#### Default List + +We still don't have multiple content Types — so, remaining consistent, +let's edit the default List template: +```html +$ vi themes/zafta/layouts/_default/list.html + + + +

      Articles

      + {{- range first 10 .Data.Pages }} +

      {{ .Title }}

      + {{- end }} +

      Home

      + + +:wq +``` +Let's render everything again: +```bash +$ rm -rf public/ + +$ hugo --verbose +INFO: {date} {source} Using config file: /tmp/mySite/config.toml +INFO: {date} {source} using a UnionFS for static directory comprised of: +INFO: {date} {source} Base: /tmp/mySite/themes/zafta/static +INFO: {date} {source} Overlay: /tmp/mySite/static/ +INFO: {date} {source} syncing static files to /tmp/mySite/public/ +Started building site +INFO: {date} {source} found taxonomies: map[string]string{"tag":"tags", "category":"categories"} +WARN: {date} {source} "404.html" is rendered empty +0 draft content +0 future content +0 expired content +2 pages created +0 non-page files copied +0 paginator pages created +0 categories created +0 tags created +in 7 ms + +$ find public -type f -name '*.html' | xargs ls -l +-rw-r--r-- 1 {user} {group} 0 {date} public/404.html +-rw-r--r-- 1 {user} {group} 473 {date} public/article/First/index.html +-rw-r--r-- 1 {user} {group} 327 {date} public/article/index.html +-rw-r--r-- 1 {user} {group} 514 {date} public/article/Second/index.html +-rw-r--r-- 1 {user} {group} 232 {date} public/index.html +``` +Now (as you can see), we have a list of articles. To confirm it, +type `hugo server`; then, in your browser, navigate to `/article/`. +(Later, we'll link to it.) +## About + +Let's add an About page, and try to display it at the top level +(as opposed to the next level down, where we placed your articles). +### Guide + +Hugo's default goal is to let the directory structure of the `./content/` +tree guide the location of the HTML it renders to the `./public/` tree. +Let's check this, by generating an About page at the content's top level: +```toml +$ hugo new About.md +/tmp/mySite/content/About.md created + +$ ls -l content/ +total 8 +-rw-r--r-- 1 {user} {group} 61 {date} About.md +drwxr-xr-x 4 {user} {group} 136 {date} article + +$ vi content/About.md ++++ +date = "2040-01-18T22:01:00-06:00" +title = "About" + ++++ +Neque porro quisquam est qui dolorem +ipsum quia dolor sit amet consectetur +adipisci velit. +:wq +``` +### Check + +Let's render your web site, and check the results: +```html +$ rm -rf public/ + +$ hugo --verbose +INFO: {date} {source} Using config file: /tmp/mySite/config.toml +INFO: {date} {source} using a UnionFS for static directory comprised of: +INFO: {date} {source} Base: /tmp/mySite/themes/zafta/static +INFO: {date} {source} Overlay: /tmp/mySite/static/ +INFO: {date} {source} syncing static files to /tmp/mySite/public/ +Started building site +INFO: {date} {source} found taxonomies: map[string]string{"tag":"tags", "category":"categories"} +WARN: {date} {source} "404.html" is rendered empty +0 draft content +0 future content +0 expired content +3 pages created +0 non-page files copied +0 paginator pages created +0 tags created +0 categories created +in 9 ms + +$ ls -l public/ +total 24 +-rw-r--r-- 1 {user} {group} 0 {date} 404.html +drwxr-xr-x 3 {user} {group} 102 {date} About +drwxr-xr-x 6 {user} {group} 204 {date} article +drwxr-xr-x 2 {user} {group} 68 {date} css +-rw-r--r-- 1 {user} {group} 316 {date} index.html +-rw-r--r-- 1 {user} {group} 2221 {date} index.xml +drwxr-xr-x 2 {user} {group} 68 {date} js +-rw-r--r-- 1 {user} {group} 681 {date} sitemap.xml + +$ ls -l public/About/ +total 8 +-rw-r--r-- 1 {user} {group} 305 {date} index.html + +$ cat public/About/index.html + + + + About + + +

      About

      +
      Wed, Jan 18, 2040
      +

      Neque porro quisquam est qui dolorem +ipsum quia dolor sit amet consectetur +adipisci velit.

      + +

      Home

      + + +``` +Oh, well. — Did you notice that your page wasn't rendered at the +top level? It was rendered to a subdirectory named `./public/About/`. +That name came from the basename of your Markdown file `./content/About.md`. +Interesting — but, we'll let that go, for now. +### Home + +One other thing — let's take a look at your home page: +```html +$ cat public/index.html + + + +

      About

      +

      Second

      +

      First

      + + +``` +Did you notice that the About link is listed with your articles? +That's not exactly where we want it; so, let's edit your home page template +(`layouts/index.html`): +```html +$ vi themes/zafta/layouts/index.html + + + +

      Articles

      + {{- range first 10 .Data.Pages -}} + {{- if eq .Type "article"}} +

      {{ .Title }}

      + {{- end -}} + {{- end }} +

      Pages

      + {{- range first 10 .Data.Pages -}} + {{- if eq .Type "page" }} +

      {{ .Title }}

      + {{- end -}} + {{- end }} + + +:wq +``` +Let's render your web site, and verify the results: +```html +$ rm -rf public/ + +$ hugo --verbose +INFO: {date} {source} Using config file: /tmp/mySite/config.toml +INFO: {date} {source} using a UnionFS for static directory comprised of: +INFO: {date} {source} Base: /tmp/mySite/themes/zafta/static +INFO: {date} {source} Overlay: /tmp/mySite/static/ +INFO: {date} {source} syncing static files to /tmp/mySite/public/ +Started building site +INFO: {date} {source} found taxonomies: map[string]string{"tag":"tags", "category":"categories"} +WARN: {date} {source} "404.html" is rendered empty +0 draft content +0 future content +0 expired content +3 pages created +0 non-page files copied +0 paginator pages created +0 tags created +0 categories created +in 9 ms + +$ cat public/index.html + + + +

      Articles

      +

      Second

      +

      First

      +

      Pages

      +

      About

      + + +``` +Good! This time, your home page has two Sections: "article" and "page", and +each Section contains the correct set of headings and links. +## Template Sharing + +If you've been following along on your computer, you might've noticed that +your home page doesn't show its title in your browser, although both of your +article pages do. That's because we didn't add your home page's title to its +template (`layouts/index.html`). That would be easy to do — but instead, +let's look at a better option. + +We can put the common information into a shared template. +These reside in the `layouts/partials/` directory. +### Header & Footer + +In Hugo (as elsewhere), a Partial is a template that's intended to be used +within other templates. We're going to create a Partial template that will +contain a header, for all of your page templates to use. That Partial will +enable us to maintain the header information in a single place, thus easing +our maintenance. Let's create both the header (`layouts/partials/header.html`) +and the footer (`layouts/partials/footer.html`): +```html +$ vi themes/zafta/layouts/partials/header.html + + + + {{ .Title }} + + +:wq + +$ vi themes/zafta/layouts/partials/footer.html +

      Home

      + + +:wq +``` +### Calling + +Any `partial` is called relative to its conventional location +`layouts/partials/`. So, you pass just the basename, followed by the context +(the period before the closing mustache). For example: +```bash +{{ partial "header.html" . }} +``` +#### From Home + +Let's change your home page template (`layouts/index.html`) +in order to use the new header Partial we just created: +```html +$ vi themes/zafta/layouts/index.html +{{ partial "header.html" . }} +

      Articles

      + {{- range first 10 .Data.Pages -}} + {{- if eq .Type "article"}} +

      {{ .Title }}

      + {{- end -}} + {{- end }} +

      Pages

      + {{- range first 10 .Data.Pages -}} + {{- if eq .Type "page" }} +

      {{ .Title }}

      + {{- end -}} + {{- end }} + + +:wq +``` +Render your web site and verify the results. Now, the title on your home page +should be "My New Hugo Site". This comes from the "title" variable +in the `./config.toml` file. +#### From Default + +Let's also edit the default templates (`layouts/_default/single.html` and +`layouts/_default/list.html`) to use your new Partials: +```html +$ vi themes/zafta/layouts/_default/single.html +{{ partial "header.html" . }} +

      {{ .Title }}

      +
      {{ .Date.Format "Mon, Jan 2, 2006" }}
      + {{ .Content }} +{{ partial "footer.html" . -}} +:wq + +$ vi themes/zafta/layouts/_default/list.html +{{ partial "header.html" . -}} +

      Articles

      + {{- range first 10 .Data.Pages }} +

      {{ .Title }}

      + {{- end }} +{{ partial "footer.html" . -}} +:wq +``` +Render your web site and verify the results. +Now, the title of your About page should reflect the value of the "title" +variable in its corresponding Markdown file (`./content/About.md`). +The same should be true for each of your article pages as well (i.e., +`./content/article/First.md` and `./content/article/Second.md`). +### DRY + +Don't Repeat Yourself (also known as DRY) is a desirable goal, +in any kind of source code development — +and Hugo's partials do a fine job to help with that. + +Part of the art of good templates is knowing when to add new ones, and when +to edit existing ones. While you're still figuring out the art of templates, +you should accept that you'll do some refactoring — Hugo makes this +easy and fast. And it's okay to delay splitting your templates into Partials. +## Section +### Date + +Articles commonly display the date they were published +(or finalized) — so, here, let's do the same. + +The front-matter of your articles contains a "date" variable +(as discussed above). Hugo sets this, when it creates each content file. +Now, sometimes an article requires many days to prepare, so its actual +publishing date might be later than the front-matter's "date". However, for +simplicity's sake, let's pretend this is the date we want to display, each time. + +In Hugo, in order to format a variable date (or time), +we must do it by formatting the Go language [reference +time](https://golang.org/pkg/time/); for example: +```bash +{{ .Date.Format "Mon, Jan 2, 2006" }} +``` +Now, your articles use the `layouts/_default/single.html` template (see above). +Because that template includes a date-formatting snippet, they show a +nice looking date. However, your About page uses the same default template. +Unfortunately, now it too shows its creation date (which makes no sense)! + +There are a couple of ways to make the date display only for articles. +We could use an "if" statement, to display the date only when the Type equals +"article." That is workable, and acceptable for web sites with only a couple +of content Types. It aligns with the principle of "code for today," too. +### Template + +Let's assume, though (for didactic purposes), that you've made your web site so +complex that you feel you must create a new template Type. In Hugo-speak, this +will be a new Section. It will contain your new, "article" Single template. + +Let's restore your default Single template (`layouts/_default/single.html`) +to its earlier state (before we forget): +```html +$ vi themes/zafta/layouts/_default/single.html +{{ partial "header.html" . }} +

      {{ .Title }}

      + {{ .Content }} +{{ partial "footer.html" . -}} +:wq +``` +Now, let's create your new template. If you remember Hugo's rules, +the template engine will prefer this version over the default. The first step +is to create (within your theme) its Section's directory: `layouts/article/`. +Then, create a Single template (`layouts/article/single.html`) within it: +```html +$ mkdir themes/zafta/layouts/article + +$ vi themes/zafta/layouts/article/single.html +{{ partial "header.html" . }} +

      {{ .Title }}

      +
      {{ .Date.Format "Mon, Jan 2, 2006" }}
      + {{ .Content }} +{{ partial "footer.html" . -}} +:wq +``` +Basically, we moved the date logic — from the default template, to the +new "article" Section, Single template: `layouts/article/single.html`. + +Let's render your web site and verify the results: +```html +$ rm -rf public/ + +$ hugo --verbose +INFO: {date} {source} Using config file: /tmp/mySite/config.toml +INFO: {date} {source} using a UnionFS for static directory comprised of: +INFO: {date} {source} Base: /tmp/mySite/themes/zafta/static +INFO: {date} {source} Overlay: /tmp/mySite/static/ +INFO: {date} {source} syncing static files to /tmp/mySite/public/ +Started building site +INFO: {date} {source} found taxonomies: map[string]string{"tag":"tags", "category":"categories"} +WARN: {date} {source} "404.html" is rendered empty +0 draft content +0 future content +0 expired content +3 pages created +0 non-page files copied +0 paginator pages created +0 tags created +0 categories created +in 10 ms + +$ cat public/article/First/index.html + + + + First + + + +

      First

      +
      Wed, Jan 18, 2040
      +

      In vel ligula tortor. Aliquam erat volutpat. +Pellentesque at felis eu quam tincidunt dignissim. +Nulla facilisi.

      + +

      Pellentesque tempus nisi et interdum convallis. +In quam ante, vulputate at massa et, rutrum +gravida dui. Phasellus tristique libero at ex.

      + +

      Home

      + + + +$ cat public/About/index.html + + + + About + + + +

      About

      +

      Neque porro quisquam est qui dolorem +ipsum quia dolor sit amet consectetur +adipisci velit.

      + +

      Home

      + + +``` +Now, as you can see, your articles show their dates, +and your About page (sensibly) doesn't. diff --git a/docs/content/tutorials/deployment-with-rsync.md b/docs/content/tutorials/deployment-with-rsync.md new file mode 100644 index 000000000..7bc516650 --- /dev/null +++ b/docs/content/tutorials/deployment-with-rsync.md @@ -0,0 +1,125 @@ +--- +authors: +- Adrien Poupin +date: 2016-11-01 +linktitle: Deployment with rsync +toc: true +menu: + main: + parent: tutorials +next: /tutorials/creating-a-new-theme +prev: /tutorials/automated-deployments +title: Easy deployments with rsync +weight: 11 + +--- + +# How to build and deploy with hugo and rsync +We assume here that you have an access to your web host with SSH. In that case, as you will see, deployment is very simple. We also assume that you have a functional static site with hugo installed. + +The spoil is, you can deploy your entire site with a command that looks like this: + +```bash +hugo && rsync -avz --delete public/ www-data@ftp.topologix.fr:~/www/ +``` + +As you will see, we put it in a shell script file, which makes building and deployment as easy as executing `./deploy`. + +## Installing SSH Key + +If it is not done yet, we will make an automated way to SSH to your server. If you have already installed an SSH key, switch to the next section. + +First, install the ssh client. On Debian/Ubuntu/derivates, enter `sudo apt-get install openssh-client`. + +Then generate your ssh key by entering the following commands: +``` +~$ cd && mkdir .ssh & cd .ssh +~/.ssh/$ ssh-keygen -t rsa -q -C "For SSH" -f rsa_id +~/.ssh/$ cat >> config < "GitHub Pages" -> "Source" -> Select "master branch /docs folder"). +If that option isn't enabled, you likely haven't pushed your _docs_ folder yet. + +This is the simplest approach but requires the usage of a non-standard publish directory +(GitHub Pages cannot be configured to use another directory than _docs_ currently). +Also the presence of generated files on the master branch may not be to everyone's taste. + +## Deployment via gh-pages branch + +Alternatively, you can deploy site through a separate branch called "gh_pages". +That approach is a bit more complex but has some advantages: + +* It keeps sources and generated HTML in two different branches +* It uses the default _public_ folder +* It keeps the histories of source branch and gh-pages branch fully separated from each other + +### Preparations + +These steps only need to be done once (replace "upstream" with the name of your remote, e.g. "origin"): +First, add the _public_ folder to _.gitignore_ so it's ignored on the master branch: + + echo "public" >> .gitignore + +Then initialize the gh-pages branch as an empty [orphan branch](https://git-scm.com/docs/git-checkout/#git-checkout---orphanltnewbranchgt): + + git checkout --orphan gh-pages + git reset --hard + git commit --allow-empty -m "Initializing gh-pages branch" + git push upstream gh-pages + git checkout master + +### Building and Deployment + +Now check out the gh-pages branch into your _public_ folder, using git's [worktree feature](https://git-scm.com/docs/git-worktree) +(essentially, it allows you to have multiple branches of the same local repo to be checked out in different directories): + + rm -rf public + git worktree add -B gh-pages public upstream/gh-pages + +Regenerate the site using Hugo and commit the generated files on the gh-pages branch: + + hugo + cd public && git add --all && git commit -m "Publishing to gh-pages" && cd .. + +If the changes in your local gh-pages branch look alright, push them to the remote repo: + + git push upstream gh-pages + +After a short while you'll see the updated contents on your GitHub Pages site. + +### Putting it into a script + +To automate these steps, you can create a script _scripts/publish_to_ghpages.sh_ with the following contents: + +``` +#!/bin/sh + +DIR=$(dirname "$0") + +cd $DIR/.. + +if [[ $(git status -s) ]] +then + echo "The working directory is dirty. Please commit any pending changes." + exit 1; +fi + +echo "Deleting old publication" +rm -rf public +mkdir public +git worktree prune +rm -rf .git/worktrees/public/ + +echo "Checking out gh-pages branch into public" +git worktree add -B gh-pages public upstream/gh-pages + +echo "Removing existing files" +rm -rf public/* + +echo "Generating site" +hugo + +echo "Updating gh-pages branch" +cd public && git add --all && git commit -m "Publishing to gh-pages (publish.sh)" +``` + +This will abort if there are pending changes in the working directory and also makes sure that all previously existing output files are removed. +Adjust the script to taste, e.g. to include the final push to the remote repository if you don't need to take a look at the gh-pages branch before pushing. Or adding `echo yourdomainname.com >> CNAME` if you set up for your gh-pages to use customize domain. + +## Deployment with Git 2.4 and earlier + +The `worktree` command was only introduced in Git 2.5. +If you are still on an earlier version and cannot update, you can simply clone your local repo into the _public_ directory, only keeping the gh-pages branch: + + git clone .git --branch gh-pages public + +Having re-generated the site, you'd push back the gh-pages branch to your primary local repo: + + cd public && git add --all && git commit -m "Publishing to gh-pages" && git push origin gh-pages + +The other steps are the same as with the worktree approach. + +## Hosting Personal/Organization Pages + +As mentioned [in this GitHub's article](https://help.github.com/articles/user-organization-and-project-pages/), besides project pages, you may also want to host a user/organization page. Here are the key differences: + +> - You must use the `username.github.io` naming scheme. +> - Content from the `master` branch will be used to build and publish your GitHub Pages site. + +It becomes much simpler in that case: we'll create two separate repos, one for Hugo's content, and a git submodule with the `public` folder's content in it. + +Step by step: + +1. Create on GitHub `-hugo` repository (it will host Hugo's content) +2. Create on GitHub `.github.io` repository (it will host the `public` folder: the static website) +3. `git clone <-hugo-url> && cd -hugo` +4. Make your website work locally (`hugo server -t `) +5. Once you are happy with the results, Ctrl+C (kill server) and `rm -rf public` (don't worry, it can always be regenerated with `hugo -t `) +6. `git submodule add -b master git@github.com:/.github.io.git public` +7. Almost done: add a `deploy.sh` script to help you (and make it executable: `chmod +x deploy.sh`): + +``` +#!/bin/bash + +echo -e "\033[0;32mDeploying updates to GitHub...\033[0m" + +# Build the project. +hugo # if using a theme, replace by `hugo -t ` + +# Go To Public folder +cd public +# Add changes to git. +git add -A + +# Commit changes. +msg="rebuilding site `date`" +if [ $# -eq 1 ] + then msg="$1" +fi +git commit -m "$msg" + +# Push source and build repos. +git push origin master + +# Come Back +cd .. +``` +7. `./deploy.sh "Your optional commit message"` to send changes to `.github.io` (careful, you may also want to commit changes on the `-hugo` repo). + +That's it! Your personal page is running at [http://username.github.io/](http://username.github.io/) (after up to 10 minutes delay). + +## Using a custom domain + +If you'd like to use a custom domain for your GitHub Pages site, create a file _static/CNAME_ with the domain name as its sole contents. +This will put the CNAME file to the root of the published site as required by GitHub Pages. + +Refer to the [official documentation](https://help.github.com/articles/using-a-custom-domain-with-github-pages/) for further information. + +## Conclusion + +Hopefully this tutorial helped you to get your website off its feet and out into the open! If you have any further questions, feel free to contact the community through the [discussion forum](/community/mailing-list/). diff --git a/docs/content/tutorials/hosting-on-bitbucket.md b/docs/content/tutorials/hosting-on-bitbucket.md new file mode 100644 index 000000000..027618fa5 --- /dev/null +++ b/docs/content/tutorials/hosting-on-bitbucket.md @@ -0,0 +1,138 @@ +--- +authors: +- Jason Gowans +lastmod: 2017-02-04 +date: 2017-02-04 +linktitle: Hosting on Bitbucket +toc: true +menu: + main: + parent: tutorials +next: /tutorials/github-pages-blog +prev: /tutorials/creating-a-new-theme +title: Continuous deployment with Bitbucket & Aerobatic +weight: 10 +--- + +# Continuous deployment with Bitbucket & Aerobatic + +## Introduction + +In this tutorial, we will use [Bitbucket](https://bitbucket.org/) and [Aerobatic](https://www.aerobatic.com) to build, deploy, and host a Hugo site. Aerobatic is a static hosting service that integrates with Bitbucket and provides a free hosting tier. + +It is assumed that you know how to use git for version control and have a Bitbucket account. + +## Install Aerobatic CLI + +If you haven't previously used Aerobatic, you'll first need to install the Command Line Interface (CLI) and create an account. For a list of all commands available, see the [Aerobatic CLI](https://www.aerobatic.com/docs/cli/) docs. + +```bash +npm install aerobatic-cli -g +aero register +``` + +## Create and Deploy Site + +```bash +hugo new site my-new-hugo-site +cd my-new-hugo-site +cd themes; git clone https://github.com/eliasson/liquorice +hugo -t liquorice +aero create # create the Aerobatic site +hugo --baseURL https://my-new-hugo-site.aerobatic.io # build the site overriding baseURL +aero deploy -d public # deploy output to Aerobatic + +Version v1 deployment complete. +View now at https://hugo-docs-test.aerobatic.io +``` + +In the rendered page response, the `https://__baseurl__` will be replaced with your actual site url (in this example, `https://my-new-hugo-site.aerobatic.io`). You can always rename your Aerobatic website with the `aero rename` command. + +## Push Hugo site to Bitbucket + +We will now create a git repository and then push our code to Bitbucket. In Bitbucket, create a repository. + +![][1] + +[1]: /img/tutorials/hosting-on-bitbucket/bitbucket-create-repo.png + + +```bash +# initialize new git repository +git init + +# set up our .gitignore file +echo -e "/public \n/themes \naero-deploy.tar.gz" >> .gitignore + +# commit and push code to master branch +git add --all +git commit -m "Initial commit" +git remote add origin git@bitbucket.org:YourUsername/my-new-hugo-site.git +git push -u origin master +``` + +## Continuous Deployment With Bitbucket Pipelines +In the example above, we pushed the compiled assets in the `/public` folder to Aerobatic. In the following example, we use Bitbucket Pipelines to continuously create and deploy the compiled assets to Aerobatic. + +### Step 1: Configure Bitbucket Pipelines + +In your Hugo website's Bitbucket repo; + +1. Click the Pipelines link in the left nav menu of your Bitbucket repository. +2. Click the Enable Pipelines button. +3. On the next screen, leave the default template and click Next. +4. In the editor, paste in the yaml contents below and click Commit. + +```bash +image: beevelop/nodejs-python +pipelines: + branches: + master: + - step: + script: + - apt-get update -y && apt-get install wget + - apt-get -y install git + - wget https://github.com/gohugoio/hugo/releases/download/v0.18/hugo_0.18-64bit.deb + - dpkg -i hugo*.deb + - git clone https://github.com/eliasson/liquorice themes/liquorice + - hugo --theme=liquorice --baseURL https://__baseurl__ --buildDrafts + - npm install -g aerobatic-cli + - aero deploy +``` + +### Step 2: Create `AEROBATIC_API_KEY` environment variable. + +This step only needs to be done once per account. From the command line; + +```bash +aero apikey +``` + +1. Navigate to the Bitbucket account settings for the account that the website repo belongs to. +2. Scroll down to the bottom of the left nav and click the Environment variables link in the PIPELINES section. +3. Create a new environment variable called AEROBATIC_API_KEY with the value you got by running the `aero apikey` command. Be sure to click the Secured checkbox. + +## Step 3: Edit and Commit Code + +```bash +hugo new post/good-to-great.md +hugo server --buildDrafts -t liquorice #Check that all looks good + +# commit and push code to master branch +git add --all +git commit -m "New blog post" +git push -u origin master +``` + +Your code will be committed to Bitbucket, Bitbucket Pipelines will run your build, and a new version of your site will be deployed to Aerobatic. + +At this point, you can now create and edit blog posts directly in the Bitbucket UI. + +![][2] + +[2]: /img/tutorials/hosting-on-bitbucket/bitbucket-blog-post.png + + +## Suggested next steps + +The code for this example can be found in this Bitbucket [repository](https://bitbucket.org/dundonian/hugo-docs-test). Aerobatic also provides a number of additional [plugins](https://www.aerobatic.com/docs) such as auth and redirects that you can use for your Hugo site. diff --git a/docs/content/tutorials/hosting-on-gitlab.md b/docs/content/tutorials/hosting-on-gitlab.md new file mode 100644 index 000000000..0c213d1ee --- /dev/null +++ b/docs/content/tutorials/hosting-on-gitlab.md @@ -0,0 +1,68 @@ +--- +author: Riku-Pekka Silvola +lastmod: 2016-06-23 +date: 2016-06-23 +linktitle: Hosting on GitLab +toc: true +menu: + main: + parent: tutorials +next: /tutorials/how-to-contribute-to-hugo/ +prev: /tutorials/github-pages-blog +title: Hosting on GitLab Pages +weight: 10 +--- +# Continuous deployment with GitLab + +## Introduction + +In this tutorial, we will use [GitLab](https://gitlab.com/) to build, deploy, and host a [Hugo](https://gohugo.io/) site. With Hugo and GitLab, this is incredibly easy. + +It is assumed that you know how to use git for version control and have a GitLab account, and that you have gone through the [quickstart guide]({{< relref "overview/quickstart.md" >}}) and already have a Hugo site on your local machine. + + +## Create .gitlab-ci.yml + +```bash +cd your-hugo-site +``` + +In the root directory of your Hugo site, create a `.gitlab-ci.yml` file. The `.gitlab-ci.yml` configures the GitLab CI on how to build your page. Simply add the content below. + +```yml +image: publysher/hugo + +pages: + script: + - hugo + artifacts: + paths: + - public + only: + - master +``` + +## Push Hugo site to GitLab +Next up, create a new repository on GitLab. It is *not* necessary to set the repository public. In addition, you might want to add `/public` to your .gitignore file, as there is no need to push compiled assets to GitLab. + +```bash +# initialize new git repository +git init + +# add /public directory to our .gitignore file +echo "/public" >> .gitignore + +# commit and push code to master branch +git add . +git commit -m "Initial commit" +git remote add origin https://gitlab.com/YourUsername/your-hugo-site.git +git push -u origin master +``` + +## Wait for your page to be built +That's it! You can now follow the CI agent building your page at https://gitlab.com/YourUsername/your-hugo-site/pipelines. +After the build has passed, your new website is available at https://YourUsername.gitlab.io/your-hugo-site/ + +## Suggested next steps + +GitLab supports using custom CNAME's and TLS certificates, but this is out of the scope of this tutorial. For more details on GitLab Pages, see [https://about.gitlab.com/2016/04/07/gitlab-pages-setup/](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/) diff --git a/docs/content/tutorials/how-to-contribute-to-hugo.md b/docs/content/tutorials/how-to-contribute-to-hugo.md new file mode 100644 index 000000000..5f3795f20 --- /dev/null +++ b/docs/content/tutorials/how-to-contribute-to-hugo.md @@ -0,0 +1,352 @@ +--- +date: 2016-04-03T13:21:56+02:00 +linktitle: How to contribute +menu: + main: + parent: tutorials +next: /tutorials/installing-on-mac/ +prev: /tutorials/github-pages-blog/ +title: How to contribute to Hugo +weight: 10 +--- + +## Introduction + +Hugo is an open source project and lives by the work of its [contributors](https://github.com/gohugoio/hugo/graphs/contributors). Help to make Hugo even more awesome. There are plenty of [open issues](https://github.com/gohugoio/hugo/issues) on GitHub and we need your help. + +This tutorial is intended for people who are new to Git, GitHub or open source projects in general. It should help to overcome most of the barriers that newcomers encounter. It describes step by step what you need to do. + +For any kind of questions please take a look at our [forum](https://discourse.gohugo.io/). + +## Install Go + +The installation of Go should take only a few minutes. [Download](https://golang.org/dl/) the latest stable version of Go and follow the official [installation guide](https://golang.org/doc/install). + +Let's confirm the correct installation of Go. Open a terminal (or command line under Windows). Execute `go version` and you should see the version number of your Go installation. Next, make sure that you setup the `GOPATH` as described in the installation guide. + +You can print the `GOPATH` with `echo $GOPATH`. You should see a non-empty string containing a valid path to your Go workspace. + +### GVM as alternative + +More experienced users can use the [Go Version Manager](https://github.com/moovweb/gvm), or GVM for short. It allows you to switch between different Go versions *on the same machine*. Probably you don't need this feature. But you can easily upgrade to a new released Go version with a few commands. + +This is handy if you follow the developement of Hugo over a longer period of time. Future versions of Hugo will usually be compiled with the latest version of Go. Sooner or later you have to upgrade if you want to keep up. + + +## Create an account on GitHub + +If you're going to contribute code, you'll need to have an account on GitHub. Go to [www.github.com/join](https://github.com/join) and set up a personal account. + + +## Install Git on your system + +You will need to install Git. This tutorial assumes basic knowledge about Git. Refer to this excellent [Git book](https://git-scm.com/) if you are not sure where to begin. The used terminology will be explained with annotations. + +Git is a [version control system](https://en.wikipedia.org/wiki/Version_control) to track the changes of source code. Hugo depends on smaller third-party packages that are used to extend the functionality. We use them because we don't want to reinvent the wheel. + +Go ships with a sub-command called `get` that will download these packages for us when we setup our working environment. The source code of the packages is tracked with Git. `get` will interact with the Git servers of the package hosters in order to fetch all dependencies. + +Move back to the terminal and check if Git is already installed. Type in `git version` and press enter. You can skip the rest of this section if the command returned a version number. Otherwise [download](https://git-scm.com/downloads) the lastest version of Git and follow this [installation guide](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git). + +Finally, check again with `git version` if Git was installed successfully. + +### Git Graphical Front Ends + +There are several [GUI clients](https://git-scm.com/downloads/guis) that help you to operate Git. Not all are available for all operating systems and maybe differ in their usage. Thus, so we will use the command line since the commands are everywhere the same. + +### Install Hub on your system (optional) + +Hub is a great tool for working with GitHub. The main site for it is [www.hub.github.com](https://hub.github.com/). Feel free to install this little Git wrapper. + +On a Mac, install Hub using brew: + +```sh +brew install hub +``` + +Create an alias (in Bash) so that typing git actually runs Hub: + +```sh +echo "alias git='hub'" >> ~/.bash_profile +``` + +Confirm the installation: + +```sh +git version 2.6.3 +hub version 2.2.2 +``` + + +## Set up your working copy + +The working copy is set up locally on your computer. It's what you'll edit, compile, and end up pushing back to GitHub. The main steps are cloning the repository and creating your fork as a remote. + +### Vendored Dependencies + +Hugo uses [govendor](https://github.com/kardianos/govendor) to vendor dependencies, but we don't commit the vendored packages themselves to the Hugo git repository. +Therefore, a simple `go get` is not supported since `go get` is not vendor-aware. +You **must use govendor** to fetch and manage Hugo's dependencies. + +```sh +go get -v -u github.com/kardianos/govendor +``` + +### Fetch the Sources from GitHub + +We assume that you've set up your `GOPATH` (see the section above if you're unsure about this). You should now copy the Hugo repository down to your computer. You'll hear this called "clone the repo". GitHub's [help pages](https://help.github.com/articles/cloning-a-repository/) give us a short explanation: + +> When you create a repository on GitHub, it exists as a remote repository. You can create a local clone of your repository on your computer and sync between the two locations. + +We're going to clone the [master Hugo repository](https://github.com/gohugoio/hugo). That seems counter-intuitive, since you won't have commit rights on it. But it's required for the Go workflow. You'll work on a copy of the master and push your changes to your own repository on GitHub. + +So, let's clone that master repository with govendor: + +```sh +govendor get -v github.com/gohugoio/hugo +``` + +### Fork the repository + +If you're not familiar with this term, GitHub's [help pages](https://help.github.com/articles/fork-a-repo/) provide again a simple explanation: + +> A fork is a copy of a repository. Forking a repository allows you to freely experiment with changes without affecting the original project. + +#### Fork by hand + +Open the [Hugo repository](https://github.com/gohugoio/hugo) on GitHub and click on the "Fork" button in the top right. + +![Fork button](/img/tutorials/how-to-contribute-to-hugo/forking-a-repository.png) + +Now open your fork repository on GitHub and copy the remote url of your fork. You can choose between HTTPS and SSH as protocol that Git should use for the following operations. HTTPS works always [if you're not sure](https://help.github.com/articles/which-remote-url-should-i-use/). + +![Copy remote url](/img/tutorials/how-to-contribute-to-hugo/copy-remote-url.png) + +Switch back to the terminal and move into the directory of the cloned master repository from the last step. + +```sh +cd $GOPATH/src/github.com/gohugoio/hugo +``` + +Now Git needs to know that our fork exists by adding the copied remote url: + +```sh +git remote add +``` + +#### Fork with Hub + +Alternatively, you can use the Git wrapper Hub. Hub makes forking a repository easy: + +```sh +git fork +``` + +That command will log in to GitHub using your account, create a fork of the repository that you're currently working in, and add it as a remote to your working copy. + +#### Trust, but verify + +Let's check if everything went right by listing all known remotes: + +```sh +git remote -v +``` + +The output should look similar: + +```sh +digitalcraftsman git@github.com:digitalcraftsman/hugo.git (fetch) +digitalcraftsman git@github.com:digitalcraftsman/hugo.git (push) +origin https://github.com/gohugoio/hugo (fetch) +origin https://github.com/gohugoio/hugo (push) +``` + + +## The contribution workflow + +### Create a new branch + +You should never develop against the "master" branch. The development team will not accept a pull request against that branch. Instead, create a descriptive named branch and work on it. + +First, you should always pull the latest changes from the master repository: + +```sh +git checkout master +git pull +``` + +Now we can create a new branch for your additions: + +```sh +git checkout -b +``` + +You can check on which branch your are with `git branch`. You should see a list of all local branches. The current branch is indicated with a little asterisk. + +### Contributing to the documentation + +Perhaps you want to start contributing to the docs. Then you can ignore most of the following steps. You can find the documentation within the cloned repository in the subfolder `docs`. Change the directory with `cd docs`. Install the [latest release]({{< relref "overview/installing.md" >}}). Or read on and build Hugo from source. + +You can start Hugo's built-in server via `hugo server`. Browse the documentation by entering [http://localhost:1313](http://localhost:1313) in the address bar of your browser. The server automatically updates the page if you change its content. + +### Building Hugo + +While making changes in the codebase it's a good idea to build the binary to test them: + +```sh +make hugo +``` + +### Testing + +Sometimes changes on the codebase can cause unintended side effects. Or they don't work as expected. Most functions have their own test cases. You can find them in files ending with `_test.go`. + +```sh +make check +``` + +### Formatting + +The Go code styleguide maybe is opinionated but it ensures that the codebase looks the same, regardless who wrote the code. Go comes with its own formatting tool. Let's apply the styleguide to our additions: + +```sh +govendor fmt +local +``` + +Once you made your additions commit your changes. Make sure that you follow our [code contribution guidelines](https://github.com/gohugoio/hugo/blob/master/CONTRIBUTING.md): + +```sh +# Add all changed files +git add --all +git commit --message "YOUR COMMIT MESSAGE" +``` + +The commit message should describe what the commit does (e.g. add feature XYZ), not how it is done. + +### Modify commits + +You noticed some commit messages don't fulfill the code contribution guidelines or you just forget something to add some files? No problem. Git provides the necessary tools to fix such problems. The next two methods cover all common cases. + +If you are unsure what a command does leave the commit as it is. We can fix your commits later in the pull request. + +#### Modifying the last commit + +Let's say you want to modify the last commit message. Run the following command and replace the current message: + +```sh +git commit --amend -m"YOUR NEW COMMIT MESSAGE" +``` + +Take a look at the commit log to see the change: + +```sh +git log +# Exit with q +``` + +After making the last commit you may forgot something. There is no need to create a new commit. Just add the latest changes and merge them into the intended commit: + +```sh +git add --all +git commit --amend +``` + +#### Modifying multiple commits + +This is a bit more advanced. Git allows you to [rebase](https://git-scm.com/docs/git-rebase) commits interactively. In other words: it allows you to rewrite the commit history. **Take care of your actions. They can cause unintended changes. Skip this section if you're not sure!** + +```sh +git rebase --interactive @~6 +``` + +The `6` at the end of the command represents the number of commits that should be modified. An editor should open and present a list of last six commit messages: + +```sh +pick 80d02a1 tpl: Add hasPrefix to the template funcs' "smoke test" +pick aaee038 tpl: Sort the smoke tests +pick f0dbf2c tpl: Add the other test case for hasPrefix +pick 911c35b Add "How to contribute to Hugo" tutorial +pick 33c8973 Begin workflow +pick 3502f2e Refactoring and typo fixes +``` + +In the case above we should merge the last to commits in the commit of this tutorial (`Add "How to contribute to Hugo" tutorial`). You can "squash" commits, i.e. merge two or more commits into a single one. + +All operations are written before the commit message. Replace "pick" with an operation. In this case `squash` or `s` for short: + + +```sh +pick 80d02a1 tpl: Add hasPrefix to the template funcs' "smoke test" +pick aaee038 tpl: Sort the smoke tests +pick f0dbf2c tpl: Add the other test case for hasPrefix +pick 911c35b Add "How to contribute to Hugo" tutorial +squash 33c8973 Begin workflow +squash 3502f2e Refactoring and typo fixes +``` + +We also want to rewrite the commits message of the third last commit. We forgot "docs:" as prefix according to the code contribution guidelines. The operation to rewrite a commit is called `reword` (or `r` as shortcut). + +You should end up with a similar setup: + +```sh +pick 80d02a1 tpl: Add hasPrefix to the template funcs' "smoke test" +pick aaee038 tpl: Sort the smoke tests +pick f0dbf2c tpl: Add the other test case for hasPrefix +reword 911c35b Add "How to contribute to Hugo" tutorial +squash 33c8973 Begin workflow +squash 3502f2e Refactoring and typo fixes +``` + +Close the editor. It should open again with a new tab. A text is instructing you to define a new commit message for the last two commits that should be merged (a.k.a. squashed). Save the file (CTRL+S) and close the editor again. + +A last time a new tab opens. Enter a new commit message and save again. Your terminal should contain a status message. Hopefully this one: + +```sh +Successfully rebased and updated refs/heads/. +``` + +Check the commit log if everything looks as expected. Should an error occur you can abort this rebase with `git rebase --abort`. + +### Push commits + +To push our commits to the fork on GitHub we need to specify a destination. A destination is defined by the remote and a branch name. Earlier, the defined that the remote url of our fork is the same as our GitHub handle, in my case `digitalcraftsman`. The branch should have the same as our local one. This makes it easy to identify corresponding branches. + +```sh +git push --set-upstream +``` + +Now Git knows the destination. Next time when you to push commits you just need to enter `git push`. + +If you modified your commit history in the last step GitHub will reject your try to push. This is a safety-feature because the commit history isn't the same and new commits can't be appended as usual. You can enforce this push explicitly with `git push --force`. + + +## Open a pull request + +We made a lot of progress. Good work. In this step we finally open a pull request to submit our additions. Open the [Hugo master repository](https://github.com/gohugoio/hugo/) on GitHub in your browser. + +You should find a green button labeled with "New pull request". But GitHub is clever and probably suggests you a pull request like in the beige box below: + +Open a pull request + +The new page summaries the most important information of your pull request. Scroll down and you find the additions of all your commits. Make sure everything looks as expected and click on "Create pull request". + +### Accept the contributor license agreement + +Last but not least you should accept the contributor license agreement (CLA). A new comment should be added automatically to your pull request. Click on the yellow badge, accept the agreement and authenticate yourself with your GitHub account. It just takes a few clicks and only needs to be done once. + +Accept the CLA + + +### Automatic builds + +We use the [Travis CI loop](https://travis-ci.org/gohugoio/hugo) (Linux and OS X) and [AppVeyor](https://ci.appveyor.com/project/gohugoio/hugo/branch/master) (Windows) to compile Hugo with your additions. This should ensure that everything works as expected before merging your pull request. This in most cases only relevant if you made changes to the codebase of Hugo. + +Automic builds and their status + +Above you can see that Travis wasn't able to compile the changes in this pull request. Click on "Details" and try to investigate why the build failed. But it doesn't have to be your fault. Mostly, the `master` branch that we used as foundation for your pull request should build without problems. + +If you have questions leave a comment in the pull request. We are willing to assist you. + +## Where to start? + +Thank you for reading this tutorial. Hopefully, we see you again on GitHub. There are plenty of [open issues](https://github.com/gohugoio/hugo/issues) on GitHub. Feel free to open an issue if you think you found a bug or you have a new idea to improve Hugo. We are happy to hear from you. diff --git a/docs/content/tutorials/installing-on-mac.md b/docs/content/tutorials/installing-on-mac.md new file mode 100644 index 000000000..12bf01e2b --- /dev/null +++ b/docs/content/tutorials/installing-on-mac.md @@ -0,0 +1,240 @@ +--- +author: "Michael Henderson" +lastmod: 2016-08-10 +date: 2015-02-22 +linktitle: Installing on Mac +toc: true +menu: + main: + parent: tutorials +next: /tutorials/installing-on-windows +prev: /tutorials/how-to-contribute-to-hugo/ +title: Installing on a Mac +weight: 10 +--- + +# Installing Hugo on a Mac + +This tutorial aims to be a complete guide to installing Hugo on your Mac computer. + +## Assumptions + +1. You know how to open a terminal window. +2. You're running a modern 64-bit Mac. +3. You will use `~/Sites` as the starting point for your site. + +## Pick Your Method + +There are three ways to install Hugo on your Mac computer: the `brew` utility, from the distribution, or from source. +There's no "best" way to do this. You should use the method that works best for your use case. + +There are pros and cons for each. + +1. `Brew` is the simplest and least work to maintain. The drawbacks + aren't severe. The default package will be for the most recent + release, so it will not have bug-fixes until the next release + (unless you install it with the `--HEAD` option). The release to + `brew` may lag a few days behind because it has to be coordinated + with another team. Still, I'd recommend `brew` if you want to work + from a stable, widely used source. It works well and is really easy + to update. + +2. Downloading the tarball and installing from it is also easy. You have to have a few more command line skills. Updates are easy, too. You just repeat the process with the new binary. This gives you the flexibility to have multiple versions on your computer. If you don't want to use `brew`, then the binary is a good choice. + +3. Compiling from source is the most work. The advantage is that you don't have to wait for a release to add features or bug fixes. The disadvantage is that you need to spend more time managing the setup. It's not a lot, but it's more than with the other two options. + +Since this is a "beginner" how-to, I'm going to cover the first two +options in detail and go over the third more quickly. + +## Brew + +### Step 1: Install `brew` if you haven't already + +Go to the `brew` website, http://brew.sh/, and follow the directions there. The most important step is: + +``` +ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" +``` + +When I did this, I had some problems with directory permissions. Searches on Google pointed me to pages that walked me through updating permissions on the `/usr/local` directory. Seemed scary, but it's worked well since. + +### Step 2: Run the `brew` command to install `hugo` + +```bash +$ brew install hugo +==> Downloading https://homebrew.bintray.com/bottles/hugo-0.21.sierra.bottle.tar.gz +######################################################################## 100,0% +==> Pouring hugo-0.21.sierra.bottle.tar.gz +==> Using the sandbox +==> Caveats +Bash completion has been installed to: + /usr/local/etc/bash_completion.d +==> Summary +🍺 /usr/local/Cellar/hugo/0.21: 32 files, 17.4MB +``` + +(Note: Replace `brew install hugo` with `brew install hugo --HEAD` +if you want the absolute latest version in development, +but beware — there might be bugs!) + +`Brew` should have updated your path to include Hugo. Confirm by opening a new terminal window and running a few commands: + +```bash +$ # show the location of the hugo executable +$ which hugo +/usr/local/bin/hugo + +$ # show the installed version +$ ls -l $( which hugo ) +lrwxr-xr-x 1 mdhender admin 30 Mar 28 22:19 /usr/local/bin/hugo -> ../Cellar/hugo/0.13_1/bin/hugo + +$ # verify that hugo runs correctly +$ hugo version +Hugo Static Site Generator v0.13 BuildDate: 2015-03-09T21:34:47-05:00 +``` + +### Step 3: You're Done + +You've installed Hugo. Now you need to set up your site. Read the +[Quickstart guide](/overview/quickstart/), explore the rest of the +documentation, and if you still have questions +[just ask!](https://discourse.gohugo.io/ "Discussion forum") + +## From Tarball + +### Step 1: Decide on the location + +When installing from the tarball, you have to decide if you're going to install the binary in `/usr/local/bin` or in your home directory. There are three camps on this: + +1. Install it in `/usr/local/bin` so that all the users on your system have access to it. This is a good idea because it's a fairly standard place for executables. The downside is that you may need elevated privileges to put software into that location. Also, if there are multiple users on your system, they will all run the same version. Sometimes this can be an issue if you want to try out a new release. + +2. Install it in `~/bin` so that only you can execute it. This is a good idea because it's easy to do, easy to maintain, and doesn't require elevated privileges. The downside is that only you can run Hugo. If there are other users on your site, they have to maintain their own copies. That can lead to people running different versions. Of course, this does make it easier for you to experiment with different releases. + +3. Install it in your `sites` directory. This is not a bad idea if you have only one site that you're building. It keeps every thing in a single place. If you want to try out new releases, you can just make a copy of the entire site, update the Hugo executable, and have it. + +All three locations will work for you. I'm going to document the second option, mostly because I'm comfortable with it. + +### Step 2: Download the Tarball + +1. Open in your browser. + +2. Find the current release by scrolling down and looking for the green tag that reads "Latest Release." + +3. Download the current tarball for the Mac. The name will be something like `hugo_X.Y_osx-64bit.tgz`, where `X.YY` is the release number. + +4. By default, the tarball will be saved to your `~/Downloads` directory. If you chose to use a different location, you'll need to change that in the following steps. + +### Step 3: Confirm your download + +Verify that the tarball wasn't corrupted during the download: + +``` +$ tar tvf ~/Downloads/hugo_X.Y_osx-64bit.tgz +-rwxrwxrwx 0 0 0 0 Feb 22 04:02 hugo_X.Y_osx-64bit/hugo_X.Y_osx-64bit.tgz +-rwxrwxrwx 0 0 0 0 Feb 22 03:24 hugo_X.Y_osx-64bit/README.md +-rwxrwxrwx 0 0 0 0 Jan 30 18:48 hugo_X.Y_osx-64bit/LICENSE.md +``` + +The `.md` files are documentation. The other file is the executable. + +### Step 4: Install into your bin directory + +``` +$ # create the directory if needed +$ mkdir -p ~/bin + +$ # make it the working directory +$ cd ~/bin + +$ # extract the tarball +$ tar -xvzf ~/Downloads/hugo_X.Y_osx-64bit.tgz +Archive: hugo_X.Y_osx-64bit.tgz + x ./ + x ./hugo + x ./LICENSE.md + x ./README.md + +$ # verify that it runs +$ ./hugo version +Hugo Static Site Generator v0.13 BuildDate: 2015-02-22T04:02:30-06:00 +``` + +You may need to add your bin directory to your `PATH` variable. The `which` command will check for us. If it can find `hugo`, it will print the full path to it. Otherwise, it will not print anything. + +``` +$ # check if hugo is in the path +$ which hugo +/Users/USERNAME/bin/hugo +``` + +If `hugo` is not in your `PATH`, add it by updating your `~/.bash_profile` file. First, start up an editor: + +``` +$ nano ~/.bash_profile +``` + +Add a line to update your `PATH` variable: + +``` +export PATH=$PATH:$HOME/bin +``` + +Then save the file by pressing Control-X, then Y to save the file and return to the prompt. + +Close the terminal and then open a new terminal to pick up the changes to your profile. Verify by running the `which hugo` command again. + +### Step 5: You're Done + +You've installed Hugo. Now you need to set up your site. Read the +[Quickstart guide](/overview/quickstart/), explore the rest of the +documentation, and if you still have questions +[just ask!](https://discourse.gohugo.io/ "Discussion forum") + +## Building from Source + +If you want to compile Hugo yourself, you'll need +[Go](http://golang.org), which is also available from Homebrew: `brew +install go`. + +### Step 1: Get the Source + +If you want to compile a specific version, go to + and download the source code +for the version of your choice. If you want to compile Hugo with all +the latest changes (which might include bugs), clone the Hugo +repository: + +``` +git clone https://github.com/gohugoio/hugo +``` + +### Step 2: Compiling + +Make the directory containing the source your working directory, then +fetch Hugo's dependencies: + +``` +mkdir -p src/github.com/spf13 +ln -sf $(pwd) src/github.com/gohugoio/hugo + +# set the build path for Go +export GOPATH=$(pwd) + +go get +``` + +This will fetch the absolute latest version of the dependencies, so if +Hugo fails to build it may be because the author of a dependency +introduced a breaking change. + +Then compile: + +``` +go build -o hugo main.go +``` + +Then place the `hugo` executable somewhere in your `$PATH`. + +### Step 3: You're Done + +You probably know where to go from here. diff --git a/docs/content/tutorials/installing-on-windows.md b/docs/content/tutorials/installing-on-windows.md new file mode 100644 index 000000000..d249571f6 --- /dev/null +++ b/docs/content/tutorials/installing-on-windows.md @@ -0,0 +1,123 @@ +--- +author: "Michael Henderson" +lastmod: 2016-07-18 +date: 2015-03-30 +linktitle: Installing on Windows +toc: true +menu: + main: + parent: tutorials +next: /tutorials/mathjax +prev: /tutorials/installing-on-mac +title: Installing on Windows +weight: 10 +--- + +# Installing Hugo on Windows + +This tutorial aims to be a complete guide to installing Hugo on your Windows computer. + +## Assumptions + +1. We'll call your website `example.com` for the purpose of this tutorial. +2. You will use `C:\Hugo\Sites` as the starting point for your site. +3. You will use `C:\Hugo\bin` to store executable files. + +## Setup Your Directories + +You'll need a place to store the Hugo executable, your content (the files that you build), and the generated files (the HTML that Hugo builds for you). + +1. Open Windows Explorer. +2. Create a new folder: `C:\Hugo` (assuming you want Hugo on your C drive – it can go anywhere.) +3. Create a subfolder in the Hugo folder: `C:\Hugo\bin`. +4. Create another subfolder in Hugo: `C:\Hugo\Sites`. + +## Technical users + +1. Download the latest zipped Hugo executable from the [Hugo Releases](https://github.com/gohugoio/hugo/releases) page. +2. Extract all contents to your `..\Hugo\bin` folder. +3. The hugo executable will be named as `hugo_hugo-version_platform_arch.exe`. Rename that executable to `hugo.exe` for ease of use. +4. In PowerShell or your preferred CLI, add the `hugo.exe` executable to your PATH by navigating to `C:\Hugo\bin` (or the location of your hugo.exe file) and use the command `set PATH=%PATH%;C:\Hugo\bin`. If the `hugo` command does not work after a reboot, you may have to run the command prompt as administrator. + +## Less technical users + +1. Go the [Hugo Releases](https://github.com/gohugoio/hugo/releases) page. +2. The latest release is announced on top. Scroll to the bottom of the release announcement to see the downloads. They're all ZIP files. +3. Find the Windows files near the bottom (they're in alphabetical order, so Windows is last) – download either the 32-bit or 64-bit file depending on whether you have 32-bit or 64-bit Windows. (If you don't know, [see here](https://esupport.trendmicro.com/en-us/home/pages/technical-support/1038680.aspx).) +4. Move the ZIP file into your `C:\Hugo\bin` folder. +5. Double-click on the ZIP file and extract its contents. Be sure to extract the contents into the same `C:\Hugo\bin` folder – Windows will do this by default unless you tell it to extract somewhere else. +6. You should now have three new files: hugo executable (example: hugo_0.18_windows_amd64.exe), license.md, and readme.md. (you can delete the ZIP download now.). Rename that hugo executable (hugo_hugo-version_platform_arch.exe) to hugo.exe for ease of use. +7. Now add Hugo to your Windows PATH settings: + + ### For Windows 10 users: + + - Right click on the **Start** button. + - Click on **System**. + - Click on **Advanced System Settings** on the left. + - Click on the **Environment Variables...** button on the bottom. + - In the User variables section, find the row that starts with PATH (PATH will be all caps). + - Double-click on **PATH**. + - Click the **New...** button. + - Type in the folder where `hugo.exe` was extracted, which is `C:\Hugo\bin` if you went by the instructions above. *The PATH entry should be the folder where Hugo lives, not the binary.* Press Enter when you're done typing. + - Click OK at every window to exit. + + > Note that the path editor in Windows 10 was added in the large [November 2015 Update](https://blogs.windows.com/windowsexperience/2015/11/12/first-major-update-for-windows-10-available-today/). You'll need to have that or a later update installed for the above steps to work. You can see what Windows 10 build you have by clicking on the  Start button → Settings → System → About. See [here](http://www.howtogeek.com/236195/how-to-find-out-which-build-and-version-of-windows-10-you-have/) for more.) + + ### For Windows 7 and 8.x users: + + Windows 7 and 8.1 do not include the easy path editor included in Windows 10, so non-technical users on those platforms are advised to install a free third-party path editor like [Windows Environment Variables Editor](http://eveditor.com/) or [Path Editor](https://patheditor2.codeplex.com/). + +## Verify the executable + +Run a few commands to verify that the executable is ready to run, and then build a sample site to get started. + +1. Open a command prompt window. + +2. At the prompt, type `hugo help` and press the Enter key. You should see output that starts with: + + {{< nohighlight >}}hugo 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 http://gohugo.io/. +{{< /nohighlight >}} + + If you do, then the installation is complete. If you don't, double-check the path that you placed the `hugo.exe` file in and that you typed that path correctly when you added it to your PATH variable. If you're still not getting the output, post a note on the Hugo discussion list (in the `Support` topic) with your command and the output. + +3. At the prompt, change your directory to the `Sites` directory. + + {{< nohighlight >}}C:\Program Files> cd C:\Hugo\Sites +C:\Hugo\Sites> +{{< /nohighlight >}} + +4. Run the command to generate a new site. I'm using `example.com` as the name of the site. + + {{< nohighlight >}}C:\Hugo\Sites> hugo new site example.com +{{< /nohighlight >}} + +5. You should now have a directory at `C:\Hugo\Sites\example.com`. Change into that directory and list the contents. You should get output similar to the following: + + {{< nohighlight >}}C:\Hugo\Sites>cd example.com +C:\Hugo\Sites\example.com>dir + Directory of C:\hugo\sites\example.com +  +04/13/2015 10:44 PM <DIR> . +04/13/2015 10:44 PM <DIR> .. +04/13/2015 10:44 PM <DIR> archetypes +04/13/2015 10:44 PM 83 config.toml +04/13/2015 10:44 PM <DIR> content +04/13/2015 10:44 PM <DIR> data +04/13/2015 10:44 PM <DIR> layouts +04/13/2015 10:44 PM <DIR> static + 1 File(s) 83 bytes + 7 Dir(s) 6,273,331,200 bytes free +{{< /nohighlight >}} + +You now have Hugo installed and a site to work with. You need to add a layout (or theme), then create some content. Go to http://gohugo.io/overview/quickstart/ for steps on doing that. + +## Troubleshooting + +@dhersam has created a nice video on common issues: + +{{< youtube c8fJIRNChmU >}} diff --git a/docs/content/tutorials/mathjax.md b/docs/content/tutorials/mathjax.md new file mode 100644 index 000000000..e8d896354 --- /dev/null +++ b/docs/content/tutorials/mathjax.md @@ -0,0 +1,84 @@ +--- +author: Spencer Lyon +lastmod: 2015-05-22 +date: 2014-03-20 +menu: + main: + parent: tutorials +next: /tutorials/migrate-from-jekyll +prev: /tutorials/installing-on-windows +title: MathJax Support +toc: true +weight: 10 +--- + +## What is MathJax? + +[MathJax](http://www.mathjax.org/) is a JavaScript library that allows the display of mathematical expressions described via a LaTeX-style syntax in the HTML (or Markdown) source of a web page. As it is a pure a JavaScript library, getting it to work within Hugo is fairly straightforward, but does have some oddities that will be discussed here. + +This is not an introduction into actually using MathJax to render typeset mathematics on your website. Instead, this page is a collection of tips and hints for one way to get MathJax working on a website built with Hugo. + +## Enabling MathJax + +The first step is to enable MathJax on pages that you would like to have typeset math. There are multiple ways to do this (adventurous readers can consult the [Loading and Configuring](http://docs.mathjax.org/en/latest/configuration.html) section of the MathJax documentation for additional methods of including MathJax), but the easiest way is to use [the officially recommended secure CDN](https://cdnjs.com/) by including the following HTML snippet in the source of a page: + + + +One way to ensure that this code is included in all pages is to put it in one of the templates that live in the `layouts/partials/` directory. For example, I have included this in the bottom of my template `footer.html` because I know that the footer will be included in every page of my website. + +### Options and Features + +MathJax is a stable open-source library with many features. I encourage the interested reader to view the [MathJax Documentation](http://docs.mathjax.org/en/latest/index.html), specifically the sections on [Basic Usage](http://docs.mathjax.org/en/latest/index.html#basic-usage) and [MathJax Configuration Options](http://docs.mathjax.org/en/latest/index.html#mathjax-configuration-options). + +## Issues with Markdown + +After enabling MathJax, any math entered in-between proper markers (see documentation) will be processed and typeset in the web page. One issue that comes up, however, with Markdown is that the underscore character (`_`) is interpreted by Markdown as a way to wrap text in `emph` blocks while LaTeX (MathJax) interprets the underscore as a way to create a subscript. This "double speak" of the underscore can result in some unexpected and unwanted behavior. + +### Solution + +There are multiple ways to remedy this problem. One solution is to simply escape each underscore in your math code by entering `\_` instead of `_`. This can become quite tedious if the equations you are entering are full of subscripts. + +Another option is to tell Markdown to treat the MathJax code as verbatim code and not process it. One way to do this is to wrap the math expression inside a `
      ` `
      ` block. Markdown would ignore these sections and they would get passed directly on to MathJax and processed correctly. This works great for display style mathematics, but for inline math expressions the line break induced by the `
      ` is not acceptable. The syntax for instructing Markdown to treat inline text as verbatim is by wrapping it in backticks (`` ` ``). You might have noticed, however, that the text included in between backticks is rendered differently than standard text (on this site these are items highlighted in red). To get around this problem, we could create a new CSS entry that would apply standard styling to all inline verbatim text that includes MathJax code. Below I will show the HTML and CSS source that would accomplish this (note this solution was adapted from [this blog post](http://doswa.com/2011/07/20/mathjax-in-markdown.html)---all credit goes to the original author). + + + + + +As before, this content should be included in the HTML source of each page that will be using MathJax. The next code snippet contains the CSS that is used to have verbatim MathJax blocks render with the same font style as the body of the page. + + + code.has-jax {font: inherit; + font-size: 100%; + background: inherit; + border: inherit; + color: #515151;} + +In the CSS snippet, notice the line `color: #515151;`. `#515151` is the value assigned to the `color` attribute of the `body` class in my CSS. In order for the equations to fit in with the body of a web page, this value should be the same as the color of the body. + +### Usage + +With this setup, everything is in place for a natural usage of MathJax on pages generated using Hugo. In order to include inline mathematics, just put LaTeX code in between `` `$ TeX Code $` `` or `` `\( TeX Code \)` ``. To include display style mathematics, just put LaTeX code in between `
      $$TeX Code$$
      `. All the math will be properly typeset and displayed within your Hugo generated web page! diff --git a/docs/content/tutorials/migrate-from-jekyll.md b/docs/content/tutorials/migrate-from-jekyll.md new file mode 100644 index 000000000..0fd5d4af7 --- /dev/null +++ b/docs/content/tutorials/migrate-from-jekyll.md @@ -0,0 +1,162 @@ +--- +lastmod: 2015-12-24 +date: 2014-03-10 +linktitle: Migrating from Jekyll +toc: true +menu: + main: + parent: tutorials +prev: /tutorials/mathjax +next: /tutorials/create-a-multilingual-site +title: Migrate to Hugo from Jekyll +weight: 10 +--- + +**Note:** Hugo 0.15 comes with a `hugo import jekyll` command, see [import from Jekyll](/commands/hugo_import_jekyll/). +## Move static content to `static` +Jekyll has a rule that any directory not starting with `_` will be copied as-is to the `_site` output. Hugo keeps all static content under `static`. You should therefore move it all there. +With Jekyll, something that looked like + + ▾ / + ▾ images/ + logo.png + +should become + + ▾ / + ▾ static/ + ▾ images/ + logo.png + +Additionally, you'll want any files that should reside at the root (such as `CNAME`) to be moved to `static`. + +## Create your Hugo configuration file +Hugo can read your configuration as JSON, YAML or TOML. Hugo supports parameters custom configuration too. Refer to the [Hugo configuration documentation](/overview/configuration/) for details. + +## Set your configuration publish folder to `_site` +The default is for Jekyll to publish to `_site` and for Hugo to publish to `public`. If, like me, you have [`_site` mapped to a git submodule on the `gh-pages` branch](http://blog.blindgaenger.net/generate_github_pages_in_a_submodule.html), you'll want to do one of two alternatives: + +1. Change your submodule to point to map `gh-pages` to public instead of `_site` (recommended). + + git submodule deinit _site + git rm _site + git submodule add -b gh-pages git@github.com:your-username/your-repo.git public + +2. Or, change the Hugo configuration to use `_site` instead of `public`. + + { + .. + "publishDir": "_site", + .. + } + +## Convert Jekyll templates to Hugo templates +That's the bulk of the work right here. The documentation is your friend. You should refer to [Jekyll's template documentation](http://jekyllrb.com/docs/templates/) if you need to refresh your memory on how you built your blog and [Hugo's template](/layout/templates/) to learn Hugo's way. + +As a single reference data point, converting my templates for [heyitsalex.net](http://heyitsalex.net/) took me no more than a few hours. + +## Convert Jekyll plugins to Hugo shortcodes +Jekyll has [plugins](http://jekyllrb.com/docs/plugins/); Hugo has [shortcodes](/doc/shortcodes/). It's fairly trivial to do a port. + +### Implementation +As an example, I was using a custom [`image_tag`](https://github.com/alexandre-normand/alexandre-normand/blob/74bb12036a71334fdb7dba84e073382fc06908ec/_plugins/image_tag.rb) plugin to generate figures with caption when running Jekyll. As I read about shortcodes, I found Hugo had a nice built-in shortcode that does exactly the same thing. + +Jekyll's plugin: + +```ruby +module Jekyll + class ImageTag < Liquid::Tag + @url = nil + @caption = nil + @class = nil + @link = nil + // Patterns + IMAGE_URL_WITH_CLASS_AND_CAPTION = + IMAGE_URL_WITH_CLASS_AND_CAPTION_AND_LINK = /(\w+)(\s+)((https?:\/\/|\/)(\S+))(\s+)"(.*?)"(\s+)->((https?:\/\/|\/)(\S+))(\s*)/i + IMAGE_URL_WITH_CAPTION = /((https?:\/\/|\/)(\S+))(\s+)"(.*?)"/i + IMAGE_URL_WITH_CLASS = /(\w+)(\s+)((https?:\/\/|\/)(\S+))/i + IMAGE_URL = /((https?:\/\/|\/)(\S+))/i + def initialize(tag_name, markup, tokens) + super + if markup =~ IMAGE_URL_WITH_CLASS_AND_CAPTION_AND_LINK + @class = $1 + @url = $3 + @caption = $7 + @link = $9 + elsif markup =~ IMAGE_URL_WITH_CLASS_AND_CAPTION + @class = $1 + @url = $3 + @caption = $7 + elsif markup =~ IMAGE_URL_WITH_CAPTION + @url = $1 + @caption = $5 + elsif markup =~ IMAGE_URL_WITH_CLASS + @class = $1 + @url = $3 + elsif markup =~ IMAGE_URL + @url = $1 + end + end + def render(context) + if @class + source = "
      " + else + source = "
      " + end + if @link + source += "" + end + source += "" + if @link + source += "" + end + source += "
      #{@caption}
      " if @caption + source += "
      " + source + end + end +end +Liquid::Template.register_tag('image', Jekyll::ImageTag) +``` + +is written as this Hugo shortcode: + + +
      + {{ with .Get "link"}}{{ end }} + + {{ if .Get "link"}}{{ end }} + {{ if or (or (.Get "title") (.Get "caption")) (.Get "attr")}} +
      {{ if isset .Params "title" }} + {{ .Get "title" }}{{ end }} + {{ if or (.Get "caption") (.Get "attr")}}

      + {{ .Get "caption" }} + {{ with .Get "attrlink"}} {{ end }} + {{ .Get "attr" }} + {{ if .Get "attrlink"}} {{ end }} +

      {{ end }} +
      + {{ end }} +
      + + +### Usage +I simply changed: + + {% image full http://farm5.staticflickr.com/4136/4829260124_57712e570a_o_d.jpg "One of my favorite touristy-type photos. I secretly waited for the good light while we were "having fun" and took this. Only regret: a stupid pole in the top-left corner of the frame I had to clumsily get rid of at post-processing." ->http://www.flickr.com/photos/alexnormand/4829260124/in/set-72157624547713078/ %} + +to this (this example uses a slightly extended version named `fig`, different than the built-in `figure`): + + {{%/* fig class="full" src="http://farm5.staticflickr.com/4136/4829260124_57712e570a_o_d.jpg" title="One of my favorite touristy-type photos. I secretly waited for the good light while we were having fun and took this. Only regret: a stupid pole in the top-left corner of the frame I had to clumsily get rid of at post-processing." link="http://www.flickr.com/photos/alexnormand/4829260124/in/set-72157624547713078/" */%}} + +As a bonus, the shortcode named parameters are, arguably, more readable. + +## Finishing touches +### Fix content +Depending on the amount of customization that was done with each post with Jekyll, this step will require more or less effort. There are no hard and fast rules here except that `hugo server` is your friend. Test your changes and fix errors as needed. + +### Clean up +You'll want to remove the Jekyll configuration at this point. If you have anything else that isn't used, delete it. + +## A practical example in a diff +[Hey, it's Alex](http://heyitsalex.net/) was migrated in less than a _father-with-kids day_ from Jekyll to Hugo. You can see all the changes (and screw-ups) by looking at this [diff](https://github.com/alexandre-normand/alexandre-normand/compare/869d69435bd2665c3fbf5b5c78d4c22759d7613a...b7f6605b1265e83b4b81495423294208cc74d610). diff --git a/docs/data/articles.toml b/docs/data/articles.toml deleted file mode 100644 index 37b66928f..000000000 --- a/docs/data/articles.toml +++ /dev/null @@ -1,731 +0,0 @@ -[[article]] - title = "A visit to the Workshop: Hugo/Unix/Vim integration" - url = "https://blog.afoolishmanifesto.com/posts/hugo-unix-vim-integration/" - author = "fREW Schmidt" - date = "2017-07-22" - -[[article]] - title = "Hugo Easy Gallery - Automagical PhotoSwipe image gallery with a one-line shortcode" - url = "https://www.liwen.id.au/heg/" - author = "Li-Wen Yip" - date = "2017-03-25" - -[[article]] - title = "Automagical Image Gallery in Hugo with PhotoSwipe and jQuery" - url = "https://www.liwen.id.au/photoswipe/" - author = "Li-Wen Yip" - date = "2017-03-04" - -[[article]] - title = "Adding Isso Comments to Hugo" - url = "https://stiobhart.net/2017-02-24-isso-comments/" - author = "Stíobhart Matulevicz" - date = "2017-02-24" - -[[article]] - title = "Hugo Tutorial: How to Build & Host a (Very Fast) Static E-Commerce Site" - url = "https://snipcart.com/blog/hugo-tutorial-static-site-ecommerce" - author = "Snipcart" - date = "2017-02-23" - -[[article]] - title = "How to Password Protect a Hugo Site" - url = "https://www.aerobatic.com/blog/password-protect-a-hugo-site/" - author = "Aerobatic" - date = "2017-02-19" - -[[article]] - title = "Switching from WordPress to Hugo" - url = "http://schnuddelhuddel.de/switching-from-wordpress-to-hugo/" - author = "Mario Martelli" - date = "2017-02-19" - -[[article]] - title = "Zero to HTTP/2 with AWS and Hugo" - url = "https://habd.as/zero-to-http-2-aws-hugo/" - author = "Josh Habdas" - date = "2017-02-16" - -[[article]] - title = "Deploy a Hugo site to Aerobatic with CircleCI" - url = "https://www.aerobatic.com/blog/hugo-github-circleci/" - author = "Aerobatic" - date = "2017-02-14" - -[[article]] - title = "NPM scripts for building and deploying Hugo site" - url = "https://www.aerobatic.com/blog/hugo-npm-buildtool-setup/" - author = "Aerobatic" - date = "2017-02-12" - -[[article]] - title = "Getting started with Hugo and the plain-blog theme, on NearlyFreeSpeech.Net" - url = "https://www.penwatch.net/cms/get_started_plain_blog/" - author = "Li-aung “Lewis” Yip" - date = "2017-02-12" - -[[article]] - title = "Choose Hugo over Jekyll" - url = "https://habd.as/choose-hugo-over-jekyll/" - author = "Josh Habdas" - date = "2017-02-10" - -[[article]] - title = "Build a Hugo site using Cloud9 IDE and host on App Engine" - url = "https://loyall.ch/lab/2017/01/build-a-static-website-with-cloud9-hugo-and-app-engine/" - author = "Pascal Aubort" - date = "2017-02-05" - -[[article]] - title = "Hugo Continuous Deployment with Bitbucket Pipelines and Aerobatic" - url = "https://www.aerobatic.com/blog/hugo-bitbucket-pipelines/" - author = "Aerobatic" - date = "2017-02-04" - -[[article]] - title = "How to use Firebase to host a Hugo site" - url = "https://code.selfmadefighter.com/post/static-site-firebase/" - author = "Andrew Cuga" - date= "2017-02-04" - -[[article]] - title = "A publishing workflow for teams using static site generators" - url = "https://www.keybits.net/post/publishing-workflow-for-teams-using-static-site-generators/" - author = "Tom Atkins" - date = "2017-01-02" - -[[article]] - title = "How To Dynamically Use Google Fonts In A Hugo Website" - url = "https://stoned.io/web-development/hugo/How-To-Dynamically-Use-Google-Fonts-In-A-Hugo-Website/" - author = "Hash Borgir" - date = "2016-10-27" - -[[article]] - title = "Embedding Facebook In A Hugo Template" - url = "https://stoned.io/web-development/hugo/Embedding-Facebook-In-A-Hugo-Template/" - author = "Hash Borgir" - date = "2016-10-22" - -[[article]] - title = "通过 Gitlab-cl 将 Hugo blog 自动部署至 GitHub (Chinese, Continuous integration)" - url = "https://zetaoyang.github.io/post/2016/10/17/gitlab-cl.html" - author = "Zetao Yang" - date = "2016-10-17" - -[[article]] - title = "A Step-by-Step Guide: Hugo on Netlify" - url = "https://www.netlify.com/blog/2016/09/21/a-step-by-step-guide-hugo-on-netlify/" - author = "Eli Williamson" - date = "2016-09-21" - -[[article]] - title = "Building our site: From Django & WordPress to a static generator (Part I)" - url = "https://tryolabs.com/blog/2016/09/20/building-our-site-django-wordpress-to-static-part-i/" - author = "Alan Descoins" - date = "2016-09-20" - -[[article]] - title = "Webseitenmaschine - Statische Websites mit Hugo erzeugen (German, $)" - url = "http://www.heise.de/ct/ausgabe/2016-12-Statische-Websites-mit-Hugo-erzeugen-3211704.html" - author = "Christian Helmbold" - date = "2016-05-27" - -[[article]] - title = "Cómo hacer sitios web estáticos con Hugo y Go - Platzi (Video tutorial)" - url = "https://www.youtube.com/watch?v=qaXXpdiCHXE" - author = "Verónica López" - date = "2016-04-06" - -[[article]] - title = "CDNOverview: A CDN comparison site made with Hugo" - url = "https://www.cloakfusion.com/cdnoverview-cdn-comparison-site-made-hugo/" - author = "Thijs de Zoete" - date = "2016-02-23" - -[[article]] - title = "Hugo: A Modern Website Engine That Just Works" - url = "https://github.com/shekhargulati/52-technologies-in-2016/blob/master/07-hugo/README.md" - author = "Shekhar Gulati" - date = "2016-02-14" - -[[article]] - title = "Minify Hugo Generated HTML" - url = "http://ratson.name/blog/minify-hugo-generated-html/" - author = "Ratson" - date = "2016-02-02" - -[[article]] - title = "HugoのデプロイをWerckerからCircle CIに変更した - log" - url = "http://log.deprode.net/logs/2016-01-17/" - author = "Deprode" - date = "2016-01-17" - -[[article]] - title = "Static site generators: el futuro de las webs estáticas
      (Hugo, Jekyll, Flask y otros)" - url = "http://sitelabs.es/static-site-generators-futuro-las-webs-estaticas/" - author = "Eneko Sarasola" - date = "2016-01-09" - -[[article]] - title = "Writing a Lambda Function for Hugo" - url = "https://blog.jolexa.net/post/writing-a-lambda-function-for-hugo/" - author = "Jeremy Olexa" - date = "2016-01-01" - -[[article]] - title = "Ein Blog mit Hugo erstellen - Tutorial (Deutsch/German)" - url = "http://privat.albicker.org/tags/hugo.html" - author = "Bernhard Albicker" - date = "2015-12-30" - -[[article]] - title = "How to host Hugo static website generator on AWS Lambda" - url = "http://bezdelev.com/post/hugo-aws-lambda-static-website/" - author = "Ilya Bezdelev" - date = "2015-12-15" - -[[article]] - title = "Migrating from Pelican to Hugo" - url = "http://www.softinio.com/post/migrating-from-pelican-to-hugo/" - author = "Salar Rahmanian" - date = "2015-11-29" - -[[article]] - title = "Static Website Generators Reviewed: Jekyll, Middleman, Roots, Hugo" - url = "http://www.smashingmagazine.com/2015/11/static-website-generators-jekyll-middleman-roots-hugo-review/" - author = "Mathias Biilmann Christensen" - date = "2015-11-16" - -[[article]] - title = "How To Deploy a Hugo Site to Production with Git Hooks on Ubuntu 14.04" - url = "https://www.digitalocean.com/community/tutorials/how-to-deploy-a-hugo-site-to-production-with-git-hooks-on-ubuntu-14-04" - author = "Justin Ellingwood" - date = "2015-11-12" - -[[article]] - title = "How To Install and Use Hugo, a Static Site Generator, on Ubuntu 14.04" - url = "https://www.digitalocean.com/community/tutorials/how-to-install-and-use-hugo-a-static-site-generator-on-ubuntu-14-04" - author = "Justin Ellingwood" - date = "2015-11-09" - -[[article]] - title = "Switching from Wordpress to Hugo" - url = "http://justinfx.com/2015/11/08/switching-from-wordpress-to-hugo/" - author = "Justin Israel" - date = "2015-11-08" - -[[article]] - title = "Hands-on Experience with Hugo as a Static Site Generator" - url = "http://usersnap.com/blog/hands-on-experience-with-hugo-static-site-generator/" - author = "Thomas Peham" - date = "2015-10-15" - -[[article]] - title = "Statische Webseites mit Hugo erstellen/Vortrag mit Foliensatz (deutsch)" - url = "http://sfd.koelnerlinuxtreffen.de/2015/HaraldWeidner/" - author = "Harald Weidner" - date = "2015-09-19" - -[[article]] - title = "Moving from WordPress to Hugo" - url = "http://abhipandey.com/2015/09/moving-to-hugo/" - author = "Abhishek Pandey" - date = "2015-09-15" - -[[article]] - title = "通过webhook将Hugo自动部署至GitHub Pages和GitCafe Pages (Automated deployment)" - url = "http://blog.coderzh.com/2015/09/13/use-webhook-automated-deploy-hugo/" - author = "CoderZh" - date = "2015-09-13" - -[[article]] - title = "使用hugo搭建个人博客站点 (Using Hugo to build a personal blog site)" - url = "http://blog.coderzh.com/2015/08/29/hugo/" - author = "CoderZh" - date = "2015-08-29" - -[[article]] - title = "Good-Bye Wordpress, Hello Hugo! (German)" - url = "http://blog.arminhanisch.de/2015/08/blog-migration-zu-hugo/" - author = "Armin Hanisch" - date = "2015-08-18" - -[[article]] - title = "Générer votre site web statique avec Hugo (Generate your static site with Hugo)" - url = "http://www.linux-pratique.com/?p=191" - author = "Benoît Benedetti" - date = "2015-06-26" - -[[article]] - title = "Hugo向けの新しいテーマを作った (I created a new theme for Hugo)" - url = "https://yet.unresolved.xyz/blog/2016/10/03/how-to-make-of-hugo-theme/" - author = "Daisuke Tsuji" - date = "2015-06-20" - -[[article]] - title = "Hugo - Gerando um site com conteúdo estático. (Portuguese Brazil)" - url = "http://blog.ffrizzo.com/posts/hugo/" - author = "Fabiano Frizzo" - date = "2015-06-02" - -[[article]] - title = "An Introduction to Static Site Generators" - url = "http://davidwalsh.name/introduction-static-site-generators" - author = "Eduardo Bouças" - date = "2015-05-20" - -[[article]] - title = "Hugo Still Rules" - url = "http://cheekycoder.com/2015/05/hugo-still-rules/" - author = "Cheeky Coder" - date = "2015-05-18" - -[[article]] - title = "hugo - Static Site Generator" - url = "http://gscacco.github.io/post/hugo/" - author = "G Scaccoio" - date = "2015-05-04" - -[[article]] - title = "WindowsでHugoを使う" - url = "http://ureta.net/2015/05/hugo-on-windows/" - author = "うれ太郎" - date = "2015-05-01" - -[[article]] - title = "Hugoのshortcodesを用いてサイトにスライドなどを埋め込む" - url = "http://blog.yucchiy.com/2015/04/29/hugo-shortcode/" - author = "Yucchiy" - date = "2015-04-29" - -[[article]] - title = "HugoとCircleCIでGitHub PagesにBlogを公開してみたら超簡単だった" - url = "http://hori-ryota.github.io/blog/create-blog-with-hugo-and-circleci/" - author = "Hori Ryota" - date = "2015-04-17" - -[[article]] - title = "10 Best Static Site Generators" - url = "http://beebom.com/2015/04/best-static-site-generators" - author = "Aniruddha Mysore" - date = "2015-04-06" - -[[article]] - title = "Goodbye WordPress; Hello Hugo" - url = "http://willwarren.com/2015/04/05/goodbye-wordpress-hello-hugo/" - author = "Will Warren" - date = "2015-04-05" - -[[article]] - title = "Static Websites with Hugo on Google Cloud Storage" - url = "http://www.moxie.io/post/static-websites-with-hugo-on-google-cloud-storage/" - author = "Moxie Input/Output" - date = "2015-04-02" - -[[article]] - title = "De nuevo iniciando un blog" - url = "https://alvarolizama.net/" - author = "Alvaro Lizama" - date = "2015-03-29" - -[[article]] - title = "We moved our blog from Posthaven to Hugo after only three posts. Why?" - url = "http://blog.hypriot.com/post/moved-from-posthaven-to-hugo/" - author = "Hypriot" - date = "2015-03-27" - -[[article]] - title = "Top Static Site Generators in 2015" - url = "http://superdevresources.com/static-site-generators-2015/" - author = "Kanishk Kunal" - date = "2015-03-12" - -[[article]] - title = "Moving to Hugo" - url = "http://abiosoft.com/moving-to-hugo/" - author = "Abiola Ibrahim" - date = "2015-03-08" - -[[article]] - title = "Migrating a blog (yes, this one!) from Wordpress to Hugo" - url = "http://justindunham.net/migrating-from-wordpress-to-hugo/" - author = "Justin Dunham" - date = "2015-02-13" - -[[article]] - title = "blogをoctopressからHugoに乗り換えたメモ" - url = "http://blog.jigyakkuma.org/2015/02/11/hugo/" - author = "jigyakkuma" - date = "2015-02-11" - -[[article]] - title = "Hugoでブログをつくった" - url = "http://porgy13.github.io/post/new-hugo-blog/" - author = "porgy13" - date = "2015-02-07" - -[[article]] - title = "Hugoにブログを移行した" - url = "http://keichi.net/post/first/" - author = "Keichi Takahashi" - date = "2015-02-04" - -[[article]] - title = "Hugo静态网站生成器中文教程" - url = "http://nanshu.wang/post/2015-01-31/" - author = "Nanshu Wang" - date = "2015-01-31" - -[[article]] - title = "Hugo + Github Pages + Wercker CI = ¥0(無料)
      でコマンド 1 発(自動化)でサイト
      ・ブログを公開・運営・分析・収益化
      " - url = "http://qiita.com/yoheimuta/items/8a619cac356bed89a4c9" - author = "Yohei Yoshimuta" - date = "2015-01-31" - -[[article]] - title = "Running Hugo websites on anynines" - url = "http://blog.anynines.com/running-hugo-websites-on-anynines/" - author = "Julian Weber" - date = "2015-01-30" - -[[article]] - title = "MiddlemanからHugoへ移行した" - url = "http://re-dzine.net/2015/01/hugo/" - author = "Haruki Konishi" - date = "2015-01-21" - -[[article]] - title = "WordPress から Hugo に乗り換えました" - url = "http://rakuishi.com/archives/wordpress-to-hugo/" - author = "rakuishi" - date = "2015-01-20" - -[[article]] - title = "HUGOを使ってサイトを立ち上げる方法" - url = "http://qiita.com/syui/items/869538099551f24acbbf" - author = "Syui" - date = "2015-01-17" - -[[article]] - title = "Jekyllが許されるのは小学生までだよね" - url = "http://t32k.me/mol/log/hugo/" - author = "Ishimoto Koji" - date = "2015-01-16" - -[[article]] - title = "Getting started with Hugo" - url = "http://anthonyfok.org/post/getting-started-with-hugo/" - author = "Anthony Fok" - date = "2015-01-12" - -[[article]] - title = "把这个博客静态化了 (Migrate to Hugo)" - url = "http://lich-eng.com/2015/01/03/migrate-to-hugo/" - author = "Li Cheng" - date = "2015-01-03" - -[[article]] - title = "Porting my blog with Hugo" - url = "http://blog.srackham.com/posts/porting-my-blog-with-hugo/" - author = "Stuart Rackham" - date = "2014-12-30" - -[[article]] - title = "Hugoを使ってみたときのメモ" - url = "http://machortz.github.io/posts/usinghugo/" - author = "Machortz" - date = "2014-12-29" - -[[article]] - title = "OctopressからHugoへ移行した" - url = "http://deeeet.com/writing/2014/12/25/hugo/" - author = "Taichi Nakashima" - date = "2014-12-25" - -[[article]] - title = "Migrating to Hugo From Octopress" - url = "http://nathanleclaire.com/blog/2014/12/22/migrating-to-hugo-from-octopress/" - author = "Nathan LeClaire" - date = "2014-12-22" - -[[article]] - title = "Dynamic Pages with GoHugo.io" - url = "http://cyrillschumacher.com/2014/12/21/dynamic-pages-with-gohugo.io/" - author = "Cyrill Schumacher" - date = "2014-12-21" - -[[article]] - title = "6 Static Blog Generators That Aren’t Jekyll" - url = "http://www.sitepoint.com/6-static-blog-generators-arent-jekyll/" - author = "David Turnbull" - date = "2014-12-08" - -[[article]] - title = "Travel Blogging Setup" - url = "http://www.stou.dk/2014/11/travel-blogging-setup/" - author = "Rasmus Stougaard" - date = "2014-11-23" - -[[article]] - title = "Hosting A Hugo Website Behind Nginx" - url = "http://www.bigbeeconsultants.co.uk/blog/hosting-hugo-website-behind-nginx" - author = "Rick Beton" - date = "2014-11-20" - -[[article]] - title = "使用Hugo搭建免费个人Blog (How to use Hugo)" - url = "http://ulricqin.com/post/how-to-use-hugo/" - author = "Ulric Qin 秦晓辉" - date = "2014-11-11" - -[[article]] - title = "Built in Speed and Built for Speed by Hugo" - url = "http://cheekycoder.com/2014/10/built-for-speed-by-hugo/" - author = "Cheeky Coder" - date = "2014-10-30" - -[[article]] - title = "Hugo para crear sitios web estáticos" - url = "http://www.webbizarro.com/noticias/1076/hugo-para-crear-sitios-web-estaticos/" - author = "Web Bizarro" - date = "2014-08-19" - -[[article]] - title = "Going with Hugo" - url = "http://www.markuseliasson.se/article/going-with-hugo/" - author = "Markus Eliasson" - date = "2014-08-18" - -[[article]] - title = "Benchmarking Jekyll, Hugo and Wintersmith" - url = "http://fredrikloch.me/post/2014-08-12-Jekyll-and-its-alternatives-from-a-site-generation-point-of-view/" - author = "Fredrik Loch" - date = "2014-08-12" - -[[article]] - title = "Goodbye Octopress, Hello Hugo!" - url = "http://andreimihu.com/blog/2014/08/11/goodbye-octopress-hello-hugo/" - author = "Andrei Mihu" - date = "2014-08-11" - -[[article]] - title = "Beautiful sites for Open Source Projects" - url = "http://beautifulopen.com/2014/08/09/hugo/" - author = "Beautiful Open" - date = "2014-08-09" - -[[article]] - title = "Hugo: Beyond the Defaults" - url = "http://npf.io/2014/08/hugo-beyond-the-defaults/" - author = "Nate Finch" - date = "2014-08-08" - -[[article]] - title = "First Impressions of Hugo" - url = "https://peteraba.com/blog/first-impressions-of-hugo/" - author = "Peter Aba" - date = "2014-06-06" - -[[article]] - title = "New Site Workflow" - url = "http://vurt.co.uk/post/new_website/" - author = "Giles Paterson" - date = "2014-08-05" - -[[article]] - title = "How I Learned to Stop Worrying and Love the (Static) Web" - url = "http://cognition.ca/post/about-hugo/" - author = "Joshua McKenty" - date = "2014-08-04" - -[[article]] - title = "Hugo - Static Site Generator" - url = "http://kenwoo.io/blog/hugo---static-site-generator/" - author = "Kenny Woo" - date = "2014-08-03" - -[[article]] - title = "Hugo Is Freakin' Awesome" - url = "http://npf.io/2014/08/hugo-is-awesome/" - author = "Nate Finch" - date = "2014-08-01" - -[[article]] - title = "再次搬家 (Move from WordPress to Hugo)" - url = "http://www.chingli.com/misc/move-from-wordpress-to-hugo/" - author = "青砾 (chingli)" - date = "2014-07-12" - -[[article]] - title = "Embedding Gists in Hugo" - url = "http://danmux.com/posts/embedded_gists/" - author = "Dan Mull" - date = "2014-07-05" - -[[article]] - title = "An Introduction To Hugo" - url = "http://www.cirrushosting.com/web-hosting-blog/an-introduction-to-hugo/" - author = "Dan Silber" - date = "2014-07-01" - -[[article]] - title = "Moving to Hugo" - url = "http://danmux.com/posts/hugo_based_blog/" - author = "Dan Mull" - date = "2014-05-29" - -[[article]] - title = "开源之静态站点生成器排行榜
      (Leaderboard of open-source static website generators)" - url = "http://code.csdn.net/news/2819909" - author = "CSDN.net" - date = "2014-05-23" - -[[article]] - title = "Finally, a satisfying and effective blog setup" - url = "http://michaelwhatcott.com/now-powered-by-hugo/" - author = "Michael Whatcott" - date = "2014-05-20" - -[[article]] - title = "Hugo from scratch" - url = "http://zackofalltrades.com/notes/2014/05/hugo-from-scratch/" - author = "Zack Williams" - date = "2014-05-18" - -[[article]] - title = "Why I switched away from Jekyll" - url = "http://www.jakejanuzelli.com/why-I-switched-away-from-jekyll/" - author = "Jake Januzelli" - date = "2014-05-10" - -[[article]] - title = "Welcome our new blog" - url = "http://blog.ninya.io/posts/welcome-our-new-blog/" - author = "Ninya.io" - date = "2014-04-11" - -[[article]] - title = "Mission Not Accomplished" - url = "http://johnsto.co.uk/blog/mission-not-accomplished/" - author = "Dave Johnston" - date = "2014-04-03" - -[[article]] - title = "Hugo - A Static Site Builder in Go" - url = "http://deepfriedcode.com/post/hugo/" - author = "Deep Fried Code" - date = "2014-03-30" - -[[article]] - title = "Adventures in Angular Podcast" - url = "http://devchat.tv/adventures-in-angular/003-aia-gdes" - author = "Matias Niemela" - date = "2014-03-28" - -[[article]] - title = "Hugo" - url = "http://bra.am/post/hugo/" - author = "bra.am" - date = "2014-03-23" - -[[article]] - title = "Converting Blogger To Markdown" - url = "http://trishagee.github.io/project/atom-to-hugo/" - author = "Trisha Gee" - date = "2014-03-20" - -[[article]] - title = "Moving to Hugo Static Web Pages" - url = "http://tepid.org/tech/hugo-web/" - author = "Tobias Weingartner" - date = "2014-03-16" - -[[article]] - title = "New Blog Engine: Hugo" - url = "https://blog.afoolishmanifesto.com/posts/hugo/" - author = "fREW Schmidt" - date = "2014-03-15" - -[[article]] - title = "Hugo + gulp.js = Huggle" - url = "http://ktmud.github.io/huggle/en/intro/)" - author = "Jesse Yang 杨建超" - date = "2014-03-08" - -[[article]] - title = "Powered by Hugo" - url = "http://kieranhealy.org/blog/archives/2014/02/24/powered-by-hugo/" - author = "Kieran Healy" - date = "2014-02-24" - -[[article]] - title = "静的サイトを素早く構築するために
      GoLangで作られたジェネレータHugo
      " - url = "http://hamasyou.com/blog/2014/02/21/hugo/" - author = "
      Shogo Hamada
      濱田章吾
      " - date = "2014-02-21" - -[[article]] - title = "Latest Roundup of Useful Tools For Developers" - url = "http://codegeekz.com/latest-roundup-of-useful-tools-for-developers/" - author = "CodeGeekz" - date = "2014-02-13" - -[[article]] - title = "Hugo: Static Site Generator written in Go" - url = "http://www.braveterry.com/2014/02/06/hugo-static-site-generator-written-in-go/" - author = "Brave Terry" - date = "2014-02-06" - -[[article]] - title = "10 Useful HTML5 Tools for Web Designers and Developers" - url = "http://designdizzy.com/10-useful-html5-tools-for-web-designers-and-developers/" - author = "Design Dizzy" - date = "2014-02-04" - -[[article]] - title = "Hugo – Fast, Flexible Static Site Generator" - url = "http://cube3x.com/hugo-fast-flexible-static-site-generator/" - author = "Joby Joseph" - date = "2014-01-18" - -[[article]] - title = "Hugo: A new way to build static website" - url = "http://www.w3update.com/opensource/hugo-a-new-way-to-build-static-website.html" - author = "w3update" - date = "2014-01-17" - -[[article]] - title = "Xaprb now uses Hugo" - url = "http://xaprb.com/blog/2014/01/15/using-hugo/" - author = "Baron Schwartz" - date = "2014-01-15" - -[[article]] - title = "New jQuery Plugins And Resources That Web Designers Need" - url = "http://www.designyourway.net/blog/resources/new-jquery-plugins-and-resources-that-web-designers-need/" - author = "Design Your Way" - date = "2014-01-01" - -[[article]] - title = "On Blog Construction" - url = "http://alexla.sh/post/on-blog-construction/" - author = "Alexander Lash" - date = "2013-12-27" - -[[article]] - title = "Hugo" - url = "http://onethingwell.org/post/69070926608/hugo" - author = "One Thing Well" - date = "2013-12-05" - -[[article]] - title = "In Praise Of Hugo" - url = "http://sound-guru.com/blog/post/hello-world/" - author = "sound-guru.com" - date = "2013-10-19" - -[[article]] - title = "Hosting a blog on S3 and Cloudfront" - url = "http://www.danesparza.net/2013/07/hosting-a-blog-on-s3-and-cloudfront/" - author = "Dan Esparza" - date = "2013-07-24" diff --git a/docs/data/docs.json b/docs/data/docs.json new file mode 100644 index 000000000..90edb9bc8 --- /dev/null +++ b/docs/data/docs.json @@ -0,0 +1,1712 @@ +{ + "media": { + "types": [ + { + "Type": "application/javascript", + "String": "application/javascript+js", + "MainType": "application", + "SubType": "javascript", + "Suffix": "js", + "Delimiter": "." + }, + { + "Type": "application/json", + "String": "application/json+json", + "MainType": "application", + "SubType": "json", + "Suffix": "json", + "Delimiter": "." + }, + { + "Type": "application/rss", + "String": "application/rss+xml", + "MainType": "application", + "SubType": "rss", + "Suffix": "xml", + "Delimiter": "." + }, + { + "Type": "application/xml", + "String": "application/xml+xml", + "MainType": "application", + "SubType": "xml", + "Suffix": "xml", + "Delimiter": "." + }, + { + "Type": "text/calendar", + "String": "text/calendar+ics", + "MainType": "text", + "SubType": "calendar", + "Suffix": "ics", + "Delimiter": "." + }, + { + "Type": "text/css", + "String": "text/css+css", + "MainType": "text", + "SubType": "css", + "Suffix": "css", + "Delimiter": "." + }, + { + "Type": "text/csv", + "String": "text/csv+csv", + "MainType": "text", + "SubType": "csv", + "Suffix": "csv", + "Delimiter": "." + }, + { + "Type": "text/html", + "String": "text/html+html", + "MainType": "text", + "SubType": "html", + "Suffix": "html", + "Delimiter": "." + }, + { + "Type": "text/plain", + "String": "text/plain+txt", + "MainType": "text", + "SubType": "plain", + "Suffix": "txt", + "Delimiter": "." + } + ] + }, + "output": { + "formats": [ + { + "MediaType": "text/html+html", + "Name": "AMP", + "Path": "amp", + "BaseName": "index", + "Rel": "amphtml", + "Protocol": "", + "IsPlainText": false, + "IsHTML": true, + "NoUgly": false, + "NotAlternative": false + }, + { + "MediaType": "text/css+css", + "Name": "CSS", + "Path": "", + "BaseName": "styles", + "Rel": "stylesheet", + "Protocol": "", + "IsPlainText": true, + "IsHTML": false, + "NoUgly": false, + "NotAlternative": true + }, + { + "MediaType": "text/csv+csv", + "Name": "CSV", + "Path": "", + "BaseName": "index", + "Rel": "alternate", + "Protocol": "", + "IsPlainText": true, + "IsHTML": false, + "NoUgly": false, + "NotAlternative": false + }, + { + "MediaType": "text/calendar+ics", + "Name": "Calendar", + "Path": "", + "BaseName": "index", + "Rel": "alternate", + "Protocol": "webcal://", + "IsPlainText": true, + "IsHTML": false, + "NoUgly": false, + "NotAlternative": false + }, + { + "MediaType": "text/html+html", + "Name": "HTML", + "Path": "", + "BaseName": "index", + "Rel": "canonical", + "Protocol": "", + "IsPlainText": false, + "IsHTML": true, + "NoUgly": false, + "NotAlternative": false + }, + { + "MediaType": "application/json+json", + "Name": "JSON", + "Path": "", + "BaseName": "index", + "Rel": "alternate", + "Protocol": "", + "IsPlainText": true, + "IsHTML": false, + "NoUgly": false, + "NotAlternative": false + }, + { + "MediaType": "application/rss+xml", + "Name": "RSS", + "Path": "", + "BaseName": "index", + "Rel": "alternate", + "Protocol": "", + "IsPlainText": false, + "IsHTML": false, + "NoUgly": true, + "NotAlternative": false + } + ], + "layouts": [ + { + "Example": "AMP home, with theme \"demoTheme\".", + "OutputFormat": "AMP", + "Suffix": "html", + "Template Lookup Order": [ + "layouts/index.amp.html", + "layouts/index.html", + "layouts/_default/list.amp.html", + "layouts/_default/list.html", + "demoTheme/layouts/index.amp.html", + "demoTheme/layouts/index.html", + "demoTheme/layouts/_default/list.amp.html", + "demoTheme/layouts/_default/list.html" + ] + }, + { + "Example": "AMP home, French language\".", + "OutputFormat": "AMP", + "Suffix": "html", + "Template Lookup Order": [ + "layouts/index.fr.amp.html", + "layouts/index.amp.html", + "layouts/index.fr.html", + "layouts/index.html", + "layouts/_default/list.fr.amp.html", + "layouts/_default/list.amp.html", + "layouts/_default/list.fr.html", + "layouts/_default/list.html" + ] + }, + { + "Example": "JSON home, no theme.", + "OutputFormat": "JSON", + "Suffix": "json", + "Template Lookup Order": [ + "layouts/index.json.json", + "layouts/index.json", + "layouts/_default/list.json.json", + "layouts/_default/list.json" + ] + }, + { + "Example": "CSV regular, \"layout: demolayout\" in front matter.", + "OutputFormat": "CSV", + "Suffix": "csv", + "Template Lookup Order": [ + "layouts/_default/demolayout.csv.csv", + "layouts/_default/demolayout.csv" + ] + }, + { + "Example": "JSON regular, \"type: demotype\" in front matter.", + "OutputFormat": "JSON", + "Suffix": "json", + "Template Lookup Order": [ + "layouts/demotype/single.json.json", + "layouts/demotype/single.json", + "layouts/_default/single.json.json", + "layouts/_default/single.json" + ] + }, + { + "Example": "HTML regular.", + "OutputFormat": "HTML", + "Suffix": "html", + "Template Lookup Order": [ + "layouts/_default/single.html.html", + "layouts/_default/single.html" + ] + }, + { + "Example": "AMP regular.", + "OutputFormat": "AMP", + "Suffix": "html", + "Template Lookup Order": [ + "layouts/_default/single.amp.html", + "layouts/_default/single.html" + ] + }, + { + "Example": "Calendar blog section.", + "OutputFormat": "Calendar", + "Suffix": "ics", + "Template Lookup Order": [ + "layouts/section/blog.calendar.ics", + "layouts/section/blog.ics", + "layouts/blog/list.calendar.ics", + "layouts/blog/list.ics", + "layouts/_default/section.calendar.ics", + "layouts/_default/section.ics", + "layouts/_default/list.calendar.ics", + "layouts/_default/list.ics" + ] + }, + { + "Example": "Calendar taxonomy list.", + "OutputFormat": "Calendar", + "Suffix": "ics", + "Template Lookup Order": [ + "layouts/taxonomy/tag.calendar.ics", + "layouts/taxonomy/tag.ics", + "layouts/_default/taxonomy.calendar.ics", + "layouts/_default/taxonomy.ics", + "layouts/_default/list.calendar.ics", + "layouts/_default/list.ics" + ] + }, + { + "Example": "Calendar taxonomy term.", + "OutputFormat": "Calendar", + "Suffix": "ics", + "Template Lookup Order": [ + "layouts/taxonomy/tag.terms.calendar.ics", + "layouts/taxonomy/tag.terms.ics", + "layouts/_default/terms.calendar.ics", + "layouts/_default/terms.ics" + ] + } + ] + }, + "tpl": { + "funcs": { + "cast": { + "ToInt": { + "Description": "ToInt converts the given value to an int.", + "Args": [ + "v" + ], + "Aliases": [ + "int" + ], + "Examples": [ + [ + "{{ \"1234\" | int | printf \"%T\" }}", + "int" + ] + ] + }, + "ToString": { + "Description": "ToString converts the given value to a string.", + "Args": [ + "v" + ], + "Aliases": [ + "string" + ], + "Examples": [ + [ + "{{ 1234 | string | printf \"%T\" }}", + "string" + ] + ] + } + }, + "compare": { + "Default": { + "Description": "Default checks whether a given value is set and returns a default value if it\nis not. \"Set\" in this context means non-zero for numeric types and times;\nnon-zero length for strings, arrays, slices, and maps;\nany boolean or struct value; or non-nil for any other types.", + "Args": [ + "dflt", + "given" + ], + "Aliases": [ + "default" + ], + "Examples": [ + [ + "{{ \"Hugo Rocks!\" | default \"Hugo Rules!\" }}", + "Hugo Rocks!" + ], + [ + "{{ \"\" | default \"Hugo Rules!\" }}", + "Hugo Rules!" + ] + ] + }, + "Eq": { + "Description": "Eq returns the boolean truth of arg1 == arg2.", + "Args": [ + "x", + "y" + ], + "Aliases": [ + "eq" + ], + "Examples": [ + [ + "{{ if eq .Section \"blog\" }}current{{ end }}", + "current" + ] + ] + }, + "Ge": { + "Description": "Ge returns the boolean truth of arg1 \u003e= arg2.", + "Args": [ + "a", + "b" + ], + "Aliases": [ + "ge" + ], + "Examples": [] + }, + "Gt": { + "Description": "Gt returns the boolean truth of arg1 \u003e arg2.", + "Args": [ + "a", + "b" + ], + "Aliases": [ + "gt" + ], + "Examples": [] + }, + "Le": { + "Description": "Le returns the boolean truth of arg1 \u003c= arg2.", + "Args": [ + "a", + "b" + ], + "Aliases": [ + "le" + ], + "Examples": [] + }, + "Lt": { + "Description": "Lt returns the boolean truth of arg1 \u003c arg2.", + "Args": [ + "a", + "b" + ], + "Aliases": [ + "lt" + ], + "Examples": [] + }, + "Ne": { + "Description": "Ne returns the boolean truth of arg1 != arg2.", + "Args": [ + "x", + "y" + ], + "Aliases": [ + "ne" + ], + "Examples": [] + } + }, + "collections": { + "After": { + "Description": "After returns all the items after the first N in a rangeable list.", + "Args": [ + "index", + "seq" + ], + "Aliases": [ + "after" + ], + "Examples": [] + }, + "Apply": { + "Description": "Apply takes a map, array, or slice and returns a new slice with the function fname applied over it.", + "Args": [ + "seq", + "fname", + "args" + ], + "Aliases": [ + "apply" + ], + "Examples": [] + }, + "Delimit": { + "Description": "Delimit takes a given sequence and returns a delimited HTML string.\nIf last is passed to the function, it will be used as the final delimiter.", + "Args": [ + "seq", + "delimiter", + "last" + ], + "Aliases": [ + "delimit" + ], + "Examples": [ + [ + "{{ delimit (slice \"A\" \"B\" \"C\") \", \" \" and \" }}", + "A, B and C" + ] + ] + }, + "Dictionary": { + "Description": "Dictionary creates a map[string]interface{} from the given parameters by\nwalking the parameters and treating them as key-value pairs. The number\nof parameters must be even.", + "Args": [ + "values" + ], + "Aliases": [ + "dict" + ], + "Examples": [] + }, + "EchoParam": { + "Description": "EchoParam returns a given value if it is set; otherwise, it returns an\nempty string.", + "Args": [ + "a", + "key" + ], + "Aliases": [ + "echoParam" + ], + "Examples": [ + [ + "{{ echoParam .Params \"langCode\" }}", + "en" + ] + ] + }, + "First": { + "Description": "First returns the first N items in a rangeable list.", + "Args": [ + "limit", + "seq" + ], + "Aliases": [ + "first" + ], + "Examples": [] + }, + "In": { + "Description": "In returns whether v is in the set l. l may be an array or slice.", + "Args": [ + "l", + "v" + ], + "Aliases": [ + "in" + ], + "Examples": [ + [ + "{{ if in \"this string contains a substring\" \"substring\" }}Substring found!{{ end }}", + "Substring found!" + ] + ] + }, + "Index": { + "Description": "Index returns the result of indexing its first argument by the following\narguments. Thus \"index x 1 2 3\" is, in Go syntax, x[1][2][3]. Each\nindexed item must be a map, slice, or array.\n\nCopied from Go stdlib src/text/template/funcs.go.\n\nWe deviate from the stdlib due to https://github.com/golang/go/issues/14751.\n\nTODO(moorereason): merge upstream changes.", + "Args": [ + "item", + "indices" + ], + "Aliases": [ + "index" + ], + "Examples": [] + }, + "Intersect": { + "Description": "Intersect returns the common elements in the given sets, l1 and l2. l1 and\nl2 must be of the same type and may be either arrays or slices.", + "Args": [ + "l1", + "l2" + ], + "Aliases": [ + "intersect" + ], + "Examples": [] + }, + "IsSet": { + "Description": "IsSet returns whether a given array, channel, slice, or map has a key\ndefined.", + "Args": [ + "a", + "key" + ], + "Aliases": [ + "isSet", + "isset" + ], + "Examples": [] + }, + "Last": { + "Description": "Last returns the last N items in a rangeable list.", + "Args": [ + "limit", + "seq" + ], + "Aliases": [ + "last" + ], + "Examples": [] + }, + "Querify": { + "Description": "Querify encodes the given parameters in URL-encoded form (\"bar=baz\u0026foo=quux\") sorted by key.", + "Args": [ + "params" + ], + "Aliases": [ + "querify" + ], + "Examples": [ + [ + "{{ (querify \"foo\" 1 \"bar\" 2 \"baz\" \"with spaces\" \"qux\" \"this\u0026that=those\") | safeHTML }}", + "bar=2\u0026baz=with+spaces\u0026foo=1\u0026qux=this%26that%3Dthose" + ], + [ + "\u003ca href=\"https://www.google.com?{{ (querify \"q\" \"test\" \"page\" 3) | safeURL }}\"\u003eSearch\u003c/a\u003e", + "\u003ca href=\"https://www.google.com?page=3\u0026amp;q=test\"\u003eSearch\u003c/a\u003e" + ] + ] + }, + "Seq": { + "Description": "Seq creates a sequence of integers. It's named and used as GNU's seq.\n\nExamples:\n 3 =\u003e 1, 2, 3\n 1 2 4 =\u003e 1, 3\n -3 =\u003e -1, -2, -3\n 1 4 =\u003e 1, 2, 3, 4\n 1 -2 =\u003e 1, 0, -1, -2", + "Args": [ + "args" + ], + "Aliases": [ + "seq" + ], + "Examples": [ + [ + "{{ seq 3 }}", + "[1 2 3]" + ] + ] + }, + "Shuffle": { + "Description": "Shuffle returns the given rangeable list in a randomised order.", + "Args": [ + "seq" + ], + "Aliases": [ + "shuffle" + ], + "Examples": [] + }, + "Slice": { + "Description": "Slice returns a slice of all passed arguments.", + "Args": [ + "args" + ], + "Aliases": [ + "slice" + ], + "Examples": [ + [ + "{{ slice \"B\" \"C\" \"A\" | sort }}", + "[A B C]" + ] + ] + }, + "Sort": { + "Description": "Sort returns a sorted sequence.", + "Args": [ + "seq", + "args" + ], + "Aliases": [ + "sort" + ], + "Examples": [] + }, + "Union": { + "Description": "Union returns the union of the given sets, l1 and l2. l1 and\nl2 must be of the same type and may be either arrays or slices.\nIf l1 and l2 aren't of the same type then l1 will be returned.\nIf either l1 or l2 is nil then the non-nil list will be returned.", + "Args": [ + "l1", + "l2" + ], + "Aliases": [ + "union" + ], + "Examples": [ + [ + "{{ union (slice 1 2 3) (slice 3 4 5) }}", + "[1 2 3 4 5]" + ] + ] + }, + "Uniq": { + "Description": "Uniq takes in a slice or array and returns a slice with subsequent\nduplicate elements removed.", + "Args": [ + "l" + ], + "Aliases": [ + "uniq" + ], + "Examples": [ + [ + "{{ slice 1 2 3 2 | uniq }}", + "[1 2 3]" + ] + ] + }, + "Where": { + "Description": "Where returns a filtered subset of a given data type.", + "Args": [ + "seq", + "key", + "args" + ], + "Aliases": [ + "where" + ], + "Examples": [] + } + }, + "crypto": { + "MD5": { + "Description": "MD5 hashes the given input and returns its MD5 checksum.", + "Args": [ + "in" + ], + "Aliases": [ + "md5" + ], + "Examples": [ + [ + "{{ md5 \"Hello world, gophers!\" }}", + "b3029f756f98f79e7f1b7f1d1f0dd53b" + ], + [ + "{{ crypto.MD5 \"Hello world, gophers!\" }}", + "b3029f756f98f79e7f1b7f1d1f0dd53b" + ] + ] + }, + "SHA1": { + "Description": "SHA1 hashes the given input and returns its SHA1 checksum.", + "Args": [ + "in" + ], + "Aliases": [ + "sha1" + ], + "Examples": [ + [ + "{{ sha1 \"Hello world, gophers!\" }}", + "c8b5b0e33d408246e30f53e32b8f7627a7a649d4" + ] + ] + }, + "SHA256": { + "Description": "SHA256 hashes the given input and returns its SHA256 checksum.", + "Args": [ + "in" + ], + "Aliases": [ + "sha256" + ], + "Examples": [ + [ + "{{ sha256 \"Hello world, gophers!\" }}", + "6ec43b78da9669f50e4e422575c54bf87536954ccd58280219c393f2ce352b46" + ] + ] + } + }, + "data": { + "GetCSV": { + "Description": "GetCSV expects a data separator and one or n-parts of a URL to a resource which\ncan either be a local or a remote one.\nThe data separator can be a comma, semi-colon, pipe, etc, but only one character.\nIf you provide multiple parts for the URL they will be joined together to the final URL.\nGetCSV returns nil or a slice slice to use in a short code.", + "Args": [ + "sep", + "urlParts" + ], + "Aliases": [ + "getCSV" + ], + "Examples": [] + }, + "GetJSON": { + "Description": "GetJSON expects one or n-parts of a URL to a resource which can either be a local or a remote one.\nIf you provide multiple parts they will be joined together to the final URL.\nGetJSON returns nil or parsed JSON to use in a short code.", + "Args": [ + "urlParts" + ], + "Aliases": [ + "getJSON" + ], + "Examples": [] + } + }, + "encoding": { + "Base64Decode": { + "Description": "Base64Decode returns the base64 decoding of the given content.", + "Args": [ + "content" + ], + "Aliases": [ + "base64Decode" + ], + "Examples": [ + [ + "{{ \"SGVsbG8gd29ybGQ=\" | base64Decode }}", + "Hello world" + ], + [ + "{{ 42 | base64Encode | base64Decode }}", + "42" + ] + ] + }, + "Base64Encode": { + "Description": "Base64Encode returns the base64 encoding of the given content.", + "Args": [ + "content" + ], + "Aliases": [ + "base64Encode" + ], + "Examples": [ + [ + "{{ \"Hello world\" | base64Encode }}", + "SGVsbG8gd29ybGQ=" + ] + ] + }, + "Jsonify": { + "Description": "Jsonify encodes a given object to JSON.", + "Args": [ + "v" + ], + "Aliases": [ + "jsonify" + ], + "Examples": [ + [ + "{{ (slice \"A\" \"B\" \"C\") | jsonify }}", + "[\"A\",\"B\",\"C\"]" + ] + ] + } + }, + "fmt": { + "Print": { + "Description": "", + "Args": [ + "a" + ], + "Aliases": [ + "print" + ], + "Examples": [ + [ + "{{ print \"works!\" }}", + "works!" + ] + ] + }, + "Printf": { + "Description": "", + "Args": [ + "format", + "a" + ], + "Aliases": [ + "printf" + ], + "Examples": [ + [ + "{{ printf \"%s!\" \"works\" }}", + "works!" + ] + ] + }, + "Println": { + "Description": "", + "Args": [ + "a" + ], + "Aliases": [ + "println" + ], + "Examples": [ + [ + "{{ println \"works!\" }}", + "works!\n" + ] + ] + } + }, + "images": { + "Config": { + "Description": "Config returns the image.Config for the specified path relative to the\nworking directory.", + "Args": [ + "path" + ], + "Aliases": [ + "imageConfig" + ], + "Examples": [] + } + }, + "inflect": { + "Humanize": { + "Description": "Humanize returns the humanized form of a single parameter.\n\nIf the parameter is either an integer or a string containing an integer\nvalue, the behavior is to add the appropriate ordinal.\n\n Example: \"my-first-post\" -\u003e \"My first post\"\n Example: \"103\" -\u003e \"103rd\"\n Example: 52 -\u003e \"52nd\"", + "Args": [ + "in" + ], + "Aliases": [ + "humanize" + ], + "Examples": [ + [ + "{{ humanize \"my-first-post\" }}", + "My first post" + ], + [ + "{{ humanize \"myCamelPost\" }}", + "My camel post" + ], + [ + "{{ humanize \"52\" }}", + "52nd" + ], + [ + "{{ humanize 103 }}", + "103rd" + ] + ] + }, + "Pluralize": { + "Description": "Pluralize returns the plural form of a single word.", + "Args": [ + "in" + ], + "Aliases": [ + "pluralize" + ], + "Examples": [ + [ + "{{ \"cat\" | pluralize }}", + "cats" + ] + ] + }, + "Singularize": { + "Description": "Singularize returns the singular form of a single word.", + "Args": [ + "in" + ], + "Aliases": [ + "singularize" + ], + "Examples": [ + [ + "{{ \"cats\" | singularize }}", + "cat" + ] + ] + } + }, + "lang": { + "NumFmt": { + "Description": "NumFmt formats a number with the given precision using the\nnegative, decimal, and grouping options. The `options`\nparameter is a string consisting of `\u003cnegative\u003e \u003cdecimal\u003e \u003cgrouping\u003e`. The\ndefault `options` value is `- . ,`.\n\nNote that numbers are rounded up at 5 or greater.\nSo, with precision set to 0, 1.5 becomes `2`, and 1.4 becomes `1`.", + "Args": [ + "precision", + "number", + "options" + ], + "Aliases": null, + "Examples": [ + [ + "{{ lang.NumFmt 2 12345.6789 }}", + "12,345.68" + ], + [ + "{{ lang.NumFmt 2 12345.6789 \"- , .\" }}", + "12.345,68" + ], + [ + "{{ lang.NumFmt 6 -12345.6789 \"- .\" }}", + "-12345.678900" + ], + [ + "{{ lang.NumFmt 0 -12345.6789 \"- . ,\" }}", + "-12,346" + ], + [ + "{{ -98765.4321 | lang.NumFmt 2 }}", + "-98,765.43" + ] + ] + }, + "Translate": { + "Description": "Translate ...", + "Args": [ + "id", + "args" + ], + "Aliases": [ + "i18n", + "T" + ], + "Examples": [] + } + }, + "math": { + "Add": { + "Description": "", + "Args": [ + "a", + "b" + ], + "Aliases": [ + "add" + ], + "Examples": [ + [ + "{{add 1 2}}", + "3" + ] + ] + }, + "Div": { + "Description": "", + "Args": [ + "a", + "b" + ], + "Aliases": [ + "div" + ], + "Examples": [ + [ + "{{div 6 3}}", + "2" + ] + ] + }, + "Log": { + "Description": "", + "Args": [ + "a" + ], + "Aliases": null, + "Examples": [ + [ + "{{math.Log 1}}", + "0" + ] + ] + }, + "Mod": { + "Description": "Mod returns a % b.", + "Args": [ + "a", + "b" + ], + "Aliases": [ + "mod" + ], + "Examples": [ + [ + "{{mod 15 3}}", + "0" + ] + ] + }, + "ModBool": { + "Description": "ModBool returns the boolean of a % b. If a % b == 0, return true.", + "Args": [ + "a", + "b" + ], + "Aliases": [ + "modBool" + ], + "Examples": [ + [ + "{{modBool 15 3}}", + "true" + ] + ] + }, + "Mul": { + "Description": "", + "Args": [ + "a", + "b" + ], + "Aliases": [ + "mul" + ], + "Examples": [ + [ + "{{mul 2 3}}", + "6" + ] + ] + }, + "Sub": { + "Description": "", + "Args": [ + "a", + "b" + ], + "Aliases": [ + "sub" + ], + "Examples": [ + [ + "{{sub 3 2}}", + "1" + ] + ] + } + }, + "os": { + "Getenv": { + "Description": "Getenv retrieves the value of the environment variable named by the key.\nIt returns the value, which will be empty if the variable is not present.", + "Args": [ + "key" + ], + "Aliases": [ + "getenv" + ], + "Examples": [] + }, + "ReadDir": { + "Description": "ReadDir lists the directory contents relative to the configured WorkingDir.", + "Args": [ + "i" + ], + "Aliases": [ + "readDir" + ], + "Examples": [ + [ + "{{ range (readDir \".\") }}{{ .Name }}{{ end }}", + "README.txt" + ] + ] + }, + "ReadFile": { + "Description": "ReadFilereads the file named by filename relative to the configured\nWorkingDir. It returns the contents as a string. There is a upper size\nlimit set at 1 megabytes.", + "Args": [ + "i" + ], + "Aliases": [ + "readFile" + ], + "Examples": [ + [ + "{{ readFile \"README.txt\" }}", + "Hugo Rocks!" + ] + ] + } + }, + "partials": { + "Include": { + "Description": "Include executes the named partial and returns either a string,\nwhen the partial is a text/template, or template.HTML when html/template.", + "Args": [ + "name", + "contextList" + ], + "Aliases": [ + "partial" + ], + "Examples": [ + [ + "{{ partial \"header.html\" . }}", + "\u003ctitle\u003eHugo Rocks!\u003c/title\u003e" + ] + ] + } + }, + "safe": { + "CSS": { + "Description": "CSS returns a given string as html/template CSS content.", + "Args": [ + "a" + ], + "Aliases": [ + "safeCSS" + ], + "Examples": [ + [ + "{{ \"Bat\u0026Man\" | safeCSS | safeCSS }}", + "Bat\u0026amp;Man" + ] + ] + }, + "HTML": { + "Description": "HTML returns a given string as html/template HTML content.", + "Args": [ + "a" + ], + "Aliases": [ + "safeHTML" + ], + "Examples": [ + [ + "{{ \"Bat\u0026Man\" | safeHTML | safeHTML }}", + "Bat\u0026Man" + ], + [ + "{{ \"Bat\u0026Man\" | safeHTML }}", + "Bat\u0026Man" + ] + ] + }, + "HTMLAttr": { + "Description": "HTMLAttr returns a given string as html/template HTMLAttr content.", + "Args": [ + "a" + ], + "Aliases": [ + "safeHTMLAttr" + ], + "Examples": [] + }, + "JS": { + "Description": "JS returns the given string as a html/template JS content.", + "Args": [ + "a" + ], + "Aliases": [ + "safeJS" + ], + "Examples": [ + [ + "{{ \"(1*2)\" | safeJS | safeJS }}", + "(1*2)" + ] + ] + }, + "JSStr": { + "Description": "JSStr returns the given string as a html/template JSStr content.", + "Args": [ + "a" + ], + "Aliases": [ + "safeJSStr" + ], + "Examples": [] + }, + "SanitizeURL": { + "Description": "SanitizeURL returns a given string as html/template URL content.", + "Args": [ + "a" + ], + "Aliases": [ + "sanitizeURL", + "sanitizeurl" + ], + "Examples": [] + }, + "URL": { + "Description": "URL returns a given string as html/template URL content.", + "Args": [ + "a" + ], + "Aliases": [ + "safeURL" + ], + "Examples": [ + [ + "{{ \"http://gohugo.io\" | safeURL | safeURL }}", + "http://gohugo.io" + ] + ] + } + }, + "strings": { + "Chomp": { + "Description": "Chomp returns a copy of s with all trailing newline characters removed.", + "Args": [ + "s" + ], + "Aliases": [ + "chomp" + ], + "Examples": [ + [ + "{{chomp \"\u003cp\u003eBlockhead\u003c/p\u003e\\n\" }}", + "\u003cp\u003eBlockhead\u003c/p\u003e" + ] + ] + }, + "Contains": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "ContainsAny": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "CountRunes": { + "Description": "CountRunes returns the number of runes in s, excluding whitepace.", + "Args": [ + "s" + ], + "Aliases": [ + "countrunes" + ], + "Examples": [] + }, + "CountWords": { + "Description": "CountWords returns the approximate word count in s.", + "Args": [ + "s" + ], + "Aliases": [ + "countwords" + ], + "Examples": [] + }, + "FindRE": { + "Description": "FindRE returns a list of strings that match the regular expression. By default all matches\nwill be included. The number of matches can be limited with an optional third parameter.", + "Args": [ + "expr", + "content", + "limit" + ], + "Aliases": [ + "findRE" + ], + "Examples": [ + [ + "{{ findRE \"[G|g]o\" \"Hugo is a static side generator written in Go.\" \"1\" }}", + "[go]" + ] + ] + }, + "HasPrefix": { + "Description": "HasPrefix tests whether the input s begins with prefix.", + "Args": [ + "s", + "prefix" + ], + "Aliases": [ + "hasPrefix" + ], + "Examples": [ + [ + "{{ hasPrefix \"Hugo\" \"Hu\" }}", + "true" + ], + [ + "{{ hasPrefix \"Hugo\" \"Fu\" }}", + "false" + ] + ] + }, + "HasSuffix": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "Replace": { + "Description": "Replace returns a copy of the string s with all occurrences of old replaced\nwith new.", + "Args": [ + "s", + "old", + "new" + ], + "Aliases": [ + "replace" + ], + "Examples": [ + [ + "{{ replace \"Batman and Robin\" \"Robin\" \"Catwoman\" }}", + "Batman and Catwoman" + ] + ] + }, + "ReplaceRE": { + "Description": "ReplaceRE returns a copy of s, replacing all matches of the regular\nexpression pattern with the replacement text repl.", + "Args": [ + "pattern", + "repl", + "s" + ], + "Aliases": [ + "replaceRE" + ], + "Examples": [] + }, + "SliceString": { + "Description": "SliceString slices a string by specifying a half-open range with\ntwo indices, start and end. 1 and 4 creates a slice including elements 1 through 3.\nThe end index can be omitted, it defaults to the string's length.", + "Args": [ + "a", + "startEnd" + ], + "Aliases": [ + "slicestr" + ], + "Examples": [ + [ + "{{slicestr \"BatMan\" 0 3}}", + "Bat" + ], + [ + "{{slicestr \"BatMan\" 3}}", + "Man" + ] + ] + }, + "Split": { + "Description": "Split slices an input string into all substrings separated by delimiter.", + "Args": [ + "a", + "delimiter" + ], + "Aliases": [ + "split" + ], + "Examples": [] + }, + "Substr": { + "Description": "Substr extracts parts of a string, beginning at the character at the specified\nposition, and returns the specified number of characters.\n\nIt normally takes two parameters: start and length.\nIt can also take one parameter: start, i.e. length is omitted, in which case\nthe substring starting from start until the end of the string will be returned.\n\nTo extract characters from the end of the string, use a negative start number.\n\nIn addition, borrowing from the extended behavior described at http://php.net/substr,\nif length is given and is negative, then that many characters will be omitted from\nthe end of string.", + "Args": [ + "a", + "nums" + ], + "Aliases": [ + "substr" + ], + "Examples": [ + [ + "{{substr \"BatMan\" 0 -3}}", + "Bat" + ], + [ + "{{substr \"BatMan\" 3 3}}", + "Man" + ] + ] + }, + "Title": { + "Description": "Title returns a copy of the input s with all Unicode letters that begin words\nmapped to their title case.", + "Args": [ + "s" + ], + "Aliases": [ + "title" + ], + "Examples": [ + [ + "{{title \"Bat man\"}}", + "Bat Man" + ] + ] + }, + "ToLower": { + "Description": "ToLower returns a copy of the input s with all Unicode letters mapped to their\nlower case.", + "Args": [ + "s" + ], + "Aliases": [ + "lower" + ], + "Examples": [ + [ + "{{lower \"BatMan\"}}", + "batman" + ] + ] + }, + "ToUpper": { + "Description": "ToUpper returns a copy of the input s with all Unicode letters mapped to their\nupper case.", + "Args": [ + "s" + ], + "Aliases": [ + "upper" + ], + "Examples": [ + [ + "{{upper \"BatMan\"}}", + "BATMAN" + ] + ] + }, + "Trim": { + "Description": "Trim returns a string with all leading and trailing characters defined\ncontained in cutset removed.", + "Args": [ + "s", + "cutset" + ], + "Aliases": [ + "trim" + ], + "Examples": [ + [ + "{{ trim \"++Batman--\" \"+-\" }}", + "Batman" + ] + ] + }, + "TrimPrefix": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "TrimSuffix": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "Truncate": { + "Description": "", + "Args": [ + "a", + "options" + ], + "Aliases": [ + "truncate" + ], + "Examples": [ + [ + "{{ \"this is a very long text\" | truncate 10 \" ...\" }}", + "this is a ..." + ], + [ + "{{ \"With [Markdown](/markdown) inside.\" | markdownify | truncate 14 }}", + "With \u003ca href=\"/markdown\"\u003eMarkdown …\u003c/a\u003e" + ] + ] + } + }, + "time": { + "AsTime": { + "Description": "AsTime converts the textual representation of the datetime string into\na time.Time interface.", + "Args": [ + "v" + ], + "Aliases": null, + "Examples": [ + [ + "{{ (time \"2015-01-21\").Year }}", + "2015" + ] + ] + }, + "Format": { + "Description": "Format converts the textual representation of the datetime string into\nthe other form or returns it of the time.Time value. These are formatted\nwith the layout string", + "Args": [ + "layout", + "v" + ], + "Aliases": [ + "dateFormat" + ], + "Examples": [ + [ + "dateFormat: {{ dateFormat \"Monday, Jan 2, 2006\" \"2015-01-21\" }}", + "dateFormat: Wednesday, Jan 21, 2015" + ] + ] + }, + "Now": { + "Description": "Now returns the current local time.", + "Args": null, + "Aliases": [ + "now" + ], + "Examples": [] + } + }, + "transform": { + "Emojify": { + "Description": "Emojify returns a copy of s with all emoji codes replaced with actual emojis.\n\nSee http://www.emoji-cheat-sheet.com/", + "Args": [ + "s" + ], + "Aliases": [ + "emojify" + ], + "Examples": [ + [ + "{{ \"I :heart: Hugo\" | emojify }}", + "I ❤️ Hugo" + ] + ] + }, + "HTMLEscape": { + "Description": "HTMLEscape returns a copy of s with reserved HTML characters escaped.", + "Args": [ + "s" + ], + "Aliases": [ + "htmlEscape" + ], + "Examples": [ + [ + "{{ htmlEscape \"Cathal Garvey \u0026 The Sunshine Band \u003ccathal@foo.bar\u003e\" | safeHTML}}", + "Cathal Garvey \u0026amp; The Sunshine Band \u0026lt;cathal@foo.bar\u0026gt;" + ], + [ + "{{ htmlEscape \"Cathal Garvey \u0026 The Sunshine Band \u003ccathal@foo.bar\u003e\"}}", + "Cathal Garvey \u0026amp;amp; The Sunshine Band \u0026amp;lt;cathal@foo.bar\u0026amp;gt;" + ], + [ + "{{ htmlEscape \"Cathal Garvey \u0026 The Sunshine Band \u003ccathal@foo.bar\u003e\" | htmlUnescape | safeHTML }}", + "Cathal Garvey \u0026 The Sunshine Band \u003ccathal@foo.bar\u003e" + ] + ] + }, + "HTMLUnescape": { + "Description": "HTMLUnescape returns a copy of with HTML escape requences converted to plain\ntext.", + "Args": [ + "s" + ], + "Aliases": [ + "htmlUnescape" + ], + "Examples": [ + [ + "{{ htmlUnescape \"Cathal Garvey \u0026amp; The Sunshine Band \u0026lt;cathal@foo.bar\u0026gt;\" | safeHTML}}", + "Cathal Garvey \u0026 The Sunshine Band \u003ccathal@foo.bar\u003e" + ], + [ + "{{\"Cathal Garvey \u0026amp;amp; The Sunshine Band \u0026amp;lt;cathal@foo.bar\u0026amp;gt;\" | htmlUnescape | htmlUnescape | safeHTML}}", + "Cathal Garvey \u0026 The Sunshine Band \u003ccathal@foo.bar\u003e" + ], + [ + "{{\"Cathal Garvey \u0026amp;amp; The Sunshine Band \u0026amp;lt;cathal@foo.bar\u0026amp;gt;\" | htmlUnescape | htmlUnescape }}", + "Cathal Garvey \u0026amp; The Sunshine Band \u0026lt;cathal@foo.bar\u0026gt;" + ], + [ + "{{ htmlUnescape \"Cathal Garvey \u0026amp; The Sunshine Band \u0026lt;cathal@foo.bar\u0026gt;\" | htmlEscape | safeHTML }}", + "Cathal Garvey \u0026amp; The Sunshine Band \u0026lt;cathal@foo.bar\u0026gt;" + ] + ] + }, + "Highlight": { + "Description": "Highlight returns a copy of s as an HTML string with syntax\nhighlighting applied.", + "Args": [ + "s", + "lang", + "opts" + ], + "Aliases": [ + "highlight" + ], + "Examples": [] + }, + "Markdownify": { + "Description": "Markdownify renders a given input from Markdown to HTML.", + "Args": [ + "s" + ], + "Aliases": [ + "markdownify" + ], + "Examples": [ + [ + "{{ .Title | markdownify}}", + "\u003cstrong\u003eBatMan\u003c/strong\u003e" + ] + ] + }, + "Plainify": { + "Description": "Plainify returns a copy of s with all HTML tags removed.", + "Args": [ + "s" + ], + "Aliases": [ + "plainify" + ], + "Examples": [ + [ + "{{ plainify \"Hello \u003cstrong\u003eworld\u003c/strong\u003e, gophers!\" }}", + "Hello world, gophers!" + ] + ] + } + }, + "urls": { + "AbsLangURL": { + "Description": "AbsLangURL takes a given string and converts it to an absolute URL according\nto a page's position in the project directory structure and the current\nlanguage.", + "Args": [ + "a" + ], + "Aliases": [ + "absLangURL" + ], + "Examples": [] + }, + "AbsURL": { + "Description": "AbsURL takes a given string and converts it to an absolute URL.", + "Args": [ + "a" + ], + "Aliases": [ + "absURL" + ], + "Examples": [] + }, + "Ref": { + "Description": "Ref returns the absolute URL path to a given content item.", + "Args": [ + "in", + "refs" + ], + "Aliases": [ + "ref" + ], + "Examples": [] + }, + "RelLangURL": { + "Description": "RelLangURL takes a given string and prepends the relative path according to a\npage's position in the project directory structure and the current language.", + "Args": [ + "a" + ], + "Aliases": [ + "relLangURL" + ], + "Examples": [] + }, + "RelRef": { + "Description": "RelRef returns the relative URL path to a given content item.", + "Args": [ + "in", + "refs" + ], + "Aliases": [ + "relref" + ], + "Examples": [] + }, + "RelURL": { + "Description": "RelURL takes a given string and prepends the relative path according to a\npage's position in the project directory structure.", + "Args": [ + "a" + ], + "Aliases": [ + "relURL" + ], + "Examples": [] + }, + "URLize": { + "Description": "", + "Args": [ + "a" + ], + "Aliases": [ + "urlize" + ], + "Examples": [] + } + } + } + } +} diff --git a/docs/data/docs.yaml b/docs/data/docs.yaml deleted file mode 100644 index 0db4c7fe9..000000000 --- a/docs/data/docs.yaml +++ /dev/null @@ -1,4914 +0,0 @@ -chroma: - lexers: - - Aliases: - - abap - Name: ABAP - - Aliases: - - abnf - Name: ABNF - - Aliases: - - as - - actionscript - Name: ActionScript - - Aliases: - - as3 - - actionscript3 - Name: ActionScript 3 - - Aliases: - - ada - - ada95 - - ada2005 - Name: Ada - - Aliases: - - agda - Name: Agda - - Aliases: - - al - Name: AL - - Aliases: - - alloy - Name: Alloy - - Aliases: - - ng2 - Name: Angular2 - - Aliases: - - antlr - Name: ANTLR - - Aliases: - - apacheconf - - aconf - - apache - Name: ApacheConf - - Aliases: - - apl - Name: APL - - Aliases: - - applescript - Name: AppleScript - - Aliases: - - aql - Name: ArangoDB AQL - - Aliases: - - arduino - Name: Arduino - - Aliases: - - armasm - Name: ArmAsm - - Aliases: - - atl - Name: ATL - - Aliases: - - autohotkey - - ahk - Name: AutoHotkey - - Aliases: - - autoit - Name: AutoIt - - Aliases: - - awk - - gawk - - mawk - - nawk - Name: Awk - - Aliases: - - ballerina - Name: Ballerina - - Aliases: - - bash - - sh - - ksh - - zsh - - shell - Name: Bash - - Aliases: - - bash-session - - console - - shell-session - Name: Bash Session - - Aliases: - - bat - - batch - - dosbatch - - winbatch - Name: Batchfile - - Aliases: - - beef - Name: Beef - - Aliases: - - bib - - bibtex - Name: BibTeX - - Aliases: - - bicep - Name: Bicep - - Aliases: - - blitzbasic - - b3d - - bplus - Name: BlitzBasic - - Aliases: - - bnf - Name: BNF - - Aliases: - - bqn - Name: BQN - - Aliases: - - brainfuck - - bf - Name: Brainfuck - - Aliases: - - c - Name: C - - Aliases: - - csharp - - c# - Name: C# - - Aliases: - - cpp - - c++ - Name: C++ - - Aliases: - - caddyfile - - caddy - Name: Caddyfile - - Aliases: - - caddyfile-directives - - caddyfile-d - - caddy-d - Name: Caddyfile Directives - - Aliases: - - capnp - Name: Cap'n Proto - - Aliases: - - cassandra - - cql - Name: Cassandra CQL - - Aliases: - - ceylon - Name: Ceylon - - Aliases: - - cfengine3 - - cf3 - Name: CFEngine3 - - Aliases: - - cfs - Name: cfstatement - - Aliases: - - chai - - chaiscript - Name: ChaiScript - - Aliases: - - chapel - - chpl - Name: Chapel - - Aliases: - - cheetah - - spitfire - Name: Cheetah - - Aliases: - - clojure - - clj - - edn - Name: Clojure - - Aliases: - - cmake - Name: CMake - - Aliases: - - cobol - Name: COBOL - - Aliases: - - coffee-script - - coffeescript - - coffee - Name: CoffeeScript - - Aliases: - - common-lisp - - cl - - lisp - Name: Common Lisp - - Aliases: - - coq - Name: Coq - - Aliases: - - cr - - crystal - Name: Crystal - - Aliases: - - css - Name: CSS - - Aliases: - - csv - Name: CSV - - Aliases: - - cue - Name: CUE - - Aliases: - - cython - - pyx - - pyrex - Name: Cython - - Aliases: - - d - Name: D - - Aliases: - - dart - Name: Dart - - Aliases: - - dax - Name: Dax - - Aliases: - - desktop - - desktop_entry - Name: Desktop file - - Aliases: - - diff - - udiff - Name: Diff - - Aliases: - - django - - jinja - Name: Django/Jinja - - Aliases: - - zone - - bind - Name: dns - - Aliases: - - docker - - dockerfile - Name: Docker - - Aliases: - - dtd - Name: DTD - - Aliases: - - dylan - Name: Dylan - - Aliases: - - ebnf - Name: EBNF - - Aliases: - - elixir - - ex - - exs - Name: Elixir - - Aliases: - - elm - Name: Elm - - Aliases: - - emacs - - elisp - - emacs-lisp - Name: EmacsLisp - - Aliases: - - erlang - Name: Erlang - - Aliases: - - factor - Name: Factor - - Aliases: - - fennel - - fnl - Name: Fennel - - Aliases: - - fish - - fishshell - Name: Fish - - Aliases: - - forth - Name: Forth - - Aliases: - - fortran - - f90 - Name: Fortran - - Aliases: - - fortranfixed - Name: FortranFixed - - Aliases: - - fsharp - Name: FSharp - - Aliases: - - gas - - asm - Name: GAS - - Aliases: - - gdscript - - gd - Name: GDScript - - Aliases: - - gdscript3 - - gd3 - Name: GDScript3 - - Aliases: - - genshi - - kid - - xml+genshi - - xml+kid - Name: Genshi - - Aliases: - - html+genshi - - html+kid - Name: Genshi HTML - - Aliases: - - genshitext - Name: Genshi Text - - Aliases: - - cucumber - - Cucumber - - gherkin - - Gherkin - Name: Gherkin - - Aliases: - - gleam - Name: Gleam - - Aliases: - - glsl - Name: GLSL - - Aliases: - - gnuplot - Name: Gnuplot - - Aliases: - - go - - golang - Name: Go - - Aliases: - - go-html-template - Name: Go HTML Template - - Aliases: - - go-template - Name: Go Template - - Aliases: - - go-text-template - Name: Go Text Template - - Aliases: - - graphql - - graphqls - - gql - Name: GraphQL - - Aliases: - - groff - - nroff - - man - Name: Groff - - Aliases: - - groovy - Name: Groovy - - Aliases: - - handlebars - - hbs - Name: Handlebars - - Aliases: - - hare - Name: Hare - - Aliases: - - haskell - - hs - Name: Haskell - - Aliases: - - hx - - haxe - - hxsl - Name: Haxe - - Aliases: - - hcl - Name: HCL - - Aliases: - - hexdump - Name: Hexdump - - Aliases: - - hlb - Name: HLB - - Aliases: - - hlsl - Name: HLSL - - Aliases: - - holyc - Name: HolyC - - Aliases: - - html - Name: HTML - - Aliases: - - http - Name: HTTP - - Aliases: - - hylang - Name: Hy - - Aliases: - - idris - - idr - Name: Idris - - Aliases: - - igor - - igorpro - Name: Igor - - Aliases: - - ini - - cfg - - dosini - Name: INI - - Aliases: - - io - Name: Io - - Aliases: - - iscdhcpd - Name: ISCdhcpd - - Aliases: - - j - Name: J - - Aliases: - - java - Name: Java - - Aliases: - - js - - javascript - Name: JavaScript - - Aliases: - - json - Name: JSON - - Aliases: - - jsonata - Name: JSONata - - Aliases: - - jsonnet - Name: Jsonnet - - Aliases: - - julia - - jl - Name: Julia - - Aliases: - - jungle - Name: Jungle - - Aliases: - - kotlin - Name: Kotlin - - Aliases: - - lighty - - lighttpd - Name: Lighttpd configuration file - - Aliases: - - llvm - Name: LLVM - - Aliases: - - lua - Name: Lua - - Aliases: - - make - - makefile - - mf - - bsdmake - Name: Makefile - - Aliases: - - mako - Name: Mako - - Aliases: - - md - - mkd - Name: markdown - - Aliases: - - mason - Name: Mason - - Aliases: - - materialize - - mzsql - Name: Materialize SQL dialect - - Aliases: - - mathematica - - mma - - nb - Name: Mathematica - - Aliases: - - matlab - Name: Matlab - - Aliases: - - mcfunction - - mcf - Name: MCFunction - - Aliases: - - meson - - meson.build - Name: Meson - - Aliases: - - metal - Name: Metal - - Aliases: - - minizinc - - MZN - - mzn - Name: MiniZinc - - Aliases: - - mlir - Name: MLIR - - Aliases: - - modula2 - - m2 - Name: Modula-2 - - Aliases: - - monkeyc - Name: MonkeyC - - Aliases: - - morrowind - - mwscript - Name: MorrowindScript - - Aliases: - - myghty - Name: Myghty - - Aliases: - - mysql - - mariadb - Name: MySQL - - Aliases: - - nasm - Name: NASM - - Aliases: - - natural - Name: Natural - - Aliases: - - ndisasm - Name: NDISASM - - Aliases: - - newspeak - Name: Newspeak - - Aliases: - - nginx - Name: Nginx configuration file - - Aliases: - - nim - - nimrod - Name: Nim - - Aliases: - - nixos - - nix - Name: Nix - - Aliases: - - nsis - - nsi - - nsh - Name: NSIS - - Aliases: - - objective-c - - objectivec - - obj-c - - objc - Name: Objective-C - - Aliases: - - objectpascal - Name: ObjectPascal - - Aliases: - - ocaml - Name: OCaml - - Aliases: - - octave - Name: Octave - - Aliases: - - odin - Name: Odin - - Aliases: - - ones - - onesenterprise - - 1S - - 1S:Enterprise - Name: OnesEnterprise - - Aliases: - - openedge - - abl - - progress - - openedgeabl - Name: OpenEdge ABL - - Aliases: - - openscad - Name: OpenSCAD - - Aliases: - - org - - orgmode - Name: Org Mode - - Aliases: - - pacmanconf - Name: PacmanConf - - Aliases: - - perl - - pl - Name: Perl - - Aliases: - - php - - php3 - - php4 - - php5 - Name: PHP - - Aliases: - - phtml - Name: PHTML - - Aliases: - - pig - Name: Pig - - Aliases: - - pkgconfig - Name: PkgConfig - - Aliases: - - plpgsql - Name: PL/pgSQL - - Aliases: - - text - - plain - - no-highlight - Name: plaintext - - Aliases: - - plutus-core - - plc - Name: Plutus Core - - Aliases: - - pony - Name: Pony - - Aliases: - - postgresql - - postgres - Name: PostgreSQL SQL dialect - - Aliases: - - postscript - - postscr - Name: PostScript - - Aliases: - - pov - Name: POVRay - - Aliases: - - powerquery - - pq - Name: PowerQuery - - Aliases: - - powershell - - posh - - ps1 - - psm1 - - psd1 - - pwsh - Name: PowerShell - - Aliases: - - prolog - Name: Prolog - - Aliases: - - promela - Name: Promela - - Aliases: - - promql - Name: PromQL - - Aliases: - - java-properties - Name: properties - - Aliases: - - protobuf - - proto - Name: Protocol Buffer - - Aliases: - - prql - Name: PRQL - - Aliases: - - psl - Name: PSL - - Aliases: - - puppet - Name: Puppet - - Aliases: - - python - - py - - sage - - python3 - - py3 - Name: Python - - Aliases: - - python2 - - py2 - Name: Python 2 - - Aliases: - - qbasic - - basic - Name: QBasic - - Aliases: - - qml - - qbs - Name: QML - - Aliases: - - splus - - s - - r - Name: R - - Aliases: - - racket - - rkt - Name: Racket - - Aliases: - - ragel - Name: Ragel - - Aliases: - - perl6 - - pl6 - - raku - Name: Raku - - Aliases: - - jsx - - react - Name: react - - Aliases: - - reason - - reasonml - Name: ReasonML - - Aliases: - - registry - Name: reg - - Aliases: - - rego - Name: Rego - - Aliases: - - rst - - rest - - restructuredtext - Name: reStructuredText - - Aliases: - - rexx - - arexx - Name: Rexx - - Aliases: - - spec - Name: RPMSpec - - Aliases: - - rb - - ruby - - duby - Name: Ruby - - Aliases: - - rust - - rs - Name: Rust - - Aliases: - - sas - Name: SAS - - Aliases: - - sass - Name: Sass - - Aliases: - - scala - Name: Scala - - Aliases: - - scheme - - scm - Name: Scheme - - Aliases: - - scilab - Name: Scilab - - Aliases: - - scss - Name: SCSS - - Aliases: - - sed - - gsed - - ssed - Name: Sed - - Aliases: - - sieve - Name: Sieve - - Aliases: - - smali - Name: Smali - - Aliases: - - smalltalk - - squeak - - st - Name: Smalltalk - - Aliases: - - smarty - Name: Smarty - - Aliases: - - snbt - Name: SNBT - - Aliases: - - snobol - Name: Snobol - - Aliases: - - sol - - solidity - Name: Solidity - - Aliases: - - sp - Name: SourcePawn - - Aliases: - - sparql - Name: SPARQL - - Aliases: - - sql - Name: SQL - - Aliases: - - squidconf - - squid.conf - - squid - Name: SquidConf - - Aliases: - - sml - Name: Standard ML - - Aliases: null - Name: stas - - Aliases: - - stylus - Name: Stylus - - Aliases: - - svelte - Name: Svelte - - Aliases: - - swift - Name: Swift - - Aliases: - - systemd - Name: SYSTEMD - - Aliases: - - systemverilog - - sv - Name: systemverilog - - Aliases: - - tablegen - Name: TableGen - - Aliases: - - tal - - uxntal - Name: Tal - - Aliases: - - tasm - Name: TASM - - Aliases: - - tcl - Name: Tcl - - Aliases: - - tcsh - - csh - Name: Tcsh - - Aliases: - - termcap - Name: Termcap - - Aliases: - - terminfo - Name: Terminfo - - Aliases: - - terraform - - tf - Name: Terraform - - Aliases: - - tex - - latex - Name: TeX - - Aliases: - - thrift - Name: Thrift - - Aliases: - - toml - Name: TOML - - Aliases: - - tradingview - - tv - Name: TradingView - - Aliases: - - tsql - - t-sql - Name: Transact-SQL - - Aliases: - - turing - Name: Turing - - Aliases: - - turtle - Name: Turtle - - Aliases: - - twig - Name: Twig - - Aliases: - - ts - - tsx - - typescript - Name: TypeScript - - Aliases: - - typoscript - Name: TypoScript - - Aliases: - - typoscriptcssdata - Name: TypoScriptCssData - - Aliases: - - typoscripthtmldata - Name: TypoScriptHtmlData - - Aliases: - - typst - Name: Typst - - Aliases: null - Name: ucode - - Aliases: - - v - - vlang - Name: V - - Aliases: - - vsh - - vshell - Name: V shell - - Aliases: - - vala - - vapi - Name: Vala - - Aliases: - - vb.net - - vbnet - Name: VB.net - - Aliases: - - verilog - - v - Name: verilog - - Aliases: - - vhdl - Name: VHDL - - Aliases: - - vhs - - tape - - cassette - Name: VHS - - Aliases: - - vim - Name: VimL - - Aliases: - - vue - - vuejs - Name: vue - - Aliases: null - Name: WDTE - - Aliases: - - wgsl - Name: WebGPU Shading Language - - Aliases: - - vtt - Name: WebVTT - - Aliases: - - whiley - Name: Whiley - - Aliases: - - xml - Name: XML - - Aliases: - - xorg.conf - Name: Xorg - - Aliases: - - yaml - Name: YAML - - Aliases: - - yang - Name: YANG - - Aliases: - - z80 - Name: Z80 Assembly - - Aliases: - - zed - Name: Zed - - Aliases: - - zig - Name: Zig - styles: - - abap - - algol - - algol_nu - - arduino - - autumn - - average - - base16-snazzy - - borland - - bw - - catppuccin-frappe - - catppuccin-latte - - catppuccin-macchiato - - catppuccin-mocha - - colorful - - doom-one - - doom-one2 - - dracula - - emacs - - evergarden - - friendly - - fruity - - github - - github-dark - - gruvbox - - gruvbox-light - - hr_high_contrast - - hrdark - - igor - - lovelace - - manni - - modus-operandi - - modus-vivendi - - monokai - - monokailight - - murphy - - native - - nord - - nordic - - onedark - - onesenterprise - - paraiso-dark - - paraiso-light - - pastie - - perldoc - - pygments - - rainbow_dash - - rose-pine - - rose-pine-dawn - - rose-pine-moon - - rrt - - solarized-dark - - solarized-dark256 - - solarized-light - - swapoff - - tango - - tokyonight-day - - tokyonight-moon - - tokyonight-night - - tokyonight-storm - - trac - - vim - - vs - - vulcan - - witchhazel - - xcode - - xcode-dark -config: - HTTPCache: - cache: - for: - excludes: - - '**' - includes: null - polls: - - disable: true - for: - excludes: null - includes: - - '**' - high: 0s - low: 0s - archeTypeDir: archetypes - assetDir: assets - author: {} - baseURL: "" - build: - buildStats: - disableClasses: false - disableIDs: false - disableTags: false - enable: false - cacheBusters: - - source: (postcss|tailwind)\.config\.js - target: (css|styles|scss|sass) - noJSConfigInAssets: false - useResourceCacheWhen: fallback - buildDrafts: false - buildExpired: false - buildFuture: false - cacheDir: "" - caches: - assets: - dir: :resourceDir/_gen - maxAge: -1 - getcsv: - dir: :cacheDir/:project - maxAge: -1 - getjson: - dir: :cacheDir/:project - maxAge: -1 - getresource: - dir: :cacheDir/:project - maxAge: -1 - images: - dir: :resourceDir/_gen - maxAge: -1 - misc: - dir: :cacheDir/:project - maxAge: -1 - modules: - dir: :cacheDir/modules - maxAge: -1 - canonifyURLs: false - capitalizeListTitles: true - cascade: [] - cleanDestinationDir: false - contentDir: content - contentTypes: - text/asciidoc: {} - text/html: {} - text/markdown: {} - text/org: {} - text/pandoc: {} - text/rst: {} - copyright: "" - dataDir: data - defaultContentLanguage: en - defaultContentLanguageInSubdir: false - defaultOutputFormat: html - deployment: - confirm: false - dryRun: false - force: false - invalidateCDN: true - matchers: null - maxDeletes: 256 - order: null - target: "" - targets: null - workers: 10 - disableAliases: false - disableDefaultLanguageRedirect: false - disableHugoGeneratorInject: false - disableKinds: null - disableLanguages: null - disableLiveReload: false - disablePathToLower: false - enableEmoji: false - enableGitInfo: false - enableMissingTranslationPlaceholders: false - enableRobotsTXT: false - environment: production - frontmatter: - date: - - date - - publishdate - - pubdate - - published - - lastmod - - modified - expiryDate: - - expirydate - - unpublishdate - lastmod: - - :git - - lastmod - - modified - - date - - publishdate - - pubdate - - published - publishDate: - - publishdate - - pubdate - - published - - date - hasCJKLanguage: false - i18nDir: i18n - ignoreCache: false - ignoreFiles: [] - ignoreLogs: null - ignoreVendorPaths: "" - imaging: - bgColor: '#ffffff' - hint: photo - quality: 75 - resampleFilter: box - languageCode: "" - languages: - en: - disabled: false - languageCode: "" - languageDirection: "" - languageName: "" - title: "" - weight: 0 - layoutDir: layouts - mainSections: null - markup: - asciidocExt: - attributes: {} - backend: html5 - extensions: [] - failureLevel: fatal - noHeaderOrFooter: true - preserveTOC: false - safeMode: unsafe - sectionNumbers: false - trace: false - verbose: false - workingFolderCurrent: false - defaultMarkdownHandler: goldmark - goldmark: - duplicateResourceFiles: false - extensions: - cjk: - eastAsianLineBreaks: false - eastAsianLineBreaksStyle: simple - enable: false - escapedSpace: false - definitionList: true - extras: - delete: - enable: false - insert: - enable: false - mark: - enable: false - subscript: - enable: false - superscript: - enable: false - footnote: true - linkify: true - linkifyProtocol: https - passthrough: - delimiters: - block: [] - inline: [] - enable: false - strikethrough: true - table: true - taskList: true - typographer: - apostrophe: '’' - disable: false - ellipsis: '…' - emDash: '—' - enDash: '–' - leftAngleQuote: '«' - leftDoubleQuote: '“' - leftSingleQuote: '‘' - rightAngleQuote: '»' - rightDoubleQuote: '”' - rightSingleQuote: '’' - parser: - attribute: - block: false - title: true - autoDefinitionTermID: false - autoHeadingID: true - autoIDType: github - wrapStandAloneImageWithinParagraph: true - renderHooks: - image: - enableDefault: false - link: - enableDefault: false - renderer: - hardWraps: false - unsafe: false - xhtml: false - highlight: - anchorLineNos: false - codeFences: true - guessSyntax: false - hl_Lines: "" - hl_inline: false - lineAnchors: "" - lineNoStart: 1 - lineNos: false - lineNumbersInTable: true - noClasses: true - style: monokai - tabWidth: 4 - wrapperClass: highlight - tableOfContents: - endLevel: 3 - ordered: false - startLevel: 2 - mediaTypes: - application/json: - delimiter: . - suffixes: - - json - application/manifest+json: - delimiter: . - suffixes: - - webmanifest - application/octet-stream: - delimiter: . - application/pdf: - delimiter: . - suffixes: - - pdf - application/rss+xml: - delimiter: . - suffixes: - - xml - - rss - application/toml: - delimiter: . - suffixes: - - toml - application/wasm: - delimiter: . - suffixes: - - wasm - application/xml: - delimiter: . - suffixes: - - xml - application/yaml: - delimiter: . - suffixes: - - yaml - - yml - font/otf: - delimiter: . - suffixes: - - otf - font/ttf: - delimiter: . - suffixes: - - ttf - image/bmp: - delimiter: . - suffixes: - - bmp - image/gif: - delimiter: . - suffixes: - - gif - image/jpeg: - delimiter: . - suffixes: - - jpg - - jpeg - - jpe - - jif - - jfif - image/png: - delimiter: . - suffixes: - - png - image/svg+xml: - delimiter: . - suffixes: - - svg - image/tiff: - delimiter: . - suffixes: - - tif - - tiff - image/webp: - delimiter: . - suffixes: - - webp - text/asciidoc: - delimiter: . - suffixes: - - adoc - - asciidoc - - ad - text/calendar: - delimiter: . - suffixes: - - ics - text/css: - delimiter: . - suffixes: - - css - text/csv: - delimiter: . - suffixes: - - csv - text/html: - delimiter: . - suffixes: - - html - - htm - text/javascript: - delimiter: . - suffixes: - - js - - jsm - - mjs - text/jsx: - delimiter: . - suffixes: - - jsx - text/markdown: - delimiter: . - suffixes: - - md - - mdown - - markdown - text/org: - delimiter: . - suffixes: - - org - text/pandoc: - delimiter: . - suffixes: - - pandoc - - pdc - text/plain: - delimiter: . - suffixes: - - txt - text/rst: - delimiter: . - suffixes: - - rst - text/tsx: - delimiter: . - suffixes: - - tsx - text/typescript: - delimiter: . - suffixes: - - ts - text/x-sass: - delimiter: . - suffixes: - - sass - text/x-scss: - delimiter: . - suffixes: - - scss - video/3gpp: - delimiter: . - suffixes: - - 3gpp - - 3gp - video/mp4: - delimiter: . - suffixes: - - mp4 - video/mpeg: - delimiter: . - suffixes: - - mpg - - mpeg - video/ogg: - delimiter: . - suffixes: - - ogv - video/webm: - delimiter: . - suffixes: - - webm - video/x-msvideo: - delimiter: . - suffixes: - - avi - menus: {} - minify: - disableCSS: false - disableHTML: false - disableJS: false - disableJSON: false - disableSVG: false - disableXML: false - minifyOutput: false - tdewolff: - css: - inline: false - keepCSS2: true - precision: 0 - html: - keepComments: false - keepConditionalComments: false - keepDefaultAttrVals: true - keepDocumentTags: true - keepEndTags: true - keepQuotes: false - keepSpecialComments: true - keepWhitespace: false - templateDelims: - - "" - - "" - js: - keepVarNames: false - precision: 0 - version: 2022 - json: - keepNumbers: false - precision: 0 - svg: - inline: false - keepComments: false - precision: 0 - xml: - keepWhitespace: false - module: - auth: "" - hugoVersion: - extended: false - max: "" - min: "" - imports: null - mounts: - - disableWatch: false - excludeFiles: null - includeFiles: null - lang: "" - source: content - target: content - - disableWatch: false - excludeFiles: null - includeFiles: null - lang: "" - source: data - target: data - - disableWatch: false - excludeFiles: null - includeFiles: null - lang: "" - source: layouts - target: layouts - - disableWatch: false - excludeFiles: null - includeFiles: null - lang: "" - source: i18n - target: i18n - - disableWatch: false - excludeFiles: null - includeFiles: null - lang: "" - source: archetypes - target: archetypes - - disableWatch: false - excludeFiles: null - includeFiles: null - lang: "" - source: assets - target: assets - - disableWatch: false - excludeFiles: null - includeFiles: null - lang: "" - source: static - target: static - noProxy: none - noVendor: "" - params: null - private: '*.*' - proxy: direct - replacements: null - vendorClosest: false - workspace: "off" - newContentEditor: "" - noBuildLock: false - noChmod: false - noTimes: false - outputFormats: - amp: - baseName: index - isHTML: true - isPlainText: false - mediaType: text/html - noUgly: false - notAlternative: false - path: amp - permalinkable: true - protocol: "" - rel: amphtml - root: false - ugly: false - weight: 0 - calendar: - baseName: index - isHTML: false - isPlainText: true - mediaType: text/calendar - noUgly: false - notAlternative: false - path: "" - permalinkable: false - protocol: webcal:// - rel: alternate - root: false - ugly: false - weight: 0 - css: - baseName: styles - isHTML: false - isPlainText: true - mediaType: text/css - noUgly: false - notAlternative: true - path: "" - permalinkable: false - protocol: "" - rel: stylesheet - root: false - ugly: false - weight: 0 - csv: - baseName: index - isHTML: false - isPlainText: true - mediaType: text/csv - noUgly: false - notAlternative: false - path: "" - permalinkable: false - protocol: "" - rel: alternate - root: false - ugly: false - weight: 0 - html: - baseName: index - isHTML: true - isPlainText: false - mediaType: text/html - noUgly: false - notAlternative: false - path: "" - permalinkable: true - protocol: "" - rel: canonical - root: false - ugly: false - weight: 10 - json: - baseName: index - isHTML: false - isPlainText: true - mediaType: application/json - noUgly: false - notAlternative: false - path: "" - permalinkable: false - protocol: "" - rel: alternate - root: false - ugly: false - weight: 0 - markdown: - baseName: index - isHTML: false - isPlainText: true - mediaType: text/markdown - noUgly: false - notAlternative: false - path: "" - permalinkable: false - protocol: "" - rel: alternate - root: false - ugly: false - weight: 0 - robots: - baseName: robots - isHTML: false - isPlainText: true - mediaType: text/plain - noUgly: false - notAlternative: false - path: "" - permalinkable: false - protocol: "" - rel: alternate - root: true - ugly: false - weight: 0 - rss: - baseName: index - isHTML: false - isPlainText: false - mediaType: application/rss+xml - noUgly: true - notAlternative: false - path: "" - permalinkable: false - protocol: "" - rel: alternate - root: false - ugly: false - weight: 0 - sitemap: - baseName: sitemap - isHTML: false - isPlainText: false - mediaType: application/xml - noUgly: false - notAlternative: false - path: "" - permalinkable: false - protocol: "" - rel: sitemap - root: false - ugly: true - weight: 0 - webappmanifest: - baseName: manifest - isHTML: false - isPlainText: true - mediaType: application/manifest+json - noUgly: false - notAlternative: true - path: "" - permalinkable: false - protocol: "" - rel: manifest - root: false - ugly: false - weight: 0 - outputs: - home: - - html - - rss - page: - - html - rss: - - rss - section: - - html - - rss - taxonomy: - - html - - rss - term: - - html - - rss - page: - nextPrevInSectionSortOrder: desc - nextPrevSortOrder: desc - paginate: 0 - paginatePath: "" - pagination: - disableAliases: false - pagerSize: 10 - path: page - panicOnWarning: false - params: {} - permalinks: - page: {} - section: {} - taxonomy: {} - term: {} - pluralizeListTitles: true - printI18nWarnings: false - printPathWarnings: false - printUnusedTemplates: false - privacy: - disqus: - disable: false - googleAnalytics: - disable: false - respectDoNotTrack: false - instagram: - disable: false - simple: false - twitter: - disable: false - enableDNT: false - simple: false - vimeo: - disable: false - enableDNT: false - simple: false - x: - disable: false - enableDNT: false - simple: false - youTube: - disable: false - privacyEnhanced: false - publishDir: public - refLinksErrorLevel: "" - refLinksNotFoundURL: "" - related: - includeNewer: false - indices: - - applyFilter: false - cardinalityThreshold: 0 - name: keywords - pattern: "" - toLower: false - type: basic - weight: 100 - - applyFilter: false - cardinalityThreshold: 0 - name: date - pattern: "" - toLower: false - type: basic - weight: 10 - - applyFilter: false - cardinalityThreshold: 0 - name: tags - pattern: "" - toLower: false - type: basic - weight: 80 - threshold: 80 - toLower: false - relativeURLs: false - removePathAccents: false - renderSegments: null - resourceDir: resources - sectionPagesMenu: "" - security: - enableInlineShortcodes: false - exec: - allow: - - ^(dart-)?sass(-embedded)?$ - - ^go$ - - ^git$ - - ^npx$ - - ^postcss$ - - ^tailwindcss$ - osEnv: - - (?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\w+|(XDG_CONFIG_)?HOME|USERPROFILE|SSH_AUTH_SOCK|DISPLAY|LANG|SYSTEMDRIVE)$ - funcs: - getenv: - - ^HUGO_ - - ^CI$ - http: - mediaTypes: null - methods: - - (?i)GET|POST - urls: - - .* - segments: {} - server: - headers: null - redirects: - - force: false - from: /** - fromHeaders: null - fromRe: "" - status: 404 - to: /404.html - services: - disqus: - shortname: "" - googleAnalytics: - id: "" - instagram: - accessToken: "" - disableInlineCSS: false - rss: - limit: -1 - twitter: - disableInlineCSS: false - x: - disableInlineCSS: false - sitemap: - changeFreq: "" - disable: false - filename: sitemap.xml - priority: -1 - social: null - staticDir: - - static - staticDir0: null - staticDir1: null - staticDir2: null - staticDir3: null - staticDir4: null - staticDir5: null - staticDir6: null - staticDir7: null - staticDir8: null - staticDir9: null - staticDir10: null - summaryLength: 70 - taxonomies: - category: categories - tag: tags - templateMetrics: false - templateMetricsHints: false - theme: null - themesDir: themes - timeZone: "" - timeout: 30s - title: "" - titleCaseStyle: AP - uglyURLs: false - workingDir: "" -config_helpers: - mergeStrategy: - build: - _merge: none - caches: - _merge: none - cascade: - _merge: none - contenttypes: - _merge: none - deployment: - _merge: none - frontmatter: - _merge: none - httpcache: - _merge: none - imaging: - _merge: none - languages: - _merge: none - en: - _merge: none - menus: - _merge: shallow - params: - _merge: deep - markup: - _merge: none - mediatypes: - _merge: shallow - menus: - _merge: shallow - minify: - _merge: none - module: - _merge: none - outputformats: - _merge: shallow - outputs: - _merge: none - page: - _merge: none - pagination: - _merge: none - params: - _merge: deep - permalinks: - _merge: none - privacy: - _merge: none - related: - _merge: none - security: - _merge: none - segments: - _merge: none - server: - _merge: none - services: - _merge: none - sitemap: - _merge: none - taxonomies: - _merge: none -output: - layouts: - - Example: Single page in "posts" section - Kind: page - OutputFormat: html - Suffix: html - Template Lookup Order: - - layouts/posts/single.html.html - - layouts/posts/single.html - - layouts/_default/single.html.html - - layouts/_default/single.html - - Example: Base template for single page in "posts" section - Kind: page - OutputFormat: html - Suffix: html - Template Lookup Order: - - layouts/posts/single-baseof.html.html - - layouts/posts/baseof.html.html - - layouts/posts/single-baseof.html - - layouts/posts/baseof.html - - layouts/_default/single-baseof.html.html - - layouts/_default/baseof.html.html - - layouts/_default/single-baseof.html - - layouts/_default/baseof.html - - Example: Single page in "posts" section with layout set to "demolayout" - Kind: page - OutputFormat: html - Suffix: html - Template Lookup Order: - - layouts/posts/demolayout.html.html - - layouts/posts/single.html.html - - layouts/posts/demolayout.html - - layouts/posts/single.html - - layouts/_default/demolayout.html.html - - layouts/_default/single.html.html - - layouts/_default/demolayout.html - - layouts/_default/single.html - - Example: Base template for single page in "posts" section with layout set to "demolayout" - Kind: page - OutputFormat: html - Suffix: html - Template Lookup Order: - - layouts/posts/demolayout-baseof.html.html - - layouts/posts/single-baseof.html.html - - layouts/posts/baseof.html.html - - layouts/posts/demolayout-baseof.html - - layouts/posts/single-baseof.html - - layouts/posts/baseof.html - - layouts/_default/demolayout-baseof.html.html - - layouts/_default/single-baseof.html.html - - layouts/_default/baseof.html.html - - layouts/_default/demolayout-baseof.html - - layouts/_default/single-baseof.html - - layouts/_default/baseof.html - - Example: AMP single page in "posts" section - Kind: page - OutputFormat: amp - Suffix: html - Template Lookup Order: - - layouts/posts/single.amp.html - - layouts/posts/single.html - - layouts/_default/single.amp.html - - layouts/_default/single.html - - Example: AMP single page in "posts" section, French language - Kind: page - OutputFormat: amp - Suffix: html - Template Lookup Order: - - layouts/posts/single.fr.amp.html - - layouts/posts/single.amp.html - - layouts/posts/single.fr.html - - layouts/posts/single.html - - layouts/_default/single.fr.amp.html - - layouts/_default/single.amp.html - - layouts/_default/single.fr.html - - layouts/_default/single.html - - Example: Home page - Kind: home - OutputFormat: html - Suffix: html - Template Lookup Order: - - layouts/index.html.html - - layouts/home.html.html - - layouts/list.html.html - - layouts/index.html - - layouts/home.html - - layouts/list.html - - layouts/_default/index.html.html - - layouts/_default/home.html.html - - layouts/_default/list.html.html - - layouts/_default/index.html - - layouts/_default/home.html - - layouts/_default/list.html - - Example: Base template for home page - Kind: home - OutputFormat: html - Suffix: html - Template Lookup Order: - - layouts/index-baseof.html.html - - layouts/home-baseof.html.html - - layouts/list-baseof.html.html - - layouts/baseof.html.html - - layouts/index-baseof.html - - layouts/home-baseof.html - - layouts/list-baseof.html - - layouts/baseof.html - - layouts/_default/index-baseof.html.html - - layouts/_default/home-baseof.html.html - - layouts/_default/list-baseof.html.html - - layouts/_default/baseof.html.html - - layouts/_default/index-baseof.html - - layouts/_default/home-baseof.html - - layouts/_default/list-baseof.html - - layouts/_default/baseof.html - - Example: Home page with type set to "demotype" - Kind: home - OutputFormat: html - Suffix: html - Template Lookup Order: - - layouts/demotype/index.html.html - - layouts/demotype/home.html.html - - layouts/demotype/list.html.html - - layouts/demotype/index.html - - layouts/demotype/home.html - - layouts/demotype/list.html - - layouts/index.html.html - - layouts/home.html.html - - layouts/list.html.html - - layouts/index.html - - layouts/home.html - - layouts/list.html - - layouts/_default/index.html.html - - layouts/_default/home.html.html - - layouts/_default/list.html.html - - layouts/_default/index.html - - layouts/_default/home.html - - layouts/_default/list.html - - Example: Base template for home page with type set to "demotype" - Kind: home - OutputFormat: html - Suffix: html - Template Lookup Order: - - layouts/demotype/index-baseof.html.html - - layouts/demotype/home-baseof.html.html - - layouts/demotype/list-baseof.html.html - - layouts/demotype/baseof.html.html - - layouts/demotype/index-baseof.html - - layouts/demotype/home-baseof.html - - layouts/demotype/list-baseof.html - - layouts/demotype/baseof.html - - layouts/index-baseof.html.html - - layouts/home-baseof.html.html - - layouts/list-baseof.html.html - - layouts/baseof.html.html - - layouts/index-baseof.html - - layouts/home-baseof.html - - layouts/list-baseof.html - - layouts/baseof.html - - layouts/_default/index-baseof.html.html - - layouts/_default/home-baseof.html.html - - layouts/_default/list-baseof.html.html - - layouts/_default/baseof.html.html - - layouts/_default/index-baseof.html - - layouts/_default/home-baseof.html - - layouts/_default/list-baseof.html - - layouts/_default/baseof.html - - Example: Home page with layout set to "demolayout" - Kind: home - OutputFormat: html - Suffix: html - Template Lookup Order: - - layouts/demolayout.html.html - - layouts/index.html.html - - layouts/home.html.html - - layouts/list.html.html - - layouts/demolayout.html - - layouts/index.html - - layouts/home.html - - layouts/list.html - - layouts/_default/demolayout.html.html - - layouts/_default/index.html.html - - layouts/_default/home.html.html - - layouts/_default/list.html.html - - layouts/_default/demolayout.html - - layouts/_default/index.html - - layouts/_default/home.html - - layouts/_default/list.html - - Example: AMP home, French language - Kind: home - OutputFormat: amp - Suffix: html - Template Lookup Order: - - layouts/index.fr.amp.html - - layouts/home.fr.amp.html - - layouts/list.fr.amp.html - - layouts/index.amp.html - - layouts/home.amp.html - - layouts/list.amp.html - - layouts/index.fr.html - - layouts/home.fr.html - - layouts/list.fr.html - - layouts/index.html - - layouts/home.html - - layouts/list.html - - layouts/_default/index.fr.amp.html - - layouts/_default/home.fr.amp.html - - layouts/_default/list.fr.amp.html - - layouts/_default/index.amp.html - - layouts/_default/home.amp.html - - layouts/_default/list.amp.html - - layouts/_default/index.fr.html - - layouts/_default/home.fr.html - - layouts/_default/list.fr.html - - layouts/_default/index.html - - layouts/_default/home.html - - layouts/_default/list.html - - Example: JSON home - Kind: home - OutputFormat: json - Suffix: json - Template Lookup Order: - - layouts/index.json.json - - layouts/home.json.json - - layouts/list.json.json - - layouts/index.json - - layouts/home.json - - layouts/list.json - - layouts/_default/index.json.json - - layouts/_default/home.json.json - - layouts/_default/list.json.json - - layouts/_default/index.json - - layouts/_default/home.json - - layouts/_default/list.json - - Example: RSS home - Kind: home - OutputFormat: rss - Suffix: xml - Template Lookup Order: - - layouts/index.rss.xml - - layouts/home.rss.xml - - layouts/rss.xml - - layouts/list.rss.xml - - layouts/index.xml - - layouts/home.xml - - layouts/list.xml - - layouts/_default/index.rss.xml - - layouts/_default/home.rss.xml - - layouts/_default/rss.xml - - layouts/_default/list.rss.xml - - layouts/_default/index.xml - - layouts/_default/home.xml - - layouts/_default/list.xml - - layouts/_internal/_default/rss.xml - - Example: Section list for "posts" - Kind: section - OutputFormat: html - Suffix: html - Template Lookup Order: - - layouts/posts/posts.html.html - - layouts/posts/section.html.html - - layouts/posts/list.html.html - - layouts/posts/posts.html - - layouts/posts/section.html - - layouts/posts/list.html - - layouts/section/posts.html.html - - layouts/section/section.html.html - - layouts/section/list.html.html - - layouts/section/posts.html - - layouts/section/section.html - - layouts/section/list.html - - layouts/_default/posts.html.html - - layouts/_default/section.html.html - - layouts/_default/list.html.html - - layouts/_default/posts.html - - layouts/_default/section.html - - layouts/_default/list.html - - Example: Section list for "posts" with type set to "blog" - Kind: section - OutputFormat: html - Suffix: html - Template Lookup Order: - - layouts/blog/posts.html.html - - layouts/blog/section.html.html - - layouts/blog/list.html.html - - layouts/blog/posts.html - - layouts/blog/section.html - - layouts/blog/list.html - - layouts/posts/posts.html.html - - layouts/posts/section.html.html - - layouts/posts/list.html.html - - layouts/posts/posts.html - - layouts/posts/section.html - - layouts/posts/list.html - - layouts/section/posts.html.html - - layouts/section/section.html.html - - layouts/section/list.html.html - - layouts/section/posts.html - - layouts/section/section.html - - layouts/section/list.html - - layouts/_default/posts.html.html - - layouts/_default/section.html.html - - layouts/_default/list.html.html - - layouts/_default/posts.html - - layouts/_default/section.html - - layouts/_default/list.html - - Example: Section list for "posts" with layout set to "demolayout" - Kind: section - OutputFormat: html - Suffix: html - Template Lookup Order: - - layouts/posts/demolayout.html.html - - layouts/posts/posts.html.html - - layouts/posts/section.html.html - - layouts/posts/list.html.html - - layouts/posts/demolayout.html - - layouts/posts/posts.html - - layouts/posts/section.html - - layouts/posts/list.html - - layouts/section/demolayout.html.html - - layouts/section/posts.html.html - - layouts/section/section.html.html - - layouts/section/list.html.html - - layouts/section/demolayout.html - - layouts/section/posts.html - - layouts/section/section.html - - layouts/section/list.html - - layouts/_default/demolayout.html.html - - layouts/_default/posts.html.html - - layouts/_default/section.html.html - - layouts/_default/list.html.html - - layouts/_default/demolayout.html - - layouts/_default/posts.html - - layouts/_default/section.html - - layouts/_default/list.html - - Example: Section list for "posts" - Kind: section - OutputFormat: rss - Suffix: xml - Template Lookup Order: - - layouts/posts/section.rss.xml - - layouts/posts/rss.xml - - layouts/posts/list.rss.xml - - layouts/posts/section.xml - - layouts/posts/list.xml - - layouts/section/section.rss.xml - - layouts/section/rss.xml - - layouts/section/list.rss.xml - - layouts/section/section.xml - - layouts/section/list.xml - - layouts/_default/section.rss.xml - - layouts/_default/rss.xml - - layouts/_default/list.rss.xml - - layouts/_default/section.xml - - layouts/_default/list.xml - - layouts/_internal/_default/rss.xml - - Example: Taxonomy list for "categories" - Kind: taxonomy - OutputFormat: html - Suffix: html - Template Lookup Order: - - layouts/categories/category.terms.html.html - - layouts/categories/terms.html.html - - layouts/categories/taxonomy.html.html - - layouts/categories/list.html.html - - layouts/categories/category.terms.html - - layouts/categories/terms.html - - layouts/categories/taxonomy.html - - layouts/categories/list.html - - layouts/category/category.terms.html.html - - layouts/category/terms.html.html - - layouts/category/taxonomy.html.html - - layouts/category/list.html.html - - layouts/category/category.terms.html - - layouts/category/terms.html - - layouts/category/taxonomy.html - - layouts/category/list.html - - layouts/taxonomy/category.terms.html.html - - layouts/taxonomy/terms.html.html - - layouts/taxonomy/taxonomy.html.html - - layouts/taxonomy/list.html.html - - layouts/taxonomy/category.terms.html - - layouts/taxonomy/terms.html - - layouts/taxonomy/taxonomy.html - - layouts/taxonomy/list.html - - layouts/_default/category.terms.html.html - - layouts/_default/terms.html.html - - layouts/_default/taxonomy.html.html - - layouts/_default/list.html.html - - layouts/_default/category.terms.html - - layouts/_default/terms.html - - layouts/_default/taxonomy.html - - layouts/_default/list.html - - Example: Taxonomy list for "categories" - Kind: taxonomy - OutputFormat: rss - Suffix: xml - Template Lookup Order: - - layouts/categories/category.terms.rss.xml - - layouts/categories/terms.rss.xml - - layouts/categories/taxonomy.rss.xml - - layouts/categories/rss.xml - - layouts/categories/list.rss.xml - - layouts/categories/category.terms.xml - - layouts/categories/terms.xml - - layouts/categories/taxonomy.xml - - layouts/categories/list.xml - - layouts/category/category.terms.rss.xml - - layouts/category/terms.rss.xml - - layouts/category/taxonomy.rss.xml - - layouts/category/rss.xml - - layouts/category/list.rss.xml - - layouts/category/category.terms.xml - - layouts/category/terms.xml - - layouts/category/taxonomy.xml - - layouts/category/list.xml - - layouts/taxonomy/category.terms.rss.xml - - layouts/taxonomy/terms.rss.xml - - layouts/taxonomy/taxonomy.rss.xml - - layouts/taxonomy/rss.xml - - layouts/taxonomy/list.rss.xml - - layouts/taxonomy/category.terms.xml - - layouts/taxonomy/terms.xml - - layouts/taxonomy/taxonomy.xml - - layouts/taxonomy/list.xml - - layouts/_default/category.terms.rss.xml - - layouts/_default/terms.rss.xml - - layouts/_default/taxonomy.rss.xml - - layouts/_default/rss.xml - - layouts/_default/list.rss.xml - - layouts/_default/category.terms.xml - - layouts/_default/terms.xml - - layouts/_default/taxonomy.xml - - layouts/_default/list.xml - - layouts/_internal/_default/rss.xml - - Example: Term list for "categories" - Kind: term - OutputFormat: html - Suffix: html - Template Lookup Order: - - layouts/categories/term.html.html - - layouts/categories/category.html.html - - layouts/categories/taxonomy.html.html - - layouts/categories/list.html.html - - layouts/categories/term.html - - layouts/categories/category.html - - layouts/categories/taxonomy.html - - layouts/categories/list.html - - layouts/term/term.html.html - - layouts/term/category.html.html - - layouts/term/taxonomy.html.html - - layouts/term/list.html.html - - layouts/term/term.html - - layouts/term/category.html - - layouts/term/taxonomy.html - - layouts/term/list.html - - layouts/taxonomy/term.html.html - - layouts/taxonomy/category.html.html - - layouts/taxonomy/taxonomy.html.html - - layouts/taxonomy/list.html.html - - layouts/taxonomy/term.html - - layouts/taxonomy/category.html - - layouts/taxonomy/taxonomy.html - - layouts/taxonomy/list.html - - layouts/category/term.html.html - - layouts/category/category.html.html - - layouts/category/taxonomy.html.html - - layouts/category/list.html.html - - layouts/category/term.html - - layouts/category/category.html - - layouts/category/taxonomy.html - - layouts/category/list.html - - layouts/_default/term.html.html - - layouts/_default/category.html.html - - layouts/_default/taxonomy.html.html - - layouts/_default/list.html.html - - layouts/_default/term.html - - layouts/_default/category.html - - layouts/_default/taxonomy.html - - layouts/_default/list.html - - Example: Term list for "categories" - Kind: term - OutputFormat: rss - Suffix: xml - Template Lookup Order: - - layouts/categories/term.rss.xml - - layouts/categories/category.rss.xml - - layouts/categories/taxonomy.rss.xml - - layouts/categories/rss.xml - - layouts/categories/list.rss.xml - - layouts/categories/term.xml - - layouts/categories/category.xml - - layouts/categories/taxonomy.xml - - layouts/categories/list.xml - - layouts/term/term.rss.xml - - layouts/term/category.rss.xml - - layouts/term/taxonomy.rss.xml - - layouts/term/rss.xml - - layouts/term/list.rss.xml - - layouts/term/term.xml - - layouts/term/category.xml - - layouts/term/taxonomy.xml - - layouts/term/list.xml - - layouts/taxonomy/term.rss.xml - - layouts/taxonomy/category.rss.xml - - layouts/taxonomy/taxonomy.rss.xml - - layouts/taxonomy/rss.xml - - layouts/taxonomy/list.rss.xml - - layouts/taxonomy/term.xml - - layouts/taxonomy/category.xml - - layouts/taxonomy/taxonomy.xml - - layouts/taxonomy/list.xml - - layouts/category/term.rss.xml - - layouts/category/category.rss.xml - - layouts/category/taxonomy.rss.xml - - layouts/category/rss.xml - - layouts/category/list.rss.xml - - layouts/category/term.xml - - layouts/category/category.xml - - layouts/category/taxonomy.xml - - layouts/category/list.xml - - layouts/_default/term.rss.xml - - layouts/_default/category.rss.xml - - layouts/_default/taxonomy.rss.xml - - layouts/_default/rss.xml - - layouts/_default/list.rss.xml - - layouts/_default/term.xml - - layouts/_default/category.xml - - layouts/_default/taxonomy.xml - - layouts/_default/list.xml - - layouts/_internal/_default/rss.xml -tpl: - funcs: - cast: - ToFloat: - Aliases: - - float - Args: - - v - Description: ToFloat converts v to a float. - Examples: - - - '{{ "1234" | float | printf "%T" }}' - - float64 - ToInt: - Aliases: - - int - Args: - - v - Description: ToInt converts v to an int. - Examples: - - - '{{ "1234" | int | printf "%T" }}' - - int - ToString: - Aliases: - - string - Args: - - v - Description: ToString converts v to a string. - Examples: - - - '{{ 1234 | string | printf "%T" }}' - - string - collections: - After: - Aliases: - - after - Args: - - "n" - - l - Description: After returns all the items after the first n items in list l. - Examples: [] - Append: - Aliases: - - append - Args: - - args - Description: "Append appends args up to the last one to the slice in the last - argument.\nThis construct allows template constructs like this:\n\n\t{{ - $pages = $pages | append $p2 $p1 }}\n\nNote that with 2 arguments where - both are slices of the same type,\nthe first slice will be appended to the - second:\n\n\t{{ $pages = $pages | append .Site.RegularPages }}" - Examples: [] - Apply: - Aliases: - - apply - Args: - - ctx - - c - - fname - - args - Description: Apply takes an array or slice c and returns a new slice with - the function fname applied over it. - Examples: [] - Complement: - Aliases: - - complement - Args: - - ls - Description: "Complement gives the elements in the last element of ls that - are not in\nany of the others.\n\nAll elements of ls must be slices or arrays - of comparable types.\n\nThe reasoning behind this rather clumsy API is so - we can do this in the templates:\n\n\t{{ $c := .Pages | complement $last4 - }}" - Examples: - - - '{{ slice "a" "b" "c" "d" "e" "f" | complement (slice "b" "c") (slice - "d" "e") }}' - - '[a f]' - Delimit: - Aliases: - - delimit - Args: - - ctx - - l - - sep - - last - Description: |- - Delimit takes a given list l and returns a string delimited by sep. - If last is passed to the function, it will be used as the final delimiter. - Examples: - - - '{{ delimit (slice "A" "B" "C") ", " " and " }}' - - A, B and C - Dictionary: - Aliases: - - dict - Args: - - values - Description: |- - Dictionary creates a new map from the given parameters by - treating values as key-value pairs. The number of values must be even. - The keys can be string slices, which will create the needed nested structure. - Examples: [] - First: - Aliases: - - first - Args: - - limit - - l - Description: First returns the first limit items in list l. - Examples: [] - Group: - Aliases: - - group - Args: - - key - - items - Description: |- - Group groups a set of items by the given key. - This is currently only supported for Pages. - Examples: [] - In: - Aliases: - - in - Args: - - l - - v - Description: In returns whether v is in the list l. l may be an array or - slice. - Examples: - - - '{{ if in "this string contains a substring" "substring" }}Substring found!{{ - end }}' - - Substring found! - Index: - Aliases: - - index - Args: - - item - - args - Description: |- - Index returns the result of indexing its first argument by the following - arguments. Thus "index x 1 2 3" is, in Go syntax, x[1][2][3]. Each - indexed item must be a map, slice, or array. - - Adapted from Go stdlib src/text/template/funcs.go. - - We deviate from the stdlib mostly because of https://github.com/golang/go/issues/14751. - Examples: [] - Intersect: - Aliases: - - intersect - Args: - - l1 - - l2 - Description: |- - Intersect returns the common elements in the given sets, l1 and l2. l1 and - l2 must be of the same type and may be either arrays or slices. - Examples: [] - IsSet: - Aliases: - - isSet - - isset - Args: - - c - - key - Description: |- - IsSet returns whether a given array, channel, slice, or map in c has the given key - defined. - Examples: [] - KeyVals: - Aliases: - - keyVals - Args: - - key - - values - Description: KeyVals creates a key and values wrapper. - Examples: - - - '{{ keyVals "key" "a" "b" }}' - - 'key: [a b]' - Last: - Aliases: - - last - Args: - - limit - - l - Description: Last returns the last limit items in the list l. - Examples: [] - Merge: - Aliases: - - merge - Args: - - params - Description: |- - Merge creates a copy of the final parameter in params and merges the preceding - parameters into it in reverse order. - - Currently only maps are supported. Key handling is case insensitive. - Examples: - - - '{{ dict "title" "Hugo Rocks!" | collections.Merge (dict "title" "Default - Title" "description" "Yes, Hugo Rocks!") | sort }}' - - '[Yes, Hugo Rocks! Hugo Rocks!]' - - - '{{ merge (dict "title" "Default Title" "description" "Yes, Hugo Rocks!") - (dict "title" "Hugo Rocks!") | sort }}' - - '[Yes, Hugo Rocks! Hugo Rocks!]' - - - '{{ merge (dict "title" "Default Title" "description" "Yes, Hugo Rocks!") - (dict "title" "Hugo Rocks!") (dict "extra" "For reals!") | sort }}' - - '[Yes, Hugo Rocks! For reals! Hugo Rocks!]' - NewScratch: - Aliases: - - newScratch - Args: null - Description: |- - NewScratch creates a new Scratch which can be used to store values in a - thread safe way. - Examples: - - - '{{ $scratch := newScratch }}{{ $scratch.Add "b" 2 }}{{ $scratch.Add "b" - 2 }}{{ $scratch.Get "b" }}' - - "4" - Querify: - Aliases: - - querify - Args: - - params - Description: |- - Querify returns a URL query string composed of the given key-value pairs, - encoded and sorted by key. - Examples: - - - '{{ (querify "foo" 1 "bar" 2 "baz" "with spaces" "qux" "this&that=those") - | safeHTML }}' - - bar=2&baz=with+spaces&foo=1&qux=this%26that%3Dthose - - - Search - - Search - - - '{{ slice "foo" 1 "bar" 2 | querify | safeHTML }}' - - bar=2&foo=1 - Reverse: - Aliases: null - Args: null - Description: "" - Examples: null - Seq: - Aliases: - - seq - Args: - - args - Description: "Seq creates a sequence of integers from args. It's named and - used as GNU's seq.\n\nExamples:\n\n\t3 => 1, 2, 3\n\t1 2 4 => 1, 3\n\t-3 - => -1, -2, -3\n\t1 4 => 1, 2, 3, 4\n\t1 -2 => 1, 0, -1, -2" - Examples: - - - '{{ seq 3 }}' - - '[1 2 3]' - Shuffle: - Aliases: - - shuffle - Args: - - l - Description: Shuffle returns list l in a randomized order. - Examples: [] - Slice: - Aliases: - - slice - Args: - - args - Description: Slice returns a slice of all passed arguments. - Examples: - - - '{{ slice "B" "C" "A" | sort }}' - - '[A B C]' - Sort: - Aliases: - - sort - Args: - - ctx - - l - - args - Description: Sort returns a sorted copy of the list l. - Examples: [] - SymDiff: - Aliases: - - symdiff - Args: - - s2 - - s1 - Description: |- - SymDiff returns the symmetric difference of s1 and s2. - Arguments must be either a slice or an array of comparable types. - Examples: - - - '{{ slice 1 2 3 | symdiff (slice 3 4) }}' - - '[1 2 4]' - Union: - Aliases: - - union - Args: - - l1 - - l2 - Description: |- - Union returns the union of the given sets, l1 and l2. l1 and - l2 must be of the same type and may be either arrays or slices. - If l1 and l2 aren't of the same type then l1 will be returned. - If either l1 or l2 is nil then the non-nil list will be returned. - Examples: - - - '{{ union (slice 1 2 3) (slice 3 4 5) }}' - - '[1 2 3 4 5]' - Uniq: - Aliases: - - uniq - Args: - - l - Description: Uniq returns a new list with duplicate elements in the list l - removed. - Examples: - - - '{{ slice 1 2 3 2 | uniq }}' - - '[1 2 3]' - Where: - Aliases: - - where - Args: - - ctx - - c - - key - - args - Description: Where returns a filtered subset of collection c. - Examples: [] - compare: - Conditional: - Aliases: - - cond - Args: - - cond - - v1 - - v2 - Description: |- - Conditional can be used as a ternary operator. - - It returns v1 if cond is true, else v2. - Examples: - - - '{{ cond (eq (add 2 2) 4) "2+2 is 4" "what?" | safeHTML }}' - - 2+2 is 4 - Default: - Aliases: - - default - Args: - - defaultv - - givenv - Description: |- - Default checks whether a givenv is set and returns the default value defaultv if it - is not. "Set" in this context means non-zero for numeric types and times; - non-zero length for strings, arrays, slices, and maps; - any boolean or struct value; or non-nil for any other types. - Examples: - - - '{{ "Hugo Rocks!" | default "Hugo Rules!" }}' - - Hugo Rocks! - - - '{{ "" | default "Hugo Rules!" }}' - - Hugo Rules! - Eq: - Aliases: - - eq - Args: - - first - - others - Description: Eq returns the boolean truth of arg1 == arg2 || arg1 == arg3 - || arg1 == arg4. - Examples: - - - '{{ if eq .Section "blog" }}current-section{{ end }}' - - current-section - Ge: - Aliases: - - ge - Args: - - first - - others - Description: Ge returns the boolean truth of arg1 >= arg2 && arg1 >= arg3 - && arg1 >= arg4. - Examples: - - - '{{ if ge hugo.Version "0.80" }}Reasonable new Hugo version!{{ end }}' - - Reasonable new Hugo version! - Gt: - Aliases: - - gt - Args: - - first - - others - Description: Gt returns the boolean truth of arg1 > arg2 && arg1 > arg3 && - arg1 > arg4. - Examples: [] - Le: - Aliases: - - le - Args: - - first - - others - Description: Le returns the boolean truth of arg1 <= arg2 && arg1 <= arg3 - && arg1 <= arg4. - Examples: [] - Lt: - Aliases: - - lt - Args: - - first - - others - Description: Lt returns the boolean truth of arg1 < arg2 && arg1 < arg3 && - arg1 < arg4. - Examples: [] - LtCollate: - Aliases: null - Args: null - Description: "" - Examples: null - Ne: - Aliases: - - ne - Args: - - first - - others - Description: Ne returns the boolean truth of arg1 != arg2 && arg1 != arg3 - && arg1 != arg4. - Examples: [] - crypto: - FNV32a: - Aliases: null - Args: null - Description: "" - Examples: null - HMAC: - Aliases: - - hmac - Args: - - h - - k - - m - - e - Description: HMAC returns a cryptographic hash that uses a key to sign a message. - Examples: - - - '{{ hmac "sha256" "Secret key" "Hello world, gophers!" }}' - - b6d11b6c53830b9d87036272ca9fe9d19306b8f9d8aa07b15da27d89e6e34f40 - MD5: - Aliases: - - md5 - Args: - - v - Description: MD5 hashes the v and returns its MD5 checksum. - Examples: - - - '{{ md5 "Hello world, gophers!" }}' - - b3029f756f98f79e7f1b7f1d1f0dd53b - - - '{{ crypto.MD5 "Hello world, gophers!" }}' - - b3029f756f98f79e7f1b7f1d1f0dd53b - SHA1: - Aliases: - - sha1 - Args: - - v - Description: SHA1 hashes v and returns its SHA1 checksum. - Examples: - - - '{{ sha1 "Hello world, gophers!" }}' - - c8b5b0e33d408246e30f53e32b8f7627a7a649d4 - SHA256: - Aliases: - - sha256 - Args: - - v - Description: SHA256 hashes v and returns its SHA256 checksum. - Examples: - - - '{{ sha256 "Hello world, gophers!" }}' - - 6ec43b78da9669f50e4e422575c54bf87536954ccd58280219c393f2ce352b46 - css: - PostCSS: - Aliases: - - postCSS - Args: - - args - Description: PostCSS processes the given Resource with PostCSS. - Examples: [] - Quoted: - Aliases: null - Args: null - Description: "" - Examples: null - Sass: - Aliases: - - toCSS - Args: - - args - Description: Sass processes the given Resource with SASS. - Examples: [] - TailwindCSS: - Aliases: null - Args: null - Description: "" - Examples: null - Unquoted: - Aliases: null - Args: null - Description: "" - Examples: null - data: - GetCSV: - Aliases: - - getCSV - Args: - - sep - - args - Description: |- - GetCSV expects the separator sep and one or n-parts of a URL to a resource which - can either be a local or a remote one. - The data separator can be a comma, semi-colon, pipe, etc, but only one character. - If you provide multiple parts for the URL they will be joined together to the final URL. - GetCSV returns nil or a slice slice to use in a short code. - Examples: [] - GetJSON: - Aliases: - - getJSON - Args: - - args - Description: |- - GetJSON expects one or n-parts of a URL in args to a resource which can either be a local or a remote one. - If you provide multiple parts they will be joined together to the final URL. - GetJSON returns nil or parsed JSON to use in a short code. - Examples: [] - debug: - Dump: - Aliases: null - Args: - - val - Description: |- - Dump returns a object dump of val as a string. - Note that not every value passed to Dump will print so nicely, but - we'll improve on that. - - We recommend using the "go" Chroma lexer to format the output - nicely. - - Also note that the output from Dump may change from Hugo version to the next, - so don't depend on a specific output. - Examples: - - - |- - {{ $m := newScratch }} - {{ $m.Set "Hugo" "Rocks!" }} - {{ $m.Values | debug.Dump | safeHTML }} - - |- - { - "Hugo": "Rocks!" - } - TestDeprecationErr: - Aliases: null - Args: null - Description: "" - Examples: null - TestDeprecationInfo: - Aliases: null - Args: null - Description: "" - Examples: null - TestDeprecationWarn: - Aliases: null - Args: null - Description: "" - Examples: null - Timer: - Aliases: null - Args: null - Description: "" - Examples: null - VisualizeSpaces: - Aliases: null - Args: null - Description: "" - Examples: null - diagrams: - Goat: - Aliases: null - Args: null - Description: "" - Examples: null - encoding: - Base64Decode: - Aliases: - - base64Decode - Args: - - content - Description: Base64Decode returns the base64 decoding of the given content. - Examples: - - - '{{ "SGVsbG8gd29ybGQ=" | base64Decode }}' - - Hello world - - - '{{ 42 | base64Encode | base64Decode }}' - - "42" - Base64Encode: - Aliases: - - base64Encode - Args: - - content - Description: Base64Encode returns the base64 encoding of the given content. - Examples: - - - '{{ "Hello world" | base64Encode }}' - - SGVsbG8gd29ybGQ= - Jsonify: - Aliases: - - jsonify - Args: - - args - Description: |- - Jsonify encodes a given object to JSON. To pretty print the JSON, pass a map - or dictionary of options as the first value in args. Supported options are - "prefix" and "indent". Each JSON element in the output will begin on a new - line beginning with prefix followed by one or more copies of indent according - to the indentation nesting. - Examples: - - - '{{ (slice "A" "B" "C") | jsonify }}' - - '["A","B","C"]' - - - '{{ (slice "A" "B" "C") | jsonify (dict "indent" " ") }}' - - |- - [ - "A", - "B", - "C" - ] - fmt: - Errorf: - Aliases: - - errorf - Args: - - format - - args - Description: |- - Errorf formats args according to a format specifier and logs an ERROR. - It returns an empty string. - Examples: - - - '{{ errorf "%s." "failed" }}' - - "" - Erroridf: - Aliases: - - erroridf - Args: - - id - - format - - args - Description: |- - Erroridf formats args according to a format specifier and logs an ERROR and - an information text that the error with the given id can be suppressed in config. - It returns an empty string. - Examples: - - - '{{ erroridf "my-err-id" "%s." "failed" }}' - - "" - Errormf: - Aliases: null - Args: null - Description: "" - Examples: null - Print: - Aliases: - - print - Args: - - args - Description: Print returns a string representation of args. - Examples: - - - '{{ print "works!" }}' - - works! - Printf: - Aliases: - - printf - Args: - - format - - args - Description: Printf returns string representation of args formatted with the - layout in format. - Examples: - - - '{{ printf "%s!" "works" }}' - - works! - Println: - Aliases: - - println - Args: - - args - Description: Println returns string representation of args ending with a - newline. - Examples: - - - '{{ println "works!" }}' - - | - works! - Warnf: - Aliases: - - warnf - Args: - - format - - args - Description: |- - Warnf formats args according to a format specifier and logs a WARNING. - It returns an empty string. - Examples: - - - '{{ warnf "%s." "warning" }}' - - "" - Warnidf: - Aliases: - - warnidf - Args: - - id - - format - - args - Description: |- - Warnidf formats args according to a format specifier and logs an WARNING and - an information text that the warning with the given id can be suppressed in config. - It returns an empty string. - Examples: - - - '{{ warnidf "my-warn-id" "%s." "warning" }}' - - "" - Warnmf: - Aliases: null - Args: null - Description: "" - Examples: null - hash: - FNV32a: - Aliases: null - Args: - - v - Description: FNV32a hashes v using fnv32a algorithm. - Examples: - - - '{{ hash.FNV32a "Hugo Rocks!!" }}' - - "1515779328" - XxHash: - Aliases: - - xxhash - Args: - - v - Description: XxHash returns the xxHash of the input string. - Examples: - - - '{{ hash.XxHash "The quick brown fox jumps over the lazy dog" }}' - - 0b242d361fda71bc - hugo: - Deps: - Aliases: null - Args: null - Description: "" - Examples: null - Generator: - Aliases: null - Args: null - Description: "" - Examples: null - IsDevelopment: - Aliases: null - Args: null - Description: "" - Examples: null - IsExtended: - Aliases: null - Args: null - Description: "" - Examples: null - IsMultiHost: - Aliases: null - Args: null - Description: "" - Examples: null - IsMultihost: - Aliases: null - Args: null - Description: "" - Examples: null - IsMultilingual: - Aliases: null - Args: null - Description: "" - Examples: null - IsProduction: - Aliases: null - Args: null - Description: "" - Examples: null - IsServer: - Aliases: null - Args: null - Description: "" - Examples: null - Store: - Aliases: null - Args: null - Description: "" - Examples: null - Version: - Aliases: null - Args: null - Description: "" - Examples: null - WorkingDir: - Aliases: null - Args: null - Description: "" - Examples: null - images: - AutoOrient: - Aliases: null - Args: null - Description: "" - Examples: null - Brightness: - Aliases: null - Args: null - Description: "" - Examples: null - ColorBalance: - Aliases: null - Args: null - Description: "" - Examples: null - Colorize: - Aliases: null - Args: null - Description: "" - Examples: null - Config: - Aliases: - - imageConfig - Args: - - path - Description: |- - Config returns the image.Config for the specified path relative to the - working directory. - Examples: [] - Contrast: - Aliases: null - Args: null - Description: "" - Examples: null - Dither: - Aliases: null - Args: null - Description: "" - Examples: null - Filter: - Aliases: null - Args: null - Description: "" - Examples: null - Gamma: - Aliases: null - Args: null - Description: "" - Examples: null - GaussianBlur: - Aliases: null - Args: null - Description: "" - Examples: null - Grayscale: - Aliases: null - Args: null - Description: "" - Examples: null - Hue: - Aliases: null - Args: null - Description: "" - Examples: null - Invert: - Aliases: null - Args: null - Description: "" - Examples: null - Mask: - Aliases: null - Args: null - Description: "" - Examples: null - Opacity: - Aliases: null - Args: null - Description: "" - Examples: null - Overlay: - Aliases: null - Args: null - Description: "" - Examples: null - Padding: - Aliases: null - Args: null - Description: "" - Examples: null - Pixelate: - Aliases: null - Args: null - Description: "" - Examples: null - Process: - Aliases: null - Args: null - Description: "" - Examples: null - QR: - Aliases: null - Args: null - Description: "" - Examples: null - Saturation: - Aliases: null - Args: null - Description: "" - Examples: null - Sepia: - Aliases: null - Args: null - Description: "" - Examples: null - Sigmoid: - Aliases: null - Args: null - Description: "" - Examples: null - Text: - Aliases: null - Args: null - Description: "" - Examples: null - UnsharpMask: - Aliases: null - Args: null - Description: "" - Examples: null - inflect: - Humanize: - Aliases: - - humanize - Args: - - v - Description: |- - Humanize returns the humanized form of v. - - If v is either an integer or a string containing an integer - value, the behavior is to add the appropriate ordinal. - Examples: - - - '{{ humanize "my-first-post" }}' - - My first post - - - '{{ humanize "myCamelPost" }}' - - My camel post - - - '{{ humanize "52" }}' - - 52nd - - - '{{ humanize 103 }}' - - 103rd - Pluralize: - Aliases: - - pluralize - Args: - - v - Description: Pluralize returns the plural form of the single word in v. - Examples: - - - '{{ "cat" | pluralize }}' - - cats - Singularize: - Aliases: - - singularize - Args: - - v - Description: Singularize returns the singular form of a single word in v. - Examples: - - - '{{ "cats" | singularize }}' - - cat - js: - Babel: - Aliases: - - babel - Args: - - args - Description: Babel processes the given Resource with Babel. - Examples: [] - Batch: - Aliases: null - Args: null - Description: "" - Examples: null - Build: - Aliases: null - Args: null - Description: "" - Examples: null - lang: - FormatAccounting: - Aliases: null - Args: - - precision - - currency - - number - Description: |- - FormatAccounting returns the currency representation of number for the given currency and precision - for the current language in accounting notation. - - The return value is formatted with at least two decimal places. - Examples: - - - '{{ 512.5032 | lang.FormatAccounting 2 "NOK" }}' - - NOK512.50 - FormatCurrency: - Aliases: null - Args: - - precision - - currency - - number - Description: |- - FormatCurrency returns the currency representation of number for the given currency and precision - for the current language. - - The return value is formatted with at least two decimal places. - Examples: - - - '{{ 512.5032 | lang.FormatCurrency 2 "USD" }}' - - $512.50 - FormatNumber: - Aliases: null - Args: - - precision - - number - Description: FormatNumber formats number with the given precision for the - current language. - Examples: - - - '{{ 512.5032 | lang.FormatNumber 2 }}' - - "512.50" - FormatNumberCustom: - Aliases: null - Args: - - precision - - number - - options - Description: |- - FormatNumberCustom formats a number with the given precision. The first - options parameter is a space-delimited string of characters to represent - negativity, the decimal point, and grouping. The default value is `- . ,`. - The second options parameter defines an alternate delimiting character. - - Note that numbers are rounded up at 5 or greater. - So, with precision set to 0, 1.5 becomes `2`, and 1.4 becomes `1`. - - For a simpler function that adapts to the current language, see FormatNumber. - Examples: - - - '{{ lang.FormatNumberCustom 2 12345.6789 }}' - - 12,345.68 - - - '{{ lang.FormatNumberCustom 2 12345.6789 "- , ." }}' - - 12.345,68 - - - '{{ lang.FormatNumberCustom 6 -12345.6789 "- ." }}' - - "-12345.678900" - - - '{{ lang.FormatNumberCustom 0 -12345.6789 "- . ," }}' - - -12,346 - - - '{{ lang.FormatNumberCustom 0 -12345.6789 "-|.| " "|" }}' - - -12 346 - - - '{{ -98765.4321 | lang.FormatNumberCustom 2 }}' - - -98,765.43 - FormatPercent: - Aliases: null - Args: - - precision - - number - Description: |- - FormatPercent formats number with the given precision for the current language. - Note that the number is assumed to be a percentage. - Examples: - - - '{{ 512.5032 | lang.FormatPercent 2 }}' - - 512.50% - Merge: - Aliases: null - Args: null - Description: "" - Examples: null - Translate: - Aliases: - - i18n - - T - Args: - - ctx - - id - - args - Description: Translate returns a translated string for id. - Examples: [] - math: - Abs: - Aliases: null - Args: - - "n" - Description: Abs returns the absolute value of n. - Examples: - - - '{{ math.Abs -2.1 }}' - - "2.1" - Acos: - Aliases: null - Args: - - "n" - Description: Acos returns the arccosine, in radians, of n. - Examples: - - - '{{ math.Acos 1 }}' - - "0" - Add: - Aliases: - - add - Args: - - inputs - Description: Add adds the multivalued addends n1 and n2 or more values. - Examples: - - - '{{ add 1 2 }}' - - "3" - Asin: - Aliases: null - Args: - - "n" - Description: Asin returns the arcsine, in radians, of n. - Examples: - - - '{{ math.Asin 1 }}' - - "1.5707963267948966" - Atan: - Aliases: null - Args: - - "n" - Description: Atan returns the arctangent, in radians, of n. - Examples: - - - '{{ math.Atan 1 }}' - - "0.7853981633974483" - Atan2: - Aliases: null - Args: - - "n" - - m - Description: Atan2 returns the arc tangent of n/m, using the signs of the - two to determine the quadrant of the return value. - Examples: - - - '{{ math.Atan2 1 2 }}' - - "0.4636476090008061" - Ceil: - Aliases: null - Args: - - "n" - Description: Ceil returns the least integer value greater than or equal to - n. - Examples: - - - '{{ math.Ceil 2.1 }}' - - "3" - Cos: - Aliases: null - Args: - - "n" - Description: Cos returns the cosine of the radian argument n. - Examples: - - - '{{ math.Cos 1 }}' - - "0.5403023058681398" - Counter: - Aliases: null - Args: null - Description: "" - Examples: null - Div: - Aliases: - - div - Args: - - inputs - Description: Div divides n1 by n2. - Examples: - - - '{{ div 6 3 }}' - - "2" - Floor: - Aliases: null - Args: - - "n" - Description: Floor returns the greatest integer value less than or equal to - n. - Examples: - - - '{{ math.Floor 1.9 }}' - - "1" - Log: - Aliases: null - Args: - - "n" - Description: Log returns the natural logarithm of the number n. - Examples: - - - '{{ math.Log 1 }}' - - "0" - Max: - Aliases: null - Args: - - inputs - Description: Max returns the greater of all numbers in inputs. Any slices - in inputs are flattened. - Examples: - - - '{{ math.Max 1 2 }}' - - "2" - Min: - Aliases: null - Args: - - inputs - Description: Min returns the smaller of all numbers in inputs. Any slices - in inputs are flattened. - Examples: - - - '{{ math.Min 1 2 }}' - - "1" - Mod: - Aliases: - - mod - Args: - - n1 - - n2 - Description: Mod returns n1 % n2. - Examples: - - - '{{ mod 15 3 }}' - - "0" - ModBool: - Aliases: - - modBool - Args: - - n1 - - n2 - Description: ModBool returns the boolean of n1 % n2. If n1 % n2 == 0, return - true. - Examples: - - - '{{ modBool 15 3 }}' - - "true" - Mul: - Aliases: - - mul - Args: - - inputs - Description: Mul multiplies the multivalued numbers n1 and n2 or more values. - Examples: - - - '{{ mul 2 3 }}' - - "6" - Pi: - Aliases: null - Args: null - Description: Pi returns the mathematical constant pi. - Examples: - - - '{{ math.Pi }}' - - "3.141592653589793" - Pow: - Aliases: - - pow - Args: - - n1 - - n2 - Description: Pow returns n1 raised to the power of n2. - Examples: - - - '{{ math.Pow 2 3 }}' - - "8" - Product: - Aliases: null - Args: null - Description: "" - Examples: null - Rand: - Aliases: null - Args: null - Description: Rand returns, as a float64, a pseudo-random number in the half-open - interval [0.0,1.0). - Examples: - - - '{{ math.Rand }}' - - "0.6312770459590062" - Round: - Aliases: null - Args: - - "n" - Description: Round returns the integer nearest to n, rounding half away from - zero. - Examples: - - - '{{ math.Round 1.5 }}' - - "2" - Sin: - Aliases: null - Args: - - "n" - Description: Sin returns the sine of the radian argument n. - Examples: - - - '{{ math.Sin 1 }}' - - "0.8414709848078965" - Sqrt: - Aliases: null - Args: - - "n" - Description: Sqrt returns the square root of the number n. - Examples: - - - '{{ math.Sqrt 81 }}' - - "9" - Sub: - Aliases: - - sub - Args: - - inputs - Description: Sub subtracts multivalued. - Examples: - - - '{{ sub 3 2 }}' - - "1" - Sum: - Aliases: null - Args: null - Description: "" - Examples: null - Tan: - Aliases: null - Args: - - "n" - Description: Tan returns the tangent of the radian argument n. - Examples: - - - '{{ math.Tan 1 }}' - - "1.557407724654902" - ToDegrees: - Aliases: null - Args: - - "n" - Description: ToDegrees converts radians into degrees. - Examples: - - - '{{ math.ToDegrees 1.5707963267948966 }}' - - "90" - ToRadians: - Aliases: null - Args: - - "n" - Description: ToRadians converts degrees into radians. - Examples: - - - '{{ math.ToRadians 90 }}' - - "1.5707963267948966" - openapi3: - Unmarshal: - Aliases: null - Args: null - Description: "" - Examples: [] - os: - FileExists: - Aliases: - - fileExists - Args: - - i - Description: FileExists checks whether a file exists under the given path. - Examples: - - - '{{ fileExists "foo.txt" }}' - - "false" - Getenv: - Aliases: - - getenv - Args: - - key - Description: |- - Getenv retrieves the value of the environment variable named by the key. - It returns the value, which will be empty if the variable is not present. - Examples: [] - ReadDir: - Aliases: - - readDir - Args: - - i - Description: ReadDir lists the directory contents relative to the configured - WorkingDir. - Examples: - - - '{{ range (readDir "files") }}{{ .Name }}{{ end }}' - - README.txt - ReadFile: - Aliases: - - readFile - Args: - - i - Description: |- - ReadFile reads the file named by filename relative to the configured WorkingDir. - It returns the contents as a string. - There is an upper size limit set at 1 megabytes. - Examples: - - - '{{ readFile "files/README.txt" }}' - - Hugo Rocks! - Stat: - Aliases: null - Args: null - Description: "" - Examples: null - partials: - Include: - Aliases: - - partial - Args: - - ctx - - name - - contextList - Description: |- - Include executes the named partial. - If the partial contains a return statement, that value will be returned. - Else, the rendered output will be returned: - A string if the partial is a text/template, or template.HTML when html/template. - Note that ctx is provided by Hugo, not the end user. - Examples: - - - '{{ partial "header.html" . }}' - - Hugo Rocks! - IncludeCached: - Aliases: - - partialCached - Args: - - ctx - - name - - context - - variants - Description: |- - IncludeCached executes and caches partial templates. The cache is created with name+variants as the key. - Note that ctx is provided by Hugo, not the end user. - Examples: [] - path: - Base: - Aliases: null - Args: null - Description: "" - Examples: null - BaseName: - Aliases: null - Args: null - Description: "" - Examples: null - Clean: - Aliases: null - Args: null - Description: "" - Examples: null - Dir: - Aliases: null - Args: null - Description: "" - Examples: null - Ext: - Aliases: null - Args: null - Description: "" - Examples: null - Join: - Aliases: null - Args: - - elements - Description: |- - Join joins any number of path elements into a single path, adding a - separating slash if necessary. All the input - path elements are passed into filepath.ToSlash converting any Windows slashes - to forward slashes. - The result is Cleaned; in particular, - all empty strings are ignored. - Examples: - - - '{{ slice "my/path" "filename.txt" | path.Join }}' - - my/path/filename.txt - - - '{{ path.Join "my" "path" "filename.txt" }}' - - my/path/filename.txt - - - '{{ "my/path/filename.txt" | path.Ext }}' - - .txt - - - '{{ "my/path/filename.txt" | path.Base }}' - - filename.txt - - - '{{ "my/path/filename.txt" | path.Dir }}' - - my/path - Split: - Aliases: null - Args: - - path - Description: |- - Split splits path immediately following the final slash, - separating it into a directory and file name component. - If there is no slash in path, Split returns an empty dir and - file set to path. - The input path is passed into filepath.ToSlash converting any Windows slashes - to forward slashes. - The returned values have the property that path = dir+file. - Examples: - - - '{{ "/my/path/filename.txt" | path.Split }}' - - /my/path/|filename.txt - - - '{{ "/my/path/filename.txt" | path.Split }}' - - /my/path/|filename.txt - reflect: - IsMap: - Aliases: null - Args: - - v - Description: IsMap reports whether v is a map. - Examples: - - - '{{ if reflect.IsMap (dict "a" 1) }}Map{{ end }}' - - Map - IsSlice: - Aliases: null - Args: - - v - Description: IsSlice reports whether v is a slice. - Examples: - - - '{{ if reflect.IsSlice (slice 1 2 3) }}Slice{{ end }}' - - Slice - resources: - Babel: - Aliases: null - Args: null - Description: "" - Examples: null - ByType: - Aliases: null - Args: null - Description: "" - Examples: null - Concat: - Aliases: null - Args: null - Description: "" - Examples: null - Copy: - Aliases: null - Args: null - Description: "" - Examples: null - ExecuteAsTemplate: - Aliases: null - Args: null - Description: "" - Examples: null - Fingerprint: - Aliases: - - fingerprint - Args: - - args - Description: |- - Fingerprint transforms the given Resource with a MD5 hash of the content in - the RelPermalink and Permalink. - Examples: [] - FromString: - Aliases: null - Args: null - Description: "" - Examples: null - Get: - Aliases: null - Args: - - filename - Description: |- - Get locates the filename given in Hugo's assets filesystem - and creates a Resource object that can be used for further transformations. - Examples: [] - GetMatch: - Aliases: null - Args: null - Description: "" - Examples: null - GetRemote: - Aliases: null - Args: - - args - Description: |- - GetRemote gets the URL (via HTTP(s)) in the first argument in args and creates Resource object that can be used for - further transformations. - - A second argument may be provided with an option map. - - Note: This method does not return any error as a second return value, - for any error situations the error can be checked in .Err. - Examples: [] - Match: - Aliases: null - Args: null - Description: "" - Examples: null - Minify: - Aliases: - - minify - Args: - - r - Description: |- - Minify minifies the given Resource using the MediaType to pick the correct - minifier. - Examples: [] - PostCSS: - Aliases: null - Args: null - Description: "" - Examples: null - PostProcess: - Aliases: null - Args: null - Description: "" - Examples: null - ToCSS: - Aliases: null - Args: null - Description: "" - Examples: null - safe: - CSS: - Aliases: - - safeCSS - Args: - - s - Description: CSS returns the string s as html/template CSS content. - Examples: - - - '{{ "Bat&Man" | safeCSS | safeCSS }}' - - Bat&Man - HTML: - Aliases: - - safeHTML - Args: - - s - Description: HTML returns the string s as html/template HTML content. - Examples: - - - '{{ "Bat&Man" | safeHTML | safeHTML }}' - - Bat&Man - - - '{{ "Bat&Man" | safeHTML }}' - - Bat&Man - HTMLAttr: - Aliases: - - safeHTMLAttr - Args: - - s - Description: HTMLAttr returns the string s as html/template HTMLAttr content. - Examples: [] - JS: - Aliases: - - safeJS - Args: - - s - Description: JS returns the given string as a html/template JS content. - Examples: - - - '{{ "(1*2)" | safeJS | safeJS }}' - - (1*2) - JSStr: - Aliases: - - safeJSStr - Args: - - s - Description: JSStr returns the given string as a html/template JSStr content. - Examples: [] - URL: - Aliases: - - safeURL - Args: - - s - Description: URL returns the string s as html/template URL content. - Examples: - - - '{{ "http://gohugo.io" | safeURL | safeURL }}' - - http://gohugo.io - site: - AllPages: - Aliases: null - Args: null - Description: "" - Examples: null - Author: - Aliases: null - Args: null - Description: "" - Examples: null - Authors: - Aliases: null - Args: null - Description: "" - Examples: null - BaseURL: - Aliases: null - Args: null - Description: "" - Examples: null - BuildDrafts: - Aliases: null - Args: null - Description: "" - Examples: null - CheckReady: - Aliases: null - Args: null - Description: "" - Examples: null - Config: - Aliases: null - Args: null - Description: "" - Examples: null - Copyright: - Aliases: null - Args: null - Description: "" - Examples: null - Current: - Aliases: null - Args: null - Description: "" - Examples: null - Data: - Aliases: null - Args: null - Description: "" - Examples: null - ForEeachIdentityByName: - Aliases: null - Args: null - Description: "" - Examples: null - GetPage: - Aliases: null - Args: null - Description: "" - Examples: null - Home: - Aliases: null - Args: null - Description: "" - Examples: null - Hugo: - Aliases: null - Args: null - Description: "" - Examples: null - IsMultiLingual: - Aliases: null - Args: null - Description: "" - Examples: null - Key: - Aliases: null - Args: null - Description: "" - Examples: null - Language: - Aliases: null - Args: null - Description: "" - Examples: null - LanguageCode: - Aliases: null - Args: null - Description: "" - Examples: null - LanguagePrefix: - Aliases: null - Args: null - Description: "" - Examples: null - Languages: - Aliases: null - Args: null - Description: "" - Examples: null - LastChange: - Aliases: null - Args: null - Description: "" - Examples: null - Lastmod: - Aliases: null - Args: null - Description: "" - Examples: null - MainSections: - Aliases: null - Args: null - Description: "" - Examples: null - Menus: - Aliases: null - Args: null - Description: "" - Examples: null - Pages: - Aliases: null - Args: null - Description: "" - Examples: null - Param: - Aliases: null - Args: null - Description: "" - Examples: null - Params: - Aliases: null - Args: null - Description: "" - Examples: null - RegularPages: - Aliases: null - Args: null - Description: "" - Examples: null - Sections: - Aliases: null - Args: null - Description: "" - Examples: null - ServerPort: - Aliases: null - Args: null - Description: "" - Examples: null - Sites: - Aliases: null - Args: null - Description: "" - Examples: null - Social: - Aliases: null - Args: null - Description: "" - Examples: null - Store: - Aliases: null - Args: null - Description: "" - Examples: null - Taxonomies: - Aliases: null - Args: null - Description: "" - Examples: null - Title: - Aliases: null - Args: null - Description: "" - Examples: null - strings: - Chomp: - Aliases: - - chomp - Args: - - s - Description: Chomp returns a copy of s with all trailing newline characters - removed. - Examples: - - - '{{ chomp "

      Blockhead

      \n" | safeHTML }}' - -

      Blockhead

      - Contains: - Aliases: null - Args: - - s - - substr - Description: Contains reports whether substr is in s. - Examples: - - - '{{ strings.Contains "abc" "b" }}' - - "true" - - - '{{ strings.Contains "abc" "d" }}' - - "false" - ContainsAny: - Aliases: null - Args: - - s - - chars - Description: ContainsAny reports whether any Unicode code points in chars - are within s. - Examples: - - - '{{ strings.ContainsAny "abc" "bcd" }}' - - "true" - - - '{{ strings.ContainsAny "abc" "def" }}' - - "false" - ContainsNonSpace: - Aliases: null - Args: null - Description: "" - Examples: null - Count: - Aliases: null - Args: - - substr - - s - Description: |- - Count counts the number of non-overlapping instances of substr in s. - If substr is an empty string, Count returns 1 + the number of Unicode code points in s. - Examples: - - - '{{ "aabab" | strings.Count "a" }}' - - "3" - CountRunes: - Aliases: - - countrunes - Args: - - s - Description: CountRunes returns the number of runes in s, excluding whitespace. - Examples: [] - CountWords: - Aliases: - - countwords - Args: - - s - Description: CountWords returns the approximate word count in s. - Examples: [] - Diff: - Aliases: null - Args: null - Description: "" - Examples: null - FindRE: - Aliases: - - findRE - Args: - - expr - - content - - limit - Description: |- - FindRE returns a list of strings that match the regular expression. By default all matches - will be included. The number of matches can be limited with an optional third parameter. - Examples: - - - '{{ findRE "[G|g]o" "Hugo is a static side generator written in Go." 1 - }}' - - '[go]' - FindRESubmatch: - Aliases: - - findRESubmatch - Args: - - expr - - content - - limit - Description: |- - FindRESubmatch returns a slice of all successive matches of the regular - expression in content. Each element is a slice of strings holding the text - of the leftmost match of the regular expression and the matches, if any, of - its subexpressions. - - By default all matches will be included. The number of matches can be - limited with the optional limit parameter. A return value of nil indicates - no match. - Examples: - - - '{{ findRESubmatch `(.+?)` `
    • Foo
    • -
    • Bar
    • ` | print | safeHTML }}' - - '[[Foo #foo Foo] [Bar #bar Bar]]' - FirstUpper: - Aliases: null - Args: - - s - Description: FirstUpper converts s making the first character upper case. - Examples: - - - '{{ "hugo rocks!" | strings.FirstUpper }}' - - Hugo rocks! - HasPrefix: - Aliases: - - hasPrefix - Args: - - s - - prefix - Description: HasPrefix tests whether the input s begins with prefix. - Examples: - - - '{{ hasPrefix "Hugo" "Hu" }}' - - "true" - - - '{{ hasPrefix "Hugo" "Fu" }}' - - "false" - HasSuffix: - Aliases: - - hasSuffix - Args: - - s - - suffix - Description: HasSuffix tests whether the input s begins with suffix. - Examples: - - - '{{ hasSuffix "Hugo" "go" }}' - - "true" - - - '{{ hasSuffix "Hugo" "du" }}' - - "false" - Repeat: - Aliases: null - Args: - - "n" - - s - Description: Repeat returns a new string consisting of n copies of the string - s. - Examples: - - - '{{ "yo" | strings.Repeat 4 }}' - - yoyoyoyo - Replace: - Aliases: - - replace - Args: - - s - - old - - new - - limit - Description: |- - Replace returns a copy of the string s with all occurrences of old replaced - with new. The number of replacements can be limited with an optional fourth - parameter. - Examples: - - - '{{ replace "Batman and Robin" "Robin" "Catwoman" }}' - - Batman and Catwoman - - - '{{ replace "aabbaabb" "a" "z" 2 }}' - - zzbbaabb - ReplaceRE: - Aliases: - - replaceRE - Args: - - pattern - - repl - - s - - "n" - Description: |- - ReplaceRE returns a copy of s, replacing all matches of the regular - expression pattern with the replacement text repl. The number of replacements - can be limited with an optional fourth parameter. - Examples: - - - '{{ replaceRE "a+b" "X" "aabbaabbab" }}' - - XbXbX - - - '{{ replaceRE "a+b" "X" "aabbaabbab" 1 }}' - - Xbaabbab - RuneCount: - Aliases: null - Args: - - s - Description: RuneCount returns the number of runes in s. - Examples: [] - SliceString: - Aliases: - - slicestr - Args: - - a - - startEnd - Description: |- - SliceString slices a string by specifying a half-open range with - two indices, start and end. 1 and 4 creates a slice including elements 1 through 3. - The end index can be omitted, it defaults to the string's length. - Examples: - - - '{{ slicestr "BatMan" 0 3 }}' - - Bat - - - '{{ slicestr "BatMan" 3 }}' - - Man - Split: - Aliases: - - split - Args: - - a - - delimiter - Description: Split slices an input string into all substrings separated by - delimiter. - Examples: [] - Substr: - Aliases: - - substr - Args: - - a - - nums - Description: |- - Substr extracts parts of a string, beginning at the character at the specified - position, and returns the specified number of characters. - - It normally takes two parameters: start and length. - It can also take one parameter: start, i.e. length is omitted, in which case - the substring starting from start until the end of the string will be returned. - - To extract characters from the end of the string, use a negative start number. - - In addition, borrowing from the extended behavior described at http://php.net/substr, - if length is given and is negative, then that many characters will be omitted from - the end of string. - Examples: - - - '{{ substr "BatMan" 0 -3 }}' - - Bat - - - '{{ substr "BatMan" 3 3 }}' - - Man - Title: - Aliases: - - title - Args: - - s - Description: |- - Title returns a copy of the input s with all Unicode letters that begin words - mapped to their title case. - Examples: - - - '{{ title "Bat man" }}' - - Bat Man - - - '{{ title "somewhere over the rainbow" }}' - - Somewhere Over the Rainbow - ToLower: - Aliases: - - lower - Args: - - s - Description: |- - ToLower returns a copy of the input s with all Unicode letters mapped to their - lower case. - Examples: - - - '{{ lower "BatMan" }}' - - batman - ToUpper: - Aliases: - - upper - Args: - - s - Description: |- - ToUpper returns a copy of the input s with all Unicode letters mapped to their - upper case. - Examples: - - - '{{ upper "BatMan" }}' - - BATMAN - Trim: - Aliases: - - trim - Args: - - s - - cutset - Description: |- - Trim returns converts the strings s removing all leading and trailing characters defined - contained. - Examples: - - - '{{ trim "++Batman--" "+-" }}' - - Batman - TrimLeft: - Aliases: null - Args: - - cutset - - s - Description: |- - TrimLeft returns a slice of the string s with all leading characters - contained in cutset removed. - Examples: - - - '{{ "aabbaa" | strings.TrimLeft "a" }}' - - bbaa - TrimPrefix: - Aliases: null - Args: - - prefix - - s - Description: |- - TrimPrefix returns s without the provided leading prefix string. If s doesn't - start with prefix, s is returned unchanged. - Examples: - - - '{{ "aabbaa" | strings.TrimPrefix "a" }}' - - abbaa - - - '{{ "aabbaa" | strings.TrimPrefix "aa" }}' - - bbaa - TrimRight: - Aliases: null - Args: - - cutset - - s - Description: |- - TrimRight returns a slice of the string s with all trailing characters - contained in cutset removed. - Examples: - - - '{{ "aabbaa" | strings.TrimRight "a" }}' - - aabb - TrimSpace: - Aliases: null - Args: null - Description: "" - Examples: null - TrimSuffix: - Aliases: null - Args: - - suffix - - s - Description: |- - TrimSuffix returns s without the provided trailing suffix string. If s - doesn't end with suffix, s is returned unchanged. - Examples: - - - '{{ "aabbaa" | strings.TrimSuffix "a" }}' - - aabba - - - '{{ "aabbaa" | strings.TrimSuffix "aa" }}' - - aabb - Truncate: - Aliases: - - truncate - Args: - - s - - options - Description: Truncate truncates the string in s to the specified length. - Examples: - - - '{{ "this is a very long text" | truncate 10 " ..." }}' - - this is a ... - - - '{{ "With [Markdown](/markdown) inside." | markdownify | truncate 14 }}' - - With Markdown … - templates: - Defer: - Aliases: null - Args: - - args - Description: Defer defers the execution of a template block. - Examples: [] - DoDefer: - Aliases: - - doDefer - Args: - - ctx - - id - - optsv - Description: |- - DoDefer defers the execution of a template block. - For internal use only. - Examples: [] - Exists: - Aliases: null - Args: - - name - Description: |- - Exists returns whether the template with the given name exists. - Note that this is the Unix-styled relative path including filename suffix, - e.g. partials/header.html - Examples: - - - '{{ if (templates.Exists "partials/header.html") }}Yes!{{ end }}' - - Yes! - - - '{{ if not (templates.Exists "partials/doesnotexist.html") }}No!{{ end - }}' - - No! - time: - AsTime: - Aliases: null - Args: - - v - - args - Description: |- - AsTime converts the textual representation of the datetime string into - a time.Time interface. - Examples: - - - '{{ (time "2015-01-21").Year }}' - - "2015" - Duration: - Aliases: - - duration - Args: - - unit - - number - Description: |- - Duration converts the given number to a time.Duration. - Unit is one of nanosecond/ns, microsecond/us/µs, millisecond/ms, second/s, minute/m or hour/h. - Examples: - - - '{{ mul 60 60 | duration "second" }}' - - 1h0m0s - Format: - Aliases: - - dateFormat - Args: - - layout - - v - Description: |- - Format converts the textual representation of the datetime string in v into - time.Time if needed and formats it with the given layout. - Examples: - - - 'dateFormat: {{ dateFormat "Monday, Jan 2, 2006" "2015-01-21" }}' - - 'dateFormat: Wednesday, Jan 21, 2015' - Now: - Aliases: - - now - Args: null - Description: Now returns the current local time or `clock` time - Examples: [] - ParseDuration: - Aliases: null - Args: - - s - Description: |- - ParseDuration parses the duration string s. - A duration string is a possibly signed sequence of - decimal numbers, each with optional fraction and a unit suffix, - such as "300ms", "-1.5h" or "2h45m". - Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". - See https://golang.org/pkg/time/#ParseDuration - Examples: - - - '{{ "1h12m10s" | time.ParseDuration }}' - - 1h12m10s - transform: - CanHighlight: - Aliases: null - Args: null - Description: "" - Examples: null - Emojify: - Aliases: - - emojify - Args: - - s - Description: |- - Emojify returns a copy of s with all emoji codes replaced with actual emojis. - - See http://www.emoji-cheat-sheet.com/ - Examples: - - - '{{ "I :heart: Hugo" | emojify }}' - - I ❤️ Hugo - HTMLEscape: - Aliases: - - htmlEscape - Args: - - s - Description: HTMLEscape returns a copy of s with reserved HTML characters - escaped. - Examples: - - - '{{ htmlEscape "Cathal Garvey & The Sunshine Band " | - safeHTML }}' - - Cathal Garvey & The Sunshine Band <cathal@foo.bar> - - - '{{ htmlEscape "Cathal Garvey & The Sunshine Band " }}' - - Cathal Garvey &amp; The Sunshine Band &lt;cathal@foo.bar&gt; - - - '{{ htmlEscape "Cathal Garvey & The Sunshine Band " | - htmlUnescape | safeHTML }}' - - Cathal Garvey & The Sunshine Band - HTMLUnescape: - Aliases: - - htmlUnescape - Args: - - s - Description: |- - HTMLUnescape returns a copy of s with HTML escape requences converted to plain - text. - Examples: - - - '{{ htmlUnescape "Cathal Garvey & The Sunshine Band <cathal@foo.bar>" - | safeHTML }}' - - Cathal Garvey & The Sunshine Band - - - '{{ "Cathal Garvey &amp; The Sunshine Band &lt;cathal@foo.bar&gt;" - | htmlUnescape | htmlUnescape | safeHTML }}' - - Cathal Garvey & The Sunshine Band - - - '{{ "Cathal Garvey &amp; The Sunshine Band &lt;cathal@foo.bar&gt;" - | htmlUnescape | htmlUnescape }}' - - Cathal Garvey & The Sunshine Band <cathal@foo.bar> - - - '{{ htmlUnescape "Cathal Garvey & The Sunshine Band <cathal@foo.bar>" - | htmlEscape | safeHTML }}' - - Cathal Garvey & The Sunshine Band <cathal@foo.bar> - Highlight: - Aliases: - - highlight - Args: - - s - - lang - - opts - Description: |- - Highlight returns a copy of s as an HTML string with syntax - highlighting applied. - Examples: [] - HighlightCodeBlock: - Aliases: null - Args: null - Description: "" - Examples: null - Markdownify: - Aliases: - - markdownify - Args: - - ctx - - s - Description: Markdownify renders s from Markdown to HTML. - Examples: - - - '{{ .Title | markdownify }}' - - BatMan - Plainify: - Aliases: - - plainify - Args: - - s - Description: Plainify returns a copy of s with all HTML tags removed. - Examples: - - - '{{ plainify "Hello world, gophers!" }}' - - Hello world, gophers! - Remarshal: - Aliases: null - Args: - - format - - data - Description: |- - Remarshal is used in the Hugo documentation to convert configuration - examples from YAML to JSON, TOML (and possibly the other way around). - The is primarily a helper for the Hugo docs site. - It is not a general purpose YAML to TOML converter etc., and may - change without notice if it serves a purpose in the docs. - Format is one of json, yaml or toml. - Examples: - - - '{{ "title = \"Hello World\"" | transform.Remarshal "json" | safeHTML - }}' - - | - { - "title": "Hello World" - } - ToMath: - Aliases: null - Args: null - Description: "" - Examples: null - Unmarshal: - Aliases: - - unmarshal - Args: - - args - Description: |- - Unmarshal unmarshals the data given, which can be either a string, json.RawMessage - or a Resource. Supported formats are JSON, TOML, YAML, and CSV. - You can optionally provide an options map as the first argument. - Examples: - - - '{{ "hello = \"Hello World\"" | transform.Unmarshal }}' - - map[hello:Hello World] - - - '{{ "hello = \"Hello World\"" | resources.FromString "data/greetings.toml" - | transform.Unmarshal }}' - - map[hello:Hello World] - XMLEscape: - Aliases: null - Args: - - s - Description: |- - XMLEscape returns the given string, removing disallowed characters then - escaping the result to its XML equivalent. - Examples: - - - '{{ transform.XMLEscape "

      abc

      " }}' - - '<p>abc</p>' - urls: - AbsLangURL: - Aliases: - - absLangURL - Args: - - s - Description: |- - AbsLangURL the string s and converts it to an absolute URL according - to a page's position in the project directory structure and the current - language. - Examples: [] - AbsURL: - Aliases: - - absURL - Args: - - s - Description: AbsURL takes the string s and converts it to an absolute URL. - Examples: [] - Anchorize: - Aliases: - - anchorize - Args: - - s - Description: |- - Anchorize creates sanitized anchor name version of the string s that is compatible - with how your configured markdown renderer does it. - Examples: - - - '{{ "This is a title" | anchorize }}' - - this-is-a-title - JoinPath: - Aliases: null - Args: - - elements - Description: |- - JoinPath joins the provided elements into a URL string and cleans the result - of any ./ or ../ elements. If the argument list is empty, JoinPath returns - an empty string. - Examples: - - - '{{ urls.JoinPath "https://example.org" "foo" }}' - - https://example.org/foo - - - '{{ urls.JoinPath (slice "a" "b") }}' - - a/b - Parse: - Aliases: null - Args: null - Description: "" - Examples: null - Ref: - Aliases: - - ref - Args: - - p - - args - Description: Ref returns the absolute URL path to a given content item from - Page p. - Examples: [] - RelLangURL: - Aliases: - - relLangURL - Args: - - s - Description: |- - RelLangURL takes the string s and prepends the relative path according to a - page's position in the project directory structure and the current language. - Examples: [] - RelRef: - Aliases: - - relref - Args: - - p - - args - Description: RelRef returns the relative URL path to a given content item - from Page p. - Examples: [] - RelURL: - Aliases: - - relURL - Args: - - s - Description: |- - RelURL takes the string s and prepends the relative path according to a - page's position in the project directory structure. - Examples: [] - URLize: - Aliases: - - urlize - Args: - - s - Description: URLize returns the strings s formatted as an URL. - Examples: [] diff --git a/docs/data/embedded_template_urls.toml b/docs/data/embedded_template_urls.toml deleted file mode 100644 index b2a796cd1..000000000 --- a/docs/data/embedded_template_urls.toml +++ /dev/null @@ -1,42 +0,0 @@ -# Used by the embedded template URL (eturl.html) shortcode. -# Quoted all keys because some are not valid identifiers. - -# BaseURL -'base_url' = 'https://github.com/gohugoio/hugo/blob/master/tpl/tplimpl/embedded/templates' - -# Partials -'disqus' = '_partials/disqus.html' -'google_analytics' = '_partials/google_analytics.html' -'opengraph' = '_partials/opengraph.html' -'pagination' = '_partials/pagination.html' -'schema' = '_partials/schema.html' -'twitter_cards' = '_partials/twitter_cards.html' - -# Render hooks -'render-codeblock-goat' = '_markup/render-codeblock-goat.html' -'render-image' = '_markup/render-image.html' -'render-link' = '_markup/render-link.html' -'render-table' = '_markup/render-table.html' - -# Shortcodes -'details' = '_shortcodes/details.html' -'figure' = '_shortcodes/figure.html' -'gist' = '_shortcodes/gist.html' -'highlight' = '_shortcodes/highlight.html' -'instagram' = '_shortcodes/instagram.html' -'param' = '_shortcodes/param.html' -'qr' = '_shortcodes/qr.html' -'ref' = '_shortcodes/ref.html' -'relref' = '_shortcodes/relref.html' -'vimeo' = '_shortcodes/vimeo.html' -'vimeo_simple' = '_shortcodes/vimeo_simple.html' -'x' = '_shortcodes/x.html' -'x_simple' = '_shortcodes/x_simple.html' -'youtube' = '_shortcodes/youtube.html' - -# Other -'alias' = 'alias.html' -'robots' = 'robots.txt' -'rss' = 'rss.xml' -'sitemap' = 'sitemap.xml' -'sitemapindex' = 'sitemapindex.xml' diff --git a/docs/data/homepagetweets.toml b/docs/data/homepagetweets.toml deleted file mode 100644 index b440c21df..000000000 --- a/docs/data/homepagetweets.toml +++ /dev/null @@ -1,265 +0,0 @@ -[[tweet]] -name = "Heinrich Hartmann" -twitter_handle = "@heinrichhartman" -quote = "Working with @GoHugoIO is such a joy. Having worked with #Jekyll in the past, the near instant preview is a big win! Did not expect this to make such a huge difference." -link = "https://x.com/heinrichhartman/status/1199736512264462341" -date = 2019-11-12T00:00:00Z - -[[tweet]] -name = "Joshua Steven‏‏" -twitter_handle = "@jscarto" -quote = "Can't overstate how much I enjoy @GoHugoIO. My site is relatively small, but *18 ms* to build the whole thing made template development and proofing a breeze." -link = "https://x.com/jscarto/status/1039648827815485440" -date = 2018-09-12T00:00:00Z - -[[tweet]] -name = "Christophe Diericx" -twitter_handle = "@spcrngr_" -quote = "The more I use gohugo.io, the more I really like it. Super intuitive/powerful static site generator...great job @GoHugoIO" -link = "https://x.com/spcrngr_/status/870863020905435136" -date = 2017-06-03T00:00:00Z - -[[tweet]] -name = "marcoscan" -twitter_handle = "@marcoscan" -quote = "Blog migrated from @WordPress to @GoHugoIO, with a little refresh of my theme, Vim shortcuts and a full featured deploy script #gohugo" -link = "https://x.com/marcoscan/status/869661175960752129" -date = 2017-05-30T00:00:00Z - -[[tweet]] -name = "Sandra Kuipers" -twitter_handle = "@SKuipersDesign" -quote = "Who knew static site building could be fun 🤔 Learning #gohugo today" -link = "https://x.com/SKuipersDesign/status/868796256902029312" -date = 2017-05-28T00:00:00Z - -[[tweet]] -name = "Netlify" -twitter_handle = "@Netlify" -quote = "Top Ten Static Site Generators of 2017. Congrats to the top 3: 1. @Jekyllrb 2. @GoHugoIO 3. @hexojs" -link = "https://x.com/Netlify/status/868122279221362688" -date = 2017-05-26T00:00:00Z - -[[tweet]] -name = "Phil Hawksworth" -twitter_handle = "@philhawksworth" -quote = "I've been keen on #JAMStack for some time, but @GoHugoIO is wooing me all over again. Great fun to build with. And speeeeedy." -link = "https://x.com/philhawksworth/status/866684170512326657" -date = 2017-05-22T00:00:00Z - -[[tweet]] -name = "Aras Pranckevicius" -twitter_handle = "@aras_p" -quote = "I've probably said it before...but having Hugo rebuild the whole website in 300ms is amazing. gohugo.io, #gohugo" -link = "https://x.com/aras_p/status/861157286823288832" -date = 2017-05-07T00:00:00Z - -[[tweet]] -name = "Hans Beck" -twitter_handle = "@EnrichedGamesHB" -quote = "Diving deeper into @GoHugoIO. A lot of docs there, top work! But I've the impressed that #gohugo is far easier than its feels from the docs!" -link = "https://x.com/EnrichedGamesHB/status/836854762440130560" -date = 2017-03-01T00:00:00Z - -[[tweet]] -name = "Alan Richardson" -twitter_handle = "@eviltester" -quote = "I migrated the @BlackOpsTesting .com website from docpad to Hugo last weekend. http://gohugo.io/ Super Fast HTML Generation @spf13 " -link = "https://x.com/eviltester/status/553520335115808768" -date = 2015-01-09T00:00:00Z - -[[tweet]] -name = "Janez Čadež‏" -twitter_handle = "@jamziSLO" -quote = "Building @garazaFRI website in #hugo. This static site generator is soooo damn fast! #gohugo #golang" -link = "https://x.com/jamziSLO/status/817720283977183234" -date = 2017-01-07T00:00:00Z - -[[tweet]] -name = "Execute‏‏" -twitter_handle = "@executerun" -quote = "Hah, #gohugo. I was working with #gohugo on #linux but now I realised how easy is to set-up it on #windows. Just need to add binary to #path!" -link = "https://x.com/executerun/status/809753145270272005" -date = 2016-12-16T00:00:00Z - -[[tweet]] -name = "Baron Schwartz" -twitter_handle = "@xaprb" -quote = "Hugo is impressively capable. It's a static site generator by @spf13 written in #golang . Just upgraded to latest release; very powerful. " -link = "https://x.com/xaprb/status/556894866488455169" -date = 2015-01-18T00:00:00Z - -[[tweet]] -name = "Dave Cottlehuber" -twitter_handle = "@dch__" -quote = "I just fell in love with #hugo, a static site/blog engine written by @spf13 in #golang + stellar docs" -link = "https://x.com/dch__/status/460158115498176512" -date = 2014-04-26T00:00:00Z - -[[tweet]] -name = "David Caunt" -twitter_handle = "@dcaunt" -quote = "I had a play with Hugo and it was good, uses Markdown files for content" -link = "https://x.com/dcaunt/statuses/406466996277374976" -date = 2013-11-29T00:00:00Z - -[[tweet]] -name = "David Gay" -twitter_handle = "@oddshocks" -quote = "Hugo is super-rad." -link = "https://x.com/oddshocks/statuses/405083217893421056" -date = 2013-11-25T00:00:00Z - -[[tweet]] -name = "Diti" -twitter_handle = "@DitiPengi" -quote = "The dev version of Hugo is AWESOME! <3 I promise, I will try to learn go ASAP and help contribute to the project! Just too great!" -link = "https://x.com/DitiPengi/status/472470974051676160" -date = 2014-05-30T00:00:00Z - -[[tweet]] -name = "Douglas Stephen " -twitter_handle = "@DougStephenJr" -quote = "Even as a long-time Octopress fan, I’ve gotta admit that this project Hugo looks very very cool" -link = "https://x.com/DougStephenJr/statuses/364512471660249088" -date = 2013-08-05T00:00:00Z - -[[tweet]] -name = "Hugo Rodger-Brown" -twitter_handle = "@hugorodgerbrown" -quote = "Finally someone builds me my own static site generator" -link = "https://x.com/hugorodgerbrown/statuses/364417910153818112" -date = 2013-05-08T00:00:00Z - -[[tweet]] -name = "Hugo Roy" -twitter_handle = "@hugoroyd" -quote = "Finally the answer to the question my parents have been asking: What does Hugo do?" -link = "https://x.com/hugoroyd/status/501704796727173120" -date = 2014-08-19T00:00:00Z - -[[tweet]] -name = "Daniel Miessler" -twitter_handle = "@DanielMiessler" -quote = "Websites for named vulnerabilities should run on static site generator platforms like Hugo. Read-only + burst traffic = static." -link = "https://x.com/DanielMiessler/status/704703841673957376" -date = 2016-03-01T00:00:00Z - -[[tweet]] -name = "Javier Segura" -twitter_handle = "@jsegura" -quote = "Another site generated with Hugo here! I'm getting in love with it." -link = "https://x.com/jsegura/status/465978434154659841" -date = 2014-05-12T00:00:00Z - -[[tweet]] -name = "Jim Biancolo" -twitter_handle = "@jimbiancolo" -quote = "I’m loving the static site generator renaissance we are currently enjoying. Hugo is new, looks great, written in Go" -link = "https://x.com/jimbiancolo/statuses/408678420348813314" -date = 2013-05-12T00:00:00Z - -[[tweet]] -name = "Jip J. Dekker" -twitter_handle = "@jipjdekker" -quote = "Building a personal website in Hugo. Works like a charm. And written in @golang!" -link = "https://x.com/jipjdekker/status/413783548735152131" -date = 2013-12-19T00:00:00Z - -[[tweet]] -name = "Jose Gonzalvo" -twitter_handle = "@jgonzalvo" -quote = "Checking out Hugo; Loving it so far. Like Jekyll but not so blog-oriented and written in go" -link = "https://x.com/jgonzalvo/statuses/408177855819173888" -date = 2013-12-04T00:00:00Z - -[[tweet]] -name = "Josh Matz" -twitter_handle = "@joshmatz" -quote = "A static site generator without the long build times? Yes, please!" -link = "https://x.com/joshmatz/statuses/364437436870696960" -date = 2013-08-05T00:00:00Z - -[[tweet]] -name = "Kieran Healy" -twitter_handle = "@kjhealy" -quote = "OK, so in today's speed battle of static site generators, @spf13's hugo is kicking everyone's ass, by miles." -link = "https://x.com/kjhealy/status/437349384809115648" -date = 2014-02-22T00:00:00Z - -[[tweet]] -name = "Ludovic Chabant" -twitter_handle = "@ludovicchabant" -quote = "Good work on Hugo, I’m impressed with the speed!" -link = "https://x.com/ludovicchabant/statuses/408806199602053120" -date = 2013-12-06T00:00:00Z - -[[tweet]] -name = "Luke Holder" -twitter_handle = "@lukeholder" -quote = "this is AWESOME. a single little executable and so fast." -link = "https://x.com/lukeholder/status/430352287936946176" -date = 2014-02-03T00:00:00Z - -[[tweet]] -name = "Markus Eliasson" -twitter_handle = "@markuseliasson" -quote = "Hugo is fast, dead simple to setup and well documented" -link = "https://x.com/markuseliasson/status/501594865877008384" -date = 2014-08-19T00:00:00Z - -[[tweet]] -name = "mercime" -twitter_handle = "@mercime_one" -quote = "Hugo: Makes the Web Fun Again" -link = "https://x.com/mercime_one/status/500547145087205377" -date = 2014-08-16T00:00:00Z - -[[tweet]] -name = "Michael Whatcott" -twitter_handle = "@mdwhatcott" -quote = "One more satisfied #Hugo blogger. Thanks @spf13 and friends!" -link = "https://x.com/mdwhatcott/status/469980686531571712" -date = 2014-05-23T00:00:00Z - -[[tweet]] -name = "Nathan Toups" -twitter_handle = "@rojoroboto" -quote = "I love Hugo! My site is generated with it now http://rjrbt.io" -link = "https://x.com/rojoroboto/status/423439915620106242" -date = 2014-01-15T00:00:00Z - -[[tweet]] -name = "Ruben Solvang" -twitter_handle = "@messo85" -quote = "#Hugo is the new @jekyllrb / @middlemanapp! Faster, easier and runs everywhere." -link = "https://x.com/messo85/status/472825062027182081" -date = 2014-05-31T00:00:00Z - -[[tweet]] -name = "Ryan Martinsen" -twitter_handle = "@popthestack" -quote = "Also, I re-launched my blog (it looks the same as before) using Hugo, a *fast* static engine. Very happy with it. gohugo.io" -link = "https://x.com/popthestack/status/549972754125307904" -date = 2014-12-30T00:00:00Z - -[[tweet]] -name = "The Lone Cuber" -twitter_handle = "@TheLoneCuber" -quote = "Jekyll is dead to me these days though... long live Hugo! Hugo is *by far* the best in its field. Thanks for making it happen." -link = "https://x.com/TheLoneCuber/status/495716684456398848" -date = 2014-08-02T00:00:00Z - -[[tweet]] -name = "The Lone Cuber" -twitter_handle = "@TheLoneCuber" -quote = "Finally, a publishing platform that's a joy to use. #NoMoreBarriers" -link = "https://x.com/TheLoneCuber/status/495731334711488512" -date = 2014-08-02T00:00:00Z - -[[tweet]] -name = "WorkHTML" -twitter_handle = "@workhtml" -quote = " #Hugo A very good alternative for #wordpress !!! A fast and modern static website engine gohugo.io " -link = "https://x.com/workhtml/status/563064361301053440" -date = 2015-02-04T00:00:00Z diff --git a/docs/data/keywords.yaml b/docs/data/keywords.yaml deleted file mode 100644 index 106929884..000000000 --- a/docs/data/keywords.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# We use the front matter keywords field to determine related content. To -# ensure consistency, during site build we validate each keyword against the -# entries in data/keywords.yaml. - -# As of March 5, 2025, this feature is experimental, pending usability -# assessment. We anticipate that the number of additions to data/keywords.yaml -# will decrease over time, though the initial implementation will require some -# effort. - -- menu -- resource -- highlight diff --git a/docs/data/page_filters.yaml b/docs/data/page_filters.yaml deleted file mode 100644 index 82de6168e..000000000 --- a/docs/data/page_filters.yaml +++ /dev/null @@ -1,95 +0,0 @@ -# Do not delete. Required for layouts/shortcodes/list-pages-in-section.html. -# -# When calling the list-pages-in-section shortcode, you can specify a page -# filter, and whether the pages in the filter should be included or excluded -# from the list. -# -# For example: -# -# {{% list-pages-in-section path=/functions/images filter=functions_images_no_filters filterType=exclude %}} - -functions_fmt_logging: - - /functions/fmt/errorf - - /functions/fmt/erroridf - - /functions/fmt/warnf - - /functions/fmt/warnidf -functions_images_no_filters: - - /functions/images/filter - - /functions/images/config -methods_site_multilingual: - - /methods/site/ismultilingual - - /methods/site/language - - /methods/site/languageprefix - - /methods/site/languages -methods_site_page_collections: - - /methods/site/allpages - - /methods/site/pages - - /methods/site/regularpages - - /methods/site/sections -methods_page_dates: - - /methods/page/date - - /methods/page/expirydate - - /methods/page/lastmod - - /methods/page/publishdate -methods_page_menu: - - /methods/page/hasmenucurrent - - /methods/page/ismenucurrent -methods_page_multilingual: - - /methods/page/alltranslations - - /methods/page/istranslated - - /methods/page/language - - /methods/page/translationkey - - /methods/page/translations -methods_page_page_collections: - - /methods/page/pages - - /methods/page/regularpages - - /methods/page/regularpagesrecursive - - /methods/page/sections -methods_page_parameters: - - /methods/page/param - - /methods/page/params -methods_page_sections: - - /methods/page/ancestors - - /methods/page/currentsection - - /methods/page/firstsection - - /methods/page/insection - - /methods/page/isancestor - - /methods/page/isdescendant - - /methods/page/parent - - /methods/page/sections - - /methods/page/section -methods_pages_sort: - - /methods/pages/bydate - - /methods/pages/byexpirydate - - /methods/pages/bylanguage - - /methods/pages/bylastmod - - /methods/pages/bylength - - /methods/pages/bylinktitle - - /methods/pages/byparam - - /methods/pages/bypublishdate - - /methods/pages/bytitle - - /methods/pages/byweight - - /methods/pages/reverse -methods_pages_group: - - /methods/pages/groupby - - /methods/pages/groupbydate - - /methods/pages/groupbyexpirydate - - /methods/pages/groupbylastmod - - /methods/pages/groupbyparam - - /methods/pages/groupbyparamdate - - /methods/pages/groupbypublishdate - - /methods/pages/groupbydate - - /methods/pages/groupbydate - - /methods/pages/groupbydate - - /methods/pages/groupbydate - - /methods/pages/groupbydate - - /methods/pages/groupbydate - - /methods/pages/reverse -methods_pages_navigation: - - /methods/pages/next - - /methods/pages/prev -methods_page_navigation: - - /methods/page/next - - /methods/page/nextinsection - - /methods/page/prev - - /methods/page/previnsection diff --git a/docs/data/references.toml b/docs/data/references.toml new file mode 100644 index 000000000..84d8c5935 --- /dev/null +++ b/docs/data/references.toml @@ -0,0 +1,211 @@ + +[[quotes]] +name = "Execute‏‏" +twitter_handle = "@executerun" +quote = "Hah, #gohugo. I was working with #gohugo on #linux but now I realised how easy is to set-up it on #windows. Just need to add binary to #path!" +link = "https://twitter.com/executerun/status/809753145270272005" +date = 2016-12-16T00:00:00Z + + +[[quotes]] +name = "Janez Čadež‏" +twitter_handle = "@jamziSLO" +quote = "Building @garazaFRI website in #hugo. This static site generator is soooo damn fast!" +link = "https://twitter.com/jamziSLO/status/817720283977183234" +date = 2017-01-07T00:00:00Z + +[[quotes]] +name = "Hans Beck" +twitter_handle = "@EnrichedGamesHB" +quote = "Diving deeper into @GoHugoIO . A lot of docs there, top work! But I've the impressed that #gohugo is far easier than its feels from the docs!" +link = "https://twitter.com/EnrichedGamesHB/status/836854762440130560" +date = 2017-03-01T00:00:00Z + +[[quotes]] +name = "Alan Richardson" +twitter_handle = "@eviltester" +quote = "I migrated the @BlackOpsTesting .com website from docpad to Hugo last weekend. http://gohugo.io/ Super Fast HTML Generation @spf13 " +link = "https://twitter.com/eviltester/status/553520335115808768" +date = 2015-01-09T00:00:00Z + +[[quotes]] +name = "Baron Schwartz" +twitter_handle = "@xaprb" +quote = "Hugo is impressively capable. It's a static site generator by @spf13 written in #golang . Just upgraded to latest release; very powerful. " +link = "https://twitter.com/xaprb/status/556894866488455169" +date = 2015-01-18T00:00:00Z + +[[quotes]] +name = "Dave Cottlehuber" +twitter_handle = "@dch__" +quote = "I just fell in love with #hugo, a static site/blog engine written by @spf13 in #golang + stellar docs" +link = "https://twitter.com/dch__/status/460158115498176512" +date = 2014-04-26T00:00:00Z + +[[quotes]] +name = "David Caunt" +twitter_handle = "@dcaunt" +quote = "I had a play with Hugo and it was good, uses Markdown files for content" +link = "https://twitter.com/dcaunt/statuses/406466996277374976" +date = 2013-11-29T00:00:00Z + +[[quotes]] +name = "David Gay" +twitter_handle = "@oddshocks" +quote = "Hugo is super-rad." +link = "https://twitter.com/oddshocks/statuses/405083217893421056" +date = 2013-11-25T00:00:00Z + +[[quotes]] +name = "Diti" +twitter_handle = "@DitiPengi" +quote = "The dev version of Hugo is AWESOME! <3 I promise, I will try to learn go ASAP and help contribute to the project! Just too great!" +link = "https://twitter.com/DitiPengi/status/472470974051676160" +date = 2014-05-30T00:00:00Z + +[[quotes]] +name = "Douglas Stephen " +twitter_handle = "@DougStephenJr" +quote = "Even as a long-time Octopress fan, I’ve gotta admit that this project Hugo looks very very cool" +link = "https://twitter.com/DougStephenJr/statuses/364512471660249088" +date = 2013-08-05T00:00:00Z + +[[quotes]] +name = "Hugo Rodger-Brown" +twitter_handle = "@hugorodgerbrown" +quote = "Finally someone builds me my own static site generator" +link = "https://twitter.com/hugorodgerbrown/statuses/364417910153818112" +date = 2013-05-08T00:00:00Z + +[[quotes]] +name = "Hugo Roy" +twitter_handle = "@hugoroyd" +quote = "Finally the answer to the question my parents have been asking: What does Hugo do?" +link = "https://twitter.com/hugoroyd/status/501704796727173120" +date = 2014-08-19T00:00:00Z + +[[quotes]] +name = "Daniel Miessler" +twitter_handle = "@DanielMiessler" +quote = "Websites for named vulnerabilities should run on static site generator platforms like Hugo. Read-only + burst traffic = static." +link = "https://twitter.com/DanielMiessler/status/704703841673957376" +date = 2016-03-01T00:00:00Z + +[[quotes]] +name = "Javier Segura" +twitter_handle = "@jsegura" +quote = "Another site generated with Hugo here! I'm getting in love with it." +link = "https://twitter.com/jsegura/status/465978434154659841" +date = 2014-05-12T00:00:00Z + +[[quotes]] +name = "Jim Biancolo" +twitter_handle = "@jimbiancolo" +quote = "I’m loving the static site generator renaissance we are currently enjoying. Hugo is new, looks great, written in Go" +link = "https://twitter.com/jimbiancolo/statuses/408678420348813314" +date = 2013-05-12T00:00:00Z + +[[quotes]] +name = "Jip J. Dekker" +twitter_handle = "@jipjdekker" +quote = "Building a personal website in Hugo. Works like a charm. And written in @golang!" +link = "https://twitter.com/jipjdekker/status/413783548735152131" +date = 2013-12-19T00:00:00Z + +[[quotes]] +name = "Jose Gonzalvo" +twitter_handle = "@jgonzalvo" +quote = "Checking out Hugo; Loving it so far. Like Jekyll but not so blog-oriented and written in go" +link = "https://twitter.com/jgonzalvo/statuses/408177855819173888" +date = 2013-12-04T00:00:00Z + +[[quotes]] +name = "Josh Matz" +twitter_handle = "@joshmatz" +quote = "A static site generator without the long build times? Yes, please!" +link = "https://twitter.com/joshmatz/statuses/364437436870696960" +date = 2013-08-05T00:00:00Z + +[[quotes]] +name = "Kieran Healy" +twitter_handle = "@kjhealy" +quote = "OK, so in today's speed battle of static site generators, @spf13's hugo is kicking everyone's ass, by miles." +link = "https://twitter.com/kjhealy/status/437349384809115648" +date = 2014-02-22T00:00:00Z + +[[quotes]] +name = "Ludovic Chabant" +twitter_handle = "@ludovicchabant" +quote = "Good work on Hugo, I’m impressed with the speed!" +link = "https://twitter.com/ludovicchabant/statuses/408806199602053120" +date = 2013-12-06T00:00:00Z + +[[quotes]] +name = "Luke Holder" +twitter_handle = "@lukeholder" +quote = "this is AWESOME. a single little executable and so fast." +link = "https://twitter.com/lukeholder/status/430352287936946176" +date = 2014-02-03T00:00:00Z + +[[quotes]] +name = "Markus Eliasson" +twitter_handle = "@markuseliasson" +quote = "Hugo is fast, dead simple to setup and well documented" +link = "https://twitter.com/markuseliasson/status/501594865877008384" +date = 2014-08-19T00:00:00Z + +[[quotes]] +name = "mercime" +twitter_handle = "@mercime_one" +quote = "Hugo: Makes the Web Fun Again" +link = "https://twitter.com/mercime_one/status/500547145087205377" +date = 2014-08-16T00:00:00Z + +[[quotes]] +name = "Michael Whatcott" +twitter_handle = "@mdwhatcott" +quote = "One more satisfied #Hugo blogger. Thanks @spf13 and friends!" +link = "https://twitter.com/mdwhatcott/status/469980686531571712" +date = 2014-05-23T00:00:00Z + +[[quotes]] +name = "Nathan Toups" +twitter_handle = "@rojoroboto" +quote = "I love Hugo! My site is generated with it now http://rjrbt.io" +link = "https://twitter.com/rojoroboto/status/423439915620106242" +date = 2014-01-15T00:00:00Z + +[[quotes]] +name = "Ruben Solvang" +twitter_handle = "@messo85" +quote = "#Hugo is the new @jekyllrb / @middlemanapp! Faster, easier and runs everywhere." +link = "https://twitter.com/messo85/status/472825062027182081" +date = 2014-05-31T00:00:00Z + +[[quotes]] +name = "Ryan Martinsen" +twitter_handle = "@popthestack" +quote = "Also, I re-launched my blog (it looks the same as before) using Hugo, a *fast* static engine. Very happy with it. gohugo.io" +link = "https://twitter.com/popthestack/status/549972754125307904" +date = 2014-12-30T00:00:00Z + +[[quotes]] +name = "The Lone Cuber" +twitter_handle = "@TheLoneCuber" +quote = "Jekyll is dead to me these days though... long live Hugo! Hugo is *by far* the best in its field. Thanks for making it happen." +link = "https://twitter.com/TheLoneCuber/status/495716684456398848" +date = 2014-08-02T00:00:00Z + +[[quotes]] +name = "The Lone Cuber" +twitter_handle = "@TheLoneCuber" +quote = "Finally, a publishing platform that's a joy to use. #NoMoreBarriers" +link = "https://twitter.com/TheLoneCuber/status/495731334711488512" +date = 2014-08-02T00:00:00Z + +[[quotes]] +name = "WorkHTML" +twitter_handle = "@workhtml" +quote = " #Hugo A very good alternative for #wordpress !!! A fast and modern static website engine gohugo.io " +link = "https://twitter.com/workhtml/status/563064361301053440" +date = 2015-02-04T00:00:00Z diff --git a/docs/data/sponsors.toml b/docs/data/sponsors.toml deleted file mode 100644 index 705ca9746..000000000 --- a/docs/data/sponsors.toml +++ /dev/null @@ -1,22 +0,0 @@ -[[banners]] - name = "Linode" - link = "https://www.linode.com/" - logo = "images/sponsors/linode-logo.svg" - utm_campaign = "hugosponsor" - bgcolor = "#ffffff" - -[[banners]] - name = "GoLand" - title = "The complete IDE crafted for professional Go developers." - no_query_params = true - link = "https://www.jetbrains.com/go/?utm_source=OSS&utm_medium=referral&utm_campaign=hugo" - logo = "images/sponsors/goland.svg" - bgcolor = "#f4f4f4" - -[[banners]] - name = "Your Company?" - link = "https://bep.is/en/hugo-sponsor-2023-01/" - utm_campaign = "hugosponsor" - show_on_hover = true - bgcolor = "#4e4f4f" - link_attr = "style='color: #ffffff; font-weight: bold; text-decoration: none; text-align: center'" diff --git a/docs/data/titles.toml b/docs/data/titles.toml new file mode 100644 index 000000000..2348c8561 --- /dev/null +++ b/docs/data/titles.toml @@ -0,0 +1,2 @@ +[Showcase] +title = "Site Showcase" diff --git a/docs/go.mod b/docs/go.mod deleted file mode 100644 index 4b9e0a369..000000000 --- a/docs/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/gohugoio/hugoDocs - -go 1.22.0 diff --git a/docs/go.sum b/docs/go.sum deleted file mode 100644 index af9b5febf..000000000 --- a/docs/go.sum +++ /dev/null @@ -1,2 +0,0 @@ -github.com/gohugoio/gohugoioTheme v0.0.0-20250116152525-2d382cae7743 h1:gjoqq8+RnGwpuU/LQVYGGR/LsDplrfUjOabWwoROYsM= -github.com/gohugoio/gohugoioTheme v0.0.0-20250116152525-2d382cae7743/go.mod h1:GOYeAPQJ/ok8z7oz1cjfcSlsFpXrmx6VkzQ5RpnyhZM= diff --git a/docs/hugo.toml b/docs/hugo.toml deleted file mode 100644 index e8373a87c..000000000 --- a/docs/hugo.toml +++ /dev/null @@ -1,171 +0,0 @@ -baseURL = "https://gohugo.io/" -defaultContentLanguage = "en" -enableEmoji = true -pluralizeListTitles = false -timeZone = "Europe/Oslo" -title = "Hugo" - -# We do redirects via Netlify's _redirects file, generated by Hugo (see "outputs" below). -disableAliases = true - -[build] - [build.buildStats] - disableIDs = true - enable = true - [[build.cachebusters]] - source = "assets/notwatching/hugo_stats\\.json" - target = "css" - [[build.cachebusters]] - source = "(postcss|tailwind)\\.config\\.js" - target = "css" - -[caches] - [caches.images] - dir = ":cacheDir/images" - maxAge = "1440h" - [caches.getresource] - dir = ':cacheDir/:project' - maxAge = "1h" - -[cascade] - [cascade.params] - hide_in_this_section = true - show_publish_date = true - [cascade.target] - kind = 'page' - path = '{/news/**}' - -[frontmatter] - date = ['date'] # do not add publishdate; it will affect page sorting - expiryDate = ['expirydate'] - lastmod = [':git', 'lastmod', 'publishdate', 'date'] - publishDate = ['publishdate', 'date'] - -[languages] - [languages.en] - languageCode = "en-US" - languageName = "English" - weight = 1 - -[markup] - [markup.goldmark] - [markup.goldmark.extensions] - [markup.goldmark.extensions.typographer] - disable = false - [markup.goldmark.extensions.passthrough] - enable = true - [markup.goldmark.extensions.passthrough.delimiters] - block = [['\[', '\]'], ['$$', '$$']] - inline = [['\(', '\)']] - [markup.goldmark.parser] - autoDefinitionTermID = true - [markup.goldmark.parser.attribute] - block = true - [markup.highlight] - lineNumbersInTable = false - noClasses = false - style = 'solarized-dark' - wrapperClass = 'highlight not-prose' - -[mediaTypes] - [mediaTypes."text/netlify"] - delimiter = "" - -[module] - [module.hugoVersion] - min = "0.144.0" - [[module.mounts]] - source = "assets" - target = "assets" - [[module.mounts]] - lang = 'en' - source = 'content/en' - target = 'content' - [[module.mounts]] - disableWatch = true - source = "hugo_stats.json" - target = "assets/notwatching/hugo_stats.json" - -[outputFormats] - [outputFormats.redir] - baseName = "_redirects" - isPlainText = true - mediatype = "text/netlify" - [outputFormats.headers] - baseName = "_headers" - isPlainText = true - mediatype = "text/netlify" - notAlternative = true - -[outputs] - home = ["html", "rss", "redir", "headers"] - page = ["html"] - section = ["html"] - taxonomy = ["html"] - term = ["html"] - -[params] - description = "The world’s fastest framework for building websites" - ghrepo = "https://github.com/gohugoio/hugoDocs/" - [params.render_hooks.link] - errorLevel = 'warning' # ignore (default), warning, or error (fails the build) - -[related] - includeNewer = true - threshold = 80 - toLower = true - [[related.indices]] - name = 'keywords' - weight = 1 - -[security] - [security.funcs] - getenv = ['^HUGO_', '^REPOSITORY_URL$', '^BRANCH$'] - -[server] - [[server.headers]] - for = "/*" - [server.headers.values] - X-Frame-Options = "DENY" - X-XSS-Protection = "1; mode=block" - X-Content-Type-Options = "nosniff" - Referrer-Policy = "no-referrer" - [[server.headers]] - for = "/**.{css,js}" - -[services] - [services.googleAnalytics] - ID = 'G-MBZGKNMDWC' - -[taxonomies] -category = 'categories' - -######## GLOBAL ITEMS TO BE SHARED WITH THE HUGO SITES ######## -[menus] - [[menus.global]] - identifier = 'news' - name = 'News' - pageRef = '/news/' - weight = 1 - [[menus.global]] - identifier = 'docs' - name = 'Docs' - url = '/documentation/' - weight = 5 - [[menus.global]] - identifier = 'themes' - name = 'Themes' - url = 'https://themes.gohugo.io/' - weight = 10 - [[menus.global]] - identifier = 'community' - name = 'Community' - post = 'external' - url = 'https://discourse.gohugo.io/' - weight = 150 - [[menus.global]] - identifier = 'github' - name = 'GitHub' - post = 'external' - url = 'https://github.com/gohugoio/hugo' - weight = 200 diff --git a/docs/hugo.work b/docs/hugo.work deleted file mode 100644 index 02c0ba91f..000000000 --- a/docs/hugo.work +++ /dev/null @@ -1,4 +0,0 @@ -go 1.22.0 - -use . - diff --git a/docs/hugoreleaser.yaml b/docs/hugoreleaser.yaml deleted file mode 100644 index 9f8671e06..000000000 --- a/docs/hugoreleaser.yaml +++ /dev/null @@ -1,29 +0,0 @@ -project: hugoDocs -release_settings: - name: ${HUGORELEASER_TAG} - type: github - repository: hugoDocs - repository_owner: gohugoio - draft: true - prerelease: false - release_notes_settings: - generate: true - generate_on_host: false - short_threshold: 10 - short_title: What's Changed - groups: - - regexp: snapcraft:|Merge commit|Merge branch|netlify:|release:|Squashed - ignore: true - - title: Typo fixes - regexp: typo - ordinal: 20 - - title: Dependency Updates - regexp: deps - ordinal: 30 - - title: Improvements - regexp: .* - ordinal: 10 -releases: - - paths: - - archives/** - path: myrelease diff --git a/docs/layouts/404.html b/docs/layouts/404.html index 6d962ffc0..4b140fbdb 100644 --- a/docs/layouts/404.html +++ b/docs/layouts/404.html @@ -1,22 +1,5 @@ -{{ define "main" }} -
      -
      -

      - Page not found - gopher -

      - - -
      -
      -{{ end }} +{{ partial "header.html" . }} +
      +Page Not Found. +
      +{{ partial "footer.html" . }} diff --git a/docs/layouts/_default/baseof.html b/docs/layouts/_default/baseof.html new file mode 100644 index 000000000..076f46dda --- /dev/null +++ b/docs/layouts/_default/baseof.html @@ -0,0 +1,12 @@ +{{ partial "header.html" . }} +{{ if .Params.toc }} +
      +{{block "main" .}}{{end}} +
      +
      + {{ .TableOfContents }} +
      +{{ else }} +{{block "main" .}}{{end}} +{{ end }} +{{ partial "footer.html" . }} diff --git a/docs/layouts/_default/list.html b/docs/layouts/_default/list.html new file mode 100644 index 000000000..f2b122c40 --- /dev/null +++ b/docs/layouts/_default/list.html @@ -0,0 +1,12 @@ +{{ define "main" }} +{{ with .Content }} +{{ . }} +{{ end }} +
        + {{ range .Data.Pages }} +
      • + {{ .Title }} Updated {{ .Lastmod.Format "Mon, Jan 2, 2006" }} +
      • + {{ end }} +
      +{{ end }} diff --git a/docs/layouts/_default/single.html b/docs/layouts/_default/single.html new file mode 100644 index 000000000..b8bcfa70c --- /dev/null +++ b/docs/layouts/_default/single.html @@ -0,0 +1 @@ +{{ define "main" }}{{ .Content }}{{ end }} diff --git a/docs/layouts/_markup/render-blockquote.html b/docs/layouts/_markup/render-blockquote.html deleted file mode 100644 index 98019e12d..000000000 --- a/docs/layouts/_markup/render-blockquote.html +++ /dev/null @@ -1,33 +0,0 @@ -{{- if eq .Type "alert" }} - {{- $alerts := dict - "caution" (dict "color" "red" "icon" "exclamation-triangle") - "important" (dict "color" "blue" "icon" "exclamation-circle") - "note" (dict "color" "blue" "icon" "information-circle") - "tip" (dict "color" "green" "icon" "light-bulb") - "warning" (dict "color" "orange" "icon" "exclamation-triangle") - }} - - {{- $alertTypes := slice }} - {{- range $k, $_ := $alerts }} - {{- $alertTypes = $alertTypes | append $k }} - {{- end }} - {{- $alertTypes = $alertTypes | sort }} - - {{- $alertType := strings.ToLower .AlertType }} - {{- if in $alertTypes $alertType }} - {{- partial "layouts/blocks/alert.html" (dict - "color" (or ((index $alerts $alertType).color) "blue") - "icon" (or ((index $alerts $alertType).icon) "information-circle") - "text" .Text - "title" .AlertTitle - "class" .Attributes.class - ) - }} - {{- else }} - {{- errorf `Invalid blockquote alert type. Received %s. Expected one of %s (case-insensitive). See %s.` .AlertType (delimit $alertTypes ", " ", or ") .Page.String }} - {{- end }} -{{- else }} -
      - {{ .Text }} -
      -{{- end }} diff --git a/docs/layouts/_markup/render-codeblock.html b/docs/layouts/_markup/render-codeblock.html deleted file mode 100644 index 13725ffcd..000000000 --- a/docs/layouts/_markup/render-codeblock.html +++ /dev/null @@ -1,98 +0,0 @@ -{{/* prettier-ignore-start */}} -{{/* -Renders a highlighted code block using the given options and attributes. - -In addition to the options available to the transform.Highlight function, you -may also specify the following parameters: - -@param {bool} [copy=false] Whether to display a copy-to-clipboard button. -@param {string} [file] The file name to display above the rendered code. -@param {bool} [details=false] Whether to wrap the highlighted code block within a details element. -@param {bool} [open=false] Whether to initially display the content of the details element. -@param {string} [summary=Details] The content of the details summary element rendered from Markdown to HTML. - -@returns {template.HTML} - -@examples - - ```go - fmt.Println("Hello world!") - ``` - - ```go {linenos=true file="layouts/index.html" copy=true} - fmt.Println("Hello world!") - ``` -*/}} -{{/* prettier-ignore-end */}} - -{{- $copy := false }} -{{- $file := or .Attributes.file "" }} -{{- $details := false }} -{{- $open := "" }} -{{- $summary := or .Attributes.summary "Details" | .Page.RenderString }} -{{- $ext := strings.TrimPrefix "." (path.Ext $file) }} -{{- $lang := or .Type $ext "text" }} -{{- if in (slice "html" "gotmpl") $lang }} - {{- $lang = "go-html-template" }} -{{- end }} -{{- if eq $lang "md" }} - {{- $lang = "text" }} -{{- end }} - -{{- with .Attributes.copy }} - {{- if in (slice true "true" 1) . }} - {{- $copy = true }} - {{- else if in (slice false "false" 0) . }} - {{- $copy = false }} - {{- end }} -{{- end }} - -{{- with .Attributes.details }} - {{- if in (slice true "true" 1) . }} - {{- $details = true }} - {{- else if in (slice false "false" 0) . }} - {{- $details = false }} - {{- end }} -{{- end }} - -{{- with .Attributes.open }} - {{- if in (slice true "true" 1) . }} - {{- $open = "open" }} - {{- else if in (slice false "false" 0) . }} - {{- $open = "" }} - {{- end }} -{{- end }} - -{{- if $details }} -
      - {{ $summary }} -{{- end }} - -
      - {{- $fileSelectClass := "select-none" }} - {{- if $copy }} - {{- $fileSelectClass = "select-text" }} - - - - {{- end }} - {{- with $file }} -
      - {{ . }} -
      - {{- end }} - -
      - {{- transform.Highlight (strings.TrimSpace .Inner) $lang .Options }} -
      -
      - -{{- if $details }} -
      -{{- end }} diff --git a/docs/layouts/_markup/render-link.html b/docs/layouts/_markup/render-link.html deleted file mode 100644 index 70011220e..000000000 --- a/docs/layouts/_markup/render-link.html +++ /dev/null @@ -1,320 +0,0 @@ -{{/* prettier-ignore-start */ -}} -{{- /* Last modified: 2025-01-19T14:44:56-08:00 */}} - -{{- /* -Copyright 2025 Veriphor LLC - -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 - -https://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. -*/}} - -{{- /* -This render hook resolves internal destinations by looking for a matching: - - 1. Content page - 2. Page resource (a file in the current page bundle) - 3. Section resource (a file in the current section) - 4. Global resource (a file in the assets directory) - -It skips the section resource lookup if the current page is a leaf bundle. - -External destinations are not modified. - -You must place global resources in the assets directory. If you have placed -your resources in the static directory, and you are unable or unwilling to move -them, you must mount the static directory to the assets directory by including -both of these entries in your site configuration: - - [[module.mounts]] - source = 'assets' - target = 'assets' - - [[module.mounts]] - source = 'static' - target = 'assets' - -By default, if this render hook is unable to resolve a destination, including a -fragment if present, it passes the destination through without modification. To -emit a warning or error, set the error level in your site configuration: - - [params.render_hooks.link] - errorLevel = 'warning' # ignore (default), warning, or error (fails the build) - -When you set the error level to warning, and you are in a development -environment, you can visually highlight broken internal links: - - [params.render_hooks.link] - errorLevel = 'warning' # ignore (default), warning, or error (fails the build) - highlightBroken = true # true or false (default) - -This will add a "broken" class to anchor elements with invalid src attributes. -Add a rule to your CSS targeting the broken links: - - a.broken { - background: #ff0; - border: 2px solid #f00; - padding: 0.1em 0.2em; - } - -This render hook may be unable to resolve destinations created with the ref and -relref shortcodes. Unless you set the error level to ignore you should not use -either of these shortcodes in conjunction with this render hook. - -@context {string} Destination The link destination. -@context {page} Page A reference to the page containing the link. -@context {string} PlainText The link description as plain text. -@context {string} Text The link description. -@context {string} Title The link title. - -@returns {template.html} -*/ -}} -{{/* prettier-ignore-end */ -}} -{{- /* Initialize. */}} -{{- $renderHookName := "link" }} - -{{- /* Verify minimum required version. */}} -{{- $minHugoVersion := "0.141.0" }} -{{- if lt hugo.Version $minHugoVersion }} - {{- errorf "The %q render hook requires Hugo v%s or later." $renderHookName $minHugoVersion }} -{{- end }} - -{{- /* Error level when unable to resolve destination: ignore, warning, or error. */}} -{{- $errorLevel := or site.Params.render_hooks.link.errorLevel "ignore" | lower }} - -{{- /* If true, adds "broken" class to broken links. Applicable in development environment when errorLevel is warning. */}} -{{- $highlightBrokenLinks := or site.Params.render_hooks.link.highlightBroken false }} - -{{- /* Validate error level. */}} -{{- if not (in (slice "ignore" "warning" "error") $errorLevel) }} - {{- errorf "The %q render hook is misconfigured. The errorLevel %q is invalid. Please check your site configuration." $renderHookName $errorLevel }} -{{- end }} - -{{- /* Determine content path for warning and error messages. */}} -{{- $contentPath := .Page.String }} - -{{- /* Parse destination. */}} -{{- $u := urls.Parse .Destination }} - -{{- /* Set common message. */}} -{{- $msg := printf "The %q render hook was unable to resolve the destination %q in %s" $renderHookName $u.String $contentPath }} - -{{- /* Set attributes for anchor element. */}} -{{- $attrs := dict "href" $u.String }} -{{- if eq $u.String "g" }} - {{- /* Destination is a glossary term. */}} - {{- $ctx := dict - "contentPath" $contentPath - "errorLevel" $errorLevel - "renderHookName" $renderHookName - "text" .Text - }} - {{- $attrs = partial "inline/h-rh-l/get-glossary-link-attributes.html" $ctx }} -{{- else if $u.IsAbs }} - {{- /* Destination is a remote resource. */}} - {{- $attrs = merge $attrs (dict "rel" "external") }} -{{- else }} - {{- with $u.Path }} - {{- with $p := or ($.PageInner.GetPage .) ($.PageInner.GetPage (strings.TrimRight "/" .)) }} - {{- /* Destination is a page. */}} - {{- $href := .RelPermalink }} - {{- with $u.RawQuery }} - {{- $href = printf "%s?%s" $href . }} - {{- end }} - {{- with $u.Fragment }} - {{- $ctx := dict - "contentPath" $contentPath - "errorLevel" $errorLevel - "page" $p - "parsedURL" $u - "renderHookName" $renderHookName - }} - {{- partial "inline/h-rh-l/validate-fragment.html" $ctx }} - {{- $href = printf "%s#%s" $href . }} - {{- end }} - {{- $attrs = dict "href" $href }} - {{- else with $.PageInner.Resources.Get $u.Path }} - {{- /* Destination is a page resource; drop query and fragment. */}} - {{- $attrs = dict "href" .RelPermalink }} - {{- else with (and (ne $.Page.BundleType "leaf") ($.Page.CurrentSection.Resources.Get $u.Path)) }} - {{- /* Destination is a section resource, and current page is not a leaf bundle. */}} - {{- $attrs = dict "href" .RelPermalink }} - {{- else with resources.Get $u.Path }} - {{- /* Destination is a global resource; drop query and fragment. */}} - {{- $attrs = dict "href" .RelPermalink }} - {{- else }} - {{- if eq $errorLevel "warning" }} - {{- warnf $msg }} - {{- if and $highlightBrokenLinks hugo.IsDevelopment }} - {{- $attrs = merge $attrs (dict "class" "broken") }} - {{- end }} - {{- else if eq $errorLevel "error" }} - {{- errorf $msg }} - {{- end }} - {{- end }} - {{- else }} - {{- with $u.Fragment }} - {{- /* Destination is on the same page; prepend relative permalink. */}} - {{- $ctx := dict - "contentPath" $contentPath - "errorLevel" $errorLevel - "page" $.Page - "parsedURL" $u - "renderHookName" $renderHookName - }} - {{- partial "inline/h-rh-l/validate-fragment.html" $ctx }} - {{- $attrs = dict "href" (printf "%s#%s" $.Page.RelPermalink .) }} - {{- else }} - {{- if eq $errorLevel "warning" }} - {{- warnf $msg }} - {{- if and $highlightBrokenLinks hugo.IsDevelopment }} - {{- $attrs = merge $attrs (dict "class" "broken") }} - {{- end }} - {{- else if eq $errorLevel "error" }} - {{- errorf $msg }} - {{- end }} - {{- end }} - {{- end }} -{{- end }} - -{{- /* Render anchor element. */ -}} -{{ .Text }} - -{{- define "_partials/inline/h-rh-l/validate-fragment.html" }} - {{- /* - Validates the fragment portion of a link destination. - - @context {string} contentPath The page containing the link. - @context {string} errorLevel The error level when unable to resolve destination; ignore (default), warning, or error. - @context {page} page The page corresponding to the link destination - @context {struct} parsedURL The link destination parsed by urls.Parse. - @context {string} renderHookName The name of the render hook. - */}} - - {{- /* Initialize. */}} - {{- $contentPath := .contentPath }} - {{- $errorLevel := .errorLevel }} - {{- $p := .page }} - {{- $u := .parsedURL }} - {{- $renderHookName := .renderHookName }} - - {{- /* Validate. */}} - {{- with $u.Fragment }} - {{- if $p.Fragments.Identifiers.Contains . }} - {{- if gt ($p.Fragments.Identifiers.Count .) 1 }} - {{- $msg := printf "The %q render hook detected duplicate heading IDs %q in %s" $renderHookName . $contentPath }} - {{- if eq $errorLevel "warning" }} - {{- warnf $msg }} - {{- else if eq $errorLevel "error" }} - {{- errorf $msg }} - {{- end }} - {{- end }} - {{- else }} - {{- /* Determine target path for warning and error message. */}} - {{- $targetPath := "" }} - {{- with $p.File }} - {{- $targetPath = .Path }} - {{- else }} - {{- $targetPath = .Path }} - {{- end }} - {{- /* Set common message. */}} - {{- $msg := printf "The %q render hook was unable to find heading ID %q in %s. See %s" $renderHookName . $targetPath $contentPath }} - {{- if eq $targetPath $contentPath }} - {{- $msg = printf "The %q render hook was unable to find heading ID %q in %s" $renderHookName . $targetPath }} - {{- end }} - {{- /* Throw warning or error. */}} - {{- if eq $errorLevel "warning" }} - {{- warnf $msg }} - {{- else if eq $errorLevel "error" }} - {{- errorf $msg }} - {{- end }} - {{- end }} - {{- end }} -{{- end }} - -{{- define "_partials/inline/h-rh-l/get-glossary-link-attributes.html" }} - {{- /* - Returns the anchor element attributes for a link to the given glossary term. - - It first checks for the existence of a glossary page for the given term. If - no page is found, it then checks for a glossary page for the singular form of - the term. If neither page exists it throws a warning or error dependent on - the errorLevel setting - - The returned href attribute does not point to the glossary term page. - Instead, via its fragment, it points to an entry on the glossary page. - - @context {string} contentPath The page containing the link. - @context {string} errorLevel The error level when unable to resolve destination; ignore (default), warning, or error. - @context {string} renderHookName The name of the render hook. - @context {string} text The link text. - */}} - - {{- /* Get context.. */}} - {{- $contentPath := .contentPath }} - {{- $errorLevel := .errorLevel }} - {{- $renderHookName := .renderHookName }} - {{- $text := .text | transform.Plainify | strings.ToLower }} - - {{- /* Initialize. */}} - {{- $glossaryPath := "/quick-reference/glossary" }} - {{- $termGiven := $text }} - {{- $termActual := "" }} - {{- $termSingular := inflect.Singularize $termGiven }} - - {{- /* Verify that the glossary page exists. */}} - {{- $glossaryPage := site.GetPage $glossaryPath }} - {{- if not $glossaryPage }} - {{- errorf "The %q render hook was unable to find %s: see %s" $renderHookName $glossaryPath $contentPath }} - {{- end }} - - {{- /* There's a better way to handle this, but it works for now. */}} - {{- $cheating := dict - "chaining" "chain" - "localize" "localization" - "localized" "localization" - "paginating" "paginate" - "walking" "walk" - "ci/cd" "cicd" - }} - - {{- /* Verify that a glossary term page exists for the given term. */}} - {{- if site.GetPage (urls.JoinPath $glossaryPath ($termGiven | urlize)) }} - {{- $termActual = $termGiven }} - {{- else if site.GetPage (urls.JoinPath $glossaryPath ($termSingular | urlize)) }} - {{- $termActual = $termSingular }} - {{- else }} - {{- $termToTest := index $cheating $termGiven }} - {{- if site.GetPage (urls.JoinPath $glossaryPath ($termToTest | urlize)) }} - {{- $termActual = $termToTest }} - {{- end }} - {{- end }} - - {{- if not $termActual }} - {{- errorf "The %q render hook was unable to find a glossary page for either the singular or plural form of the term %q: see %s" $renderHookName $termGiven $contentPath }} - {{- end }} - - {{- /* Create the href attribute. */}} - {{- $href := "" }} - {{- if $termActual }} - {{- $href = fmt.Printf "%s#%s" $glossaryPage.RelPermalink (anchorize $termActual) }} - {{- end }} - - {{- return (dict "href" $href) }} -{{- end -}} diff --git a/docs/layouts/_markup/render-passthrough.html b/docs/layouts/_markup/render-passthrough.html deleted file mode 100644 index 0ed001133..000000000 --- a/docs/layouts/_markup/render-passthrough.html +++ /dev/null @@ -1,9 +0,0 @@ -{{- $opts := dict "output" "htmlAndMathml" "displayMode" (eq .Type "block") }} -{{- with try (transform.ToMath .Inner $opts) }} - {{- with .Err }} - {{ errorf "Unable to render mathematical markup to HTML using the transform.ToMath function. The KaTeX display engine threw the following error: %s: see %s." . $.Position }} - {{- else }} - {{- .Value }} - {{- $.Page.Store.Set "hasMath" true }} - {{- end }} -{{- end -}} diff --git a/docs/layouts/_markup/render-table.html b/docs/layouts/_markup/render-table.html deleted file mode 100644 index 7f3a88601..000000000 --- a/docs/layouts/_markup/render-table.html +++ /dev/null @@ -1,31 +0,0 @@ -
      - - - {{- range .THead }} - - {{- range . }} - - {{- end }} - - {{- end }} - - - {{- range .TBody }} - - {{- range . }} - - {{- end }} - - {{- end }} - -
      - {{- .Text -}} -
      - {{- .Text -}} -
      -
      diff --git a/docs/layouts/_partials/docs/functions-aliases.html b/docs/layouts/_partials/docs/functions-aliases.html deleted file mode 100644 index b3a5a7607..000000000 --- a/docs/layouts/_partials/docs/functions-aliases.html +++ /dev/null @@ -1,12 +0,0 @@ -{{- with .Params.functions_and_methods.aliases }} - {{- $label := "Alias" }} - {{- if gt (len .) 1 }} - {{- $label = "Aliases" }} - {{- end }} -

      {{ $label }}

      - {{- range . }} -
      - {{- . -}} -
      - {{- end }} -{{- end -}} diff --git a/docs/layouts/_partials/docs/functions-return-type.html b/docs/layouts/_partials/docs/functions-return-type.html deleted file mode 100644 index 75c97f8d9..000000000 --- a/docs/layouts/_partials/docs/functions-return-type.html +++ /dev/null @@ -1,6 +0,0 @@ -{{- with .Params.functions_and_methods.returnType }} -

      Returns

      -
      - {{- . -}} -
      -{{- end -}} diff --git a/docs/layouts/_partials/docs/functions-signatures.html b/docs/layouts/_partials/docs/functions-signatures.html deleted file mode 100644 index 6fb61df8e..000000000 --- a/docs/layouts/_partials/docs/functions-signatures.html +++ /dev/null @@ -1,12 +0,0 @@ -{{- with .Params.functions_and_methods.signatures }} -

      Syntax

      - {{- range . }} - {{- $signature := . }} - {{- if $.Params.function.returnType }} - {{- $signature = printf "%s ⟼ %s" . $.Params.function.returnType }} - {{- end }} -
      - {{- $signature -}} -
      - {{- end }} -{{- end -}} diff --git a/docs/layouts/_partials/helpers/debug/list-item-metadata.html b/docs/layouts/_partials/helpers/debug/list-item-metadata.html deleted file mode 100644 index d027484b5..000000000 --- a/docs/layouts/_partials/helpers/debug/list-item-metadata.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - -
      weight: - {{ .Weight }} -
      keywords: - {{ delimit (or .Keywords "") ", " }} -
      categories: - {{ delimit (or .Params.categories "") ", " }} -
      diff --git a/docs/layouts/_partials/helpers/funcs/color-from-string.html b/docs/layouts/_partials/helpers/funcs/color-from-string.html deleted file mode 100644 index cd599530b..000000000 --- a/docs/layouts/_partials/helpers/funcs/color-from-string.html +++ /dev/null @@ -1,25 +0,0 @@ -{{ $colors := slice "slate" "green" "cyan" "blue" }} -{{ with .single }} - {{ $colors = slice . }} -{{ end }} - -{{ $shades := slice 300 400 500 }} -{{ if not .dark }} - {{ $shades = slice 700 800 }} -{{ end }} -{{ $hash := (hash.FNV32a .text) }} -{{ $i := mod $hash (len $colors) }} -{{ $j := mod $hash (len $shades) }} -{{ $color := index $colors $i }} -{{ $shade1 := index $shades $j }} -{{ $shade2 := 0 }} -{{ $shade3 := 0 }} -{{ if gt $shade1 500 }} - {{ $shade2 = math.Min (sub $shade1 500) 100 | int }} - {{ $shade3 = sub $shade1 100 }} -{{ else }} - {{ $shade2 = math.Max (add $shade1 500) 700 | int }} - {{ $shade3 = add $shade1 200 }} -{{ end }} -{{ $res := dict "color" $color "shade1" $shade1 "shade2" $shade2 "shade3" $shade3 }} -{{ return $res }} diff --git a/docs/layouts/_partials/helpers/funcs/get-github-info.html b/docs/layouts/_partials/helpers/funcs/get-github-info.html deleted file mode 100644 index 7e2dc89fa..000000000 --- a/docs/layouts/_partials/helpers/funcs/get-github-info.html +++ /dev/null @@ -1,28 +0,0 @@ -{{ $url := "https://api.github.com/repos/gohugoio/hugo" }} -{{ $cacheKey := print $url (now.Format "2006-01-02") }} -{{ $headers := dict }} -{{ with os.Getenv "HUGO_GH_TOKEN" }} - {{ $headers = dict "Authorization" (printf "Bearer %s" .) }} -{{ end }} -{{ $opts := dict "headers" $headers "key" $cacheKey }} -{{ $githubRepoInfo := dict }} -{{ with try (resources.GetRemote $url $opts) }} - {{ with .Err }} - {{ warnf "Failed to get GitHub repo info: %s" . }} - {{ else with (.Value | transform.Unmarshal) }} - {{ $githubRepoInfo = dict - "html_url" .html_url - "stargazers_url" .stargazers_url - "watchers_count" .watchers_count - "stargazers_count" .stargazers_count - "forks_count" .forks_count - "contributors_url" .contributors_url - "releases_url" .releases_url - "forks_count" .forks_count - }} - {{ else }} - {{ errorf "Unable to get remote resource %q" $url }} - {{ end }} -{{ end }} - -{{ return $githubRepoInfo }} diff --git a/docs/layouts/_partials/helpers/funcs/get-remote-data.html b/docs/layouts/_partials/helpers/funcs/get-remote-data.html deleted file mode 100644 index ed7043421..000000000 --- a/docs/layouts/_partials/helpers/funcs/get-remote-data.html +++ /dev/null @@ -1,24 +0,0 @@ -{{/* prettier-ignore-start */ -}} -{{/* -Parses the serialized data from the given URL and returns a map or an array. - -Supports CSV, JSON, TOML, YAML, and XML. - -@param {string} . The URL from which to retrieve the serialized data. -@returns {any} - -@example {{ partial "get-remote-data.html" "https://example.org/foo.json" }} -*/}} -{{/* prettier-ignore-end */ -}} -{{ $url := . }} -{{ $data := dict }} -{{ with try (resources.GetRemote $url) }} - {{ with .Err }} - {{ errorf "%s" . }} - {{ else with .Value }} - {{ $data = .Content | transform.Unmarshal }} - {{ else }} - {{ errorf "Unable to get remote resource %q" $url }} - {{ end }} -{{ end }} -{{ return $data }} diff --git a/docs/layouts/_partials/helpers/gtag.html b/docs/layouts/_partials/helpers/gtag.html deleted file mode 100644 index 59bf36ba2..000000000 --- a/docs/layouts/_partials/helpers/gtag.html +++ /dev/null @@ -1,27 +0,0 @@ -{{ with site.Config.Services.GoogleAnalytics.ID }} - - -{{ end }} diff --git a/docs/layouts/_partials/helpers/linkcss.html b/docs/layouts/_partials/helpers/linkcss.html deleted file mode 100644 index 814d2b52f..000000000 --- a/docs/layouts/_partials/helpers/linkcss.html +++ /dev/null @@ -1,28 +0,0 @@ -{{ $r := .r }} -{{ $attr := .attributes | default dict }} - -{{ if hugo.IsDevelopment }} - -{{ else }} - {{ with $r | minify | fingerprint }} - - {{ end }} -{{ end }} - -{{ define "render-attributes" }} - {{- range $k, $v := . -}} - {{- if $v -}} - {{- printf ` %s=%q` $k $v | safeHTMLAttr -}} - {{- else -}} - {{- printf ` %s` $k | safeHTMLAttr -}} - {{- end -}} - {{- end -}} -{{ end }} diff --git a/docs/layouts/_partials/helpers/linkjs.html b/docs/layouts/_partials/helpers/linkjs.html deleted file mode 100644 index 00129ad38..000000000 --- a/docs/layouts/_partials/helpers/linkjs.html +++ /dev/null @@ -1,15 +0,0 @@ -{{ $r := .r }} -{{ $attr := .attributes | default dict }} -{{ if hugo.IsDevelopment }} - -{{ else }} - {{ with $r | fingerprint }} - - {{ end }} -{{ end }} diff --git a/docs/layouts/_partials/helpers/picture.html b/docs/layouts/_partials/helpers/picture.html deleted file mode 100644 index 4dc16c002..000000000 --- a/docs/layouts/_partials/helpers/picture.html +++ /dev/null @@ -1,27 +0,0 @@ -{{ $image := .image }} -{{ $width := .width | default 1000 }} -{{ $width1x := div $width 2 }} -{{ $imageWebp := $image.Resize (printf "%dx webp" $width) }} -{{ $image1x := $image.Resize (printf "%dx" $width1x) }} -{{ $image1xWebp := $image.Resize (printf "%dx webp" $width1x) }} -{{ $class := .class | default "h-64 tablet:h-96 lg:h-full w-full object-cover lg:absolute" }} -{{ $loading := .loading | default "eager" }} - - - - - - - diff --git a/docs/layouts/_partials/helpers/validation/validate-keywords.html b/docs/layouts/_partials/helpers/validation/validate-keywords.html deleted file mode 100644 index 3447ec4ef..000000000 --- a/docs/layouts/_partials/helpers/validation/validate-keywords.html +++ /dev/null @@ -1,22 +0,0 @@ -{{/* prettier-ignore-start */ -}} -{{- /* -We use the front matter keywords field to determine related content. To ensure -consistency, during site build we validate each keyword against the entries in -data/keywords.yaml. - -As of March 5, 2025, this feature is experimental, pending usability -assessment. We anticipate that the number of additions to data/keywords.yaml -will decrease over time, though the initial implementation will require some -effort. -*/}} -{{/* prettier-ignore-end */ -}} -{{- $t := debug.Timer "validateKeywords" }} -{{- $allowedKeywords := collections.Apply site.Data.keywords "strings.ToLower" "." }} -{{- range $p := site.Pages }} - {{- range .Params.keywords }} - {{- if not (in $allowedKeywords (lower .)) }} - {{- warnf "The word or phrase %q is not in the keywords data file. See %s." . $p.Page.String }} - {{- end }} - {{- end }} -{{- end }} -{{- $t.Stop }} diff --git a/docs/layouts/_partials/layouts/blocks/alert.html b/docs/layouts/_partials/layouts/blocks/alert.html deleted file mode 100644 index 45f0044d9..000000000 --- a/docs/layouts/_partials/layouts/blocks/alert.html +++ /dev/null @@ -1,25 +0,0 @@ -{{- $title := .title | default "" }} -{{- $color := .color | default "yellow" }} -{{- $icon := .icon | default "exclamation-triangle" }} -{{- $text := .text | default "" }} -{{- $class := .class | default "mt-6 mb-8" }} -
      -
      -
      - - - -
      -
      - {{- with $title }} -

      - {{ . }} -

      - {{- end }} -
      - {{ $text }} -
      -
      -
      -
      diff --git a/docs/layouts/_partials/layouts/blocks/modal.html b/docs/layouts/_partials/layouts/blocks/modal.html deleted file mode 100644 index 7d825c06e..000000000 --- a/docs/layouts/_partials/layouts/blocks/modal.html +++ /dev/null @@ -1,30 +0,0 @@ -
      -
      - {{ .modal_button }} -
      - -
      diff --git a/docs/layouts/_partials/layouts/breadcrumbs.html b/docs/layouts/_partials/layouts/breadcrumbs.html deleted file mode 100644 index 69bcf7bd5..000000000 --- a/docs/layouts/_partials/layouts/breadcrumbs.html +++ /dev/null @@ -1,44 +0,0 @@ -{{ $documentation := site.GetPage "/documentation" }} - - - - -{{ define "breadcrumbs-arrow" }} - - - -{{ end }} diff --git a/docs/layouts/_partials/layouts/date.html b/docs/layouts/_partials/layouts/date.html deleted file mode 100644 index 2ec1450a5..000000000 --- a/docs/layouts/_partials/layouts/date.html +++ /dev/null @@ -1,5 +0,0 @@ -{{ $humanDate := time.Format "January 2, 2006" . }} -{{ $machineDate := time.Format "2006-01-02T15:04:05-07:00" . }} - diff --git a/docs/layouts/_partials/layouts/docsheader.html b/docs/layouts/_partials/layouts/docsheader.html deleted file mode 100644 index 7e8e950f3..000000000 --- a/docs/layouts/_partials/layouts/docsheader.html +++ /dev/null @@ -1,9 +0,0 @@ -
      - {{ partial "layouts/breadcrumbs.html" . }} - {{ if and .IsPage (not (eq .Layout "list")) }} -

      - {{ .Title }} -

      - {{ end }} -
      diff --git a/docs/layouts/_partials/layouts/explorer.html b/docs/layouts/_partials/layouts/explorer.html deleted file mode 100644 index bb6f8e96a..000000000 --- a/docs/layouts/_partials/layouts/explorer.html +++ /dev/null @@ -1,47 +0,0 @@ -{{/* This is currently not in use, but kept in case I change my mind. */}} - - -{{ define "docs-explorer-section" }} - {{ $p := .p }} - {{ $level := .level }} - {{ $pleft := $level }} - {{ if gt $level 0 }} - {{ $pleft = add $level 1 }} - {{ end }} - {{ $pl := printf "pl-%d" $pleft }} - {{ $pages := $p.Sections }} - - {{ range $pages }} - {{ $hasChildren := gt (len .Pages) 0 }} - {{ $class := cond (eq $level 0) "text-primary hover:text-primary/70" "text-gray-900 dark:text-gray-400 hover:dark:text-gray-300" }} -
    • - - {{ .LinkTitle }} - - {{ if $hasChildren }} -
        - {{ template "docs-explorer-section" (dict "p" . "level" (add $level 1)) }} -
      - {{ end }} -
    • - {{ end }} - -{{ end }} diff --git a/docs/layouts/_partials/layouts/footer.html b/docs/layouts/_partials/layouts/footer.html deleted file mode 100644 index 1b17e44e4..000000000 --- a/docs/layouts/_partials/layouts/footer.html +++ /dev/null @@ -1,73 +0,0 @@ -
      -
      -
      - {{/* Column 1 */}} -
      -
      - By the - Hugo Authors
      -
      - - Hugo Logo - - -
      - - {{/* Sponsors */}} -
      - {{ partial "layouts/home/sponsors.html" (dict - "ctx" . - "gtag" "footer" - - ) - }} -
      -
      -
      -

      - The Hugo logos are copyright © Steve Francia 2013–{{ now.Year }}. The - Hugo Gopher is based on an original work by Renée French. -

      -
      -
      -
      diff --git a/docs/layouts/_partials/layouts/head/head-js.html b/docs/layouts/_partials/layouts/head/head-js.html deleted file mode 100644 index d83efcd0f..000000000 --- a/docs/layouts/_partials/layouts/head/head-js.html +++ /dev/null @@ -1,11 +0,0 @@ -{{ $githubInfo := partialCached "helpers/funcs/get-github-info.html" . "-" }} -{{ $opts := dict "minify" true }} -{{ with resources.Get "js/head-early.js" | js.Build $opts }} - {{ partial "helpers/linkjs.html" (dict "r" . "attributes" (dict "async" "")) }} -{{ end }} -{{ with resources.Get "js/main.js" | js.Build $opts }} - {{ partial "helpers/linkjs.html" (dict "r" . "attributes" (dict "defer" "")) }} -{{ end }} -{{ with resources.Get "js/turbo.js" | js.Build $opts }} - {{ partial "helpers/linkjs.html" (dict "r" . "attributes" (dict "defer" "")) }} -{{ end }} diff --git a/docs/layouts/_partials/layouts/head/head.html b/docs/layouts/_partials/layouts/head/head.html deleted file mode 100644 index bb27f6a24..000000000 --- a/docs/layouts/_partials/layouts/head/head.html +++ /dev/null @@ -1,49 +0,0 @@ - -{{ hugo.Generator }} - -{{ if hugo.IsProduction }} - -{{ else }} - -{{ end }} - - - {{ with .Title }}{{ . }} |{{ end }} - {{ .Site.Title }} - - - - - - - - - - - -{{ range .AlternativeOutputFormats -}} - -{{ end -}} - - - - - - -{{ partial "opengraph/opengraph.html" . }} -{{- template "_internal/schema.html" . -}} -{{- template "_internal/twitter_cards.html" . -}} - -{{ if hugo.IsProduction }} - {{ partial "helpers/gtag.html" . }} -{{ end }} diff --git a/docs/layouts/_partials/layouts/header/githubstars.html b/docs/layouts/_partials/layouts/header/githubstars.html deleted file mode 100644 index 75db5682a..000000000 --- a/docs/layouts/_partials/layouts/header/githubstars.html +++ /dev/null @@ -1,16 +0,0 @@ -{{ with partialCached "helpers/funcs/get-github-info.html" . "-" }} - - - - - - - - {{ printf "%0.1fk" (div .stargazers_count 1000) }} - - -{{ end }} diff --git a/docs/layouts/_partials/layouts/header/header.html b/docs/layouts/_partials/layouts/header/header.html deleted file mode 100644 index 0d2e720d7..000000000 --- a/docs/layouts/_partials/layouts/header/header.html +++ /dev/null @@ -1,44 +0,0 @@ -
      -
      - {{ with site.Home }} - HUGO - {{ end }} -
      -
      - {{ range .Site.Menus.global }} - {{ .Name }} - {{ end }} - -
      - -
      - {{/* Search. */}} - {{ partial "layouts/search/input.html" . }} -
      -
      - {{/* QR code. */}} - {{ partial "layouts/header/qr.html" . }} - {{/* Theme selector. */}} - {{ partial "layouts/header/theme.html" . }} - - {{/* Social. */}} - -
      -
      diff --git a/docs/layouts/_partials/layouts/header/qr.html b/docs/layouts/_partials/layouts/header/qr.html deleted file mode 100644 index fea64f625..000000000 --- a/docs/layouts/_partials/layouts/header/qr.html +++ /dev/null @@ -1,25 +0,0 @@ -{{ $t := debug.Timer "qr" }} -{{ $qr := partial "_inline/qr" (dict - "page" $ - "img_class" "w-10 bg-white view-transition-qr" ) -}} -{{ $qrBig := partial "_inline/qr" (dict "page" $ "img_class" "w-64 p-4") }} -{{ $t.Stop }} - - -{{ define "_partials/_inline/qr" }} - {{ $img_class := .img_class | default "w-10" }} - {{ with images.QR $.page.Permalink (dict "targetDir" "images/qr") }} - - QR code linking to {{ $.page.Permalink }} - {{ end }} -{{ end }} diff --git a/docs/layouts/_partials/layouts/header/theme.html b/docs/layouts/_partials/layouts/header/theme.html deleted file mode 100644 index e0b356d1d..000000000 --- a/docs/layouts/_partials/layouts/header/theme.html +++ /dev/null @@ -1,35 +0,0 @@ -
      - -
      diff --git a/docs/layouts/_partials/layouts/home/features.html b/docs/layouts/_partials/layouts/home/features.html deleted file mode 100644 index 527c98cb1..000000000 --- a/docs/layouts/_partials/layouts/home/features.html +++ /dev/null @@ -1,56 +0,0 @@ -{{/* icons source: https://heroicons.com/ */}} -{{ $dataTOML := ` - [[features]] - heading = "Optimized for speed" - copy = "Written in Go, optimized for speed and designed for flexibility. With its advanced templating system and fast asset pipelines, Hugo renders a large site in seconds, often less." - icon = """ - - - """ - [[features]] - heading = "Flexible framework" - copy = "With its multilingual support, and powerful taxonomy system, Hugo is widely used to create documentation sites, landing pages, corporate, government, nonprofit, education, news, event, and project sites." - icon = """ - - - """ - [[features]] - heading = "Fast assets pipeline" - copy = "Image processing (convert, resize, crop, rotate, adjust colors, apply filters, overlay text and images, and extract EXIF data), JavaScript bundling (tree shake, code splitting), Sass processing, great TailwindCSS support." - icon = """ - - - """ - [[features]] - heading = "Embedded web server" - copy = "Use Hugo's embedded web server during development to instantly see changes to content, structure, behavior, and presentation. " - icon = """ - - - """ - ` -}} -{{ $data := $dataTOML | transform.Unmarshal }} -
      -
      -
      - {{ range $data.features }} -
      -
      -
      - {{ .icon | safeHTML }} -
      - {{ .heading }} -
      -
      - {{ .copy }} -
      -
      - {{ end }} - -
      -
      -
      diff --git a/docs/layouts/_partials/layouts/home/opensource.html b/docs/layouts/_partials/layouts/home/opensource.html deleted file mode 100644 index 153c0f4ff..000000000 --- a/docs/layouts/_partials/layouts/home/opensource.html +++ /dev/null @@ -1,111 +0,0 @@ -{{ $githubInfo := partialCached "helpers/funcs/get-github-info.html" . "-" }} -
      -
      -
      -
      -

      - Open source -

      -

      - Hugo is open source and free to use. It is distributed under the - Apache 2.0 License. -

      -
      -
      -
      - - - - Popular. -
      -
      - Hugo has - {{ $githubInfo.stargazers_count | lang.FormatNumber 0 }} - stars on GitHub as of {{ now.Format "January 2, 2006" }}. - Join the crowd and hit the - Star button. -
      -
      -
      -
      - - - - Active. -
      -
      - Hugo has a large and active community. If you have questions or - need help, you can ask in the - Hugo forums. -
      -
      -
      -
      - - - - Frequent releases. -
      -
      - Hugo has a fast - release - cycle. The project is actively maintained and new features are - added regularly. -
      -
      -
      -
      -
      - {{ partial "helpers/picture.html" (dict - "image" (resources.Get "images/hugo-github-screenshot.png") - "alt" "Hugo GitHub Repository" - "width" 640 - "class" "w-full max-w-[38rem] ring-1 shadow-xl dark:shadow-gray-500 ring-gray-400/10") - }} -
      -
      diff --git a/docs/layouts/_partials/layouts/home/sponsors.html b/docs/layouts/_partials/layouts/home/sponsors.html deleted file mode 100644 index 1f391e1ec..000000000 --- a/docs/layouts/_partials/layouts/home/sponsors.html +++ /dev/null @@ -1,44 +0,0 @@ -{{ $gtag := .gtag | default "unknown" }} -{{ $gtag := .gtag | default "unknown" }} -{{ $isFooter := (eq $gtag "footer") }} -{{ $utmSource := cond $isFooter "hugofooter" "hugohome" }} -{{ $containerClass := .containerClass | default "mx-auto max-w-7xl px-6 lg:px-8" }} -{{ with .ctx.Site.Data.sponsors }} -
      -

      Hugo Sponsors

      -
      - {{ range .banners }} -
      - {{ $query_params := .query_params | default "" }} - {{ $url := .link }} - {{ if not .no_query_params }} - {{ $url = printf "%s?%s%s" .link $query_params (querify "utm_source" (.utm_source | default $utmSource ) "utm_medium" (.utm_medium | default "banner") "utm_campaign" (.utm_campaign | default "hugosponsor") "utm_content" (.utm_content | default "gohugoio")) | safeURL }} - {{ end }} - {{ $logo := resources.Get .logo }} - {{ $gtagID := printf "Sponsor %s %s" .name $gtag | title }} - - - -
      - {{ end }} -
      -
      -{{ end }} diff --git a/docs/layouts/_partials/layouts/hooks/body-end.html b/docs/layouts/_partials/layouts/hooks/body-end.html deleted file mode 100644 index 9ffd93ba8..000000000 --- a/docs/layouts/_partials/layouts/hooks/body-end.html +++ /dev/null @@ -1,3 +0,0 @@ -{{- if .IsHome }} - {{- partial "helpers/validation/validate-keywords.html" }} -{{- end }} diff --git a/docs/layouts/_partials/layouts/hooks/body-main-start.html b/docs/layouts/_partials/layouts/hooks/body-main-start.html deleted file mode 100644 index b28dd21c8..000000000 --- a/docs/layouts/_partials/layouts/hooks/body-main-start.html +++ /dev/null @@ -1,8 +0,0 @@ -{{ if or .IsSection .IsPage }} - -{{end }} diff --git a/docs/layouts/_partials/layouts/hooks/body-start.html b/docs/layouts/_partials/layouts/hooks/body-start.html deleted file mode 100644 index 3430bd846..000000000 --- a/docs/layouts/_partials/layouts/hooks/body-start.html +++ /dev/null @@ -1,3 +0,0 @@ -{{ with resources.Get "js/body-start.js" | js.Build (dict "minify" true) }} - {{ partial "helpers/linkjs.html" (dict "r" . "attributes" (dict "" "")) }} -{{ end }} diff --git a/docs/layouts/_partials/layouts/icons.html b/docs/layouts/_partials/layouts/icons.html deleted file mode 100644 index b05adef00..000000000 --- a/docs/layouts/_partials/layouts/icons.html +++ /dev/null @@ -1,70 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -{{/* https://heroicons.com/mini exclamation-circle */}} - - - - -{{/* https://heroicons.com/mini exclamation-triangle */}} - - - - -{{/* https://heroicons.com/mini information-circle */}} - - - - -{{/* https://heroicons.com/mini light-bulb */}} - - - diff --git a/docs/layouts/_partials/layouts/in-this-section.html b/docs/layouts/_partials/layouts/in-this-section.html deleted file mode 100644 index 28e5ed7eb..000000000 --- a/docs/layouts/_partials/layouts/in-this-section.html +++ /dev/null @@ -1,34 +0,0 @@ -{{- with .CurrentSection.RegularPages }} - {{ $hasTocOrRelated := or ($.Store.Get "hasToc") ($.Store.Get "hasRelated") }} -
      -

      - In this section -

      - - -
      -{{- end }} diff --git a/docs/layouts/_partials/layouts/page-edit.html b/docs/layouts/_partials/layouts/page-edit.html deleted file mode 100644 index 6a976d11a..000000000 --- a/docs/layouts/_partials/layouts/page-edit.html +++ /dev/null @@ -1,26 +0,0 @@ -
      -
      - -
      - Last updated: - {{ .Lastmod.Format "January 2, 2006" }}{{ with .GitInfo }} - : - {{ .Subject }} ({{ .AbbreviatedHash }}) - {{ end }} -
      - - {{ with .File }} - {{ if not .IsContentAdapter }} - {{ $href := printf "%sedit/master/content/%s/%s" site.Params.ghrepo $.Lang .Path }} - - Improve this page - - {{ end }} - {{ end }} -
      diff --git a/docs/layouts/_partials/layouts/related.html b/docs/layouts/_partials/layouts/related.html deleted file mode 100644 index 0245ba771..000000000 --- a/docs/layouts/_partials/layouts/related.html +++ /dev/null @@ -1,22 +0,0 @@ -{{- $heading := "See also" }} -{{- $related := site.Pages.Related . }} -{{- $related = $related | complement .CurrentSection.Pages | first 7 }} - -{{- with $related }} - {{ $.Store.Set "hasRelated" true }} -

      - {{ $heading }} -

      - -{{- end }} diff --git a/docs/layouts/_partials/layouts/search/algolialogo.html b/docs/layouts/_partials/layouts/search/algolialogo.html deleted file mode 100644 index a7fc6c0ae..000000000 --- a/docs/layouts/_partials/layouts/search/algolialogo.html +++ /dev/null @@ -1,45 +0,0 @@ -
      - Search by - - - - - - - - - - -
      diff --git a/docs/layouts/_partials/layouts/search/button.html b/docs/layouts/_partials/layouts/search/button.html deleted file mode 100644 index 07c1f7335..000000000 --- a/docs/layouts/_partials/layouts/search/button.html +++ /dev/null @@ -1,22 +0,0 @@ -{{ $textColor := "text-gray-300" }} -{{ $fillColor := "fill-slate-400 dark:fill-slate-500" }} -{{ if .standalone }} - {{ $textColor = "text-gray-800 dark:text-gray-300 " }} - {{ $fillColor = "fill-slate-500 dark:fill-slate-400" }} -{{ end }} - - - diff --git a/docs/layouts/_partials/layouts/search/input.html b/docs/layouts/_partials/layouts/search/input.html deleted file mode 100644 index 5f5ff07b9..000000000 --- a/docs/layouts/_partials/layouts/search/input.html +++ /dev/null @@ -1,4 +0,0 @@ -
      - {{ partial "layouts/search/button.html" (dict "page" . "standalone" false) }} - {{ partial "layouts/search/results.html" . }} -
      diff --git a/docs/layouts/_partials/layouts/search/results.html b/docs/layouts/_partials/layouts/search/results.html deleted file mode 100644 index cd9b88dc0..000000000 --- a/docs/layouts/_partials/layouts/search/results.html +++ /dev/null @@ -1,90 +0,0 @@ - diff --git a/docs/layouts/_partials/layouts/templates.html b/docs/layouts/_partials/layouts/templates.html deleted file mode 100644 index 72b71a3d9..000000000 --- a/docs/layouts/_partials/layouts/templates.html +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/docs/layouts/_partials/layouts/toc.html b/docs/layouts/_partials/layouts/toc.html deleted file mode 100644 index 774bc15c7..000000000 --- a/docs/layouts/_partials/layouts/toc.html +++ /dev/null @@ -1,46 +0,0 @@ -{{ with .Fragments }} - {{ with .Headings }} -
      -

      - On this page -

      - -
      - {{ end }} -{{ end }} - -{{ define "render-toc-level" }} - {{ range .h }} - {{ if and .ID (and (ge .Level 2) (le .Level 4)) }} - {{ $indentation := "ml-0" }} - {{ if eq .Level 3 }} - {{ $indentation = "ml-2 lg:ml-3" }} - {{ else if eq .Level 4 }} - {{ $indentation = "ml-4 lg:ml-6" }} - {{ end }} - {{ $.p.Store.Set "hasToc" true }} -
    • - - {{ .Title | safeHTML }} - -
    • - {{ end }} - {{ with .Headings }} -
        - {{ template "render-toc-level" (dict "h" . "p" $.p) }} -
      - {{ end }} - {{ end }} -{{ end }} diff --git a/docs/layouts/_partials/opengraph/get-featured-image.html b/docs/layouts/_partials/opengraph/get-featured-image.html deleted file mode 100644 index 50ee2a44d..000000000 --- a/docs/layouts/_partials/opengraph/get-featured-image.html +++ /dev/null @@ -1,26 +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 }} diff --git a/docs/layouts/_partials/opengraph/opengraph.html b/docs/layouts/_partials/opengraph/opengraph.html deleted file mode 100644 index e32e07298..000000000 --- a/docs/layouts/_partials/opengraph/opengraph.html +++ /dev/null @@ -1,84 +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.Params.social.facebook_admin }} - -{{ end }} diff --git a/docs/layouts/_shortcodes/chroma-lexers.html b/docs/layouts/_shortcodes/chroma-lexers.html deleted file mode 100644 index fd7130501..000000000 --- a/docs/layouts/_shortcodes/chroma-lexers.html +++ /dev/null @@ -1,28 +0,0 @@ -{{/* prettier-ignore-start */ -}} -{{- /* -Renders an HTML template of Chroma lexers and their aliases. - -@example {{< chroma-lexers >}} -*/ -}} -{{/* prettier-ignore-end */ -}} -
      - - - - - - - {{- range site.Data.docs.chroma.lexers }} - - - - - {{- end }} - -
      LanguageIdentifiers
      {{ .Name }} - {{- range $k, $_ := .Aliases }} - {{- if $k }},{{ end }} - {{ . }} - {{- end }} -
      -
      diff --git a/docs/layouts/_shortcodes/code-toggle.html b/docs/layouts/_shortcodes/code-toggle.html deleted file mode 100644 index a22c378be..000000000 --- a/docs/layouts/_shortcodes/code-toggle.html +++ /dev/null @@ -1,120 +0,0 @@ -{{/* prettier-ignore-start */ -}} -{{- /* -Renders syntax-highlighted configuration data in JSON, TOML, and YAML formats. - -@param {string} [config] The section of site.Data.docs.config to render. -@param {bool} [copy=false] Whether to display a copy-to-clipboard button. -@param {string} [dataKey] The section of site.Data.docs to render. -@param {string} [file] The file name to display above the rendered code. -@param {bool} [fm=false] Whether to render the code as front matter. -@param {bool} [skipHeader=false] Whether to omit top level key(s) when rendering a section of site.Data.docs.config. - -@example {{< code-toggle file=hugo config=build />}} - -@example {{< code-toggle file=content/example.md fm="true" }} - title='Example' - draft='false - {{< /code-toggle }} -*/ -}} -{{/* prettier-ignore-end */ -}} -{{- /* Initialize. */}} -{{- $config := "" }} -{{- $copy := false }} -{{- $dataKey := "" }} -{{- $file := "" }} -{{- $fm := false }} -{{- $skipHeader := false }} - -{{- /* Get parameters. */}} -{{- $config = .Get "config" }} -{{- $dataKey = .Get "dataKey" }} -{{- $file = .Get "file" }} -{{- if in (slice "false" false 0) (.Get "copy") }} - {{- $copy = false }} -{{- else if in (slice "true" true 1) (.Get "copy") }} - {{- $copy = true }} -{{- end }} -{{- if in (slice "false" false 0) (.Get "fm") }} - {{- $fm = false }} -{{- else if in (slice "true" true 1) (.Get "fm") }} - {{- $fm = true }} -{{- end }} -{{- if in (slice "false" false 0) (.Get "skipHeader") }} - {{- $skipHeader = false }} -{{- else if in (slice "true" true 1) (.Get "skipHeader") }} - {{- $skipHeader = true }} -{{- end }} - -{{- /* Define constants. */}} -{{- $delimiters := dict "toml" "+++" "yaml" "---" }} -{{- $langs := slice "yaml" "toml" "json" }} -{{- $placeHolder := "#-hugo-placeholder-#" }} - -{{- /* Render. */}} -{{- $code := "" }} -{{- if $config }} - {{- $file = $file | default "hugo" }} - {{- $sections := (split $config ".") }} - {{- $configSection := index $.Site.Data.docs.config $sections }} - {{- $code = dict $sections $configSection }} - {{- if $skipHeader }} - {{- $code = $configSection }} - {{- end }} -{{- else if $dataKey }} - {{- $file = $file | default $dataKey }} - {{- $sections := (split $dataKey ".") }} - {{- $code = index $.Site.Data.docs $sections }} -{{- else }} - {{- $code = $.Inner }} -{{- end }} - - -
      - {{- if $copy }} - - - - {{- end }} - - {{- if $code }} - {{- range $i, $lang := $langs }} -
      - {{- $hCode := $code | transform.Remarshal . }} - {{- if and $fm (in (slice "toml" "yaml") .) }} - {{- $hCode = printf "%s\n%s\n%s" $placeHolder $hCode $placeHolder }} - {{- end }} - {{- $hCode = $hCode | replaceRE `\n+` "\n" }} - {{- highlight $hCode . "" | replaceRE $placeHolder (index $delimiters .) | safeHTML }} -
      - {{- end }} - {{- end }} -
      diff --git a/docs/layouts/_shortcodes/datatable-filtered.html b/docs/layouts/_shortcodes/datatable-filtered.html deleted file mode 100644 index de8b0cf55..000000000 --- a/docs/layouts/_shortcodes/datatable-filtered.html +++ /dev/null @@ -1,47 +0,0 @@ -{{ $package := (index .Params 0) }} -{{ $listname := (index .Params 1) }} -{{ $filter := split (index .Params 2) " " }} -{{ $filter1 := index $filter 0 }} -{{ $filter2 := index $filter 1 }} -{{ $filter3 := index $filter 2 }} - -{{ $list := (index (index .Site.Data.docs $package) $listname) }} -{{ $fields := after 3 .Params }} -{{ $list := where $list $filter1 $filter2 $filter3 }} - - -
      - - - - {{ range $fields }} - - {{ end }} - - - - {{ range $list }} - - {{ range $k, $v := . }} - {{ $.Scratch.Set $k $v }} - {{ end }} - {{ range $k, $v := $fields }} - - {{ end }} - - {{ end }} - -
      {{ . }}
      - {{ $tdContent := $.Scratch.Get . }} - {{ if eq $k 3 }} - {{ printf "%v" $tdContent | - strings.ReplaceRE `\[` "
      1. " | - strings.ReplaceRE `\s` "
      2. " | - strings.ReplaceRE `\]` "
      " | - safeHTML - }} - {{ else }} - {{ $tdContent }} - {{ end }} -
      -
      diff --git a/docs/layouts/_shortcodes/datatable.html b/docs/layouts/_shortcodes/datatable.html deleted file mode 100644 index f135d841c..000000000 --- a/docs/layouts/_shortcodes/datatable.html +++ /dev/null @@ -1,39 +0,0 @@ -{{ $package := (index .Params 0) }} -{{ $listname := (index .Params 1) }} -{{ $list := (index (index .Site.Data.docs $package) $listname) }} -{{ $fields := after 2 .Params }} - - -
      - - - - {{ range $fields }} - {{ $s := . }} - {{ if eq $s "_key" }} - {{ $s = "type" }} - {{ end }} - - {{ end }} - - - - {{ range $k1, $v1 := $list }} - - {{ range $k2, $v2 := . }} - {{ $.Scratch.Set $k2 $v2 }} - {{ end }} - {{ range $fields }} - {{ $s := "" }} - {{ if eq . "_key" }} - {{ $s = $k1 }} - {{ else }} - {{ $s = $.Scratch.Get . }} - {{ end }} - - {{ end }} - - {{ end }} - -
      {{ $s }}
      {{ $s }}
      -
      diff --git a/docs/layouts/_shortcodes/deprecated-in.html b/docs/layouts/_shortcodes/deprecated-in.html deleted file mode 100644 index ce2ba389e..000000000 --- a/docs/layouts/_shortcodes/deprecated-in.html +++ /dev/null @@ -1,29 +0,0 @@ -{{/* prettier-ignore-start */ -}} -{{- /* -Renders a callout indicating the version in which a feature was deprecated. - -Include descriptive text between the opening and closing tags, or omit the -descriptive text and call the shortcode with a self-closing tag. - -@param {string} 0 The semantic version string, with or without a leading v. - -@example {{< deprecated-in 0.144.0 />}} - -@example {{< deprecated-in 0.144.0 >}} - Some descriptive text here. - {{< /deprecated-in >}} -*/ -}} -{{/* prettier-ignore-end */ -}} -{{- with $version := .Get 0 | strings.TrimLeft "vV" }} - {{- $href := printf "https://github.com/gohugoio/hugo/releases/tag/v%s" $version }} - {{- $inner := strings.TrimSpace $.Inner }} - {{- $text := printf "Deprecated in [v%s](%s)\n\n%s" $version $href $inner | $.Page.RenderString (dict "display" "block") }} - {{- partial "layouts/blocks/alert.html" (dict - "color" "orange" - "icon" "exclamation" - "text" $text - ) - }} -{{- else }} - {{- errorf "The %q shortcode requires a single positional parameter indicating version. See %s" .Name .Position }} -{{- end }} diff --git a/docs/layouts/_shortcodes/eturl.html b/docs/layouts/_shortcodes/eturl.html deleted file mode 100644 index a0237dbe0..000000000 --- a/docs/layouts/_shortcodes/eturl.html +++ /dev/null @@ -1,26 +0,0 @@ -{{/* prettier-ignore-start */ -}} -{{- /* -Renders an absolute URL to the source code for an embedded template. - -Accepts either positional or named parameters, and depends on the -embedded_templates.toml file in the data directory. - -@param {string} filename The embedded template's file name, excluding extension. - -@example {{% et robots.txt %}} -@example {{% et filename=robots.txt %}} -*/ -}} -{{/* prettier-ignore-end */ -}} -{{- with $filename := or (.Get "filename") (.Get 0) }} - {{- with site.Data.embedded_template_urls }} - {{- with index . $filename }} - {{- urls.JoinPath site.Data.embedded_template_urls.base_url . }} - {{- else }} - {{- errorf "The %q shortcode was unable to find a URL for the embedded template named %q. Check the name. See %s" $.Name $filename $.Position }} - {{- end }} - {{- else }} - {{- errorf "The %q shortcode was unable to find the embedded_template_urls data file in the site's data directory. See %s" $.Name $.Position }} - {{- end }} -{{- else }} - {{- errorf "The %q shortcodes requires a named or positional parameter, the file name of the embedded template, excluding its extension. See %s" .Name .Position }} -{{- end -}} diff --git a/docs/layouts/_shortcodes/glossary-term.html b/docs/layouts/_shortcodes/glossary-term.html deleted file mode 100644 index 2a45dc8cb..000000000 --- a/docs/layouts/_shortcodes/glossary-term.html +++ /dev/null @@ -1,18 +0,0 @@ -{{- /* -Renders the definition of the given glossary term. - -@param {string} (.Get 0) The glossary term. - -@example {{% glossary-term float %}} -@example {{% glossary-term "floating point" %}} -*/ -}} -{{- with .Get 0 }} - {{- $path := printf "/quick-reference/glossary/%s" (urlize .) }} - {{- with site.GetPage $path }} -{{ .RenderShortcodes }}{{/* Do not indent. */}} - {{- else }} - {{- errorf "The glossary term (%s) shortcode was unable to find %s: see %s" $.Name $path $.Position }} - {{- end }} -{{- else }} - {{- errorf "The glossary term (%s) shortcode requires one positional parameter: see %s" $.Name $.Position }} -{{- end -}} diff --git a/docs/layouts/_shortcodes/glossary.html b/docs/layouts/_shortcodes/glossary.html deleted file mode 100644 index 7331d5c9f..000000000 --- a/docs/layouts/_shortcodes/glossary.html +++ /dev/null @@ -1,54 +0,0 @@ -{{- /* -Renders the glossary of terms. - -When you call this shortcode using the {{% %}} notation, the glossary terms are -Markdown headings (level 6) which means they are members of .Fragments. This -allows the link render hook to verify links to glossary terms. - -Yes, the terms themselves are pages, but we don't want to link to the pages, at -least not right now. Instead, we want to link to the ids rendered by this -shortcode. - -@example {{% glossary %}} -*/ -}} -{{- $path := "/quick-reference/glossary" }} -{{- with site.GetPage $path }} - - {{- /* Build and render alphabetical index. */}} - {{- $m := dict }} - {{- range $p := .Pages.ByTitle }} - {{- $k := substr .Title 0 1 | strings.ToUpper }} - {{- if index $m $k }} - {{- continue }} - {{- end }} - {{- $anchor := path.BaseName .Path | anchorize }} - {{- $m = merge $m (dict $k $anchor) }} - {{- end }} - {{- range $k, $v := $m }} -[{{ $k }}](#{{ $v }}) {{/* Do not indent. */}} - {{- end }} - - {{/* Render glossary terms. */}} - {{- range $p := .Pages.ByTitle }} -{{ .Title }}{{/* Do not indent. */}} -: {{ .RawContent | strings.TrimSpace | safeHTML }}{{/* Do not indent. */}} - {{ with .Params.reference }} - {{- $destination := "" }} - {{- with $u := urls.Parse . }} - {{- if $u.IsAbs }} - {{- $destination = $u.String }} - {{- else }} - {{- with site.GetPage $u.Path }} - {{- $destination = .RelPermalink }} - {{- else }} - {{- errorf "The %q shortcode was unable to find the reference link %s: see %s" $.Name . $p.String }} - {{- end }} - {{- end }} - {{- end }} -: See [details]({{ $destination }}).{{/* Do not indent. */}} - {{- end }} - {{ end }} - -{{- else }} - {{- errorf "The %q shortcode was unable to get %s: see %s" .Name $path .Position}} -{{- end }} diff --git a/docs/layouts/_shortcodes/hl.html b/docs/layouts/_shortcodes/hl.html deleted file mode 100644 index 3fafcb5e8..000000000 --- a/docs/layouts/_shortcodes/hl.html +++ /dev/null @@ -1,14 +0,0 @@ -{{/* prettier-ignore-start */ -}} -{{- /* -Returns syntax-highlighted code from the given text. - -This is useful as a terse way to highlight inline code snippets. Calling the -highlight shortcode for inline snippets is verbose. - -@example This is {{< hl python >}}inline{{< /hl >}} code. -*/ -}} -{{/* prettier-ignore-end */ -}} -{{- $code := .Inner | strings.TrimSpace }} -{{- $lang := or (.Get 0) "go" }} -{{- $opts := dict "hl_inline" true "noClasses" true }} -{{- transform.Highlight $code $lang $opts }} diff --git a/docs/layouts/_shortcodes/img.html b/docs/layouts/_shortcodes/img.html deleted file mode 100644 index e49afc57f..000000000 --- a/docs/layouts/_shortcodes/img.html +++ /dev/null @@ -1,391 +0,0 @@ -{{/* prettier-ignore-start */ -}} -{{- /* -Renders the given image using the given filter, if any. - -When using the text filter, provide the arguments in this order: - -0. The text -1. The horizontal offset, in pixels, relative to the left of the image (default 20) -2. The vertical offset, in pixels, relative to the top of the image (default 20) -3. The font size in pixels (default 64) -4. The line height (default 1.2) -5. The font color (default #ffffff) - -When using the padding filter, provide all arguments in this order: - -0. Padding top -1. Padding right -2. Padding bottom -3. Padding right -4. Canvas color - -@param {string} src The path to the image which must be a remote, page, or global resource. -@param {string} [filter] The filter to apply to the image (case-insensitive). -@param {string} [filterArgs] A comma-delimited list of arguments to pass to the filter. -@param {bool} [example=false] If true, renders a before/after example. -@param {int} [exampleWidth=384] Image width, in pixels, when rendering a before/after example. - -@example {{< img src="zion-national-park.jpg" >}} -@example {{< img src="zion-national-park.jpg" alt="Zion National Park" >}} - -@example {{< img - src="zion-national-park.jpg" - alt="Zion National Park" - filter="grayscale" - >}} - -@example {{< img - src="zion-national-park.jpg" - alt="Zion National Park" - filter="process" - filterArgs="resize 400x webp" - >}} - -@example {{< img - src="zion-national-park.jpg" - alt="Zion National Park" - filter="colorize" - filterArgs="180,50,20" - >}} - -@example {{< img - src="zion-national-park.jpg" - alt="Zion National Park" - filter="grayscale" - example=true - >}} - -@example {{< img - src="zion-national-park.jpg" - alt="Zion National Park" - filter="grayscale" - example=true - exampleWidth=400 - >}} - -@example {{< img - src="images/examples/zion-national-park.jpg" - alt="Zion National Park" - filter="Text" - filterArgs="Zion National Park,25,250,56" - example=true - >}} - -@example {{< img - src="images/examples/zion-national-park.jpg" - alt="Zion National Park" - filter="Padding" - filterArgs="20,50,20,50,#0705" - example=true - >}} -*/ -}} -{{/* prettier-ignore-end */ -}} -{{- /* Initialize. */}} -{{- $alt := "" }} -{{- $src := "" }} -{{- $filter := "" }} -{{- $filterArgs := slice }} -{{- $example := false }} -{{- $exampleWidth := 384 }} - -{{- /* Default values to use with the text filter. */}} -{{ $textFilterOpts := dict - "xOffset" 20 - "yOffset" 20 - "fontSize" 64 - "lineHeight" 1.2 - "fontColor" "#ffffff" - "fontPath" "https://github.com/google/fonts/raw/refs/heads/main/ofl/lato/Lato-Regular.ttf" -}} - -{{- /* Get and validate parameters. */}} -{{- with .Get "alt" }} - {{- $alt = . }} -{{- end }} - -{{- with .Get "src" }} - {{- $src = . }} -{{- else }} - {{- errorf "The %q shortcode requires a file parameter. See %s" .Name .Position }} -{{- end }} - -{{- with .Get "filter" }} - {{- $filter = . | lower }} -{{- end }} - -{{- $validFilters := slice - "autoorient" "brightness" "colorbalance" "colorize" "contrast" "dither" - "gamma" "gaussianblur" "grayscale" "hue" "invert" "mask" "none" "opacity" - "overlay" "padding" "pixelate" "process" "saturation" "sepia" "sigmoid" "text" - "unsharpmask" -}} - -{{- with $filter }} - {{- if not (in $validFilters .) }} - {{- errorf "The filter passed to the %q shortcode is invalid. The filter must be one of %s. See %s" $.Name (delimit $validFilters ", " ", or ") $.Position }} - {{- end }} -{{- end }} - -{{- with .Get "filterArgs" }} - {{- $filterArgs = split . "," }} - {{- $filterArgs = apply $filterArgs "trim" "." " " }} -{{- end }} - -{{- if in (slice "false" false 0) (.Get "example") }} - {{- $example = false }} -{{- else if in (slice "true" true 1) (.Get "example") }} - {{- $example = true }} -{{- end }} - -{{- with .Get "exampleWidth" }} - {{- $exampleWidth = . | int }} -{{- end }} - -{{- /* Get image. */}} -{{- $ctx := dict "page" .Page "src" $src "name" .Name "position" .Position }} -{{- $i := partial "inline/get-resource.html" $ctx }} - -{{- /* Resize if rendering before/after examples. */}} -{{- if $example }} - {{- $i = $i.Resize (printf "%dx" $exampleWidth) }} -{{- end }} - -{{- /* Create filter. */}} -{{- $f := "" }} -{{- $ctx := dict "filter" $filter "args" $filterArgs "name" .Name "position" .Position }} -{{- if eq $filter "autoorient" }} - {{- $ctx = merge $ctx (dict "argsRequired" 0) }} - {{- template "validate-arg-count" $ctx }} - {{- $f = images.AutoOrient }} -{{- else if eq $filter "brightness" }} - {{- $ctx = merge $ctx (dict "argsRequired" 1) }} - {{- template "validate-arg-count" $ctx }} - {{- $filterArgs = apply $filterArgs "float" "." }} - {{- $ctx = merge $ctx (dict "argName" "percentage" "argValue" (index $filterArgs 0) "min" -100 "max" 100) }} - {{- template "validate-arg-value" $ctx }} - {{- $f = images.Brightness (index $filterArgs 0) }} -{{- else if eq $filter "colorbalance" }} - {{- $ctx = merge $ctx (dict "argsRequired" 3) }} - {{- template "validate-arg-count" $ctx }} - {{- $filterArgs = apply $filterArgs "float" "." }} - {{- $ctx = merge $ctx (dict "argName" "percentage red" "argValue" (index $filterArgs 0) "min" -100 "max" 500) }} - {{- template "validate-arg-value" $ctx }} - {{- $ctx = merge $ctx (dict "argName" "percentage green" "argValue" (index $filterArgs 1) "min" -100 "max" 500) }} - {{- template "validate-arg-value" $ctx }} - {{- $ctx = merge $ctx (dict "argName" "percentage blue" "argValue" (index $filterArgs 2) "min" -100 "max" 500) }} - {{- template "validate-arg-value" $ctx }} - {{- $f = images.ColorBalance (index $filterArgs 0) (index $filterArgs 1) (index $filterArgs 2) }} -{{- else if eq $filter "colorize" }} - {{- $ctx = merge $ctx (dict "argsRequired" 3) }} - {{- template "validate-arg-count" $ctx }} - {{- $filterArgs = apply $filterArgs "float" "." }} - {{- $ctx = merge $ctx (dict "argName" "hue" "argValue" (index $filterArgs 0) "min" 0 "max" 360) }} - {{- template "validate-arg-value" $ctx }} - {{- $ctx = merge $ctx (dict "argName" "saturation" "argValue" (index $filterArgs 1) "min" 0 "max" 100) }} - {{- template "validate-arg-value" $ctx }} - {{- $ctx = merge $ctx (dict "argName" "percentage" "argValue" (index $filterArgs 2) "min" 0 "max" 100) }} - {{- template "validate-arg-value" $ctx }} - {{- $f = images.Colorize (index $filterArgs 0) (index $filterArgs 1) (index $filterArgs 2) }} -{{- else if eq $filter "contrast" }} - {{- $ctx = merge $ctx (dict "argsRequired" 1) }} - {{- template "validate-arg-count" $ctx }} - {{- $filterArgs = apply $filterArgs "float" "." }} - {{- $ctx = merge $ctx (dict "argName" "percentage" "argValue" (index $filterArgs 0) "min" -100 "max" 100) }} - {{- template "validate-arg-value" $ctx }} - {{- $f = images.Contrast (index $filterArgs 0) }} -{{- else if eq $filter "dither" }} - {{- $f = images.Dither }} -{{- else if eq $filter "gamma" }} - {{- $ctx = merge $ctx (dict "argsRequired" 1) }} - {{- template "validate-arg-count" $ctx }} - {{- $filterArgs = apply $filterArgs "float" "." }} - {{- $ctx = merge $ctx (dict "argName" "gamma" "argValue" (index $filterArgs 0) "min" 0 "max" 100) }} - {{- template "validate-arg-value" $ctx }} - {{- $f = images.Gamma (index $filterArgs 0) }} -{{- else if eq $filter "gaussianblur" }} - {{- $ctx = merge $ctx (dict "argsRequired" 1) }} - {{- template "validate-arg-count" $ctx }} - {{- $filterArgs = apply $filterArgs "float" "." }} - {{- $ctx = merge $ctx (dict "argName" "sigma" "argValue" (index $filterArgs 0) "min" 0 "max" 1000) }} - {{- template "validate-arg-value" $ctx }} - {{- $f = images.GaussianBlur (index $filterArgs 0) }} -{{- else if eq $filter "grayscale" }} - {{- $ctx = merge $ctx (dict "argsRequired" 0) }} - {{- template "validate-arg-count" $ctx }} - {{- $f = images.Grayscale }} -{{- else if eq $filter "hue" }} - {{- $ctx = merge $ctx (dict "argsRequired" 1) }} - {{- template "validate-arg-count" $ctx }} - {{- $filterArgs = apply $filterArgs "float" "." }} - {{- $ctx = merge $ctx (dict "argName" "shift" "argValue" (index $filterArgs 0) "min" -180 "max" 180) }} - {{- template "validate-arg-value" $ctx }} - {{- $f = images.Hue (index $filterArgs 0) }} -{{- else if eq $filter "invert" }} - {{- $ctx = merge $ctx (dict "argsRequired" 0) }} - {{- template "validate-arg-count" $ctx }} - {{- $f = images.Invert }} -{{- else if eq $filter "mask" }} - {{- $ctx = merge $ctx (dict "argsRequired" 1) }} - {{- template "validate-arg-count" $ctx }} - {{- $ctx := dict "src" (index $filterArgs 0) "name" .Name "position" .Position }} - {{- $maskImage := partial "inline/get-resource.html" $ctx }} - {{- $f = images.Mask $maskImage }} -{{- else if eq $filter "opacity" }} - {{- $ctx = merge $ctx (dict "argsRequired" 1) }} - {{- template "validate-arg-count" $ctx }} - {{- $filterArgs = apply $filterArgs "float" "." }} - {{- $ctx = merge $ctx (dict "argName" "opacity" "argValue" (index $filterArgs 0) "min" 0 "max" 1) }} - {{- template "validate-arg-value" $ctx }} - {{- $f = images.Opacity (index $filterArgs 0) }} -{{- else if eq $filter "overlay" }} - {{- $ctx = merge $ctx (dict "argsRequired" 3) }} - {{- template "validate-arg-count" $ctx }} - {{- $ctx := dict "src" (index $filterArgs 0) "name" .Name "position" .Position }} - {{- $overlayImg := partial "inline/get-resource.html" $ctx }} - {{- $f = images.Overlay $overlayImg (index $filterArgs 1 | float ) (index $filterArgs 2 | float) }} -{{- else if eq $filter "padding" }} - {{- $ctx = merge $ctx (dict "argsRequired" 5) }} - {{- template "validate-arg-count" $ctx }} - {{- $f = images.Padding - (index $filterArgs 0 | int) - (index $filterArgs 1 | int) - (index $filterArgs 2 | int) - (index $filterArgs 3 | int) - (index $filterArgs 4) - }} -{{- else if eq $filter "pixelate" }} - {{- $ctx = merge $ctx (dict "argsRequired" 1) }} - {{- template "validate-arg-count" $ctx }} - {{- $filterArgs = apply $filterArgs "float" "." }} - {{- $ctx = merge $ctx (dict "argName" "size" "argValue" (index $filterArgs 0) "min" 0 "max" 1000) }} - {{- template "validate-arg-value" $ctx }} - {{- $f = images.Pixelate (index $filterArgs 0) }} -{{- else if eq $filter "process" }} - {{- $ctx = merge $ctx (dict "argsRequired" 1) }} - {{- template "validate-arg-count" $ctx }} - {{- $f = images.Process (index $filterArgs 0) }} -{{- else if eq $filter "saturation" }} - {{- $ctx = merge $ctx (dict "argsRequired" 1) }} - {{- template "validate-arg-count" $ctx }} - {{- $filterArgs = apply $filterArgs "float" "." }} - {{- $ctx = merge $ctx (dict "argName" "percentage" "argValue" (index $filterArgs 0) "min" -100 "max" 500) }} - {{- template "validate-arg-value" $ctx }} - {{- $f = images.Saturation (index $filterArgs 0) }} -{{- else if eq $filter "sepia" }} - {{- $ctx = merge $ctx (dict "argsRequired" 1) }} - {{- template "validate-arg-count" $ctx }} - {{- $filterArgs = apply $filterArgs "float" "." }} - {{- $ctx = merge $ctx (dict "argName" "percentage" "argValue" (index $filterArgs 0) "min" 0 "max" 100) }} - {{- template "validate-arg-value" $ctx }} - {{- $f = images.Sepia (index $filterArgs 0) }} -{{- else if eq $filter "sigmoid" }} - {{- $ctx = merge $ctx (dict "argsRequired" 2) }} - {{- template "validate-arg-count" $ctx }} - {{- $filterArgs = apply $filterArgs "float" "." }} - {{- $ctx = merge $ctx (dict "argName" "midpoint" "argValue" (index $filterArgs 0) "min" 0 "max" 1) }} - {{- template "validate-arg-value" $ctx }} - {{- $ctx = merge $ctx (dict "argName" "factor" "argValue" (index $filterArgs 1) "min" -10 "max" 10) }} - {{- template "validate-arg-value" $ctx }} - {{- $f = images.Sigmoid (index $filterArgs 0) (index $filterArgs 1) }} -{{- else if eq $filter "text" }} - {{- $ctx = merge $ctx (dict "argsRequired" 1) }} - {{- template "validate-arg-count" $ctx }} - {{- $ctx := dict "src" $textFilterOpts.fontPath "name" .Name "position" .Position }} - {{- $font := or (partial "inline/get-resource.html" $ctx) }} - {{- $fontSize := or (index $filterArgs 3 | int) $textFilterOpts.fontSize }} - {{- $lineHeight := math.Max (or (index $filterArgs 4 | float) $textFilterOpts.lineHeight) 1 }} - {{- $opts := dict - "x" (or (index $filterArgs 1 | int) $textFilterOpts.xOffset) - "y" (or (index $filterArgs 2 | int) $textFilterOpts.yOffset) - "size" $fontSize - "linespacing" (mul (sub $lineHeight 1) $fontSize) - "color" (or (index $filterArgs 5) $textFilterOpts.fontColor) - "font" $font - }} - {{- $f = images.Text (index $filterArgs 0) $opts }} -{{- else if eq $filter "unsharpmask" }} - {{- $ctx = merge $ctx (dict "argsRequired" 3) }} - {{- template "validate-arg-count" $ctx }} - {{- $filterArgs = apply $filterArgs "float" "." }} - {{- $ctx = merge $ctx (dict "argName" "sigma" "argValue" (index $filterArgs 0) "min" 0 "max" 500) }} - {{- template "validate-arg-value" $ctx }} - {{- $ctx = merge $ctx (dict "argName" "amount" "argValue" (index $filterArgs 1) "min" 0 "max" 100) }} - {{- template "validate-arg-value" $ctx }} - {{- $ctx = merge $ctx (dict "argName" "threshold" "argValue" (index $filterArgs 2) "min" 0 "max" 1) }} - {{- template "validate-arg-value" $ctx }} - {{- $f = images.UnsharpMask (index $filterArgs 0) (index $filterArgs 1) (index $filterArgs 2) }} -{{- end }} - -{{- /* Apply filter. */}} -{{- $fi := $i }} -{{- with $f }} - {{- $fi = $i.Filter . }} -{{- end }} - -{{- /* Render. */}} -{{- $class := "border-1 border-gray-300 dark:border-gray-500" }} -{{- if $example }} -

      Original

      - {{ $alt }} -

      Processed

      - {{ $alt }} -{{- else -}} - {{ $alt }} -{{- end }} - -{{- define "validate-arg-count" }} - {{- $msg := "When using the %q filter, the %q shortcode requires an args parameter with %d %s. See %s" }} - {{- if lt (len .args) .argsRequired }} - {{- $text := "values" }} - {{- if eq 1 .argsRequired }} - {{- $text = "value" }} - {{- end }} - {{- errorf $msg .filter .name .argsRequired $text .position }} - {{- end }} -{{- end }} - -{{- define "validate-arg-value" }} - {{- $msg := "The %q argument passed to the %q shortcode is invalid. Expected a value in the range [%v,%v], but received %v. See %s" }} - {{- if or (lt .argValue .min) (gt .argValue .max) }} - {{- errorf $msg .argName .name .min .max .argValue .position }} - {{- end }} -{{- end }} - -{{- define "_partials/inline/get-resource.html" }} - {{- $r := "" }} - {{- $u := urls.Parse .src }} - {{- $msg := "The %q shortcode was unable to resolve %s. See %s" }} - {{- if $u.IsAbs }} - {{- with try (resources.GetRemote $u.String) }} - {{- with .Err }} - {{- errorf "%s" . }} - {{- else with .Value }} - {{- /* This is a remote resource. */}} - {{- $r = . }} - {{- else }} - {{- errorf $msg $.name $u.String $.position }} - {{- end }} - {{- end }} - {{- else }} - {{- with .page.Resources.Get (strings.TrimPrefix "./" $u.Path) }} - {{- /* This is a page resource. */}} - {{- $r = . }} - {{- else }} - {{- with resources.Get $u.Path }} - {{- /* This is a global resource. */}} - {{- $r = . }} - {{- else }} - {{- errorf $msg $.name $u.Path $.position }} - {{- end }} - {{- end }} - {{- end }} - {{- return $r }} -{{- end -}} diff --git a/docs/layouts/_shortcodes/imgproc.html b/docs/layouts/_shortcodes/imgproc.html deleted file mode 100644 index fee48525a..000000000 --- a/docs/layouts/_shortcodes/imgproc.html +++ /dev/null @@ -1,39 +0,0 @@ -{{/* prettier-ignore-start */ -}} -{{- /* -Renders the given image using the given process specification. - -@param {string} path The path to the image, either a page resource or a global resource. -@param {string} spec The image processing specification. -@param {string} alt The alt attribute of the img element. - -@example {{< imgproc path="sunset.jpg" spec="resize 300x" alt="A sunset" >}} -*/ -}} -{{/* prettier-ignore-end */ -}} -{{- with $.Get "path" }} - {{- with $i := or ($.Page.Resources.Get .) (resources.Get .) }} - {{- with $spec := $.Get "spec" }} - {{- with $i.Process . }} -
      - {{ $.Get `alt` }} -
      - {{- with $.Inner }} - {{ . }} - {{- else }} - {{ $spec }} - {{- end }} -
      -
      - {{- end }} - {{- else }} - {{- errorf "The %q shortcode requires a 'spec' argument containing the image processing specification. See %s" $.Name $.Position }} - {{- end }} - {{- else }} - {{- errorf "The %q shortcode was unable to find %q. See %s" $.Name . $.Position }} - {{- end }} -{{- else }} - {{- errorf "The %q shortcode requires a 'path' argument indicating the image path. The image must be a page resource or a global resource. See %s" $.Name $.Position }} -{{- end }} diff --git a/docs/layouts/_shortcodes/include.html b/docs/layouts/_shortcodes/include.html deleted file mode 100644 index 81b0c1d8f..000000000 --- a/docs/layouts/_shortcodes/include.html +++ /dev/null @@ -1,20 +0,0 @@ -{{/* prettier-ignore-start */ -}} -{{- /* -Renders the page using the RenderShortcode method on the Page object. - -You must call this shortcode using the {{% %}} notation. - -@param {string} (positional parameter 0) The path to the page, relative to the content directory. - -@example {{% include "functions/_common/glob-patterns" %}} -*/ -}} -{{/* prettier-ignore-end */ -}} -{{- with .Get 0 }} - {{- with or ($.Page.GetPage .) (site.GetPage .) }} - {{- .RenderShortcodes }} - {{- else }} - {{- errorf "The %q shortcode was unable to find %q. See %s" $.Name . $.Position }} - {{- end }} -{{- else }} - {{- errorf "The %q shortcode requires a positional parameter indicating the path of the file to include. See %s" .Name .Position }} -{{- end }} diff --git a/docs/layouts/_shortcodes/list-pages-in-section.html b/docs/layouts/_shortcodes/list-pages-in-section.html deleted file mode 100644 index f6dfe7275..000000000 --- a/docs/layouts/_shortcodes/list-pages-in-section.html +++ /dev/null @@ -1,69 +0,0 @@ -{{- /* -Renders a description list of the pages in the given section. - -Render a subset of the pages in the section by specifying a predefined filter, -and whether to include those pages. - -Filters are defined in the data directory, in the file named page_filters. Each -filter is an array of paths to a file, relative to the root of the content -directory. Hugo will throw an error if the specified filter does not exist, or -if any of the pages in the filter do not exist. - -@param {string} path The path to the section. -@param {string} [filter=""] The name of filter list. -@param {string} [filterType=""] The type of filter, either include or exclude. -@param {string} [titlePrefix=""] The string to prepend to the link title. - -@example {{< list-pages-in-section path=/methods/resources >}} -@example {{< list-pages-in-section path=/functions/images filter=some_filter filterType=exclude >}} -@example {{< list-pages-in-section path=/functions/images filter=some_filter filterType=exclude titlePrefix=foo >}} -*/}} - -{{/* Initialize. */}} -{{ $filter := or "" (.Get "filter" | lower) }} -{{ $filterType := or (.Get "filterType") "none" | lower }} -{{ $filteredPages := slice }} -{{ $titlePrefix := or (.Get "titlePrefix") "" }} - -{{/* Build slice of filtered pages. */}} -{{ with $filter }} - {{ with index site.Data.page_filters . }} - {{ range . }} - {{ with site.GetPage . }} - {{ $filteredPages = $filteredPages | append . }} - {{ else }} - {{ errorf "The %q shortcode was unable to find %q as specified in the page_filters data file. See %s" $.Name . $.Position }} - {{ end }} - {{ end }} - {{ else }} - {{ errorf "The %q shortcode was unable to find the %q filter in the page_filters data file. See %s" $.Name . $.Position }} - {{ end }} -{{ end }} - -{{/* Render. */}} -{{ with $sectionPath := .Get "path" }} - {{ with site.GetPage . }} - {{ with .RegularPages }} - {{ range $page := .ByTitle }} - {{ if or - (and (eq $filterType "include") (in $filteredPages $page)) - (and (eq $filterType "exclude") (not (in $filteredPages $page))) - (eq $filterType "none") - }} - {{ $linkTitle := .LinkTitle }} - {{ with $titlePrefix }} - {{ $linkTitle = printf "%s%s" . $linkTitle }} - {{ end }} -[{{ $linkTitle }}]({{ $page.RelPermalink }}){{/* Do not indent. */}} -: {{ $page.Description }}{{/* Do not indent. */}} - {{ end }} - {{ end }} - {{ else }} - {{ warnf "The %q shortcode found no pages in the %q section. See %s" $.Name $sectionPath $.Position }} - {{ end }} - {{ else }} - {{ errorf "The %q shortcode was unable to find %q. See %s" $.Name $sectionPath $.Position }} - {{ end }} -{{ else }} - {{ errorf "The %q shortcode requires a 'path' parameter indicating the path to the section. See %s" $.Name $.Position }} -{{ end }} diff --git a/docs/layouts/_shortcodes/module-mounts-note.html b/docs/layouts/_shortcodes/module-mounts-note.html deleted file mode 100644 index ba89abcbf..000000000 --- a/docs/layouts/_shortcodes/module-mounts-note.html +++ /dev/null @@ -1,2 +0,0 @@ -For a more flexible approach to configuring this directory, consult the section -on [module mounts](/configuration/module/#mounts). diff --git a/docs/layouts/_shortcodes/new-in.html b/docs/layouts/_shortcodes/new-in.html deleted file mode 100644 index 955d0a710..000000000 --- a/docs/layouts/_shortcodes/new-in.html +++ /dev/null @@ -1,64 +0,0 @@ -{{/* prettier-ignore-start */ -}} -{{- /* -Renders a callout or badge indicating the version in which a feature was added. - -To render a callout, include descriptive text between the opening and closing -tags. To render a badge,omit the descriptive text and call the shortcode with a -self-closing tag. - -When comparing the current version to the specified version, the "new in" -button will be hidden if any of the following conditions is true: - -- The major version difference exceeds the majorVersionDiffThreshold -- The minor version difference exceeds the minorVersionDiffThreshold - -@param {string} 0 The semantic version string, with or without a leading v. - -@example {{< new-in 0.100.0 />}} - -@example {{{< new-in 0.100.0 >}} - Some descriptive text here. - {{< /new-in >}} -*/ -}} -{{/* prettier-ignore-end */ -}} -{{- $majorVersionDiffThreshold := 0 }} -{{- $minorVersionDiffThreshold := 30 }} -{{- $displayExpirationWarning := true }} - -{{- with $version := .Get 0 | strings.TrimLeft "vV" }} - {{- $majorVersionDiff := sub (index (split hugo.Version ".") 0 | int) (index (split $version ".") 0 | int) }} - {{- $minorVersionDiff := sub (index (split hugo.Version ".") 1 | int) (index (split $version ".") 1 | int) }} - {{- if or (gt $majorVersionDiff $majorVersionDiffThreshold) (gt $minorVersionDiff $minorVersionDiffThreshold) }} - {{- if $displayExpirationWarning }} - {{- warnf "This call to the %q shortcode should be removed: %s. The button is now hidden because the specified version (%s) is older than the display threshold." $.Name $.Position $version }} - {{- end }} - {{- else }} - {{- $href := printf "https://github.com/gohugoio/hugo/releases/tag/v%s" $version }} - {{- with $.Inner }} - {{- $inner := strings.TrimSpace . }} - {{- $text := printf "New in [v%s](%s)\n\n%s" $version $href $inner | $.Page.RenderString (dict "display" "block") }} - {{ partial "layouts/blocks/alert.html" (dict - "color" "green" - "icon" "exclamation" - "text" $text - ) - }} - {{- else }} - - - - - - New in - v{{ $version }} - - - {{- end }} - {{- end }} -{{- else }} - {{- errorf "The %q shortcode requires a single positional parameter indicating version. See %s" .Name .Position }} -{{- end }} diff --git a/docs/layouts/_shortcodes/per-lang-config-keys.html b/docs/layouts/_shortcodes/per-lang-config-keys.html deleted file mode 100644 index 31d7daf6a..000000000 --- a/docs/layouts/_shortcodes/per-lang-config-keys.html +++ /dev/null @@ -1,71 +0,0 @@ -{{/* prettier-ignore-start */ -}} -{{- /* -Renders a responsive grid of the configuration keys that can be defined -separately for each language. -*/ -}} -{{/* prettier-ignore-end */ -}} -{{- $siteConfigKeys := slice - (dict "baseURL" "/configuration/all/#baseurl") - (dict "buildDrafts" "/configuration/all/#builddrafts") - (dict "buildExpired" "/configuration/all/#buildexpired") - (dict "buildFuture" "/configuration/all/#buildfuture") - (dict "canonifyURLs" "/configuration/all/#canonifyurls") - (dict "capitalizeListTitles" "/configuration/all/#capitalizelisttitles") - (dict "contentDir" "/configuration/all/#contentdir") - (dict "copyright" "/configuration/all/#copyright") - (dict "disableAliases" "/configuration/all/#disablealiases") - (dict "disableHugoGeneratorInject" "/configuration/all/#disablehugogeneratorinject") - (dict "disableKinds" "/configuration/all/#disablekinds") - (dict "disableLiveReload" "/configuration/all/#disablelivereload") - (dict "disablePathToLower" "/configuration/all/#disablepathtolower") - (dict "enableEmoji " "/configuration/all/#enableemoji") - (dict "frontmatter" "/configuration/front-matter/") - (dict "hasCJKLanguage" "/configuration/all/#hascjklanguage") - (dict "languageCode" "/configuration/all/#languagecode") - (dict "mainSections" "/configuration/all/#mainsections") - (dict "markup" "/configuration/markup/") - (dict "mediaTypes" "/configuration/media-types/") - (dict "menus" "/configuration/menus/") - (dict "outputFormats" "/configuration/output-formats") - (dict "outputs" "/configuration/outputs/") - (dict "page" "/configuration/page/") - (dict "pagination" "/configuration/pagination/") - (dict "params" "/configuration/params/") - (dict "permalinks" "/configuration/permalinks/") - (dict "pluralizeListTitles" "/configuration/all/#pluralizelisttitles") - (dict "privacy" "/configuration/privacy/") - (dict "refLinksErrorLevel" "/configuration/all/#reflinkserrorlevel") - (dict "refLinksNotFoundURL" "/configuration/all/#reflinksnotfoundurl") - (dict "related" "/configuration/related-content/") - (dict "relativeURLs" "/configuration/all/#relativeurls") - (dict "removePathAccents" "/configuration/all/#removepathaccents") - (dict "renderSegments" "/configuration/all/#rendersegments") - (dict "sectionPagesMenu" "/configuration/all/#sectionpagesmenu") - (dict "security" "/configuration/security/") - (dict "services" "/configuration/services/") - (dict "sitemap" "/configuration/sitemap/") - (dict "staticDir" "/configuration/all/#staticdir") - (dict "summaryLength" "/configuration/all/#summarylength") - (dict "taxonomies" "/configuration/taxonomies/") - (dict "timeZone" "/configuration/all/#timezone") - (dict "title" "/configuration/all/#title") - (dict "titleCaseStyle" "/configuration/all/#titlecasestyle") -}} - -{{- $a := len $siteConfigKeys }} -{{- $b := math.Ceil (div $a 2.) }} -{{- $c := math.Ceil (div $a 3.) }} - - -
      - {{- range $siteConfigKeys }} - {{ range $k, $v := . }} - {{ $u := urls.Parse $v }} - {{ if not (site.GetPage $u.Path) }} - {{ errorf "The %q shorcode was unable to find %s. See %s." $.Name $u.Path $.Position }} - {{ end }} - {{ $k }} - {{ end }} - {{- end }} -
      diff --git a/docs/layouts/_shortcodes/quick-reference.html b/docs/layouts/_shortcodes/quick-reference.html deleted file mode 100644 index 0ac544036..000000000 --- a/docs/layouts/_shortcodes/quick-reference.html +++ /dev/null @@ -1,30 +0,0 @@ -{{- /* -Renders the child sections of the given top-level section, listing each child's -immediate descendants. - -@param {string} section The top-level section to render. - -@example {{% quick-reference section="/functions" %}} -*/ -}} -{{ $section := "" }} -{{ with .Get "section" }} - {{ $section = . }} -{{ else }} - {{ errorf "The %q shortcode requires a 'section' parameter. See %s" .Name .Position }} -{{ end }} - -{{ with site.GetPage $section }} - {{ range .Sections }} -## {{ .LinkTitle }}{{/* Do not indent. */}} -{{ .Description }}{{/* Do not indent. */}} - {{ .Content }} - {{ with .Pages }} - {{ range . }} -[{{ .LinkTitle }}]({{ .RelPermalink }}){{/* Do not indent. */}} -: {{ .Description }}{{/* Do not indent. */}} - {{ end }} - {{ end }} - {{ end }} -{{ else }} - {{ errorf "The %q shortcodes was unable to find the %q section. See %s" .Name $section .Position }} -{{ end }} diff --git a/docs/layouts/_shortcodes/root-configuration-keys.html b/docs/layouts/_shortcodes/root-configuration-keys.html deleted file mode 100644 index 46a6e074f..000000000 --- a/docs/layouts/_shortcodes/root-configuration-keys.html +++ /dev/null @@ -1,45 +0,0 @@ -{{/* prettier-ignore-start */ -}} -{{/* -Renders a comma-separated list of links to the root key configuration pages. - -@example {{< root-configuration-keys >}} -*/ -}} -{{/* prettier-ignore-end */ -}} -{{- /* Create scratch map of key:filename. */}} -{{- $s := newScratch }} -{{- range $k, $v := site.Data.docs.config }} - {{- if or (reflect.IsMap .) (reflect.IsSlice .) }} - {{- $s.Set $k ($k | humanize | anchorize) }} - {{- end }} -{{- end }} - -{{/* Deprecated. */}} -{{- $s.Delete "author" }} - -{{/* Use mounts instead. */}} -{{- $s.Delete "staticDir" }} -{{- $s.Delete "ignoreFiles" }} - -{{/* This key is "HTTPCache" not "httpCache". */}} -{{- $s.Set "HTTPCache" "http-cache" }} - -{{/* This key is "frontmatter" not "frontMatter" */}} -{{- $s.Set "frontmatter" "front-matter" }} - -{{/* The page title is "Related content" not "related". */}} -{{- $s.Set "related" "related-content" }} - -{{/* It can be configured as bool or map; we want to show map. */}} -{{- $s.Set "uglyURLs" "ugly-urls" }} - -{{- $links := slice }} -{{- range $k, $v := $s.Values }} - {{- $path := printf "/configuration/%s" $v }} - {{- with site.GetPage $path }} - {{- $links = $links | append (printf "%s" .RelPermalink $k) }} - {{- else }} - {{- errorf "The %q shortcode was unable to find the page %s. See %s." $.Name $path $.Position }} - {{- end }} -{{- end }} - -{{- delimit $links ", " ", and " | safeHTML -}} diff --git a/docs/layouts/_shortcodes/syntax-highlighting-styles.html b/docs/layouts/_shortcodes/syntax-highlighting-styles.html deleted file mode 100644 index 297849cef..000000000 --- a/docs/layouts/_shortcodes/syntax-highlighting-styles.html +++ /dev/null @@ -1,70 +0,0 @@ -{{- /* -Renders a gallery a Chroma syntax highlighting styles. - -@example {{% syntax-highlighting-styles %}} -*/ -}} -{{- $examples := slice }} - -{{- /* Example: css */}} -{{- $example := dict "lang" "css" "code" ` -body { - font-size: 16px; /* comment */ -} -`}} -{{- $examples = $examples | append $example }} - -{{- /* Example: html */}} -{{- $example = dict "lang" "html" "code" ` -Example -`}} -{{- $examples = $examples | append $example }} - -{{- /* Example: go-html-template */}} -{{- $example = dict "lang" "go-html-template" "code" ` -{{ with $.Page.Params.content }} - {{ . | $.Page.RenderString }} {{/* comment */}} -{{ end }} -`}} -{{- $examples = $examples | append $example }} - -{{- /* Example: javascript */}} -{{- $example = dict "lang" "javascript" "code" ` -if ([1,"one",2,"two"].includes(value)){ - console.log("Number is either 1 or 2."); // comment -} -`}} -{{- $examples := $examples | append $example }} - -{{- /* Example: markdown */}} -{{- $example = dict "lang" "markdown" "code" ` -{{< figure src="kitten.jpg" >}} -[example](https://example.org "An example") -`}} -{{- $examples := $examples | append $example }} - -{{- /* Example: toml */}} -{{- $example = dict "lang" "toml" "code" ` -[params] -bool = true # comment -string = 'foo' -`}} -{{- $examples := $examples | append $example }} - -{{- /* Render */}} -{{- with site.Data.docs.chroma.styles }} - {{- range $style := . }} - -### {{ $style }} {class="!mt-7 !mb-6"}{{/* Do not indent. */}} - - {{- range $examples }} - -{{ .lang }}{{/* Do not indent. */}} -{class="text-sm !-mt-3 !-mb-5"}{{/* Do not indent. */}} - -```{{ .lang }} {noClasses=true style="{{ $style }}"}{{/* Do not indent. */}} -{{- .code | safeHTML -}}{{/* Do not indent. */}} -```{{/* Do not indent. */}} - - {{- end }} - {{- end }} -{{- end }} diff --git a/docs/layouts/baseof.html b/docs/layouts/baseof.html deleted file mode 100644 index 4c14a6b6d..000000000 --- a/docs/layouts/baseof.html +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - {{ .Title }} - - - - {{ partial "layouts/head/head-js.html" . }} - {{ with (templates.Defer (dict "key" "global")) }} - {{ $t := debug.Timer "tailwindcss" }} - {{ with resources.Get "css/styles.css" }} - {{ $opts := dict - "inlineImports" true - "minify" (not hugo.IsDevelopment) - }} - {{ with . | css.TailwindCSS $opts }} - {{ partial "helpers/linkcss.html" (dict "r" .) }} - {{ end }} - {{ end }} - {{ $t.Stop }} - {{ end }} - {{ $noop := .WordCount }} - {{ if .Page.Store.Get "hasMath" }} - - {{ end }} - {{ partial "layouts/head/head.html" . }} - - - {{ partial "layouts/hooks/body-start.html" . }} - {{/* Layout. */}} - {{ block "header" . }} - {{ partial "layouts/header/header.html" . }} - {{ end }} - {{ block "subheader" . }} - {{ end }} - {{ block "hero" . }} - {{ end }} -
      -
      - {{ partial "layouts/hooks/body-main-start.html" . }} - {{ block "main" . }}{{ end }} -
      - {{ block "rightsidebar" . }} - - {{ end }} -
      - {{/* Common icons. */}} - {{ partial "layouts/icons.html" . }} - {{/* Common templates. */}} - {{ partial "layouts/templates.html" . }} - {{/* Footer. */}} - {{ block "footer" . }} - {{ partial "layouts/footer.html" . }} - {{ end }} - {{ partial "layouts/hooks/body-end.html" . }} - - diff --git a/docs/layouts/home.headers b/docs/layouts/home.headers deleted file mode 100644 index 1216e42d4..000000000 --- a/docs/layouts/home.headers +++ /dev/null @@ -1,5 +0,0 @@ -/* - X-Frame-Options: DENY - X-XSS-Protection: 1; mode=block - X-Content-Type-Options: nosniff - Referrer-Policy: origin-when-cross-origin diff --git a/docs/layouts/home.html b/docs/layouts/home.html deleted file mode 100644 index 392f66cd8..000000000 --- a/docs/layouts/home.html +++ /dev/null @@ -1,52 +0,0 @@ -{{ define "main" }} -
      - {{ partial "layouts/home/opensource.html" . }} -
      - {{ partial "layouts/home/sponsors.html" (dict "ctx" . "gtag" "home" ) }} -
      - {{ partial "layouts/home/features.html" . }} -
      -{{ end }} - -{{ define "hero" }} -
      -
      -
      - Hugo Logo -

      - The world’s fastest framework for building websites -

      -
      - Hugo is one of the most popular open-source static site generators. - With its amazing speed and flexibility, Hugo makes building websites - fun again. -
      -
      - {{ with site.GetPage "/getting-started" }} - {{ .LinkTitle }} - {{ end }} -
      - {{ partial "layouts/search/button.html" (dict "page" . "standalone" true) }} -
      -
      -
      -
      -
      -{{ end }} - -{{ define "rightsidebar" }} - {{ printf "%c" '\u00A0' }} -{{ end }} - -{{ define "leftsidebar" }} - {{ printf "%c" '\u00A0' }} -{{ end }} diff --git a/docs/layouts/home.redir b/docs/layouts/home.redir deleted file mode 100644 index bb72f96e5..000000000 --- a/docs/layouts/home.redir +++ /dev/null @@ -1,6 +0,0 @@ -# Netlify redirects. See https://www.netlify.com/docs/redirects/ -{{ range $p := .Site.Pages -}} -{{ range .Aliases }} -{{ . | printf "%-35s" }} {{ $p.RelPermalink -}} -{{ end -}} -{{- end -}} diff --git a/docs/layouts/index.html b/docs/layouts/index.html new file mode 100644 index 000000000..a212e1229 --- /dev/null +++ b/docs/layouts/index.html @@ -0,0 +1,319 @@ + + + + + + + + Hugo :: A fast and modern static website engine + + {{ "" | safeHTML }} + + + {{ "" | safeHTML }} + + + {{ "" | safeHTML }} + + + + + {{ "" | safeHTML }} + + + + + + + + + + {{ "" | safeHTML }} + + {{ "" | safeHTML }} + + {{ "" | safeHTML }} +
      + + {{ "" | safeHTML }} +
      +
      + + {{ "" | safeHTML }} +
      +
      + +

      Make the Web Fun Again

      +

      + Introducing Hugo, a new idea for making website creation simple again. + Hugo works flexibly with many formats, and is ideal for + blogs, docs, portfolios and much more. + Hugo’s speed fosters + creativity—it makes building a website fun again. +

      +
      +
      + {{ "" | safeHTML }} + +
      +
      + {{ "" | safeHTML }} + + {{ "" | safeHTML }} +
      +
      + + {{ "" | safeHTML }} +
      +
      + + + +

      Run Anywhere

      +

      + Hugo is quite possibly the easiest software to install you’ve ever used: + simply download and run! + + Hugo doesn’t depend on administrative privileges, databases, runtimes, + interpreters or external libraries. + + Sites built with Hugo can be deployed on S3, GitHub Pages, Dropbox or any web host. +

      +
      +
      + {{ "" | safeHTML }} + +
      +
      + {{ "" | safeHTML }} + + {{ "" | safeHTML }} +
      +
      + + {{ "" | safeHTML }} +
      +
      + +

      Fast & Powerful

      +

      + Hugo is designed for speed and performance. Great care has been + taken to ensure build time with Hugo is as short as possible. + We’re talking milliseconds to build your entire site—for most setups! +

      +
      +
      + {{ "" | safeHTML }} + +
      +
      + {{ "" | safeHTML }} + + {{ "" | safeHTML }} +
      +
      + + {{ "" | safeHTML }} +
      +
      + +

      Flexible

      +

      + Hugo is designed to work the way you do. + Organize your content however you want with any URL structure. + Group your content using your own indexes and categories. + Define your own metadata in any format: YAML, TOML or JSON. + Best of all, Hugo handles all these variations + with virtually no configuration. Hugo + just works. +

      +
      +
      + {{ "" | safeHTML }} + +
      +
      + {{ "" | safeHTML }} + + + {{ "" | safeHTML }} +
      +
      + + {{ "" | safeHTML }} +
      +
      + +

      +
      +
      + {{ "" | safeHTML }} + + {{ "" | safeHTML }} +
      +
      + + {{ "" | safeHTML }} + + {{ "" | safeHTML }} + +
      +
      + {{ "" | safeHTML }} + +
      +
      + {{ "" | safeHTML }} + +
      + {{ "" | safeHTML }} + + {{ "" | safeHTML }} +
      +
      + + {{ "" | safeHTML }} +
      +
      + +

      Open and Free

      +

      + Hugo is open source and completely free. +

      +
      +
      + {{ "" | safeHTML }} + +
      +
      + {{ "" | safeHTML }} + + {{ "" | safeHTML }} +
      +
      + + {{ "" | safeHTML }} +
      +
      + + + +

      Built with

      +

      + Hugo is developed with love by spf13 and friends. + We welcome all contributions. + New to Go? We’ll help you. + Not a developer? No problem! Help + with docs, testing + and themes. +

      +
      +
      + {{ "" | safeHTML }} + +
      +
      + {{ "" | safeHTML }} + + {{ "" | safeHTML }} +
      +
      + + {{ "" | safeHTML }} +
      +
      +

      Getting Started

      + + Download + + + + Quickstart Guide + +

       

      +

      Using Homebrew?

      +
      brew install hugo
      +
      +
      + {{ "" | safeHTML }} + +
      +
      + {{ "" | safeHTML }} + + {{ "" | safeHTML }} +
      +
      + + {{ "" | safeHTML }} +
      +
      +
        +
      • +
      • +
      +
      +

      Copyright © Steve Francia 2013–{{ now.Format "2006" }}

      +
      +
      + {{ "" | safeHTML }} + +
      +
      + {{ "" | safeHTML }} + + {{ "" | safeHTML }} + + + {{ "" | safeHTML }} + {{ `` | safeHTML }} + + + + + {{ "" | safeHTML }} + + +{{ "" | safeHTML }} +{{ template "partials/analytics.html" . }} + + diff --git a/docs/layouts/index.redir b/docs/layouts/index.redir new file mode 100644 index 000000000..2dfd2bc0f --- /dev/null +++ b/docs/layouts/index.redir @@ -0,0 +1,6 @@ +# Netlify redirects. See https://www.netlify.com/docs/redirects/ +{{ range $p := .Site.Pages -}} +{{ range .Aliases }} +{{ . | printf "%-35s" }} {{ $p.RelPermalink -}} +{{ end -}} +{{- end -}} diff --git a/docs/layouts/list.html b/docs/layouts/list.html deleted file mode 100644 index b049b6da9..000000000 --- a/docs/layouts/list.html +++ /dev/null @@ -1,69 +0,0 @@ -{{ define "main" }} - {{ $pages := "" }} - {{ if .IsPage }} - {{/* We currently have a slightly odd content structure with no top level /docs section. */}} - {{ $pages = .CurrentSection.Pages }} - {{ else }} - {{ $pages = .Pages }} - {{ if eq .Section "news" }} - {{ $pages = $pages.ByPublishDate.Reverse }} - {{ end }} - {{ end }} - - -
      - {{ partial "layouts/docsheader.html" . }} - -
      -{{ end }} - -{{ define "rightsidebar" }} - {{ printf "%c" '\u00A0' }} -{{ end }} diff --git a/docs/layouts/list.rss.xml b/docs/layouts/list.rss.xml deleted file mode 100644 index 90fa22148..000000000 --- a/docs/layouts/list.rss.xml +++ /dev/null @@ -1,33 +0,0 @@ -{{- printf "" | safeHTML }} - - - Hugo News - Recent news about Hugo, a static site generator written in Go, optimized for speed and designed for flexibility. - {{ .Permalink }} - Hugo {{ hugo.Version }} - {{ or site.Language.LanguageCode site.Language.Lang }} - {{- with site.Copyright }} - {{ . }} - {{- end }} - {{- with .OutputFormats.Get "rss" }} - {{ printf "" .Permalink .MediaType | safeHTML }} - {{- end }} - {{- $limit := cond (gt site.Config.Services.RSS.Limit 0) site.Config.Services.RSS.Limit 999 }} - {{- $pages := "" }} - {{- with site.GetPage "/news" }} - {{- $pages = .Pages.ByPublishDate.Reverse | first $limit }} - {{- else }} - {{- errorf "The list.rss.xml layout was unable to find the 'news' page." }} - {{- end }} - {{ (index $pages 0).PublishDate.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }} - {{- range $pages }} - - {{ .Title }} - {{ or .Params.permalink .Permalink }} - {{ .PublishDate.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }} - {{ or .Params.permalink .Permalink }} - {{ .Summary | transform.XMLEscape | safeHTML }} - - {{- end }} - - diff --git a/docs/layouts/partials/analytics.html b/docs/layouts/partials/analytics.html new file mode 100644 index 000000000..a30754c0a --- /dev/null +++ b/docs/layouts/partials/analytics.html @@ -0,0 +1,10 @@ + diff --git a/docs/layouts/partials/footer.html b/docs/layouts/partials/footer.html new file mode 100644 index 000000000..6f9976448 --- /dev/null +++ b/docs/layouts/partials/footer.html @@ -0,0 +1,59 @@ +
      + +
      + +
      + + +
    + +
    + {{ if .IsPage }} + {{ with .GetParam "next" }} + + {{ end }} + {{ end }} +
    + + + + + + + + + + + + + + + + + + + + + {{ template "partials/analytics.html" . }} + + diff --git a/docs/layouts/partials/header.html b/docs/layouts/partials/header.html new file mode 100644 index 000000000..8149a7bb6 --- /dev/null +++ b/docs/layouts/partials/header.html @@ -0,0 +1,93 @@ + + + + + + + {{ with .Site.Params.author }}{{ end }} + {{ .Hugo.Generator }} + +{{ .Scratch.Add "title" "" }}{{ if isset .Site.Data.titles .Title }}{{ .Scratch.Set "title" (index .Site.Data.titles .Title).title }}{{ else }}{{ .Scratch.Set "title" .Title}}{{end}} + Hugo - {{ .Scratch.Get "title" }} + + + + + + + + + + + + + + + + +
    + + +
    +
    + +
    +
    + + + + +
    + + +
    + + +
    + + +{{ partial "menu.html" . }} + + +
    +
    + + + + + + + + + + + + + +
    +
    + {{ if .IsPage }} + {{ with .GetParam "prev" }} + + {{ end }} + {{ end }} +
    +
    +
    + + + +
    +
    +
    + {{ partial "search.html" . }} +
    +
    diff --git a/docs/layouts/partials/menu.html b/docs/layouts/partials/menu.html new file mode 100644 index 000000000..d9667291d --- /dev/null +++ b/docs/layouts/partials/menu.html @@ -0,0 +1,40 @@ + + + diff --git a/docs/layouts/partials/quotes.html b/docs/layouts/partials/quotes.html new file mode 100644 index 000000000..3ba0db953 --- /dev/null +++ b/docs/layouts/partials/quotes.html @@ -0,0 +1,12 @@ +{{ range . }} +
    +
    +

    + {{ .quote | safeHTML }} +

    + — {{ .name }} ({{ .twitter_handle }}) + {{ dateFormat "January 2, 2006" .date }} + +
    +
    +{{ end }} diff --git a/docs/layouts/partials/search.html b/docs/layouts/partials/search.html new file mode 100644 index 000000000..6a58160cc --- /dev/null +++ b/docs/layouts/partials/search.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/layouts/section/commands.html b/docs/layouts/section/commands.html new file mode 100644 index 000000000..afa045452 --- /dev/null +++ b/docs/layouts/section/commands.html @@ -0,0 +1,12 @@ +{{ partial "header.html" . }} +

    Hugo Commands

    +

    Hugo’s CLI (command line interface) commands are listed here. +This autogenerated list is always up to date (thanks to + Cobra). +

    + +{{ partial "footer.html" . }} diff --git a/docs/layouts/section/release-notes.html b/docs/layouts/section/release-notes.html new file mode 100644 index 000000000..6af512603 --- /dev/null +++ b/docs/layouts/section/release-notes.html @@ -0,0 +1,6 @@ +{{ define "main" }} +{{ range .Pages }} +

    {{ .Title }} {{ .Date.Format "Jan 2, 2006" }}

    +{{ .Content }} +{{ end }} +{{ end }} \ No newline at end of file diff --git a/docs/layouts/section/showcase.html b/docs/layouts/section/showcase.html new file mode 100644 index 000000000..be13fc612 --- /dev/null +++ b/docs/layouts/section/showcase.html @@ -0,0 +1,17 @@ +{{ partial "header.html" . }} + +

    Hugo-built Sites (with source)

    + +
    + {{ range .Data.Pages.ByDate }} + {{ .Render "thumbnail"}} + {{ end }} +
    + +
    + +
    +If you want to be added to this page, please send a pull request. Check out the how-to guide. +
    + +{{ partial "footer.html" . }} diff --git a/docs/layouts/shortcodes/datatable-vertical.html b/docs/layouts/shortcodes/datatable-vertical.html new file mode 100644 index 000000000..1d2629eca --- /dev/null +++ b/docs/layouts/shortcodes/datatable-vertical.html @@ -0,0 +1,26 @@ +{{ $package := (index .Params 0) }} +{{ $listname := (index .Params 1) }} +{{ $list := (index (index .Site.Data.docs $package) $listname) }} +{{ $fields := after 2 .Params }} + + {{ range $list }} + {{ range $k, $v := . }} + {{ $.Scratch.Set $k $v }} + {{ end }} + {{ end }} + + {{ range $i, $_ := $fields }} + + {{ $.Scratch.Set "i" $i }} + + {{ $field := (index $fields ($.Scratch.Get "i") ) }} + + {{ range $list }} + + {{ end }} + + + {{ end }} +
    {{ $field }} + {{ index . $field }} +
    \ No newline at end of file diff --git a/docs/layouts/shortcodes/datatable.html b/docs/layouts/shortcodes/datatable.html new file mode 100644 index 000000000..f40605404 --- /dev/null +++ b/docs/layouts/shortcodes/datatable.html @@ -0,0 +1,23 @@ +{{ $package := (index .Params 0) }} +{{ $listname := (index .Params 1) }} +{{ $list := (index (index .Site.Data.docs $package) $listname) }} +{{ $fields := after 2 .Params }} + + + + {{ range $fields }} + + {{ end }} + + {{ range $list }} + + {{ range $k, $v := . }} + {{ $.Scratch.Set $k $v }} + {{ end }} + {{ range $fields }} + + {{ end }} + + {{ end }} +
    {{ . }}
    {{ $.Scratch.Get . }}
    + diff --git a/docs/layouts/shortcodes/directoryindex.html b/docs/layouts/shortcodes/directoryindex.html new file mode 100644 index 000000000..02a4efadc --- /dev/null +++ b/docs/layouts/shortcodes/directoryindex.html @@ -0,0 +1,13 @@ +{{- $pathURL := .Get "pathURL" -}} +{{- $path := .Get "path" -}} +{{- $files := readDir $path -}} + + + +{{- range $files }} + + + + +{{- end }} +
    Size in bytesName
    {{ .Size }} {{ .Name }}
    diff --git a/docs/layouts/shortcodes/gh.html b/docs/layouts/shortcodes/gh.html new file mode 100644 index 000000000..0a28bf121 --- /dev/null +++ b/docs/layouts/shortcodes/gh.html @@ -0,0 +1,9 @@ +{{ range .Params }} +{{ if eq (substr . 0 1) "@" }} +{{ . }} +{{ else if eq (substr . 0 2) "0x" }} +{{ substr . 2 6 }} +{{ else }} +#{{ . }} +{{ end }} +{{ end }} diff --git a/docs/layouts/shortcodes/nohighlight.html b/docs/layouts/shortcodes/nohighlight.html new file mode 100644 index 000000000..d9cb5f302 --- /dev/null +++ b/docs/layouts/shortcodes/nohighlight.html @@ -0,0 +1 @@ +
    {{ .Inner }}
    diff --git a/docs/layouts/shortcodes/readfile.html b/docs/layouts/shortcodes/readfile.html new file mode 100644 index 000000000..f5b3459bf --- /dev/null +++ b/docs/layouts/shortcodes/readfile.html @@ -0,0 +1 @@ +{{- .Get 0 | readFile -}} diff --git a/docs/layouts/shortcodes/youtube.html b/docs/layouts/shortcodes/youtube.html new file mode 100644 index 000000000..ce7dd0508 --- /dev/null +++ b/docs/layouts/shortcodes/youtube.html @@ -0,0 +1,4 @@ +
    + +
    diff --git a/docs/layouts/showcase/thumbnail.html b/docs/layouts/showcase/thumbnail.html new file mode 100644 index 000000000..5c36e5d2f --- /dev/null +++ b/docs/layouts/showcase/thumbnail.html @@ -0,0 +1,17 @@ +
    +
    +
    +
    + {{ .Description }} +

    + {{ .Title | safeHTML }} + {{ if (isset .Params "sourcelink") }} + source + {{ end }} +

    + {{ range .Params.tags }} + {{ . }} + {{ end }} +
    +
    +
    diff --git a/docs/layouts/single.html b/docs/layouts/single.html deleted file mode 100644 index 2e9e4f379..000000000 --- a/docs/layouts/single.html +++ /dev/null @@ -1,80 +0,0 @@ -{{ define "main" }} - {{ $ttop := debug.Timer "single" }} -
    - {{ partial "layouts/docsheader.html" . }} -
    - {{ with .Params.description }} -
    - {{ . | markdownify }} -
    - {{ end }} - {{ if .Params.show_publish_date }} - {{ with .PublishDate }} -

    - {{ partial "layouts/date.html" . }} -

    - {{ end }} - {{ end }} - {{ $t := debug.Timer "single.categories" }} - {{ $categories := .GetTerms "categories" }} - {{ with $categories }} -
    - {{ range . }} - {{ $text := .LinkTitle }} - {{ $class := "" }} - {{ range (slice true false ) }} - {{ $color := partial "helpers/funcs/color-from-string.html" (dict "text" $text "dark" . "--single" "green" ) }} - - {{ $prefix := "" }} - {{ if . }} - {{ $prefix = "dark:" }} - {{ end }} - {{ $class = printf "%sbg-%s-%d %stext-%s-%d border %sborder-%s-%d" - $prefix $color.color $color.shade1 - $prefix $color.color $color.shade2 - $prefix $color.color $color.shade3 - }} - {{ end }} - - - - {{ .LinkTitle }} - - {{ end }} -
    - {{ end }} - {{ $t.Stop }} - - {{ if .Params.functions_and_methods.signatures }} -
    - {{- partial "docs/functions-signatures.html" . -}} - {{- partial "docs/functions-return-type.html" . -}} - {{- partial "docs/functions-aliases.html" . -}} -
    - {{ end }} - {{ $t := debug.Timer "single.content" }} - {{ .Content }} - {{ $t.Stop }} - {{ $t := debug.Timer "single.page-edit" }} - {{ partial "layouts/page-edit.html" . }} - {{ $t.Stop }} -
    -
    - {{ $ttop.Stop }} -{{ end }} - -{{ define "rightsidebar_content" }} - {{/* in-this-section.html depends on these being reneredc first. */}} - {{ $related := partial "layouts/related.html" . }} - {{ $toc := partial "layouts/toc.html" . }} - {{ if not .Params.hide_in_this_section }} - {{ partial "layouts/in-this-section.html" . }} - {{ end }} - {{ $related }} - {{ if $.Store.Get "hasToc" }} - {{ $toc }} - {{ end }} -{{ end }} diff --git a/docs/netlify.toml b/docs/netlify.toml deleted file mode 100644 index c24a32a60..000000000 --- a/docs/netlify.toml +++ /dev/null @@ -1,55 +0,0 @@ -[build] - publish = "public" - command = "hugo --gc --minify" - - [build.environment] - HUGO_VERSION = "0.146.7" - -[context.production.environment] - HUGO_ENV = "production" - HUGO_ENABLEGITINFO = "true" - -[context.split1] - command = "hugo --gc --minify --enableGitInfo" - - [context.split1.environment] - HUGO_ENV = "production" - -[context.deploy-preview] - command = "hugo --gc --minify --buildFuture -b $DEPLOY_PRIME_URL --enableGitInfo" - -[context.branch-deploy] - command = "hugo --gc --minify -b $DEPLOY_PRIME_URL" - -[context.next.environment] - HUGO_ENABLEGITINFO = "true" - -[[headers]] - for = "/*.jpg" - - [headers.values] - Cache-Control = "public, max-age=31536000" - -[[headers]] - for = "/*.png" - - [headers.values] - Cache-Control = "public, max-age=31536000" - -[[headers]] - for = "/*.css" - - [headers.values] - Cache-Control = "public, max-age=31536000" - -[[headers]] - for = "/*.js" - - [headers.values] - Cache-Control = "public, max-age=31536000" - -[[headers]] - for = "/*.ttf" - - [headers.values] - Cache-Control = "public, max-age=31536000" diff --git a/docs/package.hugo.json b/docs/package.hugo.json deleted file mode 100644 index 24ffc7ff5..000000000 --- a/docs/package.hugo.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "hugoDocs", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "", - "license": "", - "devDependencies": { - "@awmottaz/prettier-plugin-void-html": "~1.8.0", - "@tailwindcss/cli": "~4.1.0", - "@tailwindcss/typography": "~0.5.16", - "prettier": "~3.5.3", - "prettier-plugin-go-template": "~0.0.15", - "tailwindcss": "~4.1.0" - }, - "dependencies": { - "@alpinejs/focus": "~3.14.9", - "@alpinejs/persist": "~3.14.9", - "@hotwired/turbo": "~8.0.13", - "alpinejs": "~3.14.9" - } -} diff --git a/docs/package.json b/docs/package.json deleted file mode 100644 index 24ffc7ff5..000000000 --- a/docs/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "hugoDocs", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "", - "license": "", - "devDependencies": { - "@awmottaz/prettier-plugin-void-html": "~1.8.0", - "@tailwindcss/cli": "~4.1.0", - "@tailwindcss/typography": "~0.5.16", - "prettier": "~3.5.3", - "prettier-plugin-go-template": "~0.0.15", - "tailwindcss": "~4.1.0" - }, - "dependencies": { - "@alpinejs/focus": "~3.14.9", - "@alpinejs/persist": "~3.14.9", - "@hotwired/turbo": "~8.0.13", - "alpinejs": "~3.14.9" - } -} diff --git a/docs/static/_headers b/docs/static/_headers new file mode 100644 index 000000000..53cc866dc --- /dev/null +++ b/docs/static/_headers @@ -0,0 +1,4 @@ +/* + X-Frame-Options: DENY + X-XSS-Protection: 1; mode=block + X-Content-Type-Options: nosniff \ No newline at end of file diff --git a/docs/static/android-chrome-144x144.png b/docs/static/android-chrome-144x144.png deleted file mode 100644 index 975cb33ba..000000000 Binary files a/docs/static/android-chrome-144x144.png and /dev/null differ diff --git a/docs/static/android-chrome-192x192.png b/docs/static/android-chrome-192x192.png deleted file mode 100644 index 7ab6c3849..000000000 Binary files a/docs/static/android-chrome-192x192.png and /dev/null differ diff --git a/docs/static/android-chrome-256x256.png b/docs/static/android-chrome-256x256.png deleted file mode 100644 index ed88a2224..000000000 Binary files a/docs/static/android-chrome-256x256.png and /dev/null differ diff --git a/docs/static/android-chrome-36x36.png b/docs/static/android-chrome-36x36.png deleted file mode 100644 index 3695eb088..000000000 Binary files a/docs/static/android-chrome-36x36.png and /dev/null differ diff --git a/docs/static/android-chrome-48x48.png b/docs/static/android-chrome-48x48.png deleted file mode 100644 index ca275dad6..000000000 Binary files a/docs/static/android-chrome-48x48.png and /dev/null differ diff --git a/docs/static/android-chrome-72x72.png b/docs/static/android-chrome-72x72.png deleted file mode 100644 index 966891f25..000000000 Binary files a/docs/static/android-chrome-72x72.png and /dev/null differ diff --git a/docs/static/android-chrome-96x96.png b/docs/static/android-chrome-96x96.png deleted file mode 100644 index feb1d3ebf..000000000 Binary files a/docs/static/android-chrome-96x96.png and /dev/null differ diff --git a/docs/static/apple-touch-icon.png b/docs/static/apple-touch-icon.png index ecf1fc020..50e23ce1d 100644 Binary files a/docs/static/apple-touch-icon.png and b/docs/static/apple-touch-icon.png differ diff --git a/docs/static/css/bootstrap-additions-gohugo.css b/docs/static/css/bootstrap-additions-gohugo.css new file mode 100644 index 000000000..4128f3b8c --- /dev/null +++ b/docs/static/css/bootstrap-additions-gohugo.css @@ -0,0 +1,65 @@ +/*! + * Bootstrap for http://gohugo.io/ + * additional property-value pairs, for selectors already in Bootstrap; + * also, additional Bootstrap-like selectors + * + * Keep all such property additions to Bootstrap v3.3.6 here. + * + * Here, maintain the same order as the original Bootstrap file. + * + * Keep any additional Bootstrap-like selectors at the bottom. + * + * Copyright 2013-2016 Steve Francia and the Hugo Authors + * + * Based on 'dist/css/bootstrap.css' from: + * + * Bootstrap v3.3.6 (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +.btn { + -webkit-transition: all 0.15s; + -moz-transition: all 0.15s; + transition: all 0.15s; +} +.btn-primary:focus, +.btn-primary:active, +.btn-success:focus, +.btn-success:active, +.btn-info:focus, +.btn-info:active { + background-color: transparent; +} +.btn-default:hover, +.btn-default:active, +.btn-primary:hover, +.btn-primary:active, +.btn-success:hover, +.btn-success:active, +.btn-info:hover, +.btn-info:active { + outline: none; +} + +/* additional Bootstrap-like selectors */ + +.btn-repo { + border-color: black; + background-color: rgba(30, 30, 30, 0.8); + color: white; +} +.btn-repo:focus, +.btn-repo:active { + color: black; +} +.btn-repo:hover { + background-color: aliceblue; +} +.btn-repo:focus, +.btn-repo:active { + background-color: transparent; +} +.btn-repo:hover, +.btn-repo:active { + outline: none; +} diff --git a/docs/static/css/bootstrap-changes-gohugo.css b/docs/static/css/bootstrap-changes-gohugo.css new file mode 100644 index 000000000..d4ef99bcc --- /dev/null +++ b/docs/static/css/bootstrap-changes-gohugo.css @@ -0,0 +1,136 @@ +/*! + * Bootstrap for http://gohugo.io/ + * value changes to properties in Bootstrap selectors; + * Bootstrap must already have the same properties in the same selectors. + * + * Keep all such value changes to Bootstrap v3.3.6 here. + * + * Here, maintain the same order as the original Bootstrap file. + * + * Keep all new properties, for new or existing selectors, + * in other stylesheets. + * + * Copyright 2013-2016 Steve Francia and the Hugo Authors + * + * Based on 'dist/css/bootstrap.css' from: + * + * Bootstrap v3.3.6 (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +body { + font-family: Lato; + color: #2b2b2b; +} +a { + color: #ff4088; +} +a:hover, +a:focus { + color: #ff4088; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: Lato; + font-weight: 400; +} +code, +kbd, +pre, +samp { + font-family: Menlo, Consolas, 'DejaVu Sans Mono', + 'Bitstream Vera Sans Mono', 'Lucida Console', Monaco, + 'Droid Sans Mono', monospace; +} +legend { + color: #2b2b2b; + border-bottom: 1px solid #c7c7cc; +} +.btn:focus { + /*outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px;*/ + outline: none; +} +.btn:hover, +.btn:focus { + color: #2b2b2b; +} +.btn-default:focus { + color: #fff; + background-color: #9e9e9e; + border-color: #9e9e9e; +} +.btn-default:hover { + color: #fff; + background-color: #9e9e9e; + border-color: #9e9e9e; +} +.btn-default:active { + color: #fff; + background-color: #9e9e9e; + border-color: #9e9e9e; +} +.btn-primary { + background-color: #007aff; + border-color: #007aff; +} +.btn-primary:focus { + color: #007aff; + border-color: #007aff; +} +.btn-primary:hover { + color: #007aff; + background-color: aliceblue; + border-color: #007aff; +} +.btn-primary:active { + color: #007aff; + border-color: #007aff; +} +.btn-success { + background-color: #4cd964; + border-color: #4cd964; +} +.btn-success:focus { + color: #4cd964; + border-color: #4cd964; +} +.btn-success:hover { + color: #4cd964; + background-color: aliceblue; + border-color: #4cd964; +} +.btn-success:active { + color: #4cd964; + border-color: #4cd964; +} +.btn-info { + background-color: #34aadc; + border-color: #34aadc; +} +.btn-info:focus { + color: #34aadc; + border-color: #34aadc; +} +.btn-info:hover { + color: #34aadc; + background-color: aliceblue; + border-color: #34aadc; +} +.btn-info:active { + color: #34aadc; + border-color: #34aadc; +} +.panel { + border: 0px solid transparent; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); +} +.panel-body { + padding: 15px 15px 0px 15px; +} diff --git a/docs/static/css/bootstrap-stripped-gohugo.css b/docs/static/css/bootstrap-stripped-gohugo.css new file mode 100644 index 000000000..aafb0df53 --- /dev/null +++ b/docs/static/css/bootstrap-stripped-gohugo.css @@ -0,0 +1,1380 @@ +/*! + * Bootstrap for http://gohugo.io/ + * with many unused property-value pairs, as well as selectors, removed + * + * Copyright 2013-2016 Steve Francia and the Hugo Authors + * + * Based on 'dist/css/bootstrap.css' from: + * + * Bootstrap v3.3.6 (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ +html { + font-family: sans-serif; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} +body { + margin: 0; +} +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +menu, +nav, +section, +summary { + display: block; +} +audio, +canvas, +progress, +video { + display: inline-block; + vertical-align: baseline; +} +a:active, +a:hover { + outline: 0; +} +b, +strong { + font-weight: bold; +} +dfn { + font-style: italic; +} +h1 { + margin: .67em 0; + font-size: 2em; +} +mark { + color: #000; + background: #ff0; +} +small { + font-size: 80%; +} +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} +sup { + top: -.5em; +} +sub { + bottom: -.25em; +} +img { + border: 0; +} +figure { + margin: 1em 40px; +} +hr { + height: 0; + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; +} +pre { + overflow: auto; +} +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} +button, +input, +optgroup, +select, +textarea { + margin: 0; + font: inherit; + color: inherit; +} +button { + overflow: visible; +} +button, +select { + text-transform: none; +} +button { + -webkit-appearance: button; + cursor: pointer; +} +button::-moz-focus-inner, +input::-moz-focus-inner { + padding: 0; + border: 0; +} +input { + line-height: normal; +} +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} +fieldset { + padding: .35em .625em .75em; + margin: 0 2px; + border: 1px solid #c0c0c0; +} +legend { + padding: 0; + border: 0; +} +textarea { + overflow: auto; +} +optgroup { + font-weight: bold; +} +table { + border-spacing: 0; + border-collapse: collapse; +} +td, +th { + padding: 0; +} +/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */ +@media print { + *, + *:before, + *:after { + color: #000 !important; + text-shadow: none !important; + background: transparent !important; + -webkit-box-shadow: none !important; + box-shadow: none !important; + } + a, + a:visited { + text-decoration: underline; + } + a[href]:after { + content: " (" attr(href) ")"; + } + a[href^="#"]:after, + a[href^="javascript:"]:after { + content: ""; + } + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + thead { + display: table-header-group; + } + tr, + img { + page-break-inside: avoid; + } + img { + max-width: 100% !important; + } + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + h2, + h3 { + page-break-after: avoid; + } +} +@font-face { + font-family: 'Glyphicons Halflings'; + + src: url('../fonts/glyphicons-halflings-regular.eot'); + src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); +} +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +*:before, +*:after { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +html { + font-size: 10px; + + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} +body { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 1.42857143; + color: #333; + background-color: #fff; +} +input, +button, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; +} +a { + color: #337ab7; + text-decoration: none; +} +a:hover, +a:focus { + color: #23527c; + text-decoration: underline; +} +a:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +figure { + margin: 0; +} +img { + vertical-align: middle; +} +hr { + margin-top: 20px; + margin-bottom: 20px; + border: 0; + border-top: 1px solid #eee; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: inherit; + font-weight: 500; + line-height: 1.1; + color: inherit; +} +h1, +h2, +h3 { + margin-top: 20px; + margin-bottom: 10px; +} +h4, +h5, +h6 { + margin-top: 10px; + margin-bottom: 10px; +} +h1 { + font-size: 36px; +} +h2 { + font-size: 30px; +} +h3 { + font-size: 24px; +} +h4 { + font-size: 18px; +} +h5 { + font-size: 14px; +} +h6 { + font-size: 12px; +} +p { + margin: 0 0 10px; +} +.lead { + margin-bottom: 20px; + font-size: 16px; + font-weight: 300; + line-height: 1.4; +} +@media (min-width: 768px) { + .lead { + font-size: 21px; + } +} +small, +.small { + font-size: 85%; +} +.text-center { + text-align: center; +} +ul, +ol { + margin-top: 0; + margin-bottom: 10px; +} +ul ul { + margin-bottom: 0; +} +.list-inline { + padding-left: 0; + margin-left: -5px; + list-style: none; +} +.list-inline > li { + display: inline-block; + padding-right: 5px; + padding-left: 5px; +} +dl { + margin-top: 0; + margin-bottom: 20px; +} +dt, +dd { + line-height: 1.42857143; +} +dt { + font-weight: bold; +} +dd { + margin-left: 0; +} +blockquote { + padding: 10px 20px; + margin: 0 0 20px; + font-size: 17.5px; + border-left: 5px solid #eee; +} +blockquote p:last-child, +blockquote ul:last-child, +blockquote ol:last-child { + margin-bottom: 0; +} +address { + margin-bottom: 20px; + font-style: normal; + line-height: 1.42857143; +} +code, +kbd, +pre, +samp { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; +} +code { + padding: 2px 4px; + font-size: 90%; + color: #c7254e; + background-color: #f9f2f4; + border-radius: 4px; +} +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.42857143; + color: #333; + word-break: break-all; + word-wrap: break-word; + background-color: #f5f5f5; + border: 1px solid #ccc; + border-radius: 4px; +} +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; +} +.container { + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} +@media (min-width: 768px) { + .container { + width: 750px; + } +} +@media (min-width: 992px) { + .container { + width: 970px; + } +} +@media (min-width: 1200px) { + .container { + width: 1170px; + } +} +.row { + margin-right: -15px; + margin-left: -15px; +} +.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; +} +.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 { + float: left; +} +.col-xs-12 { + width: 100%; +} +.col-xs-11 { + width: 91.66666667%; +} +.col-xs-10 { + width: 83.33333333%; +} +.col-xs-9 { + width: 75%; +} +.col-xs-8 { + width: 66.66666667%; +} +.col-xs-7 { + width: 58.33333333%; +} +.col-xs-6 { + width: 50%; +} +.col-xs-5 { + width: 41.66666667%; +} +.col-xs-4 { + width: 33.33333333%; +} +.col-xs-3 { + width: 25%; +} +.col-xs-2 { + width: 16.66666667%; +} +.col-xs-1 { + width: 8.33333333%; +} +.col-xs-pull-12 { + right: 100%; +} +.col-xs-pull-11 { + right: 91.66666667%; +} +.col-xs-pull-10 { + right: 83.33333333%; +} +.col-xs-pull-9 { + right: 75%; +} +.col-xs-pull-8 { + right: 66.66666667%; +} +.col-xs-pull-7 { + right: 58.33333333%; +} +.col-xs-pull-6 { + right: 50%; +} +.col-xs-pull-5 { + right: 41.66666667%; +} +.col-xs-pull-4 { + right: 33.33333333%; +} +.col-xs-pull-3 { + right: 25%; +} +.col-xs-pull-2 { + right: 16.66666667%; +} +.col-xs-pull-1 { + right: 8.33333333%; +} +.col-xs-pull-0 { + right: auto; +} +.col-xs-push-12 { + left: 100%; +} +.col-xs-push-11 { + left: 91.66666667%; +} +.col-xs-push-10 { + left: 83.33333333%; +} +.col-xs-push-9 { + left: 75%; +} +.col-xs-push-8 { + left: 66.66666667%; +} +.col-xs-push-7 { + left: 58.33333333%; +} +.col-xs-push-6 { + left: 50%; +} +.col-xs-push-5 { + left: 41.66666667%; +} +.col-xs-push-4 { + left: 33.33333333%; +} +.col-xs-push-3 { + left: 25%; +} +.col-xs-push-2 { + left: 16.66666667%; +} +.col-xs-push-1 { + left: 8.33333333%; +} +.col-xs-push-0 { + left: auto; +} +.col-xs-offset-12 { + margin-left: 100%; +} +.col-xs-offset-11 { + margin-left: 91.66666667%; +} +.col-xs-offset-10 { + margin-left: 83.33333333%; +} +.col-xs-offset-9 { + margin-left: 75%; +} +.col-xs-offset-8 { + margin-left: 66.66666667%; +} +.col-xs-offset-7 { + margin-left: 58.33333333%; +} +.col-xs-offset-6 { + margin-left: 50%; +} +.col-xs-offset-5 { + margin-left: 41.66666667%; +} +.col-xs-offset-4 { + margin-left: 33.33333333%; +} +.col-xs-offset-3 { + margin-left: 25%; +} +.col-xs-offset-2 { + margin-left: 16.66666667%; +} +.col-xs-offset-1 { + margin-left: 8.33333333%; +} +.col-xs-offset-0 { + margin-left: 0; +} +@media (min-width: 768px) { + .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 { + float: left; + } + .col-sm-12 { + width: 100%; + } + .col-sm-11 { + width: 91.66666667%; + } + .col-sm-10 { + width: 83.33333333%; + } + .col-sm-9 { + width: 75%; + } + .col-sm-8 { + width: 66.66666667%; + } + .col-sm-7 { + width: 58.33333333%; + } + .col-sm-6 { + width: 50%; + } + .col-sm-5 { + width: 41.66666667%; + } + .col-sm-4 { + width: 33.33333333%; + } + .col-sm-3 { + width: 25%; + } + .col-sm-2 { + width: 16.66666667%; + } + .col-sm-1 { + width: 8.33333333%; + } + .col-sm-pull-12 { + right: 100%; + } + .col-sm-pull-11 { + right: 91.66666667%; + } + .col-sm-pull-10 { + right: 83.33333333%; + } + .col-sm-pull-9 { + right: 75%; + } + .col-sm-pull-8 { + right: 66.66666667%; + } + .col-sm-pull-7 { + right: 58.33333333%; + } + .col-sm-pull-6 { + right: 50%; + } + .col-sm-pull-5 { + right: 41.66666667%; + } + .col-sm-pull-4 { + right: 33.33333333%; + } + .col-sm-pull-3 { + right: 25%; + } + .col-sm-pull-2 { + right: 16.66666667%; + } + .col-sm-pull-1 { + right: 8.33333333%; + } + .col-sm-pull-0 { + right: auto; + } + .col-sm-push-12 { + left: 100%; + } + .col-sm-push-11 { + left: 91.66666667%; + } + .col-sm-push-10 { + left: 83.33333333%; + } + .col-sm-push-9 { + left: 75%; + } + .col-sm-push-8 { + left: 66.66666667%; + } + .col-sm-push-7 { + left: 58.33333333%; + } + .col-sm-push-6 { + left: 50%; + } + .col-sm-push-5 { + left: 41.66666667%; + } + .col-sm-push-4 { + left: 33.33333333%; + } + .col-sm-push-3 { + left: 25%; + } + .col-sm-push-2 { + left: 16.66666667%; + } + .col-sm-push-1 { + left: 8.33333333%; + } + .col-sm-push-0 { + left: auto; + } + .col-sm-offset-12 { + margin-left: 100%; + } + .col-sm-offset-11 { + margin-left: 91.66666667%; + } + .col-sm-offset-10 { + margin-left: 83.33333333%; + } + .col-sm-offset-9 { + margin-left: 75%; + } + .col-sm-offset-8 { + margin-left: 66.66666667%; + } + .col-sm-offset-7 { + margin-left: 58.33333333%; + } + .col-sm-offset-6 { + margin-left: 50%; + } + .col-sm-offset-5 { + margin-left: 41.66666667%; + } + .col-sm-offset-4 { + margin-left: 33.33333333%; + } + .col-sm-offset-3 { + margin-left: 25%; + } + .col-sm-offset-2 { + margin-left: 16.66666667%; + } + .col-sm-offset-1 { + margin-left: 8.33333333%; + } + .col-sm-offset-0 { + margin-left: 0; + } +} +@media (min-width: 992px) { + .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 { + float: left; + } + .col-md-12 { + width: 100%; + } + .col-md-11 { + width: 91.66666667%; + } + .col-md-10 { + width: 83.33333333%; + } + .col-md-9 { + width: 75%; + } + .col-md-8 { + width: 66.66666667%; + } + .col-md-7 { + width: 58.33333333%; + } + .col-md-6 { + width: 50%; + } + .col-md-5 { + width: 41.66666667%; + } + .col-md-4 { + width: 33.33333333%; + } + .col-md-3 { + width: 25%; + } + .col-md-2 { + width: 16.66666667%; + } + .col-md-1 { + width: 8.33333333%; + } + .col-md-pull-12 { + right: 100%; + } + .col-md-pull-11 { + right: 91.66666667%; + } + .col-md-pull-10 { + right: 83.33333333%; + } + .col-md-pull-9 { + right: 75%; + } + .col-md-pull-8 { + right: 66.66666667%; + } + .col-md-pull-7 { + right: 58.33333333%; + } + .col-md-pull-6 { + right: 50%; + } + .col-md-pull-5 { + right: 41.66666667%; + } + .col-md-pull-4 { + right: 33.33333333%; + } + .col-md-pull-3 { + right: 25%; + } + .col-md-pull-2 { + right: 16.66666667%; + } + .col-md-pull-1 { + right: 8.33333333%; + } + .col-md-pull-0 { + right: auto; + } + .col-md-push-12 { + left: 100%; + } + .col-md-push-11 { + left: 91.66666667%; + } + .col-md-push-10 { + left: 83.33333333%; + } + .col-md-push-9 { + left: 75%; + } + .col-md-push-8 { + left: 66.66666667%; + } + .col-md-push-7 { + left: 58.33333333%; + } + .col-md-push-6 { + left: 50%; + } + .col-md-push-5 { + left: 41.66666667%; + } + .col-md-push-4 { + left: 33.33333333%; + } + .col-md-push-3 { + left: 25%; + } + .col-md-push-2 { + left: 16.66666667%; + } + .col-md-push-1 { + left: 8.33333333%; + } + .col-md-push-0 { + left: auto; + } + .col-md-offset-12 { + margin-left: 100%; + } + .col-md-offset-11 { + margin-left: 91.66666667%; + } + .col-md-offset-10 { + margin-left: 83.33333333%; + } + .col-md-offset-9 { + margin-left: 75%; + } + .col-md-offset-8 { + margin-left: 66.66666667%; + } + .col-md-offset-7 { + margin-left: 58.33333333%; + } + .col-md-offset-6 { + margin-left: 50%; + } + .col-md-offset-5 { + margin-left: 41.66666667%; + } + .col-md-offset-4 { + margin-left: 33.33333333%; + } + .col-md-offset-3 { + margin-left: 25%; + } + .col-md-offset-2 { + margin-left: 16.66666667%; + } + .col-md-offset-1 { + margin-left: 8.33333333%; + } + .col-md-offset-0 { + margin-left: 0; + } +} +@media (min-width: 1200px) { + .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 { + float: left; + } + .col-lg-12 { + width: 100%; + } + .col-lg-11 { + width: 91.66666667%; + } + .col-lg-10 { + width: 83.33333333%; + } + .col-lg-9 { + width: 75%; + } + .col-lg-8 { + width: 66.66666667%; + } + .col-lg-7 { + width: 58.33333333%; + } + .col-lg-6 { + width: 50%; + } + .col-lg-5 { + width: 41.66666667%; + } + .col-lg-4 { + width: 33.33333333%; + } + .col-lg-3 { + width: 25%; + } + .col-lg-2 { + width: 16.66666667%; + } + .col-lg-1 { + width: 8.33333333%; + } + .col-lg-pull-12 { + right: 100%; + } + .col-lg-pull-11 { + right: 91.66666667%; + } + .col-lg-pull-10 { + right: 83.33333333%; + } + .col-lg-pull-9 { + right: 75%; + } + .col-lg-pull-8 { + right: 66.66666667%; + } + .col-lg-pull-7 { + right: 58.33333333%; + } + .col-lg-pull-6 { + right: 50%; + } + .col-lg-pull-5 { + right: 41.66666667%; + } + .col-lg-pull-4 { + right: 33.33333333%; + } + .col-lg-pull-3 { + right: 25%; + } + .col-lg-pull-2 { + right: 16.66666667%; + } + .col-lg-pull-1 { + right: 8.33333333%; + } + .col-lg-pull-0 { + right: auto; + } + .col-lg-push-12 { + left: 100%; + } + .col-lg-push-11 { + left: 91.66666667%; + } + .col-lg-push-10 { + left: 83.33333333%; + } + .col-lg-push-9 { + left: 75%; + } + .col-lg-push-8 { + left: 66.66666667%; + } + .col-lg-push-7 { + left: 58.33333333%; + } + .col-lg-push-6 { + left: 50%; + } + .col-lg-push-5 { + left: 41.66666667%; + } + .col-lg-push-4 { + left: 33.33333333%; + } + .col-lg-push-3 { + left: 25%; + } + .col-lg-push-2 { + left: 16.66666667%; + } + .col-lg-push-1 { + left: 8.33333333%; + } + .col-lg-push-0 { + left: auto; + } + .col-lg-offset-12 { + margin-left: 100%; + } + .col-lg-offset-11 { + margin-left: 91.66666667%; + } + .col-lg-offset-10 { + margin-left: 83.33333333%; + } + .col-lg-offset-9 { + margin-left: 75%; + } + .col-lg-offset-8 { + margin-left: 66.66666667%; + } + .col-lg-offset-7 { + margin-left: 58.33333333%; + } + .col-lg-offset-6 { + margin-left: 50%; + } + .col-lg-offset-5 { + margin-left: 41.66666667%; + } + .col-lg-offset-4 { + margin-left: 33.33333333%; + } + .col-lg-offset-3 { + margin-left: 25%; + } + .col-lg-offset-2 { + margin-left: 16.66666667%; + } + .col-lg-offset-1 { + margin-left: 8.33333333%; + } + .col-lg-offset-0 { + margin-left: 0; + } +} +table { + background-color: transparent; +} +caption { + padding-top: 8px; + padding-bottom: 8px; + color: #777; + text-align: left; +} +th { + text-align: left; +} +fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; +} +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 20px; + font-size: 21px; + line-height: inherit; + color: #333; + border: 0; + border-bottom: 1px solid #e5e5e5; +} +label { + display: inline-block; + max-width: 100%; + margin-bottom: 5px; + font-weight: bold; +} +.form-control::-moz-placeholder { + color: #999; + opacity: 1; +} +.form-control:-ms-input-placeholder { + color: #999; +} +.form-control::-webkit-input-placeholder { + color: #999; +} +.btn { + display: inline-block; + padding: 6px 12px; + margin-bottom: 0; + font-size: 14px; + font-weight: normal; + line-height: 1.42857143; + text-align: center; + white-space: nowrap; + vertical-align: middle; + -ms-touch-action: manipulation; + touch-action: manipulation; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; +} +.btn:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +.btn:hover, +.btn:focus { + color: #333; + text-decoration: none; +} +.btn:active { + outline: 0; +} +.btn-default { + color: #333; + background-color: #fff; + border-color: #ccc; +} +.btn-default:focus { + color: #333; + background-color: #e6e6e6; + border-color: #8c8c8c; +} +.btn-default:hover { + color: #333; + background-color: #e6e6e6; + border-color: #adadad; +} +.btn-default:active { + color: #333; + background-color: #e6e6e6; + border-color: #adadad; +} +.btn-default:active { + background-image: none; +} +.btn-primary { + color: #fff; + background-color: #337ab7; + border-color: #2e6da4; +} +.btn-primary:focus { + color: #fff; + border-color: #122b40; +} +.btn-primary:hover { + color: #fff; + background-color: #286090; + border-color: #204d74; +} +.btn-primary:active { + color: #fff; + border-color: #204d74; +} +.btn-primary:active { + background-image: none; +} +.btn-success { + color: #fff; + background-color: #5cb85c; + border-color: #4cae4c; +} +.btn-success:focus { + color: #fff; + border-color: #255625; +} +.btn-success:hover { + color: #fff; + background-color: #449d44; + border-color: #398439; +} +.btn-success:active { + color: #fff; + border-color: #398439; +} +.btn-success:active { + background-image: none; +} +.btn-info { + color: #fff; + background-color: #5bc0de; + border-color: #46b8da; +} +.btn-info:focus { + color: #fff; + border-color: #1b6d85; +} +.btn-info:hover { + color: #fff; + background-color: #31b0d5; + border-color: #269abc; +} +.btn-info:active { + color: #fff; + border-color: #269abc; +} +.btn-lg, +.btn-group-lg > .btn { + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; + border-radius: 6px; +} +.btn-sm, +.btn-group-sm > .btn { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +.btn-xs, +.btn-group-xs > .btn { + padding: 1px 5px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +.nav { + padding-left: 0; + margin-bottom: 0; + list-style: none; +} +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +@-o-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +@keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +.panel { + margin-bottom: 20px; + background-color: #fff; + border: 1px solid transparent; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .05); + box-shadow: 0 1px 1px rgba(0, 0, 0, .05); +} +.panel-body { + padding: 15px; +} +.carousel { + position: relative; +} +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} +.carousel-inner > .item { + position: relative; + display: none; + -webkit-transition: .6s ease-in-out left; + -o-transition: .6s ease-in-out left; + transition: .6s ease-in-out left; +} +.carousel-inner > .active { + display: block; +} +.carousel-inner > .active { + left: 0; +} +.carousel-indicators { + position: absolute; + bottom: 10px; + left: 50%; + z-index: 15; + width: 60%; + padding-left: 0; + margin-left: -30%; + text-align: center; + list-style: none; +} +.carousel-indicators li { + display: inline-block; + width: 10px; + height: 10px; + margin: 1px; + text-indent: -999px; + cursor: pointer; + background-color: #000 \9; + background-color: rgba(0, 0, 0, 0); + border: 1px solid #fff; + border-radius: 10px; +} +.carousel-indicators .active { + width: 12px; + height: 12px; + margin: 0; + background-color: #fff; +} +@media screen and (min-width: 768px) { + .carousel-indicators { + bottom: 20px; + } +} +.container:before, +.container:after, +.row:before, +.row:after, +.nav:before, +.nav:after, +.panel-body:before, +.panel-body:after { + display: table; + content: " "; +} +.container:after, +.row:after, +.nav:after, +.panel-body:after { + clear: both; +} +.pull-right { + float: right !important; +} +@-ms-viewport { + width: device-width; +} +@media (max-width: 767px) { + .hidden-xs { + display: none !important; + } +} diff --git a/docs/static/css/content-style.css b/docs/static/css/content-style.css new file mode 100644 index 000000000..1356b0495 --- /dev/null +++ b/docs/static/css/content-style.css @@ -0,0 +1,84 @@ +/* Styles used by tables at the URLs: + + 1. /overview/configuration/#configure-blackfriday-rendering + 2. /templates/functions/#math + +Their HTML is in the files: + + 1. ./docs/content/overview/configuration.md + 2. ./docs/content/templates/functions.md +*/ + +table.table { + margin: 1em 0; +} +table.table-bordered tr th, +table.table-bordered tr td { + border-width: 2px; + border-color: #dddddd; + border-style: solid; + padding: 0 0.5em; +} +table.table-bordered-configuration { + max-width: 100%; +} +table.table-bordered-configuration, +table.table-bordered-configuration tr, +table.table-bordered-configuration tr th, +table.table-bordered-configuration tr td:not(.purpose-description) { + border-width: 2px; +} +table.table-bordered-configuration tr td.purpose-description { + border-width: 1px; +} +table.table-bordered-configuration, +table.table-bordered-configuration tr, +table.table-bordered-configuration tr th, +table.table-bordered-configuration tr td { + border-color: #dddddd; +} +table.table-bordered-configuration { + border-right-style: solid; + border-bottom-style: solid; +} +table.table-bordered-configuration tr, +table.table-bordered-configuration tr th, +table.table-bordered-configuration tr td:not(.purpose-description) { + border-left-style: solid; +} +table.table-bordered-configuration tr th, +table.table-bordered-configuration tr td:not(.purpose-description) { + border-top-style: solid; +} +table.table-bordered-configuration tr td.purpose-description { + border-top-style: dotted; +} +table.table-bordered-configuration tr th, +table.table-bordered-configuration tr td { + padding-top: 0; + padding-right: 0.5em; + padding-left: 0.5em; +} +table.table-bordered-configuration tr th, +table.table-bordered-configuration tr td:not(.purpose-description) { + padding-bottom: 0; +} +table.table-bordered-configuration tr td.purpose-description { + padding-bottom: 0.5em; +} +table.table-bordered-configuration tr th, +table.table-bordered-configuration tr td:not(.purpose-description) { + text-align: center; +} +table.table-bordered-configuration tr td:not(.purpose-description) code { + padding: 0; + border-radius: 0; + font-size: 14px; + background-color: inherit; + color: darkgreen; +} +table.table-bordered-configuration tr td span.purpose-title { + padding-right: 0.15em; + font-style: italic; + color: chocolate; +} diff --git a/docs/static/css/home-page-style-responsive.css b/docs/static/css/home-page-style-responsive.css new file mode 100644 index 000000000..46ce3eb43 --- /dev/null +++ b/docs/static/css/home-page-style-responsive.css @@ -0,0 +1,44 @@ +/* full page image header area */ + +@media (min-width: 1024.1px) { + .header { + background-image: url('../img/desk.jpg'); + } +} +@media (max-width: 319.9px) { + .header { + background-image: url('../img/desk-sm.jpg'); + } +} +@media (max-width: 319.9px), (min-width: 1024.1px) { + .header { + background-position: center center; + -webkit-background-size: cover; + -moz-background-size: cover; + -o-background-size: cover; + background-size: cover; + background-attachment: fixed; + } +} +@media (min-width: 320px) and (max-width: 1024px) { + .header { + background-position: 0% 0%; + -webkit-background-size: 100% 100%; + -moz-background-size: 100% 100%; + -o-background-size: 100% 100%; + background-size: 100% 100%; + background-attachment: scroll; + background-clip: border-box; + background-origin: padding-box; + } +} +@media (min-width: 320px) and (max-width: 1024px) and (orientation: portrait) { + .header { + background-image: url('../img/desk-mini.jpg'); + } +} +@media (min-width: 320px) and (max-width: 1024px) and (orientation: landscape) { + .header { + background-image: url('../img/desk-wide.jpg'); + } +} diff --git a/docs/static/css/home-page-style.css b/docs/static/css/home-page-style.css new file mode 100644 index 000000000..4ff3bc815 --- /dev/null +++ b/docs/static/css/home-page-style.css @@ -0,0 +1,194 @@ +/* global styles */ + +html { + width: 100%; + height: 100%; + letter-spacing: 0.5px; +} +body { + width: 100%; + height: 100%; + letter-spacing: 0.5px; + line-height: 1.6; + font-family: 'Arbutus Slab', "Helvetica Neue", "Helvetica", sans-serif !important; +} +h1, h2, h3, h4, h5, h6 { + font-family: 'Cabin', "Helvetica Neue", "Helvetica", sans-serif; +} +div.vert-text { + display: table-cell; + vertical-align: middle; + text-align: center; +} + +/* full page image header area */ + +div.buttonbox { + margin: 2em 0 4em; +} +img.logo { + padding: 2em; + width: 100%; + max-width: 35em; +} +div#main { + position: relative; + z-index: 99999; + background: rgb(255, 255, 255); + box-shadow: 0 0 15px rgba(0, 0, 0, 0.5); +} +.header { + display: table; + position: relative; + width: 100%; + height: 70%; + min-height: 70%; + z-index: 99999; + background-color: black; + background-repeat: no-repeat; +} +.header a.btn { + margin: 10px; + font-weight: 100; +} + +/* intro */ + +a:hover { + color: rgb(52, 73, 94); +} +i.callout-icon, i.lead-icon, i.point-icon { + display: inline-block; + vertical-align: middle; + text-align: center; + width: 140px; + height: 140px; + font-size: 56px; + line-height: 136px; + border-radius: 50%; + -webkit-transition: box-shadow 0.2s; + -moz-transition: box-shadow 0.2s; + transition: box-shadow 0.2s; +} +div.counterpoint { + background-color: rgb(255, 252, 244); +} +div.counterpoint, div.point { + padding: 50px 0; +} +div.counterpoint a, div.point a { + color: rgb(7,162,166); +} +div.counterpoint h2, div.point h2 { + line-height: 1.7; + font-size: 32pt; +} +a.icon-2x { + font-size: 200%; +} +i.lead-icon { + border: 3px solid #222222; +} +i.lead-icon:hover { + border: 3px solid black; + background: black; + color: #ffffff; +} +div.point { + background: rgb(96,210,211); + color: #ffffff; +} +div.point h2 > i { + color:#FF4088; +} +i.point-icon { + border: 3px solid #ffffff; +} +i.point-icon:hover { + background: #ffffff; + color: rgb(22, 203, 230); +} + +/* callout */ + +div.callout { + display: table; + table-layout: fixed; + width: 100%; + height: 420px; + padding: 50px 0; + background-color: rgb(118,156,172); + color: #ffffff; +} +i.callout-icon { + border: 3px solid #ffffff; +} +i.callout-icon:hover { + background: #ffffff; + color: rgb(249, 176, 190); +} + +/* call to action */ + +div#action.call-to-action { + padding: 30px 0px 40px; + padding: 30px 0px 50px; + width: 100%; + background-color: rgba(255, 255, 255, 0.19); + background: url('../img/gray.png'); + color: #ffffff; +} +div#action.call-to-action code { + font-size: 14pt; +} +div#action.call-to-action h1 { + padding-bottom: .5em; +} +div#action.call-to-action pre { + background-color: #545454; + color: #f9f2f4; + margin-bottom: 0; +} +div#action.call-to-action pre:hover { + background-color: #f9f2f4; + color: #545454; +} +div#action.call-to-action a.btn { + margin: 10px; +} +div#action.call-to-action a.quickstart { + font-weight: 300; + color: white; +} + +/* footer */ + +footer { + padding: 50px 0px 25px 0px; + font-size: 14px; + background: rgb(255, 255, 255); +} +footer a { + color: black; +} +footer a:focus, +footer a:hover { + text-decoration: none; + outline: none; +} +footer a:active { + color: green; +} + +/* Bootstrap addons */ + +div.owl-carousel a { + white-space: nowrap; + color: #243382; +} +div.owl-carousel blockquote { + border-left: 0px; +} +div.owl-carousel blockquote p { + font-size: 20pt; +} diff --git a/docs/static/css/hugofont.css b/docs/static/css/hugofont.css new file mode 100644 index 000000000..09d6ce070 --- /dev/null +++ b/docs/static/css/hugofont.css @@ -0,0 +1,184 @@ +@font-face { + font-family: 'hugo'; + src:url('../fonts/hugo.eot'); + src:url('../fonts/hugo.eot?#iefix') format('embedded-opentype'), + url('../fonts/hugo.woff') format('woff'), + url('../fonts/hugo.ttf') format('truetype'), + url('../fonts/hugo.svg#hugo') format('svg'); + font-weight: normal; + font-style: normal; +} + +[class^="icon-"], [class*=" icon-"] { + font-family: 'hugo'; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-home:before { + content: "\21"; +} +.icon-html5:before { + content: "\23"; +} +.icon-css3:before { + content: "\24"; +} +.icon-console:before { + content: "\25"; +} +.icon-link:before { + content: "\26"; +} +.icon-fire:before { + content: "\28"; +} +.icon-check-alt:before { + content: "\29"; +} +.icon-hugo_serif:before { + content: "\e600"; +} +.icon-x-altx-alt:before { + content: "\2a"; +} +.icon-circlestar:before { + content: "\2b"; +} +.icon-file-css:before { + content: "\2c"; +} +.icon-radio-checked:before { + content: "\2e"; +} +.icon-quote:before { + content: "\44"; +} +.icon-airplane2:before { + content: "\45"; +} +.icon-heart:before { + content: "\46"; +} +.icon-rocket:before { + content: "\47"; +} +.icon-house:before { + content: "\48"; +} +.icon-arrow-right:before { + content: "\e001"; +} +.icon-arrow-left:before { + content: "\e002"; +} +.icon-flow-branch:before { + content: "\e004"; +} +.icon-pen:before { + content: "\e005"; +} +.icon-idea:before { + content: "\3b"; +} +.icon-gears:before { + content: "\3c"; +} +.icon-talking:before { + content: "\3d"; +} +.icon-tag:before { + content: "\3e"; +} +.icon-rocket2:before { + content: "\3f"; +} +.icon-octocat:before { + content: "\41"; +} +.icon-announce:before { + content: "\42"; +} +.icon-edit:before { + content: "\43"; +} +.icon-power-cord:before { + content: "\50"; +} +.icon-apple:before { + content: "\51"; +} +.icon-windows8:before { + content: "\52"; +} +.icon-tux:before { + content: "\53"; +} +.icon-file-xml:before { + content: "\54"; +} +.icon-fork:before { + content: "\55"; +} +.icon-arrow-down:before { + content: "\56"; +} +.icon-pacman:before { + content: "\e000"; +} +.icon-embed:before { + content: "\2f"; +} +.icon-code:before { + content: "\30"; +} +.icon-cc:before { + content: "\31"; +} +.icon-cc-by:before { + content: "\32"; +} +.icon-cc-nc:before { + content: "\33"; +} +.icon-beaker-alt:before { + content: "\39"; +} +.icon-w3c:before { + content: "\3a"; +} +.icon-bolt:before { + content: "\49"; +} +.icon-flow-tree:before { + content: "\4a"; +} +.icon-twitter:before { + content: "\4b"; +} +.icon-beaker:before { + content: "\4c"; +} +.icon-images:before { + content: "\4d"; +} +.icon-bubbles:before { + content: "\4e"; +} +.icon-meter2:before { + content: "\4f"; +} +.icon-hugo_sans:before { + content: "\68"; +} +.icon-spf13:before { + content: "\27"; +} diff --git a/docs/static/css/style-responsive.css b/docs/static/css/style-responsive.css new file mode 100644 index 000000000..58f614bb4 --- /dev/null +++ b/docs/static/css/style-responsive.css @@ -0,0 +1,72 @@ +@media (max-width: 768px) { + + .header { + position: absolute; + } + + /*sidebar*/ + + #sidebar { + height: auto; + overflow: hidden; + position: absolute; + width: 100%; + z-index: 1001; + } + + + /* body container */ + #main-content { + margin: 0px!important; + position: none !important; + } + + .wrapper { + padding: inherit; + } + + #sidebar > ul > li { + margin: 0 10px 5px 10px; + } + #sidebar > ul > li > a { + height:35px; + line-height:35px; + padding: 0 10px; + text-align: left; + } + + #sidebar > ul > li > a, #sidebar > ul > li > ul.sub > li { + width: 100%; + } + #sidebar > ul > li > ul.sub > li > a { + background: transparent !important ; + } + + /* sidebar */ + #sidebar { + margin: 0px !important; + } + + .btn { + margin-bottom: 5px; + } + + .navigation.next { + display: none; + } + +} + +@media (max-width: 480px) { + + .notification-row { + display: none; + } +} + +@media (max-width:360px) { + + h1 { + font-size: 1.9em; + } +} diff --git a/docs/static/css/style.css b/docs/static/css/style.css new file mode 100644 index 000000000..312c247c9 --- /dev/null +++ b/docs/static/css/style.css @@ -0,0 +1,684 @@ +/* Import fonts */ +@import url(//fonts.googleapis.com/css?family=Lato:300,400,700,900,300italic,400italic,700italic,900italic); + +/* ****************************** + For the github btn +****************************** */ + +.github-btn { + font-size: 11px; +} +.github-btn, +.github-btn .btn { + font-weight: bold; +} +.github-btn .btn-default { + text-shadow: 0 1px 0 #fff; + background-image: -webkit-gradient(linear, left 0%, left 100%, from(#ffffff), to(#e0e0e0)); + background-image: -webkit-linear-gradient(top, #ffffff, 0%, #e0e0e0, 100%); + background-image: -moz-linear-gradient(top, #ffffff 0%, #e0e0e0 100%); + background-image: linear-gradient(to bottom, #ffffff 0%, #e0e0e0 100%); + background-repeat: repeat-x; + border-color: #dbdbdb; + border-color: #ccc; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0); +} + +.github-btn .btn-default:hover, .github-btn .btn-default:focus { + background-color: #e0e0e0; + background-position: 0 -15px; + color: #333; + border-color: #adadad; +} + +.nav-github { + width: 325px; +} + .nav-github > span { + padding-right: 0.5em; + } + + .icon-github { + display: inline-block; + font-family: FontAwesome; + font-style: normal; + font-weight: normal; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + + .github-watchers .icon-github:before{ + content: "\f005"; + } + + .github-forks .icon-github:before{ + content: "\f126"; + } + +.gh-count{ + padding: 2px 5px 3px 4px; + color: #555; + text-decoration: none; + text-shadow:0 1px 0 #fff; + white-space:nowrap; + cursor:pointer; + border-radius:3px; + position:relative; + display:none; + margin-left:4px; + background-color:#fafafa; + border:1px solid #d4d4d4; +} + +.gh-count:hover,.gh-count:focus{color:#4183c4;text-decoration: none;} +.gh-count:before,.gh-count:after{content:' ';position:absolute;display:inline-block;width:0;height:0;border-color:transparent;border-style:solid} +.gh-count:before{top:50%;left:-3px;margin-top:-4px;border-width:4px 4px 4px 0;border-right-color:#fafafa} +.gh-count:after{top:50%;left:-4px;z-index:-1;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#d4d4d4} + +thead { + font-weight: bold; +} + +table { + width: 100%; +} + + +h1, h2, h3 { + margin-top: .8em; + margin-bottom: .7em; +} + +pre code { + font-size: 15px !important; + font-family: Menlo, Consolas, 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Lucida Console', Monaco, 'Droid Sans Mono', monospace; +} + +body { + color: #353b44; + background: #edece4; + font-family: 'Lato', sans-serif; + padding: 0px !important; + margin: 0px !important; + font-size: 16px !important; + font-weight: 400; +} + +h2,h3,h4,h5{ + font-weight: 700; +} + + +h1[id]:before, h2[id]:before, h3[id]:before, h4[id]:before, h5[id]:before { + display: block; + content: " "; + margin-top: -75px; + height: 75px; + visibility: hidden; +} + +label{ + font-weight: 400; +} + +.sidebar-menu .fa { + width: 30px; + text-align: center; +} + +a, a:hover, a:focus { + text-decoration: none; + outline: none; + outline: 0; +} + +img { + max-width: 100%; + height: auto; +} + +.panel-body a { + line-height: 1.1; + display: inline-block; +} +.panel-body a:after { + display: block; + content: ""; + height: 1px; + width: 0%; + background-color: #ff4088; + -webkit-transition: width 0.5s ease; + -moz-transition: width 0.5s ease; + -ms-transition: width 0.5s ease; + transition: width 0.5s ease; +} + +.panel-body a:hover:after, .panel-body a:focus:after { + width: 100%; +} + +input:focus, textarea:focus { outline: none; } +*:focus {outline: none;} +::selection { + background: #ff4088; + color: #fff; +} +::-moz-selection { + background: #ff4088; + color: #fff; +} + +#container { + width: 100%; + height: 100%; +} + +/*sidebar navigation*/ + +#sidebar { + width: 214px; + height: 100%; + position: fixed; + background: #ffffff; + overflow-y: auto; +} + + +ul.sidebar-menu , ul.sidebar-menu li ul.sub{ + margin: -2px 0 0; + padding: 0; +} + +ul.sidebar-menu { + margin-top: 60px; +} + +#sidebar > ul > li > ul.sub { + display: none; +} + +#sidebar > ul > li.active > ul.sub, #sidebar > ul > li > ul.sub > li > a { + display: block; +} + +ul.sidebar-menu li ul.sub li{ + background: #eeeeee; + margin-bottom: 0; + margin-left: 0; + margin-right: 0; +} + +ul.sidebar-menu li ul.sub li:last-child{ + border-radius: 0 0 4px 4px; + -webkit-border-radius: 0 0 4px 4px; +} + +ul.sidebar-menu li ul.sub li a { + font-size: 12px; + padding: 0 0 0 32px; + line-height: 35px; + height: 35px; + -webkit-transition: all 0.3s ease; + -moz-transition: all 0.3s ease; + -o-transition: all 0.3s ease; + -ms-transition: all 0.3s ease; + transition: all 0.3s ease; + color: #656C73; + font-size: 14px; +} + +ul.sidebar-menu li ul.sub li a:hover, ul.sidebar-menu li ul.sub li.active a { + color: #ff4088; + -webkit-transition: all 0.3s ease; + -moz-transition: all 0.3s ease; + -o-transition: all 0.3s ease; + -ms-transition: all 0.3s ease; + transition: all 0.3s ease; + display: block; +} + +ul.sidebar-menu li{ + line-height: 20px !important; +} + +ul.sidebar-menu li.sub-menu{ + line-height: 15px; + font-size: 16px; +} + +ul.sidebar-menu li a span{ + display: inline-block; +} + +ul.sidebar-menu li a{ + color: #72767D; + text-decoration: none; + display: block; + padding: 10px 0 10px 10px; + font-size: 16px; + font-weight: 400; + outline: none; + -webkit-transition: all 0.3s ease; + -moz-transition: all 0.3s ease; + -o-transition: all 0.3s ease; + -ms-transition: all 0.3s ease; + transition: all 0.3s ease; + border-right: 1px solid #D7D7D7; + border-bottom: 1px solid #D7D7D7; + white-space: nowrap; +} + +ul.sidebar-menu li.active a, ul.sidebar-menu li a:hover, ul.sidebar-menu li a:focus { + background: #eeeeee; + color: #ff4088; + display: block; + /*border-radius: 4px; + -webkit-border-radius: 4px;*/ + -webkit-transition: all 0.3s ease; + -moz-transition: all 0.3s ease; + -o-transition: all 0.3s ease; + -ms-transition: all 0.3s ease; + transition: all 0.3s ease; +} +ul.sidebar-menu li a:hover, ul.sidebar-menu li a:focus { + border-bottom: 1px solid #ff4088; +} +/*ul.sidebar-menu li.active a,*/ ul.sidebar-menu .sub-menu li.active a{ + border-bottom: 1px solid #ff4088; +} + +ul.sidebar-menu li a i { + font-size: 18px; + padding-right: 6px; + /*color: #ff4088;*/ +} + +ul.sidebar-menu li a:hover i, ul.sidebar-menu li a:focus i { + color: #ff4088; +} + +ul.sidebar-menu li.active a i { + color: #ff4088; +} + + +#sidebar ul > li > a .menu-arrow { + float: right; + margin-right: 8px; + margin-top: 6px; +} + +@-moz-document url-prefix() { + #sidebar ul > li > a .menu-arrow { + float: right; + margin-right: 8px; + margin-top: -16px; + } +} + +#main-content { + margin-left: 200px; + line-height: 1.8; + font-size: 18px; +} + +.header { + min-height: 60px; + padding: 0 10px; +} +.header { + position: fixed; + left: 0; + right: 0; + z-index: 1002; + text-align:center; +} + + +.black-bg { + background: rgba(20,20,20,0.9); + border-bottom: 1px solid #f1f2f7; +} + +.wrapper { + display: inline-block; + margin-top: 60px; + padding: 0px 15px 15px 0px; + width: 100%; +} + +a.logo { + font-size: 22px; + font-weight: 400; + color: #8E8E93; + float: left; + margin-top: 10px; + text-transform: uppercase; +} + +a.logo:hover, a.logo:focus { + text-decoration: none; + outline: none; +} + +h1.top-menu { + margin-top: -5px; +} +.title-row { + margin-top: 15px; + margin-left: 16px; + color: #EEE; +} +.notification-row { + float: right; + margin-top: 15px; + margin-left: 65px; +} + + +.top-nav { + margin-top: 15px; +} + +/*--sidebar toggle---*/ + +.toggle-nav { + float: left; + padding-right: 5px; + margin-top: 20px; + cursor: pointer; + color: gray; +} + +.toggle-nav .icon-reorder { + cursor: pointer; + display: inline-block; + font-size: 20px; +} + + +@-webkit-keyframes square { + 0% { background-position: 0 0; } + 25% { background-position: 100% 0; } + 50% { background-position: 100% 100%; } + 75% { background-position: 0 100%; } + 100% { background-position: 0 0; } +} + +@-ms-keyframes square { + 0% { background-position: 0 0; } + 25% { background-position: 100% 0; } + 50% { background-position: 100% 100%; } + 75% { background-position: 0 100%; } + 100% { background-position: 0 0; } +} + +@keyframes square { + 0% { background-position: 0 0; } + 25% { background-position: 100% 0; } + 50% { background-position: 100% 100%; } + 75% { background-position: 0 100%; } + 100% { background-position: 0 0; } +} + +.navigation { + position: absolute; + top: 0; + bottom: 0; + margin: 0; + max-width: 150px; + min-width: 90px; + width:100%; + min-height:1200px; + cursor:pointer; + display: flex; + justify-content: center; + align-content: center; + flex-direction: column; + font-size: 6em; + color: rgba(0,0,0,0.5); + text-align: center; + -webkit-transition: all 350ms ease; + transition: all 350ms ease; +} + +.navigation.next { + right:0; +} + + +.navigation:hover { + background-color: rgba(0,0,0,0.1); +} + +/* Google Custom Search box */ + +input.gsc-input, +.gsc-input-box, +.gsc-input-box-hover, +.gsc-input-box-focus, +.gsc-search-button, +.gsc-inline-block { + box-sizing: content-box; + line-height: normal; +} + +.gsc-control-cse { + padding: 0.1em 0 0.5em 1em !important; + width: 16em !important; + float: right; +} + +input.gsc-search-button-v2 { + padding: 6px 12px !important; +} + +.gsc-search-box-tools .gsc-search-box .gsc-input { + padding-right: 1px !important; +} + +/* Styled keypress from Wikipedia */ + +kbd { + border: 1px solid #aaa; + -moz-border-radius: 0.2em; + -webkit-border-radius: 0.2em; + border-radius: 0.2em; + -moz-box-shadow: 0.1em 0.2em 0.2em #ddd; + -webkit-box-shadow: 0.1em 0.2em 0.2em #ddd; + box-shadow: 0.1em 0.2em 0.2em #ddd; + background-color: #f9f9f9; + background-image: -moz-linear-gradient(top, #eee, #f9f9f9, #eee); + background-image: -o-linear-gradient(top, #eee, #f9f9f9, #eee); + background-image: -webkit-linear-gradient(top, #eee, #f9f9f9, #eee); + background-image: linear-gradient(to bottom, #eee, #f9f9f9, #eee); + padding: 0.1em 0.3em; + font-family: inherit; + font-size: 0.85em; +} + +/* For definitions of variables */ + +dl { + margin: 1em; + border-bottom: 1px solid #ccc; +} + +dt { + float: left; + clear: left; + width: 9.5em; + margin: 0.125em; + padding: 2px 4px; +} + +dd { + padding: 0.2em 0 0.2em 10em; + border-top: 1px solid #ccc; +} + +/* Prevent linebreak right after an icon */ +#main-content .fa { + display: inline; +} + +/* Logo for FreeBSD until Font Awesome adds it, see https://github.com/FortAwesome/Font-Awesome/issues/1116 */ +i.freebsd-19px:before { + content: url(/img/freebsd-19px.svg); + vertical-align: -7%; +} + +/* Responsive videos */ +.video-container { + position: relative; + padding-bottom: 56.25%; /* 16:9 */ + padding-top: 30px; + height: 0; + overflow: hidden; + margin: 20px 0; +} + +.video-container iframe, +.video-container object, +.video-container embed { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +/* Google custom search */ +.cse { + margin-top: 20px; + padding-right: 20px; +} + + +/* Table of contents */ + +.toc ul { list-style: none; margin: 0; padding: 0 5px; } +.toc ul li { display: inline; } +#TableOfContents > ul > li > ul > li > ul li { margin-right: 8px; } +#TableOfContents > ul > li > ul > li > a, #TableOfContents > ul > li > a { font-weight: bold; background-color: #eeeeee; padding: 0 10px; margin: 0 2px; } +#TableOfContents > ul > li > ul > li > a { font-style: italic; } +.toc.compact ul > li > ul > li > ul { display: none; } + +#toc { + position:fixed; + background-color: rgba(0, 0, 0, 0.1); + padding: 10px 50px 10px 20px; +} + +.showcase-container { + display: inline-block; + position: relative; + width: 100%; +} + +.showcase-container img { + border: 1px solid #555; +} + +.showcase-container h4 { + margin-top: 0; + margin-bottom: 0; +} +.dummy { + padding-top: 90%; /* Making rows line up even if img proportions off */ +} + +.thumbnail { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; +} + +@media(max-width:1200px) { + .toc { + display: none; + } +} + + +/* Footer panel */ +.footer-panel { + width: 100%; + border-top:1px #efefef solid; + line-height: 30px; + padding: 25px 0px 15px; + margin-top: 15px; + background: #f9f9f9; + display: inline-block; + float: left; +} + +.footer-panel p { + padding-left: 20px; + padding-right: 20px; + font-size: medium; + font-style: italic; +} + + +/* Search form */ +#search-input { + width: 100%; + border: 1px solid #B3B3B3; + border-radius: 3px; + padding: 5px; +} + +#search-input:focus { + border-color: #F04A9C; +} + +/* Search result wrapper */ +.algolia-autocomplete { + width: 100%; +} + +/* List of search results */ +.aa-dropdown-menu { + box-sizing: border-box; + width: 100%; + background-color: #FFFFFF; + border: 1px solid #B3B3B3; + padding: 0; + font-size: 16px; + margin: 4 0 4 0; +} + +/* Highlight terms in search result headers */ +.algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--highlight { + background-color: #F04A9C; +} + +/* Highlight terms in search result body */ +.algolia-docsearch-suggestion--highlight { + color: #F04A9C; + font-weight: 900; +} + +/* Currently selected search result */ +.aa-cursor .algolia-docsearch-suggestion--content { + color: inherit; +} + +.aa-cursor .algolia-docsearch-suggestion { + background: #EFEFEF; + color: #353B44; +} + +.algolia-docsearch-suggestion { + font-size: 16px; + color: #9AA2AB; +} + +.algolia-docsearch-suggestion--category-header, +.algolia-docsearch-suggestion--subcategory-column { + display: none !important; +} diff --git a/docs/static/favicon-16x16.png b/docs/static/favicon-16x16.png deleted file mode 100644 index c62ce6fb2..000000000 Binary files a/docs/static/favicon-16x16.png and /dev/null differ diff --git a/docs/static/favicon-32x32.png b/docs/static/favicon-32x32.png deleted file mode 100644 index 57a018e35..000000000 Binary files a/docs/static/favicon-32x32.png and /dev/null differ diff --git a/docs/static/favicon.ico b/docs/static/favicon.ico index dc007a99e..36693330b 100644 Binary files a/docs/static/favicon.ico and b/docs/static/favicon.ico differ diff --git a/docs/static/fonts/Mulish-Italic-VariableFont_wght.ttf b/docs/static/fonts/Mulish-Italic-VariableFont_wght.ttf deleted file mode 100644 index e5425c75e..000000000 Binary files a/docs/static/fonts/Mulish-Italic-VariableFont_wght.ttf and /dev/null differ diff --git a/docs/static/fonts/Mulish-VariableFont_wght.ttf b/docs/static/fonts/Mulish-VariableFont_wght.ttf deleted file mode 100644 index 410f7aa63..000000000 Binary files a/docs/static/fonts/Mulish-VariableFont_wght.ttf and /dev/null differ diff --git a/docs/static/fonts/glyphicons-halflings-regular.eot b/docs/static/fonts/glyphicons-halflings-regular.eot new file mode 100644 index 000000000..b93a4953f Binary files /dev/null and b/docs/static/fonts/glyphicons-halflings-regular.eot differ diff --git a/docs/static/fonts/glyphicons-halflings-regular.svg b/docs/static/fonts/glyphicons-halflings-regular.svg new file mode 100644 index 000000000..94fb5490a --- /dev/null +++ b/docs/static/fonts/glyphicons-halflings-regular.svg @@ -0,0 +1,288 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/static/fonts/glyphicons-halflings-regular.ttf b/docs/static/fonts/glyphicons-halflings-regular.ttf new file mode 100644 index 000000000..1413fc609 Binary files /dev/null and b/docs/static/fonts/glyphicons-halflings-regular.ttf differ diff --git a/docs/static/fonts/glyphicons-halflings-regular.woff b/docs/static/fonts/glyphicons-halflings-regular.woff new file mode 100644 index 000000000..9e612858f Binary files /dev/null and b/docs/static/fonts/glyphicons-halflings-regular.woff differ diff --git a/docs/static/fonts/glyphicons-halflings-regular.woff2 b/docs/static/fonts/glyphicons-halflings-regular.woff2 new file mode 100644 index 000000000..64539b54c Binary files /dev/null and b/docs/static/fonts/glyphicons-halflings-regular.woff2 differ diff --git a/docs/static/fonts/hugo.eot b/docs/static/fonts/hugo.eot new file mode 100644 index 000000000..b92f00f93 Binary files /dev/null and b/docs/static/fonts/hugo.eot differ diff --git a/docs/static/fonts/hugo.svg b/docs/static/fonts/hugo.svg new file mode 100644 index 000000000..7913f7c1f --- /dev/null +++ b/docs/static/fonts/hugo.svg @@ -0,0 +1,63 @@ + + + +Generated by IcoMoon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/static/fonts/hugo.ttf b/docs/static/fonts/hugo.ttf new file mode 100644 index 000000000..962914d33 Binary files /dev/null and b/docs/static/fonts/hugo.ttf differ diff --git a/docs/static/fonts/hugo.woff b/docs/static/fonts/hugo.woff new file mode 100644 index 000000000..4693fbe7f Binary files /dev/null and b/docs/static/fonts/hugo.woff differ diff --git a/docs/static/images/gopher-hero.svg b/docs/static/images/gopher-hero.svg deleted file mode 100644 index 36d9f1c41..000000000 --- a/docs/static/images/gopher-hero.svg +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/static/images/gopher-side_color.svg b/docs/static/images/gopher-side_color.svg deleted file mode 100644 index 85f949783..000000000 --- a/docs/static/images/gopher-side_color.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/static/images/hugo-logo-wide.svg b/docs/static/images/hugo-logo-wide.svg deleted file mode 100644 index 1f6a79ea6..000000000 --- a/docs/static/images/hugo-logo-wide.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/docs/static/img/2626info-tn.png b/docs/static/img/2626info-tn.png new file mode 100644 index 000000000..fb6b11757 Binary files /dev/null and b/docs/static/img/2626info-tn.png differ diff --git a/docs/static/img/antzucaro-tn.jpg b/docs/static/img/antzucaro-tn.jpg new file mode 100644 index 000000000..188769c0f Binary files /dev/null and b/docs/static/img/antzucaro-tn.jpg differ diff --git a/docs/static/img/apperneticioblog.png b/docs/static/img/apperneticioblog.png new file mode 100644 index 000000000..f2fcf6d96 Binary files /dev/null and b/docs/static/img/apperneticioblog.png differ diff --git a/docs/static/img/arresteddevops-tn.png b/docs/static/img/arresteddevops-tn.png new file mode 100644 index 000000000..868df1d77 Binary files /dev/null and b/docs/static/img/arresteddevops-tn.png differ diff --git a/docs/static/img/asc-tn.jpg b/docs/static/img/asc-tn.jpg new file mode 100644 index 000000000..a5148e236 Binary files /dev/null and b/docs/static/img/asc-tn.jpg differ diff --git a/docs/static/img/astrochili-tn.png b/docs/static/img/astrochili-tn.png new file mode 100644 index 000000000..ec11741ee Binary files /dev/null and b/docs/static/img/astrochili-tn.png differ diff --git a/docs/static/img/aydoscom.png b/docs/static/img/aydoscom.png new file mode 100644 index 000000000..f2cfc3982 Binary files /dev/null and b/docs/static/img/aydoscom.png differ diff --git a/docs/static/img/balaramadurai-net-tn.jpg b/docs/static/img/balaramadurai-net-tn.jpg new file mode 100644 index 000000000..207a4a840 Binary files /dev/null and b/docs/static/img/balaramadurai-net-tn.jpg differ diff --git a/docs/static/img/barricade-tn.png b/docs/static/img/barricade-tn.png new file mode 100644 index 000000000..96eed0fbe Binary files /dev/null and b/docs/static/img/barricade-tn.png differ diff --git a/docs/static/img/bepsays-tn.png b/docs/static/img/bepsays-tn.png new file mode 100644 index 000000000..ca9119cc5 Binary files /dev/null and b/docs/static/img/bepsays-tn.png differ diff --git a/docs/static/img/bharathpalavalli-tn.png b/docs/static/img/bharathpalavalli-tn.png new file mode 100644 index 000000000..bcf15ed0a Binary files /dev/null and b/docs/static/img/bharathpalavalli-tn.png differ diff --git a/docs/static/img/bugtrackersio-tn.jpg b/docs/static/img/bugtrackersio-tn.jpg new file mode 100644 index 000000000..a56e94009 Binary files /dev/null and b/docs/static/img/bugtrackersio-tn.jpg differ diff --git a/docs/static/img/bullion-investor-com.png b/docs/static/img/bullion-investor-com.png new file mode 100644 index 000000000..3cd78b97d Binary files /dev/null and b/docs/static/img/bullion-investor-com.png differ diff --git a/docs/static/img/camunda-blog.png b/docs/static/img/camunda-blog.png new file mode 100644 index 000000000..95a004ff7 Binary files /dev/null and b/docs/static/img/camunda-blog.png differ diff --git a/docs/static/img/camunda-docs.png b/docs/static/img/camunda-docs.png new file mode 100644 index 000000000..e008fdabb Binary files /dev/null and b/docs/static/img/camunda-docs.png differ diff --git a/docs/static/img/carnivorousplants-tn.png b/docs/static/img/carnivorousplants-tn.png new file mode 100644 index 000000000..2e45bc013 Binary files /dev/null and b/docs/static/img/carnivorousplants-tn.png differ diff --git a/docs/static/img/cdnoverview-tn.png b/docs/static/img/cdnoverview-tn.png new file mode 100644 index 000000000..a95852c45 Binary files /dev/null and b/docs/static/img/cdnoverview-tn.png differ diff --git a/docs/static/img/chinese-grammar-tn.png b/docs/static/img/chinese-grammar-tn.png new file mode 100644 index 000000000..3d84184cf Binary files /dev/null and b/docs/static/img/chinese-grammar-tn.png differ diff --git a/docs/static/img/chingli-tn.jpg b/docs/static/img/chingli-tn.jpg new file mode 100644 index 000000000..61ee53e04 Binary files /dev/null and b/docs/static/img/chingli-tn.jpg differ diff --git a/docs/static/img/chipsncookies-tn.png b/docs/static/img/chipsncookies-tn.png new file mode 100644 index 000000000..f355cb5a4 Binary files /dev/null and b/docs/static/img/chipsncookies-tn.png differ diff --git a/docs/static/img/christianmendoza-tn.jpg b/docs/static/img/christianmendoza-tn.jpg new file mode 100644 index 000000000..82b45afa4 Binary files /dev/null and b/docs/static/img/christianmendoza-tn.jpg differ diff --git a/docs/static/img/cinegyopen-tn.png b/docs/static/img/cinegyopen-tn.png new file mode 100644 index 000000000..3216259fc Binary files /dev/null and b/docs/static/img/cinegyopen-tn.png differ diff --git a/docs/static/img/clearhaus-tn.png b/docs/static/img/clearhaus-tn.png new file mode 100644 index 000000000..4785019a6 Binary files /dev/null and b/docs/static/img/clearhaus-tn.png differ diff --git a/docs/static/img/cloudshark-tn.jpg b/docs/static/img/cloudshark-tn.jpg new file mode 100644 index 000000000..68f8018ef Binary files /dev/null and b/docs/static/img/cloudshark-tn.jpg differ diff --git a/docs/static/img/codingjournal-tn.png b/docs/static/img/codingjournal-tn.png new file mode 100644 index 000000000..e2bfde580 Binary files /dev/null and b/docs/static/img/codingjournal-tn.png differ diff --git a/docs/static/img/consequently.jpg b/docs/static/img/consequently.jpg new file mode 100644 index 000000000..fdb1ebd7b Binary files /dev/null and b/docs/static/img/consequently.jpg differ diff --git a/docs/static/img/content/archetypes/archetype-hierarchy.png b/docs/static/img/content/archetypes/archetype-hierarchy.png new file mode 100644 index 000000000..cb0d0bcf4 Binary files /dev/null and b/docs/static/img/content/archetypes/archetype-hierarchy.png differ diff --git a/docs/static/img/ctlcompiled-tn.png b/docs/static/img/ctlcompiled-tn.png new file mode 100644 index 000000000..e5137da94 Binary files /dev/null and b/docs/static/img/ctlcompiled-tn.png differ diff --git a/docs/static/img/danmux-tn.jpg b/docs/static/img/danmux-tn.jpg new file mode 100644 index 000000000..e6c82a2ef Binary files /dev/null and b/docs/static/img/danmux-tn.jpg differ diff --git a/docs/static/img/datapipelinearchitect-tn.jpg b/docs/static/img/datapipelinearchitect-tn.jpg new file mode 100644 index 000000000..597ecdbba Binary files /dev/null and b/docs/static/img/datapipelinearchitect-tn.jpg differ diff --git a/docs/static/img/davidepetilli-tn.jpg b/docs/static/img/davidepetilli-tn.jpg new file mode 100644 index 000000000..a46fb9149 Binary files /dev/null and b/docs/static/img/davidepetilli-tn.jpg differ diff --git a/docs/static/img/davidrallen-tn.png b/docs/static/img/davidrallen-tn.png new file mode 100644 index 000000000..937147dee Binary files /dev/null and b/docs/static/img/davidrallen-tn.png differ diff --git a/docs/static/img/davidyates-tn.png b/docs/static/img/davidyates-tn.png new file mode 100644 index 000000000..a6a0a6143 Binary files /dev/null and b/docs/static/img/davidyates-tn.png differ diff --git a/docs/static/img/dbzman-online-tn.png b/docs/static/img/dbzman-online-tn.png new file mode 100644 index 000000000..eb9d4ea0c Binary files /dev/null and b/docs/static/img/dbzman-online-tn.png differ diff --git a/docs/static/img/desk-mini.jpg b/docs/static/img/desk-mini.jpg new file mode 100644 index 000000000..ff296c8f7 Binary files /dev/null and b/docs/static/img/desk-mini.jpg differ diff --git a/docs/static/img/desk-sm.jpg b/docs/static/img/desk-sm.jpg new file mode 100644 index 000000000..d4a21ad55 Binary files /dev/null and b/docs/static/img/desk-sm.jpg differ diff --git a/docs/static/img/desk-wide.jpg b/docs/static/img/desk-wide.jpg new file mode 100644 index 000000000..8ff17bc4d Binary files /dev/null and b/docs/static/img/desk-wide.jpg differ diff --git a/docs/static/img/desk.jpg b/docs/static/img/desk.jpg new file mode 100644 index 000000000..43ecc6694 Binary files /dev/null and b/docs/static/img/desk.jpg differ diff --git a/docs/static/img/devmonk-tn.jpg b/docs/static/img/devmonk-tn.jpg new file mode 100644 index 000000000..05331d119 Binary files /dev/null and b/docs/static/img/devmonk-tn.jpg differ diff --git a/docs/static/img/dmitriid.com.png b/docs/static/img/dmitriid.com.png new file mode 100644 index 000000000..9c7217f6e Binary files /dev/null and b/docs/static/img/dmitriid.com.png differ diff --git a/docs/static/img/docs.eurie.io-tn.png b/docs/static/img/docs.eurie.io-tn.png new file mode 100644 index 000000000..43443167b Binary files /dev/null and b/docs/static/img/docs.eurie.io-tn.png differ diff --git a/docs/static/img/emilyhorsman.com-tn.jpg b/docs/static/img/emilyhorsman.com-tn.jpg new file mode 100644 index 000000000..99d2f9559 Binary files /dev/null and b/docs/static/img/emilyhorsman.com-tn.jpg differ diff --git a/docs/static/img/enjoyablerecipes-tn.png b/docs/static/img/enjoyablerecipes-tn.png new file mode 100644 index 000000000..460c804bc Binary files /dev/null and b/docs/static/img/enjoyablerecipes-tn.png differ diff --git a/docs/static/img/esaezgil_com-tn.png b/docs/static/img/esaezgil_com-tn.png new file mode 100644 index 000000000..f9b4087b6 Binary files /dev/null and b/docs/static/img/esaezgil_com-tn.png differ diff --git a/docs/static/img/esolia_com-tn.png b/docs/static/img/esolia_com-tn.png new file mode 100644 index 000000000..4574b085f Binary files /dev/null and b/docs/static/img/esolia_com-tn.png differ diff --git a/docs/static/img/esolia_pro-tn.png b/docs/static/img/esolia_pro-tn.png new file mode 100644 index 000000000..021911456 Binary files /dev/null and b/docs/static/img/esolia_pro-tn.png differ diff --git a/docs/static/img/examples/trees.svg b/docs/static/img/examples/trees.svg deleted file mode 100644 index 0aaccfcff..000000000 --- a/docs/static/img/examples/trees.svg +++ /dev/null @@ -1,104 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -1 -2 -3 -4 -1 -2 -3 -4 -1 -2 -3 -4 -1 -2 -3 -4 -1 -2 -3 -4 -1 -2 -3 -4 - - diff --git a/docs/static/img/fale-tn.png b/docs/static/img/fale-tn.png new file mode 100644 index 000000000..2c5f53f84 Binary files /dev/null and b/docs/static/img/fale-tn.png differ diff --git a/docs/static/img/firstnameclub.png b/docs/static/img/firstnameclub.png new file mode 100644 index 000000000..b5bf80847 Binary files /dev/null and b/docs/static/img/firstnameclub.png differ diff --git a/docs/static/img/fixatom-tn.png b/docs/static/img/fixatom-tn.png new file mode 100644 index 000000000..39ded2463 Binary files /dev/null and b/docs/static/img/fixatom-tn.png differ diff --git a/docs/static/img/freebsd-19px.svg b/docs/static/img/freebsd-19px.svg new file mode 100644 index 000000000..4215b83a9 --- /dev/null +++ b/docs/static/img/freebsd-19px.svg @@ -0,0 +1,127 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/static/img/furqansoftware-tn.png b/docs/static/img/furqansoftware-tn.png new file mode 100644 index 000000000..e1d0e964f Binary files /dev/null and b/docs/static/img/furqansoftware-tn.png differ diff --git a/docs/static/img/fxsitecompat-tn.png b/docs/static/img/fxsitecompat-tn.png new file mode 100644 index 000000000..3df542f59 Binary files /dev/null and b/docs/static/img/fxsitecompat-tn.png differ diff --git a/docs/static/img/gntech-tn.png b/docs/static/img/gntech-tn.png new file mode 100644 index 000000000..0a3fad9ba Binary files /dev/null and b/docs/static/img/gntech-tn.png differ diff --git a/docs/static/img/gogb-tn.jpg b/docs/static/img/gogb-tn.jpg new file mode 100644 index 000000000..caed5cfbb Binary files /dev/null and b/docs/static/img/gogb-tn.jpg differ diff --git a/docs/static/img/goin5minutes-tn.png b/docs/static/img/goin5minutes-tn.png new file mode 100644 index 000000000..eef26f110 Binary files /dev/null and b/docs/static/img/goin5minutes-tn.png differ diff --git a/docs/static/img/gray.png b/docs/static/img/gray.png new file mode 100644 index 000000000..3807691d3 Binary files /dev/null and b/docs/static/img/gray.png differ diff --git a/docs/static/img/h10n.me-tn.png b/docs/static/img/h10n.me-tn.png new file mode 100644 index 000000000..74bfee21b Binary files /dev/null and b/docs/static/img/h10n.me-tn.png differ diff --git a/docs/static/img/heimatverein-niederjosbach-tn.png b/docs/static/img/heimatverein-niederjosbach-tn.png new file mode 100644 index 000000000..f47425b7e Binary files /dev/null and b/docs/static/img/heimatverein-niederjosbach-tn.png differ diff --git a/docs/static/img/horeaporutiu-tn.jpg b/docs/static/img/horeaporutiu-tn.jpg new file mode 100644 index 000000000..7e0d0fc80 Binary files /dev/null and b/docs/static/img/horeaporutiu-tn.jpg differ diff --git a/docs/static/img/hugo-logo-med.png b/docs/static/img/hugo-logo-med.png index 11d91b320..dcc141690 100644 Binary files a/docs/static/img/hugo-logo-med.png and b/docs/static/img/hugo-logo-med.png differ diff --git a/docs/static/img/hugo-logo.png b/docs/static/img/hugo-logo.png index 0a78f8eaa..a4f1321b0 100644 Binary files a/docs/static/img/hugo-logo.png and b/docs/static/img/hugo-logo.png differ diff --git a/docs/static/img/hugo-tn.jpg b/docs/static/img/hugo-tn.jpg new file mode 100644 index 000000000..9ac04a38a Binary files /dev/null and b/docs/static/img/hugo-tn.jpg differ diff --git a/docs/static/img/invincible-tn.jpg b/docs/static/img/invincible-tn.jpg new file mode 100644 index 000000000..f8c11f82a Binary files /dev/null and b/docs/static/img/invincible-tn.jpg differ diff --git a/docs/static/img/invision-tn.png b/docs/static/img/invision-tn.png new file mode 100644 index 000000000..097f71bb3 Binary files /dev/null and b/docs/static/img/invision-tn.png differ diff --git a/docs/static/img/jamescampbell-tn.png b/docs/static/img/jamescampbell-tn.png new file mode 100644 index 000000000..31fb7dc28 Binary files /dev/null and b/docs/static/img/jamescampbell-tn.png differ diff --git a/docs/static/img/jorgennilsson-tn.png b/docs/static/img/jorgennilsson-tn.png new file mode 100644 index 000000000..abcc54ab3 Binary files /dev/null and b/docs/static/img/jorgennilsson-tn.png differ diff --git a/docs/static/img/kjhealy-tn.jpg b/docs/static/img/kjhealy-tn.jpg new file mode 100644 index 000000000..dd2561ae3 Binary files /dev/null and b/docs/static/img/kjhealy-tn.jpg differ diff --git a/docs/static/img/klingt-net-tn.png b/docs/static/img/klingt-net-tn.png new file mode 100644 index 000000000..5ffd3d6a7 Binary files /dev/null and b/docs/static/img/klingt-net-tn.png differ diff --git a/docs/static/img/launchcode-tn.jpg b/docs/static/img/launchcode-tn.jpg new file mode 100644 index 000000000..c422450a1 Binary files /dev/null and b/docs/static/img/launchcode-tn.jpg differ diff --git a/docs/static/img/leepenney-tn.jpg b/docs/static/img/leepenney-tn.jpg new file mode 100644 index 000000000..c1085d779 Binary files /dev/null and b/docs/static/img/leepenney-tn.jpg differ diff --git a/docs/static/img/leowkahman-tn.png b/docs/static/img/leowkahman-tn.png new file mode 100644 index 000000000..ae7803f57 Binary files /dev/null and b/docs/static/img/leowkahman-tn.png differ diff --git a/docs/static/img/lk4d4-tn.jpg b/docs/static/img/lk4d4-tn.jpg new file mode 100644 index 000000000..687606814 Binary files /dev/null and b/docs/static/img/lk4d4-tn.jpg differ diff --git a/docs/static/img/losslesslife-tn.png b/docs/static/img/losslesslife-tn.png new file mode 100644 index 000000000..cc9e286aa Binary files /dev/null and b/docs/static/img/losslesslife-tn.png differ diff --git a/docs/static/img/lucumt.info.png b/docs/static/img/lucumt.info.png new file mode 100644 index 000000000..15a3a213d Binary files /dev/null and b/docs/static/img/lucumt.info.png differ diff --git a/docs/static/img/mariosanchez-tn.jpg b/docs/static/img/mariosanchez-tn.jpg new file mode 100644 index 000000000..75d116c22 Binary files /dev/null and b/docs/static/img/mariosanchez-tn.jpg differ diff --git a/docs/static/img/mayan-edms-tn.png b/docs/static/img/mayan-edms-tn.png new file mode 100644 index 000000000..8feca78e4 Binary files /dev/null and b/docs/static/img/mayan-edms-tn.png differ diff --git a/docs/static/img/michaelwhatcott-tn.jpg b/docs/static/img/michaelwhatcott-tn.jpg new file mode 100644 index 000000000..4cb1b5e80 Binary files /dev/null and b/docs/static/img/michaelwhatcott-tn.jpg differ diff --git a/docs/static/img/mongodb-eng-tn.png b/docs/static/img/mongodb-eng-tn.png new file mode 100644 index 000000000..6b223e745 Binary files /dev/null and b/docs/static/img/mongodb-eng-tn.png differ diff --git a/docs/static/img/mtbhomer-tn.png b/docs/static/img/mtbhomer-tn.png new file mode 100644 index 000000000..c53f68792 Binary files /dev/null and b/docs/static/img/mtbhomer-tn.png differ diff --git a/docs/static/img/myearworms-tn.jpg b/docs/static/img/myearworms-tn.jpg new file mode 100644 index 000000000..49992889b Binary files /dev/null and b/docs/static/img/myearworms-tn.jpg differ diff --git a/docs/static/img/neavey-tn.jpg b/docs/static/img/neavey-tn.jpg new file mode 100644 index 000000000..6f81fcb69 Binary files /dev/null and b/docs/static/img/neavey-tn.jpg differ diff --git a/docs/static/img/nickoneill-tn.jpg b/docs/static/img/nickoneill-tn.jpg new file mode 100644 index 000000000..b8f1d1ae5 Binary files /dev/null and b/docs/static/img/nickoneill-tn.jpg differ diff --git a/docs/static/img/ninjaducks-tn.png b/docs/static/img/ninjaducks-tn.png new file mode 100644 index 000000000..dd70268be Binary files /dev/null and b/docs/static/img/ninjaducks-tn.png differ diff --git a/docs/static/img/ninya-tn.jpg b/docs/static/img/ninya-tn.jpg new file mode 100644 index 000000000..06bba8083 Binary files /dev/null and b/docs/static/img/ninya-tn.jpg differ diff --git a/docs/static/img/nodesk-tn.png b/docs/static/img/nodesk-tn.png new file mode 100644 index 000000000..76457d994 Binary files /dev/null and b/docs/static/img/nodesk-tn.png differ diff --git a/docs/static/img/novelist-xyz.png b/docs/static/img/novelist-xyz.png new file mode 100644 index 000000000..c2ebed74e Binary files /dev/null and b/docs/static/img/novelist-xyz.png differ diff --git a/docs/static/img/npf-tn.jpg b/docs/static/img/npf-tn.jpg new file mode 100644 index 000000000..d3eba9c8d Binary files /dev/null and b/docs/static/img/npf-tn.jpg differ diff --git a/docs/static/img/nutspubcrawl.jpg b/docs/static/img/nutspubcrawl.jpg new file mode 100644 index 000000000..34862b5a2 Binary files /dev/null and b/docs/static/img/nutspubcrawl.jpg differ diff --git a/docs/static/img/ocul-maps.png b/docs/static/img/ocul-maps.png new file mode 100644 index 000000000..298d55ecd Binary files /dev/null and b/docs/static/img/ocul-maps.png differ diff --git a/docs/static/img/petanikode.png b/docs/static/img/petanikode.png new file mode 100644 index 000000000..3935a5c96 Binary files /dev/null and b/docs/static/img/petanikode.png differ diff --git a/docs/static/img/peteraba-tn.jpg b/docs/static/img/peteraba-tn.jpg new file mode 100644 index 000000000..f30d3f042 Binary files /dev/null and b/docs/static/img/peteraba-tn.jpg differ diff --git a/docs/static/img/picturingjordan-tn.png b/docs/static/img/picturingjordan-tn.png new file mode 100644 index 000000000..75e6ea115 Binary files /dev/null and b/docs/static/img/picturingjordan-tn.png differ diff --git a/docs/static/img/promotive.png b/docs/static/img/promotive.png new file mode 100644 index 000000000..9f3f6209f Binary files /dev/null and b/docs/static/img/promotive.png differ diff --git a/docs/static/img/quickstart/bookshelf-bleak-theme.png b/docs/static/img/quickstart/bookshelf-bleak-theme.png new file mode 100644 index 000000000..ccd18c42d Binary files /dev/null and b/docs/static/img/quickstart/bookshelf-bleak-theme.png differ diff --git a/docs/static/img/quickstart/bookshelf-disqus.png b/docs/static/img/quickstart/bookshelf-disqus.png new file mode 100644 index 000000000..3ce645a0c Binary files /dev/null and b/docs/static/img/quickstart/bookshelf-disqus.png differ diff --git a/docs/static/img/quickstart/bookshelf-new-default-image.png b/docs/static/img/quickstart/bookshelf-new-default-image.png new file mode 100644 index 000000000..d7274c7a6 Binary files /dev/null and b/docs/static/img/quickstart/bookshelf-new-default-image.png differ diff --git a/docs/static/img/quickstart/bookshelf-only-picture.png b/docs/static/img/quickstart/bookshelf-only-picture.png new file mode 100644 index 000000000..a363383bc Binary files /dev/null and b/docs/static/img/quickstart/bookshelf-only-picture.png differ diff --git a/docs/static/img/quickstart/bookshelf-robust-theme.png b/docs/static/img/quickstart/bookshelf-robust-theme.png new file mode 100644 index 000000000..7c5e6b8d2 Binary files /dev/null and b/docs/static/img/quickstart/bookshelf-robust-theme.png differ diff --git a/docs/static/img/quickstart/bookshelf-updated-config.png b/docs/static/img/quickstart/bookshelf-updated-config.png new file mode 100644 index 000000000..bbda606c7 Binary files /dev/null and b/docs/static/img/quickstart/bookshelf-updated-config.png differ diff --git a/docs/static/img/quickstart/bookshelf.png b/docs/static/img/quickstart/bookshelf.png new file mode 100644 index 000000000..3b572adbb Binary files /dev/null and b/docs/static/img/quickstart/bookshelf.png differ diff --git a/docs/static/img/quickstart/default.jpg b/docs/static/img/quickstart/default.jpg new file mode 100644 index 000000000..78d7bd28e Binary files /dev/null and b/docs/static/img/quickstart/default.jpg differ diff --git a/docs/static/img/rahulrai_in-tn.png b/docs/static/img/rahulrai_in-tn.png new file mode 100644 index 000000000..cd146dce5 Binary files /dev/null and b/docs/static/img/rahulrai_in-tn.png differ diff --git a/docs/static/img/rakutentech-tn.png b/docs/static/img/rakutentech-tn.png new file mode 100644 index 000000000..04f56e314 Binary files /dev/null and b/docs/static/img/rakutentech-tn.png differ diff --git a/docs/static/img/rdegges-tn.png b/docs/static/img/rdegges-tn.png new file mode 100644 index 000000000..a2e4b6c86 Binary files /dev/null and b/docs/static/img/rdegges-tn.png differ diff --git a/docs/static/img/readtext-tn.png b/docs/static/img/readtext-tn.png new file mode 100644 index 000000000..9e71627b0 Binary files /dev/null and b/docs/static/img/readtext-tn.png differ diff --git a/docs/static/img/richardsumilang-tn.png b/docs/static/img/richardsumilang-tn.png new file mode 100644 index 000000000..68815495f Binary files /dev/null and b/docs/static/img/richardsumilang-tn.png differ diff --git a/docs/static/img/rick_cogley_info-tn.jpg b/docs/static/img/rick_cogley_info-tn.jpg new file mode 100644 index 000000000..414e0108c Binary files /dev/null and b/docs/static/img/rick_cogley_info-tn.jpg differ diff --git a/docs/static/img/ridingbytes-tn.png b/docs/static/img/ridingbytes-tn.png new file mode 100644 index 000000000..624cab96d Binary files /dev/null and b/docs/static/img/ridingbytes-tn.png differ diff --git a/docs/static/img/robertbasic-tn.png b/docs/static/img/robertbasic-tn.png new file mode 100644 index 000000000..5ceecfead Binary files /dev/null and b/docs/static/img/robertbasic-tn.png differ diff --git a/docs/static/img/sanjay-saxena-tn.png b/docs/static/img/sanjay-saxena-tn.png new file mode 100644 index 000000000..85bc5cc58 Binary files /dev/null and b/docs/static/img/sanjay-saxena-tn.png differ diff --git a/docs/static/img/scottcwilson-tn.png b/docs/static/img/scottcwilson-tn.png new file mode 100644 index 000000000..5517edf22 Binary files /dev/null and b/docs/static/img/scottcwilson-tn.png differ diff --git a/docs/static/img/shapeshed-tn.png b/docs/static/img/shapeshed-tn.png new file mode 100644 index 000000000..218b96c5e Binary files /dev/null and b/docs/static/img/shapeshed-tn.png differ diff --git a/docs/static/img/shelan-tn.png b/docs/static/img/shelan-tn.png new file mode 100644 index 000000000..0f7634041 Binary files /dev/null and b/docs/static/img/shelan-tn.png differ diff --git a/docs/static/img/siba-tn.png b/docs/static/img/siba-tn.png new file mode 100644 index 000000000..52373df20 Binary files /dev/null and b/docs/static/img/siba-tn.png differ diff --git a/docs/static/img/silvergeko.jpg b/docs/static/img/silvergeko.jpg new file mode 100644 index 000000000..19b8f98cb Binary files /dev/null and b/docs/static/img/silvergeko.jpg differ diff --git a/docs/static/img/softinio-tn.png b/docs/static/img/softinio-tn.png new file mode 100644 index 000000000..ad94ae876 Binary files /dev/null and b/docs/static/img/softinio-tn.png differ diff --git a/docs/static/img/spf13-tn.jpg b/docs/static/img/spf13-tn.jpg new file mode 100644 index 000000000..a987c71f9 Binary files /dev/null and b/docs/static/img/spf13-tn.jpg differ diff --git a/docs/static/img/steambap.png b/docs/static/img/steambap.png new file mode 100644 index 000000000..bad21f438 Binary files /dev/null and b/docs/static/img/steambap.png differ diff --git a/docs/static/img/stefano.chiodino-tn.jpg b/docs/static/img/stefano.chiodino-tn.jpg new file mode 100644 index 000000000..7747798d5 Binary files /dev/null and b/docs/static/img/stefano.chiodino-tn.jpg differ diff --git a/docs/static/img/stou-tn.png b/docs/static/img/stou-tn.png new file mode 100644 index 000000000..fe449b797 Binary files /dev/null and b/docs/static/img/stou-tn.png differ diff --git a/docs/static/img/szymonkatra-tn.png b/docs/static/img/szymonkatra-tn.png new file mode 100644 index 000000000..e64f2b33d Binary files /dev/null and b/docs/static/img/szymonkatra-tn.png differ diff --git a/docs/static/img/techmadeplain-tn.jpg b/docs/static/img/techmadeplain-tn.jpg new file mode 100644 index 000000000..cae544861 Binary files /dev/null and b/docs/static/img/techmadeplain-tn.jpg differ diff --git a/docs/static/img/tendermint-tn.jpg b/docs/static/img/tendermint-tn.jpg new file mode 100644 index 000000000..807b42d74 Binary files /dev/null and b/docs/static/img/tendermint-tn.jpg differ diff --git a/docs/static/img/thecodeking-tn.jpg b/docs/static/img/thecodeking-tn.jpg new file mode 100644 index 000000000..158384d4e Binary files /dev/null and b/docs/static/img/thecodeking-tn.jpg differ diff --git a/docs/static/img/thehome-tn.png b/docs/static/img/thehome-tn.png new file mode 100644 index 000000000..7b66e6215 Binary files /dev/null and b/docs/static/img/thehome-tn.png differ diff --git a/docs/static/img/thislittleduck-tn.png b/docs/static/img/thislittleduck-tn.png new file mode 100644 index 000000000..0d7407d62 Binary files /dev/null and b/docs/static/img/thislittleduck-tn.png differ diff --git a/docs/static/img/tibobeijen-nl-tn.png b/docs/static/img/tibobeijen-nl-tn.png new file mode 100644 index 000000000..801e34a12 Binary files /dev/null and b/docs/static/img/tibobeijen-nl-tn.png differ diff --git a/docs/static/img/ttsreader-tn.png b/docs/static/img/ttsreader-tn.png new file mode 100644 index 000000000..db33322b4 Binary files /dev/null and b/docs/static/img/ttsreader-tn.png differ diff --git a/docs/static/img/tutorialonfly-tn.jpg b/docs/static/img/tutorialonfly-tn.jpg new file mode 100644 index 000000000..5ff99fff6 Binary files /dev/null and b/docs/static/img/tutorialonfly-tn.jpg differ diff --git a/docs/static/img/tutorials/automated-deployments/adding-a-deploy-pipeline.png b/docs/static/img/tutorials/automated-deployments/adding-a-deploy-pipeline.png new file mode 100644 index 000000000..29f637b06 Binary files /dev/null and b/docs/static/img/tutorials/automated-deployments/adding-a-deploy-pipeline.png differ diff --git a/docs/static/img/tutorials/automated-deployments/adding-a-deploy-step.png b/docs/static/img/tutorials/automated-deployments/adding-a-deploy-step.png new file mode 100644 index 000000000..346187927 Binary files /dev/null and b/docs/static/img/tutorials/automated-deployments/adding-a-deploy-step.png differ diff --git a/docs/static/img/tutorials/automated-deployments/adding-the-project-to-github.png b/docs/static/img/tutorials/automated-deployments/adding-the-project-to-github.png new file mode 100644 index 000000000..e1065bb00 Binary files /dev/null and b/docs/static/img/tutorials/automated-deployments/adding-the-project-to-github.png differ diff --git a/docs/static/img/tutorials/automated-deployments/creating-a-basic-hugo-site.png b/docs/static/img/tutorials/automated-deployments/creating-a-basic-hugo-site.png new file mode 100644 index 000000000..78d238f88 Binary files /dev/null and b/docs/static/img/tutorials/automated-deployments/creating-a-basic-hugo-site.png differ diff --git a/docs/static/img/tutorials/automated-deployments/public-or-not.png b/docs/static/img/tutorials/automated-deployments/public-or-not.png new file mode 100644 index 000000000..9d81a8ba4 Binary files /dev/null and b/docs/static/img/tutorials/automated-deployments/public-or-not.png differ diff --git a/docs/static/img/tutorials/automated-deployments/using-hugo-build.png b/docs/static/img/tutorials/automated-deployments/using-hugo-build.png new file mode 100644 index 000000000..b0dbec94c Binary files /dev/null and b/docs/static/img/tutorials/automated-deployments/using-hugo-build.png differ diff --git a/docs/static/img/tutorials/automated-deployments/wercker-access.png b/docs/static/img/tutorials/automated-deployments/wercker-access.png new file mode 100644 index 000000000..6e89c0ef3 Binary files /dev/null and b/docs/static/img/tutorials/automated-deployments/wercker-access.png differ diff --git a/docs/static/img/tutorials/automated-deployments/wercker-add-app.png b/docs/static/img/tutorials/automated-deployments/wercker-add-app.png new file mode 100644 index 000000000..94ccef518 Binary files /dev/null and b/docs/static/img/tutorials/automated-deployments/wercker-add-app.png differ diff --git a/docs/static/img/tutorials/automated-deployments/wercker-git-connections.png b/docs/static/img/tutorials/automated-deployments/wercker-git-connections.png new file mode 100644 index 000000000..d89c0cd8b Binary files /dev/null and b/docs/static/img/tutorials/automated-deployments/wercker-git-connections.png differ diff --git a/docs/static/img/tutorials/automated-deployments/wercker-search.png b/docs/static/img/tutorials/automated-deployments/wercker-search.png new file mode 100644 index 000000000..d099cfd5c Binary files /dev/null and b/docs/static/img/tutorials/automated-deployments/wercker-search.png differ diff --git a/docs/static/img/tutorials/automated-deployments/wercker-select-repository.png b/docs/static/img/tutorials/automated-deployments/wercker-select-repository.png new file mode 100644 index 000000000..0f7d63d98 Binary files /dev/null and b/docs/static/img/tutorials/automated-deployments/wercker-select-repository.png differ diff --git a/docs/static/img/tutorials/automated-deployments/wercker-sign-up-page.png b/docs/static/img/tutorials/automated-deployments/wercker-sign-up-page.png new file mode 100644 index 000000000..55b0ebd52 Binary files /dev/null and b/docs/static/img/tutorials/automated-deployments/wercker-sign-up-page.png differ diff --git a/docs/static/img/tutorials/automated-deployments/wercker-sign-up.png b/docs/static/img/tutorials/automated-deployments/wercker-sign-up.png new file mode 100644 index 000000000..9c6270061 Binary files /dev/null and b/docs/static/img/tutorials/automated-deployments/wercker-sign-up.png differ diff --git a/docs/static/img/tutorials/automated-deployments/werckeryml.png b/docs/static/img/tutorials/automated-deployments/werckeryml.png new file mode 100644 index 000000000..daa392b4a Binary files /dev/null and b/docs/static/img/tutorials/automated-deployments/werckeryml.png differ diff --git a/docs/static/img/tutorials/hosting-on-bitbucket/bitbucket-blog-post.png b/docs/static/img/tutorials/hosting-on-bitbucket/bitbucket-blog-post.png new file mode 100644 index 000000000..b78f6fd15 Binary files /dev/null and b/docs/static/img/tutorials/hosting-on-bitbucket/bitbucket-blog-post.png differ diff --git a/docs/static/img/tutorials/hosting-on-bitbucket/bitbucket-create-repo.png b/docs/static/img/tutorials/hosting-on-bitbucket/bitbucket-create-repo.png new file mode 100644 index 000000000..e97f13465 Binary files /dev/null and b/docs/static/img/tutorials/hosting-on-bitbucket/bitbucket-create-repo.png differ diff --git a/docs/static/img/tutorials/how-to-contribute-to-hugo/accept-cla.png b/docs/static/img/tutorials/how-to-contribute-to-hugo/accept-cla.png new file mode 100644 index 000000000..929fda6ab Binary files /dev/null and b/docs/static/img/tutorials/how-to-contribute-to-hugo/accept-cla.png differ diff --git a/docs/static/img/tutorials/how-to-contribute-to-hugo/ci-errors.png b/docs/static/img/tutorials/how-to-contribute-to-hugo/ci-errors.png new file mode 100644 index 000000000..95cd290b6 Binary files /dev/null and b/docs/static/img/tutorials/how-to-contribute-to-hugo/ci-errors.png differ diff --git a/docs/static/img/tutorials/how-to-contribute-to-hugo/copy-remote-url.png b/docs/static/img/tutorials/how-to-contribute-to-hugo/copy-remote-url.png new file mode 100644 index 000000000..9006f4a48 Binary files /dev/null and b/docs/static/img/tutorials/how-to-contribute-to-hugo/copy-remote-url.png differ diff --git a/docs/static/img/tutorials/how-to-contribute-to-hugo/forking-a-repository.png b/docs/static/img/tutorials/how-to-contribute-to-hugo/forking-a-repository.png new file mode 100644 index 000000000..ea132cab3 Binary files /dev/null and b/docs/static/img/tutorials/how-to-contribute-to-hugo/forking-a-repository.png differ diff --git a/docs/static/img/tutorials/how-to-contribute-to-hugo/open-pull-request.png b/docs/static/img/tutorials/how-to-contribute-to-hugo/open-pull-request.png new file mode 100644 index 000000000..63b504fb2 Binary files /dev/null and b/docs/static/img/tutorials/how-to-contribute-to-hugo/open-pull-request.png differ diff --git a/docs/static/img/tutswiki-tn.jpg b/docs/static/img/tutswiki-tn.jpg new file mode 100644 index 000000000..11efb2a5b Binary files /dev/null and b/docs/static/img/tutswiki-tn.jpg differ diff --git a/docs/static/img/ucsb-tn.jpg b/docs/static/img/ucsb-tn.jpg new file mode 100644 index 000000000..45962027d Binary files /dev/null and b/docs/static/img/ucsb-tn.jpg differ diff --git a/docs/static/img/upbeat.png b/docs/static/img/upbeat.png new file mode 100644 index 000000000..e7a6a694c Binary files /dev/null and b/docs/static/img/upbeat.png differ diff --git a/docs/static/img/vamp_landingpage-tn.png b/docs/static/img/vamp_landingpage-tn.png new file mode 100644 index 000000000..474261e0e Binary files /dev/null and b/docs/static/img/vamp_landingpage-tn.png differ diff --git a/docs/static/img/viglug-tn.png b/docs/static/img/viglug-tn.png new file mode 100644 index 000000000..d18ab4023 Binary files /dev/null and b/docs/static/img/viglug-tn.png differ diff --git a/docs/static/img/vurt.co-tn.jpg b/docs/static/img/vurt.co-tn.jpg new file mode 100644 index 000000000..5e7f131a0 Binary files /dev/null and b/docs/static/img/vurt.co-tn.jpg differ diff --git a/docs/static/img/worldtowriters-com.jpg b/docs/static/img/worldtowriters-com.jpg new file mode 100644 index 000000000..570d06fa9 Binary files /dev/null and b/docs/static/img/worldtowriters-com.jpg differ diff --git a/docs/static/img/yslow-rules-tn.png b/docs/static/img/yslow-rules-tn.png new file mode 100644 index 000000000..5c75a6943 Binary files /dev/null and b/docs/static/img/yslow-rules-tn.png differ diff --git a/docs/static/img/ysqi-blog.png b/docs/static/img/ysqi-blog.png new file mode 100644 index 000000000..6dd234109 Binary files /dev/null and b/docs/static/img/ysqi-blog.png differ diff --git a/docs/static/img/yulinling-tn.jpg b/docs/static/img/yulinling-tn.jpg new file mode 100644 index 000000000..bdb12f0e7 Binary files /dev/null and b/docs/static/img/yulinling-tn.jpg differ diff --git a/docs/static/js/livereload.js b/docs/static/js/livereload.js new file mode 100644 index 000000000..f6c3b7f90 --- /dev/null +++ b/docs/static/js/livereload.js @@ -0,0 +1,1155 @@ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o tag"); + return; + } + this.reloader = new Reloader(this.window, this.console, Timer); + this.connector = new Connector(this.options, this.WebSocket, Timer, { + connecting: (function(_this) { + return function() {}; + })(this), + socketConnected: (function(_this) { + return function() {}; + })(this), + connected: (function(_this) { + return function(protocol) { + var _base; + if (typeof (_base = _this.listeners).connect === "function") { + _base.connect(); + } + _this.log("LiveReload is connected to " + _this.options.host + ":" + _this.options.port + " (protocol v" + protocol + ")."); + return _this.analyze(); + }; + })(this), + error: (function(_this) { + return function(e) { + if (e instanceof ProtocolError) { + if (typeof console !== "undefined" && console !== null) { + return console.log("" + e.message + "."); + } + } else { + if (typeof console !== "undefined" && console !== null) { + return console.log("LiveReload internal error: " + e.message); + } + } + }; + })(this), + disconnected: (function(_this) { + return function(reason, nextDelay) { + var _base; + if (typeof (_base = _this.listeners).disconnect === "function") { + _base.disconnect(); + } + switch (reason) { + case 'cannot-connect': + return _this.log("LiveReload cannot connect to " + _this.options.host + ":" + _this.options.port + ", will retry in " + nextDelay + " sec."); + case 'broken': + return _this.log("LiveReload disconnected from " + _this.options.host + ":" + _this.options.port + ", reconnecting in " + nextDelay + " sec."); + case 'handshake-timeout': + return _this.log("LiveReload cannot connect to " + _this.options.host + ":" + _this.options.port + " (handshake timeout), will retry in " + nextDelay + " sec."); + case 'handshake-failed': + return _this.log("LiveReload cannot connect to " + _this.options.host + ":" + _this.options.port + " (handshake failed), will retry in " + nextDelay + " sec."); + case 'manual': + break; + case 'error': + break; + default: + return _this.log("LiveReload disconnected from " + _this.options.host + ":" + _this.options.port + " (" + reason + "), reconnecting in " + nextDelay + " sec."); + } + }; + })(this), + message: (function(_this) { + return function(message) { + switch (message.command) { + case 'reload': + return _this.performReload(message); + case 'alert': + return _this.performAlert(message); + } + }; + })(this) + }); + } + + LiveReload.prototype.on = function(eventName, handler) { + return this.listeners[eventName] = handler; + }; + + LiveReload.prototype.log = function(message) { + return this.console.log("" + message); + }; + + LiveReload.prototype.performReload = function(message) { + var _ref, _ref1; + this.log("LiveReload received reload request: " + (JSON.stringify(message, null, 2))); + return this.reloader.reload(message.path, { + liveCSS: (_ref = message.liveCSS) != null ? _ref : true, + liveImg: (_ref1 = message.liveImg) != null ? _ref1 : true, + originalPath: message.originalPath || '', + overrideURL: message.overrideURL || '', + serverURL: "http://" + this.options.host + ":" + this.options.port + }); + }; + + LiveReload.prototype.performAlert = function(message) { + return alert(message.message); + }; + + LiveReload.prototype.shutDown = function() { + var _base; + this.connector.disconnect(); + this.log("LiveReload disconnected."); + return typeof (_base = this.listeners).shutdown === "function" ? _base.shutdown() : void 0; + }; + + LiveReload.prototype.hasPlugin = function(identifier) { + return !!this.pluginIdentifiers[identifier]; + }; + + LiveReload.prototype.addPlugin = function(pluginClass) { + var plugin; + if (this.hasPlugin(pluginClass.identifier)) { + return; + } + this.pluginIdentifiers[pluginClass.identifier] = true; + plugin = new pluginClass(this.window, { + _livereload: this, + _reloader: this.reloader, + _connector: this.connector, + console: this.console, + Timer: Timer, + generateCacheBustUrl: (function(_this) { + return function(url) { + return _this.reloader.generateCacheBustUrl(url); + }; + })(this) + }); + this.plugins.push(plugin); + this.reloader.addPlugin(plugin); + }; + + LiveReload.prototype.analyze = function() { + var plugin, pluginData, pluginsData, _i, _len, _ref; + if (!(this.connector.protocol >= 7)) { + return; + } + pluginsData = {}; + _ref = this.plugins; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + plugin = _ref[_i]; + pluginsData[plugin.constructor.identifier] = pluginData = (typeof plugin.analyze === "function" ? plugin.analyze() : void 0) || {}; + pluginData.version = plugin.constructor.version; + } + this.connector.sendCommand({ + command: 'info', + plugins: pluginsData, + url: this.window.location.href + }); + }; + + return LiveReload; + + })(); + +}).call(this); + +},{"./connector":1,"./options":5,"./reloader":7,"./timer":9}],5:[function(require,module,exports){ +(function() { + var Options; + + exports.Options = Options = (function() { + function Options() { + this.host = null; + this.port = 35729; + this.snipver = null; + this.ext = null; + this.extver = null; + this.mindelay = 1000; + this.maxdelay = 60000; + this.handshake_timeout = 5000; + } + + Options.prototype.set = function(name, value) { + if (typeof value === 'undefined') { + return; + } + if (!isNaN(+value)) { + value = +value; + } + return this[name] = value; + }; + + return Options; + + })(); + + Options.extract = function(document) { + var element, keyAndValue, m, mm, options, pair, src, _i, _j, _len, _len1, _ref, _ref1; + _ref = document.getElementsByTagName('script'); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + element = _ref[_i]; + if ((src = element.src) && (m = src.match(/^[^:]+:\/\/(.*)\/z?livereload\.js(?:\?(.*))?$/))) { + options = new Options(); + if (mm = m[1].match(/^([^\/:]+)(?::(\d+))?$/)) { + options.host = mm[1]; + if (mm[2]) { + options.port = parseInt(mm[2], 10); + } + } + if (m[2]) { + _ref1 = m[2].split('&'); + for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { + pair = _ref1[_j]; + if ((keyAndValue = pair.split('=')).length > 1) { + options.set(keyAndValue[0].replace(/-/g, '_'), keyAndValue.slice(1).join('=')); + } + } + } + return options; + } + } + return null; + }; + +}).call(this); + +},{}],6:[function(require,module,exports){ +(function() { + var PROTOCOL_6, PROTOCOL_7, Parser, ProtocolError, + __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; + + exports.PROTOCOL_6 = PROTOCOL_6 = 'http://livereload.com/protocols/official-6'; + + exports.PROTOCOL_7 = PROTOCOL_7 = 'http://livereload.com/protocols/official-7'; + + exports.ProtocolError = ProtocolError = (function() { + function ProtocolError(reason, data) { + this.message = "LiveReload protocol error (" + reason + ") after receiving data: \"" + data + "\"."; + } + + return ProtocolError; + + })(); + + exports.Parser = Parser = (function() { + function Parser(handlers) { + this.handlers = handlers; + this.reset(); + } + + Parser.prototype.reset = function() { + return this.protocol = null; + }; + + Parser.prototype.process = function(data) { + var command, e, message, options, _ref; + try { + if (this.protocol == null) { + if (data.match(/^!!ver:([\d.]+)$/)) { + this.protocol = 6; + } else if (message = this._parseMessage(data, ['hello'])) { + if (!message.protocols.length) { + throw new ProtocolError("no protocols specified in handshake message"); + } else if (__indexOf.call(message.protocols, PROTOCOL_7) >= 0) { + this.protocol = 7; + } else if (__indexOf.call(message.protocols, PROTOCOL_6) >= 0) { + this.protocol = 6; + } else { + throw new ProtocolError("no supported protocols found"); + } + } + return this.handlers.connected(this.protocol); + } else if (this.protocol === 6) { + message = JSON.parse(data); + if (!message.length) { + throw new ProtocolError("protocol 6 messages must be arrays"); + } + command = message[0], options = message[1]; + if (command !== 'refresh') { + throw new ProtocolError("unknown protocol 6 command"); + } + return this.handlers.message({ + command: 'reload', + path: options.path, + liveCSS: (_ref = options.apply_css_live) != null ? _ref : true + }); + } else { + message = this._parseMessage(data, ['reload', 'alert']); + return this.handlers.message(message); + } + } catch (_error) { + e = _error; + if (e instanceof ProtocolError) { + return this.handlers.error(e); + } else { + throw e; + } + } + }; + + Parser.prototype._parseMessage = function(data, validCommands) { + var e, message, _ref; + try { + message = JSON.parse(data); + } catch (_error) { + e = _error; + throw new ProtocolError('unparsable JSON', data); + } + if (!message.command) { + throw new ProtocolError('missing "command" key', data); + } + if (_ref = message.command, __indexOf.call(validCommands, _ref) < 0) { + throw new ProtocolError("invalid command '" + message.command + "', only valid commands are: " + (validCommands.join(', ')) + ")", data); + } + return message; + }; + + return Parser; + + })(); + +}).call(this); + +},{}],7:[function(require,module,exports){ +(function() { + var IMAGE_STYLES, Reloader, numberOfMatchingSegments, pathFromUrl, pathsMatch, pickBestMatch, splitUrl; + + splitUrl = function(url) { + var hash, index, params; + if ((index = url.indexOf('#')) >= 0) { + hash = url.slice(index); + url = url.slice(0, index); + } else { + hash = ''; + } + if ((index = url.indexOf('?')) >= 0) { + params = url.slice(index); + url = url.slice(0, index); + } else { + params = ''; + } + return { + url: url, + params: params, + hash: hash + }; + }; + + pathFromUrl = function(url) { + var path; + url = splitUrl(url).url; + if (url.indexOf('file://') === 0) { + path = url.replace(/^file:\/\/(localhost)?/, ''); + } else { + path = url.replace(/^([^:]+:)?\/\/([^:\/]+)(:\d*)?\//, '/'); + } + return decodeURIComponent(path); + }; + + pickBestMatch = function(path, objects, pathFunc) { + var bestMatch, object, score, _i, _len; + bestMatch = { + score: 0 + }; + for (_i = 0, _len = objects.length; _i < _len; _i++) { + object = objects[_i]; + score = numberOfMatchingSegments(path, pathFunc(object)); + if (score > bestMatch.score) { + bestMatch = { + object: object, + score: score + }; + } + } + if (bestMatch.score > 0) { + return bestMatch; + } else { + return null; + } + }; + + numberOfMatchingSegments = function(path1, path2) { + var comps1, comps2, eqCount, len; + path1 = path1.replace(/^\/+/, '').toLowerCase(); + path2 = path2.replace(/^\/+/, '').toLowerCase(); + if (path1 === path2) { + return 10000; + } + comps1 = path1.split('/').reverse(); + comps2 = path2.split('/').reverse(); + len = Math.min(comps1.length, comps2.length); + eqCount = 0; + while (eqCount < len && comps1[eqCount] === comps2[eqCount]) { + ++eqCount; + } + return eqCount; + }; + + pathsMatch = function(path1, path2) { + return numberOfMatchingSegments(path1, path2) > 0; + }; + + IMAGE_STYLES = [ + { + selector: 'background', + styleNames: ['backgroundImage'] + }, { + selector: 'border', + styleNames: ['borderImage', 'webkitBorderImage', 'MozBorderImage'] + } + ]; + + exports.Reloader = Reloader = (function() { + function Reloader(window, console, Timer) { + this.window = window; + this.console = console; + this.Timer = Timer; + this.document = this.window.document; + this.importCacheWaitPeriod = 200; + this.plugins = []; + } + + Reloader.prototype.addPlugin = function(plugin) { + return this.plugins.push(plugin); + }; + + Reloader.prototype.analyze = function(callback) { + return results; + }; + + Reloader.prototype.reload = function(path, options) { + var plugin, _base, _i, _len, _ref; + this.options = options; + if ((_base = this.options).stylesheetReloadTimeout == null) { + _base.stylesheetReloadTimeout = 15000; + } + _ref = this.plugins; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + plugin = _ref[_i]; + if (plugin.reload && plugin.reload(path, options)) { + return; + } + } + if (options.liveCSS) { + if (path.match(/\.css$/i)) { + if (this.reloadStylesheet(path)) { + return; + } + } + } + if (options.liveImg) { + if (path.match(/\.(jpe?g|png|gif)$/i)) { + this.reloadImages(path); + return; + } + } + return this.reloadPage(); + }; + + Reloader.prototype.reloadPage = function() { + return this.window.document.location.reload(); + }; + + Reloader.prototype.reloadImages = function(path) { + var expando, img, selector, styleNames, styleSheet, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1, _ref2, _ref3, _results; + expando = this.generateUniqueString(); + _ref = this.document.images; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + img = _ref[_i]; + if (pathsMatch(path, pathFromUrl(img.src))) { + img.src = this.generateCacheBustUrl(img.src, expando); + } + } + if (this.document.querySelectorAll) { + for (_j = 0, _len1 = IMAGE_STYLES.length; _j < _len1; _j++) { + _ref1 = IMAGE_STYLES[_j], selector = _ref1.selector, styleNames = _ref1.styleNames; + _ref2 = this.document.querySelectorAll("[style*=" + selector + "]"); + for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { + img = _ref2[_k]; + this.reloadStyleImages(img.style, styleNames, path, expando); + } + } + } + if (this.document.styleSheets) { + _ref3 = this.document.styleSheets; + _results = []; + for (_l = 0, _len3 = _ref3.length; _l < _len3; _l++) { + styleSheet = _ref3[_l]; + _results.push(this.reloadStylesheetImages(styleSheet, path, expando)); + } + return _results; + } + }; + + Reloader.prototype.reloadStylesheetImages = function(styleSheet, path, expando) { + var e, rule, rules, styleNames, _i, _j, _len, _len1; + try { + rules = styleSheet != null ? styleSheet.cssRules : void 0; + } catch (_error) { + e = _error; + } + if (!rules) { + return; + } + for (_i = 0, _len = rules.length; _i < _len; _i++) { + rule = rules[_i]; + switch (rule.type) { + case CSSRule.IMPORT_RULE: + this.reloadStylesheetImages(rule.styleSheet, path, expando); + break; + case CSSRule.STYLE_RULE: + for (_j = 0, _len1 = IMAGE_STYLES.length; _j < _len1; _j++) { + styleNames = IMAGE_STYLES[_j].styleNames; + this.reloadStyleImages(rule.style, styleNames, path, expando); + } + break; + case CSSRule.MEDIA_RULE: + this.reloadStylesheetImages(rule, path, expando); + } + } + }; + + Reloader.prototype.reloadStyleImages = function(style, styleNames, path, expando) { + var newValue, styleName, value, _i, _len; + for (_i = 0, _len = styleNames.length; _i < _len; _i++) { + styleName = styleNames[_i]; + value = style[styleName]; + if (typeof value === 'string') { + newValue = value.replace(/\burl\s*\(([^)]*)\)/, (function(_this) { + return function(match, src) { + if (pathsMatch(path, pathFromUrl(src))) { + return "url(" + (_this.generateCacheBustUrl(src, expando)) + ")"; + } else { + return match; + } + }; + })(this)); + if (newValue !== value) { + style[styleName] = newValue; + } + } + } + }; + + Reloader.prototype.reloadStylesheet = function(path) { + var imported, link, links, match, style, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1; + links = (function() { + var _i, _len, _ref, _results; + _ref = this.document.getElementsByTagName('link'); + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + link = _ref[_i]; + if (link.rel.match(/^stylesheet$/i) && !link.__LiveReload_pendingRemoval) { + _results.push(link); + } + } + return _results; + }).call(this); + imported = []; + _ref = this.document.getElementsByTagName('style'); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + style = _ref[_i]; + if (style.sheet) { + this.collectImportedStylesheets(style, style.sheet, imported); + } + } + for (_j = 0, _len1 = links.length; _j < _len1; _j++) { + link = links[_j]; + this.collectImportedStylesheets(link, link.sheet, imported); + } + if (this.window.StyleFix && this.document.querySelectorAll) { + _ref1 = this.document.querySelectorAll('style[data-href]'); + for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) { + style = _ref1[_k]; + links.push(style); + } + } + this.console.log("LiveReload found " + links.length + " LINKed stylesheets, " + imported.length + " @imported stylesheets"); + match = pickBestMatch(path, links.concat(imported), (function(_this) { + return function(l) { + return pathFromUrl(_this.linkHref(l)); + }; + })(this)); + if (match) { + if (match.object.rule) { + this.console.log("LiveReload is reloading imported stylesheet: " + match.object.href); + this.reattachImportedRule(match.object); + } else { + this.console.log("LiveReload is reloading stylesheet: " + (this.linkHref(match.object))); + this.reattachStylesheetLink(match.object); + } + } else { + this.console.log("LiveReload will reload all stylesheets because path '" + path + "' did not match any specific one"); + for (_l = 0, _len3 = links.length; _l < _len3; _l++) { + link = links[_l]; + this.reattachStylesheetLink(link); + } + } + return true; + }; + + Reloader.prototype.collectImportedStylesheets = function(link, styleSheet, result) { + var e, index, rule, rules, _i, _len; + try { + rules = styleSheet != null ? styleSheet.cssRules : void 0; + } catch (_error) { + e = _error; + } + if (rules && rules.length) { + for (index = _i = 0, _len = rules.length; _i < _len; index = ++_i) { + rule = rules[index]; + switch (rule.type) { + case CSSRule.CHARSET_RULE: + continue; + case CSSRule.IMPORT_RULE: + result.push({ + link: link, + rule: rule, + index: index, + href: rule.href + }); + this.collectImportedStylesheets(link, rule.styleSheet, result); + break; + default: + break; + } + } + } + }; + + Reloader.prototype.waitUntilCssLoads = function(clone, func) { + var callbackExecuted, executeCallback, poll; + callbackExecuted = false; + executeCallback = (function(_this) { + return function() { + if (callbackExecuted) { + return; + } + callbackExecuted = true; + return func(); + }; + })(this); + clone.onload = (function(_this) { + return function() { + _this.console.log("LiveReload: the new stylesheet has finished loading"); + _this.knownToSupportCssOnLoad = true; + return executeCallback(); + }; + })(this); + if (!this.knownToSupportCssOnLoad) { + (poll = (function(_this) { + return function() { + if (clone.sheet) { + _this.console.log("LiveReload is polling until the new CSS finishes loading..."); + return executeCallback(); + } else { + return _this.Timer.start(50, poll); + } + }; + })(this))(); + } + return this.Timer.start(this.options.stylesheetReloadTimeout, executeCallback); + }; + + Reloader.prototype.linkHref = function(link) { + return link.href || link.getAttribute('data-href'); + }; + + Reloader.prototype.reattachStylesheetLink = function(link) { + var clone, parent; + if (link.__LiveReload_pendingRemoval) { + return; + } + link.__LiveReload_pendingRemoval = true; + if (link.tagName === 'STYLE') { + clone = this.document.createElement('link'); + clone.rel = 'stylesheet'; + clone.media = link.media; + clone.disabled = link.disabled; + } else { + clone = link.cloneNode(false); + } + clone.href = this.generateCacheBustUrl(this.linkHref(link)); + parent = link.parentNode; + if (parent.lastChild === link) { + parent.appendChild(clone); + } else { + parent.insertBefore(clone, link.nextSibling); + } + return this.waitUntilCssLoads(clone, (function(_this) { + return function() { + var additionalWaitingTime; + if (/AppleWebKit/.test(navigator.userAgent)) { + additionalWaitingTime = 5; + } else { + additionalWaitingTime = 200; + } + return _this.Timer.start(additionalWaitingTime, function() { + var _ref; + if (!link.parentNode) { + return; + } + link.parentNode.removeChild(link); + clone.onreadystatechange = null; + return (_ref = _this.window.StyleFix) != null ? _ref.link(clone) : void 0; + }); + }; + })(this)); + }; + + Reloader.prototype.reattachImportedRule = function(_arg) { + var href, index, link, media, newRule, parent, rule, tempLink; + rule = _arg.rule, index = _arg.index, link = _arg.link; + parent = rule.parentStyleSheet; + href = this.generateCacheBustUrl(rule.href); + media = rule.media.length ? [].join.call(rule.media, ', ') : ''; + newRule = "@import url(\"" + href + "\") " + media + ";"; + rule.__LiveReload_newHref = href; + tempLink = this.document.createElement("link"); + tempLink.rel = 'stylesheet'; + tempLink.href = href; + tempLink.__LiveReload_pendingRemoval = true; + if (link.parentNode) { + link.parentNode.insertBefore(tempLink, link); + } + return this.Timer.start(this.importCacheWaitPeriod, (function(_this) { + return function() { + if (tempLink.parentNode) { + tempLink.parentNode.removeChild(tempLink); + } + if (rule.__LiveReload_newHref !== href) { + return; + } + parent.insertRule(newRule, index); + parent.deleteRule(index + 1); + rule = parent.cssRules[index]; + rule.__LiveReload_newHref = href; + return _this.Timer.start(_this.importCacheWaitPeriod, function() { + if (rule.__LiveReload_newHref !== href) { + return; + } + parent.insertRule(newRule, index); + return parent.deleteRule(index + 1); + }); + }; + })(this)); + }; + + Reloader.prototype.generateUniqueString = function() { + return 'livereload=' + Date.now(); + }; + + Reloader.prototype.generateCacheBustUrl = function(url, expando) { + var hash, oldParams, originalUrl, params, _ref; + if (expando == null) { + expando = this.generateUniqueString(); + } + _ref = splitUrl(url), url = _ref.url, hash = _ref.hash, oldParams = _ref.params; + if (this.options.overrideURL) { + if (url.indexOf(this.options.serverURL) < 0) { + originalUrl = url; + url = this.options.serverURL + this.options.overrideURL + "?url=" + encodeURIComponent(url); + this.console.log("LiveReload is overriding source URL " + originalUrl + " with " + url); + } + } + params = oldParams.replace(/(\?|&)livereload=(\d+)/, function(match, sep) { + return "" + sep + expando; + }); + if (params === oldParams) { + if (oldParams.length === 0) { + params = "?" + expando; + } else { + params = "" + oldParams + "&" + expando; + } + } + return url + params + hash; + }; + + return Reloader; + + })(); + +}).call(this); + +},{}],8:[function(require,module,exports){ +(function() { + var CustomEvents, LiveReload, k; + + CustomEvents = require('./customevents'); + + LiveReload = window.LiveReload = new (require('./livereload').LiveReload)(window); + + for (k in window) { + if (k.match(/^LiveReloadPlugin/)) { + LiveReload.addPlugin(window[k]); + } + } + + LiveReload.addPlugin(require('./less')); + + LiveReload.on('shutdown', function() { + return delete window.LiveReload; + }); + + LiveReload.on('connect', function() { + return CustomEvents.fire(document, 'LiveReloadConnect'); + }); + + LiveReload.on('disconnect', function() { + return CustomEvents.fire(document, 'LiveReloadDisconnect'); + }); + + CustomEvents.bind(document, 'LiveReloadShutDown', function() { + return LiveReload.shutDown(); + }); + +}).call(this); + +},{"./customevents":2,"./less":3,"./livereload":4}],9:[function(require,module,exports){ +(function() { + var Timer; + + exports.Timer = Timer = (function() { + function Timer(func) { + this.func = func; + this.running = false; + this.id = null; + this._handler = (function(_this) { + return function() { + _this.running = false; + _this.id = null; + return _this.func(); + }; + })(this); + } + + Timer.prototype.start = function(timeout) { + if (this.running) { + clearTimeout(this.id); + } + this.id = setTimeout(this._handler, timeout); + return this.running = true; + }; + + Timer.prototype.stop = function() { + if (this.running) { + clearTimeout(this.id); + this.running = false; + return this.id = null; + } + }; + + return Timer; + + })(); + + Timer.start = function(timeout, func) { + return setTimeout(func, timeout); + }; + +}).call(this); + +},{}]},{},[8]); diff --git a/docs/static/js/owl.carousel-custom.js b/docs/static/js/owl.carousel-custom.js new file mode 100644 index 000000000..685c8e361 --- /dev/null +++ b/docs/static/js/owl.carousel-custom.js @@ -0,0 +1,16 @@ +$('.owl-carousel').owlCarousel({ + loop:true, + margin:10, + nav:true, + autoplay:true, + autoplayHoverPause:true, + autoplayTimeout:3000, + responsive:{ + 0:{ + items:1 + }, + 1000:{ + items:3 + }, + } +}) diff --git a/docs/static/js/scripts.js b/docs/static/js/scripts.js new file mode 100644 index 000000000..ef6074f55 --- /dev/null +++ b/docs/static/js/scripts.js @@ -0,0 +1,283 @@ +function initializeJS() { + + //tool tips + jQuery('.tooltips').tooltip(); + + //popovers + jQuery('.popovers').popover(); + + //sidebar dropdown menu + jQuery('#sidebar .sub-menu > a').click(function () { + // Close previous open submenu + var last = jQuery('.sub.open', jQuery('#sidebar')); + jQuery(last).slideUp(200); + jQuery(last).removeClass("open"); + jQuery('.menu-arrow', jQuery(last).parent()).addClass('fa-angle-right'); + jQuery('.menu-arrow', jQuery(last).parent()).removeClass('fa-angle-down'); + + // Toggle current submenu + var sub = jQuery(this).next(); + if (sub.is(":visible")) { + jQuery('.menu-arrow', this).addClass('fa-angle-right'); + jQuery('.menu-arrow', this).removeClass('fa-angle-down'); + sub.slideUp(200); + jQuery(sub).removeClass("open") + } else { + jQuery('.menu-arrow', this).addClass('fa-angle-down'); + jQuery('.menu-arrow', this).removeClass('fa-angle-right'); + sub.slideDown(200); + jQuery(sub).addClass("open") + } + + // Center menu on screen + var o = (jQuery(this).offset()); + diff = 200 - o.top; + if(diff>0) + jQuery("#sidebar").scrollTo("-="+Math.abs(diff),500); + else + jQuery("#sidebar").scrollTo("+="+Math.abs(diff),500); + }); + + + // sidebar menu toggle + jQuery(function() { + function responsiveView() { + var wSize = jQuery(window).width(); + if (wSize <= 768) { + jQuery('#container').addClass('sidebar-close'); + jQuery('#sidebar > ul').hide(); + } + + if (wSize > 768) { + jQuery('#container').removeClass('sidebar-close'); + jQuery('#sidebar > ul').show(); + } + } + jQuery(window).on('load', responsiveView); + jQuery(window).on('resize', responsiveView); + }); + + jQuery('.toggle-nav').click(function () { + if (jQuery('#sidebar > ul').is(":visible") === true) { + jQuery('#main-content').css({ + 'margin-left': '0px' + }); + jQuery('#sidebar').css({ + 'margin-left': '-180px' + }); + jQuery('#sidebar > ul').hide(); + jQuery("#container").addClass("sidebar-closed"); + } else { + jQuery('#main-content').css({ + 'margin-left': '180px' + }); + jQuery('#sidebar > ul').show(); + jQuery('#sidebar').css({ + 'margin-left': '0' + }); + jQuery("#container").removeClass("sidebar-closed"); + } + }); + + //bar chart + if (jQuery(".custom-custom-bar-chart")) { + jQuery(".bar").each(function () { + var i = jQuery(this).find(".value").html(); + jQuery(this).find(".value").html(""); + jQuery(this).find(".value").animate({ + height: i + }, 2000) + }) + } + +} + +(function(){ + var caches = {}; + $.fn.showGithub = function(user, repo, type, count){ + $(this).each(function(){ + var $e = $(this); + var user = $e.data('user') || user, + repo = $e.data('repo') || repo, + type = $e.data('type') || type || 'watch', + count = $e.data('count') == 'true' || count || true; + var $mainButton = $e.html(' ').find('.github-btn'), + $button = $mainButton.find('.btn'), + $text = $mainButton.find('.gh-text'), + $counter = $mainButton.find('.gh-count'); + + function addCommas(a) { + return String(a).replace(/(\d)(?=(\d{3})+$)/g, '$1,'); + } + + function callback(a) { + if (type == 'watch') { + $counter.html(addCommas(a.watchers)); + } else { + if (type == 'fork') { + $counter.html(addCommas(a.forks)); + } else { + if (type == 'follow') { + $counter.html(addCommas(a.followers)); + } + } + } + + if (count) { + $counter.css('display', 'inline-block'); + } + } + + function jsonp(url) { + var ctx = caches[url] || {}; + caches[url] = ctx; + if(ctx.onload || ctx.data){ + if(ctx.data){ + callback(ctx.data); + } else { + setTimeout(jsonp, 500, url); + } + }else{ + ctx.onload = true; + $.getJSON(url, function(a){ + ctx.onload = false; + ctx.data = a; + callback(a); + }); + } + } + + var urlBase = 'https://github.com/' + user + '/' + repo; + + $button.attr('href', urlBase + '/'); + + if (type == 'watch') { + $mainButton.addClass('github-watchers'); + $text.html('Star'); + $counter.attr('href', urlBase + '/stargazers'); + } else { + if (type == 'fork') { + $mainButton.addClass('github-forks'); + $text.html('Fork'); + $counter.attr('href', urlBase + '/network'); + } else { + if (type == 'follow') { + $mainButton.addClass('github-me'); + $text.html('Follow @' + user); + $button.attr('href', 'https://github.com/' + user); + $counter.attr('href', 'https://github.com/' + user + '/followers'); + } + } + } + + if (type == 'follow') { + jsonp('https://api.github.com/users/' + user); + } else { + jsonp('https://api.github.com/repos/' + user + '/' + repo); + } + + }); + }; + + })(); + + +(function($){ + (function(){ + var caches = {}; + $.fn.showGithub = function(user, repo, type, count){ + + $(this).each(function(){ + var $e = $(this); + + var user = $e.data('user') || user, + repo = $e.data('repo') || repo, + type = $e.data('type') || type || 'watch', + count = $e.data('count') == 'true' || count || true; + + var $mainButton = $e.html(' ').find('.github-btn'), + $button = $mainButton.find('.btn'), + $text = $mainButton.find('.gh-text'), + $counter = $mainButton.find('.gh-count'); + + function addCommas(a) { + return String(a).replace(/(\d)(?=(\d{3})+$)/g, '$1,'); + } + + function callback(a) { + if (type == 'watch') { + $counter.html(addCommas(a.watchers)); + } else { + if (type == 'fork') { + $counter.html(addCommas(a.forks)); + } else { + if (type == 'follow') { + $counter.html(addCommas(a.followers)); + } + } + } + + if (count) { + $counter.css('display', 'inline-block'); + } + } + + function jsonp(url) { + var ctx = caches[url] || {}; + caches[url] = ctx; + if(ctx.onload || ctx.data){ + if(ctx.data){ + callback(ctx.data); + } else { + setTimeout(jsonp, 500, url); + } + }else{ + ctx.onload = true; + $.getJSON(url, function(a){ + ctx.onload = false; + ctx.data = a; + callback(a); + }); + } + } + + var urlBase = 'https://github.com/' + user + '/' + repo; + + $button.attr('href', urlBase + '/'); + + if (type == 'watch') { + $mainButton.addClass('github-watchers'); + $text.html('Star'); + $counter.attr('href', urlBase + '/stargazers'); + } else { + if (type == 'fork') { + $mainButton.addClass('github-forks'); + $text.html('Fork'); + $counter.attr('href', urlBase + '/network'); + } else { + if (type == 'follow') { + $mainButton.addClass('github-me'); + $text.html('@' + user); + $button.attr('href', 'https://github.com/' + user); + $counter.attr('href', 'https://github.com/' + user + '/followers'); + } + } + } + + if (type == 'follow') { + jsonp('https://api.github.com/users/' + user); + } else { + jsonp('https://api.github.com/repos/' + user + '/' + repo); + } + + }); + }; + + })(); +})(jQuery); + +jQuery(document).ready(function(){ + initializeJS(); + $('[rel=show-github]').showGithub(); +}); + diff --git a/docs/static/manifest.json b/docs/static/manifest.json deleted file mode 100644 index e671ac45a..000000000 --- a/docs/static/manifest.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "name": "Hugo", - "short_name": "Hugo", - "icons": [ - { - "src": "/android-chrome-36x36.png", - "sizes": "36x36", - "type": "image/png" - }, - { - "src": "/android-chrome-48x48.png", - "sizes": "48x48", - "type": "image/png" - }, - { - "src": "/android-chrome-72x72.png", - "sizes": "72x72", - "type": "image/png" - }, - { - "src": "/android-chrome-96x96.png", - "sizes": "96x96", - "type": "image/png" - }, - { - "src": "/android-chrome-144x144.png", - "sizes": "144x144", - "type": "image/png" - }, - { - "src": "/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/android-chrome-256x256.png", - "sizes": "256x256", - "type": "image/png" - } - ], - "start_url": "./?utm_source=web_app_manifest", - "theme_color": "#0A1922", - "background_color": "#FFF", - "display": "standalone" -} diff --git a/docs/static/mstile-144x144.png b/docs/static/mstile-144x144.png deleted file mode 100644 index e54b4bd75..000000000 Binary files a/docs/static/mstile-144x144.png and /dev/null differ diff --git a/docs/static/mstile-150x150.png b/docs/static/mstile-150x150.png deleted file mode 100644 index c7b84c690..000000000 Binary files a/docs/static/mstile-150x150.png and /dev/null differ diff --git a/docs/static/mstile-310x310.png b/docs/static/mstile-310x310.png deleted file mode 100644 index 2cde5c08c..000000000 Binary files a/docs/static/mstile-310x310.png and /dev/null differ diff --git a/docs/static/safari-pinned-tab.svg b/docs/static/safari-pinned-tab.svg deleted file mode 100644 index 80ff2dae3..000000000 --- a/docs/static/safari-pinned-tab.svg +++ /dev/null @@ -1,22 +0,0 @@ - - - - -Created by potrace 1.11, written by Peter Selinger 2001-2013 - - - - - diff --git a/docs/static/share/hugo-tall.png b/docs/static/share/hugo-tall.png new file mode 100644 index 000000000..001ce5eb3 Binary files /dev/null and b/docs/static/share/hugo-tall.png differ diff --git a/docs/static/share/made-with-hugo-dark.png b/docs/static/share/made-with-hugo-dark.png new file mode 100644 index 000000000..c6cadf283 Binary files /dev/null and b/docs/static/share/made-with-hugo-dark.png differ diff --git a/docs/static/share/made-with-hugo-long-dark.png b/docs/static/share/made-with-hugo-long-dark.png new file mode 100644 index 000000000..1e49995fb Binary files /dev/null and b/docs/static/share/made-with-hugo-long-dark.png differ diff --git a/docs/static/share/made-with-hugo-long.png b/docs/static/share/made-with-hugo-long.png new file mode 100644 index 000000000..c5df534cf Binary files /dev/null and b/docs/static/share/made-with-hugo-long.png differ diff --git a/docs/static/share/made-with-hugo.png b/docs/static/share/made-with-hugo.png new file mode 100644 index 000000000..52dfd19e5 Binary files /dev/null and b/docs/static/share/made-with-hugo.png differ diff --git a/docs/static/share/powered-by-hugo-dark.png b/docs/static/share/powered-by-hugo-dark.png new file mode 100644 index 000000000..a8e2ebc80 Binary files /dev/null and b/docs/static/share/powered-by-hugo-dark.png differ diff --git a/docs/static/share/powered-by-hugo-long-dark.png b/docs/static/share/powered-by-hugo-long-dark.png new file mode 100644 index 000000000..1b760b1bf Binary files /dev/null and b/docs/static/share/powered-by-hugo-long-dark.png differ diff --git a/docs/static/share/powered-by-hugo-long.png b/docs/static/share/powered-by-hugo-long.png new file mode 100644 index 000000000..37131359d Binary files /dev/null and b/docs/static/share/powered-by-hugo-long.png differ diff --git a/docs/static/share/powered-by-hugo.png b/docs/static/share/powered-by-hugo.png new file mode 100644 index 000000000..27ff099d5 Binary files /dev/null and b/docs/static/share/powered-by-hugo.png differ diff --git a/docs/static/shared/examples/data/books.json b/docs/static/shared/examples/data/books.json deleted file mode 100644 index ae2f36db2..000000000 --- a/docs/static/shared/examples/data/books.json +++ /dev/null @@ -1,55 +0,0 @@ -[ - { - "author": "Victor Hugo", - "cover": "https://gohugo.io/shared/examples/images/the-hunchback-of-notre-dame.webp", - "date": "2024-05-06", - "isbn": "978-0140443530", - "rating": 4, - "summary": "In the vaulted Gothic towers of **Notre-Dame Cathedral** lives Quasimodo, the hunchbacked bellringer. Mocked and shunned for his appearance, he is pitied only by Esmerelda, a beautiful gypsy dancer to whom he becomes completely devoted. Esmerelda, however, has also attracted the attention of the sinister archdeacon Claude Frollo, and when she rejects his lecherous approaches, Frollo hatches a plot to destroy her, that only Quasimodo can prevent. Victor Hugo's sensational, evocative novel brings life to the medieval Paris he loved, and mourns its passing in one of the greatest historical romances of the nineteenth century.", - "tags": [ - "fiction", - "historical" - ], - "title": "The Hunchback of Notre Dame" - }, - { - "author": "Victor Hugo", - "cover": "https://gohugo.io/shared/examples/images/les-misérables.webp", - "date": "2022-12-30", - "isbn": "978-0451419439", - "rating": 5, - "summary": "Introducing one of the most **famous characters** in literature, Jean Valjean—the noble peasant imprisoned for stealing a loaf of bread—Les Misérables ranks among the greatest novels of all time. In it, Victor Hugo takes readers deep into the Parisian underworld, immerses them in a battle between good and evil, and carries them to the barricades during the uprising of 1832 with a breathtaking realism that is unsurpassed in modern prose.", - "tags": [ - "fiction", - "historical", - "revolution" - ], - "title": "Les Misérables" - }, - { - "author": "Alexis de Tocqueville", - "cover": "https://gohugo.io/shared/examples/images/the-ancien-régime-and-the-revolution.webp", - "date": "2023-04-01", - "isbn": "978-0141441641", - "rating": 3, - "summary": "The Ancien Régime and the Revolution is a comparison of **revolutionary France** and the despotic rule it toppled. Alexis de Tocqueville (1805–59) is an objective observer of both periods – providing a merciless critique of the ancien régime, with its venality, oppression and inequality, yet acknowledging the reforms introduced under Louis XVI, and claiming that the post-Revolution state was in many ways as tyrannical as that of the King; its once lofty and egalitarian ideals corrupted and forgotten. Writing in the 1850s, Tocqueville wished to expose the return to despotism he witnessed in his own time under Napoleon III, by illuminating the grand, but ultimately doomed, call to liberty made by the French people in 1789. His eloquent and instructive study raises questions about liberty, nationalism and justice that remain urgent today.", - "tags": [ - "nonfiction", - "revolution" - ], - "title": "The Ancien Régime and the Revolution" - }, - { - "author": "François Furet", - "cover": "https://gohugo.io/shared/examples/images/interpreting-the-french-revolution.webp", - "date": "2024-01-12", - "isbn": "978-0521280495", - "rating": 5, - "summary": "The French Revolution is an historical event **unlike any other**. It is more than just a topic of intellectual interest: it has become part of a moral and political heritage. But after two centuries, this central event in French history has usually been thought of in much the same terms as it was by its contemporaries. There have been many accounts of the French Revolution, and though their opinions differ, they have often been commemorative or anniversary interpretations of the original event. The dividing line of revolutionary historiography, in intellectual terms, is therefore not between the right and the left, but between commemorative and conceptual history, as exemplified respectively in the works of Michelet and Tocquevifle. In this book, François Furet analyses how an event like the French Revolution can be conceptualised, and identifies the radically new changes the Revolution produced as well as the continuity it provided, albeit under the appearance of change. This question has become a riddle for the European left, answered neither by Marx nor by the theorists of our own century. In his analysis of the tragic relevance of the Revolution, Furet both refers to contemporary experience and discusses various elements in the work of Alexis de Tocclueville and that of Augustin Cochin, which has never been systematically applied by historians of the Revolution. Furet's book is based on the complementary ideas of these two writers in an attempt to cut through the apparent and misleading clarity of various contradictory views of the Revolution, and to help decipher some of the enigmatic problems of revolutionary ideology. It will be of value to historians of modern Europe and their students; to political, social and economic historians; to sociologists; and to students of political thought.", - "tags": [ - "nonfiction", - "revolution" - ], - "title": "Interpreting the French Revolution" - } -] diff --git a/docs/static/shared/examples/images/interpreting-the-french-revolution.webp b/docs/static/shared/examples/images/interpreting-the-french-revolution.webp deleted file mode 100644 index 4004b6613..000000000 Binary files a/docs/static/shared/examples/images/interpreting-the-french-revolution.webp and /dev/null differ diff --git a/docs/static/shared/examples/images/les-misérables.webp b/docs/static/shared/examples/images/les-misérables.webp deleted file mode 100644 index e336a5f16..000000000 Binary files a/docs/static/shared/examples/images/les-misérables.webp and /dev/null differ diff --git a/docs/static/shared/examples/images/the-ancien-régime-and-the-revolution.webp b/docs/static/shared/examples/images/the-ancien-régime-and-the-revolution.webp deleted file mode 100644 index f35183945..000000000 Binary files a/docs/static/shared/examples/images/the-ancien-régime-and-the-revolution.webp and /dev/null differ diff --git a/docs/static/shared/examples/images/the-hunchback-of-notre-dame.webp b/docs/static/shared/examples/images/the-hunchback-of-notre-dame.webp deleted file mode 100644 index f13e2224a..000000000 Binary files a/docs/static/shared/examples/images/the-hunchback-of-notre-dame.webp and /dev/null differ diff --git a/docs/static/vendor/OwlCarousel2/LICENSE b/docs/static/vendor/OwlCarousel2/LICENSE new file mode 100644 index 000000000..7162d578b --- /dev/null +++ b/docs/static/vendor/OwlCarousel2/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2014 Owl + +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. diff --git a/docs/static/vendor/OwlCarousel2/css/owl.carousel.css b/docs/static/vendor/OwlCarousel2/css/owl.carousel.css new file mode 100644 index 000000000..aaf80dd13 --- /dev/null +++ b/docs/static/vendor/OwlCarousel2/css/owl.carousel.css @@ -0,0 +1,179 @@ +/* + * Owl Carousel - Animate Plugin + */ +.owl-carousel .animated { + -webkit-animation-duration: 1000ms; + animation-duration: 1000ms; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; } +.owl-carousel .owl-animated-in { + z-index: 0; } +.owl-carousel .owl-animated-out { + z-index: 1; } +.owl-carousel .fadeOut { + -webkit-animation-name: fadeOut; + animation-name: fadeOut; } + +@-webkit-keyframes fadeOut { + 0% { + opacity: 1; } + + 100% { + opacity: 0; } } + +@keyframes fadeOut { + 0% { + opacity: 1; } + + 100% { + opacity: 0; } } + +/* + * Owl Carousel - Auto Height Plugin + */ +.owl-height { + -webkit-transition: height 500ms ease-in-out; + -moz-transition: height 500ms ease-in-out; + -ms-transition: height 500ms ease-in-out; + -o-transition: height 500ms ease-in-out; + transition: height 500ms ease-in-out; } + +/* + * Core Owl Carousel CSS File + */ +.owl-carousel { + display: none; + width: 100%; + -webkit-tap-highlight-color: transparent; + /* position relative and z-index fix webkit rendering fonts issue */ + position: relative; + z-index: 1; } + .owl-carousel .owl-stage { + position: relative; + -ms-touch-action: pan-Y; } + .owl-carousel .owl-stage:after { + content: "."; + display: block; + clear: both; + visibility: hidden; + line-height: 0; + height: 0; } + .owl-carousel .owl-stage-outer { + position: relative; + overflow: hidden; + /* fix for flashing background */ + -webkit-transform: translate3d(0px, 0px, 0px); } + .owl-carousel .owl-item { + position: relative; + min-height: 1px; + float: left; + -webkit-backface-visibility: hidden; + -webkit-tap-highlight-color: transparent; + -webkit-touch-callout: none; } + .owl-carousel .owl-item img { + display: block; + width: 100%; + -webkit-transform-style: preserve-3d; } + .owl-carousel .owl-nav.disabled, .owl-carousel .owl-dots.disabled { + display: none; } + .owl-carousel .owl-nav .owl-prev, .owl-carousel .owl-nav .owl-next, .owl-carousel .owl-dot { + cursor: pointer; + cursor: hand; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } + .owl-carousel.owl-loaded { + display: block; } + .owl-carousel.owl-loading { + opacity: 0; + display: block; } + .owl-carousel.owl-hidden { + opacity: 0; } + .owl-carousel.owl-refresh .owl-item { + display: none; } + .owl-carousel.owl-drag .owl-item { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } + .owl-carousel.owl-grab { + cursor: move; + cursor: -webkit-grab; + cursor: -o-grab; + cursor: -ms-grab; + cursor: grab; } + .owl-carousel.owl-rtl { + direction: rtl; } + .owl-carousel.owl-rtl .owl-item { + float: right; } + +/* No Js */ +.no-js .owl-carousel { + display: block; } + +/* + * Owl Carousel - Lazy Load Plugin + */ +.owl-carousel .owl-item .owl-lazy { + opacity: 0; + -webkit-transition: opacity 400ms ease; + -moz-transition: opacity 400ms ease; + -ms-transition: opacity 400ms ease; + -o-transition: opacity 400ms ease; + transition: opacity 400ms ease; } +.owl-carousel .owl-item img { + transform-style: preserve-3d; } + +/* + * Owl Carousel - Video Plugin + */ +.owl-carousel .owl-video-wrapper { + position: relative; + height: 100%; + background: #000; } +.owl-carousel .owl-video-play-icon { + position: absolute; + height: 80px; + width: 80px; + left: 50%; + top: 50%; + margin-left: -40px; + margin-top: -40px; + background: url("owl.video.play.png") no-repeat; + cursor: pointer; + z-index: 1; + -webkit-backface-visibility: hidden; + -webkit-transition: scale 100ms ease; + -moz-transition: scale 100ms ease; + -ms-transition: scale 100ms ease; + -o-transition: scale 100ms ease; + transition: scale 100ms ease; } +.owl-carousel .owl-video-play-icon:hover { + -webkit-transition: scale(1.3, 1.3); + -moz-transition: scale(1.3, 1.3); + -ms-transition: scale(1.3, 1.3); + -o-transition: scale(1.3, 1.3); + transition: scale(1.3, 1.3); } +.owl-carousel .owl-video-playing .owl-video-tn, .owl-carousel .owl-video-playing .owl-video-play-icon { + display: none; } +.owl-carousel .owl-video-tn { + opacity: 0; + height: 100%; + background-position: center center; + background-repeat: no-repeat; + -webkit-background-size: contain; + -moz-background-size: contain; + -o-background-size: contain; + background-size: contain; + -webkit-transition: opacity 400ms ease; + -moz-transition: opacity 400ms ease; + -ms-transition: opacity 400ms ease; + -o-transition: opacity 400ms ease; + transition: opacity 400ms ease; } +.owl-carousel .owl-video-frame { + position: relative; + z-index: 1; + height: 100%; + width: 100%; } diff --git a/docs/static/vendor/OwlCarousel2/css/owl.theme.default.css b/docs/static/vendor/OwlCarousel2/css/owl.theme.default.css new file mode 100644 index 000000000..dcd4c82ae --- /dev/null +++ b/docs/static/vendor/OwlCarousel2/css/owl.theme.default.css @@ -0,0 +1,51 @@ +/* + * Default theme - Owl Carousel CSS File + */ +.owl-theme .owl-nav { + margin-top: 10px; + text-align: center; + -webkit-tap-highlight-color: transparent; } + .owl-theme .owl-nav [class*='owl-'] { + color: #FFF; + font-size: 14px; + margin: 5px; + padding: 4px 7px; + background: #D6D6D6; + display: inline-block; + cursor: pointer; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; } + .owl-theme .owl-nav [class*='owl-']:hover { + background: #869791; + color: #FFF; + text-decoration: none; } + .owl-theme .owl-nav .disabled { + opacity: 0.5; + cursor: default; } +.owl-theme .owl-nav.disabled + .owl-dots { + margin-top: 10px; } +.owl-theme .owl-dots { + text-align: center; + -webkit-tap-highlight-color: transparent; } + .owl-theme .owl-dots .owl-dot { + display: inline-block; + zoom: 1; + *display: inline; } + .owl-theme .owl-dots .owl-dot span { + width: 10px; + height: 10px; + margin: 5px 7px; + background: #D6D6D6; + display: block; + -webkit-backface-visibility: visible; + -webkit-transition: opacity 200ms ease; + -moz-transition: opacity 200ms ease; + -ms-transition: opacity 200ms ease; + -o-transition: opacity 200ms ease; + transition: opacity 200ms ease; + -webkit-border-radius: 30px; + -moz-border-radius: 30px; + border-radius: 30px; } + .owl-theme .owl-dots .owl-dot.active span, .owl-theme .owl-dots .owl-dot:hover span { + background: #869791; } diff --git a/docs/static/vendor/OwlCarousel2/js/owl.carousel.min.js b/docs/static/vendor/OwlCarousel2/js/owl.carousel.min.js new file mode 100644 index 000000000..cd327896d --- /dev/null +++ b/docs/static/vendor/OwlCarousel2/js/owl.carousel.min.js @@ -0,0 +1,2 @@ +!function(a,b,c,d){function e(b,c){this.settings=null,this.options=a.extend({},e.Defaults,c),this.$element=a(b),this._handlers={},this._plugins={},this._supress={},this._current=null,this._speed=null,this._coordinates=[],this._breakpoint=null,this._width=null,this._items=[],this._clones=[],this._mergers=[],this._widths=[],this._invalidated={},this._pipe=[],this._drag={time:null,target:null,pointer:null,stage:{start:null,current:null},direction:null},this._states={current:{},tags:{initializing:["busy"],animating:["busy"],dragging:["interacting"]}},a.each(["onResize","onThrottledResize"],a.proxy(function(b,c){this._handlers[c]=a.proxy(this[c],this)},this)),a.each(e.Plugins,a.proxy(function(a,b){this._plugins[a.charAt(0).toLowerCase()+a.slice(1)]=new b(this)},this)),a.each(e.Workers,a.proxy(function(b,c){this._pipe.push({filter:c.filter,run:a.proxy(c.run,this)})},this)),this.setup(),this.initialize()}e.Defaults={items:3,loop:!1,center:!1,rewind:!1,mouseDrag:!0,touchDrag:!0,pullDrag:!0,freeDrag:!1,margin:0,stagePadding:0,merge:!1,mergeFit:!0,autoWidth:!1,startPosition:0,rtl:!1,smartSpeed:250,fluidSpeed:!1,dragEndSpeed:!1,responsive:{},responsiveRefreshRate:200,responsiveBaseElement:b,fallbackEasing:"swing",info:!1,nestedItemSelector:!1,itemElement:"div",stageElement:"div",refreshClass:"owl-refresh",loadedClass:"owl-loaded",loadingClass:"owl-loading",rtlClass:"owl-rtl",responsiveClass:"owl-responsive",dragClass:"owl-drag",itemClass:"owl-item",stageClass:"owl-stage",stageOuterClass:"owl-stage-outer",grabClass:"owl-grab"},e.Width={Default:"default",Inner:"inner",Outer:"outer"},e.Type={Event:"event",State:"state"},e.Plugins={},e.Workers=[{filter:["width","settings"],run:function(){this._width=this.$element.width()}},{filter:["width","items","settings"],run:function(a){a.current=this._items&&this._items[this.relative(this._current)]}},{filter:["items","settings"],run:function(){this.$stage.children(".cloned").remove()}},{filter:["width","items","settings"],run:function(a){var b=this.settings.margin||"",c=!this.settings.autoWidth,d=this.settings.rtl,e={width:"auto","margin-left":d?b:"","margin-right":d?"":b};!c&&this.$stage.children().css(e),a.css=e}},{filter:["width","items","settings"],run:function(a){var b=(this.width()/this.settings.items).toFixed(3)-this.settings.margin,c=null,d=this._items.length,e=!this.settings.autoWidth,f=[];for(a.items={merge:!1,width:b};d--;)c=this._mergers[d],c=this.settings.mergeFit&&Math.min(c,this.settings.items)||c,a.items.merge=c>1||a.items.merge,f[d]=e?b*c:this._items[d].width();this._widths=f}},{filter:["items","settings"],run:function(){var b=[],c=this._items,d=this.settings,e=Math.max(2*d.items,4),f=2*Math.ceil(c.length/2),g=d.loop&&c.length?d.rewind?e:Math.max(e,f):0,h="",i="";for(g/=2;g--;)b.push(this.normalize(b.length/2,!0)),h+=c[b[b.length-1]][0].outerHTML,b.push(this.normalize(c.length-1-(b.length-1)/2,!0)),i=c[b[b.length-1]][0].outerHTML+i;this._clones=b,a(h).addClass("cloned").appendTo(this.$stage),a(i).addClass("cloned").prependTo(this.$stage)}},{filter:["width","items","settings"],run:function(){for(var a=this.settings.rtl?1:-1,b=this._clones.length+this._items.length,c=-1,d=0,e=0,f=[];++cc;c++)a=this._coordinates[c-1]||0,b=Math.abs(this._coordinates[c])+f*e,(this.op(a,"<=",g)&&this.op(a,">",h)||this.op(b,"<",g)&&this.op(b,">",h))&&i.push(c);this.$stage.children(".active").removeClass("active"),this.$stage.children(":eq("+i.join("), :eq(")+")").addClass("active"),this.settings.center&&(this.$stage.children(".center").removeClass("center"),this.$stage.children().eq(this.current()).addClass("center"))}}],e.prototype.initialize=function(){if(this.enter("initializing"),this.trigger("initialize"),this.$element.toggleClass(this.settings.rtlClass,this.settings.rtl),this.settings.autoWidth&&!this.is("pre-loading")){var b,c,e;b=this.$element.find("img"),c=this.settings.nestedItemSelector?"."+this.settings.nestedItemSelector:d,e=this.$element.children(c).width(),b.length&&0>=e&&this.preloadAutoWidthImages(b)}this.$element.addClass(this.options.loadingClass),this.$stage=a("<"+this.settings.stageElement+' class="'+this.settings.stageClass+'"/>').wrap('
    '),this.$element.append(this.$stage.parent()),this.replace(this.$element.children().not(this.$stage.parent())),this.$element.is(":visible")?this.refresh():this.invalidate("width"),this.$element.removeClass(this.options.loadingClass).addClass(this.options.loadedClass),this.registerEventHandlers(),this.leave("initializing"),this.trigger("initialized")},e.prototype.setup=function(){var b=this.viewport(),c=this.options.responsive,d=-1,e=null;c?(a.each(c,function(a){b>=a&&a>d&&(d=Number(a))}),e=a.extend({},this.options,c[d]),delete e.responsive,e.responsiveClass&&this.$element.attr("class",this.$element.attr("class").replace(new RegExp("("+this.options.responsiveClass+"-)\\S+\\s","g"),"$1"+d))):e=a.extend({},this.options),(null===this.settings||this._breakpoint!==d)&&(this.trigger("change",{property:{name:"settings",value:e}}),this._breakpoint=d,this.settings=e,this.invalidate("settings"),this.trigger("changed",{property:{name:"settings",value:this.settings}}))},e.prototype.optionsLogic=function(){this.settings.autoWidth&&(this.settings.stagePadding=!1,this.settings.merge=!1)},e.prototype.prepare=function(b){var c=this.trigger("prepare",{content:b});return c.data||(c.data=a("<"+this.settings.itemElement+"/>").addClass(this.options.itemClass).append(b)),this.trigger("prepared",{content:c.data}),c.data},e.prototype.update=function(){for(var b=0,c=this._pipe.length,d=a.proxy(function(a){return this[a]},this._invalidated),e={};c>b;)(this._invalidated.all||a.grep(this._pipe[b].filter,d).length>0)&&this._pipe[b].run(e),b++;this._invalidated={},!this.is("valid")&&this.enter("valid")},e.prototype.width=function(a){switch(a=a||e.Width.Default){case e.Width.Inner:case e.Width.Outer:return this._width;default:return this._width-2*this.settings.stagePadding+this.settings.margin}},e.prototype.refresh=function(){this.enter("refreshing"),this.trigger("refresh"),this.setup(),this.optionsLogic(),this.$element.addClass(this.options.refreshClass),this.update(),this.$element.removeClass(this.options.refreshClass),this.leave("refreshing"),this.trigger("refreshed")},e.prototype.onThrottledResize=function(){b.clearTimeout(this.resizeTimer),this.resizeTimer=b.setTimeout(this._handlers.onResize,this.settings.responsiveRefreshRate)},e.prototype.onResize=function(){return this._items.length?this._width===this.$element.width()?!1:this.$element.is(":visible")?(this.enter("resizing"),this.trigger("resize").isDefaultPrevented()?(this.leave("resizing"),!1):(this.invalidate("width"),this.refresh(),this.leave("resizing"),void this.trigger("resized"))):!1:!1},e.prototype.registerEventHandlers=function(){a.support.transition&&this.$stage.on(a.support.transition.end+".owl.core",a.proxy(this.onTransitionEnd,this)),this.settings.responsive!==!1&&this.on(b,"resize",this._handlers.onThrottledResize),this.settings.mouseDrag&&(this.$element.addClass(this.options.dragClass),this.$stage.on("mousedown.owl.core",a.proxy(this.onDragStart,this)),this.$stage.on("dragstart.owl.core selectstart.owl.core",function(){return!1})),this.settings.touchDrag&&(this.$stage.on("touchstart.owl.core",a.proxy(this.onDragStart,this)),this.$stage.on("touchcancel.owl.core",a.proxy(this.onDragEnd,this)))},e.prototype.onDragStart=function(b){var d=null;3!==b.which&&(a.support.transform?(d=this.$stage.css("transform").replace(/.*\(|\)| /g,"").split(","),d={x:d[16===d.length?12:4],y:d[16===d.length?13:5]}):(d=this.$stage.position(),d={x:this.settings.rtl?d.left+this.$stage.width()-this.width()+this.settings.margin:d.left,y:d.top}),this.is("animating")&&(a.support.transform?this.animate(d.x):this.$stage.stop(),this.invalidate("position")),this.$element.toggleClass(this.options.grabClass,"mousedown"===b.type),this.speed(0),this._drag.time=(new Date).getTime(),this._drag.target=a(b.target),this._drag.stage.start=d,this._drag.stage.current=d,this._drag.pointer=this.pointer(b),a(c).on("mouseup.owl.core touchend.owl.core",a.proxy(this.onDragEnd,this)),a(c).one("mousemove.owl.core touchmove.owl.core",a.proxy(function(b){var d=this.difference(this._drag.pointer,this.pointer(b));a(c).on("mousemove.owl.core touchmove.owl.core",a.proxy(this.onDragMove,this)),Math.abs(d.x)0^this.settings.rtl?"left":"right";a(c).off(".owl.core"),this.$element.removeClass(this.options.grabClass),(0!==d.x&&this.is("dragging")||!this.is("valid"))&&(this.speed(this.settings.dragEndSpeed||this.settings.smartSpeed),this.current(this.closest(e.x,0!==d.x?f:this._drag.direction)),this.invalidate("position"),this.update(),this._drag.direction=f,(Math.abs(d.x)>3||(new Date).getTime()-this._drag.time>300)&&this._drag.target.one("click.owl.core",function(){return!1})),this.is("dragging")&&(this.leave("dragging"),this.trigger("dragged"))},e.prototype.closest=function(b,c){var d=-1,e=30,f=this.width(),g=this.coordinates();return this.settings.freeDrag||a.each(g,a.proxy(function(a,h){return b>h-e&&h+e>b?d=a:this.op(b,"<",h)&&this.op(b,">",g[a+1]||h-f)&&(d="left"===c?a+1:a),-1===d},this)),this.settings.loop||(this.op(b,">",g[this.minimum()])?d=b=this.minimum():this.op(b,"<",g[this.maximum()])&&(d=b=this.maximum())),d},e.prototype.animate=function(b){var c=this.speed()>0;this.is("animating")&&this.onTransitionEnd(),c&&(this.enter("animating"),this.trigger("translate")),a.support.transform3d&&a.support.transition?this.$stage.css({transform:"translate3d("+b+"px,0px,0px)",transition:this.speed()/1e3+"s"}):c?this.$stage.animate({left:b+"px"},this.speed(),this.settings.fallbackEasing,a.proxy(this.onTransitionEnd,this)):this.$stage.css({left:b+"px"})},e.prototype.is=function(a){return this._states.current[a]&&this._states.current[a]>0},e.prototype.current=function(a){if(a===d)return this._current;if(0===this._items.length)return d;if(a=this.normalize(a),this._current!==a){var b=this.trigger("change",{property:{name:"position",value:a}});b.data!==d&&(a=this.normalize(b.data)),this._current=a,this.invalidate("position"),this.trigger("changed",{property:{name:"position",value:this._current}})}return this._current},e.prototype.invalidate=function(b){return"string"===a.type(b)&&(this._invalidated[b]=!0,this.is("valid")&&this.leave("valid")),a.map(this._invalidated,function(a,b){return b})},e.prototype.reset=function(a){a=this.normalize(a),a!==d&&(this._speed=0,this._current=a,this.suppress(["translate","translated"]),this.animate(this.coordinates(a)),this.release(["translate","translated"]))},e.prototype.normalize=function(b,c){var e=this._items.length,f=c?0:this._clones.length;return!a.isNumeric(b)||1>e?b=d:(0>b||b>=e+f)&&(b=((b-f/2)%e+e)%e+f/2),b},e.prototype.relative=function(a){return a-=this._clones.length/2,this.normalize(a,!0)},e.prototype.maximum=function(a){var b,c=this.settings,d=this._coordinates.length,e=Math.abs(this._coordinates[d-1])-this._width,f=-1;if(c.loop)d=this._clones.length/2+this._items.length-1;else if(c.autoWidth||c.merge)for(;d-f>1;)Math.abs(this._coordinates[b=d+f>>1])0)-(0>e),g=this._items.length,h=this.minimum(),i=this.maximum();this.settings.loop?(!this.settings.rewind&&Math.abs(e)>g/2&&(e+=-1*f*g),a=c+e,d=((a-h)%g+g)%g+h,d!==a&&i>=d-e&&d-e>0&&(c=d-e,a=d,this.reset(c))):this.settings.rewind?(i+=1,a=(a%i+i)%i):a=Math.max(h,Math.min(i,a)),this.speed(this.duration(c,a,b)),this.current(a),this.$element.is(":visible")&&this.update()},e.prototype.next=function(a){a=a||!1,this.to(this.relative(this.current())+1,a)},e.prototype.prev=function(a){a=a||!1,this.to(this.relative(this.current())-1,a)},e.prototype.onTransitionEnd=function(a){return a!==d&&(a.stopPropagation(),(a.target||a.srcElement||a.originalTarget)!==this.$stage.get(0))?!1:(this.leave("animating"),void this.trigger("translated"))},e.prototype.viewport=function(){var d;if(this.options.responsiveBaseElement!==b)d=a(this.options.responsiveBaseElement).width();else if(b.innerWidth)d=b.innerWidth;else{if(!c.documentElement||!c.documentElement.clientWidth)throw"Can not detect viewport width.";d=c.documentElement.clientWidth}return d},e.prototype.replace=function(b){this.$stage.empty(),this._items=[],b&&(b=b instanceof jQuery?b:a(b)),this.settings.nestedItemSelector&&(b=b.find("."+this.settings.nestedItemSelector)),b.filter(function(){return 1===this.nodeType}).each(a.proxy(function(a,b){b=this.prepare(b),this.$stage.append(b),this._items.push(b),this._mergers.push(1*b.find("[data-merge]").andSelf("[data-merge]").attr("data-merge")||1)},this)),this.reset(a.isNumeric(this.settings.startPosition)?this.settings.startPosition:0),this.invalidate("items")},e.prototype.add=function(b,c){var e=this.relative(this._current);c=c===d?this._items.length:this.normalize(c,!0),b=b instanceof jQuery?b:a(b),this.trigger("add",{content:b,position:c}),b=this.prepare(b),0===this._items.length||c===this._items.length?(0===this._items.length&&this.$stage.append(b),0!==this._items.length&&this._items[c-1].after(b),this._items.push(b),this._mergers.push(1*b.find("[data-merge]").andSelf("[data-merge]").attr("data-merge")||1)):(this._items[c].before(b),this._items.splice(c,0,b),this._mergers.splice(c,0,1*b.find("[data-merge]").andSelf("[data-merge]").attr("data-merge")||1)),this._items[e]&&this.reset(this._items[e].index()),this.invalidate("items"),this.trigger("added",{content:b,position:c})},e.prototype.remove=function(a){a=this.normalize(a,!0),a!==d&&(this.trigger("remove",{content:this._items[a],position:a}),this._items[a].remove(),this._items.splice(a,1),this._mergers.splice(a,1),this.invalidate("items"),this.trigger("removed",{content:null,position:a}))},e.prototype.preloadAutoWidthImages=function(b){b.each(a.proxy(function(b,c){this.enter("pre-loading"),c=a(c),a(new Image).one("load",a.proxy(function(a){c.attr("src",a.target.src),c.css("opacity",1),this.leave("pre-loading"),!this.is("pre-loading")&&!this.is("initializing")&&this.refresh()},this)).attr("src",c.attr("src")||c.attr("data-src")||c.attr("data-src-retina"))},this))},e.prototype.destroy=function(){this.$element.off(".owl.core"),this.$stage.off(".owl.core"),a(c).off(".owl.core"),this.settings.responsive!==!1&&(b.clearTimeout(this.resizeTimer),this.off(b,"resize",this._handlers.onThrottledResize));for(var d in this._plugins)this._plugins[d].destroy();this.$stage.children(".cloned").remove(),this.$stage.unwrap(),this.$stage.children().contents().unwrap(),this.$stage.children().unwrap(),this.$element.removeClass(this.options.refreshClass).removeClass(this.options.loadingClass).removeClass(this.options.loadedClass).removeClass(this.options.rtlClass).removeClass(this.options.dragClass).removeClass(this.options.grabClass).attr("class",this.$element.attr("class").replace(new RegExp(this.options.responsiveClass+"-\\S+\\s","g"),"")).removeData("owl.carousel")},e.prototype.op=function(a,b,c){var d=this.settings.rtl;switch(b){case"<":return d?a>c:c>a;case">":return d?c>a:a>c;case">=":return d?c>=a:a>=c;case"<=":return d?a>=c:c>=a}},e.prototype.on=function(a,b,c,d){a.addEventListener?a.addEventListener(b,c,d):a.attachEvent&&a.attachEvent("on"+b,c)},e.prototype.off=function(a,b,c,d){a.removeEventListener?a.removeEventListener(b,c,d):a.detachEvent&&a.detachEvent("on"+b,c)},e.prototype.trigger=function(b,c,d,f,g){var h={item:{count:this._items.length,index:this.current()}},i=a.camelCase(a.grep(["on",b,d],function(a){return a}).join("-").toLowerCase()),j=a.Event([b,"owl",d||"carousel"].join(".").toLowerCase(),a.extend({relatedTarget:this},h,c));return this._supress[b]||(a.each(this._plugins,function(a,b){b.onTrigger&&b.onTrigger(j)}),this.register({type:e.Type.Event,name:b}),this.$element.trigger(j),this.settings&&"function"==typeof this.settings[i]&&this.settings[i].call(this,j)),j},e.prototype.enter=function(b){a.each([b].concat(this._states.tags[b]||[]),a.proxy(function(a,b){this._states.current[b]===d&&(this._states.current[b]=0),this._states.current[b]++},this))},e.prototype.leave=function(b){a.each([b].concat(this._states.tags[b]||[]),a.proxy(function(a,b){this._states.current[b]--},this))},e.prototype.register=function(b){if(b.type===e.Type.Event){if(a.event.special[b.name]||(a.event.special[b.name]={}),!a.event.special[b.name].owl){var c=a.event.special[b.name]._default;a.event.special[b.name]._default=function(a){return!c||!c.apply||a.namespace&&-1!==a.namespace.indexOf("owl")?a.namespace&&a.namespace.indexOf("owl")>-1:c.apply(this,arguments)},a.event.special[b.name].owl=!0}}else b.type===e.Type.State&&(this._states.tags[b.name]?this._states.tags[b.name]=this._states.tags[b.name].concat(b.tags):this._states.tags[b.name]=b.tags,this._states.tags[b.name]=a.grep(this._states.tags[b.name],a.proxy(function(c,d){return a.inArray(c,this._states.tags[b.name])===d},this)))},e.prototype.suppress=function(b){a.each(b,a.proxy(function(a,b){this._supress[b]=!0},this))},e.prototype.release=function(b){a.each(b,a.proxy(function(a,b){delete this._supress[b]},this))},e.prototype.pointer=function(a){var c={x:null,y:null};return a=a.originalEvent||a||b.event,a=a.touches&&a.touches.length?a.touches[0]:a.changedTouches&&a.changedTouches.length?a.changedTouches[0]:a,a.pageX?(c.x=a.pageX,c.y=a.pageY):(c.x=a.clientX,c.y=a.clientY),c},e.prototype.difference=function(a,b){return{x:a.x-b.x,y:a.y-b.y}},a.fn.owlCarousel=function(b){var c=Array.prototype.slice.call(arguments,1);return this.each(function(){var d=a(this),f=d.data("owl.carousel");f||(f=new e(this,"object"==typeof b&&b),d.data("owl.carousel",f),a.each(["next","prev","to","destroy","refresh","replace","add","remove"],function(b,c){f.register({type:e.Type.Event,name:c}),f.$element.on(c+".owl.carousel.core",a.proxy(function(a){a.namespace&&a.relatedTarget!==this&&(this.suppress([c]),f[c].apply(this,[].slice.call(arguments,1)),this.release([c]))},f))})),"string"==typeof b&&"_"!==b.charAt(0)&&f[b].apply(f,c)})},a.fn.owlCarousel.Constructor=e}(window.Zepto||window.jQuery,window,document),function(a,b,c,d){var e=function(b){this._core=b,this._interval=null,this._visible=null,this._handlers={"initialized.owl.carousel":a.proxy(function(a){a.namespace&&this._core.settings.autoRefresh&&this.watch()},this)},this._core.options=a.extend({},e.Defaults,this._core.options),this._core.$element.on(this._handlers)};e.Defaults={autoRefresh:!0,autoRefreshInterval:500},e.prototype.watch=function(){this._interval||(this._visible=this._core.$element.is(":visible"),this._interval=b.setInterval(a.proxy(this.refresh,this),this._core.settings.autoRefreshInterval))},e.prototype.refresh=function(){this._core.$element.is(":visible")!==this._visible&&(this._visible=!this._visible,this._core.$element.toggleClass("owl-hidden",!this._visible),this._visible&&this._core.invalidate("width")&&this._core.refresh())},e.prototype.destroy=function(){var a,c;b.clearInterval(this._interval);for(a in this._handlers)this._core.$element.off(a,this._handlers[a]);for(c in Object.getOwnPropertyNames(this))"function"!=typeof this[c]&&(this[c]=null)},a.fn.owlCarousel.Constructor.Plugins.AutoRefresh=e}(window.Zepto||window.jQuery,window,document),function(a,b,c,d){var e=function(b){this._core=b,this._loaded=[],this._handlers={"initialized.owl.carousel change.owl.carousel":a.proxy(function(b){if(b.namespace&&this._core.settings&&this._core.settings.lazyLoad&&(b.property&&"position"==b.property.name||"initialized"==b.type))for(var c=this._core.settings,d=c.center&&Math.ceil(c.items/2)||c.items,e=c.center&&-1*d||0,f=(b.property&&b.property.value||this._core.current())+e,g=this._core.clones().length,h=a.proxy(function(a,b){this.load(b)},this);e++-1||(e.each(a.proxy(function(c,d){var e,f=a(d),g=b.devicePixelRatio>1&&f.attr("data-src-retina")||f.attr("data-src");this._core.trigger("load",{element:f,url:g},"lazy"),f.is("img")?f.one("load.owl.lazy",a.proxy(function(){f.css("opacity",1),this._core.trigger("loaded",{element:f,url:g},"lazy")},this)).attr("src",g):(e=new Image,e.onload=a.proxy(function(){f.css({"background-image":"url("+g+")",opacity:"1"}),this._core.trigger("loaded",{element:f,url:g},"lazy")},this),e.src=g)},this)),this._loaded.push(d.get(0)))},e.prototype.destroy=function(){var a,b;for(a in this.handlers)this._core.$element.off(a,this.handlers[a]);for(b in Object.getOwnPropertyNames(this))"function"!=typeof this[b]&&(this[b]=null)},a.fn.owlCarousel.Constructor.Plugins.Lazy=e}(window.Zepto||window.jQuery,window,document),function(a,b,c,d){var e=function(b){this._core=b,this._handlers={"initialized.owl.carousel refreshed.owl.carousel":a.proxy(function(a){a.namespace&&this._core.settings.autoHeight&&this.update()},this),"changed.owl.carousel":a.proxy(function(a){a.namespace&&this._core.settings.autoHeight&&"position"==a.property.name&&this.update()},this),"loaded.owl.lazy":a.proxy(function(a){a.namespace&&this._core.settings.autoHeight&&a.element.closest("."+this._core.settings.itemClass).index()===this._core.current()&&this.update()},this)},this._core.options=a.extend({},e.Defaults,this._core.options),this._core.$element.on(this._handlers)};e.Defaults={autoHeight:!1,autoHeightClass:"owl-height"},e.prototype.update=function(){var b=this._core._current,c=b+this._core.settings.items,d=this._core.$stage.children().toArray().slice(b,c);heights=[],maxheight=0,a.each(d,function(b,c){heights.push(a(c).height())}),maxheight=Math.max.apply(null,heights),this._core.$stage.parent().height(maxheight).addClass(this._core.settings.autoHeightClass)},e.prototype.destroy=function(){var a,b;for(a in this._handlers)this._core.$element.off(a,this._handlers[a]);for(b in Object.getOwnPropertyNames(this))"function"!=typeof this[b]&&(this[b]=null)},a.fn.owlCarousel.Constructor.Plugins.AutoHeight=e}(window.Zepto||window.jQuery,window,document),function(a,b,c,d){var e=function(b){this._core=b,this._videos={},this._playing=null,this._handlers={"initialized.owl.carousel":a.proxy(function(a){a.namespace&&this._core.register({type:"state",name:"playing",tags:["interacting"]})},this),"resize.owl.carousel":a.proxy(function(a){a.namespace&&this._core.settings.video&&this.isInFullScreen()&&a.preventDefault()},this),"refreshed.owl.carousel":a.proxy(function(a){a.namespace&&this._core.is("resizing")&&this._core.$stage.find(".cloned .owl-video-frame").remove()},this),"changed.owl.carousel":a.proxy(function(a){a.namespace&&"position"===a.property.name&&this._playing&&this.stop()},this),"prepared.owl.carousel":a.proxy(function(b){if(b.namespace){var c=a(b.content).find(".owl-video");c.length&&(c.css("display","none"),this.fetch(c,a(b.content)))}},this)},this._core.options=a.extend({},e.Defaults,this._core.options),this._core.$element.on(this._handlers),this._core.$element.on("click.owl.video",".owl-video-play-icon",a.proxy(function(a){this.play(a)},this))};e.Defaults={video:!1,videoHeight:!1,videoWidth:!1},e.prototype.fetch=function(a,b){var c=a.attr("data-vimeo-id")?"vimeo":"youtube",d=a.attr("data-vimeo-id")||a.attr("data-youtube-id"),e=a.attr("data-width")||this._core.settings.videoWidth,f=a.attr("data-height")||this._core.settings.videoHeight,g=a.attr("href");if(!g)throw new Error("Missing video URL.");if(d=g.match(/(http:|https:|)\/\/(player.|www.)?(vimeo\.com|youtu(be\.com|\.be|be\.googleapis\.com))\/(video\/|embed\/|watch\?v=|v\/)?([A-Za-z0-9._%-]*)(\&\S+)?/),d[3].indexOf("youtu")>-1)c="youtube";else{if(!(d[3].indexOf("vimeo")>-1))throw new Error("Video URL not supported.");c="vimeo"}d=d[6],this._videos[g]={type:c,id:d,width:e,height:f},b.attr("data-video",g),this.thumbnail(a,this._videos[g])},e.prototype.thumbnail=function(b,c){var d,e,f,g=c.width&&c.height?'style="width:'+c.width+"px;height:"+c.height+'px;"':"",h=b.find("img"),i="src",j="",k=this._core.settings,l=function(a){e='
    ',d=k.lazyLoad?'
    ':'
    ',b.after(d),b.after(e)};return b.wrap('
    "),this._core.settings.lazyLoad&&(i="data-src",j="owl-lazy"),h.length?(l(h.attr(i)),h.remove(),!1):void("youtube"===c.type?(f="http://img.youtube.com/vi/"+c.id+"/hqdefault.jpg",l(f)):"vimeo"===c.type&&a.ajax({type:"GET",url:"http://vimeo.com/api/v2/video/"+c.id+".json",jsonp:"callback",dataType:"jsonp",success:function(a){f=a[0].thumbnail_large,l(f)}}))},e.prototype.stop=function(){this._core.trigger("stop",null,"video"),this._playing.find(".owl-video-frame").remove(),this._playing.removeClass("owl-video-playing"),this._playing=null,this._core.leave("playing"),this._core.trigger("stopped",null,"video")},e.prototype.play=function(b){var c,d=a(b.target),e=d.closest("."+this._core.settings.itemClass),f=this._videos[e.attr("data-video")],g=f.width||"100%",h=f.height||this._core.$stage.height();this._playing||(this._core.enter("playing"),this._core.trigger("play",null,"video"),e=this._core.items(this._core.relative(e.index())),this._core.reset(e.index()),"youtube"===f.type?c='':"vimeo"===f.type&&(c=''),a('
    '+c+"
    ").insertAfter(e.find(".owl-video")),this._playing=e.addClass("owl-video-playing"))},e.prototype.isInFullScreen=function(){var b=c.fullscreenElement||c.mozFullScreenElement||c.webkitFullscreenElement;return b&&a(b).parent().hasClass("owl-video-frame")},e.prototype.destroy=function(){var a,b;this._core.$element.off("click.owl.video");for(a in this._handlers)this._core.$element.off(a,this._handlers[a]);for(b in Object.getOwnPropertyNames(this))"function"!=typeof this[b]&&(this[b]=null)},a.fn.owlCarousel.Constructor.Plugins.Video=e}(window.Zepto||window.jQuery,window,document),function(a,b,c,d){var e=function(b){this.core=b,this.core.options=a.extend({},e.Defaults,this.core.options),this.swapping=!0,this.previous=d,this.next=d,this.handlers={"change.owl.carousel":a.proxy(function(a){a.namespace&&"position"==a.property.name&&(this.previous=this.core.current(),this.next=a.property.value)},this),"drag.owl.carousel dragged.owl.carousel translated.owl.carousel":a.proxy(function(a){a.namespace&&(this.swapping="translated"==a.type)},this),"translate.owl.carousel":a.proxy(function(a){a.namespace&&this.swapping&&(this.core.options.animateOut||this.core.options.animateIn)&&this.swap()},this)},this.core.$element.on(this.handlers)};e.Defaults={animateOut:!1,animateIn:!1},e.prototype.swap=function(){if(1===this.core.settings.items&&a.support.animation&&a.support.transition){this.core.speed(0);var b,c=a.proxy(this.clear,this),d=this.core.$stage.children().eq(this.previous),e=this.core.$stage.children().eq(this.next),f=this.core.settings.animateIn,g=this.core.settings.animateOut;this.core.current()!==this.previous&&(g&&(b=this.core.coordinates(this.previous)-this.core.coordinates(this.next),d.one(a.support.animation.end,c).css({left:b+"px"}).addClass("animated owl-animated-out").addClass(g)),f&&e.one(a.support.animation.end,c).addClass("animated owl-animated-in").addClass(f))}},e.prototype.clear=function(b){a(b.target).css({left:""}).removeClass("animated owl-animated-out owl-animated-in").removeClass(this.core.settings.animateIn).removeClass(this.core.settings.animateOut),this.core.onTransitionEnd()},e.prototype.destroy=function(){var a,b;for(a in this.handlers)this.core.$element.off(a,this.handlers[a]);for(b in Object.getOwnPropertyNames(this))"function"!=typeof this[b]&&(this[b]=null)},a.fn.owlCarousel.Constructor.Plugins.Animate=e}(window.Zepto||window.jQuery,window,document),function(a,b,c,d){var e=function(b){this._core=b,this._interval=null,this._paused=!1,this._handlers={"changed.owl.carousel":a.proxy(function(a){a.namespace&&"settings"===a.property.name&&(this._core.settings.autoplay?this.play():this.stop())},this),"initialized.owl.carousel":a.proxy(function(a){a.namespace&&this._core.settings.autoplay&&this.play()},this),"play.owl.autoplay":a.proxy(function(a,b,c){a.namespace&&this.play(b,c)},this),"stop.owl.autoplay":a.proxy(function(a){a.namespace&&this.stop()},this),"mouseover.owl.autoplay":a.proxy(function(){this._core.settings.autoplayHoverPause&&this._core.is("rotating")&&this.pause()},this),"mouseleave.owl.autoplay":a.proxy(function(){ +this._core.settings.autoplayHoverPause&&this._core.is("rotating")&&this.play()},this)},this._core.$element.on(this._handlers),this._core.options=a.extend({},e.Defaults,this._core.options)};e.Defaults={autoplay:!1,autoplayTimeout:5e3,autoplayHoverPause:!1,autoplaySpeed:!1},e.prototype.play=function(d,e){this._paused=!1,this._core.is("rotating")||(this._core.enter("rotating"),this._interval=b.setInterval(a.proxy(function(){this._paused||this._core.is("busy")||this._core.is("interacting")||c.hidden||this._core.next(e||this._core.settings.autoplaySpeed)},this),d||this._core.settings.autoplayTimeout))},e.prototype.stop=function(){this._core.is("rotating")&&(b.clearInterval(this._interval),this._core.leave("rotating"))},e.prototype.pause=function(){this._core.is("rotating")&&(this._paused=!0)},e.prototype.destroy=function(){var a,b;this.stop();for(a in this._handlers)this._core.$element.off(a,this._handlers[a]);for(b in Object.getOwnPropertyNames(this))"function"!=typeof this[b]&&(this[b]=null)},a.fn.owlCarousel.Constructor.Plugins.autoplay=e}(window.Zepto||window.jQuery,window,document),function(a,b,c,d){"use strict";var e=function(b){this._core=b,this._initialized=!1,this._pages=[],this._controls={},this._templates=[],this.$element=this._core.$element,this._overrides={next:this._core.next,prev:this._core.prev,to:this._core.to},this._handlers={"prepared.owl.carousel":a.proxy(function(b){b.namespace&&this._core.settings.dotsData&&this._templates.push('
    '+a(b.content).find("[data-dot]").andSelf("[data-dot]").attr("data-dot")+"
    ")},this),"added.owl.carousel":a.proxy(function(a){a.namespace&&this._core.settings.dotsData&&this._templates.splice(a.position,0,this._templates.pop())},this),"remove.owl.carousel":a.proxy(function(a){a.namespace&&this._core.settings.dotsData&&this._templates.splice(a.position,1)},this),"changed.owl.carousel":a.proxy(function(a){a.namespace&&"position"==a.property.name&&this.draw()},this),"initialized.owl.carousel":a.proxy(function(a){a.namespace&&!this._initialized&&(this._core.trigger("initialize",null,"navigation"),this.initialize(),this.update(),this.draw(),this._initialized=!0,this._core.trigger("initialized",null,"navigation"))},this),"refreshed.owl.carousel":a.proxy(function(a){a.namespace&&this._initialized&&(this._core.trigger("refresh",null,"navigation"),this.update(),this.draw(),this._core.trigger("refreshed",null,"navigation"))},this)},this._core.options=a.extend({},e.Defaults,this._core.options),this.$element.on(this._handlers)};e.Defaults={nav:!1,navText:["prev","next"],navSpeed:!1,navElement:"div",navContainer:!1,navContainerClass:"owl-nav",navClass:["owl-prev","owl-next"],slideBy:1,dotClass:"owl-dot",dotsClass:"owl-dots",dots:!0,dotsEach:!1,dotsData:!1,dotsSpeed:!1,dotsContainer:!1},e.prototype.initialize=function(){var b,c=this._core.settings;this._controls.$relative=(c.navContainer?a(c.navContainer):a("
    ").addClass(c.navContainerClass).appendTo(this.$element)).addClass("disabled"),this._controls.$previous=a("<"+c.navElement+">").addClass(c.navClass[0]).html(c.navText[0]).prependTo(this._controls.$relative).on("click",a.proxy(function(a){this.prev(c.navSpeed)},this)),this._controls.$next=a("<"+c.navElement+">").addClass(c.navClass[1]).html(c.navText[1]).appendTo(this._controls.$relative).on("click",a.proxy(function(a){this.next(c.navSpeed)},this)),c.dotsData||(this._templates=[a("
    ").addClass(c.dotClass).append(a("")).prop("outerHTML")]),this._controls.$absolute=(c.dotsContainer?a(c.dotsContainer):a("
    ").addClass(c.dotsClass).appendTo(this.$element)).addClass("disabled"),this._controls.$absolute.on("click","div",a.proxy(function(b){var d=a(b.target).parent().is(this._controls.$absolute)?a(b.target).index():a(b.target).parent().index();b.preventDefault(),this.to(d,c.dotsSpeed)},this));for(b in this._overrides)this._core[b]=a.proxy(this[b],this)},e.prototype.destroy=function(){var a,b,c,d;for(a in this._handlers)this.$element.off(a,this._handlers[a]);for(b in this._controls)this._controls[b].remove();for(d in this.overides)this._core[d]=this._overrides[d];for(c in Object.getOwnPropertyNames(this))"function"!=typeof this[c]&&(this[c]=null)},e.prototype.update=function(){var a,b,c,d=this._core.clones().length/2,e=d+this._core.items().length,f=this._core.maximum(!0),g=this._core.settings,h=g.center||g.autoWidth||g.dotsData?1:g.dotsEach||g.items;if("page"!==g.slideBy&&(g.slideBy=Math.min(g.slideBy,g.items)),g.dots||"page"==g.slideBy)for(this._pages=[],a=d,b=0,c=0;e>a;a++){if(b>=h||0===b){if(this._pages.push({start:Math.min(f,a-d),end:a-d+h-1}),Math.min(f,a-d)===f)break;b=0,++c}b+=this._core.mergers(this._core.relative(a))}},e.prototype.draw=function(){var b,c=this._core.settings,d=this._core.items().length<=c.items,e=this._core.relative(this._core.current()),f=c.loop||c.rewind;this._controls.$relative.toggleClass("disabled",!c.nav||d),c.nav&&(this._controls.$previous.toggleClass("disabled",!f&&e<=this._core.minimum(!0)),this._controls.$next.toggleClass("disabled",!f&&e>=this._core.maximum(!0))),this._controls.$absolute.toggleClass("disabled",!c.dots||d),c.dots&&(b=this._pages.length-this._controls.$absolute.children().length,c.dotsData&&0!==b?this._controls.$absolute.html(this._templates.join("")):b>0?this._controls.$absolute.append(new Array(b+1).join(this._templates[0])):0>b&&this._controls.$absolute.children().slice(b).remove(),this._controls.$absolute.find(".active").removeClass("active"),this._controls.$absolute.children().eq(a.inArray(this.current(),this._pages)).addClass("active"))},e.prototype.onTrigger=function(b){var c=this._core.settings;b.page={index:a.inArray(this.current(),this._pages),count:this._pages.length,size:c&&(c.center||c.autoWidth||c.dotsData?1:c.dotsEach||c.items)}},e.prototype.current=function(){var b=this._core.relative(this._core.current());return a.grep(this._pages,a.proxy(function(a,c){return a.start<=b&&a.end>=b},this)).pop()},e.prototype.getPosition=function(b){var c,d,e=this._core.settings;return"page"==e.slideBy?(c=a.inArray(this.current(),this._pages),d=this._pages.length,b?++c:--c,c=this._pages[(c%d+d)%d].start):(c=this._core.relative(this._core.current()),d=this._core.items().length,b?c+=e.slideBy:c-=e.slideBy),c},e.prototype.next=function(b){a.proxy(this._overrides.to,this._core)(this.getPosition(!0),b)},e.prototype.prev=function(b){a.proxy(this._overrides.to,this._core)(this.getPosition(!1),b)},e.prototype.to=function(b,c,d){var e;d?a.proxy(this._overrides.to,this._core)(b,c):(e=this._pages.length,a.proxy(this._overrides.to,this._core)(this._pages[(b%e+e)%e].start,c))},a.fn.owlCarousel.Constructor.Plugins.Navigation=e}(window.Zepto||window.jQuery,window,document),function(a,b,c,d){"use strict";var e=function(c){this._core=c,this._hashes={},this.$element=this._core.$element,this._handlers={"initialized.owl.carousel":a.proxy(function(c){c.namespace&&"URLHash"===this._core.settings.startPosition&&a(b).trigger("hashchange.owl.navigation")},this),"prepared.owl.carousel":a.proxy(function(b){if(b.namespace){var c=a(b.content).find("[data-hash]").andSelf("[data-hash]").attr("data-hash");if(!c)return;this._hashes[c]=b.content}},this),"changed.owl.carousel":a.proxy(function(c){if(c.namespace&&"position"===c.property.name){var d=this._core.items(this._core.relative(this._core.current())),e=a.map(this._hashes,function(a,b){return a===d?b:null}).join();if(!e||b.location.hash.slice(1)===e)return;b.location.hash=e}},this)},this._core.options=a.extend({},e.Defaults,this._core.options),this.$element.on(this._handlers),a(b).on("hashchange.owl.navigation",a.proxy(function(a){var c=b.location.hash.substring(1),e=this._core.$stage.children(),f=this._hashes[c]&&e.index(this._hashes[c]);f!==d&&f!==this._core.current()&&this._core.to(this._core.relative(f),!1,!0)},this))};e.Defaults={URLhashListener:!1},e.prototype.destroy=function(){var c,d;a(b).off("hashchange.owl.navigation");for(c in this._handlers)this._core.$element.off(c,this._handlers[c]);for(d in Object.getOwnPropertyNames(this))"function"!=typeof this[d]&&(this[d]=null)},a.fn.owlCarousel.Constructor.Plugins.Hash=e}(window.Zepto||window.jQuery,window,document),function(a,b,c,d){function e(b,c){var e=!1,f=b.charAt(0).toUpperCase()+b.slice(1);return a.each((b+" "+h.join(f+" ")+f).split(" "),function(a,b){return g[b]!==d?(e=c?b:!0,!1):void 0}),e}function f(a){return e(a,!0)}var g=a("").get(0).style,h="Webkit Moz O ms".split(" "),i={transition:{end:{WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd",transition:"transitionend"}},animation:{end:{WebkitAnimation:"webkitAnimationEnd",MozAnimation:"animationend",OAnimation:"oAnimationEnd",animation:"animationend"}}},j={csstransforms:function(){return!!e("transform")},csstransforms3d:function(){return!!e("perspective")},csstransitions:function(){return!!e("transition")},cssanimations:function(){return!!e("animation")}};j.csstransitions()&&(a.support.transition=new String(f("transition")),a.support.transition.end=i.transition.end[a.support.transition]),j.cssanimations()&&(a.support.animation=new String(f("animation")),a.support.animation.end=i.animation.end[a.support.animation]),j.csstransforms()&&(a.support.transform=new String(f("transform")),a.support.transform3d=j.csstransforms3d())}(window.Zepto||window.jQuery,window,document); \ No newline at end of file diff --git a/docs/static/vendor/OwlCarousel2/notes.txt b/docs/static/vendor/OwlCarousel2/notes.txt new file mode 100644 index 000000000..eeb8f7a89 --- /dev/null +++ b/docs/static/vendor/OwlCarousel2/notes.txt @@ -0,0 +1,12 @@ +Version: 2.0.0-beta.3 + +Source: + https://github.com/OwlCarousel2/OwlCarousel2 + +Files (there): + dist/assets/owl.carousel.css + dist/assets/owl.theme.default.css + dist/owl.carousel.js + +Roadmap (from August 22, 2016): + https://github.com/OwlCarousel2/OwlCarousel2/issues/1538 diff --git a/docs/static/vendor/dieulot/js/instantclick.min.js b/docs/static/vendor/dieulot/js/instantclick.min.js new file mode 100644 index 000000000..a2539d884 --- /dev/null +++ b/docs/static/vendor/dieulot/js/instantclick.min.js @@ -0,0 +1,13 @@ +/* InstantClick 3.1.0 | (C) 2014 Alexandre Dieulot | http://instantclick.io/license */ +var InstantClick=function(d,e){function w(a){var b=a.indexOf("#");return 0>b?a:a.substr(0,b)}function z(a){for(;a&&"A"!=a.nodeName;)a=a.parentNode;return a}function A(a){var b=e.protocol+"//"+e.host;if(!(b=a.target||a.hasAttribute("download")||0!=a.href.indexOf(b+"/")||-1+new Date-500||(a=z(a.target))&&A(a)&&x(a.href)}function N(a){G>+new Date-500||(a=z(a.target))&&A(a)&&(a.addEventListener("mouseout",T),H?(O=a.href,l=setTimeout(x,H)):x(a.href))}function U(a){G=+new Date;(a=z(a.target))&&A(a)&&(D?a.removeEventListener("mousedown", +M):a.removeEventListener("mouseover",N),x(a.href))}function V(a){var b=z(a.target);!b||!A(b)||1p.readyState)&&0!=p.status){q.ready=+new Date-q.start;if(p.getResponseHeader("Content-Type").match(/\/(x|ht|xht)ml/)){var a=d.implementation.createHTMLDocument("");a.documentElement.innerHTML=p.responseText.replace(//gi,"");y=a.title; +u=a.body;var b=t("receive",r,u,y);b&&("body"in b&&(u=b.body),"title"in b&&(y=b.title));b=w(r);h[b]={body:u,title:y,scrollY:b in h?h[b].scrollY:0};for(var a=a.head.children,b=0,c,g=a.length-1;0<=g;g--)if(c=a[g],c.hasAttribute("data-instant-track")){c=c.getAttribute("href")||c.getAttribute("src")||c.innerHTML;for(var e=E.length-1;0<=e;e--)E[e]==c&&b++}b!=E.length&&(F=!0)}else F=!0;m&&(m=!1,P(r))}}function L(a){d.body.addEventListener("touchstart",U,!0);D?d.body.addEventListener("mousedown",M,!0):d.body.addEventListener("mouseover", +N,!0);d.body.addEventListener("click",V,!0);if(!a){a=d.body.getElementsByTagName("script");var b,c,g,e;i=0;for(j=a.length;i+new Date-(q.start+q.display)||(l&&(clearTimeout(l),l=!1),a||(a=O),v&&(a==r||m))||(v=!0,m=!1,r=a,F=u=!1,q={start:+new Date},t("fetch"), +p.open("GET",a),p.send())}function P(a){"display"in q||(q.display=+new Date-q.start);l||!v?l&&r&&r!=a?e.href=a:(x(a),C.start(0,!0),t("wait"),m=!0):m?e.href=a:F?e.href=r:u?(h[k].scrollY=pageYOffset,m=v=!1,K(y,u,r)):(C.start(0,!0),t("wait"),m=!0)}var I=navigator.userAgent,S=-1b;b++)a[b]+"Transform"in h.style&&(k=a[b]+"Transform");var c="transition";if(!(c in h.style))for(b=0;3>b;b++)a[b]+"Transition"in +h.style&&(c="-"+a[b].toLowerCase()+"-"+c);a=d.createElement("style");a.innerHTML="#instantclick{position:"+(Q?"absolute":"fixed")+";top:0;left:0;width:100%;pointer-events:none;z-index:2147483647;"+c+":opacity .25s .1s}.instantclick-bar{background:#29d;width:100%;margin-left:-100%;height:2px;"+c+":all .25s}";d.head.appendChild(a);Q&&(m(),addEventListener("resize",m),addEventListener("scroll",m))},start:a,done:e}}(),R="pushState"in history&&(!I.match("Android")||I.match("Chrome/"))&&"file:"!=e.protocol; +return{supported:R,init:function(){if(!k)if(R){for(var a=arguments.length-1;0<=a;a--){var b=arguments[a];!0===b?J=!0:"mousedown"==b?D=!0:"number"==typeof b&&(H=b)}k=w(e.href);h[k]={body:d.body,title:d.title,scrollY:pageYOffset};for(var b=d.head.children,c,a=b.length-1;0<=a;a--)c=b[a],c.hasAttribute("data-instant-track")&&(c=c.getAttribute("href")||c.getAttribute("src")||c.innerHTML,E.push(c));p=new XMLHttpRequest;p.addEventListener("readystatechange",W);L(!0);C.init();t("change",!0);addEventListener("popstate", +function(){var a=w(e.href);a!=k&&(a in h?(h[k].scrollY=pageYOffset,k=a,K(h[a].title,h[a].body,!1,h[a].scrollY)):e.href=e.href)})}else t("change",!0)},on:function(a,b){B[a].push(b)}}}(document,location); diff --git a/docs/static/vendor/flesler/js/jquery.scrollTo.min.js b/docs/static/vendor/flesler/js/jquery.scrollTo.min.js new file mode 100644 index 000000000..65a020d92 --- /dev/null +++ b/docs/static/vendor/flesler/js/jquery.scrollTo.min.js @@ -0,0 +1,7 @@ +/** + * Copyright (c) 2007-2015 Ariel Flesler - afleslergmailcom | http://flesler.blogspot.com + * Licensed under MIT + * @author Ariel Flesler + * @version 2.1.2 + */ +;(function(f){"use strict";"function"===typeof define&&define.amd?define(["jquery"],f):"undefined"!==typeof module&&module.exports?module.exports=f(require("jquery")):f(jQuery)})(function($){"use strict";function n(a){return!a.nodeName||-1!==$.inArray(a.nodeName.toLowerCase(),["iframe","#document","html","body"])}function h(a){return $.isFunction(a)||$.isPlainObject(a)?a:{top:a,left:a}}var p=$.scrollTo=function(a,d,b){return $(window).scrollTo(a,d,b)};p.defaults={axis:"xy",duration:0,limit:!0};$.fn.scrollTo=function(a,d,b){"object"=== typeof d&&(b=d,d=0);"function"===typeof b&&(b={onAfter:b});"max"===a&&(a=9E9);b=$.extend({},p.defaults,b);d=d||b.duration;var u=b.queue&&1=f[g]?0:Math.min(f[g],n));!a&&1li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"} diff --git a/docs/static/vendor/font-awesome/fonts/FontAwesome.otf b/docs/static/vendor/font-awesome/fonts/FontAwesome.otf new file mode 100644 index 000000000..3ed7f8b48 Binary files /dev/null and b/docs/static/vendor/font-awesome/fonts/FontAwesome.otf differ diff --git a/docs/static/vendor/font-awesome/fonts/fontawesome-webfont.eot b/docs/static/vendor/font-awesome/fonts/fontawesome-webfont.eot new file mode 100644 index 000000000..9b6afaedc Binary files /dev/null and b/docs/static/vendor/font-awesome/fonts/fontawesome-webfont.eot differ diff --git a/docs/static/vendor/font-awesome/fonts/fontawesome-webfont.svg b/docs/static/vendor/font-awesome/fonts/fontawesome-webfont.svg new file mode 100644 index 000000000..d05688e9e --- /dev/null +++ b/docs/static/vendor/font-awesome/fonts/fontawesome-webfont.svg @@ -0,0 +1,655 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/static/vendor/font-awesome/fonts/fontawesome-webfont.ttf b/docs/static/vendor/font-awesome/fonts/fontawesome-webfont.ttf new file mode 100644 index 000000000..26dea7951 Binary files /dev/null and b/docs/static/vendor/font-awesome/fonts/fontawesome-webfont.ttf differ diff --git a/docs/static/vendor/font-awesome/fonts/fontawesome-webfont.woff b/docs/static/vendor/font-awesome/fonts/fontawesome-webfont.woff new file mode 100644 index 000000000..dc35ce3c2 Binary files /dev/null and b/docs/static/vendor/font-awesome/fonts/fontawesome-webfont.woff differ diff --git a/docs/static/vendor/font-awesome/fonts/fontawesome-webfont.woff2 b/docs/static/vendor/font-awesome/fonts/fontawesome-webfont.woff2 new file mode 100644 index 000000000..500e51725 Binary files /dev/null and b/docs/static/vendor/font-awesome/fonts/fontawesome-webfont.woff2 differ diff --git a/docs/static/vendor/highlightjs/css/monokai-sublime.css b/docs/static/vendor/highlightjs/css/monokai-sublime.css new file mode 100644 index 000000000..2864170da --- /dev/null +++ b/docs/static/vendor/highlightjs/css/monokai-sublime.css @@ -0,0 +1,83 @@ +/* + +Monokai Sublime style. Derived from Monokai by noformnocontent http://nn.mit-license.org/ + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #23241f; +} + +.hljs, +.hljs-tag, +.hljs-subst { + color: #f8f8f2; +} + +.hljs-strong, +.hljs-emphasis { + color: #a8a8a2; +} + +.hljs-bullet, +.hljs-quote, +.hljs-number, +.hljs-regexp, +.hljs-literal, +.hljs-link { + color: #ae81ff; +} + +.hljs-code, +.hljs-title, +.hljs-section, +.hljs-selector-class { + color: #a6e22e; +} + +.hljs-strong { + font-weight: bold; +} + +.hljs-emphasis { + font-style: italic; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-name, +.hljs-attr { + color: #f92672; +} + +.hljs-symbol, +.hljs-attribute { + color: #66d9ef; +} + +.hljs-params, +.hljs-class .hljs-title { + color: #f8f8f2; +} + +.hljs-string, +.hljs-type, +.hljs-built_in, +.hljs-builtin-name, +.hljs-selector-id, +.hljs-selector-attr, +.hljs-selector-pseudo, +.hljs-addition, +.hljs-variable, +.hljs-template-variable { + color: #e6db74; +} + +.hljs-comment, +.hljs-deletion, +.hljs-meta { + color: #75715e; +} diff --git a/docs/static/vendor/highlightjs/js/highlight.pack.js b/docs/static/vendor/highlightjs/js/highlight.pack.js new file mode 100644 index 000000000..ab4712ecf --- /dev/null +++ b/docs/static/vendor/highlightjs/js/highlight.pack.js @@ -0,0 +1,3 @@ +/*! highlight.js v9.0.0 | BSD3 License | git.io/hljslicense */ +!function(e){"undefined"!=typeof exports?e(exports):(self.hljs=e({}),"function"==typeof define&&define.amd&&define("hljs",[],function(){return self.hljs}))}(function(e){function t(e){return e.replace(/&/gm,"&").replace(//gm,">")}function r(e){return e.nodeName.toLowerCase()}function n(e,t){var r=e&&e.exec(t);return r&&0==r.index}function a(e){return/^(no-?highlight|plain|text)$/i.test(e)}function i(e){var t,r,n,i=e.className+" ";if(i+=e.parentNode?e.parentNode.className:"",r=/\blang(?:uage)?-([\w-]+)\b/i.exec(i))return w(r[1])?r[1]:"no-highlight";for(i=i.split(/\s+/),t=0,n=i.length;n>t;t++)if(w(i[t])||a(i[t]))return i[t]}function o(e,t){var r,n={};for(r in e)n[r]=e[r];if(t)for(r in t)n[r]=t[r];return n}function c(e){var t=[];return function n(e,a){for(var i=e.firstChild;i;i=i.nextSibling)3==i.nodeType?a+=i.nodeValue.length:1==i.nodeType&&(t.push({event:"start",offset:a,node:i}),a=n(i,a),r(i).match(/br|hr|img|input/)||t.push({event:"stop",offset:a,node:i}));return a}(e,0),t}function s(e,n,a){function i(){return e.length&&n.length?e[0].offset!=n[0].offset?e[0].offset"}function c(e){b+=""}function s(e){("start"==e.event?o:c)(e.node)}for(var l=0,b="",u=[];e.length||n.length;){var d=i();if(b+=t(a.substr(l,d[0].offset-l)),l=d[0].offset,d==e){u.reverse().forEach(c);do s(d.splice(0,1)[0]),d=i();while(d==e&&d.length&&d[0].offset==l);u.reverse().forEach(o)}else"start"==d[0].event?u.push(d[0].node):u.pop(),s(d.splice(0,1)[0])}return b+t(a.substr(l))}function l(e){function t(e){return e&&e.source||e}function r(r,n){return new RegExp(t(r),"m"+(e.cI?"i":"")+(n?"g":""))}function n(a,i){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var c={},s=function(t,r){e.cI&&(r=r.toLowerCase()),r.split(" ").forEach(function(e){var r=e.split("|");c[r[0]]=[t,r[1]?Number(r[1]):1]})};"string"==typeof a.k?s("keyword",a.k):Object.keys(a.k).forEach(function(e){s(e,a.k[e])}),a.k=c}a.lR=r(a.l||/\b\w+\b/,!0),i&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=r(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=r(a.e)),a.tE=t(a.e)||"",a.eW&&i.tE&&(a.tE+=(a.e?"|":"")+i.tE)),a.i&&(a.iR=r(a.i)),void 0===a.r&&(a.r=1),a.c||(a.c=[]);var l=[];a.c.forEach(function(e){e.v?e.v.forEach(function(t){l.push(o(e,t))}):l.push("self"==e?a:e)}),a.c=l,a.c.forEach(function(e){n(e,a)}),a.starts&&n(a.starts,i);var b=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(t).filter(Boolean);a.t=b.length?r(b.join("|"),!0):{exec:function(){return null}}}}n(e)}function b(e,r,a,i){function o(e,t){for(var r=0;r";return i+=e+'">',i+t+o}function f(){if(!x.k)return t(S);var e="",r=0;x.lR.lastIndex=0;for(var n=x.lR.exec(S);n;){e+=t(S.substr(r,n.index-r));var a=d(x,n);a?(E+=a[1],e+=p(a[0],t(n[0]))):e+=t(n[0]),r=x.lR.lastIndex,n=x.lR.exec(S)}return e+t(S.substr(r))}function m(){var e="string"==typeof x.sL;if(e&&!k[x.sL])return t(S);var r=e?b(x.sL,S,!0,_[x.sL]):u(S,x.sL.length?x.sL:void 0);return x.r>0&&(E+=r.r),e&&(_[x.sL]=r.top),p(r.language,r.value,!1,!0)}function g(){return void 0!==x.sL?m():f()}function h(e,r){var n=e.cN?p(e.cN,"",!0):"";e.rB?(M+=n,S=""):e.eB?(M+=t(r)+n,S=""):(M+=n,S=r),x=Object.create(e,{parent:{value:x}})}function N(e,r){if(S+=e,void 0===r)return M+=g(),0;var n=o(r,x);if(n)return M+=g(),h(n,r),n.rB?0:r.length;var a=c(x,r);if(a){var i=x;i.rE||i.eE||(S+=r),M+=g();do x.cN&&(M+=""),E+=x.r,x=x.parent;while(x!=a.parent);return i.eE&&(M+=t(r)),S="",a.starts&&h(a.starts,""),i.rE?0:r.length}if(s(r,x))throw new Error('Illegal lexeme "'+r+'" for mode "'+(x.cN||"")+'"');return S+=r,r.length||1}var v=w(e);if(!v)throw new Error('Unknown language: "'+e+'"');l(v);var C,x=i||v,_={},M="";for(C=x;C!=v;C=C.parent)C.cN&&(M=p(C.cN,"",!0)+M);var S="",E=0;try{for(var $,A,z=0;;){if(x.t.lastIndex=z,$=x.t.exec(r),!$)break;A=N(r.substr(z,$.index-z),$[0]),z=$.index+A}for(N(r.substr(z)),C=x;C.parent;C=C.parent)C.cN&&(M+="");return{r:E,value:M,language:e,top:x}}catch(B){if(-1!=B.message.indexOf("Illegal"))return{r:0,value:t(r)};throw B}}function u(e,r){r=r||y.languages||Object.keys(k);var n={r:0,value:t(e)},a=n;return r.forEach(function(t){if(w(t)){var r=b(t,e,!1);r.language=t,r.r>a.r&&(a=r),r.r>n.r&&(a=n,n=r)}}),a.language&&(n.second_best=a),n}function d(e){return y.tabReplace&&(e=e.replace(/^((<[^>]+>|\t)+)/gm,function(e,t){return t.replace(/\t/g,y.tabReplace)})),y.useBR&&(e=e.replace(/\n/g,"
    ")),e}function p(e,t,r){var n=t?C[t]:r,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),-1===e.indexOf(n)&&a.push(n),a.join(" ").trim()}function f(e){var t=i(e);if(!a(t)){var r;y.useBR?(r=document.createElementNS("http://www.w3.org/1999/xhtml","div"),r.innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n")):r=e;var n=r.textContent,o=t?b(t,n,!0):u(n),l=c(r);if(l.length){var f=document.createElementNS("http://www.w3.org/1999/xhtml","div");f.innerHTML=o.value,o.value=s(l,c(f),n)}o.value=d(o.value),e.innerHTML=o.value,e.className=p(e.className,t,o.language),e.result={language:o.language,re:o.r},o.second_best&&(e.second_best={language:o.second_best.language,re:o.second_best.r})}}function m(e){y=o(y,e)}function g(){if(!g.called){g.called=!0;var e=document.querySelectorAll("pre code");Array.prototype.forEach.call(e,f)}}function h(){addEventListener("DOMContentLoaded",g,!1),addEventListener("load",g,!1)}function N(t,r){var n=k[t]=r(e);n.aliases&&n.aliases.forEach(function(e){C[e]=t})}function v(){return Object.keys(k)}function w(e){return e=(e||"").toLowerCase(),k[e]||k[C[e]]}var y={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0},k={},C={};return e.highlight=b,e.highlightAuto=u,e.fixMarkup=d,e.highlightBlock=f,e.configure=m,e.initHighlighting=g,e.initHighlightingOnLoad=h,e.registerLanguage=N,e.listLanguages=v,e.getLanguage=w,e.inherit=o,e.IR="[a-zA-Z]\\w*",e.UIR="[a-zA-Z_]\\w*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|like)\b/},e.C=function(t,r,n){var a=e.inherit({cN:"comment",b:t,e:r,c:[]},n||{});return a.c.push(e.PWM),a.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),a},e.CLCM=e.C("//","$"),e.CBCM=e.C("/\\*","\\*/"),e.HCM=e.C("#","$"),e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e.registerLanguage("apache",function(e){var t={cN:"number",b:"[\\$%]\\d+"};return{aliases:["apacheconf"],cI:!0,c:[e.HCM,{cN:"section",b:""},{cN:"attribute",b:/\w+/,r:0,k:{nomarkup:"order deny allow setenv rewriterule rewriteengine rewritecond documentroot sethandler errordocument loadmodule options header listen serverroot servername"},starts:{e:/$/,r:0,k:{literal:"on off all"},c:[{cN:"meta",b:"\\s\\[",e:"\\]$"},{cN:"variable",b:"[\\$%]\\{",e:"\\}",c:["self",t]},t,e.QSM]}}],i:/\S/}}),e.registerLanguage("xml",function(e){var t="[A-Za-z0-9\\._:-]+",r={b:/<\?(php)?(?!\w)/,e:/\?>/,sL:"php"},n={eW:!0,i:/]+/}]}]}]};return{aliases:["html","xhtml","rss","atom","xsl","plist"],cI:!0,c:[{cN:"meta",b:"",r:10,c:[{b:"\\[",e:"\\]"}]},e.C("",{r:10}),{b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{cN:"tag",b:"|$)",e:">",k:{name:"style"},c:[n],starts:{e:"",rE:!0,sL:["css","xml"]}},{cN:"tag",b:"|$)",e:">",k:{name:"script"},c:[n],starts:{e:"",rE:!0,sL:["actionscript","javascript","handlebars","xml"]}},r,{cN:"meta",b:/<\?\w+/,e:/\?>/,r:10},{cN:"tag",b:"",c:[{cN:"name",b:/[^\/><\s]+/,r:0},n]}]}}),e.registerLanguage("asciidoc",function(e){return{aliases:["adoc"],c:[e.C("^/{4,}\\n","\\n/{4,}$",{r:10}),e.C("^//","$",{r:0}),{cN:"title",b:"^\\.\\w.*$"},{b:"^[=\\*]{4,}\\n",e:"\\n^[=\\*]{4,}$",r:10},{cN:"section",r:10,v:[{b:"^(={1,5}) .+?( \\1)?$"},{b:"^[^\\[\\]\\n]+?\\n[=\\-~\\^\\+]{2,}$"}]},{cN:"meta",b:"^:.+?:",e:"\\s",eE:!0,r:10},{cN:"meta",b:"^\\[.+?\\]$",r:0},{cN:"quote",b:"^_{4,}\\n",e:"\\n_{4,}$",r:10},{cN:"code",b:"^[\\-\\.]{4,}\\n",e:"\\n[\\-\\.]{4,}$",r:10},{b:"^\\+{4,}\\n",e:"\\n\\+{4,}$",c:[{b:"<",e:">",sL:"xml",r:0}],r:10},{cN:"bullet",b:"^(\\*+|\\-+|\\.+|[^\\n]+?::)\\s+"},{cN:"symbol",b:"^(NOTE|TIP|IMPORTANT|WARNING|CAUTION):\\s+",r:10},{cN:"strong",b:"\\B\\*(?![\\*\\s])",e:"(\\n{2}|\\*)",c:[{b:"\\\\*\\w",r:0}]},{cN:"emphasis",b:"\\B'(?!['\\s])",e:"(\\n{2}|')",c:[{b:"\\\\'\\w",r:0}],r:0},{cN:"emphasis",b:"_(?![_\\s])",e:"(\\n{2}|_)",r:0},{cN:"string",v:[{b:"``.+?''"},{b:"`.+?'"}]},{cN:"code",b:"(`.+?`|\\+.+?\\+)",r:0},{cN:"code",b:"^[ \\t]",e:"$",r:0},{b:"^'{3,}[ \\t]*$",r:10},{b:"(link:)?(http|https|ftp|file|irc|image:?):\\S+\\[.*?\\]",rB:!0,c:[{b:"(link|image:?):",r:0},{cN:"link",b:"\\w",e:"[^\\[]+",r:0},{cN:"string",b:"\\[",e:"\\]",eB:!0,eE:!0,r:0}],r:10}]}}),e.registerLanguage("bash",function(e){var t={cN:"variable",v:[{b:/\$[\w\d#@][\w\d_]*/},{b:/\$\{(.*?)}/}]},r={cN:"string",b:/"/,e:/"/,c:[e.BE,t,{cN:"variable",b:/\$\(/,e:/\)/,c:[e.BE]}]},n={cN:"string",b:/'/,e:/'/};return{aliases:["sh","zsh"],l:/-?[a-z\.]+/,k:{keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp",_:"-ne -eq -lt -gt -f -d -e -s -l -a"},c:[{cN:"meta",b:/^#![^\n]+sh\s*$/,r:10},{cN:"function",b:/\w[\w\d_]*\s*\(\s*\)\s*\{/,rB:!0,c:[e.inherit(e.TM,{b:/\w[\w\d_]*/})],r:0},e.HCM,r,n,t]}}),e.registerLanguage("coffeescript",function(e){var t={keyword:"in if for while finally new do return else break catch instanceof throw try this switch continue typeof delete debugger super then unless until loop of by when and or is isnt not",literal:"true false null undefined yes no on off",built_in:"npm require console print module global window document"},r="[A-Za-z$_][0-9A-Za-z$_]*",n={cN:"subst",b:/#\{/,e:/}/,k:t},a=[e.BNM,e.inherit(e.CNM,{starts:{e:"(\\s*/)?",r:0}}),{cN:"string",v:[{b:/'''/,e:/'''/,c:[e.BE]},{b:/'/,e:/'/,c:[e.BE]},{b:/"""/,e:/"""/,c:[e.BE,n]},{b:/"/,e:/"/,c:[e.BE,n]}]},{cN:"regexp",v:[{b:"///",e:"///",c:[n,e.HCM]},{b:"//[gim]*",r:0},{b:/\/(?![ *])(\\\/|.)*?\/[gim]*(?=\W|$)/}]},{b:"@"+r},{b:"`",e:"`",eB:!0,eE:!0,sL:"javascript"}];n.c=a;var i=e.inherit(e.TM,{b:r}),o="(\\(.*\\))?\\s*\\B[-=]>",c={cN:"params",b:"\\([^\\(]",rB:!0,c:[{b:/\(/,e:/\)/,k:t,c:["self"].concat(a)}]};return{aliases:["coffee","cson","iced"],k:t,i:/\/\*/,c:a.concat([e.C("###","###"),e.HCM,{cN:"function",b:"^\\s*"+r+"\\s*=\\s*"+o,e:"[-=]>",rB:!0,c:[i,c]},{b:/[:\(,=]\s*/,r:0,c:[{cN:"function",b:o,e:"[-=]>",rB:!0,c:[c]}]},{cN:"class",bK:"class",e:"$",i:/[:="\[\]]/,c:[{bK:"extends",eW:!0,i:/[:="\[\]]/,c:[i]},i]},{b:r+":",e:":",rB:!0,rE:!0,r:0}])}}),e.registerLanguage("css",function(e){var t="[a-zA-Z-][a-zA-Z0-9_-]*",r={b:/[A-Z\_\.\-]+\s*:/,rB:!0,e:";",eW:!0,c:[{cN:"attribute",b:/\S/,e:":",eE:!0,starts:{eW:!0,eE:!0,c:[{b:/[\w-]+\s*\(/,rB:!0,c:[{cN:"built_in",b:/[\w-]+/}]},e.CSSNM,e.QSM,e.ASM,e.CBCM,{cN:"number",b:"#[0-9A-Fa-f]+"},{cN:"meta",b:"!important"}]}}]};return{cI:!0,i:/[=\/|'\$]/,c:[e.CBCM,{cN:"selector-id",b:/#[A-Za-z0-9_-]+/},{cN:"selector-class",b:/\.[A-Za-z0-9_-]+/},{cN:"selector-attr",b:/\[/,e:/\]/,i:"$"},{cN:"selector-pseudo",b:/:(:)?[a-zA-Z0-9\_\-\+\(\)"']+/},{b:"@(font-face|page)",l:"[a-z-]+",k:"font-face page"},{b:"@",e:"[{;]",c:[{cN:"keyword",b:/\S+/},{b:/\s/,eW:!0,eE:!0,r:0,c:[e.ASM,e.QSM,e.CSSNM]}]},{cN:"selector-tag",b:t,r:0},{b:"{",e:"}",i:/\S/,c:[e.CBCM,r]}]}}),e.registerLanguage("diff",function(e){return{aliases:["patch"],c:[{cN:"meta",r:10,v:[{b:/^@@ +\-\d+,\d+ +\+\d+,\d+ +@@$/},{b:/^\*\*\* +\d+,\d+ +\*\*\*\*$/},{b:/^\-\-\- +\d+,\d+ +\-\-\-\-$/}]},{cN:"comment",v:[{b:/Index: /,e:/$/},{b:/=====/,e:/=====$/},{b:/^\-\-\-/,e:/$/},{b:/^\*{3} /,e:/$/},{b:/^\+\+\+/,e:/$/},{b:/\*{5}/,e:/\*{5}$/}]},{cN:"addition",b:"^\\+",e:"$"},{cN:"deletion",b:"^\\-",e:"$"},{cN:"addition",b:"^\\!",e:"$"}]}}),e.registerLanguage("django",function(e){var t={b:/\|[A-Za-z]+:?/,k:{name:"truncatewords removetags linebreaksbr yesno get_digit timesince random striptags filesizeformat escape linebreaks length_is ljust rjust cut urlize fix_ampersands title floatformat capfirst pprint divisibleby add make_list unordered_list urlencode timeuntil urlizetrunc wordcount stringformat linenumbers slice date dictsort dictsortreversed default_if_none pluralize lower join center default truncatewords_html upper length phone2numeric wordwrap time addslashes slugify first escapejs force_escape iriencode last safe safeseq truncatechars localize unlocalize localtime utc timezone"},c:[e.QSM,e.ASM]};return{aliases:["jinja"],cI:!0,sL:"xml",c:[e.C(/\{%\s*comment\s*%}/,/\{%\s*endcomment\s*%}/),e.C(/\{#/,/#}/),{cN:"template-tag",b:/\{%/,e:/%}/,c:[{cN:"name",b:/\w+/,k:{name:"comment endcomment load templatetag ifchanged endifchanged if endif firstof for endfor ifnotequal endifnotequal widthratio extends include spaceless endspaceless regroup ifequal endifequal ssi now with cycle url filter endfilter debug block endblock else autoescape endautoescape csrf_token empty elif endwith static trans blocktrans endblocktrans get_static_prefix get_media_prefix plural get_current_language language get_available_languages get_current_language_bidi get_language_info get_language_info_list localize endlocalize localtime endlocaltime timezone endtimezone get_current_timezone verbatim"},starts:{eW:!0,k:"in by as",c:[t],r:0}}]},{cN:"template-variable",b:/\{\{/,e:/}}/,c:[t]}]}}),e.registerLanguage("dockerfile",function(e){return{aliases:["docker"],cI:!0,k:"from maintainer cmd expose add copy entrypoint volume user workdir onbuild run env label",c:[e.HCM,{k:"run cmd entrypoint volume add copy workdir onbuild label",b:/^ *(onbuild +)?(run|cmd|entrypoint|volume|add|copy|workdir|label) +/,starts:{e:/[^\\]\n/,sL:"bash"}},{k:"from maintainer expose env user onbuild",b:/^ *(onbuild +)?(from|maintainer|expose|env|user|onbuild) +/,e:/[^\\]\n/,c:[e.ASM,e.QSM,e.NM,e.HCM]}]}}),e.registerLanguage("dos",function(e){var t=e.C(/@?rem\b/,/$/,{r:10}),r={cN:"symbol",b:"^\\s*[A-Za-z._?][A-Za-z0-9_$#@~.?]*(:|\\s+label)",r:0};return{aliases:["bat","cmd"],cI:!0,i:/\/\*/,k:{keyword:"if else goto for in do call exit not exist errorlevel defined equ neq lss leq gtr geq",built_in:"prn nul lpt3 lpt2 lpt1 con com4 com3 com2 com1 aux shift cd dir echo setlocal endlocal set pause copy append assoc at attrib break cacls cd chcp chdir chkdsk chkntfs cls cmd color comp compact convert date dir diskcomp diskcopy doskey erase fs find findstr format ftype graftabl help keyb label md mkdir mode more move path pause print popd pushd promt rd recover rem rename replace restore rmdir shiftsort start subst time title tree type ver verify vol ping net ipconfig taskkill xcopy ren del"},c:[{cN:"variable",b:/%%[^ ]|%[^ ]+?%|![^ ]+?!/},{cN:"function",b:r.b,e:"goto:eof",c:[e.inherit(e.TM,{b:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),t]},{cN:"number",b:"\\b\\d+",r:0},t]}}),e.registerLanguage("go",function(e){var t={keyword:"break default func interface select case map struct chan else goto package switch const fallthrough if range type continue for import return var go defer bool byte complex64 complex128 float32 float64 int8 int16 int32 int64 string uint8 uint16 uint32 uint64 int uint uintptr rune",literal:"true false iota nil",built_in:"append cap close complex copy imag len make new panic print println real recover delete"};return{aliases:["golang"],k:t,i:">|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?",r="and false then defined module in return redo if BEGIN retry end for true self when next until do begin unless END rescue nil else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor",n={cN:"doctag",b:"@[A-Za-z]+"},a={b:"#<",e:">"},i=[e.C("#","$",{c:[n]}),e.C("^\\=begin","^\\=end",{c:[n],r:10}),e.C("^__END__","\\n$")],o={cN:"subst",b:"#\\{",e:"}",k:r},c={cN:"string",c:[e.BE,o],v:[{b:/'/,e:/'/},{b:/"/,e:/"/},{b:/`/,e:/`/},{b:"%[qQwWx]?\\(",e:"\\)"},{b:"%[qQwWx]?\\[",e:"\\]"},{b:"%[qQwWx]?{",e:"}"},{b:"%[qQwWx]?<",e:">"},{b:"%[qQwWx]?/",e:"/"},{b:"%[qQwWx]?%",e:"%"},{b:"%[qQwWx]?-",e:"-"},{b:"%[qQwWx]?\\|",e:"\\|"},{b:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/}]},s={cN:"params",b:"\\(",e:"\\)",endsParent:!0,k:r},l=[c,a,{cN:"class",bK:"class module",e:"$|;",i:/=/,c:[e.inherit(e.TM,{b:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{b:"<\\s*",c:[{b:"("+e.IR+"::)?"+e.IR}]}].concat(i)},{cN:"function",bK:"def",e:"$|;",c:[e.inherit(e.TM,{b:t}),s].concat(i)},{cN:"symbol",b:e.UIR+"(\\!|\\?)?:",r:0},{cN:"symbol",b:":",c:[c,{b:t}],r:0},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{b:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{b:"("+e.RSR+")\\s*",c:[a,{cN:"regexp",c:[e.BE,o],i:/\n/,v:[{b:"/",e:"/[a-z]*"},{b:"%r{",e:"}[a-z]*"},{b:"%r\\(",e:"\\)[a-z]*"},{b:"%r!",e:"![a-z]*"},{b:"%r\\[",e:"\\][a-z]*"}]}].concat(i),r:0}].concat(i);o.c=l,s.c=l;var b="[>?]>",u="[\\w#]+\\(\\w+\\):\\d+:\\d+>",d="(\\w+-)?\\d+\\.\\d+\\.\\d(p\\d+)?[^>]+>",p=[{b:/^\s*=>/,starts:{e:"$",c:l}},{cN:"meta",b:"^("+b+"|"+u+"|"+d+")",starts:{e:"$",c:l}}];return{aliases:["rb","gemspec","podspec","thor","irb"],k:r,i:/\/\*/,c:i.concat(p).concat(l)}}),e.registerLanguage("haml",function(e){return{cI:!0,c:[{cN:"meta",b:"^!!!( (5|1\\.1|Strict|Frameset|Basic|Mobile|RDFa|XML\\b.*))?$",r:10},e.C("^\\s*(!=#|=#|-#|/).*$",!1,{r:0}),{b:"^\\s*(-|=|!=)(?!#)",starts:{e:"\\n",sL:"ruby"}},{cN:"tag",b:"^\\s*%",c:[{cN:"selector-tag",b:"\\w+"},{cN:"selector-id",b:"#[\\w-]+"},{cN:"selector-class",b:"\\.[\\w-]+"},{b:"{\\s*",e:"\\s*}",c:[{b:":\\w+\\s*=>",e:",\\s+",rB:!0,eW:!0,c:[{cN:"attr",b:":\\w+"},e.ASM,e.QSM,{b:"\\w+",r:0}]}]},{b:"\\(\\s*",e:"\\s*\\)",eE:!0,c:[{b:"\\w+\\s*=",e:"\\s+",rB:!0,eW:!0,c:[{cN:"attr",b:"\\w+",r:0},e.ASM,e.QSM,{b:"\\w+",r:0}]}]}]},{b:"^\\s*[=~]\\s*"},{b:"#{",starts:{e:"}",sL:"ruby"}}]}}),e.registerLanguage("handlebars",function(e){var t={"builtin-name":"each in with if else unless bindattr action collection debugger log outlet template unbound view yield"};return{aliases:["hbs","html.hbs","html.handlebars"],cI:!0,sL:"xml",c:[e.C("{{!(--)?","(--)?}}"),{cN:"template-tag",b:/\{\{[#\/]/,e:/\}\}/,c:[{cN:"name",b:/[a-zA-Z\.-]+/,k:t,starts:{eW:!0,r:0,c:[e.QSM]}}]},{cN:"template-variable",b:/\{\{/,e:/\}\}/,k:t}]}}),e.registerLanguage("http",function(e){var t="HTTP/[0-9\\.]+";return{aliases:["https"],i:"\\S",c:[{b:"^"+t,e:"$",c:[{cN:"number",b:"\\b\\d{3}\\b"}]},{b:"^[A-Z]+ (.*?) "+t+"$",rB:!0,e:"$",c:[{cN:"string",b:" ",e:" ",eB:!0,eE:!0},{b:t},{cN:"keyword",b:"[A-Z]+"}]},{cN:"attribute",b:"^\\w",e:": ",eE:!0,i:"\\n|\\s|=",starts:{e:"$",r:0}},{b:"\\n\\n",starts:{sL:[],eW:!0}}]}}),e.registerLanguage("ini",function(e){var t={cN:"string",c:[e.BE],v:[{b:"'''",e:"'''",r:10},{b:'"""',e:'"""',r:10},{b:'"',e:'"'},{b:"'",e:"'"}]};return{aliases:["toml"],cI:!0,i:/\S/,c:[e.C(";","$"),e.HCM,{cN:"section",b:/^\s*\[+/,e:/\]+/},{b:/^[a-z0-9\[\]_-]+\s*=\s*/,e:"$",rB:!0,c:[{cN:"attr",b:/[a-z0-9\[\]_-]+/},{b:/=/,eW:!0,r:0,c:[{cN:"literal",b:/\bon|off|true|false|yes|no\b/},{cN:"variable",v:[{b:/\$[\w\d"][\w\d_]*/},{b:/\$\{(.*?)}/}]},t,{cN:"number",b:/([\+\-]+)?[\d]+_[\d_]+/},e.NM]}]}]}}),e.registerLanguage("javascript",function(e){return{aliases:["js"],k:{keyword:"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await import from as",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise"},c:[{cN:"meta",r:10,b:/^\s*['"]use (strict|asm)['"]/},e.ASM,e.QSM,{cN:"string",b:"`",e:"`",c:[e.BE,{cN:"subst",b:"\\$\\{",e:"\\}"}]},e.CLCM,e.CBCM,{cN:"number",v:[{b:"\\b(0[bB][01]+)"},{b:"\\b(0[oO][0-7]+)"},{b:e.CNR}],r:0},{b:"("+e.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[e.CLCM,e.CBCM,e.RM,{b:/\s*[);\]]/,r:0,sL:"xml"}],r:0},{cN:"function",bK:"function",e:/\{/,eE:!0,c:[e.inherit(e.TM,{b:/[A-Za-z$_][0-9A-Za-z$_]*/}),{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,c:[e.CLCM,e.CBCM]}],i:/\[|%/},{b:/\$[(.]/},{b:"\\."+e.IR,r:0},{cN:"class",bK:"class",e:/[{;=]/,eE:!0,i:/[:"\[\]]/,c:[{bK:"extends"},e.UTM]},{bK:"constructor",e:/\{/,eE:!0}],i:/#/}}),e.registerLanguage("json",function(e){var t={literal:"true false null"},r=[e.QSM,e.CNM],n={e:",",eW:!0,eE:!0,c:r,k:t},a={b:"{",e:"}",c:[{cN:"attr",b:'\\s*"',e:'"\\s*:\\s*',eB:!0,eE:!0,c:[e.BE],i:"\\n",starts:n}],i:"\\S"},i={b:"\\[",e:"\\]",c:[e.inherit(n)],i:"\\S"};return r.splice(r.length,0,a,i),{c:r,k:t,i:"\\S"}}),e.registerLanguage("less",function(e){var t="[\\w-]+",r="("+t+"|@{"+t+"})",n=[],a=[],i=function(e){return{cN:"string",b:"~?"+e+".*?"+e}},o=function(e,t,r){return{cN:e,b:t,r:r}},c={b:"\\(",e:"\\)",c:a,r:0};a.push(e.CLCM,e.CBCM,i("'"),i('"'),e.CSSNM,{b:"(url|data-uri)\\(",starts:{cN:"string",e:"[\\)\\n]",eE:!0}},o("number","#[0-9A-Fa-f]+\\b"),c,o("variable","@@?"+t,10),o("variable","@{"+t+"}"),o("built_in","~?`[^`]*?`"),{cN:"attribute",b:t+"\\s*:",e:":",rB:!0,eE:!0},{cN:"meta",b:"!important"});var s=a.concat({b:"{",e:"}",c:n}),l={bK:"when",eW:!0,c:[{bK:"and not"}].concat(a)},b={cN:"attribute",b:r,e:":",eE:!0,c:[e.CLCM,e.CBCM],i:/\S/,starts:{e:"[;}]",rE:!0,c:a,i:"[<=$]"}},u={cN:"keyword",b:"@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\b",starts:{e:"[;{}]",rE:!0,c:a,r:0}},d={cN:"variable",v:[{b:"@"+t+"\\s*:",r:15},{b:"@"+t}],starts:{e:"[;}]",rE:!0,c:s}},p={v:[{b:"[\\.#:&\\[]",e:"[;{}]"},{b:r+"[^;]*{",e:"{"}],rB:!0,rE:!0,i:"[<='$\"]",c:[e.CLCM,e.CBCM,l,o("keyword","all\\b"),o("variable","@{"+t+"}"),o("selector-tag",r+"%?",0),o("selector-id","#"+r),o("selector-class","\\."+r,0),o("selector-tag","&",0),{cN:"selector-attr",b:"\\[",e:"\\]"},{b:"\\(",e:"\\)",c:s},{b:"!important"}]};return n.push(e.CLCM,e.CBCM,u,d,p,b),{cI:!0,i:"[=>'/<($\"]",c:n}}),e.registerLanguage("makefile",function(e){var t={cN:"variable",b:/\$\(/,e:/\)/,c:[e.BE]};return{aliases:["mk","mak"],c:[e.HCM,{b:/^\w+\s*\W*=/,rB:!0,r:0,starts:{e:/\s*\W*=/,eE:!0,starts:{e:/$/,r:0,c:[t]}}},{cN:"section",b:/^[\w]+:\s*$/},{cN:"meta",b:/^\.PHONY:/,e:/$/,k:{"meta-keyword":".PHONY"},l:/[\.\w]+/},{b:/^\t+/,e:/$/,r:0,c:[e.QSM,t]}]}}),e.registerLanguage("markdown",function(e){return{aliases:["md","mkdown","mkd"],c:[{cN:"section",v:[{b:"^#{1,6}",e:"$"},{b:"^.+?\\n[=-]{2,}$"}]},{b:"<",e:">",sL:"xml",r:0},{cN:"bullet",b:"^([*+-]|(\\d+\\.))\\s+"},{cN:"strong",b:"[*_]{2}.+?[*_]{2}"},{cN:"emphasis",v:[{b:"\\*.+?\\*"},{b:"_.+?_",r:0}]},{cN:"quote",b:"^>\\s+",e:"$"},{cN:"code",v:[{b:"`.+?`"},{b:"^( {4}| )",e:"$",r:0}]},{b:"^[-\\*]{3,}",e:"$"},{b:"\\[.+?\\][\\(\\[].*?[\\)\\]]",rB:!0,c:[{cN:"string",b:"\\[",e:"\\]",eB:!0,rE:!0,r:0},{cN:"link",b:"\\]\\(",e:"\\)",eB:!0,eE:!0},{cN:"symbol",b:"\\]\\[",e:"\\]",eB:!0,eE:!0}],r:10},{b:"^\\[.+\\]:",rB:!0,c:[{cN:"symbol",b:"\\[",e:"\\]:",eB:!0,eE:!0,starts:{cN:"link",e:"$"}}]}]}}),e.registerLanguage("nginx",function(e){var t={cN:"variable",v:[{b:/\$\d+/},{b:/\$\{/,e:/}/},{b:"[\\$\\@]"+e.UIR}]},r={eW:!0,l:"[a-z/_]+",k:{literal:"on off yes no true false none blocked debug info notice warn error crit select break last permanent redirect kqueue rtsig epoll poll /dev/poll"},r:0,i:"=>",c:[e.HCM,{cN:"string",c:[e.BE,t],v:[{b:/"/,e:/"/},{b:/'/,e:/'/}]},{b:"([a-z]+):/",e:"\\s",eW:!0,eE:!0,c:[t]},{cN:"regexp",c:[e.BE,t],v:[{b:"\\s\\^",e:"\\s|{|;",rE:!0},{b:"~\\*?\\s+",e:"\\s|{|;",rE:!0},{b:"\\*(\\.[a-z\\-]+)+"},{b:"([a-z\\-]+\\.)+\\*"}]},{cN:"number",b:"\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(:\\d{1,5})?\\b"},{cN:"number",b:"\\b\\d+[kKmMgGdshdwy]*\\b",r:0},t]};return{aliases:["nginxconf"],c:[e.HCM,{b:e.UIR+"\\s+{",rB:!0,e:"{",c:[{cN:"section",b:e.UIR}],r:0},{b:e.UIR+"\\s",e:";|{",rB:!0,c:[{cN:"attribute",b:e.UIR,starts:r}],r:0}],i:"[^\\s\\}]"}}),e.registerLanguage("php",function(e){var t={b:"\\$+[a-zA-Z_-ÿ][a-zA-Z0-9_-ÿ]*"},r={cN:"meta",b:/<\?(php)?|\?>/},n={cN:"string",c:[e.BE,r],v:[{b:'b"',e:'"'},{b:"b'",e:"'"},e.inherit(e.ASM,{i:null}),e.inherit(e.QSM,{i:null})]},a={v:[e.BNM,e.CNM]};return{aliases:["php3","php4","php5","php6"],cI:!0,k:"and include_once list abstract global private echo interface as static endswitch array null if endwhile or const for endforeach self var while isset public protected exit foreach throw elseif include __FILE__ empty require_once do xor return parent clone use __CLASS__ __LINE__ else break print eval new catch __METHOD__ case exception default die require __FUNCTION__ enddeclare final try switch continue endfor endif declare unset true false trait goto instanceof insteadof __DIR__ __NAMESPACE__ yield finally",c:[e.CLCM,e.HCM,e.C("/\\*","\\*/",{c:[{cN:"doctag",b:"@[A-Za-z]+"},r]}),e.C("__halt_compiler.+?;",!1,{eW:!0,k:"__halt_compiler",l:e.UIR}),{cN:"string",b:/<<<['"]?\w+['"]?$/,e:/^\w+;?$/,c:[e.BE,{cN:"subst",v:[{b:/\$\w+/},{b:/\{\$/,e:/\}/}]}]},r,t,{b:/(::|->)+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/},{cN:"function",bK:"function",e:/[;{]/,eE:!0,i:"\\$|\\[|%",c:[e.UTM,{cN:"params",b:"\\(",e:"\\)",c:["self",t,e.CBCM,n,a]}]},{cN:"class",bK:"class interface",e:"{",eE:!0,i:/[:\(\$"]/,c:[{bK:"extends implements"},e.UTM]},{bK:"namespace",e:";",i:/[\.']/,c:[e.UTM]},{bK:"use",e:";",c:[e.UTM]},{b:"=>"},n,a]}}),e.registerLanguage("powershell",function(e){var t={b:"`[\\s\\S]",r:0},r={cN:"variable",v:[{b:/\$[\w\d][\w\d_:]*/}]},n={cN:"literal",b:/\$(null|true|false)\b/},a={cN:"string",b:/"/,e:/"/,c:[t,r,{cN:"variable",b:/\$[A-z]/,e:/[^A-z]/}]},i={cN:"string",b:/'/,e:/'/};return{aliases:["ps"],l:/-?[A-z\.\-]+/,cI:!0,k:{keyword:"if else foreach return function do while until elseif begin for trap data dynamicparam end break throw param continue finally in switch exit filter try process catch",built_in:"Add-Content Add-History Add-Member Add-PSSnapin Clear-Content Clear-Item Clear-Item Property Clear-Variable Compare-Object ConvertFrom-SecureString Convert-Path ConvertTo-Html ConvertTo-SecureString Copy-Item Copy-ItemProperty Export-Alias Export-Clixml Export-Console Export-Csv ForEach-Object Format-Custom Format-List Format-Table Format-Wide Get-Acl Get-Alias Get-AuthenticodeSignature Get-ChildItem Get-Command Get-Content Get-Credential Get-Culture Get-Date Get-EventLog Get-ExecutionPolicy Get-Help Get-History Get-Host Get-Item Get-ItemProperty Get-Location Get-Member Get-PfxCertificate Get-Process Get-PSDrive Get-PSProvider Get-PSSnapin Get-Service Get-TraceSource Get-UICulture Get-Unique Get-Variable Get-WmiObject Group-Object Import-Alias Import-Clixml Import-Csv Invoke-Expression Invoke-History Invoke-Item Join-Path Measure-Command Measure-Object Move-Item Move-ItemProperty New-Alias New-Item New-ItemProperty New-Object New-PSDrive New-Service New-TimeSpan New-Variable Out-Default Out-File Out-Host Out-Null Out-Printer Out-String Pop-Location Push-Location Read-Host Remove-Item Remove-ItemProperty Remove-PSDrive Remove-PSSnapin Remove-Variable Rename-Item Rename-ItemProperty Resolve-Path Restart-Service Resume-Service Select-Object Select-String Set-Acl Set-Alias Set-AuthenticodeSignature Set-Content Set-Date Set-ExecutionPolicy Set-Item Set-ItemProperty Set-Location Set-PSDebug Set-Service Set-TraceSource Set-Variable Sort-Object Split-Path Start-Service Start-Sleep Start-Transcript Stop-Process Stop-Service Stop-Transcript Suspend-Service Tee-Object Test-Path Trace-Command Update-FormatData Update-TypeData Where-Object Write-Debug Write-Error Write-Host Write-Output Write-Progress Write-Verbose Write-Warning",nomarkup:"-ne -eq -lt -gt -ge -le -not -like -notlike -match -notmatch -contains -notcontains -in -notin -replace"},c:[e.HCM,e.NM,a,i,n,r]}}),e.registerLanguage("python",function(e){var t={cN:"meta",b:/^(>>>|\.\.\.) /},r={cN:"string",c:[e.BE],v:[{b:/(u|b)?r?'''/,e:/'''/,c:[t],r:10},{b:/(u|b)?r?"""/,e:/"""/,c:[t],r:10},{b:/(u|r|ur)'/,e:/'/,r:10},{b:/(u|r|ur)"/,e:/"/,r:10},{b:/(b|br)'/,e:/'/},{b:/(b|br)"/,e:/"/},e.ASM,e.QSM]},n={cN:"number",r:0,v:[{b:e.BNR+"[lLjJ]?"},{b:"\\b(0o[0-7]+)[lLjJ]?"},{b:e.CNR+"[lLjJ]?"}]},a={cN:"params",b:/\(/,e:/\)/,c:["self",t,n,r]};return{aliases:["py","gyp"],k:{keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda async await nonlocal|10 None True False",built_in:"Ellipsis NotImplemented"},i:/(<\/|->|\?)/,c:[t,n,r,e.HCM,{v:[{cN:"function",bK:"def",r:10},{cN:"class",bK:"class"}],e:/:/,i:/[${=;\n,]/,c:[e.UTM,a]},{cN:"meta",b:/^[\t ]*@/,e:/$/},{b:/\b(print|exec)\(/}]}}),e.registerLanguage("scss",function(e){var t="[a-zA-Z-][a-zA-Z0-9_-]*",r={cN:"variable",b:"(\\$"+t+")\\b"},n={cN:"number",b:"#[0-9A-Fa-f]+"};({cN:"attribute",b:"[A-Z\\_\\.\\-]+",e:":",eE:!0,i:"[^\\s]",starts:{eW:!0,eE:!0,c:[n,e.CSSNM,e.QSM,e.ASM,e.CBCM,{cN:"meta",b:"!important"}]}});return{cI:!0,i:"[=/|']",c:[e.CLCM,e.CBCM,{cN:"selector-id",b:"\\#[A-Za-z0-9_-]+",r:0},{cN:"selector-class",b:"\\.[A-Za-z0-9_-]+",r:0},{cN:"selector-attr",b:"\\[",e:"\\]",i:"$"},{cN:"selector-tag",b:"\\b(a|abbr|acronym|address|area|article|aside|audio|b|base|big|blockquote|body|br|button|canvas|caption|cite|code|col|colgroup|command|datalist|dd|del|details|dfn|div|dl|dt|em|embed|fieldset|figcaption|figure|footer|form|frame|frameset|(h[1-6])|head|header|hgroup|hr|html|i|iframe|img|input|ins|kbd|keygen|label|legend|li|link|map|mark|meta|meter|nav|noframes|noscript|object|ol|optgroup|option|output|p|param|pre|progress|q|rp|rt|ruby|samp|script|section|select|small|span|strike|strong|style|sub|sup|table|tbody|td|textarea|tfoot|th|thead|time|title|tr|tt|ul|var|video)\\b", +r:0},{b:":(visited|valid|root|right|required|read-write|read-only|out-range|optional|only-of-type|only-child|nth-of-type|nth-last-of-type|nth-last-child|nth-child|not|link|left|last-of-type|last-child|lang|invalid|indeterminate|in-range|hover|focus|first-of-type|first-line|first-letter|first-child|first|enabled|empty|disabled|default|checked|before|after|active)"},{b:"::(after|before|choices|first-letter|first-line|repeat-index|repeat-item|selection|value)"},r,{cN:"attribute",b:"\\b(z-index|word-wrap|word-spacing|word-break|width|widows|white-space|visibility|vertical-align|unicode-bidi|transition-timing-function|transition-property|transition-duration|transition-delay|transition|transform-style|transform-origin|transform|top|text-underline-position|text-transform|text-shadow|text-rendering|text-overflow|text-indent|text-decoration-style|text-decoration-line|text-decoration-color|text-decoration|text-align-last|text-align|tab-size|table-layout|right|resize|quotes|position|pointer-events|perspective-origin|perspective|page-break-inside|page-break-before|page-break-after|padding-top|padding-right|padding-left|padding-bottom|padding|overflow-y|overflow-x|overflow-wrap|overflow|outline-width|outline-style|outline-offset|outline-color|outline|orphans|order|opacity|object-position|object-fit|normal|none|nav-up|nav-right|nav-left|nav-index|nav-down|min-width|min-height|max-width|max-height|mask|marks|margin-top|margin-right|margin-left|margin-bottom|margin|list-style-type|list-style-position|list-style-image|list-style|line-height|letter-spacing|left|justify-content|initial|inherit|ime-mode|image-orientation|image-resolution|image-rendering|icon|hyphens|height|font-weight|font-variant-ligatures|font-variant|font-style|font-stretch|font-size-adjust|font-size|font-language-override|font-kerning|font-feature-settings|font-family|font|float|flex-wrap|flex-shrink|flex-grow|flex-flow|flex-direction|flex-basis|flex|filter|empty-cells|display|direction|cursor|counter-reset|counter-increment|content|column-width|column-span|column-rule-width|column-rule-style|column-rule-color|column-rule|column-gap|column-fill|column-count|columns|color|clip-path|clip|clear|caption-side|break-inside|break-before|break-after|box-sizing|box-shadow|box-decoration-break|bottom|border-width|border-top-width|border-top-style|border-top-right-radius|border-top-left-radius|border-top-color|border-top|border-style|border-spacing|border-right-width|border-right-style|border-right-color|border-right|border-radius|border-left-width|border-left-style|border-left-color|border-left|border-image-width|border-image-source|border-image-slice|border-image-repeat|border-image-outset|border-image|border-color|border-collapse|border-bottom-width|border-bottom-style|border-bottom-right-radius|border-bottom-left-radius|border-bottom-color|border-bottom|border|background-size|background-repeat|background-position|background-origin|background-image|background-color|background-clip|background-attachment|background-blend-mode|background|backface-visibility|auto|animation-timing-function|animation-play-state|animation-name|animation-iteration-count|animation-fill-mode|animation-duration|animation-direction|animation-delay|animation|align-self|align-items|align-content)\\b",i:"[^\\s]"},{b:"\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b"},{b:":",e:";",c:[r,n,e.CSSNM,e.QSM,e.ASM,{cN:"meta",b:"!important"}]},{b:"@",e:"[{;]",k:"mixin include extend for if else each while charset import debug media page content font-face namespace warn",c:[r,e.QSM,e.ASM,n,e.CSSNM,{b:"\\s[A-Za-z0-9_.-]+",r:0}]}]}}),e.registerLanguage("tex",function(e){var t={cN:"tag",b:/\\/,r:0,c:[{cN:"name",v:[{b:/[a-zA-Zа-яА-я]+[*]?/},{b:/[^a-zA-Zа-яА-я0-9]/}],starts:{eW:!0,r:0,c:[{cN:"string",v:[{b:/\[/,e:/\]/},{b:/\{/,e:/\}/}]},{b:/\s*=\s*/,eW:!0,r:0,c:[{cN:"number",b:/-?\d*\.?\d+(pt|pc|mm|cm|in|dd|cc|ex|em)?/}]}]}}]};return{c:[t,{cN:"formula",c:[t],r:0,v:[{b:/\$\$/,e:/\$\$/},{b:/\$/,e:/\$/}]},e.C("%","$",{r:0})]}}),e.registerLanguage("yaml",function(e){var t={literal:"{ } true false yes no Yes No True False null"},r="^[ \\-]*",n="[a-zA-Z_][\\w\\-]*",a={cN:"attr",v:[{b:r+n+":"},{b:r+'"'+n+'":'},{b:r+"'"+n+"':"}]},i={cN:"template-variable",v:[{b:"{{",e:"}}"},{b:"%{",e:"}"}]},o={cN:"string",r:0,v:[{b:/'/,e:/'/},{b:/"/,e:/"/}],c:[e.BE,i]};return{cI:!0,aliases:["yml","YAML","yaml"],c:[a,{cN:"meta",b:"^---s*$",r:10},{cN:"string",b:"[\\|>] *$",rE:!0,c:o.c,e:a.v[0].b},{b:"<%[%=-]?",e:"[%-]?%>",sL:"ruby",eB:!0,eE:!0,r:0},{cN:"type",b:"!!"+e.UIR},{cN:"meta",b:"&"+e.UIR+"$"},{cN:"meta",b:"\\*"+e.UIR+"$"},{cN:"bullet",b:"^ *-",r:0},o,e.HCM,e.CNM],k:t}}),e}); \ No newline at end of file diff --git a/docs/static/vendor/highlightjs/notes.txt b/docs/static/vendor/highlightjs/notes.txt new file mode 100644 index 000000000..43475e5f9 --- /dev/null +++ b/docs/static/vendor/highlightjs/notes.txt @@ -0,0 +1,13 @@ +Version: 9.0.0 + +Source: + https://github.com/isagalaev/highlight.js + +Also see Hugo commit: + 7cf7f85ad66a1cd35ab4e6026ed6b9da95c53c34. + +Files (there): + src/styles/monokai-sublime.css + +Files (we build): + highlight.pack.js diff --git a/docs/static/vendor/jquery/js/jquery-2.1.4.min.js b/docs/static/vendor/jquery/js/jquery-2.1.4.min.js new file mode 100644 index 000000000..49990d6e1 --- /dev/null +++ b/docs/static/vendor/jquery/js/jquery-2.1.4.min.js @@ -0,0 +1,4 @@ +/*! jQuery v2.1.4 | (c) 2005, 2015 jQuery Foundation, Inc. | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l=a.document,m="2.1.4",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){return!n.isArray(a)&&a-parseFloat(a)+1>=0},isPlainObject:function(a){return"object"!==n.type(a)||a.nodeType||n.isWindow(a)?!1:a.constructor&&!j.call(a.constructor.prototype,"isPrototypeOf")?!1:!0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=l.createElement("script"),b.text=a,l.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:g.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(e=d.call(arguments,2),f=function(){return a.apply(b||this,e.concat(d.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:k}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b="length"in a&&a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N=M.replace("w","w#"),O="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+N+"))|)"+L+"*\\]",P=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+O+")*)|.*)\\)|)",Q=new RegExp(L+"+","g"),R=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),S=new RegExp("^"+L+"*,"+L+"*"),T=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),U=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),V=new RegExp(P),W=new RegExp("^"+N+"$"),X={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+O),PSEUDO:new RegExp("^"+P),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,aa=/[+~]/,ba=/'|\\/g,ca=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),da=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ea=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(fa){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],k=b.nodeType,"string"!=typeof a||!a||1!==k&&9!==k&&11!==k)return d;if(!e&&p){if(11!==k&&(f=_.exec(a)))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return H.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName)return H.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=1!==k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(ba,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+ra(o[l]);w=aa.test(a)&&pa(b.parentNode)||b,x=o.join(",")}if(x)try{return H.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function pa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=g.documentElement,e=g.defaultView,e&&e!==e.top&&(e.addEventListener?e.addEventListener("unload",ea,!1):e.attachEvent&&e.attachEvent("onunload",ea)),p=!f(g),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(g.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(g.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!g.getElementsByName||!g.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ca,da);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ca,da);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(g.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="
    ",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){var b=g.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",P)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===g||a.ownerDocument===v&&t(v,a)?-1:b===g||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,h=[a],i=[b];if(!e||!f)return a===g?-1:b===g?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?la(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},g):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ca,da),a[3]=(a[3]||a[4]||a[5]||"").replace(ca,da),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ca,da).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(Q," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(ca,da),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return W.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(ca,da).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:oa(function(){return[0]}),last:oa(function(a,b){return[b-1]}),eq:oa(function(a,b,c){return[0>c?c+b:c]}),even:oa(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:oa(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:oa(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:oa(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function sa(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function ta(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ua(a,b,c){for(var d=0,e=b.length;e>d;d++)ga(a,b[d],c);return c}function va(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function wa(a,b,c,d,e,f){return d&&!d[u]&&(d=wa(d)),e&&!e[u]&&(e=wa(e,f)),ia(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ua(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:va(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=va(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=va(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function xa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=sa(function(a){return a===b},h,!0),l=sa(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[sa(ta(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return wa(i>1&&ta(m),i>1&&ra(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&xa(a.slice(i,e)),f>e&&xa(a=a.slice(e)),f>e&&ra(a))}m.push(c)}return ta(m)}function ya(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=F.call(i));s=va(s)}H.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&ga.uniqueSort(i)}return k&&(w=v,j=t),r};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=xa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,ya(e,d)),f.selector=a}return f},i=ga.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ca,da),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ca,da),aa.test(j[0].type)&&pa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&ra(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,aa.test(a)&&pa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return g.call(b,a)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:l,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}return d=l.getElementById(c[2]),d&&d.parentNode&&(this.length=1,this[0]=d),this.context=l,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};A.prototype=n.fn,y=n(l);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?g.call(n(a),this[0]):g.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(C[a]||n.unique(e),B.test(a)&&e.reverse()),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return n.each(a.match(E)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(b=a.memory&&l,c=!0,g=e||0,e=0,f=h.length,d=!0;h&&f>g;g++)if(h[g].apply(l[0],l[1])===!1&&a.stopOnFalse){b=!1;break}d=!1,h&&(i?i.length&&j(i.shift()):b?h=[]:k.disable())},k={add:function(){if(h){var c=h.length;!function g(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&g(c)})}(arguments),d?f=h.length:b&&(e=c,j(b))}return this},remove:function(){return h&&n.each(arguments,function(a,b){var c;while((c=n.inArray(b,h,c))>-1)h.splice(c,1),d&&(f>=c&&f--,g>=c&&g--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],f=0,this},disable:function(){return h=i=b=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,b||k.disable(),this},locked:function(){return!i},fireWith:function(a,b){return!h||c&&!i||(b=b||[],b=[a,b.slice?b.slice():b],d?i.push(b):j(b)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!c}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(H.resolveWith(l,[n]),n.fn.triggerHandler&&(n(l).triggerHandler("ready"),n(l).off("ready"))))}});function I(){l.removeEventListener("DOMContentLoaded",I,!1),a.removeEventListener("load",I,!1),n.ready()}n.ready.promise=function(b){return H||(H=n.Deferred(),"complete"===l.readyState?setTimeout(n.ready):(l.addEventListener("DOMContentLoaded",I,!1),a.addEventListener("load",I,!1))),H.promise(b)},n.ready.promise();var J=n.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)n.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f};n.acceptData=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function K(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=n.expando+K.uid++}K.uid=1,K.accepts=n.acceptData,K.prototype={key:function(a){if(!K.accepts(a))return 0;var b={},c=a[this.expando];if(!c){c=K.uid++;try{b[this.expando]={value:c},Object.defineProperties(a,b)}catch(d){b[this.expando]=c,n.extend(a,b)}}return this.cache[c]||(this.cache[c]={}),c},set:function(a,b,c){var d,e=this.key(a),f=this.cache[e];if("string"==typeof b)f[b]=c;else if(n.isEmptyObject(f))n.extend(this.cache[e],b);else for(d in b)f[d]=b[d];return f},get:function(a,b){var c=this.cache[this.key(a)];return void 0===b?c:c[b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=this.key(a),g=this.cache[f];if(void 0===b)this.cache[f]={};else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in g?d=[b,e]:(d=e,d=d in g?[d]:d.match(E)||[])),c=d.length;while(c--)delete g[d[c]]}},hasData:function(a){return!n.isEmptyObject(this.cache[a[this.expando]]||{})},discard:function(a){a[this.expando]&&delete this.cache[a[this.expando]]}};var L=new K,M=new K,N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(O,"-$1").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}M.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return M.hasData(a)||L.hasData(a)},data:function(a,b,c){ +return M.access(a,b,c)},removeData:function(a,b){M.remove(a,b)},_data:function(a,b,c){return L.access(a,b,c)},_removeData:function(a,b){L.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=M.get(f),1===f.nodeType&&!L.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));L.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){M.set(this,a)}):J(this,function(b){var c,d=n.camelCase(a);if(f&&void 0===b){if(c=M.get(f,a),void 0!==c)return c;if(c=M.get(f,d),void 0!==c)return c;if(c=P(f,d,void 0),void 0!==c)return c}else this.each(function(){var c=M.get(this,d);M.set(this,d,b),-1!==a.indexOf("-")&&void 0!==c&&M.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){M.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=L.get(a,b),c&&(!d||n.isArray(c)?d=L.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return L.get(a,c)||L.access(a,c,{empty:n.Callbacks("once memory").add(function(){L.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthx",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var U="undefined";k.focusinBubbles="onfocusin"in a;var V=/^key/,W=/^(?:mouse|pointer|contextmenu)|click/,X=/^(?:focusinfocus|focusoutblur)$/,Y=/^([^.]*)(?:\.(.+)|)$/;function Z(){return!0}function $(){return!1}function _(){try{return l.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return typeof n!==U&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(E)||[""],j=b.length;while(j--)h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g,!1)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.hasData(a)&&L.get(a);if(r&&(i=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&(delete r.handle,L.remove(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,m,o,p=[d||l],q=j.call(b,"type")?b.type:b,r=j.call(b,"namespace")?b.namespace.split("."):[];if(g=h=d=d||l,3!==d.nodeType&&8!==d.nodeType&&!X.test(q+n.event.triggered)&&(q.indexOf(".")>=0&&(r=q.split("."),q=r.shift(),r.sort()),k=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=r.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),o=n.event.special[q]||{},e||!o.trigger||o.trigger.apply(d,c)!==!1)){if(!e&&!o.noBubble&&!n.isWindow(d)){for(i=o.delegateType||q,X.test(i+q)||(g=g.parentNode);g;g=g.parentNode)p.push(g),h=g;h===(d.ownerDocument||l)&&p.push(h.defaultView||h.parentWindow||a)}f=0;while((g=p[f++])&&!b.isPropagationStopped())b.type=f>1?i:o.bindType||q,m=(L.get(g,"events")||{})[b.type]&&L.get(g,"handle"),m&&m.apply(g,c),m=k&&g[k],m&&m.apply&&n.acceptData(g)&&(b.result=m.apply(g,c),b.result===!1&&b.preventDefault());return b.type=q,e||b.isDefaultPrevented()||o._default&&o._default.apply(p.pop(),c)!==!1||!n.acceptData(d)||k&&n.isFunction(d[q])&&!n.isWindow(d)&&(h=d[k],h&&(d[k]=null),n.event.triggered=q,d[q](),n.event.triggered=void 0,h&&(d[k]=h)),b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(L.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(g.namespace))&&(a.handleObj=g,a.data=g.data,e=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(a.result=e)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!==this;i=i.parentNode||this)if(i.disabled!==!0||"click"!==a.type){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>=0:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]*)\/>/gi,ba=/<([\w:]+)/,ca=/<|&#?\w+;/,da=/<(?:script|style|link)/i,ea=/checked\s*(?:[^=]|=\s*.checked.)/i,fa=/^$|\/(?:java|ecma)script/i,ga=/^true\/(.*)/,ha=/^\s*\s*$/g,ia={option:[1,""],thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};ia.optgroup=ia.option,ia.tbody=ia.tfoot=ia.colgroup=ia.caption=ia.thead,ia.th=ia.td;function ja(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function ka(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function la(a){var b=ga.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function ma(a,b){for(var c=0,d=a.length;d>c;c++)L.set(a[c],"globalEval",!b||L.get(b[c],"globalEval"))}function na(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(L.hasData(a)&&(f=L.access(a),g=L.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}M.hasData(a)&&(h=M.access(a),i=n.extend({},h),M.set(b,i))}}function oa(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function pa(a,b){var c=b.nodeName.toLowerCase();"input"===c&&T.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}n.extend({clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=oa(h),f=oa(a),d=0,e=f.length;e>d;d++)pa(f[d],g[d]);if(b)if(c)for(f=f||oa(a),g=g||oa(h),d=0,e=f.length;e>d;d++)na(f[d],g[d]);else na(a,h);return g=oa(h,"script"),g.length>0&&ma(g,!i&&oa(a,"script")),h},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k=b.createDocumentFragment(),l=[],m=0,o=a.length;o>m;m++)if(e=a[m],e||0===e)if("object"===n.type(e))n.merge(l,e.nodeType?[e]:e);else if(ca.test(e)){f=f||k.appendChild(b.createElement("div")),g=(ba.exec(e)||["",""])[1].toLowerCase(),h=ia[g]||ia._default,f.innerHTML=h[1]+e.replace(aa,"<$1>")+h[2],j=h[0];while(j--)f=f.lastChild;n.merge(l,f.childNodes),f=k.firstChild,f.textContent=""}else l.push(b.createTextNode(e));k.textContent="",m=0;while(e=l[m++])if((!d||-1===n.inArray(e,d))&&(i=n.contains(e.ownerDocument,e),f=oa(k.appendChild(e),"script"),i&&ma(f),c)){j=0;while(e=f[j++])fa.test(e.type||"")&&c.push(e)}return k},cleanData:function(a){for(var b,c,d,e,f=n.event.special,g=0;void 0!==(c=a[g]);g++){if(n.acceptData(c)&&(e=c[L.expando],e&&(b=L.cache[e]))){if(b.events)for(d in b.events)f[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);L.cache[e]&&delete L.cache[e]}delete M.cache[c[M.expando]]}}}),n.fn.extend({text:function(a){return J(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=a)})},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=ja(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=ja(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(oa(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&ma(oa(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(oa(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return J(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!da.test(a)&&!ia[(ba.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(aa,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(oa(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(oa(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,m=this,o=l-1,p=a[0],q=n.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&ea.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(c=n.buildFragment(a,this[0].ownerDocument,!1,this),d=c.firstChild,1===c.childNodes.length&&(c=d),d)){for(f=n.map(oa(c,"script"),ka),g=f.length;l>j;j++)h=c,j!==o&&(h=n.clone(h,!0,!0),g&&n.merge(f,oa(h,"script"))),b.call(this[j],h,j);if(g)for(i=f[f.length-1].ownerDocument,n.map(f,la),j=0;g>j;j++)h=f[j],fa.test(h.type||"")&&!L.access(h,"globalEval")&&n.contains(i,h)&&(h.src?n._evalUrl&&n._evalUrl(h.src):n.globalEval(h.textContent.replace(ha,"")))}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),g=e.length-1,h=0;g>=h;h++)c=h===g?this:this.clone(!0),n(e[h])[b](c),f.apply(d,c.get());return this.pushStack(d)}});var qa,ra={};function sa(b,c){var d,e=n(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:n.css(e[0],"display");return e.detach(),f}function ta(a){var b=l,c=ra[a];return c||(c=sa(a,b),"none"!==c&&c||(qa=(qa||n(".*?
    \n", + }, + // set class + { + `{{< youtube w7Ft2ymGmfc video>}}`, + "(?s)\n
    .*?.*?
    \n", + }, + // set class and autoplay (using named params) + { + `{{< youtube id="w7Ft2ymGmfc" class="video" autoplay="true" >}}`, + "(?s)\n
    .*?.*?
    ", + }, + } { + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + writeSource(t, fs, filepath.Join("content", "simple.md"), fmt.Sprintf(`--- +title: Shorty +--- +%s`, this.in)) + writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), `{{ .Content }}`) + + buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + + th.assertFileContentRegexp(filepath.Join("public", "simple", "index.html"), this.expected) + } + +} + +func TestShortcodeVimeo(t *testing.T) { + t.Parallel() + + for _, this := range []struct { + in, expected string + }{ + { + `{{< vimeo 146022717 >}}`, + "(?s)\n
    .*?.*?
    \n", + }, + // set class + { + `{{< vimeo 146022717 video >}}`, + "(?s)\n
    .*?.*?
    \n", + }, + // set class (using named params) + { + `{{< vimeo id="146022717" class="video" >}}`, + "(?s)^
    .*?.*?
    ", + }, + } { + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + writeSource(t, fs, filepath.Join("content", "simple.md"), fmt.Sprintf(`--- +title: Shorty +--- +%s`, this.in)) + writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), `{{ .Content }}`) + + buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + + th.assertFileContentRegexp(filepath.Join("public", "simple", "index.html"), this.expected) + + } +} + +func TestShortcodeGist(t *testing.T) { + t.Parallel() + + for _, this := range []struct { + in, expected string + }{ + { + `{{< gist spf13 7896402 >}}`, + "(?s)^", + }, + { + `{{< gist spf13 7896402 "img.html" >}}`, + "(?s)^", + }, + } { + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + writeSource(t, fs, filepath.Join("content", "simple.md"), fmt.Sprintf(`--- +title: Shorty +--- +%s`, this.in)) + writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), `{{ .Content }}`) + + buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + + th.assertFileContentRegexp(filepath.Join("public", "simple", "index.html"), this.expected) + + } +} + +func TestShortcodeTweet(t *testing.T) { + t.Parallel() + + for i, this := range []struct { + in, resp, expected string + }{ + { + `{{< tweet 666616452582129664 >}}`, + `{"url":"https:\/\/twitter.com\/spf13\/status\/666616452582129664","author_name":"Steve Francia","author_url":"https:\/\/twitter.com\/spf13","html":"\u003Cblockquote class=\"twitter-tweet\"\u003E\u003Cp lang=\"en\" dir=\"ltr\"\u003EHugo 0.15 will have 30%+ faster render times thanks to this commit \u003Ca href=\"https:\/\/t.co\/FfzhM8bNhT\"\u003Ehttps:\/\/t.co\/FfzhM8bNhT\u003C\/a\u003E \u003Ca href=\"https:\/\/twitter.com\/hashtag\/gohugo?src=hash\"\u003E#gohugo\u003C\/a\u003E \u003Ca href=\"https:\/\/twitter.com\/hashtag\/golang?src=hash\"\u003E#golang\u003C\/a\u003E \u003Ca href=\"https:\/\/t.co\/ITbMNU2BUf\"\u003Ehttps:\/\/t.co\/ITbMNU2BUf\u003C\/a\u003E\u003C\/p\u003E— Steve Francia (@spf13) \u003Ca href=\"https:\/\/twitter.com\/spf13\/status\/666616452582129664\"\u003ENovember 17, 2015\u003C\/a\u003E\u003C\/blockquote\u003E\n\u003Cscript async src=\"\/\/platform.twitter.com\/widgets.js\" charset=\"utf-8\"\u003E\u003C\/script\u003E","width":550,"height":null,"type":"rich","cache_age":"3153600000","provider_name":"Twitter","provider_url":"https:\/\/twitter.com","version":"1.0"}`, + `(?s)^.*?`, + }, + } { + // overload getJSON to return mock API response from Twitter + tweetFuncMap := template.FuncMap{ + "getJSON": func(urlParts ...string) interface{} { + var v interface{} + err := json.Unmarshal([]byte(this.resp), &v) + if err != nil { + t.Fatalf("[%d] unexpected error in json.Unmarshal: %s", i, err) + return err + } + return v + }, + } + + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + withTemplate := func(templ tpl.TemplateHandler) error { + templ.(tpl.TemplateTestMocker).SetFuncs(tweetFuncMap) + return nil + } + + writeSource(t, fs, filepath.Join("content", "simple.md"), fmt.Sprintf(`--- +title: Shorty +--- +%s`, this.in)) + writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), `{{ .Content }}`) + + buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg, WithTemplate: withTemplate}, BuildCfg{}) + + th.assertFileContentRegexp(filepath.Join("public", "simple", "index.html"), this.expected) + + } +} + +func TestShortcodeInstagram(t *testing.T) { + t.Parallel() + + for i, this := range []struct { + in, hidecaption, resp, expected string + }{ + { + `{{< instagram BMokmydjG-M >}}`, + `0`, + `{"provider_url": "https://www.instagram.com", "media_id": "1380514280986406796_25025320", "author_name": "instagram", "height": null, "thumbnail_url": "https://scontent-amt2-1.cdninstagram.com/t51.2885-15/s640x640/sh0.08/e35/15048135_1880160212214218_7827880881132929024_n.jpg?ig_cache_key=MTM4MDUxNDI4MDk4NjQwNjc5Ng%3D%3D.2", "thumbnail_width": 640, "thumbnail_height": 640, "provider_name": "Instagram", "title": "Today, we\u2019re introducing a few new tools to help you make your story even more fun: Boomerang and mentions. We\u2019re also starting to test links inside some stories.\nBoomerang lets you turn everyday moments into something fun and unexpected. Now you can easily take a Boomerang right inside Instagram. Swipe right from your feed to open the stories camera. A new format picker under the record button lets you select \u201cBoomerang\u201d mode.\nYou can also now share who you\u2019re with or who you\u2019re thinking of by mentioning them in your story. When you add text to your story, type \u201c@\u201d followed by a username and select the person you\u2019d like to mention. Their username will appear underlined in your story. And when someone taps the mention, they'll see a pop-up that takes them to that profile.\nYou may begin to spot \u201cSee More\u201d links at the bottom of some stories. This is a test that lets verified accounts add links so it\u2019s easy to learn more. From your favorite chefs\u2019 recipes to articles from top journalists or concert dates from the musicians you love, tap \u201cSee More\u201d or swipe up to view the link right inside the app.\nTo learn more about today\u2019s updates, check out help.instagram.com.\nThese updates for Instagram Stories are available as part of Instagram version 9.7 available for iOS in the Apple App Store, for Android in Google Play and for Windows 10 in the Windows Store.", "html": "\u003cblockquote class=\"instagram-media\" data-instgrm-captioned data-instgrm-version=\"7\" style=\" background:#FFF; border:0; border-radius:3px; box-shadow:0 0 1px 0 rgba(0,0,0,0.5),0 1px 10px 0 rgba(0,0,0,0.15); margin: 1px; max-width:658px; padding:0; width:99.375%; width:-webkit-calc(100% - 2px); width:calc(100% - 2px);\"\u003e\u003cdiv style=\"padding:8px;\"\u003e \u003cdiv style=\" background:#F8F8F8; line-height:0; margin-top:40px; padding:50.0% 0; text-align:center; width:100%;\"\u003e \u003cdiv style=\" background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAsCAMAAAApWqozAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAMUExURczMzPf399fX1+bm5mzY9AMAAADiSURBVDjLvZXbEsMgCES5/P8/t9FuRVCRmU73JWlzosgSIIZURCjo/ad+EQJJB4Hv8BFt+IDpQoCx1wjOSBFhh2XssxEIYn3ulI/6MNReE07UIWJEv8UEOWDS88LY97kqyTliJKKtuYBbruAyVh5wOHiXmpi5we58Ek028czwyuQdLKPG1Bkb4NnM+VeAnfHqn1k4+GPT6uGQcvu2h2OVuIf/gWUFyy8OWEpdyZSa3aVCqpVoVvzZZ2VTnn2wU8qzVjDDetO90GSy9mVLqtgYSy231MxrY6I2gGqjrTY0L8fxCxfCBbhWrsYYAAAAAElFTkSuQmCC); display:block; height:44px; margin:0 auto -44px; position:relative; top:-22px; width:44px;\"\u003e\u003c/div\u003e\u003c/div\u003e \u003cp style=\" margin:8px 0 0 0; padding:0 4px;\"\u003e \u003ca href=\"https://www.instagram.com/p/BMokmydjG-M/\" style=\" color:#000; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:normal; line-height:17px; text-decoration:none; word-wrap:break-word;\" target=\"_blank\"\u003eToday, we\u2019re introducing a few new tools to help you make your story even more fun: Boomerang and mentions. We\u2019re also starting to test links inside some stories. Boomerang lets you turn everyday moments into something fun and unexpected. Now you can easily take a Boomerang right inside Instagram. Swipe right from your feed to open the stories camera. A new format picker under the record button lets you select \u201cBoomerang\u201d mode. You can also now share who you\u2019re with or who you\u2019re thinking of by mentioning them in your story. When you add text to your story, type \u201c@\u201d followed by a username and select the person you\u2019d like to mention. Their username will appear underlined in your story. And when someone taps the mention, they\u0026#39;ll see a pop-up that takes them to that profile. You may begin to spot \u201cSee More\u201d links at the bottom of some stories. This is a test that lets verified accounts add links so it\u2019s easy to learn more. From your favorite chefs\u2019 recipes to articles from top journalists or concert dates from the musicians you love, tap \u201cSee More\u201d or swipe up to view the link right inside the app. To learn more about today\u2019s updates, check out help.instagram.com. These updates for Instagram Stories are available as part of Instagram version 9.7 available for iOS in the Apple App Store, for Android in Google Play and for Windows 10 in the Windows Store.\u003c/a\u003e\u003c/p\u003e \u003cp style=\" color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; line-height:17px; margin-bottom:0; margin-top:8px; overflow:hidden; padding:8px 0 7px; text-align:center; text-overflow:ellipsis; white-space:nowrap;\"\u003eA photo posted by Instagram (@instagram) on \u003ctime style=\" font-family:Arial,sans-serif; font-size:14px; line-height:17px;\" datetime=\"2016-11-10T15:02:28+00:00\"\u003eNov 10, 2016 at 7:02am PST\u003c/time\u003e\u003c/p\u003e\u003c/div\u003e\u003c/blockquote\u003e\n\u003cscript async defer src=\"//platform.instagram.com/en_US/embeds.js\"\u003e\u003c/script\u003e", "width": 658, "version": "1.0", "author_url": "https://www.instagram.com/instagram", "author_id": 25025320, "type": "rich"}`, + `(?s)
    `, + }, + { + `{{< instagram BMokmydjG-M hidecaption >}}`, + `1`, + `{"provider_url": "https://www.instagram.com", "media_id": "1380514280986406796_25025320", "author_name": "instagram", "height": null, "thumbnail_url": "https://scontent-amt2-1.cdninstagram.com/t51.2885-15/s640x640/sh0.08/e35/15048135_1880160212214218_7827880881132929024_n.jpg?ig_cache_key=MTM4MDUxNDI4MDk4NjQwNjc5Ng%3D%3D.2", "thumbnail_width": 640, "thumbnail_height": 640, "provider_name": "Instagram", "title": "Today, we\u2019re introducing a few new tools to help you make your story even more fun: Boomerang and mentions. We\u2019re also starting to test links inside some stories.\nBoomerang lets you turn everyday moments into something fun and unexpected. Now you can easily take a Boomerang right inside Instagram. Swipe right from your feed to open the stories camera. A new format picker under the record button lets you select \u201cBoomerang\u201d mode.\nYou can also now share who you\u2019re with or who you\u2019re thinking of by mentioning them in your story. When you add text to your story, type \u201c@\u201d followed by a username and select the person you\u2019d like to mention. Their username will appear underlined in your story. And when someone taps the mention, they'll see a pop-up that takes them to that profile.\nYou may begin to spot \u201cSee More\u201d links at the bottom of some stories. This is a test that lets verified accounts add links so it\u2019s easy to learn more. From your favorite chefs\u2019 recipes to articles from top journalists or concert dates from the musicians you love, tap \u201cSee More\u201d or swipe up to view the link right inside the app.\nTo learn more about today\u2019s updates, check out help.instagram.com.\nThese updates for Instagram Stories are available as part of Instagram version 9.7 available for iOS in the Apple App Store, for Android in Google Play and for Windows 10 in the Windows Store.", "html": "\u003cblockquote class=\"instagram-media\" data-instgrm-version=\"7\" style=\" background:#FFF; border:0; border-radius:3px; box-shadow:0 0 1px 0 rgba(0,0,0,0.5),0 1px 10px 0 rgba(0,0,0,0.15); margin: 1px; max-width:658px; padding:0; width:99.375%; width:-webkit-calc(100% - 2px); width:calc(100% - 2px);\"\u003e\u003cdiv style=\"padding:8px;\"\u003e \u003cdiv style=\" background:#F8F8F8; line-height:0; margin-top:40px; padding:50.0% 0; text-align:center; width:100%;\"\u003e \u003cdiv style=\" background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAsCAMAAAApWqozAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAMUExURczMzPf399fX1+bm5mzY9AMAAADiSURBVDjLvZXbEsMgCES5/P8/t9FuRVCRmU73JWlzosgSIIZURCjo/ad+EQJJB4Hv8BFt+IDpQoCx1wjOSBFhh2XssxEIYn3ulI/6MNReE07UIWJEv8UEOWDS88LY97kqyTliJKKtuYBbruAyVh5wOHiXmpi5we58Ek028czwyuQdLKPG1Bkb4NnM+VeAnfHqn1k4+GPT6uGQcvu2h2OVuIf/gWUFyy8OWEpdyZSa3aVCqpVoVvzZZ2VTnn2wU8qzVjDDetO90GSy9mVLqtgYSy231MxrY6I2gGqjrTY0L8fxCxfCBbhWrsYYAAAAAElFTkSuQmCC); display:block; height:44px; margin:0 auto -44px; position:relative; top:-22px; width:44px;\"\u003e\u003c/div\u003e\u003c/div\u003e\u003cp style=\" color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; line-height:17px; margin-bottom:0; margin-top:8px; overflow:hidden; padding:8px 0 7px; text-align:center; text-overflow:ellipsis; white-space:nowrap;\"\u003e\u003ca href=\"https://www.instagram.com/p/BMokmydjG-M/\" style=\" color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:normal; line-height:17px; text-decoration:none;\" target=\"_blank\"\u003eA photo posted by Instagram (@instagram)\u003c/a\u003e on \u003ctime style=\" font-family:Arial,sans-serif; font-size:14px; line-height:17px;\" datetime=\"2016-11-10T15:02:28+00:00\"\u003eNov 10, 2016 at 7:02am PST\u003c/time\u003e\u003c/p\u003e\u003c/div\u003e\u003c/blockquote\u003e\n\u003cscript async defer src=\"//platform.instagram.com/en_US/embeds.js\"\u003e\u003c/script\u003e", "width": 658, "version": "1.0", "author_url": "https://www.instagram.com/instagram", "author_id": 25025320, "type": "rich"}`, + `(?s)
    `, + }, + } { + // overload getJSON to return mock API response from Instagram + instagramFuncMap := template.FuncMap{ + "getJSON": func(urlParts ...string) interface{} { + var v interface{} + err := json.Unmarshal([]byte(this.resp), &v) + if err != nil { + t.Fatalf("[%d] unexpected error in json.Unmarshal: %s", i, err) + return err + } + return v + }, + } + + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + withTemplate := func(templ tpl.TemplateHandler) error { + templ.(tpl.TemplateTestMocker).SetFuncs(instagramFuncMap) + return nil + } + + writeSource(t, fs, filepath.Join("content", "simple.md"), fmt.Sprintf(`--- +title: Shorty +--- +%s`, this.in)) + writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), `{{ .Content | safeHTML }}`) + + buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg, WithTemplate: withTemplate}, BuildCfg{}) + + th.assertFileContentRegexp(filepath.Join("public", "simple", "index.html"), this.expected) + + } +} diff --git a/hugolib/embedded_templates_test.go b/hugolib/embedded_templates_test.go deleted file mode 100644 index ec59751f3..000000000 --- a/hugolib/embedded_templates_test.go +++ /dev/null @@ -1,135 +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 hugolib - -import ( - "testing" -) - -func TestInternalTemplatesImage(t *testing.T) { - config := ` -baseURL = "https://example.org" - -[params] -images=["siteimg1.jpg", "siteimg2.jpg"] - -` - b := newTestSitesBuilder(t).WithConfigFile("toml", config) - - b.WithContent("mybundle/index.md", `--- -title: My Bundle -date: 2021-02-26T18:02:00-01:00 -lastmod: 2021-05-22T19:25:00-01:00 ---- -`) - - b.WithContent("mypage/index.md", `--- -title: My Page -images: ["pageimg1.jpg", "pageimg2.jpg", "https://example.local/logo.png", "sample.jpg"] -date: 2021-02-26T18:02:00+01:00 -lastmod: 2021-05-22T19:25:00+01:00 ---- -`) - - b.WithContent("mysite.md", `--- -title: My Site ---- -`) - - b.WithTemplatesAdded("_default/single.html", ` - -{{ template "_internal/twitter_cards.html" . }} -{{ template "_internal/opengraph.html" . }} -{{ template "_internal/schema.html" . }} - -`) - - b.WithSunset("content/mybundle/featured-sunset.jpg") - b.WithSunset("content/mypage/sample.jpg") - b.Build(BuildCfg{}) - - b.AssertFileContent("public/mybundle/index.html", ` - - - - - - - - - - - - -`) - b.AssertFileContent("public/mypage/index.html", ` - - - - - - - - - - - - - -`) - b.AssertFileContent("public/mysite/index.html", ` - - - -`) -} - -func TestEmbeddedPaginationTemplate(t *testing.T) { - t.Parallel() - - test := func(variant string, expectedOutput string) { - b := newTestSitesBuilder(t) - b.WithConfigFile("toml", `pagination.pagerSize = 1`) - b.WithContent( - "s1/p01.md", "---\ntitle: p01\n---", - "s1/p02.md", "---\ntitle: p02\n---", - "s1/p03.md", "---\ntitle: p03\n---", - "s1/p04.md", "---\ntitle: p04\n---", - "s1/p05.md", "---\ntitle: p05\n---", - "s1/p06.md", "---\ntitle: p06\n---", - "s1/p07.md", "---\ntitle: p07\n---", - "s1/p08.md", "---\ntitle: p08\n---", - "s1/p09.md", "---\ntitle: p09\n---", - "s1/p10.md", "---\ntitle: p10\n---", - ) - b.WithTemplates("index.html", `{{ .Paginate (where site.RegularPages "Section" "s1") }}`+variant) - b.Build(BuildCfg{}) - b.AssertFileContent("public/index.html", expectedOutput) - } - - expectedOutputDefaultFormat := "Pager 1\n
      \n
    • \n ««\n
    • \n
    • \n «\n
    • \n
    • \n 1\n
    • \n
    • \n 2\n
    • \n
    • \n 3\n
    • \n
    • \n 4\n
    • \n
    • \n 5\n
    • \n
    • \n »\n
    • \n
    • \n »»\n
    • \n
    " - expectedOutputTerseFormat := "Pager 1\n
      \n
    • \n 1\n
    • \n
    • \n 2\n
    • \n
    • \n 3\n
    • \n
    • \n »\n
    • \n
    • \n »»\n
    • \n
    " - - variant := `{{ template "_internal/pagination.html" . }}` - test(variant, expectedOutputDefaultFormat) - - variant = `{{ template "_internal/pagination.html" (dict "page" .) }}` - test(variant, expectedOutputDefaultFormat) - - variant = `{{ template "_internal/pagination.html" (dict "page" . "format" "default") }}` - test(variant, expectedOutputDefaultFormat) - - variant = `{{ template "_internal/pagination.html" (dict "page" . "format" "terse") }}` - test(variant, expectedOutputTerseFormat) -} diff --git a/hugolib/fileInfo.go b/hugolib/fileInfo.go deleted file mode 100644 index a01b37008..000000000 --- a/hugolib/fileInfo.go +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2017-present 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 hugolib - -import ( - "fmt" - - "github.com/spf13/afero" - - "github.com/gohugoio/hugo/source" -) - -type fileInfo struct { - *source.File - - overriddenLang string -} - -func (fi *fileInfo) Open() (afero.File, error) { - f, err := fi.FileInfo().Meta().Open() - if err != nil { - err = fmt.Errorf("fileInfo: %w", err) - } - - return f, err -} - -func (fi *fileInfo) Lang() string { - if fi.overriddenLang != "" { - return fi.overriddenLang - } - return fi.File.Lang() -} - -func (fi *fileInfo) String() string { - if fi == nil || fi.File == nil { - return "" - } - return fi.Path() -} diff --git a/hugolib/fileInfo_test.go b/hugolib/fileInfo_test.go deleted file mode 100644 index d8a70e9d3..000000000 --- a/hugolib/fileInfo_test.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2019 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 hugolib - -import ( - "testing" - - qt "github.com/frankban/quicktest" - "github.com/spf13/cast" -) - -func TestFileInfo(t *testing.T) { - t.Run("String", func(t *testing.T) { - t.Parallel() - c := qt.New(t) - fi := &fileInfo{} - _, err := cast.ToStringE(fi) - c.Assert(err, qt.IsNil) - }) -} diff --git a/hugolib/filesystems/basefs.go b/hugolib/filesystems/basefs.go deleted file mode 100644 index b32b8796f..000000000 --- a/hugolib/filesystems/basefs.go +++ /dev/null @@ -1,851 +0,0 @@ -// 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 filesystems provides the fine grained file systems used by Hugo. These -// are typically virtual filesystems that are composites of project and theme content. -package filesystems - -import ( - "fmt" - "io" - "os" - "path/filepath" - "strings" - "sync" - - "github.com/bep/overlayfs" - "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/htesting" - "github.com/gohugoio/hugo/hugofs/glob" - - "github.com/gohugoio/hugo/common/herrors" - "github.com/gohugoio/hugo/common/loggers" - "github.com/gohugoio/hugo/common/types" - - "github.com/rogpeppe/go-internal/lockedfile" - - "github.com/gohugoio/hugo/hugofs/files" - - "github.com/gohugoio/hugo/modules" - - hpaths "github.com/gohugoio/hugo/common/paths" - "github.com/gohugoio/hugo/hugofs" - "github.com/gohugoio/hugo/hugolib/paths" - "github.com/spf13/afero" -) - -const ( - // Used to control concurrency between multiple Hugo instances, e.g. - // a running server and building new content with 'hugo new'. - // It's placed in the project root. - lockFileBuild = ".hugo_build.lock" -) - -var filePathSeparator = string(filepath.Separator) - -// BaseFs contains the core base filesystems used by Hugo. The name "base" is used -// to underline that even if they can be composites, they all have a base path set to a specific -// resource folder, e.g "/my-project/content". So, no absolute filenames needed. -type BaseFs struct { - // SourceFilesystems contains the different source file systems. - *SourceFilesystems - - // The source filesystem (needs absolute filenames). - SourceFs afero.Fs - - // The project source. - ProjectSourceFs afero.Fs - - // The filesystem used to publish the rendered site. - // This usually maps to /my-project/public. - PublishFs afero.Fs - - // The filesystem used for static files. - PublishFsStatic afero.Fs - - // A read-only filesystem starting from the project workDir. - WorkDir afero.Fs - - theBigFs *filesystemsCollector - - workingDir string - - // Locks. - buildMu Lockable // /.hugo_build.lock -} - -type Lockable interface { - Lock() (unlock func(), err error) -} - -type fakeLockfileMutex struct { - mu sync.Mutex -} - -func (f *fakeLockfileMutex) Lock() (func(), error) { - f.mu.Lock() - return func() { f.mu.Unlock() }, nil -} - -// Tries to acquire a build lock. -func (b *BaseFs) LockBuild() (unlock func(), err error) { - return b.buildMu.Lock() -} - -func (b *BaseFs) WatchFilenames() []string { - var filenames []string - sourceFs := b.SourceFs - - for _, rfs := range b.RootFss { - for _, component := range files.ComponentFolders { - fis, err := rfs.Mounts(component) - if err != nil { - continue - } - - for _, fim := range fis { - meta := fim.Meta() - if !meta.Watch { - continue - } - - if !fim.IsDir() { - filenames = append(filenames, meta.Filename) - continue - } - - w := hugofs.NewWalkway(hugofs.WalkwayConfig{ - Fs: sourceFs, - Root: meta.Filename, - WalkFn: func(path string, fi hugofs.FileMetaInfo) error { - if !fi.IsDir() { - return nil - } - if fi.Name() == ".git" || - fi.Name() == "node_modules" || fi.Name() == "bower_components" { - return filepath.SkipDir - } - filenames = append(filenames, fi.Meta().Filename) - return nil - }, - }) - - w.Walk() - } - - } - } - - return filenames -} - -func (b *BaseFs) mountsForComponent(component string) []hugofs.FileMetaInfo { - var result []hugofs.FileMetaInfo - for _, rfs := range b.RootFss { - dirs, err := rfs.Mounts(component) - if err == nil { - result = append(result, dirs...) - } - } - return result -} - -// AbsProjectContentDir tries to construct a filename below the most -// relevant content directory. -func (b *BaseFs) AbsProjectContentDir(filename string) (string, string, error) { - isAbs := filepath.IsAbs(filename) - for _, fi := range b.mountsForComponent(files.ComponentFolderContent) { - if !fi.IsDir() { - continue - } - meta := fi.Meta() - if !meta.IsProject { - continue - } - - if isAbs { - if strings.HasPrefix(filename, meta.Filename) { - return strings.TrimPrefix(filename, meta.Filename+filePathSeparator), filename, nil - } - } else { - contentDir := strings.TrimPrefix(strings.TrimPrefix(meta.Filename, meta.BaseDir), filePathSeparator) + filePathSeparator - - if strings.HasPrefix(filename, contentDir) { - relFilename := strings.TrimPrefix(filename, contentDir) - absFilename := filepath.Join(meta.Filename, relFilename) - return relFilename, absFilename, nil - } - } - - } - - if !isAbs { - // A filename on the form "posts/mypage.md", put it inside - // the first content folder, usually /content. - // Pick the first project dir (which is probably the most important one). - for _, dir := range b.SourceFilesystems.Content.mounts() { - if !dir.IsDir() { - continue - } - meta := dir.Meta() - if meta.IsProject { - return filename, filepath.Join(meta.Filename, filename), nil - } - } - } - - return "", "", fmt.Errorf("could not determine content directory for %q", filename) -} - -// ResolveJSConfigFile resolves the JS-related config file to a absolute -// filename. One example of such would be postcss.config.js. -func (b *BaseFs) ResolveJSConfigFile(name string) string { - // First look in assets/_jsconfig - fi, err := b.Assets.Fs.Stat(filepath.Join(files.FolderJSConfig, name)) - if err == nil { - return fi.(hugofs.FileMetaInfo).Meta().Filename - } - // Fall back to the work dir. - fi, err = b.Work.Stat(name) - if err == nil { - return fi.(hugofs.FileMetaInfo).Meta().Filename - } - - return "" -} - -// SourceFilesystems contains the different source file systems. These can be -// composite file systems (theme and project etc.), and they have all root -// set to the source type the provides: data, i18n, static, layouts. -type SourceFilesystems struct { - Content *SourceFilesystem - Data *SourceFilesystem - I18n *SourceFilesystem - Layouts *SourceFilesystem - Archetypes *SourceFilesystem - Assets *SourceFilesystem - - AssetsWithDuplicatesPreserved *SourceFilesystem - - RootFss []*hugofs.RootMappingFs - - // Writable filesystem on top the project's resources directory, - // with any sub module's resource fs layered below. - ResourcesCache afero.Fs - - // The work folder (may be a composite of project and theme components). - Work afero.Fs - - // When in multihost we have one static filesystem per language. The sync - // static files is currently done outside of the Hugo build (where there is - // a concept of a site per language). - // When in non-multihost mode there will be one entry in this map with a blank key. - Static map[string]*SourceFilesystem - - conf config.AllProvider -} - -// A SourceFilesystem holds the filesystem for a given source type in Hugo (data, -// i18n, layouts, static) and additional metadata to be able to use that filesystem -// in server mode. -type SourceFilesystem struct { - // Name matches one in files.ComponentFolders - Name string - - // This is a virtual composite filesystem. It expects path relative to a context. - Fs afero.Fs - - // The source filesystem (usually the OS filesystem). - SourceFs afero.Fs - - // When syncing a source folder to the target (e.g. /public), this may - // be set to publish into a subfolder. This is used for static syncing - // in multihost mode. - PublishFolder string -} - -// StaticFs returns the static filesystem for the given language. -// This can be a composite filesystem. -func (s SourceFilesystems) StaticFs(lang string) afero.Fs { - var staticFs afero.Fs = hugofs.NoOpFs - - if fs, ok := s.Static[lang]; ok { - staticFs = fs.Fs - } else if fs, ok := s.Static[""]; ok { - staticFs = fs.Fs - } - - return staticFs -} - -// StatResource looks for a resource in these filesystems in order: static, assets and finally content. -// If found in any of them, it returns FileInfo and the relevant filesystem. -// Any non herrors.IsNotExist error will be returned. -// An herrors.IsNotExist error will be returned only if all filesystems return such an error. -// Note that if we only wanted to find the file, we could create a composite Afero fs, -// but we also need to know which filesystem root it lives in. -func (s SourceFilesystems) StatResource(lang, filename string) (fi os.FileInfo, fs afero.Fs, err error) { - for _, fsToCheck := range []afero.Fs{s.StaticFs(lang), s.Assets.Fs, s.Content.Fs} { - fs = fsToCheck - fi, err = fs.Stat(filename) - if err == nil || !herrors.IsNotExist(err) { - return - } - } - // Not found. - return -} - -// IsStatic returns true if the given filename is a member of one of the static -// filesystems. -func (s SourceFilesystems) IsStatic(filename string) bool { - for _, staticFs := range s.Static { - if staticFs.Contains(filename) { - return true - } - } - return false -} - -// IsContent returns true if the given filename is a member of the content filesystem. -func (s SourceFilesystems) IsContent(filename string) bool { - return s.Content.Contains(filename) -} - -// ResolvePaths resolves the given filename to a list of paths in the filesystems. -func (s *SourceFilesystems) ResolvePaths(filename string) []hugofs.ComponentPath { - var cpss []hugofs.ComponentPath - for _, rfs := range s.RootFss { - cps, err := rfs.ReverseLookup(filename) - if err != nil { - panic(err) - } - cpss = append(cpss, cps...) - } - return cpss -} - -// MakeStaticPathRelative makes an absolute static filename into a relative one. -// It will return an empty string if the filename is not a member of a static filesystem. -func (s SourceFilesystems) MakeStaticPathRelative(filename string) string { - for _, staticFs := range s.Static { - rel, _ := staticFs.MakePathRelative(filename, true) - if rel != "" { - return rel - } - } - return "" -} - -// MakePathRelative creates a relative path from the given filename. -func (d *SourceFilesystem) MakePathRelative(filename string, checkExists bool) (string, bool) { - cps, err := d.ReverseLookup(filename, checkExists) - if err != nil { - panic(err) - } - if len(cps) == 0 { - return "", false - } - - return filepath.FromSlash(cps[0].Path), true -} - -// ReverseLookup returns the component paths for the given filename. -func (d *SourceFilesystem) ReverseLookup(filename string, checkExists bool) ([]hugofs.ComponentPath, error) { - var cps []hugofs.ComponentPath - hugofs.WalkFilesystems(d.Fs, func(fs afero.Fs) bool { - if rfs, ok := fs.(hugofs.ReverseLookupProvder); ok { - if c, err := rfs.ReverseLookupComponent(d.Name, filename); err == nil { - if checkExists { - n := 0 - for _, cp := range c { - if _, err := d.Fs.Stat(filepath.FromSlash(cp.Path)); err == nil { - c[n] = cp - n++ - } - } - c = c[:n] - } - cps = append(cps, c...) - } - } - return false - }) - return cps, nil -} - -func (d *SourceFilesystem) mounts() []hugofs.FileMetaInfo { - var m []hugofs.FileMetaInfo - hugofs.WalkFilesystems(d.Fs, func(fs afero.Fs) bool { - if rfs, ok := fs.(*hugofs.RootMappingFs); ok { - mounts, err := rfs.Mounts(d.Name) - if err == nil { - m = append(m, mounts...) - } - } - return false - }) - - // Filter out any mounts not belonging to this filesystem. - // TODO(bep) I think this is superfluous. - n := 0 - for _, mm := range m { - if mm.Meta().Component == d.Name { - m[n] = mm - n++ - } - } - m = m[:n] - - return m -} - -func (d *SourceFilesystem) RealFilename(rel string) string { - fi, err := d.Fs.Stat(rel) - if err != nil { - return rel - } - if realfi, ok := fi.(hugofs.FileMetaInfo); ok { - return realfi.Meta().Filename - } - - return rel -} - -// Contains returns whether the given filename is a member of the current filesystem. -func (d *SourceFilesystem) Contains(filename string) bool { - for _, dir := range d.mounts() { - if !dir.IsDir() { - continue - } - if strings.HasPrefix(filename, dir.Meta().Filename) { - return true - } - } - return false -} - -// RealDirs gets a list of absolute paths to directories starting from the given -// path. -func (d *SourceFilesystem) RealDirs(from string) []string { - var dirnames []string - for _, m := range d.mounts() { - if !m.IsDir() { - continue - } - dirname := filepath.Join(m.Meta().Filename, from) - if _, err := d.SourceFs.Stat(dirname); err == nil { - dirnames = append(dirnames, dirname) - } - } - return dirnames -} - -// WithBaseFs allows reuse of some potentially expensive to create parts that remain -// the same across sites/languages. -func WithBaseFs(b *BaseFs) func(*BaseFs) error { - return func(bb *BaseFs) error { - bb.theBigFs = b.theBigFs - bb.SourceFilesystems = b.SourceFilesystems - return nil - } -} - -// NewBase builds the filesystems used by Hugo given the paths and options provided.NewBase -func NewBase(p *paths.Paths, logger loggers.Logger, options ...func(*BaseFs) error) (*BaseFs, error) { - fs := p.Fs - if logger == nil { - logger = loggers.NewDefault() - } - - publishFs := hugofs.NewBaseFileDecorator(fs.PublishDir) - projectSourceFs := hugofs.NewBaseFileDecorator(hugofs.NewBasePathFs(fs.Source, p.Cfg.BaseConfig().WorkingDir)) - sourceFs := hugofs.NewBaseFileDecorator(fs.Source) - publishFsStatic := fs.PublishDirStatic - - var buildMu Lockable - if p.Cfg.NoBuildLock() || htesting.IsTest { - buildMu = &fakeLockfileMutex{} - } else { - buildMu = lockedfile.MutexAt(filepath.Join(p.Cfg.BaseConfig().WorkingDir, lockFileBuild)) - } - - b := &BaseFs{ - SourceFs: sourceFs, - ProjectSourceFs: projectSourceFs, - WorkDir: fs.WorkingDirReadOnly, - PublishFs: publishFs, - PublishFsStatic: publishFsStatic, - workingDir: p.Cfg.BaseConfig().WorkingDir, - buildMu: buildMu, - } - - for _, opt := range options { - if err := opt(b); err != nil { - return nil, err - } - } - - if b.theBigFs != nil && b.SourceFilesystems != nil { - return b, nil - } - - builder := newSourceFilesystemsBuilder(p, logger, b) - sourceFilesystems, err := builder.Build() - if err != nil { - return nil, fmt.Errorf("build filesystems: %w", err) - } - - b.SourceFilesystems = sourceFilesystems - b.theBigFs = builder.theBigFs - - return b, nil -} - -type sourceFilesystemsBuilder struct { - logger loggers.Logger - p *paths.Paths - sourceFs afero.Fs - result *SourceFilesystems - theBigFs *filesystemsCollector -} - -func newSourceFilesystemsBuilder(p *paths.Paths, logger loggers.Logger, b *BaseFs) *sourceFilesystemsBuilder { - sourceFs := hugofs.NewBaseFileDecorator(p.Fs.Source) - return &sourceFilesystemsBuilder{ - p: p, logger: logger, sourceFs: sourceFs, theBigFs: b.theBigFs, - result: &SourceFilesystems{ - conf: p.Cfg, - }, - } -} - -func (b *sourceFilesystemsBuilder) newSourceFilesystem(name string, fs afero.Fs) *SourceFilesystem { - return &SourceFilesystem{ - Name: name, - Fs: fs, - SourceFs: b.sourceFs, - } -} - -func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) { - if b.theBigFs == nil { - theBigFs, err := b.createMainOverlayFs(b.p) - if err != nil { - return nil, fmt.Errorf("create main fs: %w", err) - } - - b.theBigFs = theBigFs - } - - createView := func(componentID string, overlayFs *overlayfs.OverlayFs) *SourceFilesystem { - if b.theBigFs == nil || b.theBigFs.overlayMounts == nil { - return b.newSourceFilesystem(componentID, hugofs.NoOpFs) - } - - fs := hugofs.NewComponentFs( - hugofs.ComponentFsOptions{ - Fs: overlayFs, - Component: componentID, - DefaultContentLanguage: b.p.Cfg.DefaultContentLanguage(), - PathParser: b.p.Cfg.PathParser(), - }, - ) - - return b.newSourceFilesystem(componentID, fs) - } - - b.result.Archetypes = createView(files.ComponentFolderArchetypes, b.theBigFs.overlayMounts) - b.result.Layouts = createView(files.ComponentFolderLayouts, b.theBigFs.overlayMounts) - b.result.Assets = createView(files.ComponentFolderAssets, b.theBigFs.overlayMounts) - b.result.ResourcesCache = b.theBigFs.overlayResources - b.result.RootFss = b.theBigFs.rootFss - - // data and i18n needs a different merge strategy. - overlayMountsPreserveDupes := b.theBigFs.overlayMounts.WithDirsMerger(hugofs.AppendDirsMerger) - b.result.Data = createView(files.ComponentFolderData, overlayMountsPreserveDupes) - b.result.I18n = createView(files.ComponentFolderI18n, overlayMountsPreserveDupes) - b.result.AssetsWithDuplicatesPreserved = createView(files.ComponentFolderAssets, overlayMountsPreserveDupes) - - contentFs := hugofs.NewComponentFs( - hugofs.ComponentFsOptions{ - Fs: b.theBigFs.overlayMountsContent, - Component: files.ComponentFolderContent, - DefaultContentLanguage: b.p.Cfg.DefaultContentLanguage(), - PathParser: b.p.Cfg.PathParser(), - }, - ) - - b.result.Content = b.newSourceFilesystem(files.ComponentFolderContent, contentFs) - b.result.Work = hugofs.NewReadOnlyFs(b.theBigFs.overlayFull) - - // Create static filesystem(s) - ms := make(map[string]*SourceFilesystem) - b.result.Static = ms - - if b.theBigFs.staticPerLanguage != nil { - // Multihost mode - for k, v := range b.theBigFs.staticPerLanguage { - sfs := b.newSourceFilesystem(files.ComponentFolderStatic, v) - sfs.PublishFolder = k - ms[k] = sfs - } - } else { - bfs := hugofs.NewBasePathFs(b.theBigFs.overlayMountsStatic, files.ComponentFolderStatic) - ms[""] = b.newSourceFilesystem(files.ComponentFolderStatic, bfs) - } - - return b.result, nil -} - -func (b *sourceFilesystemsBuilder) createMainOverlayFs(p *paths.Paths) (*filesystemsCollector, error) { - var staticFsMap map[string]*overlayfs.OverlayFs - if b.p.Cfg.IsMultihost() { - languages := b.p.Cfg.Languages() - staticFsMap = make(map[string]*overlayfs.OverlayFs) - for _, l := range languages { - staticFsMap[l.Lang] = overlayfs.New(overlayfs.Options{}) - } - } - - collector := &filesystemsCollector{ - sourceProject: b.sourceFs, - sourceModules: b.sourceFs, - staticPerLanguage: staticFsMap, - - overlayMounts: overlayfs.New(overlayfs.Options{}), - overlayMountsContent: overlayfs.New(overlayfs.Options{DirsMerger: hugofs.LanguageDirsMerger}), - overlayMountsStatic: overlayfs.New(overlayfs.Options{DirsMerger: hugofs.LanguageDirsMerger}), - overlayFull: overlayfs.New(overlayfs.Options{}), - overlayResources: overlayfs.New(overlayfs.Options{FirstWritable: true}), - } - - mods := p.AllModules() - - mounts := make([]mountsDescriptor, len(mods)) - - for i := range mods { - mod := mods[i] - dir := mod.Dir() - - isMainProject := mod.Owner() == nil - mounts[i] = mountsDescriptor{ - Module: mod, - dir: dir, - isMainProject: isMainProject, - ordinal: i, - } - - } - - err := b.createOverlayFs(collector, mounts) - - return collector, err -} - -func (b *sourceFilesystemsBuilder) isContentMount(mnt modules.Mount) bool { - return strings.HasPrefix(mnt.Target, files.ComponentFolderContent) -} - -func (b *sourceFilesystemsBuilder) isStaticMount(mnt modules.Mount) bool { - return strings.HasPrefix(mnt.Target, files.ComponentFolderStatic) -} - -func (b *sourceFilesystemsBuilder) createOverlayFs( - collector *filesystemsCollector, - mounts []mountsDescriptor, -) error { - if len(mounts) == 0 { - appendNopIfEmpty := func(ofs *overlayfs.OverlayFs) *overlayfs.OverlayFs { - if ofs.NumFilesystems() > 0 { - return ofs - } - return ofs.Append(hugofs.NoOpFs) - } - collector.overlayMounts = appendNopIfEmpty(collector.overlayMounts) - collector.overlayMountsContent = appendNopIfEmpty(collector.overlayMountsContent) - collector.overlayMountsStatic = appendNopIfEmpty(collector.overlayMountsStatic) - collector.overlayMountsFull = appendNopIfEmpty(collector.overlayMountsFull) - collector.overlayFull = appendNopIfEmpty(collector.overlayFull) - collector.overlayResources = appendNopIfEmpty(collector.overlayResources) - - return nil - } - - for _, md := range mounts { - var ( - fromTo []hugofs.RootMapping - fromToContent []hugofs.RootMapping - fromToStatic []hugofs.RootMapping - ) - - absPathify := func(path string) (string, string) { - if filepath.IsAbs(path) { - return "", path - } - return md.dir, hpaths.AbsPathify(md.dir, path) - } - - for i, mount := range md.Mounts() { - // Add more weight to early mounts. - // When two mounts contain the same filename, - // the first entry wins. - mountWeight := (10 + md.ordinal) * (len(md.Mounts()) - i) - - inclusionFilter, err := glob.NewFilenameFilter( - types.ToStringSlicePreserveString(mount.IncludeFiles), - types.ToStringSlicePreserveString(mount.ExcludeFiles), - ) - if err != nil { - return err - } - - base, filename := absPathify(mount.Source) - - rm := hugofs.RootMapping{ - From: mount.Target, - To: filename, - ToBase: base, - Module: md.Module.Path(), - ModuleOrdinal: md.ordinal, - IsProject: md.isMainProject, - Meta: &hugofs.FileMeta{ - Watch: !mount.DisableWatch && md.Watch(), - Weight: mountWeight, - InclusionFilter: inclusionFilter, - }, - } - - isContentMount := b.isContentMount(mount) - - lang := mount.Lang - if lang == "" && isContentMount { - lang = b.p.Cfg.DefaultContentLanguage() - } - - rm.Meta.Lang = lang - - if isContentMount { - fromToContent = append(fromToContent, rm) - } else if b.isStaticMount(mount) { - fromToStatic = append(fromToStatic, rm) - } else { - fromTo = append(fromTo, rm) - } - } - - modBase := collector.sourceProject - if !md.isMainProject { - modBase = collector.sourceModules - } - - sourceStatic := modBase - - rmfs, err := hugofs.NewRootMappingFs(modBase, fromTo...) - if err != nil { - return err - } - rmfsContent, err := hugofs.NewRootMappingFs(modBase, fromToContent...) - if err != nil { - return err - } - rmfsStatic, err := hugofs.NewRootMappingFs(sourceStatic, fromToStatic...) - if err != nil { - return err - } - - // We need to keep the list of directories for watching. - collector.addRootFs(rmfs) - collector.addRootFs(rmfsContent) - collector.addRootFs(rmfsStatic) - - if collector.staticPerLanguage != nil { - for _, l := range b.p.Cfg.Languages() { - lang := l.Lang - - lfs := rmfsStatic.Filter(func(rm hugofs.RootMapping) bool { - rlang := rm.Meta.Lang - return rlang == "" || rlang == lang - }) - bfs := hugofs.NewBasePathFs(lfs, files.ComponentFolderStatic) - collector.staticPerLanguage[lang] = collector.staticPerLanguage[lang].Append(bfs) - } - } - - getResourcesDir := func() string { - if md.isMainProject { - return b.p.AbsResourcesDir - } - _, filename := absPathify(files.FolderResources) - return filename - } - - collector.overlayMounts = collector.overlayMounts.Append(rmfs) - collector.overlayMountsContent = collector.overlayMountsContent.Append(rmfsContent) - collector.overlayMountsStatic = collector.overlayMountsStatic.Append(rmfsStatic) - collector.overlayFull = collector.overlayFull.Append(hugofs.NewBasePathFs(modBase, md.dir)) - collector.overlayResources = collector.overlayResources.Append(hugofs.NewBasePathFs(modBase, getResourcesDir())) - - } - - return nil -} - -//lint:ignore U1000 useful for debugging -func printFs(fs afero.Fs, path string, w io.Writer) { - if fs == nil { - return - } - afero.Walk(fs, path, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if info.IsDir() { - return nil - } - var filename string - if fim, ok := info.(hugofs.FileMetaInfo); ok { - filename = fim.Meta().Filename - } - fmt.Fprintf(w, " %q %q\n", path, filename) - return nil - }) -} - -type filesystemsCollector struct { - sourceProject afero.Fs // Source for project folders - sourceModules afero.Fs // Source for modules/themes - - overlayMounts *overlayfs.OverlayFs - overlayMountsContent *overlayfs.OverlayFs - overlayMountsStatic *overlayfs.OverlayFs - overlayMountsFull *overlayfs.OverlayFs - overlayFull *overlayfs.OverlayFs - overlayResources *overlayfs.OverlayFs - - rootFss []*hugofs.RootMappingFs - - // Set if in multihost mode - staticPerLanguage map[string]*overlayfs.OverlayFs -} - -func (c *filesystemsCollector) addRootFs(rfs *hugofs.RootMappingFs) { - c.rootFss = append(c.rootFss, rfs) -} - -type mountsDescriptor struct { - modules.Module - dir string - isMainProject bool - ordinal int // zero based starting from the project. -} diff --git a/hugolib/filesystems/basefs_test.go b/hugolib/filesystems/basefs_test.go deleted file mode 100644 index abe06ac4a..000000000 --- a/hugolib/filesystems/basefs_test.go +++ /dev/null @@ -1,690 +0,0 @@ -// 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 filesystems_test - -import ( - "errors" - "fmt" - "os" - "path/filepath" - "runtime" - "strings" - "testing" - - "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/config/testconfig" - "github.com/gohugoio/hugo/hugolib" - - "github.com/spf13/afero" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/hugofs" - "github.com/gohugoio/hugo/hugolib/filesystems" - "github.com/gohugoio/hugo/hugolib/paths" -) - -func TestNewBaseFs(t *testing.T) { - c := qt.New(t) - v := config.New() - - themes := []string{"btheme", "atheme"} - - workingDir := filepath.FromSlash("/my/work") - v.Set("workingDir", workingDir) - v.Set("contentDir", "content") - v.Set("themesDir", "themes") - v.Set("defaultContentLanguage", "en") - v.Set("theme", themes[:1]) - v.Set("publishDir", "public") - - afs := afero.NewMemMapFs() - - // Write some data to the themes - for _, theme := range themes { - for _, dir := range []string{"i18n", "data", "archetypes", "layouts"} { - base := filepath.Join(workingDir, "themes", theme, dir) - filenameTheme := filepath.Join(base, fmt.Sprintf("theme-file-%s.txt", theme)) - filenameOverlap := filepath.Join(base, "f3.txt") - afs.Mkdir(base, 0o755) - content := fmt.Appendf(nil, "content:%s:%s", theme, dir) - afero.WriteFile(afs, filenameTheme, content, 0o755) - afero.WriteFile(afs, filenameOverlap, content, 0o755) - } - // Write some files to the root of the theme - base := filepath.Join(workingDir, "themes", theme) - afero.WriteFile(afs, filepath.Join(base, fmt.Sprintf("theme-root-%s.txt", theme)), fmt.Appendf(nil, "content:%s", theme), 0o755) - afero.WriteFile(afs, filepath.Join(base, "file-theme-root.txt"), fmt.Appendf(nil, "content:%s", theme), 0o755) - } - - afero.WriteFile(afs, filepath.Join(workingDir, "file-root.txt"), []byte("content-project"), 0o755) - - afero.WriteFile(afs, filepath.Join(workingDir, "themes", "btheme", "config.toml"), []byte(` -theme = ["atheme"] -`), 0o755) - - setConfigAndWriteSomeFilesTo(afs, v, "contentDir", "mycontent", 3) - setConfigAndWriteSomeFilesTo(afs, v, "i18nDir", "myi18n", 4) - setConfigAndWriteSomeFilesTo(afs, v, "layoutDir", "mylayouts", 5) - setConfigAndWriteSomeFilesTo(afs, v, "staticDir", "mystatic", 6) - setConfigAndWriteSomeFilesTo(afs, v, "dataDir", "mydata", 7) - setConfigAndWriteSomeFilesTo(afs, v, "archetypeDir", "myarchetypes", 8) - setConfigAndWriteSomeFilesTo(afs, v, "assetDir", "myassets", 9) - setConfigAndWriteSomeFilesTo(afs, v, "resourceDir", "myrsesource", 10) - - conf := testconfig.GetTestConfig(afs, v) - fs := hugofs.NewFrom(afs, conf.BaseConfig()) - - p, err := paths.New(fs, conf) - c.Assert(err, qt.IsNil) - - bfs, err := filesystems.NewBase(p, nil) - c.Assert(err, qt.IsNil) - c.Assert(bfs, qt.Not(qt.IsNil)) - - root, err := bfs.I18n.Fs.Open("") - c.Assert(err, qt.IsNil) - dirnames, err := root.Readdirnames(-1) - c.Assert(err, qt.IsNil) - c.Assert(dirnames, qt.DeepEquals, []string{"f1.txt", "f2.txt", "f3.txt", "f4.txt", "f3.txt", "theme-file-btheme.txt", "f3.txt", "theme-file-atheme.txt"}) - - root, err = bfs.Data.Fs.Open("") - c.Assert(err, qt.IsNil) - dirnames, err = root.Readdirnames(-1) - c.Assert(err, qt.IsNil) - c.Assert(dirnames, qt.DeepEquals, []string{"f1.txt", "f2.txt", "f3.txt", "f4.txt", "f5.txt", "f6.txt", "f7.txt", "f3.txt", "theme-file-btheme.txt", "f3.txt", "theme-file-atheme.txt"}) - - checkFileCount(bfs.Layouts.Fs, "", c, 7) - - checkFileCount(bfs.Content.Fs, "", c, 3) - checkFileCount(bfs.I18n.Fs, "", c, 8) // 4 + 4 themes - - checkFileCount(bfs.Static[""].Fs, "", c, 6) - checkFileCount(bfs.Data.Fs, "", c, 11) // 7 + 4 themes - checkFileCount(bfs.Archetypes.Fs, "", c, 10) // 8 + 2 themes - checkFileCount(bfs.Assets.Fs, "", c, 9) - checkFileCount(bfs.Work, "", c, 90) - - c.Assert(bfs.IsStatic(filepath.Join(workingDir, "mystatic", "file1.txt")), qt.Equals, true) - - contentFilename := filepath.Join(workingDir, "mycontent", "file1.txt") - c.Assert(bfs.IsContent(contentFilename), qt.Equals, true) - // Check Work fs vs theme - checkFileContent(bfs.Work, "file-root.txt", c, "content-project") - checkFileContent(bfs.Work, "theme-root-atheme.txt", c, "content:atheme") - - // https://github.com/gohugoio/hugo/issues/5318 - // Check both project and theme. - for _, fs := range []afero.Fs{bfs.Archetypes.Fs, bfs.Layouts.Fs} { - for _, filename := range []string{"/f1.txt", "/theme-file-atheme.txt"} { - filename = filepath.FromSlash(filename) - f, err := fs.Open(filename) - c.Assert(err, qt.IsNil) - f.Close() - } - } -} - -func TestNewBaseFsEmpty(t *testing.T) { - c := qt.New(t) - afs := afero.NewMemMapFs() - conf := testconfig.GetTestConfig(afs, nil) - fs := hugofs.NewFrom(afs, conf.BaseConfig()) - p, err := paths.New(fs, conf) - c.Assert(err, qt.IsNil) - bfs, err := filesystems.NewBase(p, nil) - c.Assert(err, qt.IsNil) - c.Assert(bfs, qt.Not(qt.IsNil)) - c.Assert(bfs.Archetypes.Fs, qt.Not(qt.IsNil)) - c.Assert(bfs.Layouts.Fs, qt.Not(qt.IsNil)) - c.Assert(bfs.Data.Fs, qt.Not(qt.IsNil)) - c.Assert(bfs.I18n.Fs, qt.Not(qt.IsNil)) - c.Assert(bfs.Work, qt.Not(qt.IsNil)) - c.Assert(bfs.Content.Fs, qt.Not(qt.IsNil)) - c.Assert(bfs.Static, qt.Not(qt.IsNil)) -} - -func TestRealDirs(t *testing.T) { - c := qt.New(t) - v := config.New() - root, themesDir := t.TempDir(), t.TempDir() - v.Set("workingDir", root) - v.Set("themesDir", themesDir) - v.Set("assetDir", "myassets") - v.Set("theme", "mytheme") - - afs := &hugofs.OpenFilesFs{Fs: hugofs.Os} - - c.Assert(afs.MkdirAll(filepath.Join(root, "myassets", "scss", "sf1"), 0o755), qt.IsNil) - c.Assert(afs.MkdirAll(filepath.Join(root, "myassets", "scss", "sf2"), 0o755), qt.IsNil) - c.Assert(afs.MkdirAll(filepath.Join(themesDir, "mytheme", "assets", "scss", "sf2"), 0o755), qt.IsNil) - c.Assert(afs.MkdirAll(filepath.Join(themesDir, "mytheme", "assets", "scss", "sf3"), 0o755), qt.IsNil) - c.Assert(afs.MkdirAll(filepath.Join(root, "resources"), 0o755), qt.IsNil) - c.Assert(afs.MkdirAll(filepath.Join(themesDir, "mytheme", "resources"), 0o755), qt.IsNil) - - c.Assert(afs.MkdirAll(filepath.Join(root, "myassets", "js", "f2"), 0o755), qt.IsNil) - - afero.WriteFile(afs, filepath.Join(filepath.Join(root, "myassets", "scss", "sf1", "a1.scss")), []byte("content"), 0o755) - afero.WriteFile(afs, filepath.Join(filepath.Join(root, "myassets", "scss", "sf2", "a3.scss")), []byte("content"), 0o755) - afero.WriteFile(afs, filepath.Join(filepath.Join(root, "myassets", "scss", "a2.scss")), []byte("content"), 0o755) - afero.WriteFile(afs, filepath.Join(filepath.Join(themesDir, "mytheme", "assets", "scss", "sf2", "a3.scss")), []byte("content"), 0o755) - afero.WriteFile(afs, filepath.Join(filepath.Join(themesDir, "mytheme", "assets", "scss", "sf3", "a4.scss")), []byte("content"), 0o755) - - afero.WriteFile(afs, filepath.Join(filepath.Join(themesDir, "mytheme", "resources", "t1.txt")), []byte("content"), 0o755) - afero.WriteFile(afs, filepath.Join(filepath.Join(root, "resources", "p1.txt")), []byte("content"), 0o755) - afero.WriteFile(afs, filepath.Join(filepath.Join(root, "resources", "p2.txt")), []byte("content"), 0o755) - - afero.WriteFile(afs, filepath.Join(filepath.Join(root, "myassets", "js", "f2", "a1.js")), []byte("content"), 0o755) - afero.WriteFile(afs, filepath.Join(filepath.Join(root, "myassets", "js", "a2.js")), []byte("content"), 0o755) - - conf := testconfig.GetTestConfig(afs, v) - fs := hugofs.NewFrom(afs, conf.BaseConfig()) - p, err := paths.New(fs, conf) - c.Assert(err, qt.IsNil) - bfs, err := filesystems.NewBase(p, nil) - c.Assert(err, qt.IsNil) - c.Assert(bfs, qt.Not(qt.IsNil)) - - checkFileCount(bfs.Assets.Fs, "", c, 6) - - realDirs := bfs.Assets.RealDirs("scss") - c.Assert(len(realDirs), qt.Equals, 2) - c.Assert(realDirs[0], qt.Equals, filepath.Join(root, "myassets/scss")) - c.Assert(realDirs[len(realDirs)-1], qt.Equals, filepath.Join(themesDir, "mytheme/assets/scss")) - - realDirs = bfs.Assets.RealDirs("foo") - c.Assert(len(realDirs), qt.Equals, 0) - - c.Assert(afs.OpenFiles(), qt.HasLen, 0) -} - -func TestWatchFilenames(t *testing.T) { - t.Parallel() - files := ` --- hugo.toml -- -theme = "t1" -[[module.mounts]] -source = 'content' -target = 'content' -[[module.mounts]] -source = 'content2' -target = 'content/c2' -[[module.mounts]] -source = 'content3' -target = 'content/watchdisabled' -disableWatch = true -[[module.mounts]] -source = 'content4' -target = 'content/excludedsome' -excludeFiles = 'p1.md' -[[module.mounts]] -source = 'content5' -target = 'content/excludedall' -excludeFiles = '/**' -[[module.mounts]] -source = "hugo_stats.json" -target = "assets/watching/hugo_stats.json" --- hugo_stats.json -- -Some stats. --- content/foo.md -- -foo --- content2/bar.md -- --- themes/t1/layouts/_default/single.html -- -{{ .Content }} --- themes/t1/static/f1.txt -- --- content3/p1.md -- --- content4/p1.md -- --- content4/p2.md -- --- content5/p3.md -- --- content5/p4.md -- -` - b := hugolib.Test(t, files) - bfs := b.H.BaseFs - watchFilenames := toSlashes(bfs.WatchFilenames()) - - // content3 has disableWatch = true - // content5 has excludeFiles = '/**' - b.Assert(watchFilenames, qt.DeepEquals, []string{"/hugo_stats.json", "/content", "/content2", "/content4", "/themes/t1/layouts", "/themes/t1/layouts/_default", "/themes/t1/static"}) -} - -func toSlashes(in []string) []string { - out := make([]string, len(in)) - for i, s := range in { - out[i] = filepath.ToSlash(s) - } - return out -} - -func TestNoSymlinks(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("skip on Windows") - } - files := ` --- hugo.toml -- -theme = "t1" --- content/a/foo.md -- -foo --- static/a/f1.txt -- -F1 text --- themes/t1/layouts/_default/single.html -- -{{ .Content }} --- themes/t1/static/a/f1.txt -- -` - tmpDir := t.TempDir() - - wd, _ := os.Getwd() - - for _, component := range []string{"content", "static"} { - aDir := filepath.Join(tmpDir, component, "a") - bDir := filepath.Join(tmpDir, component, "b") - os.MkdirAll(aDir, 0o755) - os.MkdirAll(bDir, 0o755) - os.Chdir(bDir) - os.Symlink("../a", "c") - } - - os.Chdir(wd) - - b := hugolib.NewIntegrationTestBuilder( - hugolib.IntegrationTestConfig{ - T: t, - TxtarString: files, - NeedsOsFS: true, - WorkingDir: tmpDir, - }, - ).Build() - - bfs := b.H.BaseFs - watchFilenames := bfs.WatchFilenames() - b.Assert(watchFilenames, qt.HasLen, 10) -} - -func TestStaticFs(t *testing.T) { - c := qt.New(t) - v := config.New() - workDir := "mywork" - v.Set("workingDir", workDir) - v.Set("themesDir", "themes") - v.Set("staticDir", "mystatic") - v.Set("theme", []string{"t1", "t2"}) - - afs := afero.NewMemMapFs() - - themeStaticDir := filepath.Join(workDir, "themes", "t1", "static") - themeStaticDir2 := filepath.Join(workDir, "themes", "t2", "static") - - afero.WriteFile(afs, filepath.Join(workDir, "mystatic", "f1.txt"), []byte("Hugo Rocks!"), 0o755) - afero.WriteFile(afs, filepath.Join(themeStaticDir, "f1.txt"), []byte("Hugo Themes Rocks!"), 0o755) - afero.WriteFile(afs, filepath.Join(themeStaticDir, "f2.txt"), []byte("Hugo Themes Still Rocks!"), 0o755) - afero.WriteFile(afs, filepath.Join(themeStaticDir2, "f2.txt"), []byte("Hugo Themes Rocks in t2!"), 0o755) - - conf := testconfig.GetTestConfig(afs, v) - fs := hugofs.NewFrom(afs, conf.BaseConfig()) - p, err := paths.New(fs, conf) - - c.Assert(err, qt.IsNil) - bfs, err := filesystems.NewBase(p, nil) - c.Assert(err, qt.IsNil) - - sfs := bfs.StaticFs("en") - - checkFileContent(sfs, "f1.txt", c, "Hugo Rocks!") - checkFileContent(sfs, "f2.txt", c, "Hugo Themes Still Rocks!") -} - -func TestStaticFsMultihost(t *testing.T) { - c := qt.New(t) - v := config.New() - workDir := "mywork" - v.Set("workingDir", workDir) - v.Set("themesDir", "themes") - v.Set("staticDir", "mystatic") - v.Set("theme", "t1") - v.Set("defaultContentLanguage", "en") - - langConfig := map[string]any{ - "no": map[string]any{ - "staticDir": "static_no", - "baseURL": "https://example.org/no/", - }, - "en": map[string]any{ - "baseURL": "https://example.org/en/", - }, - } - - v.Set("languages", langConfig) - - afs := afero.NewMemMapFs() - - themeStaticDir := filepath.Join(workDir, "themes", "t1", "static") - - afero.WriteFile(afs, filepath.Join(workDir, "mystatic", "f1.txt"), []byte("Hugo Rocks!"), 0o755) - afero.WriteFile(afs, filepath.Join(workDir, "static_no", "f1.txt"), []byte("Hugo Rocks in Norway!"), 0o755) - - afero.WriteFile(afs, filepath.Join(themeStaticDir, "f1.txt"), []byte("Hugo Themes Rocks!"), 0o755) - afero.WriteFile(afs, filepath.Join(themeStaticDir, "f2.txt"), []byte("Hugo Themes Still Rocks!"), 0o755) - - conf := testconfig.GetTestConfig(afs, v) - fs := hugofs.NewFrom(afs, conf.BaseConfig()) - - p, err := paths.New(fs, conf) - c.Assert(err, qt.IsNil) - bfs, err := filesystems.NewBase(p, nil) - c.Assert(err, qt.IsNil) - enFs := bfs.StaticFs("en") - checkFileContent(enFs, "f1.txt", c, "Hugo Rocks!") - checkFileContent(enFs, "f2.txt", c, "Hugo Themes Still Rocks!") - - noFs := bfs.StaticFs("no") - checkFileContent(noFs, "f1.txt", c, "Hugo Rocks in Norway!") - checkFileContent(noFs, "f2.txt", c, "Hugo Themes Still Rocks!") -} - -func TestMakePathRelative(t *testing.T) { - files := ` --- hugo.toml -- -[[module.mounts]] -source = "bar.txt" -target = "assets/foo/baz.txt" -[[module.imports]] -path = "t1" -[[module.imports.mounts]] -source = "src" -target = "assets/foo/bar" --- bar.txt -- -Bar. --- themes/t1/src/main.js -- -Main. -` - b := hugolib.Test(t, files) - - rel, found := b.H.BaseFs.Assets.MakePathRelative(filepath.FromSlash("/themes/t1/src/main.js"), true) - b.Assert(found, qt.Equals, true) - b.Assert(rel, qt.Equals, filepath.FromSlash("foo/bar/main.js")) - - rel, found = b.H.BaseFs.Assets.MakePathRelative(filepath.FromSlash("/bar.txt"), true) - b.Assert(found, qt.Equals, true) - b.Assert(rel, qt.Equals, filepath.FromSlash("foo/baz.txt")) -} - -func TestAbsProjectContentDir(t *testing.T) { - tempDir := t.TempDir() - - files := ` --- hugo.toml -- -[[module.mounts]] -source = "content" -target = "content" --- content/foo.md -- ---- -title: "Foo" ---- -` - - b := hugolib.NewIntegrationTestBuilder( - hugolib.IntegrationTestConfig{ - T: t, - WorkingDir: tempDir, - TxtarString: files, - }, - ).Build() - - abs1 := filepath.Join(tempDir, "content", "foo.md") - rel, abs2, err := b.H.BaseFs.AbsProjectContentDir("foo.md") - b.Assert(err, qt.IsNil) - b.Assert(abs2, qt.Equals, abs1) - b.Assert(rel, qt.Equals, filepath.FromSlash("foo.md")) - rel2, abs3, err := b.H.BaseFs.AbsProjectContentDir(abs1) - b.Assert(err, qt.IsNil) - b.Assert(abs3, qt.Equals, abs1) - b.Assert(rel2, qt.Equals, rel) -} - -func TestContentReverseLookup(t *testing.T) { - files := ` --- README.md -- ---- -title: README ---- --- blog/b1.md -- ---- -title: b1 ---- --- docs/d1.md -- ---- -title: d1 ---- --- hugo.toml -- -baseURL = "https://example.com/" -[module] -[[module.mounts]] -source = "layouts" -target = "layouts" -[[module.mounts]] -source = "README.md" -target = "content/_index.md" -[[module.mounts]] -source = "blog" -target = "content/posts" -[[module.mounts]] -source = "docs" -target = "content/mydocs" --- layouts/index.html -- -Home. - -` - b := hugolib.Test(t, files) - - b.AssertFileContent("public/index.html", "Home.") - - stat := func(path string) hugofs.FileMetaInfo { - ps, err := b.H.BaseFs.Content.ReverseLookup(filepath.FromSlash(path), true) - b.Assert(err, qt.IsNil) - b.Assert(ps, qt.HasLen, 1) - first := ps[0] - fi, err := b.H.BaseFs.Content.Fs.Stat(filepath.FromSlash(first.Path)) - b.Assert(err, qt.IsNil) - b.Assert(fi, qt.Not(qt.IsNil)) - return fi.(hugofs.FileMetaInfo) - } - - sfs := b.H.Fs.Source - - _, err := sfs.Stat("blog/b1.md") - b.Assert(err, qt.Not(qt.IsNil)) - - _ = stat("blog/b1.md") -} - -func TestReverseLookupShouldOnlyConsiderFilesInCurrentComponent(t *testing.T) { - files := ` --- hugo.toml -- -baseURL = "https://example.com/" -[module] -[[module.mounts]] -source = "files/layouts" -target = "layouts" -[[module.mounts]] -source = "files/layouts/assets" -target = "assets" --- files/layouts/l1.txt -- -l1 --- files/layouts/assets/l2.txt -- -l2 -` - b := hugolib.Test(t, files) - - assetsFs := b.H.Assets - - for _, checkExists := range []bool{false, true} { - cps, err := assetsFs.ReverseLookup(filepath.FromSlash("files/layouts/assets/l2.txt"), checkExists) - b.Assert(err, qt.IsNil) - b.Assert(cps, qt.HasLen, 1) - cps, err = assetsFs.ReverseLookup(filepath.FromSlash("files/layouts/l2.txt"), checkExists) - b.Assert(err, qt.IsNil) - b.Assert(cps, qt.HasLen, 0) - } -} - -func TestAssetsIssue12175(t *testing.T) { - files := ` --- hugo.toml -- -baseURL = "https://example.com/" -[module] -[[module.mounts]] -source = "node_modules/@foo/core/assets" -target = "assets" -[[module.mounts]] -source = "assets" -target = "assets" --- node_modules/@foo/core/assets/js/app.js -- -JS. --- node_modules/@foo/core/assets/scss/app.scss -- -body { color: red; } --- assets/scss/app.scss -- -body { color: blue; } --- layouts/index.html -- -Home. -SCSS: {{ with resources.Get "scss/app.scss" }}{{ .RelPermalink }}|{{ .Content }}{{ end }}| -# Note that the pattern below will match 2 resources, which doesn't make much sense, -# but is how the current (and also < v0.123.0) merge logic works, and for most practical purposes, it doesn't matter. -SCSS Match: {{ with resources.Match "**.scss" }}{{ . | len }}|{{ range .}}{{ .RelPermalink }}|{{ end }}{{ end }}| - -` - - b := hugolib.Test(t, files) - - b.AssertFileContent("public/index.html", ` -SCSS: /scss/app.scss|body { color: blue; }| -SCSS Match: 2| -`) -} - -func TestStaticComposite(t *testing.T) { - files := ` --- hugo.toml -- -disableKinds = ["taxonomy", "term"] -[module] -[[module.mounts]] -source = "myfiles/f1.txt" -target = "static/files/f1.txt" -[[module.mounts]] -source = "f3.txt" -target = "static/f3.txt" -[[module.mounts]] -source = "static" -target = "static" --- static/files/f2.txt -- -f2 --- myfiles/f1.txt -- -f1 --- f3.txt -- -f3 --- layouts/home.html -- -Home. - -` - b := hugolib.Test(t, files) - - b.AssertFs(b.H.BaseFs.StaticFs(""), ` -. true -f3.txt false -files true -files/f1.txt false -files/f2.txt false -`) -} - -func TestMountIssue12141(t *testing.T) { - files := ` --- hugo.toml -- -disableKinds = ["taxonomy", "term"] -[module] -[[module.mounts]] -source = "myfiles" -target = "static" -[[module.mounts]] -source = "myfiles/f1.txt" -target = "static/f2.txt" --- myfiles/f1.txt -- -f1 -` - b := hugolib.Test(t, files) - fs := b.H.BaseFs.StaticFs("") - - b.AssertFs(fs, ` -. true -f1.txt false -f2.txt false -`) -} - -func checkFileCount(fs afero.Fs, dirname string, c *qt.C, expected int) { - c.Helper() - count, names, err := countFilesAndGetFilenames(fs, dirname) - namesComment := qt.Commentf("filenames: %v", names) - c.Assert(err, qt.IsNil, namesComment) - c.Assert(count, qt.Equals, expected, namesComment) -} - -func checkFileContent(fs afero.Fs, filename string, c *qt.C, expected ...string) { - b, err := afero.ReadFile(fs, filename) - c.Assert(err, qt.IsNil) - - content := string(b) - - for _, e := range expected { - c.Assert(content, qt.Contains, e) - } -} - -func countFilesAndGetFilenames(fs afero.Fs, dirname string) (int, []string, error) { - if fs == nil { - return 0, nil, errors.New("no fs") - } - - counter := 0 - var filenames []string - - wf := func(path string, info hugofs.FileMetaInfo) error { - if !info.IsDir() { - counter++ - } - - if info.Name() != "." { - name := info.Name() - name = strings.Replace(name, filepath.FromSlash("/my/work"), "WORK_DIR", 1) - filenames = append(filenames, name) - } - - return nil - } - - w := hugofs.NewWalkway(hugofs.WalkwayConfig{Fs: fs, Root: dirname, WalkFn: wf}) - - if err := w.Walk(); err != nil { - return -1, nil, err - } - - return counter, filenames, nil -} - -func setConfigAndWriteSomeFilesTo(fs afero.Fs, v config.Provider, key, val string, num int) { - workingDir := v.GetString("workingDir") - v.Set(key, val) - fs.Mkdir(val, 0o755) - for i := range num { - filename := filepath.Join(workingDir, val, fmt.Sprintf("f%d.txt", i+1)) - afero.WriteFile(fs, filename, fmt.Appendf(nil, "content:%s:%d", key, i+1), 0o755) - } -} diff --git a/hugolib/frontmatter_test.go b/hugolib/frontmatter_test.go deleted file mode 100644 index 3a2080b0e..000000000 --- a/hugolib/frontmatter_test.go +++ /dev/null @@ -1,46 +0,0 @@ -// 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 hugolib - -import "testing" - -// Issue 10624 -func TestFrontmatterPreserveDatatypesForSlices(t *testing.T) { - t.Parallel() - - files := ` --- content/post/one.md -- ---- -ints: [1, 2, 3] -mixed: ["1", 2, 3] -strings: ["1", "2","3"] ---- --- layouts/_default/single.html -- -Ints: {{ printf "%T" .Params.ints }} {{ range .Params.ints }}Int: {{ fmt.Printf "%[1]v (%[1]T)" . }}|{{ end }} -Mixed: {{ printf "%T" .Params.mixed }} {{ range .Params.mixed }}Mixed: {{ fmt.Printf "%[1]v (%[1]T)" . }}|{{ end }} -Strings: {{ printf "%T" .Params.strings }} {{ range .Params.strings }}Strings: {{ fmt.Printf "%[1]v (%[1]T)" . }}|{{ end }} -` - b := NewIntegrationTestBuilder( - IntegrationTestConfig{ - T: t, - TxtarString: files, - }, - ) - - b.Build() - - b.AssertFileContent("public/post/one/index.html", "Ints: []interface {} Int: 1 (int)|Int: 2 (int)|Int: 3 (int)|") - b.AssertFileContent("public/post/one/index.html", "Mixed: []interface {} Mixed: 1 (string)|Mixed: 2 (int)|Mixed: 3 (int)|") - b.AssertFileContent("public/post/one/index.html", "Strings: []string Strings: 1 (string)|Strings: 2 (string)|Strings: 3 (string)|") -} diff --git a/hugolib/gitinfo.go b/hugolib/gitinfo.go index 6b5261084..16d8c43a5 100644 --- a/hugolib/gitinfo.go +++ b/hugolib/gitinfo.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Hugo Authors. All rights reserved. +// Copyright 2016-present 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,53 +14,56 @@ package hugolib import ( - "io" + "path" "path/filepath" "strings" "github.com/bep/gitmap" - "github.com/gohugoio/hugo/common/hexec" - "github.com/gohugoio/hugo/deps" - "github.com/gohugoio/hugo/resources/page" - "github.com/gohugoio/hugo/source" + "github.com/gohugoio/hugo/helpers" ) -type gitInfo struct { - contentDir string - repo *gitmap.GitRepo -} - -func (g *gitInfo) forPage(p page.Page) source.GitInfo { - name := strings.TrimPrefix(filepath.ToSlash(p.File().Filename()), g.contentDir) - name = strings.TrimPrefix(name, "/") - gi, found := g.repo.Files[name] - if !found { - return source.GitInfo{} - } - return source.NewGitInfo(*gi) -} - -func newGitInfo(d *deps.Deps) (*gitInfo, error) { - opts := gitmap.Options{ - Repository: d.Conf.BaseConfig().WorkingDir, - GetGitCommandFunc: func(stdout, stderr io.Writer, args ...string) (gitmap.Runner, error) { - var argsv []any - for _, arg := range args { - argsv = append(argsv, arg) - } - argsv = append( - argsv, - hexec.WithStdout(stdout), - hexec.WithStderr(stderr), - ) - return d.ExecHelper.New("git", argsv...) - }, +func (h *HugoSites) assembleGitInfo() { + if !h.Cfg.GetBool("enableGitInfo") { + return } - gitRepo, err := gitmap.Map(opts) + var ( + workingDir = h.Cfg.GetString("workingDir") + contentDir = h.Cfg.GetString("contentDir") + ) + + gitRepo, err := gitmap.Map(workingDir, "") if err != nil { - return nil, err + h.Log.ERROR.Printf("Got error reading Git log: %s", err) + return + } + + gitMap := gitRepo.Files + repoPath := filepath.FromSlash(gitRepo.TopLevelAbsPath) + + // The Hugo site may be placed in a sub folder in the Git repo, + // one example being the Hugo docs. + // We have to find the root folder to the Hugo site below the Git root. + contentRoot := strings.TrimPrefix(workingDir, repoPath) + contentRoot = strings.TrimPrefix(contentRoot, helpers.FilePathSeparator) + + s := h.Sites[0] + + for _, p := range s.AllPages { + if p.Path() == "" { + // Home page etc. with no content file. + continue + } + // Git normalizes file paths on this form: + filename := path.Join(filepath.ToSlash(contentRoot), contentDir, filepath.ToSlash(p.Path())) + g, ok := gitMap[filename] + if !ok { + h.Log.WARN.Printf("Failed to find GitInfo for %q", filename) + return + } + + p.GitInfo = g + p.Lastmod = p.GitInfo.AuthorDate } - return &gitInfo{contentDir: gitRepo.TopLevelAbsPath, repo: gitRepo}, nil } diff --git a/hugolib/handler_base.go b/hugolib/handler_base.go new file mode 100644 index 000000000..99c15e15d --- /dev/null +++ b/hugolib/handler_base.go @@ -0,0 +1,60 @@ +// Copyright 2015 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 hugolib + +import ( + "github.com/gohugoio/hugo/source" +) + +type Handler interface { + FileConvert(*source.File, *Site) HandledResult + PageConvert(*Page) HandledResult + Read(*source.File, *Site) HandledResult + Extensions() []string +} + +type Handle struct { + extensions []string +} + +func (h Handle) Extensions() []string { + return h.extensions +} + +type HandledResult struct { + page *Page + file *source.File + err error +} + +// HandledResult is an error +func (h HandledResult) Error() string { + if h.err != nil { + if h.page != nil { + return "Error: " + h.err.Error() + " for " + h.page.File.LogicalName() + } + if h.file != nil { + return "Error: " + h.err.Error() + " for " + h.file.LogicalName() + } + } + return h.err.Error() +} + +func (h HandledResult) String() string { + return h.Error() +} + +func (h HandledResult) Page() *Page { + return h.page +} diff --git a/hugolib/handler_file.go b/hugolib/handler_file.go new file mode 100644 index 000000000..82ea85fb2 --- /dev/null +++ b/hugolib/handler_file.go @@ -0,0 +1,59 @@ +// Copyright 2015 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 hugolib + +import ( + "bytes" + + "github.com/dchest/cssmin" + "github.com/gohugoio/hugo/source" +) + +func init() { + RegisterHandler(new(cssHandler)) + RegisterHandler(new(defaultHandler)) +} + +type basicFileHandler Handle + +func (h basicFileHandler) Read(f *source.File, s *Site) HandledResult { + return HandledResult{file: f} +} + +func (h basicFileHandler) PageConvert(*Page) HandledResult { + return HandledResult{} +} + +type defaultHandler struct{ basicFileHandler } + +func (h defaultHandler) Extensions() []string { return []string{"*"} } +func (h defaultHandler) FileConvert(f *source.File, s *Site) HandledResult { + err := s.publish(f.Path(), f.Contents) + if err != nil { + return HandledResult{err: err} + } + return HandledResult{file: f} +} + +type cssHandler struct{ basicFileHandler } + +func (h cssHandler) Extensions() []string { return []string{"css"} } +func (h cssHandler) FileConvert(f *source.File, s *Site) HandledResult { + x := cssmin.Minify(f.Bytes()) + err := s.publish(f.Path(), bytes.NewReader(x)) + if err != nil { + return HandledResult{err: err} + } + return HandledResult{file: f} +} diff --git a/hugolib/handler_meta.go b/hugolib/handler_meta.go new file mode 100644 index 000000000..d2702a39e --- /dev/null +++ b/hugolib/handler_meta.go @@ -0,0 +1,117 @@ +// Copyright 2015 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 hugolib + +import ( + "errors" + + "fmt" + + "github.com/gohugoio/hugo/source" +) + +var handlers []Handler + +type MetaHandler interface { + // Read the Files in and register + Read(*source.File, *Site, HandleResults) + + // Generic Convert Function with coordination + Convert(interface{}, *Site, HandleResults) + + Handle() Handler +} + +type HandleResults chan<- HandledResult + +func NewMetaHandler(in string) *MetaHandle { + x := &MetaHandle{ext: in} + x.Handler() + return x +} + +type MetaHandle struct { + handler Handler + ext string +} + +func (mh *MetaHandle) Read(f *source.File, s *Site, results HandleResults) { + if h := mh.Handler(); h != nil { + results <- h.Read(f, s) + return + } + + results <- HandledResult{err: errors.New("No handler found"), file: f} +} + +func (mh *MetaHandle) Convert(i interface{}, s *Site, results HandleResults) { + h := mh.Handler() + + if f, ok := i.(*source.File); ok { + results <- h.FileConvert(f, s) + return + } + + if p, ok := i.(*Page); ok { + if p == nil { + results <- HandledResult{err: errors.New("file resulted in a nil page")} + return + } + + if h == nil { + results <- HandledResult{err: fmt.Errorf("No handler found for page '%s'. Verify the markup is supported by Hugo.", p.FullFilePath())} + return + } + + results <- h.PageConvert(p) + } +} + +func (mh *MetaHandle) Handler() Handler { + if mh.handler == nil { + mh.handler = FindHandler(mh.ext) + + // if no handler found, use default handler + if mh.handler == nil { + mh.handler = FindHandler("*") + } + } + return mh.handler +} + +func FindHandler(ext string) Handler { + for _, h := range Handlers() { + if HandlerMatch(h, ext) { + return h + } + } + return nil +} + +func HandlerMatch(h Handler, ext string) bool { + for _, x := range h.Extensions() { + if ext == x { + return true + } + } + return false +} + +func RegisterHandler(h Handler) { + handlers = append(handlers, h) +} + +func Handlers() []Handler { + return handlers +} diff --git a/hugolib/handler_page.go b/hugolib/handler_page.go new file mode 100644 index 000000000..6e230dad0 --- /dev/null +++ b/hugolib/handler_page.go @@ -0,0 +1,147 @@ +// Copyright 2016 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 hugolib + +import ( + "fmt" + + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/source" +) + +func init() { + RegisterHandler(new(markdownHandler)) + RegisterHandler(new(htmlHandler)) + RegisterHandler(new(asciidocHandler)) + RegisterHandler(new(rstHandler)) + RegisterHandler(new(mmarkHandler)) + RegisterHandler(new(orgHandler)) +} + +type basicPageHandler Handle + +func (b basicPageHandler) Read(f *source.File, s *Site) HandledResult { + page, err := s.NewPage(f.Path()) + + if err != nil { + return HandledResult{file: f, err: err} + } + + if _, err := page.ReadFrom(f.Contents); err != nil { + return HandledResult{file: f, err: err} + } + + // In a multilanguage setup, we use the first site to + // do the initial processing. + // That site may be different than where the page will end up, + // so we do the assignment here. + // We should clean up this, but that will have to wait. + s.assignSiteByLanguage(page) + + return HandledResult{file: f, page: page, err: err} +} + +func (b basicPageHandler) FileConvert(*source.File, *Site) HandledResult { + return HandledResult{} +} + +type markdownHandler struct { + basicPageHandler +} + +func (h markdownHandler) Extensions() []string { return []string{"mdown", "markdown", "md"} } +func (h markdownHandler) PageConvert(p *Page) HandledResult { + return commonConvert(p) +} + +type htmlHandler struct { + basicPageHandler +} + +func (h htmlHandler) Extensions() []string { return []string{"html", "htm"} } + +func (h htmlHandler) PageConvert(p *Page) HandledResult { + if p.rendered { + panic(fmt.Sprintf("Page %q already rendered, does not need conversion", p.BaseFileName())) + } + + // Work on a copy of the raw content from now on. + p.createWorkContentCopy() + + if err := p.processShortcodes(); err != nil { + p.s.Log.ERROR.Println(err) + } + + return HandledResult{err: nil} +} + +type asciidocHandler struct { + basicPageHandler +} + +func (h asciidocHandler) Extensions() []string { return []string{"asciidoc", "adoc", "ad"} } +func (h asciidocHandler) PageConvert(p *Page) HandledResult { + return commonConvert(p) +} + +type rstHandler struct { + basicPageHandler +} + +func (h rstHandler) Extensions() []string { return []string{"rest", "rst"} } +func (h rstHandler) PageConvert(p *Page) HandledResult { + return commonConvert(p) +} + +type mmarkHandler struct { + basicPageHandler +} + +func (h mmarkHandler) Extensions() []string { return []string{"mmark"} } +func (h mmarkHandler) PageConvert(p *Page) HandledResult { + return commonConvert(p) +} + +type orgHandler struct { + basicPageHandler +} + +func (h orgHandler) Extensions() []string { return []string{"org"} } +func (h orgHandler) PageConvert(p *Page) HandledResult { + return commonConvert(p) +} + +func commonConvert(p *Page) HandledResult { + if p.rendered { + panic(fmt.Sprintf("Page %q already rendered, does not need conversion", p.BaseFileName())) + } + + // Work on a copy of the raw content from now on. + p.createWorkContentCopy() + + if err := p.processShortcodes(); err != nil { + p.s.Log.ERROR.Println(err) + } + + // TODO(bep) these page handlers need to be re-evaluated, as it is hard to + // process a page in isolation. See the new preRender func. + if p.s.Cfg.GetBool("enableEmoji") { + p.workContent = helpers.Emojify(p.workContent) + } + + p.workContent = p.replaceDivider(p.workContent) + p.workContent = p.renderContent(p.workContent) + + return HandledResult{err: nil} +} diff --git a/hugolib/handler_test.go b/hugolib/handler_test.go new file mode 100644 index 000000000..aa58d1c43 --- /dev/null +++ b/hugolib/handler_test.go @@ -0,0 +1,77 @@ +// Copyright 2015 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 hugolib + +import ( + "path/filepath" + "testing" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/helpers" +) + +func TestDefaultHandler(t *testing.T) { + t.Parallel() + + var ( + cfg, fs = newTestCfg() + ) + + cfg.Set("verbose", true) + cfg.Set("uglyURLs", true) + + writeSource(t, fs, filepath.FromSlash("content/sect/doc1.html"), "---\nmarkup: markdown\n---\n# title\nsome *content*") + writeSource(t, fs, filepath.FromSlash("content/sect/doc2.html"), "more content") + writeSource(t, fs, filepath.FromSlash("content/sect/doc3.md"), "# doc3\n*some* content") + writeSource(t, fs, filepath.FromSlash("content/sect/doc4.md"), "---\ntitle: doc4\n---\n# doc4\n*some content*") + writeSource(t, fs, filepath.FromSlash("content/sect/doc3/img1.png"), "‰PNG  ��� IHDR����������:~›U��� IDATWcø��ZMoñ����IEND®B`‚") + writeSource(t, fs, filepath.FromSlash("content/sect/img2.gif"), "GIF89a��€��ÿÿÿ���,�������D�;") + writeSource(t, fs, filepath.FromSlash("content/sect/img2.spf"), "****FAKE-FILETYPE****") + writeSource(t, fs, filepath.FromSlash("content/doc7.html"), "doc7 content") + writeSource(t, fs, filepath.FromSlash("content/sect/doc8.html"), "---\nmarkup: md\n---\n# title\nsome *content*") + + writeSource(t, fs, filepath.FromSlash("layouts/_default/single.html"), "{{.Content}}") + writeSource(t, fs, filepath.FromSlash("head"), "") + writeSource(t, fs, filepath.FromSlash("head_abs"), "title\n\n

    some content

    \n"}, + {filepath.FromSlash("public/sect/doc2.html"), "more content"}, + {filepath.FromSlash("public/sect/doc3.html"), "\n\n

    doc3

    \n\n

    some content

    \n"}, + {filepath.FromSlash("public/sect/doc3/img1.png"), string([]byte("‰PNG  ��� IHDR����������:~›U��� IDATWcø��ZMoñ����IEND®B`‚"))}, + {filepath.FromSlash("public/sect/img2.gif"), string([]byte("GIF89a��€��ÿÿÿ���,�������D�;"))}, + {filepath.FromSlash("public/sect/img2.spf"), string([]byte("****FAKE-FILETYPE****"))}, + {filepath.FromSlash("public/doc7.html"), "doc7 content"}, + {filepath.FromSlash("public/sect/doc8.html"), "\n\n

    title

    \n\n

    some content

    \n"}, + } + + for _, test := range tests { + file, err := fs.Destination.Open(test.doc) + if err != nil { + t.Fatalf("Did not find %s in target.", test.doc) + } + + content := helpers.ReaderToString(file) + + if content != test.expected { + t.Errorf("%s content expected:\n%q\ngot:\n%q", test.doc, test.expected, content) + } + } + +} diff --git a/hugolib/hugo_info.go b/hugolib/hugo_info.go new file mode 100644 index 000000000..1e0c192e5 --- /dev/null +++ b/hugolib/hugo_info.go @@ -0,0 +1,49 @@ +// Copyright 2015 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 hugolib + +import ( + "fmt" + "html/template" + + "github.com/gohugoio/hugo/helpers" +) + +var ( + // CommitHash contains the current Git revision. Use make to build to make + // sure this gets set. + CommitHash string + + // BuildDate contains the date of the current build. + BuildDate string +) + +var hugoInfo *HugoInfo + +// HugoInfo contains information about the current Hugo environment +type HugoInfo struct { + Version string + Generator template.HTML + CommitHash string + BuildDate string +} + +func init() { + hugoInfo = &HugoInfo{ + Version: helpers.CurrentHugoVersion.String(), + CommitHash: CommitHash, + BuildDate: BuildDate, + Generator: template.HTML(fmt.Sprintf(``, helpers.CurrentHugoVersion.String())), + } +} diff --git a/hugolib/hugo_modules_test.go b/hugolib/hugo_modules_test.go deleted file mode 100644 index 243447805..000000000 --- a/hugolib/hugo_modules_test.go +++ /dev/null @@ -1,830 +0,0 @@ -// Copyright 2019 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 hugolib - -import ( - "fmt" - "math/rand" - "os" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/bep/logg" - "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/modules/npm" - - "github.com/spf13/afero" - - "github.com/gohugoio/hugo/common/hugo" - "github.com/gohugoio/hugo/common/loggers" - - "github.com/gohugoio/hugo/htesting" - "github.com/gohugoio/hugo/hugofs" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/testmodBuilder/mods" -) - -func TestHugoModulesVariants(t *testing.T) { - if !htesting.IsCI() { - t.Skip("skip (relative) long running modules test when running locally") - } - - tomlConfig := ` -baseURL="https://example.org" -workingDir = %q - -[module] -[[module.imports]] -path="github.com/gohugoio/hugoTestModule2" -%s -` - - createConfig := func(workingDir, moduleOpts string) string { - return fmt.Sprintf(tomlConfig, workingDir, moduleOpts) - } - - newTestBuilder := func(t testing.TB, moduleOpts string) *sitesBuilder { - b := newTestSitesBuilder(t) - tempDir := t.TempDir() - workingDir := filepath.Join(tempDir, "myhugosite") - b.Assert(os.MkdirAll(workingDir, 0o777), qt.IsNil) - cfg := config.New() - cfg.Set("workingDir", workingDir) - cfg.Set("publishDir", "public") - b.Fs = hugofs.NewDefault(cfg) - b.WithWorkingDir(workingDir).WithConfigFile("toml", createConfig(workingDir, moduleOpts)) - b.WithTemplates( - "index.html", ` -Param from module: {{ site.Params.Hugo }}| -{{ $js := resources.Get "jslibs/alpinejs/alpine.js" }} -JS imported in module: {{ with $js }}{{ .RelPermalink }}{{ end }}| -`, - "_default/single.html", `{{ .Content }}`) - b.WithContent("p1.md", `--- -title: "Page" ---- - -[A link](https://bep.is) - -`) - b.WithSourceFile("go.mod", ` -module github.com/gohugoio/tests/testHugoModules - - -`) - - b.WithSourceFile("go.sum", ` -github.com/gohugoio/hugoTestModule2 v0.0.0-20200131160637-9657d7697877 h1:WLM2bQCKIWo04T6NsIWsX/Vtirhf0TnpY66xyqGlgVY= -github.com/gohugoio/hugoTestModule2 v0.0.0-20200131160637-9657d7697877/go.mod h1:CBFZS3khIAXKxReMwq0le8sEl/D8hcXmixlOHVv+Gd0= -`) - - return b - } - - t.Run("Target in subfolder", func(t *testing.T) { - b := newTestBuilder(t, "ignoreImports=true") - b.Build(BuildCfg{}) - - b.AssertFileContent("public/p1/index.html", `

    Page|https://bep.is|Title: |Text: A link|END

    `) - }) - - t.Run("Ignore config", func(t *testing.T) { - b := newTestBuilder(t, "ignoreConfig=true") - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", ` -Param from module: | -JS imported in module: | -`) - }) - - t.Run("Ignore imports", func(t *testing.T) { - b := newTestBuilder(t, "ignoreImports=true") - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", ` -Param from module: Rocks| -JS imported in module: | -`) - }) - - t.Run("Create package.json", func(t *testing.T) { - b := newTestBuilder(t, "") - - b.WithSourceFile("package.json", `{ - "name": "mypack", - "version": "1.2.3", - "scripts": { - "client": "wait-on http://localhost:1313 && open http://localhost:1313", - "start": "run-p client server", - "test": "echo 'hoge' > hoge" - }, - "dependencies": { - "nonon": "error" - } -}`) - - b.WithSourceFile("package.hugo.json", `{ - "name": "mypack", - "version": "1.2.3", - "scripts": { - "client": "wait-on http://localhost:1313 && open http://localhost:1313", - "start": "run-p client server", - "test": "echo 'hoge' > hoge" - }, - "dependencies": { - "foo": "1.2.3" - }, - "devDependencies": { - "postcss-cli": "7.8.0", - "tailwindcss": "1.8.0" - - } -}`) - - b.Build(BuildCfg{}) - b.Assert(npm.Pack(b.H.BaseFs.ProjectSourceFs, b.H.BaseFs.AssetsWithDuplicatesPreserved.Fs), qt.IsNil) - - b.AssertFileContentFn("package.json", func(s string) bool { - return s == `{ - "comments": { - "dependencies": { - "foo": "project", - "react-dom": "github.com/gohugoio/hugoTestModule2" - }, - "devDependencies": { - "@babel/cli": "github.com/gohugoio/hugoTestModule2", - "@babel/core": "github.com/gohugoio/hugoTestModule2", - "@babel/preset-env": "github.com/gohugoio/hugoTestModule2", - "postcss-cli": "project", - "tailwindcss": "project" - } - }, - "dependencies": { - "foo": "1.2.3", - "react-dom": "^16.13.1" - }, - "devDependencies": { - "@babel/cli": "7.8.4", - "@babel/core": "7.9.0", - "@babel/preset-env": "7.9.5", - "postcss-cli": "7.8.0", - "tailwindcss": "1.8.0" - }, - "name": "mypack", - "scripts": { - "client": "wait-on http://localhost:1313 && open http://localhost:1313", - "start": "run-p client server", - "test": "echo 'hoge' > hoge" - }, - "version": "1.2.3" -} -` - }) - }) - - t.Run("Create package.json, no default", func(t *testing.T) { - b := newTestBuilder(t, "") - - const origPackageJSON = `{ - "name": "mypack", - "version": "1.2.3", - "scripts": { - "client": "wait-on http://localhost:1313 && open http://localhost:1313", - "start": "run-p client server", - "test": "echo 'hoge' > hoge" - }, - "dependencies": { - "moo": "1.2.3" - } -}` - - b.WithSourceFile("package.json", origPackageJSON) - - b.Build(BuildCfg{}) - b.Assert(npm.Pack(b.H.BaseFs.ProjectSourceFs, b.H.BaseFs.AssetsWithDuplicatesPreserved.Fs), qt.IsNil) - - b.AssertFileContentFn("package.json", func(s string) bool { - return s == `{ - "comments": { - "dependencies": { - "moo": "project", - "react-dom": "github.com/gohugoio/hugoTestModule2" - }, - "devDependencies": { - "@babel/cli": "github.com/gohugoio/hugoTestModule2", - "@babel/core": "github.com/gohugoio/hugoTestModule2", - "@babel/preset-env": "github.com/gohugoio/hugoTestModule2", - "postcss-cli": "github.com/gohugoio/hugoTestModule2", - "tailwindcss": "github.com/gohugoio/hugoTestModule2" - } - }, - "dependencies": { - "moo": "1.2.3", - "react-dom": "^16.13.1" - }, - "devDependencies": { - "@babel/cli": "7.8.4", - "@babel/core": "7.9.0", - "@babel/preset-env": "7.9.5", - "postcss-cli": "7.1.0", - "tailwindcss": "1.2.0" - }, - "name": "mypack", - "scripts": { - "client": "wait-on http://localhost:1313 && open http://localhost:1313", - "start": "run-p client server", - "test": "echo 'hoge' > hoge" - }, - "version": "1.2.3" -} -` - }) - - // https://github.com/gohugoio/hugo/issues/7690 - b.AssertFileContent("package.hugo.json", origPackageJSON) - }) - - t.Run("Create package.json, no default, no package.json", func(t *testing.T) { - b := newTestBuilder(t, "") - - b.Build(BuildCfg{}) - b.Assert(npm.Pack(b.H.BaseFs.ProjectSourceFs, b.H.BaseFs.AssetsWithDuplicatesPreserved.Fs), qt.IsNil) - - b.AssertFileContentFn("package.json", func(s string) bool { - return s == `{ - "comments": { - "dependencies": { - "react-dom": "github.com/gohugoio/hugoTestModule2" - }, - "devDependencies": { - "@babel/cli": "github.com/gohugoio/hugoTestModule2", - "@babel/core": "github.com/gohugoio/hugoTestModule2", - "@babel/preset-env": "github.com/gohugoio/hugoTestModule2", - "postcss-cli": "github.com/gohugoio/hugoTestModule2", - "tailwindcss": "github.com/gohugoio/hugoTestModule2" - } - }, - "dependencies": { - "react-dom": "^16.13.1" - }, - "devDependencies": { - "@babel/cli": "7.8.4", - "@babel/core": "7.9.0", - "@babel/preset-env": "7.9.5", - "postcss-cli": "7.1.0", - "tailwindcss": "1.2.0" - }, - "name": "myhugosite", - "version": "0.1.0" -} -` - }) - }) -} - -// TODO(bep) this fails when testmodBuilder is also building ... -func TestHugoModulesMatrix(t *testing.T) { - if !htesting.IsCI() { - t.Skip("skip (relative) long running modules test when running locally") - } - t.Parallel() - - if !htesting.IsCI() || hugo.GoMinorVersion() < 12 { - // https://github.com/golang/go/issues/26794 - // There were some concurrent issues with Go modules in < Go 12. - t.Skip("skip this on local host and for Go <= 1.11 due to a bug in Go's stdlib") - } - - if testing.Short() { - t.Skip() - } - - rnd := rand.New(rand.NewSource(time.Now().UnixNano())) - gooss := []string{"linux", "darwin", "windows"} - goos := gooss[rnd.Intn(len(gooss))] - ignoreVendor := rnd.Intn(2) == 0 - testmods := mods.CreateModules(goos).Collect() - rnd.Shuffle(len(testmods), func(i, j int) { testmods[i], testmods[j] = testmods[j], testmods[i] }) - - for _, m := range testmods[:2] { - c := qt.New(t) - - workingDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-modules-test") - c.Assert(err, qt.IsNil) - defer clean() - - v := config.New() - v.Set("workingDir", workingDir) - v.Set("publishDir", "public") - - configTemplate := ` -baseURL = "https://example.com" -title = "My Modular Site" -workingDir = %q -theme = %q -ignoreVendorPaths = %q - -` - - ignoreVendorPaths := "" - if ignoreVendor { - ignoreVendorPaths = "github.com/**" - } - config := fmt.Sprintf(configTemplate, workingDir, m.Path(), ignoreVendorPaths) - - b := newTestSitesBuilder(t) - - // Need to use OS fs for this. - b.Fs = hugofs.NewDefault(v) - - b.WithWorkingDir(workingDir).WithConfigFile("toml", config) - b.WithContent("page.md", ` ---- -title: "Foo" ---- -`) - b.WithTemplates("home.html", ` - -{{ $mod := .Site.Data.modinfo.module }} -Mod Name: {{ $mod.name }} -Mod Version: {{ $mod.version }} ----- -{{ range $k, $v := .Site.Data.modinfo }} -- {{ $k }}: {{ range $kk, $vv := $v }}{{ $kk }}: {{ $vv }}|{{ end -}} -{{ end }} - - -`) - b.WithSourceFile("go.mod", ` -module github.com/gohugoio/tests/testHugoModules - - -`) - - b.Build(BuildCfg{}) - - // Verify that go.mod is autopopulated with all the modules in config.toml. - b.AssertFileContent("go.mod", m.Path()) - - b.AssertFileContent("public/index.html", - "Mod Name: "+m.Name(), - "Mod Version: v1.4.0") - - b.AssertFileContent("public/index.html", createChildModMatchers(m, ignoreVendor, m.Vendor)...) - - } -} - -func createChildModMatchers(m *mods.Md, ignoreVendor, vendored bool) []string { - // Child dependencies are one behind. - expectMinorVersion := 3 - - if !ignoreVendor && vendored { - // Vendored modules are stuck at v1.1.0. - expectMinorVersion = 1 - } - - expectVersion := fmt.Sprintf("v1.%d.0", expectMinorVersion) - - var matchers []string - - for _, mm := range m.Children { - matchers = append( - matchers, - fmt.Sprintf("%s: name: %s|version: %s", mm.Name(), mm.Name(), expectVersion)) - matchers = append(matchers, createChildModMatchers(mm, ignoreVendor, vendored || mm.Vendor)...) - } - return matchers -} - -func TestModulesWithContent(t *testing.T) { - t.Parallel() - - b := newTestSitesBuilder(t).WithWorkingDir("/site").WithConfigFile("toml", ` -baseURL="https://example.org" - -workingDir="/site" - -defaultContentLanguage = "en" - -[module] -[[module.imports]] -path="a" -[[module.imports.mounts]] -source="myacontent" -target="content/blog" -lang="en" -[[module.imports]] -path="b" -[[module.imports.mounts]] -source="mybcontent" -target="content/blog" -lang="nn" -[[module.imports]] -path="c" -[[module.imports]] -path="d" - -[languages] - -[languages.en] -title = "Title in English" -languageName = "English" -weight = 1 -[languages.nn] -languageName = "Nynorsk" -weight = 2 -title = "Tittel på nynorsk" -[languages.nb] -languageName = "Bokmål" -weight = 3 -title = "Tittel på bokmål" -[languages.fr] -languageName = "French" -weight = 4 -title = "French Title" - - -`) - - b.WithTemplatesAdded("index.html", ` -{{ range .Site.RegularPages }} -|{{ .Title }}|{{ .RelPermalink }}|{{ .Plain }} -{{ end }} -{{ $data := .Site.Data }} -Data Common: {{ $data.common.value }} -Data C: {{ $data.c.value }} -Data D: {{ $data.d.value }} -All Data: {{ $data }} - -i18n hello1: {{ i18n "hello1" . }} -i18n theme: {{ i18n "theme" . }} -i18n theme2: {{ i18n "theme2" . }} -`) - - content := func(id string) string { - return fmt.Sprintf(`--- -title: Title %s ---- -Content %s - -`, id, id) - } - - i18nContent := func(id, value string) string { - return fmt.Sprintf(` -[%s] -other = %q -`, id, value) - } - - // Content files - b.WithSourceFile("themes/a/myacontent/page.md", content("theme-a-en")) - b.WithSourceFile("themes/b/mybcontent/page.md", content("theme-b-nn")) - b.WithSourceFile("themes/c/content/blog/c.md", content("theme-c-nn")) - - // Data files - b.WithSourceFile("data/common.toml", `value="Project"`) - b.WithSourceFile("themes/c/data/common.toml", `value="Theme C"`) - b.WithSourceFile("themes/c/data/c.toml", `value="Hugo Rocks!"`) - b.WithSourceFile("themes/d/data/c.toml", `value="Hugo Rodcks!"`) - b.WithSourceFile("themes/d/data/d.toml", `value="Hugo Rodks!"`) - - // i18n files - b.WithSourceFile("i18n/en.toml", i18nContent("hello1", "Project")) - b.WithSourceFile("themes/c/i18n/en.toml", ` -[hello1] -other="Theme C Hello" -[theme] -other="Theme C" -`) - b.WithSourceFile("themes/d/i18n/en.toml", i18nContent("theme", "Theme D")) - b.WithSourceFile("themes/d/i18n/en.toml", i18nContent("theme2", "Theme2 D")) - - // Static files - b.WithSourceFile("themes/c/static/hello.txt", `Hugo Rocks!"`) - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", "|Title theme-a-en|/blog/page/|Content theme-a-en") - b.AssertFileContent("public/nn/index.html", "|Title theme-b-nn|/nn/blog/page/|Content theme-b-nn") - - // Data - b.AssertFileContent("public/index.html", - "Data Common: Project", - "Data C: Hugo Rocks!", - "Data D: Hugo Rodks!", - ) - - // i18n - b.AssertFileContent("public/index.html", - "i18n hello1: Project", - "i18n theme: Theme C", - "i18n theme2: Theme2 D", - ) -} - -func TestModulesIgnoreConfig(t *testing.T) { - b := newTestSitesBuilder(t).WithWorkingDir("/site").WithConfigFile("toml", ` -baseURL="https://example.org" - -workingDir="/site" - -[module] -[[module.imports]] -path="a" -ignoreConfig=true - -`) - - b.WithSourceFile("themes/a/config.toml", ` -[params] -a = "Should Be Ignored!" -`) - - b.WithTemplatesAdded("index.html", `Params: {{ .Site.Params }}`) - - b.Build(BuildCfg{}) - - b.AssertFileContentFn("public/index.html", func(s string) bool { - return !strings.Contains(s, "Ignored") - }) -} - -func TestModulesDisabled(t *testing.T) { - b := newTestSitesBuilder(t).WithWorkingDir("/site").WithConfigFile("toml", ` -baseURL="https://example.org" - -workingDir="/site" - -[module] -[[module.imports]] -path="a" -[[module.imports]] -path="b" -disable=true - - -`) - - b.WithSourceFile("themes/a/config.toml", ` -[params] -a = "A param" -`) - - b.WithSourceFile("themes/b/config.toml", ` -[params] -b = "B param" -`) - - b.WithTemplatesAdded("index.html", `Params: {{ .Site.Params }}`) - - b.Build(BuildCfg{}) - - b.AssertFileContentFn("public/index.html", func(s string) bool { - return strings.Contains(s, "A param") && !strings.Contains(s, "B param") - }) -} - -func TestModulesIncompatible(t *testing.T) { - t.Parallel() - - b := newTestSitesBuilder(t).WithWorkingDir("/site").WithConfigFile("toml", ` -baseURL="https://example.org" - -workingDir="/site" - -[module] -[[module.imports]] -path="ok" -[[module.imports]] -path="incompat1" -[[module.imports]] -path="incompat2" -[[module.imports]] -path="incompat3" - -`) - - b.WithSourceFile("themes/ok/data/ok.toml", `title = "OK"`) - - b.WithSourceFile("themes/incompat1/config.toml", ` - -[module] -[module.hugoVersion] -min = "0.33.2" -max = "0.45.0" - -`) - - // Old setup. - b.WithSourceFile("themes/incompat2/theme.toml", ` -min_version = "5.0.0" - -`) - - // Issue 6162 - b.WithSourceFile("themes/incompat3/theme.toml", ` -min_version = 0.55.0 - -`) - - logger := loggers.NewDefault() - b.WithLogger(logger) - - b.Build(BuildCfg{}) - - c := qt.New(t) - - c.Assert(logger.LoggCount(logg.LevelWarn), qt.Equals, 3) -} - -func TestMountsProject(t *testing.T) { - t.Parallel() - - files := ` --- config.toml -- -baseURL="https://example.org" - -[module] -[[module.mounts]] -source="mycontent" -target="content" --- layouts/_default/single.html -- -Permalink: {{ .Permalink }}| --- mycontent/mypage.md -- ---- -title: "My Page" ---- -` - b := Test(t, files) - - b.AssertFileContent("public/mypage/index.html", "Permalink: https://example.org/mypage/|") -} - -// https://github.com/gohugoio/hugo/issues/6684 -func TestMountsContentFile(t *testing.T) { - files := ` --- hugo.toml -- -disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "page", "section"] -disableLiveReload = true -[module] -[[module.mounts]] -source = "README.md" -target = "content/_index.md" --- README.md -- -# Hello World --- layouts/index.html -- -Home: {{ .Title }}|{{ .Content }}| -` - b := Test(t, files) - b.AssertFileContent("public/index.html", "Home: |

    Hello World

    \n|") -} - -// https://github.com/gohugoio/hugo/issues/6299 -func TestSiteWithGoModButNoModules(t *testing.T) { - t.Parallel() - - tempDir := t.TempDir() - - files := ` --- hugo.toml -- -baseURL = "https://example.org" --- go.mod -- - -` - - b := Test(t, files, TestOptWithConfig(func(cfg *IntegrationTestConfig) { - cfg.WorkingDir = tempDir - })) - - b.Build() -} - -// https://github.com/gohugoio/hugo/issues/6622 -func TestModuleAbsMount(t *testing.T) { - t.Parallel() - - c := qt.New(t) - // We need to use the OS fs for this. - workDir, clean1, err := htesting.CreateTempDir(hugofs.Os, "hugo-project") - c.Assert(err, qt.IsNil) - absContentDir, clean2, err := htesting.CreateTempDir(hugofs.Os, "hugo-content") - c.Assert(err, qt.IsNil) - - cfg := config.New() - cfg.Set("workingDir", workDir) - cfg.Set("publishDir", "public") - fs := hugofs.NewFromOld(hugofs.Os, cfg) - - config := fmt.Sprintf(` -workingDir=%q - -[module] - [[module.mounts]] - source = %q - target = "content" - -`, workDir, absContentDir) - - defer clean1() - defer clean2() - - b := newTestSitesBuilder(t) - b.Fs = fs - - contentFilename := filepath.Join(absContentDir, "p1.md") - afero.WriteFile(hugofs.Os, contentFilename, []byte(` ---- -title: Abs ---- - -Content. -`), 0o777) - - b.WithWorkingDir(workDir).WithConfigFile("toml", config) - b.WithContent("dummy.md", "") - - b.WithTemplatesAdded("index.html", ` -{{ $p1 := site.GetPage "p1" }} -P1: {{ $p1.Title }}|{{ $p1.RelPermalink }}|Filename: {{ $p1.File.Filename }} -`) - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", "P1: Abs|/p1/", "Filename: "+contentFilename) -} - -// Issue 9426 -func TestMountSameSource(t *testing.T) { - files := ` --- hugo.toml -- -baseURL = 'https://example.org/' -languageCode = 'en-us' -title = 'Hugo GitHub Issue #9426' - -disableKinds = ['RSS','sitemap','taxonomy','term'] - -[[module.mounts]] -source = "content" -target = "content" - -[[module.mounts]] -source = "extra-content" -target = "content/resources-a" - -[[module.mounts]] -source = "extra-content" -target = "content/resources-b" --- layouts/_default/single.html -- -Single --- content/p1.md -- --- extra-content/_index.md -- --- extra-content/subdir/_index.md -- --- extra-content/subdir/about.md -- -" -` - b := Test(t, files) - - b.AssertFileContent("public/resources-a/subdir/about/index.html", "Single") - b.AssertFileContent("public/resources-b/subdir/about/index.html", "Single") -} - -func TestMountData(t *testing.T) { - files := ` --- hugo.toml -- -baseURL = 'https://example.org/' -disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "page", "section"] - -[[module.mounts]] -source = "data" -target = "data" - -[[module.mounts]] -source = "extra-data" -target = "data/extra" --- extra-data/test.yaml -- -message: Hugo Rocks --- layouts/index.html -- -{{ site.Data.extra.test.message }} -` - - b := Test(t, files) - - b.AssertFileContent("public/index.html", "Hugo Rocks") -} diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go index 0b68af2ec..c3829fb1d 100644 --- a/hugolib/hugo_sites.go +++ b/hugolib/hugo_sites.go @@ -1,4 +1,4 @@ -// Copyright 2024 The Hugo Authors. All rights reserved. +// Copyright 2016-present 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,603 +14,643 @@ package hugolib import ( - "context" + "errors" "fmt" - "io" "strings" "sync" - "sync/atomic" - "github.com/bep/logg" - "github.com/gohugoio/hugo/cache/dynacache" - "github.com/gohugoio/hugo/config/allconfig" - "github.com/gohugoio/hugo/hugofs/glob" - "github.com/gohugoio/hugo/hugolib/doctree" - "github.com/gohugoio/hugo/resources" + "path/filepath" - "github.com/fsnotify/fsnotify" - - "github.com/gohugoio/hugo/output" - "github.com/gohugoio/hugo/parser/metadecoders" - - "github.com/gohugoio/hugo/common/hugo" - "github.com/gohugoio/hugo/common/maps" - "github.com/gohugoio/hugo/common/para" - "github.com/gohugoio/hugo/common/types" - "github.com/gohugoio/hugo/hugofs" - - "github.com/gohugoio/hugo/source" - - "github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/lazy" - "github.com/gohugoio/hugo/resources/page" + "github.com/gohugoio/hugo/i18n" + "github.com/gohugoio/hugo/tpl" + "github.com/gohugoio/hugo/tpl/tplimpl" ) // HugoSites represents the sites to build. Each site represents a language. type HugoSites struct { Sites []*Site - Configs *allconfig.Configs + runMode runmode - hugoInfo hugo.HugoInfo - - // Render output formats for all sites. - renderFormats output.Formats - - // The currently rendered Site. - currentSite *Site + multilingual *Multilingual *deps.Deps - - gitInfo *gitInfo - codeownerInfo *codeownerInfo - - // As loaded from the /data dirs - data map[string]any - - // Cache for page listings. - cachePages *dynacache.Partition[string, page.Pages] - // Cache for content sources. - cacheContentSource *dynacache.Partition[string, *resources.StaleValue[[]byte]] - - // Before Hugo 0.122.0 we managed all translations in a map using a translationKey - // that could be overridden in front matter. - // Now the different page dimensions (e.g. language) are built-in to the page trees above. - // But we sill need to support the overridden translationKey, but that should - // be relatively rare and low volume. - translationKeyPages *maps.SliceCache[page.Page] - - pageTrees *pageTrees - - printUnusedTemplatesInit sync.Once - printPathWarningsInit sync.Once - - // File change events with filename stored in this map will be skipped. - skipRebuildForFilenamesMu sync.Mutex - skipRebuildForFilenames map[string]bool - - init *hugoSitesInit - - workersSite *para.Workers - numWorkersSites int - numWorkers int - - *fatalErrorHandler - *buildCounters - // Tracks invocations of the Build method. - buildCounter atomic.Uint64 -} - -// ShouldSkipFileChangeEvent allows skipping filesystem event early before -// the build is started. -func (h *HugoSites) ShouldSkipFileChangeEvent(ev fsnotify.Event) bool { - h.skipRebuildForFilenamesMu.Lock() - defer h.skipRebuildForFilenamesMu.Unlock() - return h.skipRebuildForFilenames[ev.Name] -} - -func (h *HugoSites) Close() error { - return h.Deps.Close() -} - -func (h *HugoSites) isRebuild() bool { - return h.buildCounter.Load() > 0 -} - -func (h *HugoSites) resolveSite(lang string) *Site { - if lang == "" { - lang = h.Conf.DefaultContentLanguage() - } - - for _, s := range h.Sites { - if s.Lang() == lang { - return s - } - } - - return nil -} - -type buildCounters struct { - contentRenderCounter atomic.Uint64 - pageRenderCounter atomic.Uint64 -} - -func (c *buildCounters) loggFields() logg.Fields { - return logg.Fields{ - {Name: "pages", Value: c.pageRenderCounter.Load()}, - {Name: "content", Value: c.contentRenderCounter.Load()}, - } -} - -type fatalErrorHandler struct { - mu sync.Mutex - - h *HugoSites - - err error - - done bool - donec chan bool // will be closed when done -} - -// FatalError error is used in some rare situations where it does not make sense to -// continue processing, to abort as soon as possible and log the error. -func (f *fatalErrorHandler) FatalError(err error) { - f.mu.Lock() - defer f.mu.Unlock() - if !f.done { - f.done = true - close(f.donec) - } - f.err = err -} - -func (f *fatalErrorHandler) getErr() error { - f.mu.Lock() - defer f.mu.Unlock() - return f.err -} - -func (f *fatalErrorHandler) Done() <-chan bool { - return f.donec -} - -type hugoSitesInit struct { - // Loads the data from all of the /data folders. - data *lazy.Init - - // Loads the Git info and CODEOWNERS for all the pages if enabled. - gitInfo *lazy.Init -} - -func (h *HugoSites) Data() map[string]any { - if _, err := h.init.data.Do(context.Background()); err != nil { - h.SendError(fmt.Errorf("failed to load data: %w", err)) - return nil - } - return h.data -} - -// Pages returns all pages for all sites. -func (h *HugoSites) Pages() page.Pages { - key := "pages" - v, err := h.cachePages.GetOrCreate(key, func(string) (page.Pages, error) { - var pages page.Pages - for _, s := range h.Sites { - pages = append(pages, s.Pages()...) - } - page.SortByDefault(pages) - return pages, nil - }) - if err != nil { - panic(err) - } - return v -} - -// Pages returns all regularpages for all sites. -func (h *HugoSites) RegularPages() page.Pages { - key := "regular-pages" - v, err := h.cachePages.GetOrCreate(key, func(string) (page.Pages, error) { - var pages page.Pages - for _, s := range h.Sites { - pages = append(pages, s.RegularPages()...) - } - page.SortByDefault(pages) - - return pages, nil - }) - if err != nil { - panic(err) - } - return v -} - -func (h *HugoSites) gitInfoForPage(p page.Page) (source.GitInfo, error) { - if _, err := h.init.gitInfo.Do(context.Background()); err != nil { - return source.GitInfo{}, err - } - - if h.gitInfo == nil { - return source.GitInfo{}, nil - } - - return h.gitInfo.forPage(p), nil -} - -func (h *HugoSites) codeownersForPage(p page.Page) ([]string, error) { - if _, err := h.init.gitInfo.Do(context.Background()); err != nil { - return nil, err - } - - if h.codeownerInfo == nil { - return nil, nil - } - - return h.codeownerInfo.forPage(p), nil -} - -func (h *HugoSites) pickOneAndLogTheRest(errors []error) error { - if len(errors) == 0 { - return nil - } - - var i int - - for j, err := range errors { - // If this is in server mode, we want to return an error to the client - // with a file context, if possible. - if herrors.UnwrapFileError(err) != nil { - i = j - break - } - } - - // Log the rest, but add a threshold to avoid flooding the log. - const errLogThreshold = 5 - - for j, err := range errors { - if j == i || err == nil { - continue - } - - if j >= errLogThreshold { - break - } - - h.Log.Errorln(err) - } - - return errors[i] -} - -func (h *HugoSites) isMultilingual() bool { - return len(h.Sites) > 1 -} - -// TODO(bep) consolidate -func (h *HugoSites) LanguageSet() map[string]int { - set := make(map[string]int) - for i, s := range h.Sites { - set[s.language.Lang] = i - } - return set -} - -func (h *HugoSites) NumLogErrors() int { - if h == nil { - return 0 - } - return h.Log.LoggCount(logg.LevelError) -} - -func (h *HugoSites) PrintProcessingStats(w io.Writer) { - stats := make([]*helpers.ProcessingStats, len(h.Sites)) - for i := range h.Sites { - stats[i] = h.Sites[i].PathSpec.ProcessingStats - } - helpers.ProcessingStatsTable(w, stats...) } // GetContentPage finds a Page with content given the absolute filename. // Returns nil if none found. -func (h *HugoSites) GetContentPage(filename string) page.Page { - var p page.Page +func (h *HugoSites) GetContentPage(filename string) *Page { + s := h.Sites[0] + contendDir := filepath.Join(s.PathSpec.AbsPathify(s.Cfg.GetString("contentDir"))) + if !strings.HasPrefix(filename, contendDir) { + return nil + } - h.withPage(func(s string, p2 *pageState) bool { - if p2.File() == nil { - return false + rel := strings.TrimPrefix(filename, contendDir) + rel = strings.TrimPrefix(rel, helpers.FilePathSeparator) + + pos := s.rawAllPages.findPagePosByFilePath(rel) + + if pos == -1 { + return nil + } + return s.rawAllPages[pos] + +} + +// NewHugoSites creates a new collection of sites given the input sites, building +// a language configuration based on those. +func newHugoSites(cfg deps.DepsCfg, sites ...*Site) (*HugoSites, error) { + + if cfg.Language != nil { + return nil, errors.New("Cannot provide Language in Cfg when sites are provided") + } + + langConfig, err := newMultiLingualFromSites(cfg.Cfg, sites...) + + if err != nil { + return nil, err + } + + h := &HugoSites{ + multilingual: langConfig, + Sites: sites} + + for _, s := range sites { + s.owner = h + } + + // TODO(bep) + cfg.Cfg.Set("multilingual", sites[0].multilingualEnabled()) + + if err := applyDepsIfNeeded(cfg, sites...); err != nil { + return nil, err + } + + h.Deps = sites[0].Deps + + return h, nil +} + +func applyDepsIfNeeded(cfg deps.DepsCfg, sites ...*Site) error { + if cfg.TemplateProvider == nil { + cfg.TemplateProvider = tplimpl.DefaultTemplateProvider + } + + if cfg.TranslationProvider == nil { + cfg.TranslationProvider = i18n.NewTranslationProvider() + } + + var ( + d *deps.Deps + err error + ) + + for _, s := range sites { + if s.Deps != nil { + continue } - if p2.File().FileInfo().Meta().Filename == filename { - p = p2 - return true + if d == nil { + cfg.Language = s.Language + cfg.WithTemplate = s.withSiteTemplates(cfg.WithTemplate) + + var err error + d, err = deps.New(cfg) + if err != nil { + return err + } + + d.OutputFormatsConfig = s.outputFormatsConfig + s.Deps = d + + if err = d.LoadResources(); err != nil { + return err + } + + } else { + d, err = d.ForLanguage(s.Language) + if err != nil { + return err + } + d.OutputFormatsConfig = s.outputFormatsConfig + s.Deps = d } - for _, r := range p2.Resources().ByType(pageResourceType) { - p3 := r.(page.Page) - if p3.File() != nil && p3.File().FileInfo().Meta().Filename == filename { - p = p3 - return true + } + + return nil +} + +// NewHugoSites creates HugoSites from the given config. +func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) { + sites, err := createSitesFromConfig(cfg) + if err != nil { + return nil, err + } + return newHugoSites(cfg, sites...) +} + +func (s *Site) withSiteTemplates(withTemplates ...func(templ tpl.TemplateHandler) error) func(templ tpl.TemplateHandler) error { + return func(templ tpl.TemplateHandler) error { + templ.LoadTemplates(s.PathSpec.GetLayoutDirPath(), "") + if s.PathSpec.ThemeSet() { + templ.LoadTemplates(s.PathSpec.GetThemeDir()+"/layouts", "theme") + } + + for _, wt := range withTemplates { + if wt == nil { + continue + } + if err := wt(templ); err != nil { + return err } } - return false - }) - - return p + return nil + } } -func (h *HugoSites) loadGitInfo() error { - if h.Configs.Base.EnableGitInfo { - gi, err := newGitInfo(h.Deps) +func createSitesFromConfig(cfg deps.DepsCfg) ([]*Site, error) { + + var ( + sites []*Site + ) + + multilingual := cfg.Cfg.GetStringMap("languages") + + if len(multilingual) == 0 { + l := helpers.NewDefaultLanguage(cfg.Cfg) + cfg.Language = l + s, err := newSite(cfg) if err != nil { - h.Log.Errorln("Failed to read Git log:", err) - } else { - h.gitInfo = gi + return nil, err + } + sites = append(sites, s) + } + + if len(multilingual) > 0 { + var err error + + languages, err := toSortedLanguages(cfg.Cfg, multilingual) + + if err != nil { + return nil, fmt.Errorf("Failed to parse multilingual config: %s", err) } - co, err := newCodeOwners(h.Configs.LoadingInfo.BaseConfig.WorkingDir) - if err != nil { - h.Log.Errorln("Failed to read CODEOWNERS:", err) - } else { - h.codeownerInfo = co + for _, lang := range languages { + var s *Site + var err error + cfg.Language = lang + s, err = newSite(cfg) + + if err != nil { + return nil, err + } + + sites = append(sites, s) } } + + return sites, nil +} + +// Reset resets the sites and template caches, making it ready for a full rebuild. +func (h *HugoSites) reset() { + for i, s := range h.Sites { + h.Sites[i] = s.reset() + } +} + +func (h *HugoSites) createSitesFromConfig() error { + + depsCfg := deps.DepsCfg{Fs: h.Fs, Cfg: h.Cfg} + sites, err := createSitesFromConfig(depsCfg) + + if err != nil { + return err + } + + langConfig, err := newMultiLingualFromSites(depsCfg.Cfg, sites...) + + if err != nil { + return err + } + + h.Sites = sites + + for _, s := range sites { + s.owner = h + } + + if err := applyDepsIfNeeded(depsCfg, sites...); err != nil { + return err + } + + h.Deps = sites[0].Deps + + h.multilingual = langConfig + return nil } -// Reset resets the sites and template caches etc., making it ready for a full rebuild. -func (h *HugoSites) reset(config *BuildCfg) { - h.fatalErrorHandler = &fatalErrorHandler{ - h: h, - donec: make(chan bool), +func (h *HugoSites) toSiteInfos() []*SiteInfo { + infos := make([]*SiteInfo, len(h.Sites)) + for i, s := range h.Sites { + infos[i] = &s.Info } -} - -// resetLogs resets the log counters etc. Used to do a new build on the same sites. -func (h *HugoSites) resetLogs() { - h.Log.Reset() - for _, s := range h.Sites { - s.Deps.Log.Reset() - } -} - -func (h *HugoSites) withSite(fn func(s *Site) error) error { - for _, s := range h.Sites { - if err := fn(s); err != nil { - return err - } - } - return nil -} - -func (h *HugoSites) withPage(fn func(s string, p *pageState) bool) { - h.withSite(func(s *Site) error { - w := &doctree.NodeShiftTreeWalker[contentNodeI]{ - Tree: s.pageMap.treePages, - LockType: doctree.LockTypeRead, - Handle: func(s string, n contentNodeI, match doctree.DimensionFlag) (bool, error) { - return fn(s, n.(*pageState)), nil - }, - } - return w.Walk(context.Background()) - }) + return infos } // BuildCfg holds build options used to, as an example, skip the render step. type BuildCfg struct { + // Whether we are in watch (server) mode + Watching bool + // Print build stats at the end of a build + PrintStats bool + // Reset site state before build. Use to force full rebuilds. + ResetState bool + // Re-creates the sites from configuration before a build. + // This is needed if new languages are added. + CreateSitesFromConfig bool // Skip rendering. Useful for testing. SkipRender bool - // Use this to indicate what changed (for rebuilds). - WhatChanged *WhatChanged - - // This is a partial re-render of some selected pages. - PartialReRender bool - - // Set in server mode when the last build failed for some reason. - ErrRecovery bool - - // Recently visited or touched URLs. This is used for partial re-rendering. - RecentlyTouched *types.EvictingQueue[string] - - // Can be set to build only with a sub set of the content source. - ContentInclusionFilter *glob.FilenameFilter - - // Set when the buildlock is already acquired (e.g. the archetype content builder). - NoBuildLock bool - - testCounters *buildCounters + whatChanged *whatChanged } -// shouldRender returns whether this output format should be rendered or not. -func (cfg *BuildCfg) shouldRender(infol logg.LevelLogger, p *pageState) bool { - if p.skipRender() { - return false - } +func (h *HugoSites) renderCrossSitesArtifacts() error { - if !p.renderOnce { - return true - } - - // The render state is incremented on render and reset when a related change is detected. - // Note that this is set per output format. - shouldRender := p.renderState == 0 - - if !shouldRender { - return false - } - - fastRenderMode := p.s.Conf.FastRenderMode() - - if !fastRenderMode || p.s.h.buildCounter.Load() == 0 { - return shouldRender - } - - if !p.render { - // Not be to rendered for this output format. - return false - } - - if relURL := p.getRelURL(); relURL != "" { - if cfg.RecentlyTouched.Contains(relURL) { - infol.Logf("render recently touched URL %q (%s)", relURL, p.outputFormat().Name) - return true - } - } - - // In fast render mode, we want to avoid re-rendering the sitemaps etc. and - // other big listings whenever we e.g. change a content file, - // but we want partial renders of the recently touched pages to also include - // alternative formats of the same HTML page (e.g. RSS, JSON). - for _, po := range p.pageOutputs { - if po.render && po.f.IsHTML && cfg.RecentlyTouched.Contains(po.getRelURL()) { - infol.Logf("render recently touched URL %q, %s version of %s", po.getRelURL(), po.f.Name, p.outputFormat().Name) - return true - } - } - - return false -} - -func (s *Site) preparePagesForRender(isRenderingSite bool, idx int) error { - var err error - - initPage := func(p *pageState) error { - if err = p.shiftToOutputFormat(isRenderingSite, idx); err != nil { - return err - } + if !h.multilingual.enabled() { return nil } - return s.pageMap.forEeachPageIncludingBundledPages(nil, - func(p *pageState) (bool, error) { - return false, initPage(p) - }, - ) + if h.Cfg.GetBool("disableSitemap") { + return nil + } + + sitemapEnabled := false + for _, s := range h.Sites { + if s.isEnabled(kindSitemap) { + sitemapEnabled = true + break + } + } + + if !sitemapEnabled { + return nil + } + + // TODO(bep) DRY + sitemapDefault := parseSitemap(h.Cfg.GetStringMap("sitemap")) + + s := h.Sites[0] + + smLayouts := []string{"sitemapindex.xml", "_default/sitemapindex.xml", "_internal/_default/sitemapindex.xml"} + + return s.renderAndWriteXML("sitemapindex", + sitemapDefault.Filename, h.toSiteInfos(), s.appendThemeTemplates(smLayouts)...) } -func (h *HugoSites) loadData() error { - h.data = make(map[string]any) - w := hugofs.NewWalkway( - hugofs.WalkwayConfig{ - Fs: h.PathSpec.BaseFs.Data.Fs, - IgnoreFile: h.SourceSpec.IgnoreFile, - PathParser: h.Conf.PathParser(), - WalkFn: func(path string, fi hugofs.FileMetaInfo) error { - if fi.IsDir() { - return nil - } - pi := fi.Meta().PathInfo - if pi == nil { - panic("no path info") - } - return h.handleDataFile(source.NewFileInfo(fi)) - }, - }) +func (h *HugoSites) assignMissingTranslations() error { + // This looks heavy, but it should be a small number of nodes by now. + allPages := h.findAllPagesByKindNotIn(KindPage) + for _, nodeType := range []string{KindHome, KindSection, KindTaxonomy, KindTaxonomyTerm} { + nodes := h.findPagesByKindIn(nodeType, allPages) - if err := w.Walk(); err != nil { - return err + // Assign translations + for _, t1 := range nodes { + for _, t2 := range nodes { + if t1.isNewTranslation(t2) { + t1.translations = append(t1.translations, t2) + } + } + } } + + // Now we can sort the translations. + for _, p := range allPages { + if len(p.translations) > 0 { + pageBy(languagePageSort).Sort(p.translations) + } + } + return nil + +} + +// createMissingPages creates home page, taxonomies etc. that isnt't created as an +// effect of having a content file. +func (h *HugoSites) createMissingPages() error { + var newPages Pages + + for _, s := range h.Sites { + if s.isEnabled(KindHome) { + // home pages + home := s.findPagesByKind(KindHome) + if len(home) > 1 { + panic("Too many homes") + } + if len(home) == 0 { + n := s.newHomePage() + s.Pages = append(s.Pages, n) + newPages = append(newPages, n) + } + } + + // Will create content-less root sections. + newSections := s.assembleSections() + s.Pages = append(s.Pages, newSections...) + newPages = append(newPages, newSections...) + + // taxonomy list and terms pages + taxonomies := s.Language.GetStringMapString("taxonomies") + if len(taxonomies) > 0 { + taxonomyPages := s.findPagesByKind(KindTaxonomy) + taxonomyTermsPages := s.findPagesByKind(KindTaxonomyTerm) + for _, plural := range taxonomies { + if s.isEnabled(KindTaxonomyTerm) { + foundTaxonomyTermsPage := false + for _, p := range taxonomyTermsPages { + if p.sections[0] == plural { + foundTaxonomyTermsPage = true + break + } + } + + if !foundTaxonomyTermsPage { + foundTaxonomyTermsPage = true + n := s.newTaxonomyTermsPage(plural) + s.Pages = append(s.Pages, n) + newPages = append(newPages, n) + } + } + + if s.isEnabled(KindTaxonomy) { + for key := range s.Taxonomies[plural] { + foundTaxonomyPage := false + origKey := key + + if s.Info.preserveTaxonomyNames { + key = s.PathSpec.MakePathSanitized(key) + } + for _, p := range taxonomyPages { + if p.sections[0] == plural && p.sections[1] == key { + foundTaxonomyPage = true + break + } + } + + if !foundTaxonomyPage { + n := s.newTaxonomyPage(plural, origKey) + s.Pages = append(s.Pages, n) + newPages = append(newPages, n) + } + } + } + } + } + } + + if len(newPages) > 0 { + // This resorting is unfortunate, but it also needs to be sorted + // when sections are created. + first := h.Sites[0] + + first.AllPages = append(first.AllPages, newPages...) + + first.AllPages.Sort() + + for _, s := range h.Sites { + s.Pages.Sort() + } + + for i := 1; i < len(h.Sites); i++ { + h.Sites[i].AllPages = first.AllPages + } + } + return nil } -func (h *HugoSites) handleDataFile(r *source.File) error { - var current map[string]any +func (s *Site) assignSiteByLanguage(p *Page) { - f, err := r.FileInfo().Meta().Open() - if err != nil { - return fmt.Errorf("data: failed to open %q: %w", r.LogicalName(), err) + pageLang := p.Lang() + + if pageLang == "" { + panic("Page language missing: " + p.Title) } - defer f.Close() - // Crawl in data tree to insert data - current = h.data - dataPath := r.FileInfo().Meta().PathInfo.Unnormalized().Dir()[1:] - keyParts := strings.Split(dataPath, "/") - - for _, key := range keyParts { - if key != "" { - if _, ok := current[key]; !ok { - current[key] = make(map[string]any) - } - current = current[key].(map[string]any) + for _, site := range s.owner.Sites { + if strings.HasPrefix(site.Language.Lang, pageLang) { + p.s = site + p.Site = &site.Info + return } } - data, err := h.readData(r) - if err != nil { - return h.errWithFileContext(err, r) +} + +func (h *HugoSites) setupTranslations() { + + master := h.Sites[0] + + for _, p := range master.rawAllPages { + if p.Lang() == "" { + panic("Page language missing: " + p.Title) + } + + if p.Kind == kindUnknown { + p.Kind = p.s.kindFromSections(p.sections) + } + + if !p.s.isEnabled(p.Kind) { + continue + } + + shouldBuild := p.shouldBuild() + + for i, site := range h.Sites { + // The site is assigned by language when read. + if site == p.s { + site.updateBuildStats(p) + if shouldBuild { + site.Pages = append(site.Pages, p) + } + } + + if !shouldBuild { + continue + } + + if i == 0 { + site.AllPages = append(site.AllPages, p) + } + } + } - if data == nil { - return nil + // Pull over the collections from the master site + for i := 1; i < len(h.Sites); i++ { + h.Sites[i].AllPages = h.Sites[0].AllPages + h.Sites[i].Data = h.Sites[0].Data } - // filepath.Walk walks the files in lexical order, '/' comes before '.' - higherPrecedentData := current[r.BaseFileName()] + if len(h.Sites) > 1 { + pages := h.Sites[0].AllPages + allTranslations := pagesToTranslationsMap(pages) + assignTranslationsToPages(allTranslations, pages) + } +} - switch data.(type) { - case map[string]any: +func (s *Site) preparePagesForRender(cfg *BuildCfg) { - switch higherPrecedentData.(type) { - case nil: - current[r.BaseFileName()] = data - case map[string]any: - // merge maps: insert entries from data for keys that - // don't already exist in higherPrecedentData - higherPrecedentMap := higherPrecedentData.(map[string]any) - for key, value := range data.(map[string]any) { - if _, exists := higherPrecedentMap[key]; exists { - // this warning could happen if - // 1. A theme uses the same key; the main data folder wins - // 2. A sub folder uses the same key: the sub folder wins - // TODO(bep) figure out a way to detect 2) above and make that a WARN - h.Log.Infof("Data for key '%s' in path '%s' is overridden by higher precedence data already in the data tree", key, r.Path()) + pageChan := make(chan *Page) + wg := &sync.WaitGroup{} + numWorkers := getGoMaxProcs() * 4 + + for i := 0; i < numWorkers; i++ { + wg.Add(1) + go func(pages <-chan *Page, wg *sync.WaitGroup) { + defer wg.Done() + for p := range pages { + if !p.shouldRenderTo(s.rc.Format) { + // No need to prepare + continue + } + var shortcodeUpdate bool + if p.shortcodeState != nil { + shortcodeUpdate = p.shortcodeState.updateDelta() + } + + if !shortcodeUpdate && !cfg.whatChanged.other && p.rendered { + // No need to process it again. + continue + } + + // If we got this far it means that this is either a new Page pointer + // or a template or similar has changed so wee need to do a rerendering + // of the shortcodes etc. + + // Mark it as rendered + p.rendered = true + + // If in watch mode or if we have multiple output formats, + // we need to keep the original so we can + // potentially repeat this process on rebuild. + needsACopy := cfg.Watching || len(p.outputFormats) > 1 + var workContentCopy []byte + if needsACopy { + workContentCopy = make([]byte, len(p.workContent)) + copy(workContentCopy, p.workContent) } else { - higherPrecedentMap[key] = value + // Just reuse the same slice. + workContentCopy = p.workContent } + + if p.Markup == "markdown" { + tmpContent, tmpTableOfContents := helpers.ExtractTOC(workContentCopy) + p.TableOfContents = helpers.BytesToHTML(tmpTableOfContents) + workContentCopy = tmpContent + } + + var err error + if workContentCopy, err = handleShortcodes(p, workContentCopy); err != nil { + s.Log.ERROR.Printf("Failed to handle shortcodes for page %s: %s", p.BaseFileName(), err) + } + + if p.Markup != "html" { + + // Now we know enough to create a summary of the page and count some words + summaryContent, err := p.setUserDefinedSummaryIfProvided(workContentCopy) + + if err != nil { + s.Log.ERROR.Printf("Failed to set user defined summary for page %q: %s", p.Path(), err) + } else if summaryContent != nil { + workContentCopy = summaryContent.content + } + + p.Content = helpers.BytesToHTML(workContentCopy) + + if summaryContent == nil { + if err := p.setAutoSummary(); err != nil { + s.Log.ERROR.Printf("Failed to set user auto summary for page %q: %s", p.pathOrTitle(), err) + } + } + + } else { + p.Content = helpers.BytesToHTML(workContentCopy) + } + + //analyze for raw stats + p.analyzePage() + } - default: - // can't merge: higherPrecedentData is not a map - h.Log.Warnf("The %T data from '%s' overridden by "+ - "higher precedence %T data already in the data tree", data, r.Path(), higherPrecedentData) - } - - case []any: - if higherPrecedentData == nil { - current[r.BaseFileName()] = data - } else { - // we don't merge array data - h.Log.Warnf("The %T data from '%s' overridden by "+ - "higher precedence %T data already in the data tree", data, r.Path(), higherPrecedentData) - } - - default: - h.Log.Errorf("unexpected data type %T in file %s", data, r.LogicalName()) + }(pageChan, wg) } - return nil -} - -func (h *HugoSites) errWithFileContext(err error, f *source.File) error { - realFilename := f.FileInfo().Meta().Filename - return herrors.NewFileErrorFromFile(err, realFilename, h.Fs.Source, nil) -} - -func (h *HugoSites) readData(f *source.File) (any, error) { - file, err := f.FileInfo().Meta().Open() - if err != nil { - return nil, fmt.Errorf("readData: failed to open data file: %w", err) + for _, p := range s.Pages { + pageChan <- p } - defer file.Close() - content := helpers.ReaderToBytes(file) - format := metadecoders.FormatFromString(f.Ext()) - return metadecoders.Default.Unmarshal(content, format) + close(pageChan) + + wg.Wait() + +} + +// Pages returns all pages for all sites. +func (h *HugoSites) Pages() Pages { + return h.Sites[0].AllPages +} + +func handleShortcodes(p *Page, rawContentCopy []byte) ([]byte, error) { + if p.shortcodeState != nil && len(p.shortcodeState.contentShortcodes) > 0 { + p.s.Log.DEBUG.Printf("Replace %d shortcodes in %q", len(p.shortcodeState.contentShortcodes), p.BaseFileName()) + err := p.shortcodeState.executeShortcodesForDelta(p) + + if err != nil { + return rawContentCopy, err + } + + rawContentCopy, err = replaceShortcodeTokens(rawContentCopy, shortcodePlaceholderPrefix, p.shortcodeState.renderedShortcodes) + + if err != nil { + p.s.Log.FATAL.Printf("Failed to replace shortcode tokens in %s:\n%s", p.BaseFileName(), err.Error()) + } + } + + return rawContentCopy, nil +} + +func (s *Site) updateBuildStats(page *Page) { + if page.IsDraft() { + s.draftCount++ + } + + if page.IsFuture() { + s.futureCount++ + } + + if page.IsExpired() { + s.expiredCount++ + } +} + +func (h *HugoSites) findPagesByKindNotIn(kind string, inPages Pages) Pages { + return h.Sites[0].findPagesByKindNotIn(kind, inPages) +} + +func (h *HugoSites) findPagesByKindIn(kind string, inPages Pages) Pages { + return h.Sites[0].findPagesByKindIn(kind, inPages) +} + +func (h *HugoSites) findAllPagesByKind(kind string) Pages { + return h.findPagesByKindIn(kind, h.Sites[0].AllPages) +} + +func (h *HugoSites) findAllPagesByKindNotIn(kind string) Pages { + return h.findPagesByKindNotIn(kind, h.Sites[0].AllPages) } diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go index ce8ddd143..fa0eac702 100644 --- a/hugolib/hugo_sites_build.go +++ b/hugolib/hugo_sites_build.go @@ -1,4 +1,4 @@ -// Copyright 2024 The Hugo Authors. All rights reserved. +// Copyright 2016-present 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,1249 +14,224 @@ package hugolib import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "os" - "path" - "path/filepath" - "strings" "time" - "github.com/bep/logg" - "github.com/gohugoio/hugo/bufferpool" - "github.com/gohugoio/hugo/deps" - "github.com/gohugoio/hugo/hugofs" - "github.com/gohugoio/hugo/hugofs/files" - "github.com/gohugoio/hugo/hugofs/glob" - "github.com/gohugoio/hugo/hugolib/doctree" - "github.com/gohugoio/hugo/hugolib/pagesfromdata" - "github.com/gohugoio/hugo/hugolib/segments" - "github.com/gohugoio/hugo/identity" - "github.com/gohugoio/hugo/output" - "github.com/gohugoio/hugo/publisher" - "github.com/gohugoio/hugo/source" - "github.com/gohugoio/hugo/tpl" - - "github.com/gohugoio/hugo/common/herrors" - "github.com/gohugoio/hugo/common/loggers" - "github.com/gohugoio/hugo/common/para" - "github.com/gohugoio/hugo/common/paths" - "github.com/gohugoio/hugo/common/rungroup" - "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/resources/page" - "github.com/gohugoio/hugo/resources/page/siteidentities" - "github.com/gohugoio/hugo/resources/postpub" - - "github.com/spf13/afero" + "errors" "github.com/fsnotify/fsnotify" + "github.com/gohugoio/hugo/helpers" ) // Build builds all sites. If filesystem events are provided, // this is considered to be a potential partial rebuild. func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error { - infol := h.Log.InfoCommand("build") - defer loggers.TimeTrackf(infol, time.Now(), nil, "") - defer func() { - h.buildCounter.Add(1) - }() - - if h.Deps == nil { - panic("must have deps") - } - - if !config.NoBuildLock { - unlock, err := h.BaseFs.LockBuild() - if err != nil { - return fmt.Errorf("failed to acquire a build lock: %w", err) - } - defer unlock() - } - - defer func() { - for _, s := range h.Sites { - s.Deps.BuildEndListeners.Notify() - } - }() - - errCollector := h.StartErrorCollector() - errs := make(chan error) - - go func(from, to chan error) { - var errors []error - i := 0 - for e := range from { - i++ - if i > 50 { - break - } - errors = append(errors, e) - } - to <- h.pickOneAndLogTheRest(errors) - - close(to) - }(errCollector, errs) - - for _, s := range h.Sites { - s.state = siteStateInit - } - - if h.Metrics != nil { - h.Metrics.Reset() - } - - h.buildCounters = config.testCounters - if h.buildCounters == nil { - h.buildCounters = &buildCounters{} - } + t0 := time.Now() // Need a pointer as this may be modified. conf := &config - if conf.WhatChanged == nil { + + if conf.whatChanged == nil { // Assume everything has changed - conf.WhatChanged = &WhatChanged{needsPagesAssembly: true} + conf.whatChanged = &whatChanged{source: true, other: true} } - var prepareErr error - - if !config.PartialReRender { - prepare := func() error { - init := func(conf *BuildCfg) error { - for _, s := range h.Sites { - s.Deps.BuildStartListeners.Notify() - } - - if len(events) > 0 || len(conf.WhatChanged.Changes()) > 0 { - // Rebuild - if err := h.initRebuild(conf); err != nil { - return fmt.Errorf("initRebuild: %w", err) - } - } else { - if err := h.initSites(conf); err != nil { - return fmt.Errorf("initSites: %w", err) - } - } - - return nil - } - - ctx := context.Background() - - if err := h.process(ctx, infol, conf, init, events...); err != nil { - return fmt.Errorf("process: %w", err) - } - - if err := h.assemble(ctx, infol, conf); err != nil { - return fmt.Errorf("assemble: %w", err) - } - - return nil - } - - if prepareErr = prepare(); prepareErr != nil { - h.SendError(prepareErr) - } - } - - for _, s := range h.Sites { - s.state = siteStateReady - } - - if prepareErr == nil { - if err := h.render(infol, conf); err != nil { - h.SendError(fmt.Errorf("render: %w", err)) - } - - // Make sure to write any build stats to disk first so it's available - // to the post processors. - if err := h.writeBuildStats(); err != nil { + if len(events) > 0 { + // Rebuild + if err := h.initRebuild(conf); err != nil { return err } - - // We need to do this before render deferred. - if err := h.printPathWarningsOnce(); err != nil { - h.SendError(fmt.Errorf("printPathWarnings: %w", err)) - } - - if err := h.renderDeferred(infol); err != nil { - h.SendError(fmt.Errorf("renderDeferred: %w", err)) - } - - // This needs to be done after the deferred rendering to get complete template usage coverage. - if err := h.printUnusedTemplatesOnce(); err != nil { - h.SendError(fmt.Errorf("printPathWarnings: %w", err)) - } - - if err := h.postProcess(infol); err != nil { - h.SendError(fmt.Errorf("postProcess: %w", err)) + } else { + if err := h.init(conf); err != nil { + return err } } - if h.Metrics != nil { - var b bytes.Buffer - h.Metrics.WriteMetrics(&b) - - h.Log.Printf("\nTemplate Metrics:\n\n") - h.Log.Println(b.String()) - } - - h.StopErrorCollector() - - err := <-errs - if err != nil { + if err := h.process(conf, events...); err != nil { return err } - if err := h.fatalErrorHandler.getErr(); err != nil { + if err := h.assemble(conf); err != nil { return err } - errorCount := h.Log.LoggCount(logg.LevelError) + loggers.Log().LoggCount(logg.LevelError) - if errorCount > 0 { - return fmt.Errorf("logged %d error(s)", errorCount) + if err := h.render(conf); err != nil { + return err + } + + if config.PrintStats { + h.Log.FEEDBACK.Printf("total in %v ms\n", int(1000*time.Since(t0).Seconds())) } return nil + } // Build lifecycle methods below. // The order listed matches the order of execution. -func (h *HugoSites) initSites(config *BuildCfg) error { - h.reset(config) +func (h *HugoSites) init(config *BuildCfg) error { + + for _, s := range h.Sites { + if s.PageCollections == nil { + s.PageCollections = newPageCollections() + } + } + + if config.ResetState { + h.reset() + } + + if config.CreateSitesFromConfig { + if err := h.createSitesFromConfig(); err != nil { + return err + } + } + + h.runMode.Watching = config.Watching + return nil } func (h *HugoSites) initRebuild(config *BuildCfg) error { - if !h.Configs.Base.Internal.Watch { - return errors.New("rebuild called when not in watch mode") + if config.CreateSitesFromConfig { + return errors.New("Rebuild does not support 'CreateSitesFromConfig'.") } - h.pageTrees.treePagesResources.WalkPrefixRaw("", func(key string, n contentNodeI) bool { - n.resetBuildState() - return false - }) + if config.ResetState { + return errors.New("Rebuild does not support 'ResetState'.") + } + + if !config.Watching { + return errors.New("Rebuild called when not in watch mode") + } + + h.runMode.Watching = config.Watching + + if config.whatChanged.source { + // This is for the non-renderable content pages (rarely used, I guess). + // We could maybe detect if this is really needed, but it should be + // pretty fast. + h.TemplateHandler().RebuildClone() + } for _, s := range h.Sites { - s.resetBuildState(config.WhatChanged.needsPagesAssembly) + s.resetBuildState() } - h.reset(config) - h.resetLogs() + helpers.InitLoggers() return nil } -// process prepares the Sites' sources for a full or partial rebuild. -// This will also parse the source and create all the Page objects. -func (h *HugoSites) process(ctx context.Context, l logg.LevelLogger, config *BuildCfg, init func(config *BuildCfg) error, events ...fsnotify.Event) error { - l = l.WithField("step", "process") - defer loggers.TimeTrackf(l, time.Now(), nil, "") +func (h *HugoSites) process(config *BuildCfg, events ...fsnotify.Event) error { + // We should probably refactor the Site and pull up most of the logic from there to here, + // but that seems like a daunting task. + // So for now, if there are more than one site (language), + // we pre-process the first one, then configure all the sites based on that. + + firstSite := h.Sites[0] if len(events) > 0 { - // This is a rebuild triggered from file events. - return h.processPartialFileEvents(ctx, l, config, init, events) - } else if len(config.WhatChanged.Changes()) > 0 { - // Rebuild triggered from remote events. - if err := init(config); err != nil { - return err - } - return h.processPartialRebuildChanges(ctx, l, config) - } - return h.processFull(ctx, l, config) -} - -// assemble creates missing sections, applies aggregate values (e.g. dates, cascading params), -// removes disabled pages etc. -func (h *HugoSites) assemble(ctx context.Context, l logg.LevelLogger, bcfg *BuildCfg) error { - l = l.WithField("step", "assemble") - defer loggers.TimeTrackf(l, time.Now(), nil, "") - - if !bcfg.WhatChanged.needsPagesAssembly { - changes := bcfg.WhatChanged.Drain() - if len(changes) > 0 { - if err := h.resolveAndClearStateForIdentities(ctx, l, nil, changes); err != nil { - return err - } - } - return nil - } - - h.translationKeyPages.Reset() - assemblers := make([]*sitePagesAssembler, len(h.Sites)) - // Changes detected during assembly (e.g. aggregate date changes) - - for i, s := range h.Sites { - assemblers[i] = &sitePagesAssembler{ - Site: s, - assembleChanges: bcfg.WhatChanged, - ctx: ctx, - } - } - - g, _ := h.workersSite.Start(ctx) - for _, s := range assemblers { - s := s - g.Run(func() error { - return s.assemblePagesStep1(ctx) - }) - } - if err := g.Wait(); err != nil { + // This is a rebuild + changed, err := firstSite.reProcess(events) + config.whatChanged = &changed return err } - changes := bcfg.WhatChanged.Drain() + return firstSite.process(*config) - // Changes from the assemble step (e.g. lastMod, cascade) needs a re-calculation - // of what needs to be re-built. - if len(changes) > 0 { - if err := h.resolveAndClearStateForIdentities(ctx, l, nil, changes); err != nil { - return err - } - } - - for _, s := range assemblers { - if err := s.assemblePagesStep2(); err != nil { - return err - } - } - - // Handle new terms from assemblePagesStep2. - changes = bcfg.WhatChanged.Drain() - if len(changes) > 0 { - if err := h.resolveAndClearStateForIdentities(ctx, l, nil, changes); err != nil { - return err - } - } - - h.renderFormats = output.Formats{} - for _, s := range h.Sites { - s.s.initRenderFormats() - h.renderFormats = append(h.renderFormats, s.renderFormats...) - } - - for _, s := range assemblers { - if err := s.assemblePagesStepFinal(); err != nil { - return err - } - } - - return nil } -// render renders the sites. -func (h *HugoSites) render(l logg.LevelLogger, config *BuildCfg) error { - l = l.WithField("step", "render") - start := time.Now() - defer func() { - loggers.TimeTrackf(l, start, h.buildCounters.loggFields(), "") - }() - - siteRenderContext := &siteRenderContext{cfg: config, infol: l, multihost: h.Configs.IsMultihost} - - renderErr := func(err error) error { - if err == nil { - return nil - } - // In Hugo 0.141.0 we replaced the special error handling for resources.GetRemote - // with the more general try. - if strings.Contains(err.Error(), "can't evaluate field Err in type") { - if strings.Contains(err.Error(), "resource.Resource") { - return fmt.Errorf("%s: Resource.Err was removed in Hugo v0.141.0 and replaced with a new try keyword, see https://gohugo.io/functions/go-template/try/", err) - } else if strings.Contains(err.Error(), "template.HTML") { - return fmt.Errorf("%s: the return type of transform.ToMath was changed in Hugo v0.141.0 and the error handling replaced with a new try keyword, see https://gohugo.io/functions/go-template/try/", err) - } - } - return err - } - - i := 0 - for _, s := range h.Sites { - segmentFilter := s.conf.C.SegmentFilter - if segmentFilter.ShouldExcludeCoarse(segments.SegmentMatcherFields{Lang: s.language.Lang}) { - l.Logf("skip language %q not matching segments set in --renderSegments", s.language.Lang) - continue - } - - siteRenderContext.languageIdx = s.languagei - h.currentSite = s - for siteOutIdx, renderFormat := range s.renderFormats { - if segmentFilter.ShouldExcludeCoarse(segments.SegmentMatcherFields{Output: renderFormat.Name, Lang: s.language.Lang}) { - l.Logf("skip output format %q for language %q not matching segments set in --renderSegments", renderFormat.Name, s.language.Lang) - continue - } - - if err := func() error { - rc := tpl.RenderingContext{Site: s, SiteOutIdx: siteOutIdx} - h.BuildState.StartStageRender(rc) - defer h.BuildState.StopStageRender(rc) - - siteRenderContext.outIdx = siteOutIdx - siteRenderContext.sitesOutIdx = i - i++ - - select { - case <-h.Done(): - return nil - default: - for _, s2 := range h.Sites { - if err := s2.preparePagesForRender(s == s2, siteRenderContext.sitesOutIdx); err != nil { - return err - } - } - if !config.SkipRender { - ll := l.WithField("substep", "pages"). - WithField("site", s.language.Lang). - WithField("outputFormat", renderFormat.Name) - - start := time.Now() - - if config.PartialReRender { - if err := s.renderPages(siteRenderContext); err != nil { - return err - } - } else { - if err := s.render(siteRenderContext); err != nil { - return renderErr(err) - } - } - loggers.TimeTrackf(ll, start, nil, "") - } - } - return nil - }(); err != nil { - return err - } - - } - } - - return nil -} - -func (h *HugoSites) renderDeferred(l logg.LevelLogger) error { - l = l.WithField("step", "render deferred") - start := time.Now() - - var deferredCount int - - for rc, de := range h.Deps.BuildState.DeferredExecutionsGroupedByRenderingContext { - if de.FilenamesWithPostPrefix.Len() == 0 { - continue - } - - deferredCount += de.FilenamesWithPostPrefix.Len() - - s := rc.Site.(*Site) - for _, s2 := range h.Sites { - if err := s2.preparePagesForRender(s == s2, rc.SiteOutIdx); err != nil { - return err - } - } - if err := s.executeDeferredTemplates(de); err != nil { - return herrors.ImproveRenderErr(err) - } - } - - loggers.TimeTrackf(l, start, logg.Fields{ - logg.Field{Name: "count", Value: deferredCount}, - }, "") - - return nil -} - -func (s *Site) executeDeferredTemplates(de *deps.DeferredExecutions) error { - handleFile := func(filename string) error { - content, err := afero.ReadFile(s.BaseFs.PublishFs, filename) - if err != nil { - return err - } - - k := 0 - changed := false - - for { - if k >= len(content) { - break - } - l := bytes.Index(content[k:], []byte(tpl.HugoDeferredTemplatePrefix)) - if l == -1 { - break - } - m := bytes.Index(content[k+l:], []byte(tpl.HugoDeferredTemplateSuffix)) + len(tpl.HugoDeferredTemplateSuffix) - - low, high := k+l, k+l+m - - forward := l + m - id := string(content[low:high]) - - if err := func() error { - deferred, found := de.Executions.Get(id) - if !found { - panic(fmt.Sprintf("deferred execution with id %q not found", id)) - } - deferred.Mu.Lock() - defer deferred.Mu.Unlock() - - if !deferred.Executed { - tmpl := s.Deps.GetTemplateStore() - ti := s.TemplateStore.LookupByPath(deferred.TemplatePath) - if ti == nil { - panic(fmt.Sprintf("template %q not found", deferred.TemplatePath)) - } - - if err := func() error { - buf := bufferpool.GetBuffer() - defer bufferpool.PutBuffer(buf) - - err = tmpl.ExecuteWithContext(deferred.Ctx, ti, buf, deferred.Data) - if err != nil { - return err - } - deferred.Result = buf.String() - deferred.Executed = true - - return nil - }(); err != nil { - return err - } - } - - content = append(content[:low], append([]byte(deferred.Result), content[high:]...)...) - forward = len(deferred.Result) - changed = true - - return nil - }(); err != nil { - return err - } - - k += forward - } - - if changed { - return afero.WriteFile(s.BaseFs.PublishFs, filename, content, 0o666) - } - - return nil - } - - g := rungroup.Run[string](context.Background(), rungroup.Config[string]{ - NumWorkers: s.h.numWorkers, - Handle: func(ctx context.Context, filename string) error { - return handleFile(filename) - }, - }) - - de.FilenamesWithPostPrefix.ForEeach(func(filename string, _ bool) bool { - g.Enqueue(filename) - return true - }) - - return g.Wait() -} - -// printPathWarningsOnce prints path warnings if enabled. -func (h *HugoSites) printPathWarningsOnce() error { - h.printPathWarningsInit.Do(func() { - conf := h.Configs.Base - if conf.PrintPathWarnings { - // We need to do this before any post processing, as that may write to the same files twice - // and create false positives. - hugofs.WalkFilesystems(h.Fs.PublishDir, func(fs afero.Fs) bool { - if dfs, ok := fs.(hugofs.DuplicatesReporter); ok { - dupes := dfs.ReportDuplicates() - if dupes != "" { - h.Log.Warnln("Duplicate target paths:", dupes) - } - } - return false - }) - } - }) - return nil -} - -// / printUnusedTemplatesOnce prints unused templates if enabled. -func (h *HugoSites) printUnusedTemplatesOnce() error { - h.printUnusedTemplatesInit.Do(func() { - conf := h.Configs.Base - if conf.PrintUnusedTemplates { - unusedTemplates := h.GetTemplateStore().UnusedTemplates() - for _, unusedTemplate := range unusedTemplates { - if unusedTemplate.Fi != nil { - h.Log.Warnf("Template %s is unused, source %q", unusedTemplate.PathInfo.Path(), unusedTemplate.Fi.Meta().Filename) - } else { - h.Log.Warnf("Template %s is unused", unusedTemplate.PathInfo.Path()) - } - } - } - }) - return nil -} - -// postProcess runs the post processors, e.g. writing the hugo_stats.json file. -func (h *HugoSites) postProcess(l logg.LevelLogger) error { - l = l.WithField("step", "postProcess") - defer loggers.TimeTrackf(l, time.Now(), nil, "") - - // This will only be set when js.Build have been triggered with - // imports that resolves to the project or a module. - // Write a jsconfig.json file to the project's /asset directory - // to help JS IntelliSense in VS Code etc. - if !h.ResourceSpec.BuildConfig().NoJSConfigInAssets { - handleJSConfig := func(fi os.FileInfo) { - m := fi.(hugofs.FileMetaInfo).Meta() - if !m.IsProject { - return - } - - if jsConfig := h.ResourceSpec.JSConfigBuilder.Build(m.SourceRoot); jsConfig != nil { - b, err := json.MarshalIndent(jsConfig, "", " ") - if err != nil { - h.Log.Warnf("Failed to create jsconfig.json: %s", err) - } else { - filename := filepath.Join(m.SourceRoot, "jsconfig.json") - if h.Configs.Base.Internal.Running { - h.skipRebuildForFilenamesMu.Lock() - h.skipRebuildForFilenames[filename] = true - h.skipRebuildForFilenamesMu.Unlock() - } - // Make sure it's written to the OS fs as this is used by - // editors. - if err := afero.WriteFile(hugofs.Os, filename, b, 0o666); err != nil { - h.Log.Warnf("Failed to write jsconfig.json: %s", err) - } - } - } - } - - fi, err := h.BaseFs.Assets.Fs.Stat("") - if err != nil { - if !herrors.IsNotExist(err) { - h.Log.Warnf("Failed to resolve jsconfig.json dir: %s", err) - } - } else { - handleJSConfig(fi) - } - } - - var toPostProcess []postpub.PostPublishedResource - for _, r := range h.ResourceSpec.PostProcessResources { - toPostProcess = append(toPostProcess, r) - } - - if len(toPostProcess) == 0 { - // Nothing more to do. - return nil - } - - workers := para.New(config.GetNumWorkerMultiplier()) - g, _ := workers.Start(context.Background()) - - handleFile := func(filename string) error { - content, err := afero.ReadFile(h.BaseFs.PublishFs, filename) - if err != nil { - return err - } - - k := 0 - changed := false - - for { - l := bytes.Index(content[k:], []byte(postpub.PostProcessPrefix)) - if l == -1 { - break - } - m := bytes.Index(content[k+l:], []byte(postpub.PostProcessSuffix)) + len(postpub.PostProcessSuffix) - - low, high := k+l, k+l+m - - field := content[low:high] - - forward := l + m - - for i, r := range toPostProcess { - if r == nil { - panic(fmt.Sprintf("resource %d to post process is nil", i+1)) - } - v, ok := r.GetFieldString(string(field)) - if ok { - content = append(content[:low], append([]byte(v), content[high:]...)...) - changed = true - forward = len(v) - break - } - } - - k += forward - } - - if changed { - return afero.WriteFile(h.BaseFs.PublishFs, filename, content, 0o666) - } - - return nil - } - - filenames := h.Deps.BuildState.GetFilenamesWithPostPrefix() - for _, filename := range filenames { - filename := filename - g.Run(func() error { - return handleFile(filename) - }) - } - - // Prepare for a new build. - for _, s := range h.Sites { - s.ResourceSpec.PostProcessResources = make(map[string]postpub.PostPublishedResource) - } - - return g.Wait() -} - -func (h *HugoSites) writeBuildStats() error { - if h.ResourceSpec == nil { - panic("h.ResourceSpec is nil") - } - if !h.ResourceSpec.BuildConfig().BuildStats.Enabled() { - return nil - } - - htmlElements := &publisher.HTMLElements{} - for _, s := range h.Sites { - stats := s.publisher.PublishStats() - htmlElements.Merge(stats.HTMLElements) - } - - htmlElements.Sort() - - stats := publisher.PublishStats{ - HTMLElements: *htmlElements, - } - - var buf bytes.Buffer - enc := json.NewEncoder(&buf) - enc.SetEscapeHTML(false) - enc.SetIndent("", " ") - err := enc.Encode(stats) - if err != nil { - return err - } - js := buf.Bytes() - - filename := filepath.Join(h.Configs.LoadingInfo.BaseConfig.WorkingDir, files.FilenameHugoStatsJSON) - - if existingContent, err := afero.ReadFile(hugofs.Os, filename); err == nil { - // Check if the content has changed. - if bytes.Equal(existingContent, js) { - return nil - } - } - - // Make sure it's always written to the OS fs. - if err := afero.WriteFile(hugofs.Os, filename, js, 0o666); err != nil { - return err - } - - // Write to the destination as well if it's a in-memory fs. - if !hugofs.IsOsFs(h.Fs.Source) { - if err := afero.WriteFile(h.Fs.WorkingDirWritable, filename, js, 0o666); err != nil { - return err - } - } - - // This step may be followed by a post process step that may - // rebuild e.g. CSS, so clear any cache that's defined for the hugo_stats.json. - h.dynacacheGCFilenameIfNotWatchedAndDrainMatching(filename) - - return nil -} - -type pathChange struct { - // The path to the changed file. - p *paths.Path - - // If true, this is a structural change (e.g. a delete or a rename). - structural bool - - // If true, this is a directory. - isDir bool -} - -func (p pathChange) isStructuralChange() bool { - return p.structural || p.isDir -} - -func (h *HugoSites) processPartialRebuildChanges(ctx context.Context, l logg.LevelLogger, config *BuildCfg) error { - if err := h.resolveAndClearStateForIdentities(ctx, l, nil, config.WhatChanged.Drain()); err != nil { - return err - } - - if err := h.processContentAdaptersOnRebuild(ctx, config); err != nil { - return err - } - return nil -} - -// processPartialFileEvents prepares the Sites' sources for a partial rebuild. -func (h *HugoSites) processPartialFileEvents(ctx context.Context, l logg.LevelLogger, config *BuildCfg, init func(config *BuildCfg) error, events []fsnotify.Event) error { - h.Log.Trace(logg.StringFunc(func() string { - var sb strings.Builder - sb.WriteString("File events:\n") - for _, ev := range events { - sb.WriteString(ev.String()) - sb.WriteString("\n") - } - return sb.String() - })) - - // For a list of events for the different OSes, see the test output in https://github.com/bep/fsnotifyeventlister/. - events = h.fileEventsFilter(events) - events = h.fileEventsTrim(events) - eventInfos := h.fileEventsApplyInfo(events) - - logger := h.Log - - var ( - tmplAdded bool - tmplChanged bool - i18nChanged bool - needsPagesAssemble bool - ) - - changedPaths := struct { - changedFiles []*paths.Path - changedDirs []*paths.Path - deleted []*paths.Path - }{} - - removeDuplicatePaths := func(ps []*paths.Path) []*paths.Path { - seen := make(map[string]bool) - var filtered []*paths.Path - for _, p := range ps { - if !seen[p.Path()] { - seen[p.Path()] = true - filtered = append(filtered, p) - } - } - return filtered - } - - var ( - cacheBusters []func(string) bool - deletedDirs []string - addedContentPaths []*paths.Path - ) - - var ( - addedOrChangedContent []pathChange - changes []identity.Identity - ) - - for _, ev := range eventInfos { - cpss := h.BaseFs.ResolvePaths(ev.Name) - pss := make([]*paths.Path, len(cpss)) - for i, cps := range cpss { - p := cps.Path - if ev.removed && !paths.HasExt(p) { - // Assume this is a renamed/removed directory. - // For deletes, we walk up the tree to find the container (e.g. branch bundle), - // so we will catch this even if it is a file without extension. - // This avoids us walking up to the home page bundle for the common case - // of renaming root sections. - p = p + "/_index.md" - deletedDirs = append(deletedDirs, cps.Path) - } - - pss[i] = h.Configs.ContentPathParser.Parse(cps.Component, p) - if ev.added && !ev.isChangedDir && cps.Component == files.ComponentFolderContent { - addedContentPaths = append(addedContentPaths, pss[i]) - } - - // Compile cache buster. - np := glob.NormalizePath(path.Join(cps.Component, cps.Path)) - g, err := h.ResourceSpec.BuildConfig().MatchCacheBuster(h.Log, np) - if err == nil && g != nil { - cacheBusters = append(cacheBusters, g) - } - - if ev.added { - changes = append(changes, identity.StructuralChangeAdd) - } - if ev.removed { - changes = append(changes, identity.StructuralChangeRemove) - } - } - - if ev.removed { - changedPaths.deleted = append(changedPaths.deleted, pss...) - } else if ev.isChangedDir { - changedPaths.changedDirs = append(changedPaths.changedDirs, pss...) - } else { - changedPaths.changedFiles = append(changedPaths.changedFiles, pss...) - } - } - - // Find the most specific identity possible. - handleChange := func(pathInfo *paths.Path, delete, isDir bool) { - switch pathInfo.Component() { - case files.ComponentFolderContent: - logger.Println("Source changed", pathInfo.Path()) - isContentDataFile := pathInfo.IsContentData() - if !isContentDataFile { - if ids := h.pageTrees.collectAndMarkStaleIdentities(pathInfo); len(ids) > 0 { - changes = append(changes, ids...) - } - } else { - h.pageTrees.treePagesFromTemplateAdapters.DeleteAllFunc(pathInfo.Base(), - func(s string, n *pagesfromdata.PagesFromTemplate) bool { - changes = append(changes, n.DependencyManager) - - // Try to open the file to see if has been deleted. - f, err := n.GoTmplFi.Meta().Open() - if err == nil { - f.Close() - } - if err != nil { - // Remove all pages and resources below. - prefix := pathInfo.Base() + "/" - h.pageTrees.treePages.DeletePrefixAll(prefix) - h.pageTrees.resourceTrees.DeletePrefixAll(prefix) - changes = append(changes, identity.NewGlobIdentity(prefix+"*")) - } - return err != nil - }) - } - - needsPagesAssemble = true - - if config.RecentlyTouched != nil { - // Fast render mode. Adding them to the visited queue - // avoids rerendering them on navigation. - for _, id := range changes { - if p, ok := id.(page.Page); ok { - config.RecentlyTouched.Add(p.RelPermalink()) - } - } - } - - h.pageTrees.treeTaxonomyEntries.DeletePrefix("") - - if delete && !isContentDataFile { - _, ok := h.pageTrees.treePages.LongestPrefixAll(pathInfo.Base()) - if ok { - h.pageTrees.treePages.DeleteAll(pathInfo.Base()) - h.pageTrees.resourceTrees.DeleteAll(pathInfo.Base()) - if pathInfo.IsBundle() { - // Assume directory removed. - h.pageTrees.treePages.DeletePrefixAll(pathInfo.Base() + "/") - h.pageTrees.resourceTrees.DeletePrefixAll(pathInfo.Base() + "/") - } - } else { - h.pageTrees.resourceTrees.DeleteAll(pathInfo.Base()) - } - } - - addedOrChangedContent = append(addedOrChangedContent, pathChange{p: pathInfo, structural: delete, isDir: isDir}) - - case files.ComponentFolderLayouts: - tmplChanged = true - templatePath := pathInfo.Unnormalized().TrimLeadingSlash().PathNoLang() - if !h.GetTemplateStore().HasTemplate(templatePath) { - tmplAdded = true - } - - if tmplAdded { - logger.Println("Template added", pathInfo.Path()) - // A new template may require a more coarse grained build. - base := pathInfo.Base() - if strings.Contains(base, "_markup") { - // It's hard to determine the exact change set of this, - // so be very coarse grained. - changes = append(changes, identity.GenghisKhan) - } - if strings.Contains(base, "shortcodes") { - changes = append(changes, identity.NewGlobIdentity(fmt.Sprintf("shortcodes/%s*", pathInfo.BaseNameNoIdentifier()))) - } else { - changes = append(changes, pathInfo) - } - } else { - logger.Println("Template changed", pathInfo.Path()) - id := h.GetTemplateStore().GetIdentity(pathInfo.Path()) - if id != nil { - changes = append(changes, id) - } else { - changes = append(changes, pathInfo) - } - } - case files.ComponentFolderAssets: - logger.Println("Asset changed", pathInfo.Path()) - changes = append(changes, pathInfo) - case files.ComponentFolderData: - logger.Println("Data changed", pathInfo.Path()) - - // This should cover all usage of site.Data. - // Currently very coarse grained. - changes = append(changes, siteidentities.Data) - h.init.data.Reset() - case files.ComponentFolderI18n: - logger.Println("i18n changed", pathInfo.Path()) - i18nChanged = true - // It's hard to determine the exact change set of this, - // so be very coarse grained for now. - changes = append(changes, identity.GenghisKhan) - case files.ComponentFolderArchetypes: - // Ignore for now. - default: - panic(fmt.Sprintf("unknown component: %q", pathInfo.Component())) - } - } - - changedPaths.deleted = removeDuplicatePaths(changedPaths.deleted) - changedPaths.changedFiles = removeDuplicatePaths(changedPaths.changedFiles) - - h.Log.Trace(logg.StringFunc(func() string { - var sb strings.Builder - sb.WriteString("Resolved paths:\n") - sb.WriteString("Deleted:\n") - for _, p := range changedPaths.deleted { - sb.WriteString("path: " + p.Path()) - sb.WriteString("\n") - } - sb.WriteString("Changed:\n") - for _, p := range changedPaths.changedFiles { - sb.WriteString("path: " + p.Path()) - sb.WriteString("\n") - } - return sb.String() - })) - - for _, deletedDir := range deletedDirs { - prefix := deletedDir + "/" - predicate := func(id identity.Identity) bool { - // This will effectively reset all pages below this dir. - return strings.HasPrefix(paths.AddLeadingSlash(id.IdentifierBase()), prefix) - } - // Test in both directions. - changes = append(changes, identity.NewPredicateIdentity( - // Is dependent. - predicate, - // Is dependency. - predicate, - ), - ) - } - - if len(addedContentPaths) > 0 { - // These content files are new and not in use anywhere. - // To make sure that these gets listed in any site.RegularPages ranges or similar - // we could invalidate everything, but first try to collect a sample set - // from the surrounding pages. - var surroundingIDs []identity.Identity - for _, p := range addedContentPaths { - if ids := h.pageTrees.collectIdentitiesSurrounding(p.Base(), 10); len(ids) > 0 { - surroundingIDs = append(surroundingIDs, ids...) - } - } - - if len(surroundingIDs) > 0 { - changes = append(changes, surroundingIDs...) - } else { - // No surrounding pages found, so invalidate everything. - changes = append(changes, identity.GenghisKhan) - } - } - - for _, deleted := range changedPaths.deleted { - handleChange(deleted, true, false) - } - - for _, id := range changedPaths.changedFiles { - handleChange(id, false, false) - } - - for _, id := range changedPaths.changedDirs { - handleChange(id, false, true) - } - - for _, id := range changes { - if id == identity.GenghisKhan { - for i, cp := range addedOrChangedContent { - cp.structural = true - addedOrChangedContent[i] = cp - } - break - } - } - - resourceFiles := h.fileEventsContentPaths(addedOrChangedContent) - - changed := &WhatChanged{ - needsPagesAssembly: needsPagesAssemble, - } - changed.Add(changes...) - - config.WhatChanged = changed - - if err := init(config); err != nil { - return err - } - - var cacheBusterOr func(string) bool - if len(cacheBusters) > 0 { - cacheBusterOr = func(s string) bool { - for _, cb := range cacheBusters { - if cb(s) { - return true - } - } - return false - } - } - - changes2 := changed.Changes() - h.Deps.OnChangeListeners.Notify(changes2...) - - if err := h.resolveAndClearStateForIdentities(ctx, l, cacheBusterOr, changed.Drain()); err != nil { - return err - } - - if tmplChanged { - if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) { - depsFinder := identity.NewFinder(identity.FinderConfig{}) - ll := l.WithField("substep", "rebuild templates") - s := h.Sites[0] - if err := s.Deps.TemplateStore.RefreshFiles(func(fi hugofs.FileMetaInfo) bool { - pi := fi.Meta().PathInfo - for _, id := range changes2 { - if depsFinder.Contains(pi, id, -1) > 0 { - return true - } - } - return false - }); err != nil { - return ll, err - } - - return ll, nil - }); err != nil { - return err - } - } - - if i18nChanged { - if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) { - ll := l.WithField("substep", "rebuild i18n") - var prototype *deps.Deps - for i, s := range h.Sites { - if err := s.Deps.Compile(prototype); err != nil { - return ll, err - } - if i == 0 { - prototype = s.Deps - } - } - return ll, nil - }); err != nil { - return err - } - } - - if resourceFiles != nil { - if err := h.processFiles(ctx, l, config, resourceFiles...); err != nil { - return err - } - } - - if h.isRebuild() { - if err := h.processContentAdaptersOnRebuild(ctx, config); err != nil { - return err - } - } - - return nil -} - -func (h *HugoSites) LogServerAddresses() { - if h.hugoInfo.IsMultihost() { +func (h *HugoSites) assemble(config *BuildCfg) error { + if config.whatChanged.source { for _, s := range h.Sites { - h.Log.Printf("Web Server is available at %s (bind address %s) %s\n", s.conf.C.BaseURL, s.conf.C.ServerInterface, s.Language().Lang) - } - } else { - s := h.Sites[0] - h.Log.Printf("Web Server is available at %s (bind address %s)\n", s.conf.C.BaseURL, s.conf.C.ServerInterface) - } -} - -func (h *HugoSites) processFull(ctx context.Context, l logg.LevelLogger, config *BuildCfg) (err error) { - if err = h.processFiles(ctx, l, config); err != nil { - err = fmt.Errorf("readAndProcessContent: %w", err) - return - } - return err -} - -func (s *Site) handleContentAdapterChanges(bi pagesfromdata.BuildInfo, buildConfig *BuildCfg) { - if !s.h.isRebuild() { - return - } - - if len(bi.ChangedIdentities) > 0 { - buildConfig.WhatChanged.Add(bi.ChangedIdentities...) - buildConfig.WhatChanged.needsPagesAssembly = true - } - - for _, p := range bi.DeletedPaths { - pp := path.Join(bi.Path.Base(), p) - if v, ok := s.pageMap.treePages.Delete(pp); ok { - buildConfig.WhatChanged.Add(v.GetIdentity()) + s.createTaxonomiesEntries() } } -} -func (h *HugoSites) processContentAdaptersOnRebuild(ctx context.Context, buildConfig *BuildCfg) error { - g := rungroup.Run[*pagesfromdata.PagesFromTemplate](ctx, rungroup.Config[*pagesfromdata.PagesFromTemplate]{ - NumWorkers: h.numWorkers, - Handle: func(ctx context.Context, p *pagesfromdata.PagesFromTemplate) error { - bi, err := p.Execute(ctx) - if err != nil { + // TODO(bep) we could probably wait and do this in one go later + h.setupTranslations() + + if len(h.Sites) > 1 { + // The first is initialized during process; initialize the rest + for _, site := range h.Sites[1:] { + site.initializeSiteInfo() + } + } + + if config.whatChanged.source { + h.assembleGitInfo() + + for _, s := range h.Sites { + if err := s.buildSiteMeta(); err != nil { return err } - s := p.Site.(*Site) - s.handleContentAdapterChanges(bi, buildConfig) - return nil - }, - }) - - h.pageTrees.treePagesFromTemplateAdapters.WalkPrefixRaw(doctree.LockTypeRead, "", func(key string, p *pagesfromdata.PagesFromTemplate) (bool, error) { - if p.StaleVersion() > 0 { - g.Enqueue(p) } - return false, nil - }) - - return g.Wait() -} - -func (s *HugoSites) processFiles(ctx context.Context, l logg.LevelLogger, buildConfig *BuildCfg, filenames ...pathChange) error { - if s.Deps == nil { - panic("nil deps on site") } - sourceSpec := source.NewSourceSpec(s.PathSpec, buildConfig.ContentInclusionFilter, s.BaseFs.Content.Fs) + if err := h.createMissingPages(); err != nil { + return err + } - // For inserts, we can pick an arbitrary pageMap. - pageMap := s.Sites[0].pageMap + for _, s := range h.Sites { + s.siteStats = &siteStats{} + for _, p := range s.Pages { + // May have been set in front matter + if len(p.outputFormats) == 0 { + p.outputFormats = s.outputFormats[p.Kind] + } - c := newPagesCollector(ctx, s.h, sourceSpec, s.Log, l, pageMap, buildConfig, filenames) + cnt := len(p.outputFormats) + if p.Kind == KindPage { + s.siteStats.pageCountRegular += cnt + } + s.siteStats.pageCount += cnt - if err := c.Collect(); err != nil { + if err := p.initTargetPathDescriptor(); err != nil { + return err + } + if err := p.initURLs(); err != nil { + return err + } + } + s.assembleMenus() + s.refreshPageCaches() + s.setupSitePages() + } + + if err := h.assignMissingTranslations(); err != nil { return err } + return nil + +} + +func (h *HugoSites) render(config *BuildCfg) error { + + for _, s := range h.Sites { + s.initRenderFormats() + for i, rf := range s.renderFormats { + s.rc = &siteRenderingContext{Format: rf} + s.preparePagesForRender(config) + + if !config.SkipRender { + if err := s.render(i); err != nil { + return err + } + } + } + + if !config.SkipRender && config.PrintStats { + s.Stats() + } + } + + if !config.SkipRender { + if err := h.renderCrossSitesArtifacts(); err != nil { + return err + } + } + return nil } diff --git a/hugolib/hugo_sites_build_errors_test.go b/hugolib/hugo_sites_build_errors_test.go deleted file mode 100644 index 1298d7f4f..000000000 --- a/hugolib/hugo_sites_build_errors_test.go +++ /dev/null @@ -1,644 +0,0 @@ -package hugolib - -import ( - "fmt" - "os" - "path/filepath" - "strings" - "testing" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/common/herrors" -) - -type testSiteBuildErrorAsserter struct { - name string - c *qt.C -} - -func (t testSiteBuildErrorAsserter) getFileError(err error) herrors.FileError { - t.c.Assert(err, qt.Not(qt.IsNil), qt.Commentf(t.name)) - fe := herrors.UnwrapFileError(err) - t.c.Assert(fe, qt.Not(qt.IsNil)) - return fe -} - -func (t testSiteBuildErrorAsserter) assertLineNumber(lineNumber int, err error) { - t.c.Helper() - fe := t.getFileError(err) - t.c.Assert(fe.Position().LineNumber, qt.Equals, lineNumber, qt.Commentf(err.Error())) -} - -func (t testSiteBuildErrorAsserter) assertErrorMessage(e1, e2 string) { - // The error message will contain filenames with OS slashes. Normalize before compare. - e1, e2 = filepath.ToSlash(e1), filepath.ToSlash(e2) - t.c.Assert(e2, qt.Contains, e1) -} - -func TestSiteBuildErrors(t *testing.T) { - const ( - yamlcontent = "yamlcontent" - tomlcontent = "tomlcontent" - jsoncontent = "jsoncontent" - shortcode = "shortcode" - base = "base" - single = "single" - ) - - // TODO(bep) add content tests after https://github.com/gohugoio/hugo/issues/5324 - // is implemented. - - tests := []struct { - name string - fileType string - fileFixer func(content string) string - assertCreateError func(a testSiteBuildErrorAsserter, err error) - assertBuildError func(a testSiteBuildErrorAsserter, err error) - }{ - { - name: "Base template parse failed", - fileType: base, - fileFixer: func(content string) string { - return strings.Replace(content, ".Title }}", ".Title }", 1) - }, - // Base templates gets parsed at build time. - assertBuildError: func(a testSiteBuildErrorAsserter, err error) { - a.assertLineNumber(4, err) - }, - }, - { - name: "Base template execute failed", - fileType: base, - fileFixer: func(content string) string { - return strings.Replace(content, ".Title", ".Titles", 1) - }, - assertBuildError: func(a testSiteBuildErrorAsserter, err error) { - a.assertLineNumber(4, err) - }, - }, - { - name: "Single template parse failed", - fileType: single, - fileFixer: func(content string) string { - return strings.Replace(content, ".Title }}", ".Title }", 1) - }, - assertCreateError: func(a testSiteBuildErrorAsserter, err error) { - fe := a.getFileError(err) - a.c.Assert(fe.Position().LineNumber, qt.Equals, 5) - a.c.Assert(fe.Position().ColumnNumber, qt.Equals, 1) - a.assertErrorMessage("\"layouts/foo/single.html:5:1\": parse failed: template: foo/single.html:5: unexpected \"}\" in operand", fe.Error()) - }, - }, - { - name: "Single template execute failed", - fileType: single, - fileFixer: func(content string) string { - return strings.Replace(content, ".Title", ".Titles", 1) - }, - assertBuildError: func(a testSiteBuildErrorAsserter, err error) { - fe := a.getFileError(err) - a.c.Assert(fe.Position().LineNumber, qt.Equals, 5) - a.c.Assert(fe.Position().ColumnNumber, qt.Equals, 14) - a.assertErrorMessage("\"layouts/_default/single.html:5:14\": execute of template failed", fe.Error()) - }, - }, - { - name: "Single template execute failed, long keyword", - fileType: single, - fileFixer: func(content string) string { - return strings.Replace(content, ".Title", ".ThisIsAVeryLongTitle", 1) - }, - assertBuildError: func(a testSiteBuildErrorAsserter, err error) { - fe := a.getFileError(err) - a.c.Assert(fe.Position().LineNumber, qt.Equals, 5) - a.c.Assert(fe.Position().ColumnNumber, qt.Equals, 14) - a.assertErrorMessage("\"layouts/_default/single.html:5:14\": execute of template failed", fe.Error()) - }, - }, - { - name: "Shortcode parse failed", - fileType: shortcode, - fileFixer: func(content string) string { - return strings.Replace(content, ".Title }}", ".Title }", 1) - }, - assertCreateError: func(a testSiteBuildErrorAsserter, err error) { - a.assertLineNumber(4, err) - }, - }, - { - name: "Shortcode execute failed", - fileType: shortcode, - fileFixer: func(content string) string { - return strings.Replace(content, ".Title", ".Titles", 1) - }, - assertBuildError: func(a testSiteBuildErrorAsserter, err error) { - fe := a.getFileError(err) - // Make sure that it contains both the content file and template - a.assertErrorMessage(`"content/myyaml.md:7:10": failed to render shortcode "sc": failed to process shortcode: "layouts/shortcodes/sc.html:4:22": execute of template failed: template: shortcodes/sc.html:4:22: executing "shortcodes/sc.html" at <.Page.Titles>: can't evaluate field Titles in type page.Page`, fe.Error()) - a.c.Assert(fe.Position().LineNumber, qt.Equals, 7) - }, - }, - { - name: "Shortode does not exist", - fileType: yamlcontent, - fileFixer: func(content string) string { - return strings.Replace(content, "{{< sc >}}", "{{< nono >}}", 1) - }, - assertBuildError: func(a testSiteBuildErrorAsserter, err error) { - fe := a.getFileError(err) - a.c.Assert(fe.Position().LineNumber, qt.Equals, 7) - a.c.Assert(fe.Position().ColumnNumber, qt.Equals, 10) - a.assertErrorMessage(`"content/myyaml.md:7:10": failed to extract shortcode: template for shortcode "nono" not found`, fe.Error()) - }, - }, - { - name: "Invalid YAML front matter", - fileType: yamlcontent, - fileFixer: func(content string) string { - return `--- -title: "My YAML Content" -foo bar ---- -` - }, - assertBuildError: func(a testSiteBuildErrorAsserter, err error) { - a.assertLineNumber(3, err) - }, - }, - { - name: "Invalid TOML front matter", - fileType: tomlcontent, - fileFixer: func(content string) string { - return strings.Replace(content, "description = ", "description &", 1) - }, - assertBuildError: func(a testSiteBuildErrorAsserter, err error) { - fe := a.getFileError(err) - a.c.Assert(fe.Position().LineNumber, qt.Equals, 6) - }, - }, - { - name: "Invalid JSON front matter", - fileType: jsoncontent, - fileFixer: func(content string) string { - return strings.Replace(content, "\"description\":", "\"description\"", 1) - }, - assertBuildError: func(a testSiteBuildErrorAsserter, err error) { - fe := a.getFileError(err) - a.c.Assert(fe.Position().LineNumber, qt.Equals, 3) - }, - }, - { - // See https://github.com/gohugoio/hugo/issues/5327 - name: "Panic in template Execute", - fileType: single, - fileFixer: func(content string) string { - return strings.Replace(content, ".Title", ".Parent.Parent.Parent", 1) - }, - - assertBuildError: func(a testSiteBuildErrorAsserter, err error) { - a.c.Assert(err, qt.Not(qt.IsNil)) - fe := a.getFileError(err) - a.c.Assert(fe.Position().LineNumber, qt.Equals, 5) - a.c.Assert(fe.Position().ColumnNumber, qt.Equals, 21) - }, - }, - } - - for _, test := range tests { - if test.name != "Invalid JSON front matter" { - continue - } - test := test - t.Run(test.name, func(t *testing.T) { - t.Parallel() - c := qt.New(t) - errorAsserter := testSiteBuildErrorAsserter{ - c: c, - name: test.name, - } - - b := newTestSitesBuilder(t).WithSimpleConfigFile() - - f := func(fileType, content string) string { - if fileType != test.fileType { - return content - } - return test.fileFixer(content) - } - - b.WithTemplatesAdded("layouts/shortcodes/sc.html", f(shortcode, `SHORTCODE L1 -SHORTCODE L2 -SHORTCODE L3: -SHORTCODE L4: {{ .Page.Title }} -`)) - b.WithTemplatesAdded("layouts/_default/baseof.html", f(base, `BASEOF L1 -BASEOF L2 -BASEOF L3 -BASEOF L4{{ if .Title }}{{ end }} -{{block "main" .}}This is the main content.{{end}} -BASEOF L6 -`)) - - b.WithTemplatesAdded("layouts/_default/single.html", f(single, `{{ define "main" }} -SINGLE L2: -SINGLE L3: -SINGLE L4: -SINGLE L5: {{ .Title }} {{ .Content }} -{{ end }} -`)) - - b.WithTemplatesAdded("layouts/foo/single.html", f(single, ` -SINGLE L2: -SINGLE L3: -SINGLE L4: -SINGLE L5: {{ .Title }} {{ .Content }} -`)) - - b.WithContent("myyaml.md", f(yamlcontent, `--- -title: "The YAML" ---- - -Some content. - - {{< sc >}} - -Some more text. - -The end. - -`)) - - b.WithContent("mytoml.md", f(tomlcontent, `+++ -title = "The TOML" -p1 = "v" -p2 = "v" -p3 = "v" -description = "Descriptioon" -+++ - -Some content. - - -`)) - - b.WithContent("myjson.md", f(jsoncontent, `{ - "title": "This is a title", - "description": "This is a description." -} - -Some content. - - -`)) - - createErr := b.CreateSitesE() - if test.assertCreateError != nil { - test.assertCreateError(errorAsserter, createErr) - } else { - c.Assert(createErr, qt.IsNil) - } - - if createErr == nil { - buildErr := b.BuildE(BuildCfg{}) - if test.assertBuildError != nil { - test.assertBuildError(errorAsserter, buildErr) - } else { - c.Assert(buildErr, qt.IsNil) - } - } - }) - } -} - -// Issue 9852 -func TestErrorMinify(t *testing.T) { - t.Parallel() - - files := ` --- config.toml -- -minify = true - --- layouts/index.html -- - - - - -` - - b, err := NewIntegrationTestBuilder( - IntegrationTestConfig{ - T: t, - TxtarString: files, - }, - ).BuildE() - - b.Assert(err, qt.IsNotNil) - fe := herrors.UnwrapFileError(err) - b.Assert(fe, qt.IsNotNil) - b.Assert(fe.Position().LineNumber, qt.Equals, 2) - b.Assert(fe.Position().ColumnNumber, qt.Equals, 9) - b.Assert(fe.Error(), qt.Contains, "unexpected = in expression on line 2 and column 9") - b.Assert(filepath.ToSlash(fe.Position().Filename), qt.Contains, "hugo-transform-error") - b.Assert(os.Remove(fe.Position().Filename), qt.IsNil) -} - -func TestErrorNestedRender(t *testing.T) { - t.Parallel() - - files := ` --- config.toml -- --- content/_index.md -- ---- -title: "Home" ---- --- layouts/index.html -- -line 1 -line 2 -1{{ .Render "myview" }} --- layouts/_default/myview.html -- -line 1 -12{{ partial "foo.html" . }} -line 4 -line 5 --- layouts/partials/foo.html -- -line 1 -line 2 -123{{ .ThisDoesNotExist }} -line 4 -` - - b, err := NewIntegrationTestBuilder( - IntegrationTestConfig{ - T: t, - TxtarString: files, - }, - ).BuildE() - - b.Assert(err, qt.IsNotNil) - errors := herrors.UnwrapFileErrorsWithErrorContext(err) - b.Assert(errors, qt.HasLen, 4) - b.Assert(errors[0].Position().LineNumber, qt.Equals, 3) - b.Assert(errors[0].Position().ColumnNumber, qt.Equals, 4) - b.Assert(errors[0].Error(), qt.Contains, filepath.FromSlash(`"/layouts/index.html:3:4": execute of template failed`)) - b.Assert(errors[0].ErrorContext().Lines, qt.DeepEquals, []string{"line 1", "line 2", "1{{ .Render \"myview\" }}"}) - b.Assert(errors[2].Position().LineNumber, qt.Equals, 2) - b.Assert(errors[2].Position().ColumnNumber, qt.Equals, 5) - b.Assert(errors[2].ErrorContext().Lines, qt.DeepEquals, []string{"line 1", "12{{ partial \"foo.html\" . }}", "line 4", "line 5"}) - - b.Assert(errors[3].Position().LineNumber, qt.Equals, 3) - b.Assert(errors[3].Position().ColumnNumber, qt.Equals, 6) - b.Assert(errors[3].ErrorContext().Lines, qt.DeepEquals, []string{"line 1", "line 2", "123{{ .ThisDoesNotExist }}", "line 4"}) -} - -func TestErrorNestedShortcode(t *testing.T) { - t.Parallel() - - files := ` --- config.toml -- --- content/_index.md -- ---- -title: "Home" ---- - -## Hello -{{< hello >}} - --- layouts/index.html -- -line 1 -line 2 -{{ .Content }} -line 5 --- layouts/shortcodes/hello.html -- -line 1 -12{{ partial "foo.html" . }} -line 4 -line 5 --- layouts/partials/foo.html -- -line 1 -line 2 -123{{ .ThisDoesNotExist }} -line 4 -` - - b, err := NewIntegrationTestBuilder( - IntegrationTestConfig{ - T: t, - TxtarString: files, - }, - ).BuildE() - - b.Assert(err, qt.IsNotNil) - errors := herrors.UnwrapFileErrorsWithErrorContext(err) - - b.Assert(errors, qt.HasLen, 4) - - b.Assert(errors[1].Position().LineNumber, qt.Equals, 6) - b.Assert(errors[1].Position().ColumnNumber, qt.Equals, 1) - b.Assert(errors[1].ErrorContext().ChromaLexer, qt.Equals, "md") - b.Assert(errors[1].Error(), qt.Contains, filepath.FromSlash(`"/content/_index.md:6:1": failed to render shortcode "hello": failed to process shortcode: "/layouts/shortcodes/hello.html:2:5":`)) - b.Assert(errors[1].ErrorContext().Lines, qt.DeepEquals, []string{"", "## Hello", "{{< hello >}}", ""}) - b.Assert(errors[2].ErrorContext().Lines, qt.DeepEquals, []string{"line 1", "12{{ partial \"foo.html\" . }}", "line 4", "line 5"}) - b.Assert(errors[3].ErrorContext().Lines, qt.DeepEquals, []string{"line 1", "line 2", "123{{ .ThisDoesNotExist }}", "line 4"}) -} - -func TestErrorRenderHookHeading(t *testing.T) { - t.Parallel() - - files := ` --- config.toml -- --- content/_index.md -- ---- -title: "Home" ---- - -## Hello - --- layouts/index.html -- -line 1 -line 2 -{{ .Content }} -line 5 --- layouts/_default/_markup/render-heading.html -- -line 1 -12{{ .Levels }} -line 4 -line 5 -` - - b, err := NewIntegrationTestBuilder( - IntegrationTestConfig{ - T: t, - TxtarString: files, - }, - ).BuildE() - - b.Assert(err, qt.IsNotNil) - errors := herrors.UnwrapFileErrorsWithErrorContext(err) - - b.Assert(errors, qt.HasLen, 3) - b.Assert(errors[0].Error(), qt.Contains, filepath.FromSlash(`"/content/_index.md:1:1": "/layouts/_default/_markup/render-heading.html:2:5": execute of template failed`)) -} - -func TestErrorRenderHookCodeblock(t *testing.T) { - t.Parallel() - - files := ` --- config.toml -- --- content/_index.md -- ---- -title: "Home" ---- - -## Hello - -§§§ foo -bar -§§§ - - --- layouts/index.html -- -line 1 -line 2 -{{ .Content }} -line 5 --- layouts/_default/_markup/render-codeblock-foo.html -- -line 1 -12{{ .Foo }} -line 4 -line 5 -` - - b, err := NewIntegrationTestBuilder( - IntegrationTestConfig{ - T: t, - TxtarString: files, - }, - ).BuildE() - - b.Assert(err, qt.IsNotNil) - errors := herrors.UnwrapFileErrorsWithErrorContext(err) - - b.Assert(errors, qt.HasLen, 3) - first := errors[0] - b.Assert(first.Error(), qt.Contains, filepath.FromSlash(`"/content/_index.md:7:1": "/layouts/_default/_markup/render-codeblock-foo.html:2:5": execute of template failed`)) -} - -func TestErrorInBaseTemplate(t *testing.T) { - t.Parallel() - - filesTemplate := ` --- config.toml -- --- content/_index.md -- ---- -title: "Home" ---- --- layouts/baseof.html -- -line 1 base -line 2 base -{{ block "main" . }}empty{{ end }} -line 4 base -{{ block "toc" . }}empty{{ end }} --- layouts/index.html -- -{{ define "main" }} -line 2 index -line 3 index -line 4 index -{{ end }} -{{ define "toc" }} -TOC: {{ partial "toc.html" . }} -{{ end }} --- layouts/partials/toc.html -- -toc line 1 -toc line 2 -toc line 3 -toc line 4 - - -` - - t.Run("base template", func(t *testing.T) { - files := strings.Replace(filesTemplate, "line 4 base", "123{{ .ThisDoesNotExist \"abc\" }}", 1) - - b, err := NewIntegrationTestBuilder( - IntegrationTestConfig{ - T: t, - TxtarString: files, - }, - ).BuildE() - - b.Assert(err, qt.IsNotNil) - b.Assert(err.Error(), qt.Contains, `baseof.html:4:6`) - }) - - t.Run("index template", func(t *testing.T) { - files := strings.Replace(filesTemplate, "line 3 index", "1234{{ .ThisDoesNotExist \"abc\" }}", 1) - - b, err := NewIntegrationTestBuilder( - IntegrationTestConfig{ - T: t, - TxtarString: files, - }, - ).BuildE() - - b.Assert(err, qt.IsNotNil) - b.Assert(err.Error(), qt.Contains, `index.html:3:7"`) - }) - - t.Run("partial from define", func(t *testing.T) { - files := strings.Replace(filesTemplate, "toc line 2", "12345{{ .ThisDoesNotExist \"abc\" }}", 1) - - b, err := NewIntegrationTestBuilder( - IntegrationTestConfig{ - T: t, - TxtarString: files, - }, - ).BuildE() - - b.Assert(err, qt.IsNotNil) - b.Assert(err.Error(), qt.Contains, `toc.html:2:8"`) - }) -} - -// https://github.com/gohugoio/hugo/issues/5375 -func TestSiteBuildTimeout(t *testing.T) { - b := newTestSitesBuilder(t) - b.WithConfigFile("toml", ` -timeout = 5 -`) - - b.WithTemplatesAdded("_default/single.html", ` -{{ .WordCount }} -`, "shortcodes/c.html", ` -{{ range .Page.Site.RegularPages }} -{{ .WordCount }} -{{ end }} - -`) - - for i := 1; i < 100; i++ { - b.WithContent(fmt.Sprintf("page%d.md", i), `--- -title: "A page" ---- - -{{< c >}}`) - } - - b.CreateSites().BuildFail(BuildCfg{}) -} - -func TestErrorTemplateRuntime(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- --- layouts/index.html -- -Home. -{{ .ThisDoesNotExist }} - ` - - b, err := TestE(t, files) - - b.Assert(err, qt.Not(qt.IsNil)) - b.Assert(err.Error(), qt.Contains, filepath.FromSlash(`/layouts/index.html:2:3`)) - b.Assert(err.Error(), qt.Contains, `can't evaluate field ThisDoesNotExist`) -} diff --git a/hugolib/hugo_sites_build_test.go b/hugolib/hugo_sites_build_test.go index 4c2bf452c..96e2c66b2 100644 --- a/hugolib/hugo_sites_build_test.go +++ b/hugolib/hugo_sites_build_test.go @@ -1,55 +1,149 @@ package hugolib import ( + "bytes" "fmt" - "path/filepath" "strings" "testing" - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/common/loggers" - "github.com/gohugoio/hugo/htesting" - "github.com/gohugoio/hugo/resources/kinds" + "html/template" + "os" + "path/filepath" + "time" + "github.com/fortytw2/leaktest" + "github.com/fsnotify/fsnotify" + "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/hugofs" + "github.com/gohugoio/hugo/source" "github.com/spf13/afero" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" ) -func TestMultiSitesMainLangInRoot(t *testing.T) { - files := ` --- hugo.toml -- -defaultContentLanguage = "fr" -defaultContentLanguageInSubdir = false -disableKinds = ["taxonomy", "term"] -[languages] -[languages.en] -weight = 1 -[languages.fr] -weight = 2 --- content/sect/doc1.en.md -- ---- -title: doc1 en ---- --- content/sect/doc1.fr.md -- ---- -title: doc1 fr -slug: doc1-fr ---- --- layouts/_default/single.html -- -Single: {{ .Title }}|{{ .Lang }}|{{ .RelPermalink }}| +type testSiteConfig struct { + DefaultContentLanguage string + DefaultContentLanguageInSubdir bool + Fs afero.Fs +} -` - b := Test(t, files) - b.AssertFileContent("public/sect/doc1-fr/index.html", "Single: doc1 fr|fr|/sect/doc1-fr/|") - b.AssertFileContent("public/en/sect/doc1/index.html", "Single: doc1 en|en|/en/sect/doc1/|") +func TestMultiSitesMainLangInRoot(t *testing.T) { + t.Parallel() + for _, b := range []bool{true, false} { + doTestMultiSitesMainLangInRoot(t, b) + } +} + +func doTestMultiSitesMainLangInRoot(t *testing.T, defaultInSubDir bool) { + + siteConfig := testSiteConfig{Fs: afero.NewMemMapFs(), DefaultContentLanguage: "fr", DefaultContentLanguageInSubdir: defaultInSubDir} + + sites := createMultiTestSites(t, siteConfig, multiSiteTOMLConfigTemplate) + fs := sites.Fs + th := testHelper{sites.Cfg, fs, t} + + err := sites.Build(BuildCfg{}) + + if err != nil { + t.Fatalf("Failed to build sites: %s", err) + } + + require.Len(t, sites.Sites, 4) + + enSite := sites.Sites[0] + frSite := sites.Sites[1] + + require.Equal(t, "/en", enSite.Info.LanguagePrefix) + + if defaultInSubDir { + require.Equal(t, "/fr", frSite.Info.LanguagePrefix) + } else { + require.Equal(t, "", frSite.Info.LanguagePrefix) + } + + require.Equal(t, "/blog/en/foo", enSite.PathSpec.RelURL("foo", true)) + + doc1en := enSite.RegularPages[0] + doc1fr := frSite.RegularPages[0] + + enPerm := doc1en.Permalink() + enRelPerm := doc1en.RelPermalink() + require.Equal(t, "http://example.com/blog/en/sect/doc1-slug/", enPerm) + require.Equal(t, "/blog/en/sect/doc1-slug/", enRelPerm) + + frPerm := doc1fr.Permalink() + frRelPerm := doc1fr.RelPermalink() + // Main language in root + require.Equal(t, th.replaceDefaultContentLanguageValue("http://example.com/blog/fr/sect/doc1/"), frPerm) + require.Equal(t, th.replaceDefaultContentLanguageValue("/blog/fr/sect/doc1/"), frRelPerm) + + th.assertFileContent("public/fr/sect/doc1/index.html", "Single", "Bonjour") + th.assertFileContent("public/en/sect/doc1-slug/index.html", "Single", "Hello") + + // Check home + if defaultInSubDir { + // should have a redirect on top level. + th.assertFileContentStraight("public/index.html", ``) + } else { + // should have redirect back to root + th.assertFileContentStraight("public/fr/index.html", ``) + } + th.assertFileContent("public/fr/index.html", "Home", "Bonjour") + th.assertFileContent("public/en/index.html", "Home", "Hello") + + // Check list pages + th.assertFileContent("public/fr/sect/index.html", "List", "Bonjour") + th.assertFileContent("public/en/sect/index.html", "List", "Hello") + th.assertFileContent("public/fr/plaques/frtag1/index.html", "List", "Bonjour") + th.assertFileContent("public/en/tags/tag1/index.html", "List", "Hello") + + // Check sitemaps + // Sitemaps behaves different: In a multilanguage setup there will always be a index file and + // one sitemap in each lang folder. + th.assertFileContentStraight("public/sitemap.xml", + "http://example.com/blog/en/sitemap.xml", + "http://example.com/blog/fr/sitemap.xml") + + if defaultInSubDir { + th.assertFileContentStraight("public/fr/sitemap.xml", "http://example.com/blog/fr/") + } else { + th.assertFileContentStraight("public/fr/sitemap.xml", "http://example.com/blog/") + } + th.assertFileContent("public/en/sitemap.xml", "http://example.com/blog/en/") + + // Check rss + th.assertFileContent("public/fr/index.xml", `http://example.com/blog/en/sitemap.xml"), sitemapIndex) + require.True(t, strings.Contains(sitemapIndex, "http://example.com/blog/fr/sitemap.xml"), sitemapIndex) + sitemapEn := readDestination(t, fs, "public/en/sitemap.xml") + sitemapFr := readDestination(t, fs, "public/fr/sitemap.xml") + require.True(t, strings.Contains(sitemapEn, "http://example.com/blog/en/sect/doc2/"), sitemapEn) + require.True(t, strings.Contains(sitemapFr, "http://example.com/blog/fr/sect/doc1/"), sitemapFr) + + // Check taxonomies + enTags := enSite.Taxonomies["tags"] + frTags := frSite.Taxonomies["plaques"] + require.Len(t, enTags, 2, fmt.Sprintf("Tags in en: %v", enTags)) + require.Len(t, frTags, 2, fmt.Sprintf("Tags in fr: %v", frTags)) + require.NotNil(t, enTags["tag1"]) + require.NotNil(t, frTags["frtag1"]) + readDestination(t, fs, "public/fr/plaques/frtag1/index.html") + readDestination(t, fs, "public/en/tags/tag1/index.html") + + // Check Blackfriday config + require.True(t, strings.Contains(string(doc1fr.Content), "«"), string(doc1fr.Content)) + require.False(t, strings.Contains(string(doc1en.Content), "«"), string(doc1en.Content)) + require.True(t, strings.Contains(string(doc1en.Content), "“"), string(doc1en.Content)) + + // Check that the drafts etc. are not built/processed/rendered. + assertShouldNotBuild(t, sites) + + // en and nn have custom site menus + require.Len(t, frSite.Menus, 0, "fr: "+configSuffix) + require.Len(t, enSite.Menus, 1, "en: "+configSuffix) + require.Len(t, nnSite.Menus, 1, "nn: "+configSuffix) + + require.Equal(t, "Home", enSite.Menus["main"].ByName()[0].Name) + require.Equal(t, "Heim", nnSite.Menus["main"].ByName()[0].Name) + + // Issue #1302 + require.Equal(t, template.URL(""), enSite.RegularPages[0].RSSLink()) + + // Issue #3108 + next := enSite.RegularPages[0].Next + require.NotNil(t, next) + require.Equal(t, KindPage, next.Kind) + + for { + if next == nil { + break + } + require.Equal(t, KindPage, next.Kind) + next = next.Next + } + +} + +func TestMultiSitesRebuild(t *testing.T) { + // t.Parallel() not supported, see https://github.com/fortytw2/leaktest/issues/4 + // This leaktest seems to be a little bit shaky on Travis. + if !isCI() { + defer leaktest.CheckTimeout(t, 30*time.Second)() + } + siteConfig := testSiteConfig{Fs: afero.NewMemMapFs(), DefaultContentLanguage: "fr", DefaultContentLanguageInSubdir: true} + sites := createMultiTestSites(t, siteConfig, multiSiteTOMLConfigTemplate) + fs := sites.Fs + cfg := BuildCfg{Watching: true} + th := testHelper{sites.Cfg, fs, t} + + err := sites.Build(cfg) + + if err != nil { + t.Fatalf("Failed to build sites: %s", err) + } + + _, err = fs.Destination.Open("public/en/sect/doc2/index.html") + + if err != nil { + t.Fatalf("Unable to locate file") + } + + enSite := sites.Sites[0] + frSite := sites.Sites[1] + + require.Len(t, enSite.RegularPages, 4) + require.Len(t, frSite.RegularPages, 3) + + // Verify translations + th.assertFileContent("public/en/sect/doc1-slug/index.html", "Hello") + th.assertFileContent("public/fr/sect/doc1/index.html", "Bonjour") + + // check single page content + th.assertFileContent("public/fr/sect/doc1/index.html", "Single", "Shortcode: Bonjour") + th.assertFileContent("public/en/sect/doc1-slug/index.html", "Single", "Shortcode: Hello") + + for i, this := range []struct { + preFunc func(t *testing.T) + events []fsnotify.Event + assertFunc func(t *testing.T) + }{ + // * Remove doc + // * Add docs existing languages + // (Add doc new language: TODO(bep) we should load config.toml as part of these so we can add languages). + // * Rename file + // * Change doc + // * Change a template + // * Change language file + { + nil, + []fsnotify.Event{{Name: "content/sect/doc2.en.md", Op: fsnotify.Remove}}, + func(t *testing.T) { + require.Len(t, enSite.RegularPages, 3, "1 en removed") + + // Check build stats + require.Equal(t, 1, enSite.draftCount, "Draft") + require.Equal(t, 1, enSite.futureCount, "Future") + require.Equal(t, 1, enSite.expiredCount, "Expired") + require.Equal(t, 0, frSite.draftCount, "Draft") + require.Equal(t, 1, frSite.futureCount, "Future") + require.Equal(t, 1, frSite.expiredCount, "Expired") + }, + }, + { + func(t *testing.T) { + writeNewContentFile(t, fs, "new_en_1", "2016-07-31", "content/new1.en.md", -5) + writeNewContentFile(t, fs, "new_en_2", "1989-07-30", "content/new2.en.md", -10) + writeNewContentFile(t, fs, "new_fr_1", "2016-07-30", "content/new1.fr.md", 10) + }, + []fsnotify.Event{ + {Name: "content/new1.en.md", Op: fsnotify.Create}, + {Name: "content/new2.en.md", Op: fsnotify.Create}, + {Name: "content/new1.fr.md", Op: fsnotify.Create}, + }, + func(t *testing.T) { + require.Len(t, enSite.RegularPages, 5) + require.Len(t, enSite.AllPages, 30) + require.Len(t, frSite.RegularPages, 4) + require.Equal(t, "new_fr_1", frSite.RegularPages[3].Title) + require.Equal(t, "new_en_2", enSite.RegularPages[0].Title) + require.Equal(t, "new_en_1", enSite.RegularPages[1].Title) + + rendered := readDestination(t, fs, "public/en/new1/index.html") + require.True(t, strings.Contains(rendered, "new_en_1"), rendered) + }, + }, + { + func(t *testing.T) { + p := "content/sect/doc1.en.md" + doc1 := readSource(t, fs, p) + doc1 += "CHANGED" + writeSource(t, fs, p, doc1) + }, + []fsnotify.Event{{Name: "content/sect/doc1.en.md", Op: fsnotify.Write}}, + func(t *testing.T) { + require.Len(t, enSite.RegularPages, 5) + doc1 := readDestination(t, fs, "public/en/sect/doc1-slug/index.html") + require.True(t, strings.Contains(doc1, "CHANGED"), doc1) + + }, + }, + // Rename a file + { + func(t *testing.T) { + if err := fs.Source.Rename("content/new1.en.md", "content/new1renamed.en.md"); err != nil { + t.Fatalf("Rename failed: %s", err) + } + }, + []fsnotify.Event{ + {Name: "content/new1renamed.en.md", Op: fsnotify.Rename}, + {Name: "content/new1.en.md", Op: fsnotify.Rename}, + }, + func(t *testing.T) { + require.Len(t, enSite.RegularPages, 5, "Rename") + require.Equal(t, "new_en_1", enSite.RegularPages[1].Title) + rendered := readDestination(t, fs, "public/en/new1renamed/index.html") + require.True(t, strings.Contains(rendered, "new_en_1"), rendered) + }}, + { + // Change a template + func(t *testing.T) { + template := "layouts/_default/single.html" + templateContent := readSource(t, fs, template) + templateContent += "{{ print \"Template Changed\"}}" + writeSource(t, fs, template, templateContent) + }, + []fsnotify.Event{{Name: "layouts/_default/single.html", Op: fsnotify.Write}}, + func(t *testing.T) { + require.Len(t, enSite.RegularPages, 5) + require.Len(t, enSite.AllPages, 30) + require.Len(t, frSite.RegularPages, 4) + doc1 := readDestination(t, fs, "public/en/sect/doc1-slug/index.html") + require.True(t, strings.Contains(doc1, "Template Changed"), doc1) + }, + }, + { + // Change a language file + func(t *testing.T) { + languageFile := "i18n/fr.yaml" + langContent := readSource(t, fs, languageFile) + langContent = strings.Replace(langContent, "Bonjour", "Salut", 1) + writeSource(t, fs, languageFile, langContent) + }, + []fsnotify.Event{{Name: "i18n/fr.yaml", Op: fsnotify.Write}}, + func(t *testing.T) { + require.Len(t, enSite.RegularPages, 5) + require.Len(t, enSite.AllPages, 30) + require.Len(t, frSite.RegularPages, 4) + docEn := readDestination(t, fs, "public/en/sect/doc1-slug/index.html") + require.True(t, strings.Contains(docEn, "Hello"), "No Hello") + docFr := readDestination(t, fs, "public/fr/sect/doc1/index.html") + require.True(t, strings.Contains(docFr, "Salut"), "No Salut") + + homeEn := enSite.getPage(KindHome) + require.NotNil(t, homeEn) + require.Len(t, homeEn.Translations(), 3) + require.Equal(t, "fr", homeEn.Translations()[0].Lang()) + + }, + }, + // Change a shortcode + { + func(t *testing.T) { + writeSource(t, fs, "layouts/shortcodes/shortcode.html", "Modified Shortcode: {{ i18n \"hello\" }}") + }, + []fsnotify.Event{ + {Name: "layouts/shortcodes/shortcode.html", Op: fsnotify.Write}, + }, + func(t *testing.T) { + require.Len(t, enSite.RegularPages, 5) + require.Len(t, enSite.AllPages, 30) + require.Len(t, frSite.RegularPages, 4) + th.assertFileContent("public/fr/sect/doc1/index.html", "Single", "Modified Shortcode: Salut") + th.assertFileContent("public/en/sect/doc1-slug/index.html", "Single", "Modified Shortcode: Hello") + }, + }, + } { + + if this.preFunc != nil { + this.preFunc(t) + } + + err = sites.Build(cfg, this.events...) + + if err != nil { + t.Fatalf("[%d] Failed to rebuild sites: %s", i, err) + } + + this.assertFunc(t) + } + + // Check that the drafts etc. are not built/processed/rendered. + assertShouldNotBuild(t, sites) + +} + +func assertShouldNotBuild(t *testing.T, sites *HugoSites) { + s := sites.Sites[0] + + for _, p := range s.rawAllPages { + // No HTML when not processed + require.Equal(t, p.shouldBuild(), bytes.Contains(p.workContent, []byte("Another header", "

    Another header

    ", "

    The End.

    "} + // Veriy Swedish site + require.Len(t, svSite.RegularPages, 1) + svPage := svSite.RegularPages[0] + require.Equal(t, "Swedish Contentfile", svPage.Title) + require.Equal(t, "sv", svPage.Lang()) + require.Len(t, svPage.Translations(), 2) + require.Len(t, svPage.AllTranslations(), 3) + require.Equal(t, "en", svPage.Translations()[0].Lang()) - for i := 1; i <= numPages; i++ { - if i%3 != 0 { - section := "s1" - if i%10 == 0 { - section = "s2" - } - checkContent(b, fmt.Sprintf("public/%s/page%d/index.html", section, i), contentMatchers...) - } - } + // Regular pages have no children + require.Len(t, svPage.Pages, 0) + require.Len(t, svPage.Data["Pages"], 0) - for i := 1; i <= numPages; i++ { - section := "s1" - if i%10 == 0 { - section = "s2" - } - checkContent(b, fmt.Sprintf("public/%s/page%d/index.json", section, i), contentMatchers...) - } - - checkContent(b, "public/s1/index.html", "P: s1/_index.md\nList: 10|List Content: 8132\n\n\nL1: 500 L2: 5\n\nRender 0: View: 8132\n\nRender 1: View: 8132\n\nRender 2: View: 8132\n\nRender 3: View: 8132\n\nRender 4: View: 8132\n\nEND\n") - checkContent(b, "public/s2/index.html", "P: s2/_index.md\nList: 10|List Content: 8132", "Render 4: View: 8132\n\nEND") - checkContent(b, "public/index.html", "P: _index.md\nList: 10|List Content: 8132", "4: View: 8132\n\nEND") - - // Check paginated pages - for i := 2; i <= 9; i++ { - checkContent(b, fmt.Sprintf("public/page/%d/index.html", i), fmt.Sprintf("Page: %d", i), "Content: 8132\n\n\nL1: 500 L2: 5\n\nRender 0: View: 8132", "Render 4: View: 8132\n\nEND") - } } -func checkContent(s *sitesBuilder, filename string, matches ...string) { - s.T.Helper() - content := readWorkingDir(s.T, s.Fs, filename) - for _, match := range matches { - if !strings.Contains(content, match) { - s.Fatalf("No match for\n%q\nin content for %s\n%q\nDiff:\n%s", match, filename, content, htesting.DiffStrings(content, match)) - } +func TestChangeDefaultLanguage(t *testing.T) { + t.Parallel() + mf := afero.NewMemMapFs() + + sites := createMultiTestSites(t, testSiteConfig{Fs: mf, DefaultContentLanguage: "fr", DefaultContentLanguageInSubdir: false}, multiSiteTOMLConfigTemplate) + + require.Equal(t, mf, sites.Fs.Source) + + cfg := BuildCfg{} + fs := sites.Fs + th := testHelper{sites.Cfg, fs, t} + + err := sites.Build(cfg) + + if err != nil { + t.Fatalf("Failed to build sites: %s", err) } + + th.assertFileContent("public/sect/doc1/index.html", "Single", "Bonjour") + th.assertFileContent("public/en/sect/doc2/index.html", "Single", "Hello") + + newConfig := createConfig(t, testSiteConfig{Fs: mf, DefaultContentLanguage: "en", DefaultContentLanguageInSubdir: false}, multiSiteTOMLConfigTemplate) + + // replace the config + writeSource(t, fs, "multilangconfig.toml", newConfig) + + // Watching does not work with in-memory fs, so we trigger a reload manually + // This does not look pretty, so we should think of something else. + require.NoError(t, th.Cfg.(*helpers.Language).Cfg.(*viper.Viper).ReadInConfig()) + err = sites.Build(BuildCfg{CreateSitesFromConfig: true}) + + if err != nil { + t.Fatalf("Failed to rebuild sites: %s", err) + } + + // Default language is now en, so that should now be the "root" language + th.assertFileContent("public/fr/sect/doc1/index.html", "Single", "Bonjour") + th.assertFileContent("public/sect/doc2/index.html", "Single", "Hello") } -func TestTranslationsFromContentToNonContent(t *testing.T) { - b := newTestSitesBuilder(t) - b.WithConfigFile("toml", ` +func TestTableOfContentsInShortcodes(t *testing.T) { + t.Parallel() + mf := afero.NewMemMapFs() -baseURL = "http://example.com/" + writeToFs(t, mf, "layouts/shortcodes/toc.html", tocShortcode) + writeToFs(t, mf, "content/post/simple.en.md", tocPageSimple) + writeToFs(t, mf, "content/post/withSCInHeading.en.md", tocPageWithShortcodesInHeadings) -defaultContentLanguage = "en" + sites := createMultiTestSites(t, testSiteConfig{Fs: mf, DefaultContentLanguage: "en", DefaultContentLanguageInSubdir: true}, multiSiteTOMLConfigTemplate) -[languages] -[languages.en] + cfg := BuildCfg{} + + err := sites.Build(cfg) + + if err != nil { + t.Fatalf("Failed to build sites: %s", err) + } + + fs := sites.Fs + th := testHelper{sites.Cfg, fs, t} + + th.assertFileContent("public/en/post/simple/index.html", tocPageSimpleExpected) + th.assertFileContent("public/en/post/withSCInHeading/index.html", tocPageWithShortcodesInHeadingsExpected) +} + +var tocShortcode = ` +{{ .Page.TableOfContents }} +` + +var tocPageSimple = `--- +title: tocTest +publishdate: "2000-01-01" +--- + +{{< toc >}} + +# Heading 1 {#1} + +Some text. + +## Subheading 1.1 {#1-1} + +Some more text. + +# Heading 2 {#2} + +Even more text. + +## Subheading 2.1 {#2-1} + +Lorem ipsum... +` + +var tocPageSimpleExpected = `` + +var tocPageWithShortcodesInHeadings = `--- +title: tocTest +publishdate: "2000-01-01" +--- + +{{< toc >}} + +# Heading 1 {#1} + +Some text. + +## Subheading 1.1 {{< shortcode >}} {#1-1} + +Some more text. + +# Heading 2 {{% shortcode %}} {#2} + +Even more text. + +## Subheading 2.1 {#2-1} + +Lorem ipsum... +` + +var tocPageWithShortcodesInHeadingsExpected = `` + +var multiSiteTOMLConfigTemplate = ` +baseURL = "http://example.com/blog" +disableSitemap = false +disableRSS = false +rssURI = "index.xml" + +paginate = 1 +disablePathToLower = true +defaultContentLanguage = "{{ .DefaultContentLanguage }}" +defaultContentLanguageInSubdir = {{ .DefaultContentLanguageInSubdir }} + +[permalinks] +other = "/somewhere/else/:filename" + +[blackfriday] +angledQuotes = true + +[Taxonomies] +tag = "tags" + +[Languages] +[Languages.en] weight = 10 -contentDir = "content/en" -[languages.nn] +title = "In English" +languageName = "English" +[Languages.en.blackfriday] +angledQuotes = false +[[Languages.en.menu.main]] +url = "/" +name = "Home" +weight = 0 + +[Languages.fr] weight = 20 -contentDir = "content/nn" +title = "Le Français" +languageName = "Français" +[Languages.fr.Taxonomies] +plaque = "plaques" +[Languages.nn] +weight = 30 +title = "På nynorsk" +languageName = "Nynorsk" +paginatePath = "side" +[Languages.nn.Taxonomies] +lag = "lag" +[[Languages.nn.menu.main]] +url = "/" +name = "Heim" +weight = 1 -`) +[Languages.nb] +weight = 40 +title = "På bokmål" +languageName = "Bokmål" +paginatePath = "side" +[Languages.nb.Taxonomies] +lag = "lag" +` - b.WithContent("en/mysection/_index.md", ` ---- -Title: My Section ---- +var multiSiteYAMLConfigTemplate = ` +baseURL: "http://example.com/blog" +disableSitemap: false +disableRSS: false +rssURI: "index.xml" -`) +disablePathToLower: true +paginate: 1 +defaultContentLanguage: "{{ .DefaultContentLanguage }}" +defaultContentLanguageInSubdir: {{ .DefaultContentLanguageInSubdir }} - b.WithContent("en/_index.md", ` ---- -Title: My Home ---- +permalinks: + other: "/somewhere/else/:filename" -`) +blackfriday: + angledQuotes: true - b.WithContent("en/categories/mycat/_index.md", ` ---- -Title: My MyCat ---- +Taxonomies: + tag: "tags" -`) +Languages: + en: + weight: 10 + title: "In English" + languageName: "English" + blackfriday: + angledQuotes: false + menu: + main: + - url: "/" + name: "Home" + weight: 0 + fr: + weight: 20 + title: "Le Français" + languageName: "Français" + Taxonomies: + plaque: "plaques" + nn: + weight: 30 + title: "På nynorsk" + languageName: "Nynorsk" + paginatePath: "side" + Taxonomies: + lag: "lag" + menu: + main: + - url: "/" + name: "Heim" + weight: 1 + nb: + weight: 40 + title: "På bokmål" + languageName: "Bokmål" + paginatePath: "side" + Taxonomies: + lag: "lag" - b.WithContent("en/categories/_index.md", ` ---- -Title: My categories ---- +` -`) +var multiSiteJSONConfigTemplate = ` +{ + "baseURL": "http://example.com/blog", + "disableSitemap": false, + "disableRSS": false, + "rssURI": "index.xml", + "paginate": 1, + "disablePathToLower": true, + "defaultContentLanguage": "{{ .DefaultContentLanguage }}", + "defaultContentLanguageInSubdir": true, + "permalinks": { + "other": "/somewhere/else/:filename" + }, + "blackfriday": { + "angledQuotes": true + }, + "Taxonomies": { + "tag": "tags" + }, + "Languages": { + "en": { + "weight": 10, + "title": "In English", + "languageName": "English", + "blackfriday": { + "angledQuotes": false + }, + "menu": { + "main": [ + { + "url": "/", + "name": "Home", + "weight": 0 + } + ] + } + }, + "fr": { + "weight": 20, + "title": "Le Français", + "languageName": "Français", + "Taxonomies": { + "plaque": "plaques" + } + }, + "nn": { + "weight": 30, + "title": "På nynorsk", + "paginatePath": "side", + "languageName": "Nynorsk", + "Taxonomies": { + "lag": "lag" + }, + "menu": { + "main": [ + { + "url": "/", + "name": "Heim", + "weight": 1 + } + ] + } + }, + "nb": { + "weight": 40, + "title": "På bokmål", + "paginatePath": "side", + "languageName": "Bokmål", + "Taxonomies": { + "lag": "lag" + } + } + } +} +` - for _, lang := range []string{"en", "nn"} { - b.WithContent(lang+"/mysection/page.md", ` ---- -Title: My Page -categories: ["mycat"] ---- +func createMultiTestSites(t *testing.T, siteConfig testSiteConfig, tomlConfigTemplate string) *HugoSites { + return createMultiTestSitesForConfig(t, siteConfig, tomlConfigTemplate, "toml") +} -`) +func createMultiTestSitesForConfig(t *testing.T, siteConfig testSiteConfig, configTemplate, configSuffix string) *HugoSites { + + configContent := createConfig(t, siteConfig, configTemplate) + + mf := siteConfig.Fs + + // Add some layouts + if err := afero.WriteFile(mf, + filepath.Join("layouts", "_default/single.html"), + []byte("Single: {{ .Title }}|{{ i18n \"hello\" }}|{{.Lang}}|{{ .Content }}"), + 0755); err != nil { + t.Fatalf("Failed to write layout file: %s", err) } - b.Build(BuildCfg{}) - - for _, path := range []string{ - "/", - "/mysection", - "/categories", - "/categories/mycat", - } { - t.Run(path, func(t *testing.T) { - c := qt.New(t) - - s1, _ := b.H.Sites[0].getPage(nil, path) - s2, _ := b.H.Sites[1].getPage(nil, path) - - c.Assert(s1, qt.Not(qt.IsNil)) - c.Assert(s2, qt.Not(qt.IsNil)) - - c.Assert(len(s1.Translations()), qt.Equals, 1) - c.Assert(len(s2.Translations()), qt.Equals, 1) - c.Assert(s1.Translations()[0], qt.Equals, s2) - c.Assert(s2.Translations()[0], qt.Equals, s1) - - m1 := s1.Translations().MergeByLanguage(s2.Translations()) - m2 := s2.Translations().MergeByLanguage(s1.Translations()) - - c.Assert(len(m1), qt.Equals, 1) - c.Assert(len(m2), qt.Equals, 1) - }) + if err := afero.WriteFile(mf, + filepath.Join("layouts", "_default/list.html"), + []byte("{{ $p := .Paginator }}List Page {{ $p.PageNumber }}: {{ .Title }}|{{ i18n \"hello\" }}|{{ .Permalink }}"), + 0755); err != nil { + t.Fatalf("Failed to write layout file: %s", err) } + + if err := afero.WriteFile(mf, + filepath.Join("layouts", "index.html"), + []byte("{{ $p := .Paginator }}Default Home Page {{ $p.PageNumber }}: {{ .Title }}|{{ .IsHome }}|{{ i18n \"hello\" }}|{{ .Permalink }}|{{ .Site.Data.hugo.slogan }}"), + 0755); err != nil { + t.Fatalf("Failed to write layout file: %s", err) + } + + if err := afero.WriteFile(mf, + filepath.Join("layouts", "index.fr.html"), + []byte("{{ $p := .Paginator }}French Home Page {{ $p.PageNumber }}: {{ .Title }}|{{ .IsHome }}|{{ i18n \"hello\" }}|{{ .Permalink }}|{{ .Site.Data.hugo.slogan }}"), + 0755); err != nil { + t.Fatalf("Failed to write layout file: %s", err) + } + + // Add a shortcode + if err := afero.WriteFile(mf, + filepath.Join("layouts", "shortcodes", "shortcode.html"), + []byte("Shortcode: {{ i18n \"hello\" }}"), + 0755); err != nil { + t.Fatalf("Failed to write layout file: %s", err) + } + + // A shortcode in multiple languages + if err := afero.WriteFile(mf, + filepath.Join("layouts", "shortcodes", "lingo.html"), + []byte("LingoDefault"), + 0755); err != nil { + t.Fatalf("Failed to write layout file: %s", err) + } + + if err := afero.WriteFile(mf, + filepath.Join("layouts", "shortcodes", "lingo.fr.html"), + []byte("LingoFrench"), + 0755); err != nil { + t.Fatalf("Failed to write layout file: %s", err) + } + + // Add some language files + if err := afero.WriteFile(mf, + filepath.Join("i18n", "en.yaml"), + []byte(` +hello: + other: "Hello" +`), + 0755); err != nil { + t.Fatalf("Failed to write language file: %s", err) + } + if err := afero.WriteFile(mf, + filepath.Join("i18n", "fr.yaml"), + []byte(` +hello: + other: "Bonjour" +`), + 0755); err != nil { + t.Fatalf("Failed to write language file: %s", err) + } + + // Sources + sources := []source.ByteSource{ + {Name: filepath.FromSlash("root.en.md"), Content: []byte(`--- +title: root +weight: 10000 +slug: root +publishdate: "2000-01-01" +--- +# root +`)}, + {Name: filepath.FromSlash("sect/doc1.en.md"), Content: []byte(`--- +title: doc1 +weight: 1 +slug: doc1-slug +tags: + - tag1 +publishdate: "2000-01-01" +--- +# doc1 +*some "content"* + +{{< shortcode >}} + +{{< lingo >}} + +NOTE: slug should be used as URL +`)}, + {Name: filepath.FromSlash("sect/doc1.fr.md"), Content: []byte(`--- +title: doc1 +weight: 1 +plaques: + - frtag1 + - frtag2 +publishdate: "2000-01-04" +--- +# doc1 +*quelque "contenu"* + +{{< shortcode >}} + +{{< lingo >}} + +NOTE: should be in the 'en' Page's 'Translations' field. +NOTE: date is after "doc3" +`)}, + {Name: filepath.FromSlash("sect/doc2.en.md"), Content: []byte(`--- +title: doc2 +weight: 2 +publishdate: "2000-01-02" +--- +# doc2 +*some content* +NOTE: without slug, "doc2" should be used, without ".en" as URL +`)}, + {Name: filepath.FromSlash("sect/doc3.en.md"), Content: []byte(`--- +title: doc3 +weight: 3 +publishdate: "2000-01-03" +tags: + - tag2 + - tag1 +url: /superbob +--- +# doc3 +*some content* +NOTE: third 'en' doc, should trigger pagination on home page. +`)}, + {Name: filepath.FromSlash("sect/doc4.md"), Content: []byte(`--- +title: doc4 +weight: 4 +plaques: + - frtag1 +publishdate: "2000-01-05" +--- +# doc4 +*du contenu francophone* +NOTE: should use the defaultContentLanguage and mark this doc as 'fr'. +NOTE: doesn't have any corresponding translation in 'en' +`)}, + {Name: filepath.FromSlash("other/doc5.fr.md"), Content: []byte(`--- +title: doc5 +weight: 5 +publishdate: "2000-01-06" +--- +# doc5 +*autre contenu francophone* +NOTE: should use the "permalinks" configuration with :filename +`)}, + // Add some for the stats + {Name: filepath.FromSlash("stats/expired.fr.md"), Content: []byte(`--- +title: expired +publishdate: "2000-01-06" +expiryDate: "2001-01-06" +--- +# Expired +`)}, + {Name: filepath.FromSlash("stats/future.fr.md"), Content: []byte(`--- +title: future +weight: 6 +publishdate: "2100-01-06" +--- +# Future +`)}, + {Name: filepath.FromSlash("stats/expired.en.md"), Content: []byte(`--- +title: expired +weight: 7 +publishdate: "2000-01-06" +expiryDate: "2001-01-06" +--- +# Expired +`)}, + {Name: filepath.FromSlash("stats/future.en.md"), Content: []byte(`--- +title: future +weight: 6 +publishdate: "2100-01-06" +--- +# Future +`)}, + {Name: filepath.FromSlash("stats/draft.en.md"), Content: []byte(`--- +title: expired +publishdate: "2000-01-06" +draft: true +--- +# Draft +`)}, + {Name: filepath.FromSlash("stats/tax.nn.md"), Content: []byte(`--- +title: Tax NN +weight: 8 +publishdate: "2000-01-06" +weight: 1001 +lag: +- Sogndal +--- +# Tax NN +`)}, + {Name: filepath.FromSlash("stats/tax.nb.md"), Content: []byte(`--- +title: Tax NB +weight: 8 +publishdate: "2000-01-06" +weight: 1002 +lag: +- Sogndal +--- +# Tax NB +`)}, + } + + configFile := "multilangconfig." + configSuffix + writeToFs(t, mf, configFile, configContent) + + cfg, err := LoadConfig(mf, "", configFile) + require.NoError(t, err) + + fs := hugofs.NewFrom(mf, cfg) + + // Hugo support using ByteSource's directly (for testing), + // but to make it more real, we write them to the mem file system. + for _, s := range sources { + if err := afero.WriteFile(mf, filepath.Join("content", s.Name), s.Content, 0755); err != nil { + t.Fatalf("Failed to write file: %s", err) + } + } + + // Add some data + writeSource(t, fs, "data/hugo.toml", "slogan = \"Hugo Rocks!\"") + + sites, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg}) //, Logger: newDebugLogger()}) + + if err != nil { + t.Fatalf("Failed to create sites: %s", err) + } + + if len(sites.Sites) != 4 { + t.Fatalf("Got %d sites", len(sites.Sites)) + } + + if sites.Fs.Source != mf { + t.Fatal("FS mismatch") + } + + return sites } func writeSource(t testing.TB, fs *hugofs.Fs, filename, content string) { - t.Helper() writeToFs(t, fs.Source, filename, content) } func writeToFs(t testing.TB, fs afero.Fs, filename, content string) { - t.Helper() - if err := afero.WriteFile(fs, filepath.FromSlash(filename), []byte(content), 0o755); err != nil { + if err := afero.WriteFile(fs, filepath.FromSlash(filename), []byte(content), 0755); err != nil { t.Fatalf("Failed to write file: %s", err) } } -func readWorkingDir(t testing.TB, fs *hugofs.Fs, filename string) string { - t.Helper() - return readFileFromFs(t, fs.WorkingDirReadOnly, filename) +func readDestination(t testing.TB, fs *hugofs.Fs, filename string) string { + return readFileFromFs(t, fs.Destination, filename) } -func workingDirExists(fs *hugofs.Fs, filename string) bool { - b, err := helpers.Exists(filename, fs.WorkingDirReadOnly) +func destinationExists(fs *hugofs.Fs, filename string) bool { + b, err := helpers.Exists(filename, fs.Destination) if err != nil { panic(err) } return b } +func readSource(t *testing.T, fs *hugofs.Fs, filename string) string { + return readFileFromFs(t, fs.Source, filename) +} + func readFileFromFs(t testing.TB, fs afero.Fs, filename string) string { - t.Helper() - filename = filepath.Clean(filename) + filename = filepath.FromSlash(filename) b, err := afero.ReadFile(fs, filename) if err != nil { // Print some debug info - hadSlash := strings.HasPrefix(filename, helpers.FilePathSeparator) - start := 0 - if hadSlash { - start = 1 - } - end := start + 1 - - parts := strings.Split(filename, helpers.FilePathSeparator) - if parts[start] == "work" { - end++ - } - - /* - root := filepath.Join(parts[start:end]...) - if hadSlash { - root = helpers.FilePathSeparator + root + root := strings.Split(filename, helpers.FilePathSeparator)[0] + afero.Walk(fs, root, func(path string, info os.FileInfo, err error) error { + if info != nil && !info.IsDir() { + fmt.Println(" ", path) } - helpers.PrintFs(fs, root, os.Stdout) - */ - + return nil + }) t.Fatalf("Failed to read file: %s", err) } return string(b) @@ -360,18 +1336,17 @@ func newTestPage(title, date string, weight int) string { return fmt.Sprintf(testPageTemplate, title, date, weight, title) } -func TestRebuildOnAssetChange(t *testing.T) { - b := newTestSitesBuilder(t).Running().WithLogger(loggers.NewDefault()) - b.WithTemplatesAdded("index.html", ` -{{ (resources.Get "data.json").Content }} -`) - b.WithSourceFile("assets/data.json", "orig data") - - b.Build(BuildCfg{}) - b.AssertFileContent("public/index.html", `orig data`) - - b.EditFiles("assets/data.json", "changed data") - - b.Build(BuildCfg{}) - b.AssertFileContent("public/index.html", `changed data`) +func writeNewContentFile(t *testing.T, fs *hugofs.Fs, title, date, filename string, weight int) { + content := newTestPage(title, date, weight) + writeSource(t, fs, filename, content) +} + +func createConfig(t *testing.T, config testSiteConfig, configTemplate string) string { + templ, err := template.New("test").Parse(configTemplate) + if err != nil { + t.Fatal("Template parse failed:", err) + } + var b bytes.Buffer + templ.Execute(&b, config) + return b.String() } diff --git a/hugolib/hugo_sites_multihost_test.go b/hugolib/hugo_sites_multihost_test.go deleted file mode 100644 index 37f7ab927..000000000 --- a/hugolib/hugo_sites_multihost_test.go +++ /dev/null @@ -1,284 +0,0 @@ -package hugolib - -import ( - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestMultihost(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -defaultContentLanguage = "fr" -defaultContentLanguageInSubdir = false -staticDir = ["s1", "s2"] -enableRobotsTXT = true - -[pagination] -pagerSize = 1 - -[permalinks] -other = "/somewhere/else/:filename" - -[taxonomies] -tag = "tags" - -[languages] -[languages.en] -staticDir2 = ["staticen"] -baseURL = "https://example.com/docs" -weight = 10 -title = "In English" -languageName = "English" -[languages.fr] -staticDir2 = ["staticfr"] -baseURL = "https://example.fr" -weight = 20 -title = "Le Français" -languageName = "Français" --- assets/css/main.css -- -body { color: red; } --- content/mysect/mybundle/index.md -- ---- -tags: [a, b] -title: "My Bundle fr" ---- -My Bundle --- content/mysect/mybundle/index.en.md -- ---- -tags: [c, d] -title: "My Bundle en" ---- -My Bundle --- content/mysect/mybundle/foo.txt -- -Foo --- layouts/_default/list.html -- -List|{{ .Title }}|{{ .Lang }}|{{ .Permalink}}|{{ .RelPermalink }}| --- layouts/_default/single.html -- -Single|{{ .Title }}|{{ .Lang }}|{{ .Permalink}}|{{ .RelPermalink }}| -{{ $foo := .Resources.Get "foo.txt" | fingerprint }} -Foo: {{ $foo.Permalink }}| -{{ $css := resources.Get "css/main.css" | fingerprint }} -CSS: {{ $css.Permalink }}|{{ $css.RelPermalink }}| --- layouts/robots.txt -- -robots|{{ site.Language.Lang }} --- layouts/404.html -- -404|{{ site.Language.Lang }} - - - -` - - b := Test(t, files) - - b.Assert(b.H.Conf.IsMultilingual(), qt.Equals, true) - b.Assert(b.H.Conf.IsMultihost(), qt.Equals, true) - - // helpers.PrintFs(b.H.Fs.PublishDir, "", os.Stdout) - - // Check regular pages. - b.AssertFileContent("public/en/mysect/mybundle/index.html", "Single|My Bundle en|en|https://example.com/docs/mysect/mybundle/|") - b.AssertFileContent("public/fr/mysect/mybundle/index.html", "Single|My Bundle fr|fr|https://example.fr/mysect/mybundle/|") - - // Check robots.txt - b.AssertFileContent("public/en/robots.txt", "robots|en") - b.AssertFileContent("public/fr/robots.txt", "robots|fr") - - // Check sitemap.xml - b.AssertFileContent("public/en/sitemap.xml", "https://example.com/docs/mysect/mybundle/") - b.AssertFileContent("public/fr/sitemap.xml", "https://example.fr/mysect/mybundle/") - - // Check 404 - b.AssertFileContent("public/en/404.html", "404|en") - b.AssertFileContent("public/fr/404.html", "404|fr") - - // Check tags. - b.AssertFileContent("public/en/tags/d/index.html", "List|D|en|https://example.com/docs/tags/d/") - b.AssertFileContent("public/fr/tags/b/index.html", "List|B|fr|https://example.fr/tags/b/") - b.AssertFileExists("public/en/tags/b/index.html", false) - b.AssertFileExists("public/fr/tags/d/index.html", false) - - // en/mysect/mybundle/foo.txt fingerprinted - b.AssertFileContent("public/en/mysect/mybundle/foo.1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa.txt", "Foo") - b.AssertFileContent("public/en/mysect/mybundle/index.html", "Foo: https://example.com/docs/mysect/mybundle/foo.1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa.txt|") - b.AssertFileContent("public/fr/mysect/mybundle/foo.1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa.txt", "Foo") - b.AssertFileContent("public/fr/mysect/mybundle/index.html", "Foo: https://example.fr/mysect/mybundle/foo.1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa.txt|") - - // Assets CSS fingerprinted - b.AssertFileContent("public/en/mysect/mybundle/index.html", "CSS: https://example.fr/css/main.5de625c36355cce7c1d5408826a0b21abfb49fb6c0e1f16c945a6f2aef38200c.css|") - b.AssertFileContent("public/en/css/main.5de625c36355cce7c1d5408826a0b21abfb49fb6c0e1f16c945a6f2aef38200c.css", "body { color: red; }") - b.AssertFileContent("public/fr/mysect/mybundle/index.html", "CSS: https://example.fr/css/main.5de625c36355cce7c1d5408826a0b21abfb49fb6c0e1f16c945a6f2aef38200c.css|") - b.AssertFileContent("public/fr/css/main.5de625c36355cce7c1d5408826a0b21abfb49fb6c0e1f16c945a6f2aef38200c.css", "body { color: red; }") -} - -func TestMultihostResourcePerLanguageMultihostMinify(t *testing.T) { - t.Parallel() - files := ` --- hugo.toml -- -disableKinds = ["taxonomy", "term"] -defaultContentLanguage = "en" -defaultContentLanguageInSubDir = true -[languages] -[languages.en] -baseURL = "https://example.en" -weight = 1 -contentDir = "content/en" -[languages.fr] -baseURL = "https://example.fr" -weight = 2 -contentDir = "content/fr" --- content/en/section/mybundle/index.md -- ---- -title: "Mybundle en" ---- --- content/fr/section/mybundle/index.md -- ---- -title: "Mybundle fr" ---- --- content/en/section/mybundle/styles.css -- -.body { - color: english; -} --- content/fr/section/mybundle/styles.css -- -.body { - color: french; -} --- layouts/_default/single.html -- -{{ $data := .Resources.GetMatch "styles*" | minify }} -{{ .Lang }}: {{ $data.Content}}|{{ $data.RelPermalink }}| - -` - b := Test(t, files) - - b.AssertFileContent("public/fr/section/mybundle/index.html", - "fr: .body{color:french}|/section/mybundle/styles.min.css|", - ) - - b.AssertFileContent("public/en/section/mybundle/index.html", - "en: .body{color:english}|/section/mybundle/styles.min.css|", - ) - - b.AssertFileContent("public/en/section/mybundle/styles.min.css", ".body{color:english}") - b.AssertFileContent("public/fr/section/mybundle/styles.min.css", ".body{color:french}") -} - -func TestResourcePerLanguageIssue12163(t *testing.T) { - files := ` --- hugo.toml -- -defaultContentLanguage = 'de' -disableKinds = ['rss','sitemap','taxonomy','term'] - -[languages.de] -baseURL = 'https://de.example.org/' -contentDir = 'content/de' -weight = 1 - -[languages.en] -baseURL = 'https://en.example.org/' -contentDir = 'content/en' -weight = 2 --- content/de/mybundle/index.md -- ---- -title: mybundle-de ---- --- content/de/mybundle/pixel.png -- -iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg== --- content/en/mybundle/index.md -- ---- -title: mybundle-en ---- --- layouts/_default/single.html -- -{{ with .Resources.Get "pixel.png" }} - {{ with .Resize "2x2" }} - {{ .RelPermalink }}| - {{ end }} -{{ end }} -` - - b := Test(t, files) - - b.AssertFileExists("public/de/mybundle/index.html", true) - b.AssertFileExists("public/en/mybundle/index.html", true) - - b.AssertFileExists("public/de/mybundle/pixel.png", true) - b.AssertFileExists("public/en/mybundle/pixel.png", true) - - b.AssertFileExists("public/de/mybundle/pixel_hu_58204cbc58507d74.png", true) - // failing test below - b.AssertFileExists("public/en/mybundle/pixel_hu_58204cbc58507d74.png", true) -} - -func TestMultihostResourceOneBaseURLWithSuPath(t *testing.T) { - files := ` --- hugo.toml -- -defaultContentLanguage = "en" -[languages] -[languages.en] -baseURL = "https://example.com/docs" -weight = 1 -contentDir = "content/en" -[languages.en.permalinks] -section = "/enpages/:slug/" -[languages.fr] -baseURL = "https://example.fr" -contentDir = "content/fr" --- content/en/section/mybundle/index.md -- ---- -title: "Mybundle en" ---- --- content/fr/section/mybundle/index.md -- ---- -title: "Mybundle fr" ---- --- content/fr/section/mybundle/file1.txt -- -File 1 fr. --- content/en/section/mybundle/file1.txt -- -File 1 en. --- content/en/section/mybundle/file2.txt -- -File 2 en. --- layouts/_default/single.html -- -{{ $files := .Resources.Match "file*" }} -Files: {{ range $files }}{{ .Permalink }}|{{ end }}$ - -` - - b := Test(t, files) - - b.AssertFileContent("public/en/enpages/mybundle-en/index.html", "Files: https://example.com/docs/enpages/mybundle-en/file1.txt|https://example.com/docs/enpages/mybundle-en/file2.txt|$") - b.AssertFileContent("public/fr/section/mybundle/index.html", "Files: https://example.fr/section/mybundle/file1.txt|https://example.fr/section/mybundle/file2.txt|$") - - b.AssertFileContent("public/en/enpages/mybundle-en/file1.txt", "File 1 en.") - b.AssertFileContent("public/fr/section/mybundle/file1.txt", "File 1 fr.") - b.AssertFileContent("public/en/enpages/mybundle-en/file2.txt", "File 2 en.") - b.AssertFileContent("public/fr/section/mybundle/file2.txt", "File 2 en.") -} - -func TestMultihostAllButOneLanguageDisabledIssue12288(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -defaultContentLanguage = "en" -disableLanguages = ["fr"] -#baseURL = "https://example.com" -[languages] -[languages.en] -baseURL = "https://example.en" -weight = 1 -[languages.fr] -baseURL = "https://example.fr" -weight = 2 --- assets/css/main.css -- -body { color: red; } --- layouts/index.html -- -{{ $css := resources.Get "css/main.css" | minify }} -CSS: {{ $css.Permalink }}|{{ $css.RelPermalink }}| -` - - b := Test(t, files) - - b.AssertFileContent("public/css/main.min.css", "body{color:red}") - b.AssertFileContent("public/index.html", "CSS: https://example.en/css/main.min.css|/css/main.min.css|") -} diff --git a/hugolib/hugo_sites_test.go b/hugolib/hugo_sites_test.go deleted file mode 100644 index 5e1a1504c..000000000 --- a/hugolib/hugo_sites_test.go +++ /dev/null @@ -1,58 +0,0 @@ -// 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 hugolib - -import "testing" - -func TestSitesAndLanguageOrder(t *testing.T) { - files := ` --- hugo.toml -- -defaultContentLanguage = "fr" -defaultContentLanguageInSubdir = true -[languages] -[languages.en] -weight = 1 -[languages.fr] -weight = 2 -[languages.de] -weight = 3 --- layouts/index.html -- -{{ $bundle := site.GetPage "bundle" }} -Bundle all translations: {{ range $bundle.AllTranslations }}{{ .Lang }}|{{ end }}$ -Bundle translations: {{ range $bundle.Translations }}{{ .Lang }}|{{ end }}$ -Site languages: {{ range site.Languages }}{{ .Lang }}|{{ end }}$ -Sites: {{ range site.Sites }}{{ .Language.Lang }}|{{ end }}$ --- content/bundle/index.fr.md -- ---- -title: "Bundle Fr" ---- --- content/bundle/index.en.md -- ---- -title: "Bundle En" ---- --- content/bundle/index.de.md -- ---- -title: "Bundle De" ---- - - ` - b := Test(t, files) - - b.AssertFileContent("public/en/index.html", - "Bundle all translations: en|fr|de|$", - "Bundle translations: fr|de|$", - "Site languages: en|fr|de|$", - "Sites: fr|en|de|$", - ) -} diff --git a/hugolib/hugo_smoke_test.go b/hugolib/hugo_smoke_test.go deleted file mode 100644 index 09d57bbff..000000000 --- a/hugolib/hugo_smoke_test.go +++ /dev/null @@ -1,600 +0,0 @@ -// 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 hugolib - -import ( - "fmt" - "math/rand" - "testing" - - "github.com/bep/logg" - qt "github.com/frankban/quicktest" -) - -// The most basic build test. -func TestHello(t *testing.T) { - files := ` --- hugo.toml -- -title = "Hello" -baseURL="https://example.org" -disableKinds = ["term", "taxonomy", "section", "page"] --- content/p1.md -- ---- -title: Page ---- --- layouts/index.html -- -Home: {{ .Title }} -` - - b := NewIntegrationTestBuilder( - IntegrationTestConfig{ - T: t, - TxtarString: files, - // LogLevel: logg.LevelTrace, - }, - ).Build() - - b.Assert(b.H.Log.LoggCount(logg.LevelWarn), qt.Equals, 0) - b.AssertFileContent("public/index.html", `Hello`) -} - -func TestSmokeOutputFormats(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.com/" -defaultContentLanguage = "en" -disableKinds = ["term", "taxonomy", "robotsTXT", "sitemap"] -[outputs] -home = ["html", "rss"] -section = ["html", "rss"] -page = ["html"] --- content/p1.md -- ---- -title: Page ---- -Page. - --- layouts/_default/list.html -- -List: {{ .Title }}|{{ .RelPermalink}}|{{ range .OutputFormats }}{{ .Name }}: {{ .RelPermalink }}|{{ end }}$ --- layouts/_default/list.xml -- -List xml: {{ .Title }}|{{ .RelPermalink}}|{{ range .OutputFormats }}{{ .Name }}: {{ .RelPermalink }}|{{ end }}$ --- layouts/_default/single.html -- -Single: {{ .Title }}|{{ .RelPermalink}}|{{ range .OutputFormats }}{{ .Name }}: {{ .RelPermalink }}|{{ end }}$ - -` - - for i := 0; i < 2; i++ { - b := Test(t, files) - b.AssertFileContent("public/index.html", `List: |/|html: /|rss: /index.xml|$`) - b.AssertFileContent("public/index.xml", `List xml: |/|html: /|rss: /index.xml|$`) - b.AssertFileContent("public/p1/index.html", `Single: Page|/p1/|html: /p1/|$`) - b.AssertFileExists("public/p1/index.xml", false) - } -} - -func TestSmoke(t *testing.T) { - t.Parallel() - - // Basic test cases. - // OK translations - // OK page collections - // OK next, prev in section - // OK GetPage - // OK Pagination - // OK RenderString with shortcode - // OK cascade - // OK site last mod, section last mod. - // OK main sections - // OK taxonomies - // OK GetTerms - // OK Resource page - // OK Resource txt - - const files = ` --- hugo.toml -- -baseURL = "https://example.com" -title = "Smoke Site" -rssLimit = 3 -defaultContentLanguage = "en" -defaultContentLanguageInSubdir = true -enableRobotsTXT = true - -[pagination] -pagerSize = 1 - -[taxonomies] -category = 'categories' -tag = 'tags' - -[languages] -[languages.en] -weight = 1 -title = "In English" -[languages.no] -weight = 2 -title = "På norsk" - -[params] -hugo = "Rules!" - -[outputs] - home = ["html", "json", "rss"] --- layouts/index.html -- -Home: {{ .Lang}}|{{ .Kind }}|{{ .RelPermalink }}|{{ .Title }}|{{ .Content }}|Len Resources: {{ len .Resources }}|HTML -Resources: {{ range .Resources }}{{ .ResourceType }}|{{ .RelPermalink }}|{{ .MediaType }} - {{ end }}| -Site last mod: {{ site.Lastmod.Format "2006-02-01" }}| -Home last mod: {{ .Lastmod.Format "2006-02-01" }}| -Len Translations: {{ len .Translations }}| -Len home.RegularPagesRecursive: {{ len .RegularPagesRecursive }}| -RegularPagesRecursive: {{ range .RegularPagesRecursive }}{{ .RelPermalink }}|{{ end }}@ -Len site.RegularPages: {{ len site.RegularPages }}| -Len site.Pages: {{ len site.Pages }}| -Len site.AllPages: {{ len site.AllPages }}| -GetPage: {{ with .Site.GetPage "posts/p1" }}{{ .RelPermalink }}|{{ .Title }}{{ end }}| -RenderString with shortcode: {{ .RenderString "{{% hello %}}" }}| -Paginate: {{ .Paginator.PageNumber }}/{{ .Paginator.TotalPages }}| --- layouts/index.json -- -Home:{{ .Lang}}|{{ .Kind }}|{{ .RelPermalink }}|{{ .Title }}|{{ .Content }}|Len Resources: {{ len .Resources }}|JSON --- layouts/_default/list.html -- -List: {{ .Lang}}|{{ .Kind }}|{{ .RelPermalink }}|{{ .Title }}|{{ .Content }}|Len Resources: {{ len .Resources }}| -Resources: {{ range .Resources }}{{ .ResourceType }}|{{ .RelPermalink }}|{{ .MediaType }} - {{ end }} -Pages Length: {{ len .Pages }} -RegularPages Length: {{ len .RegularPages }} -RegularPagesRecursive Length: {{ len .RegularPagesRecursive }} -List last mod: {{ .Lastmod.Format "2006-02-01" }} -Background: {{ .Params.background }}| -Kind: {{ .Kind }} -Type: {{ .Type }} -Paginate: {{ .Paginator.PageNumber }}/{{ .Paginator.TotalPages }}| --- layouts/_default/single.html -- -Single: {{ .Lang}}|{{ .Kind }}|{{ .RelPermalink }}|{{ .Title }}|{{ .Content }}|Len Resources: {{ len .Resources }}|Background: {{ .Params.background }}| -Resources: {{ range .Resources }}{{ .ResourceType }}|{{ .RelPermalink }}|{{ .MediaType }}|{{ .Params }} - {{ end }} -{{ $textResource := .Resources.GetMatch "**.txt" }} -{{ with $textResource }} -Icon: {{ .Params.icon }}| -{{ $textResourceFingerprinted := . | fingerprint }} -Icon fingerprinted: {{ with $textResourceFingerprinted }}{{ .Params.icon }}|{{ .RelPermalink }}{{ end }}| -{{ end }} -NextInSection: {{ with .NextInSection }}{{ .RelPermalink }}|{{ .Title }}{{ end }}| -PrevInSection: {{ with .PrevInSection }}{{ .RelPermalink }}|{{ .Title }}{{ end }}| -GetTerms: {{ range .GetTerms "tags" }}name: {{ .Name }}, title: {{ .Title }}|{{ end }} --- layouts/shortcodes/hello.html -- -Hello. --- content/_index.md -- ---- -title: Home in English ---- -Home Content. --- content/_index.no.md -- ---- -title: Hjem -cascade: - - _target: - kind: page - path: /posts/** - background: post.jpg - - _target: - kind: term - background: term.jpg ---- -Hjem Innhold. --- content/posts/f1.txt -- -posts f1 text. --- content/posts/sub/f1.txt -- -posts sub f1 text. --- content/posts/p1/index.md -- -+++ -title = "Post 1" -lastMod = "2001-01-01" -tags = ["tag1"] -[[resources]] -src = '**' -[resources.params] -icon = 'enicon' -+++ -Content 1. --- content/posts/p1/index.no.md -- -+++ -title = "Post 1 no" -lastMod = "2002-02-02" -tags = ["tag1", "tag2"] -[[resources]] -src = '**' -[resources.params] -icon = 'noicon' -+++ -Content 1 no. --- content/posts/_index.md -- ---- -title: Posts ---- --- content/posts/p1/f1.txt -- -posts p1 f1 text. --- content/posts/p1/sub/ps1.md -- ---- -title: Post Sub 1 ---- -Content Sub 1. --- content/posts/p2.md -- ---- -title: Post 2 -tags: ["tag1", "tag3"] ---- -Content 2. --- content/posts/p2.no.md -- ---- -title: Post 2 No ---- -Content 2 No. --- content/tags/_index.md -- ---- -title: Tags ---- -Content Tags. --- content/tags/tag1/_index.md -- ---- -title: Tag 1 ---- -Content Tag 1. - - -` - - b := NewIntegrationTestBuilder(IntegrationTestConfig{ - T: t, - TxtarString: files, - NeedsOsFS: true, - // Verbose: true, - // LogLevel: logg.LevelTrace, - }).Build() - - b.AssertFileContent("public/en/index.html", - "Home: en|home|/en/|Home in English|

    Home Content.

    \n|HTML", - "Site last mod: 2001-01-01", - "Home last mod: 2001-01-01", - "Translations: 1|", - "Len home.RegularPagesRecursive: 2|", - "Len site.RegularPages: 2|", - "Len site.Pages: 8|", - "Len site.AllPages: 16|", - "GetPage: /en/posts/p1/|Post 1|", - "RenderString with shortcode: Hello.|", - "Paginate: 1/2|", - ) - b.AssertFileContent("public/en/page/2/index.html", "Paginate: 2/2|") - - b.AssertFileContent("public/no/index.html", - "Home: no|home|/no/|Hjem|

    Hjem Innhold.

    \n|HTML", - "Site last mod: 2002-02-02", - "Home last mod: 2002-02-02", - "Translations: 1", - "GetPage: /no/posts/p1/|Post 1 no|", - ) - - b.AssertFileContent("public/en/index.json", "Home:en|home|/en/|Home in English|

    Home Content.

    \n|JSON") - b.AssertFileContent("public/no/index.json", "Home:no|home|/no/|Hjem|

    Hjem Innhold.

    \n|JSON") - - b.AssertFileContent("public/en/posts/p1/index.html", - "Single: en|page|/en/posts/p1/|Post 1|

    Content 1.

    \n|Len Resources: 2|", - "Resources: text|/en/posts/p1/f1.txt|text/plain|map[icon:enicon] - page||application/octet-stream|map[draft:false iscjklanguage:false title:Post Sub 1] -", - "Icon: enicon", - "Icon fingerprinted: enicon|/en/posts/p1/f1.e5746577af5cbfc4f34c558051b7955a9a5a795a84f1c6ab0609cb3473a924cb.txt|", - "NextInSection: |\nPrevInSection: /en/posts/p2/|Post 2|", - "GetTerms: name: tag1, title: Tag 1|", - ) - - b.AssertFileContent("public/no/posts/p1/index.html", - "Resources: 1", - "Resources: text|/en/posts/p1/f1.txt|text/plain|map[icon:noicon] -", - "Icon: noicon", - "Icon fingerprinted: noicon|/en/posts/p1/f1.e5746577af5cbfc4f34c558051b7955a9a5a795a84f1c6ab0609cb3473a924cb.txt|", - "Background: post.jpg", - "NextInSection: |\nPrevInSection: /no/posts/p2/|Post 2 No|", - ) - - b.AssertFileContent("public/en/posts/index.html", - "List: en|section|/en/posts/|Posts||Len Resources: 2|", - "Resources: text|/en/posts/f1.txt|text/plain - text|/en/posts/sub/f1.txt|text/plain -", - "List last mod: 2001-01-01", - ) - - b.AssertFileContent("public/no/posts/index.html", - "List last mod: 2002-02-02", - ) - - b.AssertFileContent("public/en/posts/p2/index.html", "Single: en|page|/en/posts/p2/|Post 2|

    Content 2.

    \n|", - "|Len Resources: 0", - "GetTerms: name: tag1, title: Tag 1|name: tag3, title: Tag3|", - ) - b.AssertFileContent("public/no/posts/p2/index.html", "Single: no|page|/no/posts/p2/|Post 2 No|

    Content 2 No.

    \n|") - - b.AssertFileContent("public/no/categories/index.html", - "Kind: taxonomy", - "Type: categories", - ) - b.AssertFileContent("public/no/tags/index.html", - "Kind: taxonomy", - "Type: tags", - ) - - b.AssertFileContent("public/no/tags/tag1/index.html", - "Background: term.jpg", - "Kind: term", - "Type: tags", - "Paginate: 1/1|", - ) - - b.AssertFileContent("public/en/tags/tag1/index.html", - "Kind: term", - "Type: tags", - "Paginate: 1/2|", - ) -} - -// Basic tests that verifies that the different file systems work as expected. -func TestSmokeFilesystems(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.com" -defaultContentLanguage = "en" -defaultContentLanguageInSubdir = true -[languages] -[languages.en] -title = "In English" -[languages.nn] -title = "På nynorsk" -[module] -[[module.mounts]] -source = "i18n" -target = "i18n" -[[module.mounts]] -source = "data" -target = "data" -[[module.mounts]] -source = "content/en" -target = "content" -lang = "en" -[[module.mounts]] -source = "content/nn" -target = "content" -lang = "nn" -[[module.imports]] -path = "mytheme" --- layouts/index.html -- -i18n s1: {{ i18n "s1" }}| -i18n s2: {{ i18n "s2" }}| -data s1: {{ site.Data.d1.s1 }}| -data s2: {{ site.Data.d1.s2 }}| -title: {{ .Title }}| --- themes/mytheme/hugo.toml -- -[[module.mounts]] -source = "i18n" -target = "i18n" -[[module.mounts]] -source = "data" -target = "data" -# i18n files both project and theme. --- i18n/en.toml -- -[s1] -other = 's1project' --- i18n/nn.toml -- -[s1] -other = 's1prosjekt' --- themes/mytheme/i18n/en.toml -- -[s1] -other = 's1theme' -[s2] -other = 's2theme' -# data files both project and theme. --- data/d1.yaml -- -s1: s1project --- themes/mytheme/data/d1.yaml -- -s1: s1theme -s2: s2theme -# Content --- content/en/_index.md -- ---- -title: "Home" ---- --- content/nn/_index.md -- ---- -title: "Heim" ---- - -` - b := Test(t, files) - - b.AssertFileContent("public/en/index.html", - "i18n s1: s1project", "i18n s2: s2theme", - "data s1: s1project", "data s2: s2theme", - "title: Home", - ) - - b.AssertFileContent("public/nn/index.html", - "i18n s1: s1prosjekt", "i18n s2: s2theme", - "data s1: s1project", "data s2: s2theme", - "title: Heim", - ) -} - -// https://github.com/golang/go/issues/30286 -func TestDataRace(t *testing.T) { - const page = ` ---- -title: "The Page" -outputs: ["HTML", "JSON"] ---- - -The content. - - - ` - - b := newTestSitesBuilder(t).WithSimpleConfigFile() - for i := 1; i <= 50; i++ { - b.WithContent(fmt.Sprintf("blog/page%d.md", i), page) - } - - b.WithContent("_index.md", ` ---- -title: "The Home" -outputs: ["HTML", "JSON", "CSV", "RSS"] ---- - -The content. - - -`) - - commonTemplate := `{{ .Data.Pages }}` - - b.WithTemplatesAdded("_default/single.html", "HTML Single: "+commonTemplate) - b.WithTemplatesAdded("_default/list.html", "HTML List: "+commonTemplate) - - b.CreateSites().Build(BuildCfg{}) -} - -// This is just a test to verify that BenchmarkBaseline is working as intended. -func TestBenchmarkBaseline(t *testing.T) { - cfg := IntegrationTestConfig{ - T: t, - TxtarString: benchmarkBaselineFiles(true), - } - b := NewIntegrationTestBuilder(cfg).Build() - - b.Assert(len(b.H.Sites), qt.Equals, 4) - b.Assert(len(b.H.Sites[0].RegularPages()), qt.Equals, 161) - b.Assert(len(b.H.Sites[0].Pages()), qt.Equals, 197) - b.Assert(len(b.H.Sites[2].RegularPages()), qt.Equals, 158) - b.Assert(len(b.H.Sites[2].Pages()), qt.Equals, 194) -} - -func BenchmarkBaseline(b *testing.B) { - cfg := IntegrationTestConfig{ - T: b, - TxtarString: benchmarkBaselineFiles(false), - } - builders := make([]*IntegrationTestBuilder, b.N) - - for i := range builders { - builders[i] = NewIntegrationTestBuilder(cfg) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - builders[i].Build() - } -} - -func benchmarkBaselineFiles(leafBundles bool) string { - rnd := rand.New(rand.NewSource(32)) - - files := ` --- config.toml -- -baseURL = "https://example.com" -defaultContentLanguage = 'en' - -[module] -[[module.mounts]] -source = 'content/en' -target = 'content/en' -lang = 'en' -[[module.mounts]] -source = 'content/nn' -target = 'content/nn' -lang = 'nn' -[[module.mounts]] -source = 'content/no' -target = 'content/no' -lang = 'no' -[[module.mounts]] -source = 'content/sv' -target = 'content/sv' -lang = 'sv' -[[module.mounts]] -source = 'layouts' -target = 'layouts' - -[languages] -[languages.en] -title = "English" -weight = 1 -[languages.nn] -title = "Nynorsk" -weight = 2 -[languages.no] -title = "Norsk" -weight = 3 -[languages.sv] -title = "Svenska" -weight = 4 --- layouts/_default/list.html -- -{{ .Title }} -{{ .Content }} --- layouts/_default/single.html -- -{{ .Title }} -{{ .Content }} --- layouts/shortcodes/myshort.html -- -{{ .Inner }} -` - - contentTemplate := ` ---- -title: "Page %d" -date: "2018-01-01" -weight: %d ---- - -## Heading 1 - -Duis nisi reprehenderit nisi cupidatat cillum aliquip ea id eu esse commodo et. - -## Heading 2 - -Aliqua labore enim et sint anim amet excepteur ea dolore. - -{{< myshort >}} -Hello, World! -{{< /myshort >}} - -Aliqua labore enim et sint anim amet excepteur ea dolore. - - -` - - for _, lang := range []string{"en", "nn", "no", "sv"} { - files += fmt.Sprintf("\n-- content/%s/_index.md --\n"+contentTemplate, lang, 1, 1, 1) - for i, root := range []string{"", "foo", "bar", "baz"} { - for j, section := range []string{"posts", "posts/funny", "posts/science", "posts/politics", "posts/world", "posts/technology", "posts/world/news", "posts/world/news/europe"} { - n := i + j + 1 - files += fmt.Sprintf("\n-- content/%s/%s/%s/_index.md --\n"+contentTemplate, lang, root, section, n, n, n) - for k := 1; k < rnd.Intn(30)+1; k++ { - n := n + k - ns := fmt.Sprintf("%d", n) - if leafBundles { - ns = fmt.Sprintf("%d/index", n) - } - file := fmt.Sprintf("\n-- content/%s/%s/%s/p%s.md --\n"+contentTemplate, lang, root, section, ns, n, n) - files += file - } - } - } - } - - return files -} diff --git a/hugolib/hugolib_integration_test.go b/hugolib/hugolib_integration_test.go deleted file mode 100644 index 250c7bcec..000000000 --- a/hugolib/hugolib_integration_test.go +++ /dev/null @@ -1,147 +0,0 @@ -// 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 hugolib_test - -import ( - "strings" - "testing" - - "github.com/gohugoio/hugo/hugolib" -) - -// Issue 9073 -func TestPageTranslationsMap(t *testing.T) { - t.Parallel() - - files := ` --- config.toml -- -baseURL = 'https://example.org/' -title = 'Issue-9073' -defaultContentLanguageInSubdir = true - -[taxonomies] -tag = 'tags' - -[languages.en] -contentDir = 'content/en' -weight = 1 -disableKinds = ['RSS','sitemap'] - -[languages.de] -contentDir = 'content/de' -weight = 2 -disableKinds = ['home', 'page', 'section', 'taxonomy', 'term','RSS','sitemap'] --- content/de/posts/p1.md -- ---- -title: P1 -tags: ['T1'] ---- --- content/en/posts/p1.md -- ---- -title: P1 -tags: ['T1'] ---- --- layouts/_default/single.html -- -
      {{ range .AllTranslations }}
    • {{ .Title }}-{{ .Lang }}
    • {{ end }}
    --- layouts/_default/list.html -- -
      {{ range .AllTranslations }}
    • {{ .Title }}-{{ .Lang }}
    • {{ end }}
    - ` - - b := hugolib.NewIntegrationTestBuilder( - hugolib.IntegrationTestConfig{ - T: t, - TxtarString: files, - }, - ) - b.Build() - - // Kind home - b.AssertFileContent("public/en/index.html", - "
    • Issue-9073-en
    ", - ) - // Kind section - b.AssertFileContent("public/en/posts/index.html", - "
    • Posts-en
    ", - ) - // Kind page - b.AssertFileContent("public/en/posts/p1/index.html", - "
    • P1-en
    ", - ) - // Kind taxonomy - b.AssertFileContent("public/en/tags/index.html", - "
    • Tags-en
    ", - ) - // Kind term - b.AssertFileContent("public/en/tags/t1/index.html", - "
    • T1-en
    ", - ) -} - -// Issue #11538 -func TestRenderStringBadMarkupOpt(t *testing.T) { - t.Parallel() - - files := ` --- layouts/index.html -- -{{ $opts := dict "markup" "foo" }} -{{ "something" | .RenderString $opts }} - ` - - b := hugolib.NewIntegrationTestBuilder( - hugolib.IntegrationTestConfig{ - T: t, - TxtarString: files, - }, - ) - - _, err := b.BuildE() - - want := `no content renderer found for markup "foo"` - if !strings.Contains(err.Error(), want) { - t.Errorf("error msg must contain %q, error msg actually contains %q", want, err.Error()) - } -} - -// Issue #11547 -func TestTitleCaseStyleWithAutomaticSectionPages(t *testing.T) { - t.Parallel() - - files := ` --- config.toml -- -titleCaseStyle = 'none' --- content/books/book-1.md -- ---- -title: Book 1 -tags: [fiction] ---- --- content/films/_index.md -- ---- -title: Films ---- --- layouts/index.html -- -{{ (site.GetPage "/tags").Title }} -{{ (site.GetPage "/tags/fiction").Title }} -{{ (site.GetPage "/books").Title }} -{{ (site.GetPage "/films").Title }} - ` - - b := hugolib.NewIntegrationTestBuilder( - hugolib.IntegrationTestConfig{ - T: t, - TxtarString: files, - }, - ) - b.Build() - b.AssertFileContent("public/index.html", "tags\nfiction\nbooks\nFilms") -} diff --git a/hugolib/image_test.go b/hugolib/image_test.go deleted file mode 100644 index 09a5b841e..000000000 --- a/hugolib/image_test.go +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright 2019 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 hugolib - -import ( - "testing" -) - -func TestImageResizeMultilingual(t *testing.T) { - b := newTestSitesBuilder(t).WithConfigFile("toml", ` -baseURL="https://example.org" -defaultContentLanguage = "en" - -[languages] -[languages.en] -title = "Title in English" -languageName = "English" -weight = 1 -[languages.nn] -languageName = "Nynorsk" -weight = 2 -title = "Tittel på nynorsk" -[languages.nb] -languageName = "Bokmål" -weight = 3 -title = "Tittel på bokmål" -[languages.fr] -languageName = "French" -weight = 4 -title = "French Title" - -`) - - pageContent := `--- -title: "Page" ---- -` - - b.WithContent("bundle/index.md", pageContent) - b.WithContent("bundle/index.nn.md", pageContent) - b.WithContent("bundle/index.fr.md", pageContent) - b.WithSunset("content/bundle/sunset.jpg") - b.WithSunset("assets/images/sunset.jpg") - b.WithTemplates("index.html", ` -{{ with (.Site.GetPage "bundle" ) }} -{{ $sunset := .Resources.GetMatch "sunset*" }} -{{ if $sunset }} -{{ $resized := $sunset.Resize "200x200" }} -SUNSET FOR: {{ $.Site.Language.Lang }}: {{ $resized.RelPermalink }}/{{ $resized.Width }}/Lat: {{ $resized.Exif.Lat }} -{{ end }} -{{ else }} -No bundle for {{ $.Site.Language.Lang }} -{{ end }} - -{{ $sunset2 := resources.Get "images/sunset.jpg" }} -{{ $resized2 := $sunset2.Resize "123x234" }} -SUNSET2: {{ $resized2.RelPermalink }}/{{ $resized2.Width }}/Lat: {{ $resized2.Exif.Lat }} - - -`) - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", "SUNSET FOR: en: /bundle/sunset_hu_77061c65c31d2244.jpg/200/Lat: 36.59744166666667") - b.AssertFileContent("public/fr/index.html", "SUNSET FOR: fr: /bundle/sunset_hu_77061c65c31d2244.jpg/200/Lat: 36.59744166666667") - b.AssertFileContent("public/index.html", " SUNSET2: /images/sunset_hu_b52e3343ea6a8764.jpg/123/Lat: 36.59744166666667") - b.AssertFileContent("public/nn/index.html", " SUNSET2: /images/sunset_hu_b52e3343ea6a8764.jpg/123/Lat: 36.59744166666667") - - b.AssertImage(200, 200, "public/bundle/sunset_hu_77061c65c31d2244.jpg") - - // Check the file cache - b.AssertImage(200, 200, "resources/_gen/images/bundle/sunset_hu_77061c65c31d2244.jpg") - - b.AssertFileContent("resources/_gen/images/bundle/sunset_d209dcdc6b875e26.json", - "FocalLengthIn35mmFormat|uint16", "PENTAX") - - b.AssertFileContent("resources/_gen/images/images/sunset_d209dcdc6b875e26.json", - "FocalLengthIn35mmFormat|uint16", "PENTAX") - - b.AssertNoDuplicateWrites() -} diff --git a/hugolib/integrationtest_builder.go b/hugolib/integrationtest_builder.go deleted file mode 100644 index f28407fa1..000000000 --- a/hugolib/integrationtest_builder.go +++ /dev/null @@ -1,968 +0,0 @@ -package hugolib - -import ( - "bytes" - "context" - "encoding/base64" - "errors" - "fmt" - "io" - "math/rand" - "os" - "path/filepath" - "regexp" - "runtime" - "sort" - "strings" - "sync" - "testing" - - "github.com/bep/logg" - - qt "github.com/frankban/quicktest" - "github.com/fsnotify/fsnotify" - "github.com/gohugoio/hugo/common/herrors" - "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/deps" - "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/htesting" - "github.com/gohugoio/hugo/hugofs" - "github.com/spf13/afero" - "github.com/spf13/cast" - "golang.org/x/text/unicode/norm" - "golang.org/x/tools/txtar" -) - -type TestOpt func(*IntegrationTestConfig) - -// TestOptRunning will enable running in integration tests. -func TestOptRunning() TestOpt { - return func(c *IntegrationTestConfig) { - c.Running = true - } -} - -// TestOptWatching will enable watching in integration tests. -func TestOptWatching() TestOpt { - return func(c *IntegrationTestConfig) { - c.Watching = true - } -} - -// Enable tracing in integration tests. -// THis should only be used during development and not committed to the repo. -func TestOptTrace() TestOpt { - return func(c *IntegrationTestConfig) { - c.LogLevel = logg.LevelTrace - } -} - -// TestOptDebug will enable debug logging in integration tests. -func TestOptDebug() TestOpt { - return func(c *IntegrationTestConfig) { - c.LogLevel = logg.LevelDebug - } -} - -// TestOptInfo will enable info logging in integration tests. -func TestOptInfo() TestOpt { - return func(c *IntegrationTestConfig) { - c.LogLevel = logg.LevelInfo - } -} - -// TestOptWarn will enable warn logging in integration tests. -func TestOptWarn() TestOpt { - return func(c *IntegrationTestConfig) { - c.LogLevel = logg.LevelWarn - } -} - -// TestOptOsFs will enable the real file system in integration tests. -func TestOptOsFs() TestOpt { - return func(c *IntegrationTestConfig) { - c.NeedsOsFS = true - } -} - -// TestOptWithNFDOnDarwin will normalize the Unicode filenames to NFD on Darwin. -func TestOptWithNFDOnDarwin() TestOpt { - return func(c *IntegrationTestConfig) { - c.NFDFormOnDarwin = true - } -} - -// TestOptWithOSFs enables the real file system. -func TestOptWithOSFs() TestOpt { - return func(c *IntegrationTestConfig) { - c.NeedsOsFS = true - } -} - -func TestOptWithPrintAndKeepTempDir(b bool) TestOpt { - return func(c *IntegrationTestConfig) { - c.PrintAndKeepTempDir = b - } -} - -// TestOptWithWorkingDir allows setting any config optiona as a function al option. -func TestOptWithConfig(fn func(c *IntegrationTestConfig)) TestOpt { - return func(c *IntegrationTestConfig) { - fn(c) - } -} - -// Test is a convenience method to create a new IntegrationTestBuilder from some files and run a build. -func Test(t testing.TB, files string, opts ...TestOpt) *IntegrationTestBuilder { - cfg := IntegrationTestConfig{T: t, TxtarString: files} - for _, o := range opts { - o(&cfg) - } - return NewIntegrationTestBuilder(cfg).Build() -} - -// TestE is the same as Test, but returns an error instead of failing the test. -func TestE(t testing.TB, files string, opts ...TestOpt) (*IntegrationTestBuilder, error) { - cfg := IntegrationTestConfig{T: t, TxtarString: files} - for _, o := range opts { - o(&cfg) - } - return NewIntegrationTestBuilder(cfg).BuildE() -} - -// TestRunning is a convenience method to create a new IntegrationTestBuilder from some files with Running set to true and run a build. -// Deprecated: Use Test with TestOptRunning instead. -func TestRunning(t testing.TB, files string, opts ...TestOpt) *IntegrationTestBuilder { - cfg := IntegrationTestConfig{T: t, TxtarString: files, Running: true} - for _, o := range opts { - o(&cfg) - } - return NewIntegrationTestBuilder(cfg).Build() -} - -// In most cases you should not use this function directly, but the Test or TestRunning function. -func NewIntegrationTestBuilder(conf IntegrationTestConfig) *IntegrationTestBuilder { - // Code fences. - conf.TxtarString = strings.ReplaceAll(conf.TxtarString, "§§§", "```") - // Multiline strings. - conf.TxtarString = strings.ReplaceAll(conf.TxtarString, "§§", "`") - - data := txtar.Parse([]byte(conf.TxtarString)) - - if conf.NFDFormOnDarwin { - for i, f := range data.Files { - data.Files[i].Name = norm.NFD.String(f.Name) - } - } - - c, ok := conf.T.(*qt.C) - if !ok { - c = qt.New(conf.T) - } - - if conf.NeedsOsFS { - if !filepath.IsAbs(conf.WorkingDir) { - tempDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-integration-test") - c.Assert(err, qt.IsNil) - conf.WorkingDir = filepath.Join(tempDir, conf.WorkingDir) - if !conf.PrintAndKeepTempDir { - c.Cleanup(clean) - } else { - fmt.Println("\nUsing WorkingDir dir:", conf.WorkingDir) - } - } - } else if conf.WorkingDir == "" { - conf.WorkingDir = helpers.FilePathSeparator - } - - return &IntegrationTestBuilder{ - Cfg: conf, - C: c, - data: data, - } -} - -// IntegrationTestBuilder is a (partial) rewrite of sitesBuilder. -// The main problem with the "old" one was that it was that the test data was often a little hidden, -// so it became hard to look at a test and determine what it should do, especially coming back to the -// test after a year or so. -type IntegrationTestBuilder struct { - *qt.C - - data *txtar.Archive - - fs *hugofs.Fs - H *HugoSites - - Cfg IntegrationTestConfig - - changedFiles []string - createdFiles []string - removedFiles []string - renamedFiles []string - renamedDirs []string - - buildCount int - GCCount int - counters *buildCounters - logBuff lockingBuffer - lastBuildLog string - - builderInit sync.Once -} - -type lockingBuffer struct { - sync.Mutex - buf bytes.Buffer -} - -func (b *lockingBuffer) String() string { - b.Lock() - defer b.Unlock() - return b.buf.String() -} - -func (b *lockingBuffer) Reset() { - b.Lock() - defer b.Unlock() - b.buf.Reset() -} - -func (b *lockingBuffer) ReadFrom(r io.Reader) (n int64, err error) { - b.Lock() - n, err = b.buf.ReadFrom(r) - b.Unlock() - return -} - -func (b *lockingBuffer) Write(p []byte) (n int, err error) { - b.Lock() - n, err = b.buf.Write(p) - b.Unlock() - return -} - -// AssertLogContains asserts that the last build log contains the given strings. -// Each string can be negated with a "! " prefix. -func (s *IntegrationTestBuilder) AssertLogContains(els ...string) { - s.Helper() - for _, el := range els { - var negate bool - el, negate = s.negate(el) - check := qt.Contains - if negate { - check = qt.Not(qt.Contains) - } - s.Assert(s.lastBuildLog, check, el) - } -} - -// AssertLogMatches asserts that the last build log matches the given regular expressions. -// The regular expressions can be negated with a "! " prefix. -func (s *IntegrationTestBuilder) AssertLogMatches(expression string) { - s.Helper() - var negate bool - expression, negate = s.negate(expression) - re := regexp.MustCompile(expression) - checker := qt.IsTrue - if negate { - checker = qt.IsFalse - } - - s.Assert(re.MatchString(s.lastBuildLog), checker, qt.Commentf(s.lastBuildLog)) -} - -func (s *IntegrationTestBuilder) AssertBuildCountData(count int) { - s.Helper() - s.Assert(s.H.init.data.InitCount(), qt.Equals, count) -} - -func (s *IntegrationTestBuilder) AssertBuildCountGitInfo(count int) { - s.Helper() - s.Assert(s.H.init.gitInfo.InitCount(), qt.Equals, count) -} - -func (s *IntegrationTestBuilder) AssertFileCount(dirname string, expected int) { - s.Helper() - fs := s.fs.WorkingDirReadOnly - count := 0 - afero.Walk(fs, dirname, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if info.IsDir() { - return nil - } - count++ - return nil - }) - s.Assert(count, qt.Equals, expected) -} - -func (s *IntegrationTestBuilder) negate(match string) (string, bool) { - var negate bool - if strings.HasPrefix(match, "! ") { - negate = true - match = strings.TrimPrefix(match, "! ") - } - return match, negate -} - -func (s *IntegrationTestBuilder) AssertFileContent(filename string, matches ...string) { - s.Helper() - content := strings.TrimSpace(s.FileContent(filename)) - - for _, m := range matches { - cm := qt.Commentf("File: %s Match %s\nContent:\n%s", filename, m, content) - lines := strings.Split(m, "\n") - for _, match := range lines { - match = strings.TrimSpace(match) - if match == "" || strings.HasPrefix(match, "#") { - continue - } - var negate bool - match, negate = s.negate(match) - if negate { - s.Assert(content, qt.Not(qt.Contains), match, cm) - continue - } - s.Assert(content, qt.Contains, match, cm) - } - } -} - -func (s *IntegrationTestBuilder) AssertFileContentEquals(filename string, match string) { - s.Helper() - content := s.FileContent(filename) - s.Assert(content, qt.Equals, match, qt.Commentf(match)) -} - -func (s *IntegrationTestBuilder) AssertFileContentExact(filename string, matches ...string) { - s.Helper() - content := s.FileContent(filename) - for _, m := range matches { - cm := qt.Commentf("File: %s Match %s\nContent:\n%s", filename, m, content) - s.Assert(content, qt.Contains, m, cm) - } -} - -func (s *IntegrationTestBuilder) AssertNoRenderShortcodesArtifacts() { - s.Helper() - for _, p := range s.H.Pages() { - content, err := p.Content(context.Background()) - s.Assert(err, qt.IsNil) - comment := qt.Commentf("Page: %s\n%s", p.Path(), content) - s.Assert(strings.Contains(cast.ToString(content), "__hugo_ctx"), qt.IsFalse, comment) - } -} - -func (s *IntegrationTestBuilder) AssertPublishDir(matches ...string) { - s.AssertFs(s.fs.PublishDir, matches...) -} - -func (s *IntegrationTestBuilder) AssertFs(fs afero.Fs, matches ...string) { - s.Helper() - var buff bytes.Buffer - s.Assert(s.printAndCheckFs(fs, "", &buff), qt.IsNil) - printFsLines := strings.Split(buff.String(), "\n") - sort.Strings(printFsLines) - content := strings.TrimSpace((strings.Join(printFsLines, "\n"))) - for _, m := range matches { - cm := qt.Commentf("Match: %q\nIn:\n%s", m, content) - lines := strings.Split(m, "\n") - for _, match := range lines { - match = strings.TrimSpace(match) - var negate bool - if strings.HasPrefix(match, "! ") { - negate = true - match = strings.TrimPrefix(match, "! ") - } - if negate { - s.Assert(content, qt.Not(qt.Contains), match, cm) - continue - } - s.Assert(content, qt.Contains, match, cm) - } - } -} - -func (s *IntegrationTestBuilder) printAndCheckFs(fs afero.Fs, path string, w io.Writer) error { - if fs == nil { - return nil - } - - return afero.Walk(fs, path, func(path string, info os.FileInfo, err error) error { - if err != nil { - return fmt.Errorf("error: path %q: %s", path, err) - } - path = filepath.ToSlash(path) - if path == "" { - path = "." - } - if !info.IsDir() { - f, err := fs.Open(path) - if err != nil { - return fmt.Errorf("error: path %q: %s", path, err) - } - defer f.Close() - // This will panic if the file is a directory. - var buf [1]byte - io.ReadFull(f, buf[:]) - } - fmt.Fprintln(w, path, info.IsDir()) - return nil - }) -} - -func (s *IntegrationTestBuilder) AssertFileExists(filename string, b bool) { - checker := qt.IsNil - if !b { - checker = qt.IsNotNil - } - _, err := s.fs.WorkingDirReadOnly.Stat(filename) - if !herrors.IsNotExist(err) { - s.Assert(err, qt.IsNil) - } - s.Assert(err, checker) -} - -func (s *IntegrationTestBuilder) AssertIsFileError(err error) herrors.FileError { - s.Assert(err, qt.ErrorAs, new(herrors.FileError)) - return herrors.UnwrapFileError(err) -} - -func (s *IntegrationTestBuilder) AssertRenderCountContent(count int) { - s.Helper() - s.Assert(s.counters.contentRenderCounter.Load(), qt.Equals, uint64(count)) -} - -func (s *IntegrationTestBuilder) AssertRenderCountPage(count int) { - s.Helper() - s.Assert(s.counters.pageRenderCounter.Load(), qt.Equals, uint64(count)) -} - -func (s *IntegrationTestBuilder) AssertRenderCountPageBetween(from, to int) { - s.Helper() - i := int(s.counters.pageRenderCounter.Load()) - s.Assert(i >= from && i <= to, qt.IsTrue) -} - -func (s *IntegrationTestBuilder) Build() *IntegrationTestBuilder { - s.Helper() - _, err := s.BuildE() - if s.Cfg.Verbose || err != nil { - fmt.Println(s.lastBuildLog) - if s.H != nil && err == nil { - for _, s := range s.H.Sites { - m := s.pageMap - var buff bytes.Buffer - fmt.Fprintf(&buff, "PageMap for site %q\n\n", s.Language().Lang) - m.debugPrint("", 999, &buff) - fmt.Println(buff.String()) - } - } - } else if s.Cfg.LogLevel <= logg.LevelDebug { - fmt.Println(s.lastBuildLog) - } - s.Assert(err, qt.IsNil) - if s.Cfg.RunGC { - s.GCCount, err = s.H.GC() - s.Assert(err, qt.IsNil) - } - - s.Cleanup(func() { - if h := s.H; h != nil { - s.Assert(h.Close(), qt.IsNil) - } - }) - - return s -} - -func (s *IntegrationTestBuilder) BuildPartial(urls ...string) *IntegrationTestBuilder { - if _, err := s.BuildPartialE(urls...); err != nil { - s.Fatal(err) - } - return s -} - -func (s *IntegrationTestBuilder) BuildPartialE(urls ...string) (*IntegrationTestBuilder, error) { - if s.buildCount == 0 { - panic("BuildPartial can only be used after a full build") - } - if !s.Cfg.Running { - panic("BuildPartial can only be used in server mode") - } - visited := types.NewEvictingQueue[string](len(urls)) - for _, url := range urls { - visited.Add(url) - } - buildCfg := BuildCfg{RecentlyTouched: visited, PartialReRender: true} - return s, s.build(buildCfg) -} - -func (s *IntegrationTestBuilder) Close() { - s.Helper() - s.Assert(s.H.Close(), qt.IsNil) -} - -func (s *IntegrationTestBuilder) LogString() string { - return s.lastBuildLog -} - -func (s *IntegrationTestBuilder) BuildE() (*IntegrationTestBuilder, error) { - s.Helper() - if err := s.initBuilder(); err != nil { - return s, err - } - - err := s.build(s.Cfg.BuildCfg) - return s, err -} - -func (s *IntegrationTestBuilder) Init() *IntegrationTestBuilder { - if err := s.initBuilder(); err != nil { - s.Fatalf("Failed to init builder: %s", err) - } - s.lastBuildLog = s.logBuff.String() - return s -} - -type IntegrationTestDebugConfig struct { - Out io.Writer - - PrintDestinationFs bool - PrintPagemap bool - - PrefixDestinationFs string - PrefixPagemap string -} - -func (s *IntegrationTestBuilder) EditFileReplaceAll(filename, old, new string) *IntegrationTestBuilder { - return s.EditFileReplaceFunc(filename, func(s string) string { - return strings.ReplaceAll(s, old, new) - }) -} - -func (s *IntegrationTestBuilder) EditFileReplaceFunc(filename string, replacementFunc func(s string) string) *IntegrationTestBuilder { - absFilename := s.absFilename(filename) - b, err := afero.ReadFile(s.fs.Source, absFilename) - s.Assert(err, qt.IsNil) - s.changedFiles = append(s.changedFiles, absFilename) - oldContent := string(b) - s.writeSource(absFilename, replacementFunc(oldContent)) - return s -} - -func (s *IntegrationTestBuilder) EditFiles(filenameContent ...string) *IntegrationTestBuilder { - for i := 0; i < len(filenameContent); i += 2 { - filename, content := filepath.FromSlash(filenameContent[i]), filenameContent[i+1] - absFilename := s.absFilename(filename) - s.changedFiles = append(s.changedFiles, absFilename) - s.writeSource(absFilename, content) - } - return s -} - -func (s *IntegrationTestBuilder) AddFiles(filenameContent ...string) *IntegrationTestBuilder { - for i := 0; i < len(filenameContent); i += 2 { - filename, content := filepath.FromSlash(filenameContent[i]), filenameContent[i+1] - absFilename := s.absFilename(filename) - s.createdFiles = append(s.createdFiles, absFilename) - s.writeSource(absFilename, content) - } - return s -} - -func (s *IntegrationTestBuilder) RemoveFiles(filenames ...string) *IntegrationTestBuilder { - for _, filename := range filenames { - absFilename := s.absFilename(filename) - s.removedFiles = append(s.removedFiles, absFilename) - s.Assert(s.fs.Source.Remove(absFilename), qt.IsNil) - - } - - return s -} - -func (s *IntegrationTestBuilder) RenameFile(old, new string) *IntegrationTestBuilder { - absOldFilename := s.absFilename(old) - absNewFilename := s.absFilename(new) - s.renamedFiles = append(s.renamedFiles, absOldFilename) - s.createdFiles = append(s.createdFiles, absNewFilename) - s.Assert(s.fs.Source.MkdirAll(filepath.Dir(absNewFilename), 0o777), qt.IsNil) - s.Assert(s.fs.Source.Rename(absOldFilename, absNewFilename), qt.IsNil) - return s -} - -func (s *IntegrationTestBuilder) RenameDir(old, new string) *IntegrationTestBuilder { - absOldFilename := s.absFilename(old) - absNewFilename := s.absFilename(new) - s.renamedDirs = append(s.renamedDirs, absOldFilename) - s.changedFiles = append(s.changedFiles, absNewFilename) - afero.Walk(s.fs.Source, absOldFilename, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if info.IsDir() { - return nil - } - s.createdFiles = append(s.createdFiles, strings.Replace(path, absOldFilename, absNewFilename, 1)) - return nil - }) - s.Assert(s.fs.Source.MkdirAll(filepath.Dir(absNewFilename), 0o777), qt.IsNil) - s.Assert(s.fs.Source.Rename(absOldFilename, absNewFilename), qt.IsNil) - return s -} - -func (s *IntegrationTestBuilder) FileContent(filename string) string { - s.Helper() - return s.readWorkingDir(s, s.fs, filepath.FromSlash(filename)) -} - -func (s *IntegrationTestBuilder) initBuilder() error { - var initErr error - s.builderInit.Do(func() { - var afs afero.Fs - if s.Cfg.NeedsOsFS { - afs = afero.NewOsFs() - } else { - afs = afero.NewMemMapFs() - } - - if s.Cfg.LogLevel == 0 { - s.Cfg.LogLevel = logg.LevelError - } - - isBinaryRe := regexp.MustCompile(`^(.*)(\.png|\.jpg)$`) - - const dataSourceFilenamePrefix = "sourcefilename:" - - for _, f := range s.data.Files { - filename := filepath.Join(s.Cfg.WorkingDir, f.Name) - data := bytes.TrimSuffix(f.Data, []byte("\n")) - datastr := strings.TrimSpace(string(data)) - if strings.HasPrefix(datastr, dataSourceFilenamePrefix) { - // Read from file relative to the current dir. - var err error - wd, _ := os.Getwd() - filename := filepath.Join(wd, strings.TrimSpace(strings.TrimPrefix(datastr, dataSourceFilenamePrefix))) - data, err = os.ReadFile(filename) - s.Assert(err, qt.IsNil) - } else if isBinaryRe.MatchString(filename) { - var err error - data, err = base64.StdEncoding.DecodeString(string(data)) - s.Assert(err, qt.IsNil) - - } - s.Assert(afs.MkdirAll(filepath.Dir(filename), 0o777), qt.IsNil) - s.Assert(afero.WriteFile(afs, filename, data, 0o666), qt.IsNil) - } - - configDir := "config" - if _, err := afs.Stat(filepath.Join(s.Cfg.WorkingDir, "config")); err != nil { - configDir = "" - } - - var flags config.Provider - if s.Cfg.BaseCfg != nil { - flags = s.Cfg.BaseCfg - } else { - flags = config.New() - } - - if s.Cfg.Running { - flags.Set("internal", maps.Params{ - "running": s.Cfg.Running, - "watch": s.Cfg.Running, - }) - } else if s.Cfg.Watching { - flags.Set("internal", maps.Params{ - "watch": s.Cfg.Watching, - }) - } - - if s.Cfg.WorkingDir != "" { - flags.Set("workingDir", s.Cfg.WorkingDir) - } - - var w io.Writer - if s.Cfg.LogLevel == logg.LevelTrace { - w = os.Stdout - } else { - w = &s.logBuff - } - - logger := loggers.New( - loggers.Options{ - StdOut: w, - StdErr: w, - Level: s.Cfg.LogLevel, - DistinctLevel: logg.LevelWarn, - }, - ) - - res, err := allconfig.LoadConfig( - allconfig.ConfigSourceDescriptor{ - Flags: flags, - ConfigDir: configDir, - Fs: afs, - Logger: logger, - Environ: s.Cfg.Environ, - }, - ) - if err != nil { - initErr = err - return - } - - fs := hugofs.NewFrom(afs, res.LoadingInfo.BaseConfig) - - s.Assert(err, qt.IsNil) - - depsCfg := deps.DepsCfg{Configs: res, Fs: fs, LogLevel: logger.Level(), StdErr: logger.StdErr()} - sites, err := NewHugoSites(depsCfg) - if err != nil { - initErr = err - return - } - if sites == nil { - initErr = errors.New("no sites") - return - } - - s.H = sites - s.fs = fs - - if s.Cfg.NeedsNpmInstall { - wd, _ := os.Getwd() - s.Assert(os.Chdir(s.Cfg.WorkingDir), qt.IsNil) - s.C.Cleanup(func() { os.Chdir(wd) }) - sc := security.DefaultConfig - sc.Exec.Allow, err = security.NewWhitelist("npm") - s.Assert(err, qt.IsNil) - ex := hexec.New(sc, s.Cfg.WorkingDir, loggers.NewDefault()) - command, err := ex.New("npm", "install") - s.Assert(err, qt.IsNil) - s.Assert(command.Run(), qt.IsNil) - - } - }) - - return initErr -} - -func (s *IntegrationTestBuilder) absFilename(filename string) string { - filename = filepath.FromSlash(filename) - if filepath.IsAbs(filename) { - return filename - } - if s.Cfg.WorkingDir != "" && !strings.HasPrefix(filename, s.Cfg.WorkingDir) { - filename = filepath.Join(s.Cfg.WorkingDir, filename) - } - return filename -} - -func (s *IntegrationTestBuilder) reset() { - s.changedFiles = nil - s.createdFiles = nil - s.removedFiles = nil - s.renamedFiles = nil -} - -func (s *IntegrationTestBuilder) build(cfg BuildCfg) error { - s.Helper() - defer func() { - s.reset() - s.lastBuildLog = s.logBuff.String() - s.logBuff.Reset() - }() - - changeEvents := s.changeEvents() - s.counters = &buildCounters{} - cfg.testCounters = s.counters - - s.buildCount++ - - err := s.H.Build(cfg, changeEvents...) - if err != nil { - return err - } - - return nil -} - -// We simulate the fsnotify events. -// See the test output in https://github.com/bep/fsnotifyeventlister for what events gets produced -// by the different OSes. -func (s *IntegrationTestBuilder) changeEvents() []fsnotify.Event { - var ( - events []fsnotify.Event - isLinux = runtime.GOOS == "linux" - isMacOs = runtime.GOOS == "darwin" - isWindows = runtime.GOOS == "windows" - ) - - for _, v := range s.removedFiles { - events = append(events, fsnotify.Event{ - Name: v, - Op: fsnotify.Remove, - }) - } - for _, v := range s.renamedFiles { - events = append(events, fsnotify.Event{ - Name: v, - Op: fsnotify.Rename, - }) - } - - for _, v := range s.renamedDirs { - events = append(events, fsnotify.Event{ - Name: v, - // This is what we get on MacOS. - Op: fsnotify.Remove | fsnotify.Rename, - }) - } - - for _, v := range s.changedFiles { - events = append(events, fsnotify.Event{ - Name: v, - Op: fsnotify.Write, - }) - if isLinux || isWindows { - // Duplicate write events, for some reason. - events = append(events, fsnotify.Event{ - Name: v, - Op: fsnotify.Write, - }) - } - if isMacOs { - events = append(events, fsnotify.Event{ - Name: v, - Op: fsnotify.Chmod, - }) - } - } - for _, v := range s.createdFiles { - events = append(events, fsnotify.Event{ - Name: v, - Op: fsnotify.Create, - }) - if isLinux || isWindows { - events = append(events, fsnotify.Event{ - Name: v, - Op: fsnotify.Write, - }) - } - - } - - // Shuffle events. - for i := range events { - j := rand.Intn(i + 1) - events[i], events[j] = events[j], events[i] - } - - return events -} - -func (s *IntegrationTestBuilder) readWorkingDir(t testing.TB, fs *hugofs.Fs, filename string) string { - t.Helper() - return s.readFileFromFs(t, fs.WorkingDirReadOnly, filename) -} - -func (s *IntegrationTestBuilder) readFileFromFs(t testing.TB, fs afero.Fs, filename string) string { - t.Helper() - filename = filepath.Clean(filename) - b, err := afero.ReadFile(fs, filename) - if err != nil { - // Print some debug info - hadSlash := strings.HasPrefix(filename, helpers.FilePathSeparator) - start := 0 - if hadSlash { - start = 1 - } - end := start + 1 - - parts := strings.Split(filename, helpers.FilePathSeparator) - if parts[start] == "work" { - end++ - } - - s.Assert(err, qt.IsNil) - - } - return string(b) -} - -func (s *IntegrationTestBuilder) writeSource(filename, content string) { - s.Helper() - s.writeToFs(s.fs.Source, filename, content) -} - -func (s *IntegrationTestBuilder) writeToFs(fs afero.Fs, filename, content string) { - s.Helper() - if err := afero.WriteFile(fs, filepath.FromSlash(filename), []byte(content), 0o755); err != nil { - s.Fatalf("Failed to write file: %s", err) - } -} - -type IntegrationTestConfig struct { - T testing.TB - - // The files to use on txtar format, see - // https://pkg.go.dev/golang.org/x/exp/cmd/txtar - // There are some contentions used in this test setup. - // - §§§ can be used to wrap code fences. - // - §§ can be used to wrap multiline strings. - // - filenames prefixed with sourcefilename: will be read from the file system relative to the current dir. - // - filenames with a .png or .jpg extension will be treated as binary and base64 decoded. - TxtarString string - - // COnfig to use as the base. We will also read the config from the txtar. - BaseCfg config.Provider - - // Environment variables passed to the config loader. - Environ []string - - // Whether to simulate server mode. - Running bool - - // Watch for changes. - // This is (currently) always set to true when Running is set. - // Note that the CLI for the server does allow for --watch=false, but that is not used in these test. - Watching bool - - // Will print the log buffer after the build - Verbose bool - - // The log level to use. - LogLevel logg.Level - - // Whether it needs the real file system (e.g. for js.Build tests). - NeedsOsFS bool - - // Whether to run GC after each build. - RunGC bool - - // Do not remove the temp dir after the test. - PrintAndKeepTempDir bool - - // Whether to run npm install before Build. - NeedsNpmInstall bool - - // Whether to normalize the Unicode filenames to NFD on Darwin. - NFDFormOnDarwin bool - - // The working dir to use. If not absolute, a temp dir will be created. - WorkingDir string - - // The config to pass to Build. - BuildCfg BuildCfg -} diff --git a/hugolib/language_content_dir_test.go b/hugolib/language_content_dir_test.go deleted file mode 100644 index e02e118f5..000000000 --- a/hugolib/language_content_dir_test.go +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2019 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 hugolib - -import ( - "testing" -) - -func TestLanguageContentRoot(t *testing.T) { - files := ` --- hugo.toml -- -baseURL = "https://example.org/" -defaultContentLanguage = "en" -defaultContentLanguageInSubdir = true -[languages] -[languages.en] -weight = 10 -contentDir = "content/en" -[languages.nn] -weight = 20 -contentDir = "content/nn" --- content/en/_index.md -- ---- -title: "Home" ---- --- content/nn/_index.md -- ---- -title: "Heim" ---- --- content/en/myfiles/file1.txt -- -file 1 en --- content/en/myfiles/file2.txt -- -file 2 en --- content/nn/myfiles/file1.txt -- -file 1 nn --- layouts/index.html -- -Title: {{ .Title }}| -Len Resources: {{ len .Resources }}| -{{ range $i, $e := .Resources }} -{{ $i }}|{{ .RelPermalink }}|{{ .Content }}| -{{ end }} - -` - b := Test(t, files) - b.AssertFileContent("public/en/index.html", "Home", "0|/en/myfiles/file1.txt|file 1 en|\n\n1|/en/myfiles/file2.txt|file 2 en|") - b.AssertFileContent("public/nn/index.html", "Heim", "0|/nn/myfiles/file1.txt|file 1 nn|\n\n1|/en/myfiles/file2.txt|file 2 en|") -} diff --git a/hugolib/language_test.go b/hugolib/language_test.go deleted file mode 100644 index f7dd5b79d..000000000 --- a/hugolib/language_test.go +++ /dev/null @@ -1,144 +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 hugolib - -import ( - "fmt" - "strings" - "testing" - - "github.com/gohugoio/hugo/htesting" - - qt "github.com/frankban/quicktest" -) - -func TestI18n(t *testing.T) { - c := qt.New(t) - - // https://github.com/gohugoio/hugo/issues/7804 - c.Run("pt-br should be case insensitive", func(c *qt.C) { - b := newTestSitesBuilder(c) - langCode := func() string { - c := "pt-br" - if htesting.RandBool() { - c = strings.ToUpper(c) - } - return c - } - - b.WithConfigFile(`toml`, fmt.Sprintf(` -baseURL = "https://example.com" -defaultContentLanguage = "%s" - -[languages] -[languages.%s] -weight = 1 -`, langCode(), langCode())) - - b.WithI18n(fmt.Sprintf("i18n/%s.toml", langCode()), `hello.one = "Hello"`) - b.WithTemplates("index.html", `Hello: {{ i18n "hello" 1 }}`) - b.WithContent("p1.md", "") - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", "Hello: Hello") - }) -} - -func TestLanguageBugs(t *testing.T) { - c := qt.New(t) - - // Issue #8672 - c.Run("Config with language, menu in root only", func(c *qt.C) { - b := newTestSitesBuilder(c) - b.WithConfigFile("toml", ` -theme = "test-theme" -[[menus.foo]] -name = "foo-a" -[languages.en] - -`, - ) - - b.WithThemeConfigFile("toml", `[languages.en]`) - - b.Build(BuildCfg{}) - - menus := b.H.Sites[0].Menus() - c.Assert(menus, qt.HasLen, 1) - }) -} - -func TestLanguageNumberFormatting(t *testing.T) { - b := newTestSitesBuilder(t) - b.WithConfigFile("toml", ` -baseURL = "https://example.org" - -defaultContentLanguage = "en" -defaultContentLanguageInSubDir = true - -[languages] -[languages.en] -timeZone="UTC" -weight=10 -[languages.nn] -weight=20 - -`) - - b.WithTemplates("index.html", ` - -FormatNumber: {{ 512.5032 | lang.FormatNumber 2 }} -FormatPercent: {{ 512.5032 | lang.FormatPercent 2 }} -FormatCurrency: {{ 512.5032 | lang.FormatCurrency 2 "USD" }} -FormatAccounting: {{ 512.5032 | lang.FormatAccounting 2 "NOK" }} -FormatNumberCustom: {{ lang.FormatNumberCustom 2 12345.6789 }} - - - - -`) - b.WithContent("p1.md", "") - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/en/index.html", ` -FormatNumber: 512.50 -FormatPercent: 512.50% -FormatCurrency: $512.50 -FormatAccounting: NOK512.50 -FormatNumberCustom: 12,345.68 - -`, - ) - - b.AssertFileContent("public/nn/index.html", ` -FormatNumber: 512,50 -FormatPercent: 512,50 % -FormatCurrency: 512,50 USD -FormatAccounting: 512,50 kr -FormatNumberCustom: 12,345.68 - -`) -} - -// Issue 11993. -func TestI18nDotFile(t *testing.T) { - files := ` --- hugo.toml --{} -baseURL = "https://example.com" --- i18n/.keep -- --- data/.keep -- -` - Test(t, files) -} diff --git a/hugolib/media.go b/hugolib/media.go new file mode 100644 index 000000000..aae9a7870 --- /dev/null +++ b/hugolib/media.go @@ -0,0 +1,60 @@ +// Copyright 2015 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 hugolib + +// An Image contains metadata for images + image sitemaps +// https://support.google.com/webmasters/answer/178636?hl=en +type Image struct { + + // The URL of the image. In some cases, the image URL may not be on the + // same domain as your main site. This is fine, as long as both domains + // are verified in Webmaster Tools. If, for example, you use a + // content delivery network (CDN) to host your images, make sure that the + // hosting site is verified in Webmaster Tools OR that you submit your + // sitemap using robots.txt. In addition, make sure that your robots.txt + // file doesn’t disallow the crawling of any content you want indexed. + URL string + Title string + Caption string + AltText string + + // The geographic location of the image. For example, + // Limerick, Ireland. + GeoLocation string + + // A URL to the license of the image. + License string +} + +// A Video contains metadata for videos + video sitemaps +// https://support.google.com/webmasters/answer/80471?hl=en +type Video struct { + ThumbnailLoc string + Title string + Description string + ContentLoc string + PlayerLoc string + Duration string + ExpirationDate string + Rating string + ViewCount string + PublicationDate string + FamilyFriendly string + Restriction string + GalleryLoc string + Price string + RequiresSubscription string + Uploader string + Live string +} diff --git a/hugolib/menu.go b/hugolib/menu.go new file mode 100644 index 000000000..4f6bd2b4e --- /dev/null +++ b/hugolib/menu.go @@ -0,0 +1,215 @@ +// Copyright 2015 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 hugolib + +import ( + "html/template" + "sort" + "strings" + + "github.com/spf13/cast" +) + +// MenuEntry represents a menu item defined in either Page front matter +// or in the site config. +type MenuEntry struct { + URL string + Name string + Menu string + Identifier string + Pre template.HTML + Post template.HTML + Weight int + Parent string + Children Menu +} + +// Menu is a collection of menu entries. +type Menu []*MenuEntry + +// Menus is a dictionary of menus. +type Menus map[string]*Menu + +// PageMenus is a dictionary of menus defined in the Pages. +type PageMenus map[string]*MenuEntry + +// addChild adds a new child to this menu entry. +// The default sort order will then be applied. +func (m *MenuEntry) addChild(child *MenuEntry) { + m.Children = append(m.Children, child) + m.Children.Sort() +} + +// HasChildren returns whether this menu item has any children. +func (m *MenuEntry) HasChildren() bool { + return m.Children != nil +} + +// KeyName returns the key used to identify this menu entry. +func (m *MenuEntry) KeyName() string { + if m.Identifier != "" { + return m.Identifier + } + return m.Name +} + +func (m *MenuEntry) hopefullyUniqueID() string { + if m.Identifier != "" { + return m.Identifier + } else if m.URL != "" { + return m.URL + } else { + return m.Name + } +} + +// IsEqual returns whether the two menu entries represents the same menu entry. +func (m *MenuEntry) IsEqual(inme *MenuEntry) bool { + return m.hopefullyUniqueID() == inme.hopefullyUniqueID() && m.Parent == inme.Parent +} + +// IsSameResource returns whether the two menu entries points to the same +// resource (URL). +func (m *MenuEntry) IsSameResource(inme *MenuEntry) bool { + return m.URL != "" && inme.URL != "" && m.URL == inme.URL +} + +func (m *MenuEntry) marshallMap(ime map[string]interface{}) { + for k, v := range ime { + loki := strings.ToLower(k) + switch loki { + case "url": + m.URL = cast.ToString(v) + case "weight": + m.Weight = cast.ToInt(v) + case "name": + m.Name = cast.ToString(v) + case "pre": + m.Pre = template.HTML(cast.ToString(v)) + case "post": + m.Post = template.HTML(cast.ToString(v)) + case "identifier": + m.Identifier = cast.ToString(v) + case "parent": + m.Parent = cast.ToString(v) + } + } +} + +func (m Menu) add(me *MenuEntry) Menu { + app := func(slice Menu, x ...*MenuEntry) Menu { + n := len(slice) + len(x) + if n > cap(slice) { + size := cap(slice) * 2 + if size < n { + size = n + } + new := make(Menu, size) + copy(new, slice) + slice = new + } + slice = slice[0:n] + copy(slice[n-len(x):], x) + return slice + } + + m = app(m, me) + m.Sort() + return m +} + +/* + * Implementation of a custom sorter for Menu + */ + +// A type to implement the sort interface for Menu +type menuSorter struct { + menu Menu + by menuEntryBy +} + +// Closure used in the Sort.Less method. +type menuEntryBy func(m1, m2 *MenuEntry) bool + +func (by menuEntryBy) Sort(menu Menu) { + ms := &menuSorter{ + menu: menu, + by: by, // The Sort method's receiver is the function (closure) that defines the sort order. + } + sort.Stable(ms) +} + +var defaultMenuEntrySort = func(m1, m2 *MenuEntry) bool { + if m1.Weight == m2.Weight { + if m1.Name == m2.Name { + return m1.Identifier < m2.Identifier + } + return m1.Name < m2.Name + } + + if m2.Weight == 0 { + return true + } + + if m1.Weight == 0 { + return false + } + + return m1.Weight < m2.Weight +} + +func (ms *menuSorter) Len() int { return len(ms.menu) } +func (ms *menuSorter) Swap(i, j int) { ms.menu[i], ms.menu[j] = ms.menu[j], ms.menu[i] } + +// Less is part of sort.Interface. It is implemented by calling the "by" closure in the sorter. +func (ms *menuSorter) Less(i, j int) bool { return ms.by(ms.menu[i], ms.menu[j]) } + +// Sort sorts the menu by weight, name and then by identifier. +func (m Menu) Sort() Menu { + menuEntryBy(defaultMenuEntrySort).Sort(m) + return m +} + +// Limit limits the returned menu to n entries. +func (m Menu) Limit(n int) Menu { + if len(m) > n { + return m[0:n] + } + return m +} + +// ByWeight sorts the menu by the weight defined in the menu configuration. +func (m Menu) ByWeight() Menu { + menuEntryBy(defaultMenuEntrySort).Sort(m) + return m +} + +// ByName sorts the menu by the name defined in the menu configuration. +func (m Menu) ByName() Menu { + title := func(m1, m2 *MenuEntry) bool { + return m1.Name < m2.Name + } + + menuEntryBy(title).Sort(m) + return m +} + +// Reverse reverses the order of the menu entries. +func (m Menu) Reverse() Menu { + for i, j := 0, len(m)-1; i < j; i, j = i+1, j-1 { + m[i], m[j] = m[j], m[i] + } + + return m +} diff --git a/hugolib/menu_old_test.go b/hugolib/menu_old_test.go new file mode 100644 index 000000000..7c49ed908 --- /dev/null +++ b/hugolib/menu_old_test.go @@ -0,0 +1,642 @@ +// Copyright 2016 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 hugolib + +// TODO(bep) remove this file when the reworked tests in menu_test.go is done. +// NOTE: Do not add more tests to this file! + +import ( + "fmt" + "strings" + "testing" + + "github.com/gohugoio/hugo/deps" + + "path/filepath" + + "github.com/BurntSushi/toml" + "github.com/gohugoio/hugo/source" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + confMenu1 = ` +[[menu.main]] + name = "Go Home" + url = "/" + weight = 1 + pre = "
    " + post = "
    " +[[menu.main]] + name = "Blog" + url = "/posts" +[[menu.main]] + name = "ext" + url = "http://gohugo.io" + identifier = "ext" +[[menu.main]] + name = "ext2" + url = "http://foo.local/Zoo/foo" + identifier = "ext2" +[[menu.grandparent]] + name = "grandparent" + url = "/grandparent" + identifier = "grandparentId" +[[menu.grandparent]] + name = "parent" + url = "/parent" + identifier = "parentId" + parent = "grandparentId" +[[menu.grandparent]] + name = "Go Home3" + url = "/" + identifier = "grandchildId" + parent = "parentId" +[[menu.tax]] + name = "Tax1" + url = "/two/key/" + identifier="1" +[[menu.tax]] + name = "Tax2" + url = "/two/key/" + identifier="2" +[[menu.tax]] + name = "Tax RSS" + url = "/two/key.xml" + identifier="xml" +[[menu.hash]] + name = "Tax With #" + url = "/resource#anchor" + identifier="hash" +[[menu.unicode]] + name = "Unicode Russian" + identifier = "unicode-russian" + url = "/новости-проекта"` // Russian => "news-project" +) + +var menuPage1 = []byte(`+++ +title = "One" +weight = 1 +[menu] + [menu.p_one] ++++ +Front Matter with Menu Pages`) + +var menuPage2 = []byte(`+++ +title = "Two" +weight = 2 +[menu] + [menu.p_one] + [menu.p_two] + identifier = "Two" + ++++ +Front Matter with Menu Pages`) + +var menuPage3 = []byte(`+++ +title = "Three" +weight = 3 +[menu] + [menu.p_two] + Name = "Three" + Parent = "Two" ++++ +Front Matter with Menu Pages`) + +var menuPage4 = []byte(`+++ +title = "Four" +weight = 4 +[menu] + [menu.p_two] + Name = "Four" + Parent = "Three" ++++ +Front Matter with Menu Pages`) + +var menuPageSources = []source.ByteSource{ + {Name: filepath.FromSlash("sect/doc1.md"), Content: menuPage1}, + {Name: filepath.FromSlash("sect/doc2.md"), Content: menuPage2}, + {Name: filepath.FromSlash("sect/doc3.md"), Content: menuPage3}, +} + +var menuPageSectionsSources = []source.ByteSource{ + {Name: filepath.FromSlash("first/doc1.md"), Content: menuPage1}, + {Name: filepath.FromSlash("first/doc2.md"), Content: menuPage2}, + {Name: filepath.FromSlash("second-section/doc3.md"), Content: menuPage3}, + {Name: filepath.FromSlash("Fish and Chips/doc4.md"), Content: menuPage4}, +} + +func tstCreateMenuPageWithNameTOML(title, menu, name string) []byte { + return []byte(fmt.Sprintf(`+++ +title = "%s" +weight = 1 +[menu] + [menu.%s] + name = "%s" ++++ +Front Matter with Menu with Name`, title, menu, name)) +} + +func tstCreateMenuPageWithIdentifierTOML(title, menu, identifier string) []byte { + return []byte(fmt.Sprintf(`+++ +title = "%s" +weight = 1 +[menu] + [menu.%s] + identifier = "%s" + name = "somename" ++++ +Front Matter with Menu with Identifier`, title, menu, identifier)) +} + +func tstCreateMenuPageWithNameYAML(title, menu, name string) []byte { + return []byte(fmt.Sprintf(`--- +title: "%s" +weight: 1 +menu: + %s: + name: "%s" +--- +Front Matter with Menu with Name`, title, menu, name)) +} + +func tstCreateMenuPageWithIdentifierYAML(title, menu, identifier string) []byte { + return []byte(fmt.Sprintf(`--- +title: "%s" +weight: 1 +menu: + %s: + identifier: "%s" + name: "somename" +--- +Front Matter with Menu with Identifier`, title, menu, identifier)) +} + +// Issue 817 - identifier should trump everything +func TestPageMenuWithIdentifier(t *testing.T) { + t.Parallel() + toml := []source.ByteSource{ + {Name: "sect/doc1.md", Content: tstCreateMenuPageWithIdentifierTOML("t1", "m1", "i1")}, + {Name: "sect/doc2.md", Content: tstCreateMenuPageWithIdentifierTOML("t1", "m1", "i2")}, + {Name: "sect/doc3.md", Content: tstCreateMenuPageWithIdentifierTOML("t1", "m1", "i2")}, // duplicate + } + + yaml := []source.ByteSource{ + {Name: "sect/doc1.md", Content: tstCreateMenuPageWithIdentifierYAML("t1", "m1", "i1")}, + {Name: "sect/doc2.md", Content: tstCreateMenuPageWithIdentifierYAML("t1", "m1", "i2")}, + {Name: "sect/doc3.md", Content: tstCreateMenuPageWithIdentifierYAML("t1", "m1", "i2")}, // duplicate + } + + doTestPageMenuWithIdentifier(t, toml) + doTestPageMenuWithIdentifier(t, yaml) + +} + +func doTestPageMenuWithIdentifier(t *testing.T, menuPageSources []source.ByteSource) { + + s := setupMenuTests(t, menuPageSources) + + assert.Equal(t, 3, len(s.RegularPages), "Not enough pages") + + me1 := findTestMenuEntryByID(s, "m1", "i1") + me2 := findTestMenuEntryByID(s, "m1", "i2") + + require.NotNil(t, me1) + require.NotNil(t, me2) + + assert.True(t, strings.Contains(me1.URL, "doc1"), me1.URL) + assert.True(t, strings.Contains(me2.URL, "doc2") || strings.Contains(me2.URL, "doc3"), me2.URL) + +} + +// Issue 817 contd - name should be second identifier in +func TestPageMenuWithDuplicateName(t *testing.T) { + t.Parallel() + toml := []source.ByteSource{ + {Name: "sect/doc1.md", Content: tstCreateMenuPageWithNameTOML("t1", "m1", "n1")}, + {Name: "sect/doc2.md", Content: tstCreateMenuPageWithNameTOML("t1", "m1", "n2")}, + {Name: "sect/doc3.md", Content: tstCreateMenuPageWithNameTOML("t1", "m1", "n2")}, // duplicate + } + + yaml := []source.ByteSource{ + {Name: "sect/doc1.md", Content: tstCreateMenuPageWithNameYAML("t1", "m1", "n1")}, + {Name: "sect/doc2.md", Content: tstCreateMenuPageWithNameYAML("t1", "m1", "n2")}, + {Name: "sect/doc3.md", Content: tstCreateMenuPageWithNameYAML("t1", "m1", "n2")}, // duplicate + } + + doTestPageMenuWithDuplicateName(t, toml) + doTestPageMenuWithDuplicateName(t, yaml) + +} + +func doTestPageMenuWithDuplicateName(t *testing.T, menuPageSources []source.ByteSource) { + + s := setupMenuTests(t, menuPageSources) + + assert.Equal(t, 3, len(s.RegularPages), "Not enough pages") + + me1 := findTestMenuEntryByName(s, "m1", "n1") + me2 := findTestMenuEntryByName(s, "m1", "n2") + + require.NotNil(t, me1) + require.NotNil(t, me2) + + assert.True(t, strings.Contains(me1.URL, "doc1"), me1.URL) + assert.True(t, strings.Contains(me2.URL, "doc2") || strings.Contains(me2.URL, "doc3"), me2.URL) + +} + +func TestPageMenu(t *testing.T) { + t.Parallel() + s := setupMenuTests(t, menuPageSources) + + if len(s.RegularPages) != 3 { + t.Fatalf("Posts not created, expected 3 got %d", len(s.RegularPages)) + } + + first := s.RegularPages[0] + second := s.RegularPages[1] + third := s.RegularPages[2] + + pOne := findTestMenuEntryByName(s, "p_one", "One") + pTwo := findTestMenuEntryByID(s, "p_two", "Two") + + for i, this := range []struct { + menu string + page *Page + menuItem *MenuEntry + isMenuCurrent bool + hasMenuCurrent bool + }{ + {"p_one", first, pOne, true, false}, + {"p_one", first, pTwo, false, false}, + {"p_one", second, pTwo, false, false}, + {"p_two", second, pTwo, true, false}, + {"p_two", third, pTwo, false, true}, + {"p_one", third, pTwo, false, false}, + } { + + if i != 4 { + continue + } + + isMenuCurrent := this.page.IsMenuCurrent(this.menu, this.menuItem) + hasMenuCurrent := this.page.HasMenuCurrent(this.menu, this.menuItem) + + if isMenuCurrent != this.isMenuCurrent { + t.Errorf("[%d] Wrong result from IsMenuCurrent: %v", i, isMenuCurrent) + } + + if hasMenuCurrent != this.hasMenuCurrent { + t.Errorf("[%d] Wrong result for menuItem %v for HasMenuCurrent: %v", i, this.menuItem, hasMenuCurrent) + } + + } + +} + +func TestMenuURL(t *testing.T) { + t.Parallel() + s := setupMenuTests(t, menuPageSources) + + for i, this := range []struct { + me *MenuEntry + expectedURL string + }{ + // issue #888 + {findTestMenuEntryByID(s, "hash", "hash"), "/Zoo/resource#anchor"}, + // issue #1774 + {findTestMenuEntryByID(s, "main", "ext"), "http://gohugo.io"}, + {findTestMenuEntryByID(s, "main", "ext2"), "http://foo.local/Zoo/foo"}, + } { + + if this.me == nil { + t.Errorf("[%d] MenuEntry not found", i) + continue + } + + if this.me.URL != this.expectedURL { + t.Errorf("[%d] Got URL %s expected %s", i, this.me.URL, this.expectedURL) + } + + } + +} + +// Issue #1934 +func TestYAMLMenuWithMultipleEntries(t *testing.T) { + t.Parallel() + ps1 := []byte(`--- +title: "Yaml 1" +weight: 5 +menu: ["p_one", "p_two"] +--- +Yaml Front Matter with Menu Pages`) + + ps2 := []byte(`--- +title: "Yaml 2" +weight: 5 +menu: + p_three: + p_four: +--- +Yaml Front Matter with Menu Pages`) + + s := setupMenuTests(t, []source.ByteSource{ + {Name: filepath.FromSlash("sect/yaml1.md"), Content: ps1}, + {Name: filepath.FromSlash("sect/yaml2.md"), Content: ps2}}) + + p1 := s.RegularPages[0] + assert.Len(t, p1.Menus(), 2, "List YAML") + p2 := s.RegularPages[1] + assert.Len(t, p2.Menus(), 2, "Map YAML") + +} + +// issue #719 +func TestMenuWithUnicodeURLs(t *testing.T) { + t.Parallel() + for _, canonifyURLs := range []bool{true, false} { + doTestMenuWithUnicodeURLs(t, canonifyURLs) + } +} + +func doTestMenuWithUnicodeURLs(t *testing.T, canonifyURLs bool) { + + s := setupMenuTests(t, menuPageSources, "canonifyURLs", canonifyURLs) + + unicodeRussian := findTestMenuEntryByID(s, "unicode", "unicode-russian") + + expected := "/%D0%BD%D0%BE%D0%B2%D0%BE%D1%81%D1%82%D0%B8-%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B0" + + if !canonifyURLs { + expected = "/Zoo" + expected + } + + assert.Equal(t, expected, unicodeRussian.URL) +} + +// Issue #1114 +func TestSectionPagesMenu2(t *testing.T) { + t.Parallel() + doTestSectionPagesMenu(true, t) + doTestSectionPagesMenu(false, t) +} + +func doTestSectionPagesMenu(canonifyURLs bool, t *testing.T) { + + s := setupMenuTests(t, menuPageSectionsSources, + "sectionPagesMenu", "spm", + "canonifyURLs", canonifyURLs, + ) + + sects := s.getPage(KindHome).Sections() + + require.Equal(t, 3, len(sects)) + + firstSectionPages := s.getPage(KindSection, "first").Pages + require.Equal(t, 2, len(firstSectionPages)) + secondSectionPages := s.getPage(KindSection, "second-section").Pages + require.Equal(t, 1, len(secondSectionPages)) + fishySectionPages := s.getPage(KindSection, "Fish and Chips").Pages + require.Equal(t, 1, len(fishySectionPages)) + + nodeFirst := s.getPage(KindSection, "first") + require.NotNil(t, nodeFirst) + nodeSecond := s.getPage(KindSection, "second-section") + require.NotNil(t, nodeSecond) + nodeFishy := s.getPage(KindSection, "Fish and Chips") + require.Equal(t, "Fish and Chips", nodeFishy.sections[0]) + + firstSectionMenuEntry := findTestMenuEntryByID(s, "spm", "first") + secondSectionMenuEntry := findTestMenuEntryByID(s, "spm", "second-section") + fishySectionMenuEntry := findTestMenuEntryByID(s, "spm", "Fish and Chips") + + require.NotNil(t, firstSectionMenuEntry) + require.NotNil(t, secondSectionMenuEntry) + require.NotNil(t, nodeFirst) + require.NotNil(t, nodeSecond) + require.NotNil(t, fishySectionMenuEntry) + require.NotNil(t, nodeFishy) + + require.True(t, nodeFirst.IsMenuCurrent("spm", firstSectionMenuEntry)) + require.False(t, nodeFirst.IsMenuCurrent("spm", secondSectionMenuEntry)) + require.False(t, nodeFirst.IsMenuCurrent("spm", fishySectionMenuEntry)) + require.True(t, nodeFishy.IsMenuCurrent("spm", fishySectionMenuEntry)) + require.Equal(t, "Fish and Chips", fishySectionMenuEntry.Name) + + for _, p := range firstSectionPages { + require.True(t, p.HasMenuCurrent("spm", firstSectionMenuEntry)) + require.False(t, p.HasMenuCurrent("spm", secondSectionMenuEntry)) + } + + for _, p := range secondSectionPages { + require.False(t, p.HasMenuCurrent("spm", firstSectionMenuEntry)) + require.True(t, p.HasMenuCurrent("spm", secondSectionMenuEntry)) + } + + for _, p := range fishySectionPages { + require.False(t, p.HasMenuCurrent("spm", firstSectionMenuEntry)) + require.False(t, p.HasMenuCurrent("spm", secondSectionMenuEntry)) + require.True(t, p.HasMenuCurrent("spm", fishySectionMenuEntry)) + } +} + +func TestMenuLimit(t *testing.T) { + t.Parallel() + s := setupMenuTests(t, menuPageSources) + m := *s.Menus["main"] + + // main menu has 4 entries + firstTwo := m.Limit(2) + assert.Equal(t, 2, len(firstTwo)) + for i := 0; i < 2; i++ { + assert.Equal(t, m[i], firstTwo[i]) + } + assert.Equal(t, m, m.Limit(4)) + assert.Equal(t, m, m.Limit(5)) +} + +func TestMenuSortByN(t *testing.T) { + t.Parallel() + for i, this := range []struct { + sortFunc func(p Menu) Menu + assertFunc func(p Menu) bool + }{ + {(Menu).Sort, func(p Menu) bool { return p[0].Weight == 1 && p[1].Name == "nx" && p[2].Identifier == "ib" }}, + {(Menu).ByWeight, func(p Menu) bool { return p[0].Weight == 1 && p[1].Name == "nx" && p[2].Identifier == "ib" }}, + {(Menu).ByName, func(p Menu) bool { return p[0].Name == "na" }}, + {(Menu).Reverse, func(p Menu) bool { return p[0].Identifier == "ib" && p[len(p)-1].Identifier == "ia" }}, + } { + menu := Menu{&MenuEntry{Weight: 3, Name: "nb", Identifier: "ia"}, + &MenuEntry{Weight: 1, Name: "na", Identifier: "ic"}, + &MenuEntry{Weight: 1, Name: "nx", Identifier: "ic"}, + &MenuEntry{Weight: 2, Name: "nb", Identifier: "ix"}, + &MenuEntry{Weight: 2, Name: "nb", Identifier: "ib"}} + + sorted := this.sortFunc(menu) + + if !this.assertFunc(sorted) { + t.Errorf("[%d] sort error", i) + } + } + +} + +func TestHomeNodeMenu(t *testing.T) { + t.Parallel() + s := setupMenuTests(t, menuPageSources, + "canonifyURLs", true, + "uglyURLs", false, + ) + + home := s.getPage(KindHome) + homeMenuEntry := &MenuEntry{Name: home.Title, URL: home.URL()} + + for i, this := range []struct { + menu string + menuItem *MenuEntry + isMenuCurrent bool + hasMenuCurrent bool + }{ + {"main", homeMenuEntry, true, false}, + {"doesnotexist", homeMenuEntry, false, false}, + {"main", &MenuEntry{Name: "Somewhere else", URL: "/somewhereelse"}, false, false}, + {"grandparent", findTestMenuEntryByID(s, "grandparent", "grandparentId"), false, true}, + {"grandparent", findTestMenuEntryByID(s, "grandparent", "parentId"), false, true}, + {"grandparent", findTestMenuEntryByID(s, "grandparent", "grandchildId"), true, false}, + } { + + isMenuCurrent := home.IsMenuCurrent(this.menu, this.menuItem) + hasMenuCurrent := home.HasMenuCurrent(this.menu, this.menuItem) + + if isMenuCurrent != this.isMenuCurrent { + fmt.Println("isMenuCurrent", isMenuCurrent) + fmt.Printf("this: %#v\n", this) + t.Errorf("[%d] Wrong result from IsMenuCurrent: %v for %q", i, isMenuCurrent, this.menuItem) + } + + if hasMenuCurrent != this.hasMenuCurrent { + fmt.Println("hasMenuCurrent", hasMenuCurrent) + fmt.Printf("this: %#v\n", this) + t.Errorf("[%d] Wrong result for menu %q menuItem %v for HasMenuCurrent: %v", i, this.menu, this.menuItem, hasMenuCurrent) + } + } +} + +func TestHopefullyUniqueID(t *testing.T) { + t.Parallel() + assert.Equal(t, "i", (&MenuEntry{Identifier: "i", URL: "u", Name: "n"}).hopefullyUniqueID()) + assert.Equal(t, "u", (&MenuEntry{Identifier: "", URL: "u", Name: "n"}).hopefullyUniqueID()) + assert.Equal(t, "n", (&MenuEntry{Identifier: "", URL: "", Name: "n"}).hopefullyUniqueID()) +} + +func TestAddMenuEntryChild(t *testing.T) { + t.Parallel() + root := &MenuEntry{Weight: 1} + root.addChild(&MenuEntry{Weight: 2}) + root.addChild(&MenuEntry{Weight: 1}) + assert.Equal(t, 2, len(root.Children)) + assert.Equal(t, 1, root.Children[0].Weight) +} + +var testMenuIdentityMatcher = func(me *MenuEntry, id string) bool { return me.Identifier == id } +var testMenuNameMatcher = func(me *MenuEntry, id string) bool { return me.Name == id } + +func findTestMenuEntryByID(s *Site, mn string, id string) *MenuEntry { + return findTestMenuEntry(s, mn, id, testMenuIdentityMatcher) +} +func findTestMenuEntryByName(s *Site, mn string, id string) *MenuEntry { + return findTestMenuEntry(s, mn, id, testMenuNameMatcher) +} + +func findTestMenuEntry(s *Site, mn string, id string, matcher func(me *MenuEntry, id string) bool) *MenuEntry { + var found *MenuEntry + if menu, ok := s.Menus[mn]; ok { + for _, me := range *menu { + + if matcher(me, id) { + if found != nil { + panic(fmt.Sprintf("Duplicate menu entry in menu %s with id/name %s", mn, id)) + } + found = me + } + + descendant := findDescendantTestMenuEntry(me, id, matcher) + if descendant != nil { + if found != nil { + panic(fmt.Sprintf("Duplicate menu entry in menu %s with id/name %s", mn, id)) + } + found = descendant + } + } + } + return found +} + +func findDescendantTestMenuEntry(parent *MenuEntry, id string, matcher func(me *MenuEntry, id string) bool) *MenuEntry { + var found *MenuEntry + if parent.HasChildren() { + for _, child := range parent.Children { + + if matcher(child, id) { + if found != nil { + panic(fmt.Sprintf("Duplicate menu entry in menuitem %s with id/name %s", parent.KeyName(), id)) + } + found = child + } + + descendant := findDescendantTestMenuEntry(child, id, matcher) + if descendant != nil { + if found != nil { + panic(fmt.Sprintf("Duplicate menu entry in menuitem %s with id/name %s", parent.KeyName(), id)) + } + found = descendant + } + } + } + return found +} + +func setupMenuTests(t *testing.T, pageSources []source.ByteSource, configKeyValues ...interface{}) *Site { + + var ( + cfg, fs = newTestCfg() + ) + + menus, err := tomlToMap(confMenu1) + require.NoError(t, err) + + cfg.Set("menu", menus["menu"]) + cfg.Set("baseURL", "http://foo.local/Zoo/") + + for i := 0; i < len(configKeyValues); i += 2 { + cfg.Set(configKeyValues[i].(string), configKeyValues[i+1]) + } + + for _, src := range pageSources { + writeSource(t, fs, filepath.Join("content", src.Name), string(src.Content)) + + } + + return buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) + +} + +func tomlToMap(s string) (map[string]interface{}, error) { + var data = make(map[string]interface{}) + _, err := toml.Decode(s, &data) + return data, err +} diff --git a/hugolib/menu_test.go b/hugolib/menu_test.go index 3be999c31..f044fb5e0 100644 --- a/hugolib/menu_test.go +++ b/hugolib/menu_test.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Hugo Authors. All rights reserved. +// Copyright 2017 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,10 +14,13 @@ package hugolib import ( - "fmt" "testing" - qt "github.com/frankban/quicktest" + "fmt" + + "github.com/spf13/afero" + + "github.com/stretchr/testify/require" ) const ( @@ -26,14 +29,13 @@ title: %q weight: %d menu: %s: - title: %s weight: %d --- # Doc Menu ` ) -func TestMenusSectionPagesMenu(t *testing.T) { +func TestSectionPagesMenu(t *testing.T) { t.Parallel() siteConfig := ` @@ -42,659 +44,53 @@ title = "Section Menu" sectionPagesMenu = "sect" ` - b := newTestSitesBuilder(t).WithConfigFile("toml", siteConfig) - - b.WithTemplates( - "partials/menu.html", - `{{- $p := .page -}} + th, h := newTestSitesFromConfig(t, afero.NewMemMapFs(), siteConfig, + "layouts/partials/menu.html", `{{- $p := .page -}} {{- $m := .menu -}} {{ range (index $p.Site.Menus $m) -}} -{{- .URL }}|{{ .Name }}|{{ .Title }}|{{ .Weight -}}| +{{- .URL }}|{{ .Name }}|{{ .Weight -}}| {{- if $p.IsMenuCurrent $m . }}IsMenuCurrent{{ else }}-{{ end -}}| {{- if $p.HasMenuCurrent $m . }}HasMenuCurrent{{ else }}-{{ end -}}| {{- end -}} `, - "_default/single.html", + "layouts/_default/single.html", `Single|{{ .Title }} Menu Sect: {{ partial "menu.html" (dict "page" . "menu" "sect") }} Menu Main: {{ partial "menu.html" (dict "page" . "menu" "main") }}`, - "_default/list.html", "List|{{ .Title }}|{{ .Content }}", + "layouts/_default/list.html", "List|{{ .Title }}|{{ .Content }}", ) + require.Len(t, h.Sites, 1) - b.WithContent( - "sect1/p1.md", fmt.Sprintf(menuPageTemplate, "p1", 1, "main", "atitle1", 40), - "sect1/p2.md", fmt.Sprintf(menuPageTemplate, "p2", 2, "main", "atitle2", 30), - "sect2/p3.md", fmt.Sprintf(menuPageTemplate, "p3", 3, "main", "atitle3", 20), - "sect2/p4.md", fmt.Sprintf(menuPageTemplate, "p4", 4, "main", "atitle4", 10), - "sect3/p5.md", fmt.Sprintf(menuPageTemplate, "p5", 5, "main", "atitle5", 5), - "sect1/_index.md", newTestPage("Section One", "2017-01-01", 100), - "sect5/_index.md", newTestPage("Section Five", "2017-01-01", 10), - ) + fs := th.Fs - b.Build(BuildCfg{}) - h := b.H + writeSource(t, fs, "content/sect1/p1.md", fmt.Sprintf(menuPageTemplate, "p1", 1, "main", 40)) + writeSource(t, fs, "content/sect1/p2.md", fmt.Sprintf(menuPageTemplate, "p2", 2, "main", 30)) + writeSource(t, fs, "content/sect2/p3.md", fmt.Sprintf(menuPageTemplate, "p3", 3, "main", 20)) + writeSource(t, fs, "content/sect2/p4.md", fmt.Sprintf(menuPageTemplate, "p4", 4, "main", 10)) + writeSource(t, fs, "content/sect3/p5.md", fmt.Sprintf(menuPageTemplate, "p5", 5, "main", 5)) + + writeNewContentFile(t, fs, "Section One", "2017-01-01", "content/sect1/_index.md", 100) + writeNewContentFile(t, fs, "Section Five", "2017-01-01", "content/sect5/_index.md", 10) + + err := h.Build(BuildCfg{}) + + require.NoError(t, err) s := h.Sites[0] - b.Assert(len(s.Menus()), qt.Equals, 2) + require.Len(t, s.Menus, 2) - p1 := s.RegularPages()[0].Menus() + p1 := s.RegularPages[0].Menus() // There is only one menu in the page, but it is "member of" 2 - b.Assert(len(p1), qt.Equals, 1) + require.Len(t, p1, 1) - b.AssertFileContent("public/sect1/p1/index.html", "Single", - "Menu Sect: "+ - "/sect5/|Section Five|Section Five|10|-|-|"+ - "/sect1/|Section One|Section One|100|-|HasMenuCurrent|"+ - "/sect2/|Sect2s|Sect2s|0|-|-|"+ - "/sect3/|Sect3s|Sect3s|0|-|-|", - "Menu Main: "+ - "/sect3/p5/|p5|atitle5|5|-|-|"+ - "/sect2/p4/|p4|atitle4|10|-|-|"+ - "/sect2/p3/|p3|atitle3|20|-|-|"+ - "/sect1/p2/|p2|atitle2|30|-|-|"+ - "/sect1/p1/|p1|atitle1|40|IsMenuCurrent|-|", + th.assertFileContent("public/sect1/p1/index.html", "Single", + "Menu Sect: /sect5/|Section Five|10|-|-|/sect1/|Section One|100|-|HasMenuCurrent|/sect2/|Sect2s|0|-|-|/sect3/|Sect3s|0|-|-|", + "Menu Main: /sect3/p5/|p5|5|-|-|/sect2/p4/|p4|10|-|-|/sect2/p3/|p3|20|-|-|/sect1/p2/|p2|30|-|-|/sect1/p1/|p1|40|IsMenuCurrent|-|", ) - b.AssertFileContent("public/sect2/p3/index.html", "Single", - "Menu Sect: "+ - "/sect5/|Section Five|Section Five|10|-|-|"+ - "/sect1/|Section One|Section One|100|-|-|"+ - "/sect2/|Sect2s|Sect2s|0|-|HasMenuCurrent|"+ - "/sect3/|Sect3s|Sect3s|0|-|-|") -} - -func TestMenusFrontMatter(t *testing.T) { - b := newTestSitesBuilder(t).WithSimpleConfigFile() - - b.WithTemplatesAdded("index.html", ` -Main: {{ len .Site.Menus.main }} -Other: {{ len .Site.Menus.other }} -{{ range .Site.Menus.main }} -* Main|{{ .Name }}: {{ .URL }} -{{ end }} -{{ range .Site.Menus.other }} -* Other|{{ .Name }}: {{ .URL }} -{{ end }} -`) - - // Issue #5828 - b.WithContent("blog/page1.md", ` ---- -title: "P1" -menu: main ---- - -`) - - b.WithContent("blog/page2.md", ` ---- -title: "P2" -menu: [main,other] ---- - -`) - - b.WithContent("blog/page3.md", ` ---- -title: "P3" -menu: - main: - weight: 30 ---- -`) - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", - "Main: 3", "Other: 1", - "Main|P1: /blog/page1/", - "Other|P2: /blog/page2/", - ) -} - -// https://github.com/gohugoio/hugo/issues/5849 -func TestMenusPageMultipleOutputFormats(t *testing.T) { - config := ` -baseURL = "https://example.com" - -# DAMP is similar to AMP, but not permalinkable. -[outputFormats] -[outputFormats.damp] -mediaType = "text/html" -path = "damp" - -` - - b := newTestSitesBuilder(t).WithConfigFile("toml", config) - b.WithContent("_index.md", ` ---- -Title: Home Sweet Home -outputs: [ "html", "amp" ] -menu: "main" ---- - -`) - - b.WithContent("blog/html-amp.md", ` ---- -Title: AMP and HTML -outputs: [ "html", "amp" ] -menu: "main" ---- - -`) - - b.WithContent("blog/html.md", ` ---- -Title: HTML only -outputs: [ "html" ] -menu: "main" ---- - -`) - - b.WithContent("blog/amp.md", ` ---- -Title: AMP only -outputs: [ "amp" ] -menu: "main" ---- - -`) - - b.WithTemplatesAdded("index.html", `{{ range .Site.Menus.main }}{{ .Title }}|{{ .URL }}|{{ end }}`) - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", "AMP and HTML|/blog/html-amp/|AMP only|/amp/blog/amp/|Home Sweet Home|/|HTML only|/blog/html/|") - b.AssertFileContent("public/amp/index.html", "AMP and HTML|/amp/blog/html-amp/|AMP only|/amp/blog/amp/|Home Sweet Home|/amp/|HTML only|/blog/html/|") -} - -// https://github.com/gohugoio/hugo/issues/5989 -func TestMenusPageSortByDate(t *testing.T) { - b := newTestSitesBuilder(t).WithSimpleConfigFile() - - b.WithContent("blog/a.md", ` ---- -Title: A -date: 2019-01-01 -menu: - main: - identifier: "a" - weight: 1 ---- - -`) - - b.WithContent("blog/b.md", ` ---- -Title: B -date: 2018-01-02 -menu: - main: - parent: "a" - weight: 100 ---- - -`) - - b.WithContent("blog/c.md", ` ---- -Title: C -date: 2019-01-03 -menu: - main: - parent: "a" - weight: 10 ---- - -`) - - b.WithTemplatesAdded("index.html", `{{ range .Site.Menus.main }}{{ .Title }}|Children: -{{- $children := sort .Children ".Page.Date" "desc" }}{{ range $children }}{{ .Title }}|{{ end }}{{ end }} - -`) - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", "A|Children:C|B|") -} - -// Issue #8825 -func TestMenuParamsEmptyYaml(t *testing.T) { - b := newTestSitesBuilder(t).WithConfigFile("yaml", ` - -`) - - b.WithTemplates("index.html", `{{ site.Menus }}`) - - b.WithContent("p1.md", `--- -menus: - main: - identity: journal - weight: 2 - params: ---- -`) - b.Build(BuildCfg{}) -} - -func TestMenuParams(t *testing.T) { - b := newTestSitesBuilder(t).WithConfigFile("toml", ` -[[menus.main]] -identifier = "contact" -title = "Contact Us" -url = "mailto:noreply@example.com" -weight = 300 -[menus.main.params] -foo = "foo_config" -key2 = "key2_config" -camelCase = "camelCase_config" -`) - - b.WithTemplatesAdded("index.html", ` -Main: {{ len .Site.Menus.main }} -{{ range .Site.Menus.main }} -foo: {{ .Params.foo }} -key2: {{ .Params.KEy2 }} -camelCase: {{ .Params.camelcase }} -{{ end }} -`) - - b.WithContent("_index.md", ` ---- -title: "Home" -menu: - main: - weight: 10 - params: - foo: "foo_content" - key2: "key2_content" - camelCase: "camelCase_content" ---- -`) - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", ` -Main: 2 - -foo: foo_content -key2: key2_content -camelCase: camelCase_content - -foo: foo_config -key2: key2_config -camelCase: camelCase_config -`) -} - -func TestMenusShadowMembers(t *testing.T) { - b := newTestSitesBuilder(t).WithConfigFile("toml", ` -[[menus.main]] -identifier = "contact" -pageRef = "contact" -title = "Contact Us" -url = "mailto:noreply@example.com" -weight = 1 -[[menus.main]] -pageRef = "/blog/post3" -title = "My Post 3" -url = "/blog/post3" - -`) - - commonTempl := ` -Main: {{ len .Site.Menus.main }} -{{ range .Site.Menus.main }} -{{ .Title }}|HasMenuCurrent: {{ $.HasMenuCurrent "main" . }}|Page: {{ .Page.Path }} -{{ .Title }}|IsMenuCurrent: {{ $.IsMenuCurrent "main" . }}|Page: {{ .Page.Path }} -{{ end }} -` - - b.WithTemplatesAdded("index.html", commonTempl) - b.WithTemplatesAdded("_default/single.html", commonTempl) - - b.WithContent("_index.md", ` ---- -title: "Home" -menu: - main: - weight: 10 ---- -`) - - b.WithContent("blog/_index.md", ` ---- -title: "Blog" -menu: - main: - weight: 20 ---- -`) - - b.WithContent("blog/post1.md", ` ---- -title: "My Post 1: With No Menu Defined" ---- -`) - - b.WithContent("blog/post2.md", ` ---- -title: "My Post 2: With Menu Defined" -menu: - main: - weight: 30 ---- -`) - - b.WithContent("blog/post3.md", ` ---- -title: "My Post 2: With No Menu Defined" ---- -`) - - b.WithContent("contact.md", ` ---- -title: "Contact: With No Menu Defined" ---- -`) - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", ` -Main: 5 -Home|HasMenuCurrent: false|Page: / -Blog|HasMenuCurrent: false|Page: /blog -My Post 2: With Menu Defined|HasMenuCurrent: false|Page: /blog/post2 -My Post 3|HasMenuCurrent: false|Page: /blog/post3 -Contact Us|HasMenuCurrent: false|Page: /contact -`) - - b.AssertFileContent("public/blog/post1/index.html", ` -Home|HasMenuCurrent: false|Page: / -Blog|HasMenuCurrent: true|Page: /blog -`) - - b.AssertFileContent("public/blog/post2/index.html", ` -Home|HasMenuCurrent: false|Page: / -Blog|HasMenuCurrent: true|Page: /blog -Blog|IsMenuCurrent: false|Page: /blog -`) - - b.AssertFileContent("public/blog/post3/index.html", ` -Home|HasMenuCurrent: false|Page: / -Blog|HasMenuCurrent: true|Page: /blog -`) - - b.AssertFileContent("public/contact/index.html", ` -Contact Us|HasMenuCurrent: false|Page: /contact -Contact Us|IsMenuCurrent: true|Page: /contact -Blog|HasMenuCurrent: false|Page: /blog -Blog|IsMenuCurrent: false|Page: /blog -`) -} - -// Issue 9846 -func TestMenuHasMenuCurrentSection(t *testing.T) { - t.Parallel() - - files := ` --- config.toml -- -disableKinds = ['RSS','sitemap','taxonomy','term'] -[[menu.main]] -name = 'Home' -pageRef = '/' -weight = 1 - -[[menu.main]] -name = 'Tests' -pageRef = '/tests' -weight = 2 -[[menu.main]] -name = 'Test 1' -pageRef = '/tests/test-1' -parent = 'Tests' -weight = 1 - --- content/tests/test-1.md -- ---- -title: "Test 1" ---- --- layouts/_default/list.html -- -{{ range site.Menus.main }} -{{ .Name }}|{{ .URL }}|IsMenuCurrent = {{ $.IsMenuCurrent "main" . }}|HasMenuCurrent = {{ $.HasMenuCurrent "main" . }}| -{{ range .Children }} -{{ .Name }}|{{ .URL }}|IsMenuCurrent = {{ $.IsMenuCurrent "main" . }}|HasMenuCurrent = {{ $.HasMenuCurrent "main" . }}| -{{ end }} -{{ end }} - -{{/* Some tests for issue 9925 */}} -{{ $page := .Site.GetPage "tests/test-1" }} -{{ $section := site.GetPage "tests" }} - -Home IsAncestor Self: {{ site.Home.IsAncestor site.Home }} -Home IsDescendant Self: {{ site.Home.IsDescendant site.Home }} -Section IsAncestor Self: {{ $section.IsAncestor $section }} -Section IsDescendant Self: {{ $section.IsDescendant $section}} -Page IsAncestor Self: {{ $page.IsAncestor $page }} -Page IsDescendant Self: {{ $page.IsDescendant $page}} -` - - b := Test(t, files) - - b.AssertFileContent("public/tests/index.html", ` -Tests|/tests/|IsMenuCurrent = true|HasMenuCurrent = false -Home IsAncestor Self: false -Home IsDescendant Self: false -Section IsAncestor Self: false -Section IsDescendant Self: false -Page IsAncestor Self: false -Page IsDescendant Self: false -`) -} - -func TestMenusNewConfigSetup(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.com" -title = "Hugo Menu Test" -[menus] -[[menus.main]] -name = "Home" -url = "/" -pre = "" -post = "" -weight = 1 --- layouts/index.html -- -{{ range $i, $e := site.Menus.main }} -Menu Item: {{ $i }}: {{ .Pre }}{{ .Name }}{{ .Post }}|{{ .URL }}| -{{ end }} -` - - b := Test(t, files) - - b.AssertFileContent("public/index.html", ` -Menu Item: 0: Home|/| -`) -} - -// Issue #11062 -func TestMenusSubDirInBaseURL(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.com/foo/" -title = "Hugo Menu Test" -[menus] -[[menus.main]] -name = "Posts" -url = "/posts" -weight = 1 --- layouts/index.html -- -{{ range $i, $e := site.Menus.main }} -Menu Item: {{ $i }}|{{ .URL }}| -{{ end }} -` - - b := Test(t, files) - - b.AssertFileContent("public/index.html", ` -Menu Item: 0|/foo/posts| -`) -} - -func TestSectionPagesMenuMultilingualWarningIssue12306(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ['section','rss','sitemap','taxonomy','term'] -defaultContentLanguageInSubdir = true -sectionPagesMenu = "main" -[languages.en] -[languages.fr] --- layouts/_default/home.html -- -{{- range site.Menus.main -}} - {{ .Name }} -{{- end -}} --- layouts/_default/single.html -- -{{ .Title }} --- content/p1.en.md -- ---- -title: p1 -menu: main ---- --- content/p1.fr.md -- ---- -title: p1 -menu: main ---- --- content/p2.en.md -- ---- -title: p2 -menu: main ---- -` - - b := Test(t, files, TestOptWarn()) - - b.AssertFileContent("public/en/index.html", `p1p2`) - b.AssertFileContent("public/fr/index.html", `p1`) - b.AssertLogContains("! WARN") -} - -func TestSectionPagesIssue12399(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ['rss','sitemap','taxonomy','term'] -capitalizeListTitles = false -pluralizeListTitles = false -sectionPagesMenu = 'main' --- content/p1.md -- ---- -title: p1 ---- --- content/s1/p2.md -- ---- -title: p2 -menus: main ---- --- content/s1/p3.md -- ---- -title: p3 ---- --- layouts/_default/list.html -- -{{ range site.Menus.main }}{{ .Name }}{{ end }} --- layouts/_default/single.html -- -{{ .Title }} -` - - b := Test(t, files) - - b.AssertFileExists("public/index.html", true) - b.AssertFileContent("public/index.html", `p2s1`) -} - -// Issue 13161 -func TestMenuNameAndTitleFallback(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ['rss','sitemap','taxonomy','term'] -[[menus.main]] -name = 'P1_ME_Name' -title = 'P1_ME_Title' -pageRef = '/p1' -weight = 10 -[[menus.main]] -pageRef = '/p2' -weight = 20 -[[menus.main]] -pageRef = '/p3' -weight = 30 -[[menus.main]] -name = 'S1_ME_Name' -title = 'S1_ME_Title' -pageRef = '/s1' -weight = 40 -[[menus.main]] -pageRef = '/s2' -weight = 50 -[[menus.main]] -pageRef = '/s3' -weight = 60 --- content/p1.md -- ---- -title: P1_Title ---- --- content/p2.md -- ---- -title: P2_Title ---- --- content/p3.md -- ---- -title: P3_Title -linkTitle: P3_LinkTitle ---- --- content/s1/_index.md -- ---- -title: S1_Title ---- --- content/s2/_index.md -- ---- -title: S2_Title ---- --- content/s3/_index.md -- ---- -title: S3_Title -linkTitle: S3_LinkTitle ---- --- layouts/_default/single.html -- -{{ .Content }} --- layouts/_default/list.html -- -{{ .Content }} --- layouts/_default/home.html -- -{{- range site.Menus.main }} -URL: {{ .URL }}| Name: {{ .Name }}| Title: {{ .Title }}| PageRef: {{ .PageRef }}| Page.Title: {{ .Page.Title }}| Page.LinkTitle: {{ .Page.LinkTitle }}| -{{- end }} -` - - b := Test(t, files) - b.AssertFileContent("public/index.html", - `URL: /p1/| Name: P1_ME_Name| Title: P1_ME_Title| PageRef: /p1| Page.Title: P1_Title| Page.LinkTitle: P1_Title|`, - `URL: /p2/| Name: P2_Title| Title: P2_Title| PageRef: /p2| Page.Title: P2_Title| Page.LinkTitle: P2_Title|`, - `URL: /p3/| Name: P3_LinkTitle| Title: P3_Title| PageRef: /p3| Page.Title: P3_Title| Page.LinkTitle: P3_LinkTitle|`, - `URL: /s1/| Name: S1_ME_Name| Title: S1_ME_Title| PageRef: /s1| Page.Title: S1_Title| Page.LinkTitle: S1_Title|`, - `URL: /s2/| Name: S2_Title| Title: S2_Title| PageRef: /s2| Page.Title: S2_Title| Page.LinkTitle: S2_Title|`, - `URL: /s3/| Name: S3_LinkTitle| Title: S3_Title| PageRef: /s3| Page.Title: S3_Title| Page.LinkTitle: S3_LinkTitle|`, - ) + th.assertFileContent("public/sect2/p3/index.html", "Single", + "Menu Sect: /sect5/|Section Five|10|-|-|/sect1/|Section One|100|-|-|/sect2/|Sect2s|0|-|HasMenuCurrent|/sect3/|Sect3s|0|-|-|") + } diff --git a/hugolib/minify_publisher_test.go b/hugolib/minify_publisher_test.go deleted file mode 100644 index ef460efa2..000000000 --- a/hugolib/minify_publisher_test.go +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2019 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 hugolib - -import ( - "testing" - - "github.com/gohugoio/hugo/config" -) - -func TestMinifyPublisher(t *testing.T) { - t.Parallel() - - v := config.New() - v.Set("minify", true) - v.Set("baseURL", "https://example.org/") - - htmlTemplate := ` - - - - - HTML5 boilerplate – all you really need… - - - - - - -

    {{ .Title }}

    -

    {{ .Permalink }}

    - - - -` - - b := newTestSitesBuilder(t) - b.WithViper(v).WithTemplatesAdded("layouts/index.html", htmlTemplate) - b.CreateSites().Build(BuildCfg{}) - - // Check minification - // HTML - b.AssertFileContent("public/index.html", "") - - // RSS - b.AssertFileContent("public/index.xml", "<link>https://example.org/</link>") - - // Sitemap - b.AssertFileContent("public/sitemap.xml", "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?><urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" xmlns:xhtml=\"http://www.w3.org/1999/xhtml\"><url><loc>h") -} diff --git a/hugolib/mount_filters_test.go b/hugolib/mount_filters_test.go deleted file mode 100644 index 16b062ec6..000000000 --- a/hugolib/mount_filters_test.go +++ /dev/null @@ -1,117 +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 hugolib - -import ( - "fmt" - "os" - "path/filepath" - "testing" - - "github.com/gohugoio/hugo/common/loggers" - "github.com/gohugoio/hugo/hugofs/files" - - "github.com/gohugoio/hugo/htesting" - "github.com/gohugoio/hugo/hugofs" - - qt "github.com/frankban/quicktest" -) - -func TestMountFilters(t *testing.T) { - t.Parallel() - b := newTestSitesBuilder(t) - workingDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-test-mountfilters") - b.Assert(err, qt.IsNil) - defer clean() - - for _, component := range files.ComponentFolders { - b.Assert(os.MkdirAll(filepath.Join(workingDir, component), 0o777), qt.IsNil) - } - b.WithWorkingDir(workingDir).WithLogger(loggers.NewDefault()) - b.WithConfigFile("toml", fmt.Sprintf(` -workingDir = %q - -[module] -[[module.mounts]] -source = 'content' -target = 'content' -excludeFiles = "/a/c/**" -[[module.mounts]] -source = 'static' -target = 'static' -[[module.mounts]] -source = 'layouts' -target = 'layouts' -excludeFiles = "/**/foo.html" -[[module.mounts]] -source = 'data' -target = 'data' -includeFiles = "/mydata/**" -[[module.mounts]] -source = 'assets' -target = 'assets' -excludeFiles = ["/**exclude.*", "/moooo.*"] -[[module.mounts]] -source = 'i18n' -target = 'i18n' -[[module.mounts]] -source = 'archetypes' -target = 'archetypes' - - -`, workingDir)) - - b.WithContent("/a/b/p1.md", "---\ntitle: Include\n---") - b.WithContent("/a/c/p2.md", "---\ntitle: Exclude\n---") - - b.WithSourceFile( - "data/mydata/b.toml", `b1='bval'`, - "data/nodata/c.toml", `c1='bval'`, - "layouts/partials/foo.html", `foo`, - "assets/exclude.txt", `foo`, - "assets/js/exclude.js", `foo`, - "assets/js/include.js", `foo`, - "assets/js/exclude.js", `foo`, - ) - - b.WithTemplatesAdded("index.html", ` - -Data: {{ site.Data }}:END - -Template: {{ templates.Exists "partials/foo.html" }}:END -Resource1: {{ resources.Get "js/include.js" }}:END -Resource2: {{ resources.Get "js/exclude.js" }}:END -Resource3: {{ resources.Get "exclude.txt" }}:END -Resources: {{ resources.Match "**.js" }} -`) - - b.Build(BuildCfg{}) - - assertExists := func(name string, shouldExist bool) { - b.Helper() - b.Assert(b.CheckExists(name), qt.Equals, shouldExist) - } - - assertExists("public/a/b/p1/index.html", true) - assertExists("public/a/c/p2/index.html", false) - - b.AssertFileContent(filepath.Join("public", "index.html"), ` -Data: map[mydata:map[b:map[b1:bval]]]:END -Template: false -Resource1: /js/include.js:END -Resource2: :END -Resource3: :END -Resources: [/js/include.js] -`) -} diff --git a/hugolib/multilingual.go b/hugolib/multilingual.go new file mode 100644 index 000000000..575be1396 --- /dev/null +++ b/hugolib/multilingual.go @@ -0,0 +1,117 @@ +// Copyright 2016-present 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 hugolib + +import ( + "sync" + + "sort" + + "errors" + "fmt" + + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/helpers" + "github.com/spf13/cast" +) + +type Multilingual struct { + Languages helpers.Languages + + DefaultLang *helpers.Language + + langMap map[string]*helpers.Language + langMapInit sync.Once +} + +func (ml *Multilingual) Language(lang string) *helpers.Language { + ml.langMapInit.Do(func() { + ml.langMap = make(map[string]*helpers.Language) + for _, l := range ml.Languages { + ml.langMap[l.Lang] = l + } + }) + return ml.langMap[lang] +} + +func newMultiLingualFromSites(cfg config.Provider, sites ...*Site) (*Multilingual, error) { + languages := make(helpers.Languages, len(sites)) + + for i, s := range sites { + if s.Language == nil { + return nil, errors.New("Missing language for site") + } + languages[i] = s.Language + } + + defaultLang := cfg.GetString("defaultContentLanguage") + + if defaultLang == "" { + defaultLang = "en" + } + + return &Multilingual{Languages: languages, DefaultLang: helpers.NewLanguage(defaultLang, cfg)}, nil + +} + +func newMultiLingualForLanguage(language *helpers.Language) *Multilingual { + languages := helpers.Languages{language} + return &Multilingual{Languages: languages, DefaultLang: language} +} +func (ml *Multilingual) enabled() bool { + return len(ml.Languages) > 1 +} + +func (s *Site) multilingualEnabled() bool { + if s.owner == nil { + return false + } + return s.owner.multilingual != nil && s.owner.multilingual.enabled() +} + +func toSortedLanguages(cfg config.Provider, l map[string]interface{}) (helpers.Languages, error) { + langs := make(helpers.Languages, len(l)) + i := 0 + + for lang, langConf := range l { + langsMap, err := cast.ToStringMapE(langConf) + + if err != nil { + return nil, fmt.Errorf("Language config is not a map: %T", langConf) + } + + language := helpers.NewLanguage(lang, cfg) + + for loki, v := range langsMap { + switch loki { + case "title": + language.Title = cast.ToString(v) + case "languagename": + language.LanguageName = cast.ToString(v) + case "weight": + language.Weight = cast.ToInt(v) + } + + // Put all into the Params map + language.SetParam(loki, v) + } + + langs[i] = language + i++ + } + + sort.Sort(langs) + + return langs, nil +} diff --git a/hugolib/node_as_page_test.go b/hugolib/node_as_page_test.go new file mode 100644 index 000000000..6cadafc0d --- /dev/null +++ b/hugolib/node_as_page_test.go @@ -0,0 +1,831 @@ +// Copyright 2016 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 hugolib + +import ( + "fmt" + "path/filepath" + "strings" + "testing" + + "time" + + "github.com/spf13/afero" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/hugofs" + "github.com/stretchr/testify/require" +) + +/* + This file will test the "making everything a page" transition. + + See https://github.com/gohugoio/hugo/issues/2297 + +*/ + +func TestNodesAsPage(t *testing.T) { + t.Parallel() + for _, preserveTaxonomyNames := range []bool{false, true} { + for _, ugly := range []bool{true, false} { + doTestNodeAsPage(t, ugly, preserveTaxonomyNames) + } + } +} + +func doTestNodeAsPage(t *testing.T, ugly, preserveTaxonomyNames bool) { + + /* Will have to decide what to name the node content files, but: + + Home page should have: + Content, shortcode support + Metadata (title, dates etc.) + Params + Taxonomies (categories, tags) + + */ + + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + cfg.Set("uglyURLs", ugly) + cfg.Set("preserveTaxonomyNames", preserveTaxonomyNames) + + cfg.Set("paginate", 1) + cfg.Set("title", "Hugo Rocks") + cfg.Set("rssURI", "customrss.xml") + + writeLayoutsForNodeAsPageTests(t, fs) + writeNodePagesForNodeAsPageTests(t, fs, "") + + writeRegularPagesForNodeAsPageTests(t, fs) + + sites, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg}) + + require.NoError(t, err) + + require.NoError(t, sites.Build(BuildCfg{})) + + // date order: home, sect1, sect2, cat/hugo, cat/web, categories + + th.assertFileContent(filepath.Join("public", "index.html"), + "Index Title: Home Sweet Home!", + "Home <strong>Content!</strong>", + "# Pages: 4", + "Date: 2009-01-02", + "Lastmod: 2009-01-03", + "GetPage: Section1 ", + ) + + th.assertFileContent(expectedFilePath(ugly, "public", "sect1", "regular1"), "Single Title: Page 01", "Content Page 01") + + nodes := sites.findAllPagesByKindNotIn(KindPage) + + require.Len(t, nodes, 8) + + home := nodes[7] // oldest + + require.True(t, home.IsHome()) + require.True(t, home.IsNode()) + require.False(t, home.IsPage()) + require.True(t, home.Path() != "") + + section2 := nodes[5] + require.Equal(t, "Section2", section2.Title) + + pages := sites.findAllPagesByKind(KindPage) + require.Len(t, pages, 4) + + first := pages[0] + + require.False(t, first.IsHome()) + require.False(t, first.IsNode()) + require.True(t, first.IsPage()) + + // Check Home paginator + th.assertFileContent(expectedFilePath(ugly, "public", "page", "2"), + "Pag: Page 02") + + // Check Sections + th.assertFileContent(expectedFilePath(ugly, "public", "sect1"), + "Section Title: Section", "Section1 <strong>Content!</strong>", + "Date: 2009-01-04", + "Lastmod: 2009-01-05", + ) + + th.assertFileContent(expectedFilePath(ugly, "public", "sect2"), + "Section Title: Section", "Section2 <strong>Content!</strong>", + "Date: 2009-01-06", + "Lastmod: 2009-01-07", + ) + + // Check Sections paginator + th.assertFileContent(expectedFilePath(ugly, "public", "sect1", "page", "2"), + "Pag: Page 02") + + sections := sites.findAllPagesByKind(KindSection) + + require.Len(t, sections, 2) + + // Check taxonomy lists + th.assertFileContent(expectedFilePath(ugly, "public", "categories", "hugo"), + "Taxonomy Title: Taxonomy Hugo", "Taxonomy Hugo <strong>Content!</strong>", + "Date: 2009-01-08", + "Lastmod: 2009-01-09", + ) + + th.assertFileContent(expectedFilePath(ugly, "public", "categories", "hugo-rocks"), + "Taxonomy Title: Taxonomy Hugo Rocks", + ) + + s := sites.Sites[0] + + web := s.getPage(KindTaxonomy, "categories", "web") + require.NotNil(t, web) + require.Len(t, web.Data["Pages"].(Pages), 4) + + th.assertFileContent(expectedFilePath(ugly, "public", "categories", "web"), + "Taxonomy Title: Taxonomy Web", + "Taxonomy Web <strong>Content!</strong>", + "Date: 2009-01-10", + "Lastmod: 2009-01-11", + ) + + // Check taxonomy list paginator + th.assertFileContent(expectedFilePath(ugly, "public", "categories", "hugo", "page", "2"), + "Taxonomy Title: Taxonomy Hugo", + "Pag: Page 02") + + // Check taxonomy terms + th.assertFileContent(expectedFilePath(ugly, "public", "categories"), + "Taxonomy Terms Title: Taxonomy Term Categories", "Taxonomy Term Categories <strong>Content!</strong>", "k/v: hugo", + "Date: 2009-01-14", + "Lastmod: 2009-01-15", + ) + + // Check taxonomy terms paginator + th.assertFileContent(expectedFilePath(ugly, "public", "categories", "page", "2"), + "Taxonomy Terms Title: Taxonomy Term Categories", + "Pag: Taxonomy Web") + + // RSS + th.assertFileContent(filepath.Join("public", "customrss.xml"), "Recent content in Home Sweet Home! on Hugo Rocks", "<rss") + th.assertFileContent(filepath.Join("public", "sect1", "customrss.xml"), "Recent content in Section1 on Hugo Rocks", "<rss") + th.assertFileContent(filepath.Join("public", "sect2", "customrss.xml"), "Recent content in Section2 on Hugo Rocks", "<rss") + th.assertFileContent(filepath.Join("public", "categories", "hugo", "customrss.xml"), "Recent content in Taxonomy Hugo on Hugo Rocks", "<rss") + th.assertFileContent(filepath.Join("public", "categories", "web", "customrss.xml"), "Recent content in Taxonomy Web on Hugo Rocks", "<rss") + th.assertFileContent(filepath.Join("public", "categories", "customrss.xml"), "Recent content in Taxonomy Term Categories on Hugo Rocks", "<rss") + +} + +func TestNodesWithNoContentFile(t *testing.T) { + t.Parallel() + for _, ugly := range []bool{false, true} { + doTestNodesWithNoContentFile(t, ugly) + } +} + +func doTestNodesWithNoContentFile(t *testing.T, ugly bool) { + + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + cfg.Set("uglyURLs", ugly) + cfg.Set("paginate", 1) + cfg.Set("title", "Hugo Rocks!") + cfg.Set("rssURI", "customrss.xml") + + writeLayoutsForNodeAsPageTests(t, fs) + writeRegularPagesForNodeAsPageTests(t, fs) + + sites, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg}) + + require.NoError(t, err) + + require.NoError(t, sites.Build(BuildCfg{})) + + s := sites.Sites[0] + + // Home page + homePages := s.findPagesByKind(KindHome) + require.Len(t, homePages, 1) + + homePage := homePages[0] + require.Len(t, homePage.Data["Pages"], 4) + require.Len(t, homePage.Pages, 4) + require.True(t, homePage.Path() == "") + + th.assertFileContent(filepath.Join("public", "index.html"), + "Index Title: Hugo Rocks!", + "Date: 2010-06-12", + "Lastmod: 2010-06-13", + ) + + // Taxonomy list + th.assertFileContent(expectedFilePath(ugly, "public", "categories", "hugo"), + "Taxonomy Title: Hugo", + "Date: 2010-06-12", + "Lastmod: 2010-06-13", + ) + + // Taxonomy terms + th.assertFileContent(expectedFilePath(ugly, "public", "categories"), + "Taxonomy Terms Title: Categories", + ) + + pages := s.findPagesByKind(KindTaxonomyTerm) + for _, p := range pages { + var want string + if ugly { + want = "/" + p.s.PathSpec.URLize(p.Title) + ".html" + } else { + want = "/" + p.s.PathSpec.URLize(p.Title) + "/" + } + if p.URL() != want { + t.Errorf("Taxonomy term URL mismatch: want %q, got %q", want, p.URL()) + } + } + + // Sections + th.assertFileContent(expectedFilePath(ugly, "public", "sect1"), + "Section Title: Sect1s", + "Date: 2010-06-12", + "Lastmod: 2010-06-13", + ) + + th.assertFileContent(expectedFilePath(ugly, "public", "sect2"), + "Section Title: Sect2s", + "Date: 2008-07-06", + "Lastmod: 2008-07-09", + ) + + // RSS + th.assertFileContent(filepath.Join("public", "customrss.xml"), "Hugo Rocks!", "<rss") + th.assertFileContent(filepath.Join("public", "sect1", "customrss.xml"), "Recent content in Sect1s on Hugo Rocks!", "<rss") + th.assertFileContent(filepath.Join("public", "sect2", "customrss.xml"), "Recent content in Sect2s on Hugo Rocks!", "<rss") + th.assertFileContent(filepath.Join("public", "categories", "hugo", "customrss.xml"), "Recent content in Hugo on Hugo Rocks!", "<rss") + th.assertFileContent(filepath.Join("public", "categories", "web", "customrss.xml"), "Recent content in Web on Hugo Rocks!", "<rss") + +} + +func TestNodesAsPageMultilingual(t *testing.T) { + t.Parallel() + for _, ugly := range []bool{false, true} { + t.Run(fmt.Sprintf("ugly=%t", ugly), func(t *testing.T) { + doTestNodesAsPageMultilingual(t, ugly) + }) + } +} + +func doTestNodesAsPageMultilingual(t *testing.T, ugly bool) { + + mf := afero.NewMemMapFs() + + writeToFs(t, mf, "config.toml", + ` +paginage = 1 +title = "Hugo Multilingual Rocks!" +rssURI = "customrss.xml" +defaultContentLanguage = "nn" +defaultContentLanguageInSubdir = true + + +[languages] +[languages.nn] +languageName = "Nynorsk" +weight = 1 +title = "Hugo på norsk" + +[languages.en] +languageName = "English" +weight = 2 +title = "Hugo in English" + +[languages.de] +languageName = "Deutsch" +weight = 3 +title = "Deutsche Hugo" +`) + + cfg, err := LoadConfig(mf, "", "config.toml") + require.NoError(t, err) + + cfg.Set("uglyURLs", ugly) + + fs := hugofs.NewFrom(mf, cfg) + + writeLayoutsForNodeAsPageTests(t, fs) + + for _, lang := range []string{"nn", "en"} { + writeRegularPagesForNodeAsPageTestsWithLang(t, fs, lang) + } + + th := testHelper{cfg, fs, t} + + sites, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg}) + + if err != nil { + t.Fatalf("Failed to create sites: %s", err) + } + + if len(sites.Sites) != 3 { + t.Fatalf("Got %d sites", len(sites.Sites)) + } + + // Only write node pages for the English and Deutsch + writeNodePagesForNodeAsPageTests(t, fs, "en") + writeNodePagesForNodeAsPageTests(t, fs, "de") + + err = sites.Build(BuildCfg{}) + + if err != nil { + t.Fatalf("Failed to build sites: %s", err) + } + + // The en and de language have content pages + enHome := sites.Sites[1].getPage("home") + require.NotNil(t, enHome) + require.Equal(t, "en", enHome.Language().Lang) + require.Contains(t, enHome.Content, "l-en") + + deHome := sites.Sites[2].getPage("home") + require.NotNil(t, deHome) + require.Equal(t, "de", deHome.Language().Lang) + require.Contains(t, deHome.Content, "l-de") + + require.Len(t, deHome.Translations(), 2, deHome.Translations()[0].Language().Lang) + require.Equal(t, "en", deHome.Translations()[1].Language().Lang) + require.Equal(t, "nn", deHome.Translations()[0].Language().Lang) + // See issue #3179 + require.Equal(t, expetedPermalink(false, "/de/"), deHome.Permalink()) + + enSect := sites.Sites[1].getPage("section", "sect1") + require.NotNil(t, enSect) + require.Equal(t, "en", enSect.Language().Lang) + require.Len(t, enSect.Translations(), 2, enSect.Translations()[0].Language().Lang) + require.Equal(t, "de", enSect.Translations()[1].Language().Lang) + require.Equal(t, "nn", enSect.Translations()[0].Language().Lang) + + require.Equal(t, expetedPermalink(ugly, "/en/sect1/"), enSect.Permalink()) + + th.assertFileContent(filepath.Join("public", "nn", "index.html"), + "Index Title: Hugo på norsk") + th.assertFileContent(filepath.Join("public", "en", "index.html"), + "Index Title: Home Sweet Home!", "<strong>Content!</strong>") + th.assertFileContent(filepath.Join("public", "de", "index.html"), + "Index Title: Home Sweet Home!", "<strong>Content!</strong>") + + // Taxonomy list + th.assertFileContent(expectedFilePath(ugly, "public", "nn", "categories", "hugo"), + "Taxonomy Title: Hugo") + th.assertFileContent(expectedFilePath(ugly, "public", "en", "categories", "hugo"), + "Taxonomy Title: Taxonomy Hugo") + + // Taxonomy terms + th.assertFileContent(expectedFilePath(ugly, "public", "nn", "categories"), + "Taxonomy Terms Title: Categories") + th.assertFileContent(expectedFilePath(ugly, "public", "en", "categories"), + "Taxonomy Terms Title: Taxonomy Term Categories") + + // Sections + th.assertFileContent(expectedFilePath(ugly, "public", "nn", "sect1"), + "Section Title: Sect1s") + th.assertFileContent(expectedFilePath(ugly, "public", "nn", "sect2"), + "Section Title: Sect2s") + th.assertFileContent(expectedFilePath(ugly, "public", "en", "sect1"), + "Section Title: Section1") + th.assertFileContent(expectedFilePath(ugly, "public", "en", "sect2"), + "Section Title: Section2") + + // Regular pages + th.assertFileContent(expectedFilePath(ugly, "public", "en", "sect1", "regular1"), + "Single Title: Page 01") + th.assertFileContent(expectedFilePath(ugly, "public", "nn", "sect1", "regular2"), + "Single Title: Page 02") + + // RSS + th.assertFileContent(filepath.Join("public", "nn", "customrss.xml"), "Hugo på norsk", "<rss") + th.assertFileContent(filepath.Join("public", "nn", "sect1", "customrss.xml"), "Recent content in Sect1s on Hugo på norsk", "<rss") + th.assertFileContent(filepath.Join("public", "nn", "sect2", "customrss.xml"), "Recent content in Sect2s on Hugo på norsk", "<rss") + th.assertFileContent(filepath.Join("public", "nn", "categories", "hugo", "customrss.xml"), "Recent content in Hugo on Hugo på norsk", "<rss") + th.assertFileContent(filepath.Join("public", "nn", "categories", "web", "customrss.xml"), "Recent content in Web on Hugo på norsk", "<rss") + + th.assertFileContent(filepath.Join("public", "en", "customrss.xml"), "Recent content in Home Sweet Home! on Hugo in English", "<rss") + th.assertFileContent(filepath.Join("public", "en", "sect1", "customrss.xml"), "Recent content in Section1 on Hugo in English", "<rss") + th.assertFileContent(filepath.Join("public", "en", "sect2", "customrss.xml"), "Recent content in Section2 on Hugo in English", "<rss") + th.assertFileContent(filepath.Join("public", "en", "categories", "hugo", "customrss.xml"), "Recent content in Taxonomy Hugo on Hugo in English", "<rss") + th.assertFileContent(filepath.Join("public", "en", "categories", "web", "customrss.xml"), "Recent content in Taxonomy Web on Hugo in English", "<rss") + +} + +func TestNodesWithTaxonomies(t *testing.T) { + t.Parallel() + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + cfg.Set("paginate", 1) + cfg.Set("title", "Hugo Rocks!") + + writeLayoutsForNodeAsPageTests(t, fs) + writeRegularPagesForNodeAsPageTests(t, fs) + + writeSource(t, fs, filepath.Join("content", "_index.md"), `--- +title: Home With Taxonomies +categories: [ + "Hugo", + "Home" +] +--- +`) + + h, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg}) + + require.NoError(t, err) + + require.NoError(t, h.Build(BuildCfg{})) + + th.assertFileContent(filepath.Join("public", "categories", "hugo", "index.html"), "Taxonomy Title: Hugo", "# Pages: 5") + th.assertFileContent(filepath.Join("public", "categories", "home", "index.html"), "Taxonomy Title: Home", "# Pages: 1") + +} + +func TestNodesWithMenu(t *testing.T) { + t.Parallel() + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + cfg.Set("paginate", 1) + cfg.Set("title", "Hugo Rocks!") + + writeLayoutsForNodeAsPageTests(t, fs) + writeRegularPagesForNodeAsPageTests(t, fs) + + writeSource(t, fs, filepath.Join("content", "_index.md"), `--- +title: Home With Menu +menu: + mymenu: + name: "Go Home!" +--- +`) + + writeSource(t, fs, filepath.Join("content", "sect1", "_index.md"), `--- +title: Sect1 With Menu +menu: + mymenu: + name: "Go Sect1!" +--- +`) + + writeSource(t, fs, filepath.Join("content", "categories", "hugo", "_index.md"), `--- +title: Taxonomy With Menu +menu: + mymenu: + name: "Go Tax Hugo!" +--- +`) + + h, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg}) + + require.NoError(t, err) + + require.NoError(t, h.Build(BuildCfg{})) + + th.assertFileContent(filepath.Join("public", "index.html"), "Home With Menu", "Home Menu Item: Go Home!: /") + th.assertFileContent(filepath.Join("public", "sect1", "index.html"), "Sect1 With Menu", "Section Menu Item: Go Sect1!: /sect1/") + th.assertFileContent(filepath.Join("public", "categories", "hugo", "index.html"), "Taxonomy With Menu", "Taxonomy Menu Item: Go Tax Hugo!: /categories/hugo/") + +} + +func TestNodesWithAlias(t *testing.T) { + t.Parallel() + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + cfg.Set("paginate", 1) + cfg.Set("baseURL", "http://base/") + cfg.Set("title", "Hugo Rocks!") + + writeLayoutsForNodeAsPageTests(t, fs) + writeRegularPagesForNodeAsPageTests(t, fs) + + writeSource(t, fs, filepath.Join("content", "_index.md"), `--- +title: Home With Alias +aliases: + - /my/new/home.html +--- +`) + + h, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg}) + + require.NoError(t, err) + + require.NoError(t, h.Build(BuildCfg{})) + + th.assertFileContent(filepath.Join("public", "index.html"), "Home With Alias") + th.assertFileContent(filepath.Join("public", "my", "new", "home.html"), "content=\"0; url=http://base/") + +} + +func TestNodesWithSectionWithIndexPageOnly(t *testing.T) { + t.Parallel() + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + cfg.Set("paginate", 1) + cfg.Set("title", "Hugo Rocks!") + + writeLayoutsForNodeAsPageTests(t, fs) + + writeSource(t, fs, filepath.Join("content", "sect", "_index.md"), `--- +title: MySection +--- +My Section Content +`) + + h, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg}) + + require.NoError(t, err) + + require.NoError(t, h.Build(BuildCfg{})) + + th.assertFileContent(filepath.Join("public", "sect", "index.html"), "My Section") + +} + +func TestNodesWithURLs(t *testing.T) { + t.Parallel() + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) + + cfg.Set("paginate", 1) + cfg.Set("title", "Hugo Rocks!") + cfg.Set("baseURL", "http://bep.is/base/") + + writeLayoutsForNodeAsPageTests(t, fs) + writeRegularPagesForNodeAsPageTests(t, fs) + + writeSource(t, fs, filepath.Join("content", "sect", "_index.md"), `--- +title: MySection +url: foo.html +--- +My Section Content +`) + + h, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg}) + + require.NoError(t, err) + + require.NoError(t, h.Build(BuildCfg{})) + + th.assertFileContent(filepath.Join("public", "sect", "index.html"), "My Section") + + s := h.Sites[0] + + p := s.RegularPages[0] + + require.Equal(t, "/base/sect1/regular1/", p.URL()) + + // Section with front matter and url set (which should not be used) + sect := s.getPage(KindSection, "sect") + require.Equal(t, "/base/sect/", sect.URL()) + require.Equal(t, "http://bep.is/base/sect/", sect.Permalink()) + require.Equal(t, "/base/sect/", sect.RelPermalink()) + + // Home page without front matter + require.Equal(t, "/base/", s.getPage(KindHome).URL()) + +} + +func writeRegularPagesForNodeAsPageTests(t *testing.T, fs *hugofs.Fs) { + writeRegularPagesForNodeAsPageTestsWithLang(t, fs, "") +} + +func writeRegularPagesForNodeAsPageTestsWithLang(t *testing.T, fs *hugofs.Fs, lang string) { + var langStr string + + if lang != "" { + langStr = lang + "." + } + + format := "2006-01-02" + + date, _ := time.Parse(format, "2010-06-15") + + for i := 1; i <= 4; i++ { + sect := "sect1" + if i > 2 { + sect = "sect2" + + date, _ = time.Parse(format, "2008-07-15") // Nodes are placed in 2009 + + } + date = date.Add(-24 * time.Duration(i) * time.Hour) + writeSource(t, fs, filepath.Join("content", sect, fmt.Sprintf("regular%d.%smd", i, langStr)), fmt.Sprintf(`--- +title: Page %02d +lastMod : %q +date : %q +categories: [ + "Hugo", + "Web", + "Hugo Rocks!" +] +--- +Content Page %02d +`, i, date.Add(time.Duration(i)*-24*time.Hour).Format(time.RFC822), date.Add(time.Duration(i)*-2*24*time.Hour).Format(time.RFC822), i)) + } +} + +func writeNodePagesForNodeAsPageTests(t *testing.T, fs *hugofs.Fs, lang string) { + + filename := "_index.md" + + if lang != "" { + filename = fmt.Sprintf("_index.%s.md", lang) + } + + format := "2006-01-02" + + date, _ := time.Parse(format, "2009-01-01") + + writeSource(t, fs, filepath.Join("content", filename), fmt.Sprintf(`--- +title: Home Sweet Home! +date : %q +lastMod : %q +--- +l-%s Home **Content!** +`, date.Add(1*24*time.Hour).Format(time.RFC822), date.Add(2*24*time.Hour).Format(time.RFC822), lang)) + + writeSource(t, fs, filepath.Join("content", "sect1", filename), fmt.Sprintf(`--- +title: Section1 +date : %q +lastMod : %q +--- +Section1 **Content!** +`, date.Add(3*24*time.Hour).Format(time.RFC822), date.Add(4*24*time.Hour).Format(time.RFC822))) + writeSource(t, fs, filepath.Join("content", "sect2", filename), fmt.Sprintf(`--- +title: Section2 +date : %q +lastMod : %q +--- +Section2 **Content!** +`, date.Add(5*24*time.Hour).Format(time.RFC822), date.Add(6*24*time.Hour).Format(time.RFC822))) + + writeSource(t, fs, filepath.Join("content", "categories", "hugo", filename), fmt.Sprintf(`--- +title: Taxonomy Hugo +date : %q +lastMod : %q +--- +Taxonomy Hugo **Content!** +`, date.Add(7*24*time.Hour).Format(time.RFC822), date.Add(8*24*time.Hour).Format(time.RFC822))) + + writeSource(t, fs, filepath.Join("content", "categories", "web", filename), fmt.Sprintf(`--- +title: Taxonomy Web +date : %q +lastMod : %q +--- +Taxonomy Web **Content!** +`, date.Add(9*24*time.Hour).Format(time.RFC822), date.Add(10*24*time.Hour).Format(time.RFC822))) + + writeSource(t, fs, filepath.Join("content", "categories", "hugo-rocks", filename), fmt.Sprintf(`--- +title: Taxonomy Hugo Rocks +date : %q +lastMod : %q +--- +Taxonomy Hugo Rocks **Content!** +`, date.Add(11*24*time.Hour).Format(time.RFC822), date.Add(12*24*time.Hour).Format(time.RFC822))) + + writeSource(t, fs, filepath.Join("content", "categories", filename), fmt.Sprintf(`--- +title: Taxonomy Term Categories +date : %q +lastMod : %q +--- +Taxonomy Term Categories **Content!** +`, date.Add(13*24*time.Hour).Format(time.RFC822), date.Add(14*24*time.Hour).Format(time.RFC822))) + + writeSource(t, fs, filepath.Join("content", "tags", filename), fmt.Sprintf(`--- +title: Taxonomy Term Tags +date : %q +lastMod : %q +--- +Taxonomy Term Tags **Content!** +`, date.Add(15*24*time.Hour).Format(time.RFC822), date.Add(16*24*time.Hour).Format(time.RFC822))) + +} + +func writeLayoutsForNodeAsPageTests(t *testing.T, fs *hugofs.Fs) { + writeSource(t, fs, filepath.Join("layouts", "index.html"), ` +Index Title: {{ .Title }} +Index Content: {{ .Content }} +# Pages: {{ len .Data.Pages }} +{{ range .Paginator.Pages }} + Pag: {{ .Title }} +{{ end }} +{{ with .Site.Menus.mymenu }} +{{ range . }} +Home Menu Item: {{ .Name }}: {{ .URL }} +{{ end }} +{{ end }} +Date: {{ .Date.Format "2006-01-02" }} +Lastmod: {{ .Lastmod.Format "2006-01-02" }} +GetPage: {{ with .Site.GetPage "section" "sect1" }}{{ .Title }}{{ end }} +`) + + writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), ` +Single Title: {{ .Title }} +Single Content: {{ .Content }} +Date: {{ .Date.Format "2006-01-02" }} +Lastmod: {{ .Lastmod.Format "2006-01-02" }} +`) + + writeSource(t, fs, filepath.Join("layouts", "_default", "section.html"), ` +Section Title: {{ .Title }} +Section Content: {{ .Content }} +# Pages: {{ len .Data.Pages }} +{{ range .Paginator.Pages }} + Pag: {{ .Title }} +{{ end }} +{{ with .Site.Menus.mymenu }} +{{ range . }} +Section Menu Item: {{ .Name }}: {{ .URL }} +{{ end }} +{{ end }} +Date: {{ .Date.Format "2006-01-02" }} +Lastmod: {{ .Lastmod.Format "2006-01-02" }} +`) + + // Taxonomy lists + writeSource(t, fs, filepath.Join("layouts", "_default", "taxonomy.html"), ` +Taxonomy Title: {{ .Title }} +Taxonomy Content: {{ .Content }} +# Pages: {{ len .Data.Pages }} +{{ range .Paginator.Pages }} + Pag: {{ .Title }} +{{ end }} +{{ with .Site.Menus.mymenu }} +{{ range . }} +Taxonomy Menu Item: {{ .Name }}: {{ .URL }} +{{ end }} +{{ end }} +Date: {{ .Date.Format "2006-01-02" }} +Lastmod: {{ .Lastmod.Format "2006-01-02" }} +`) + + // Taxonomy terms + writeSource(t, fs, filepath.Join("layouts", "_default", "terms.html"), ` +Taxonomy Terms Title: {{ .Title }} +Taxonomy Terms Content: {{ .Content }} +# Pages: {{ len .Data.Pages }} +{{ range .Paginator.Pages }} + Pag: {{ .Title }} +{{ end }} +{{ range $key, $value := .Data.Terms }} + k/v: {{ $key | lower }} / {{ printf "%s" $value }} +{{ end }} +{{ with .Site.Menus.mymenu }} +{{ range . }} +Taxonomy Terms Menu Item: {{ .Name }}: {{ .URL }} +{{ end }} +{{ end }} +Date: {{ .Date.Format "2006-01-02" }} +Lastmod: {{ .Lastmod.Format "2006-01-02" }} +`) +} + +func expectedFilePath(ugly bool, path ...string) string { + if ugly { + return filepath.Join(append(path[0:len(path)-1], path[len(path)-1]+".html")...) + } + return filepath.Join(append(path, "index.html")...) +} + +func expetedPermalink(ugly bool, path string) string { + if ugly { + return strings.TrimSuffix(path, "/") + ".html" + } + return path +} diff --git a/hugolib/page.go b/hugolib/page.go index bb3835c1e..0f44b8b99 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -1,4 +1,4 @@ -// Copyright 2024 The Hugo Authors. All rights reserved. +// Copyright 2016 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,771 +14,1764 @@ package hugolib import ( - "context" + "bytes" + "errors" "fmt" - "path/filepath" - "strconv" - "strings" - "sync/atomic" + "reflect" - "github.com/gohugoio/hugo/hugofs" - "github.com/gohugoio/hugo/hugolib/doctree" - "github.com/gohugoio/hugo/hugolib/segments" - "github.com/gohugoio/hugo/identity" - "github.com/gohugoio/hugo/media" + "github.com/bep/gitmap" + + "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/output" - "github.com/gohugoio/hugo/related" - "github.com/gohugoio/hugo/resources" - "github.com/gohugoio/hugo/tpl/tplimpl" - "github.com/spf13/afero" + "github.com/gohugoio/hugo/parser" + "github.com/mitchellh/mapstructure" - "github.com/gohugoio/hugo/markup/converter" - "github.com/gohugoio/hugo/markup/tableofcontents" - - "github.com/gohugoio/hugo/common/herrors" - "github.com/gohugoio/hugo/common/types" + "html/template" + "io" + "path" + "path/filepath" + "regexp" + "strings" + "sync" + "time" + "unicode/utf8" + bp "github.com/gohugoio/hugo/bufferpool" "github.com/gohugoio/hugo/source" - - "github.com/gohugoio/hugo/common/collections" - "github.com/gohugoio/hugo/common/text" - "github.com/gohugoio/hugo/resources/kinds" - "github.com/gohugoio/hugo/resources/page" - "github.com/gohugoio/hugo/resources/resource" + "github.com/spf13/cast" ) var ( - _ page.Page = (*pageState)(nil) - _ collections.Grouper = (*pageState)(nil) - _ collections.Slicer = (*pageState)(nil) - _ identity.DependencyManagerScopedProvider = (*pageState)(nil) - _ contentNodeI = (*pageState)(nil) - _ pageContext = (*pageState)(nil) + cjk = regexp.MustCompile(`\p{Han}|\p{Hangul}|\p{Hiragana}|\p{Katakana}`) + + // This is all the kinds we can expect to find in .Site.Pages. + allKindsInPages = []string{KindPage, KindHome, KindSection, KindTaxonomy, KindTaxonomyTerm} + + allKinds = append(allKindsInPages, []string{kindRSS, kindSitemap, kindRobotsTXT, kind404}...) ) -var ( - pageTypesProvider = resource.NewResourceTypesProvider(media.Builtin.OctetType, pageResourceType) - nopPageOutput = &pageOutput{ - pagePerOutputProviders: nopPagePerOutput, - MarkupProvider: page.NopPage, - ContentProvider: page.NopPage, - } +const ( + KindPage = "page" + + // The rest are node types; home page, sections etc. + KindHome = "home" + KindSection = "section" + KindTaxonomy = "taxonomy" + KindTaxonomyTerm = "taxonomyTerm" + + // Temporary state. + kindUnknown = "unknown" + + // The following are (currently) temporary nodes, + // i.e. nodes we create just to render in isolation. + kindRSS = "RSS" + kindSitemap = "sitemap" + kindRobotsTXT = "robotsTXT" + kind404 = "404" ) -// pageContext provides contextual information about this page, for error -// logging and similar. -type pageContext interface { - posOffset(offset int) text.Position - wrapError(err error) error - getContentConverter() converter.Converter -} +type Page struct { + *pageInit + + // Kind is the discriminator that identifies the different page types + // in the different page collections. This can, as an example, be used + // to to filter regular pages, find sections etc. + // Kind will, for the pages available to the templates, be one of: + // page, home, section, taxonomy and taxonomyTerm. + // It is of string type to make it easy to reason about in + // the templates. + Kind string + + // Since Hugo 0.18 we got rid of the Node type. So now all pages are ... + // pages (regular pages, home page, sections etc.). + // Sections etc. will have child pages. These were earlier placed in .Data.Pages, + // but can now be more intuitively also be fetched directly from .Pages. + // This collection will be nil for regular pages. + Pages Pages + + // translations will contain references to this page in other language + // if available. + translations Pages + + // Params contains configuration defined in the params section of page frontmatter. + Params map[string]interface{} + + // Content sections + Content template.HTML + Summary template.HTML + TableOfContents template.HTML + + Aliases []string + + Images []Image + Videos []Video + + Truncated bool + Draft bool + Status string + + PublishDate time.Time + ExpiryDate time.Time + + // PageMeta contains page stats such as word count etc. + PageMeta + + // Markup contains the markup type for the content. + Markup string + + extension string + contentType string + renderable bool + + Layout string + + // For npn-renderable pages (see IsRenderable), the content itself + // is used as template and the template name is stored here. + selfLayout string + + linkTitle string + + frontmatter []byte + + // rawContent is the raw content read from the content file. + rawContent []byte + + // workContent is a copy of rawContent that may be mutated during site build. + workContent []byte + + // state telling if this is a "new page" or if we have rendered it previously. + rendered bool + + // whether the content is in a CJK language. + isCJKLanguage bool + + shortcodeState *shortcodeHandler + + // the content stripped for HTML + plain string // TODO should be []byte + plainWords []string + + // rendering configuration + renderingConfig *helpers.Blackfriday + + // menus + pageMenus PageMenus + + Source + + Position `json:"-"` + + GitInfo *gitmap.GitInfo + + // This was added as part of getting the Nodes (taxonomies etc.) to work as + // Pages in Hugo 0.18. + // It is deliberately named similar to Section, but not exported (for now). + // We currently have only one level of section in Hugo, but the page can live + // any number of levels down the file path. + // To support taxonomies like /categories/hugo etc. we will need to keep track + // of that information in a general way. + // So, sections represents the path to the content, i.e. a content file or a + // virtual content file in the situations where a taxonomy or a section etc. + // isn't accomanied by one. + sections []string + + // Will only be set for sections and regular pages. + parent *Page + + // When we create paginator pages, we create a copy of the original, + // but keep track of it here. + origOnCopy *Page + + // Will only be set for section pages and the home page. + subSections Pages -type pageSiteAdapter struct { - p page.Page s *Site + + // Pulled over from old Node. TODO(bep) reorg and group (embed) + + Site *SiteInfo `json:"-"` + + Title string + Description string + Keywords []string + Data map[string]interface{} + + Date time.Time + Lastmod time.Time + + Sitemap Sitemap + + URLPath + permalink string + relPermalink string + + layoutDescriptor output.LayoutDescriptor + + scratch *Scratch + + // It would be tempting to use the language set on the Site, but in they way we do + // multi-site processing, these values may differ during the initial page processing. + language *helpers.Language + + lang string + + // The output formats this page will be rendered to. + outputFormats output.Formats + + // This is the PageOutput that represents the first item in outputFormats. + // Use with care, as there are potential for inifinite loops. + mainPageOutput *PageOutput + + targetPathDescriptorPrototype *targetPathDescriptor } -func (pa pageSiteAdapter) GetPage(ref string) (page.Page, error) { - p, err := pa.s.getPage(pa.p, ref) - - if p == nil { - // The nil struct has meaning in some situations, mostly to avoid breaking - // existing sites doing $nilpage.IsDescendant($p), which will always return - // false. - p = page.NilPage +func (p *Page) RSSLink() template.URL { + f, found := p.outputFormats.GetByName(output.RSSFormat.Name) + if !found { + return "" } - return p, err + return template.URL(newOutputFormat(p, f).Permalink()) } -type pageState struct { - // Incremented for each new page created. - // Note that this will change between builds for a given Page. - pid uint64 +func (p *Page) createLayoutDescriptor() output.LayoutDescriptor { + var section string - // This slice will be of same length as the number of global slice of output - // formats (for all sites). - pageOutputs []*pageOutput - - // Used to determine if we can reuse content across output formats. - pageOutputTemplateVariationsState *atomic.Uint32 - - // This will be shifted out when we start to render a new output format. - pageOutputIdx int - *pageOutput - - // Common for all output formats. - *pageCommon - - resource.Staler - dependencyManager identity.Manager -} - -func (p *pageState) incrPageOutputTemplateVariation() { - p.pageOutputTemplateVariationsState.Add(1) -} - -func (p *pageState) canReusePageOutputContent() bool { - return p.pageOutputTemplateVariationsState.Load() == 1 -} - -func (p *pageState) IdentifierBase() string { - return p.Path() -} - -func (p *pageState) GetIdentity() identity.Identity { - return p -} - -func (p *pageState) ForEeachIdentity(f func(identity.Identity) bool) bool { - return f(p) -} - -func (p *pageState) GetDependencyManager() identity.Manager { - return p.dependencyManager -} - -func (p *pageState) GetDependencyManagerForScope(scope int) identity.Manager { - switch scope { - case pageDependencyScopeDefault: - return p.dependencyManagerOutput - case pageDependencyScopeGlobal: - return p.dependencyManager + switch p.Kind { + case KindSection: + // In Hugo 0.22 we introduce nested sections, but we still only + // use the first level to pick the correct template. This may change in + // the future. + section = p.sections[0] + case KindTaxonomy, KindTaxonomyTerm: + section = p.s.taxonomiesPluralSingular[p.sections[0]] default: - return identity.NopManager + } + + return output.LayoutDescriptor{ + Kind: p.Kind, + Type: p.Type(), + Lang: p.Lang(), + Layout: p.Layout, + Section: section, } } -func (p *pageState) GetDependencyManagerForScopesAll() []identity.Manager { - return []identity.Manager{p.dependencyManager, p.dependencyManagerOutput} +// pageInit lazy initializes different parts of the page. It is extracted +// into its own type so we can easily create a copy of a given page. +type pageInit struct { + languageInit sync.Once + pageMenusInit sync.Once + pageMetaInit sync.Once + pageOutputInit sync.Once + plainInit sync.Once + plainWordsInit sync.Once + renderingConfigInit sync.Once } -func (p *pageState) Key() string { - return "page-" + strconv.FormatUint(p.pid, 10) +// IsNode returns whether this is an item of one of the list types in Hugo, +// i.e. not a regular content page. +func (p *Page) IsNode() bool { + return p.Kind != KindPage } -// RelatedKeywords implements the related.Document interface needed for fast page searches. -func (p *pageState) RelatedKeywords(cfg related.IndexConfig) ([]related.Keyword, error) { - v, found, err := page.NamedPageMetaValue(p, cfg.Name) +// IsHome returns whether this is the home page. +func (p *Page) IsHome() bool { + return p.Kind == KindHome +} + +// IsSection returns whether this is a section page. +func (p *Page) IsSection() bool { + return p.Kind == KindSection +} + +// IsPage returns whether this is a regular content page. +func (p *Page) IsPage() bool { + return p.Kind == KindPage +} + +type Source struct { + Frontmatter []byte + Content []byte + source.File +} +type PageMeta struct { + wordCount int + fuzzyWordCount int + readingTime int + Weight int +} + +type Position struct { + Prev *Page + Next *Page + PrevInSection *Page + NextInSection *Page +} + +type Pages []*Page + +func (ps Pages) String() string { + return fmt.Sprintf("Pages(%d)", len(ps)) +} + +func (ps Pages) findPagePosByFilePath(inPath string) int { + for i, x := range ps { + if x.Source.Path() == inPath { + return i + } + } + return -1 +} + +func (ps Pages) findFirstPagePosByFilePathPrefix(prefix string) int { + if prefix == "" { + return -1 + } + for i, x := range ps { + if strings.HasPrefix(x.Source.Path(), prefix) { + return i + } + } + return -1 +} + +// findPagePos Given a page, it will find the position in Pages +// will return -1 if not found +func (ps Pages) findPagePos(page *Page) int { + for i, x := range ps { + if x.Source.Path() == page.Source.Path() { + return i + } + } + return -1 +} + +func (p *Page) createWorkContentCopy() { + p.workContent = make([]byte, len(p.rawContent)) + copy(p.workContent, p.rawContent) +} + +func (p *Page) Plain() string { + p.initPlain() + return p.plain +} + +func (p *Page) PlainWords() []string { + p.initPlainWords() + return p.plainWords +} + +func (p *Page) initPlain() { + p.plainInit.Do(func() { + p.plain = helpers.StripHTML(string(p.Content)) + return + }) +} + +func (p *Page) initPlainWords() { + p.plainWordsInit.Do(func() { + p.plainWords = strings.Fields(p.Plain()) + return + }) +} + +// Param is a convenience method to do lookups in Page's and Site's Params map, +// in that order. +// +// This method is also implemented on Node and SiteInfo. +func (p *Page) Param(key interface{}) (interface{}, error) { + keyStr, err := cast.ToStringE(key) if err != nil { return nil, err } - if !found { + keyStr = strings.ToLower(keyStr) + result, _ := p.traverseDirect(keyStr) + if result != nil { + return result, nil + } + + keySegments := strings.Split(keyStr, ".") + if len(keySegments) == 1 { return nil, nil } - return cfg.ToKeywords(v) + return p.traverseNested(keySegments) } -func (p *pageState) resetBuildState() { - // Nothing to do for now. +func (p *Page) traverseDirect(key string) (interface{}, error) { + keyStr := strings.ToLower(key) + if val, ok := p.Params[keyStr]; ok { + return val, nil + } + + return p.Site.Params[keyStr], nil } -func (p *pageState) skipRender() bool { - b := p.s.conf.C.SegmentFilter.ShouldExcludeFine( - segments.SegmentMatcherFields{ - Path: p.Path(), - Kind: p.Kind(), - Lang: p.Lang(), - Output: p.pageOutput.f.Name, - }, +func (p *Page) traverseNested(keySegments []string) (interface{}, error) { + result := traverse(keySegments, p.Params) + if result != nil { + return result, nil + } + + result = traverse(keySegments, p.Site.Params) + if result != nil { + return result, nil + } + + // Didn't find anything, but also no problems. + return nil, nil +} + +func traverse(keys []string, m map[string]interface{}) interface{} { + // Shift first element off. + firstKey, rest := keys[0], keys[1:] + result := m[firstKey] + + // No point in continuing here. + if result == nil { + return result + } + + if len(rest) == 0 { + // That was the last key. + return result + } else { + // That was not the last key. + return traverse(rest, cast.ToStringMap(result)) + } +} + +func (p *Page) Author() Author { + authors := p.Authors() + + for _, author := range authors { + return author + } + return Author{} +} + +func (p *Page) Authors() AuthorList { + authorKeys, ok := p.Params["authors"] + if !ok { + return AuthorList{} + } + authors := authorKeys.([]string) + if len(authors) < 1 || len(p.Site.Authors) < 1 { + return AuthorList{} + } + + al := make(AuthorList) + for _, author := range authors { + a, ok := p.Site.Authors[author] + if ok { + al[author] = a + } + } + return al +} + +func (p *Page) UniqueID() string { + return p.Source.UniqueID() +} + +// for logging +func (p *Page) lineNumRawContentStart() int { + return bytes.Count(p.frontmatter, []byte("\n")) + 1 +} + +var ( + internalSummaryDivider = []byte("HUGOMORE42") +) + +// We have to replace the <!--more--> with something that survives all the +// rendering engines. +// TODO(bep) inline replace +func (p *Page) replaceDivider(content []byte) []byte { + summaryDivider := helpers.SummaryDivider + // TODO(bep) handle better. + if p.Ext() == "org" || p.Markup == "org" { + summaryDivider = []byte("# more") + } + sections := bytes.Split(content, summaryDivider) + + // If the raw content has nothing but whitespace after the summary + // marker then the page shouldn't be marked as truncated. This check + // is simplest against the raw content because different markup engines + // (rst and asciidoc in particular) add div and p elements after the + // summary marker. + p.Truncated = (len(sections) == 2 && + len(bytes.Trim(sections[1], " \n\r")) > 0) + + return bytes.Join(sections, internalSummaryDivider) +} + +// Returns the page as summary and main if a user defined split is provided. +func (p *Page) setUserDefinedSummaryIfProvided(rawContentCopy []byte) (*summaryContent, error) { + + sc, err := splitUserDefinedSummaryAndContent(p.Markup, rawContentCopy) + + if err != nil { + return nil, err + } + + if sc == nil { + // No divider found + return nil, nil + } + + p.Summary = helpers.BytesToHTML(sc.summary) + + return sc, nil +} + +// Make this explicit so there is no doubt about what is what. +type summaryContent struct { + summary []byte + content []byte +} + +func splitUserDefinedSummaryAndContent(markup string, c []byte) (sc *summaryContent, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("summary split failed: %s", r) + } + }() + + c = bytes.TrimSpace(c) + startDivider := bytes.Index(c, internalSummaryDivider) + + if startDivider == -1 { + return + } + + endDivider := startDivider + len(internalSummaryDivider) + endSummary := startDivider + + var ( + startMarkup []byte + endMarkup []byte + addDiv bool ) - return b + switch markup { + default: + startMarkup = []byte("<p>") + endMarkup = []byte("</p>") + case "asciidoc": + startMarkup = []byte("<div class=\"paragraph\">") + endMarkup = []byte("</div>") + case "rst": + startMarkup = []byte("<p>") + endMarkup = []byte("</p>") + addDiv = true + } + + // Find the closest end/start markup string to the divider + fromStart := -1 + fromIdx := bytes.LastIndex(c[:startDivider], startMarkup) + if fromIdx != -1 { + fromStart = startDivider - fromIdx - len(startMarkup) + } + fromEnd := bytes.Index(c[endDivider:], endMarkup) + + if fromEnd != -1 && fromEnd <= fromStart { + endSummary = startDivider + fromEnd + len(endMarkup) + } else if fromStart != -1 && fromEnd != -1 { + endSummary = startDivider - fromStart - len(startMarkup) + } + + withoutDivider := bytes.TrimSpace(append(c[:startDivider], c[endDivider:]...)) + var ( + summary []byte + ) + + if len(withoutDivider) > 0 { + summary = bytes.TrimSpace(withoutDivider[:endSummary]) + } + + if addDiv { + // For the rst + summary = append(append([]byte(nil), summary...), []byte("</div>")...) + } + + if err != nil { + return + } + + sc = &summaryContent{ + summary: summary, + content: withoutDivider, + } + + return } -func (po *pageState) isRenderedAny() bool { - for _, o := range po.pageOutputs { - if o.isRendered() { +func (p *Page) setAutoSummary() error { + var summary string + var truncated bool + if p.isCJKLanguage { + summary, truncated = helpers.TruncateWordsByRune(p.PlainWords(), helpers.SummaryLength) + } else { + summary, truncated = helpers.TruncateWordsToWholeSentence(p.Plain(), helpers.SummaryLength) + } + p.Summary = template.HTML(summary) + p.Truncated = truncated + + return nil +} + +func (p *Page) renderContent(content []byte) []byte { + var fn helpers.LinkResolverFunc + var fileFn helpers.FileResolverFunc + if p.getRenderingConfig().SourceRelativeLinksEval { + fn = func(ref string) (string, error) { + return p.Site.SourceRelativeLink(ref, p) + } + fileFn = func(ref string) (string, error) { + return p.Site.SourceRelativeLinkFile(ref, p) + } + } + + return p.s.ContentSpec.RenderBytes(&helpers.RenderingContext{ + Content: content, RenderTOC: true, PageFmt: p.determineMarkupType(), + Cfg: p.Language(), + DocumentID: p.UniqueID(), DocumentName: p.Path(), + Config: p.getRenderingConfig(), LinkResolver: fn, FileResolver: fileFn}) +} + +func (p *Page) getRenderingConfig() *helpers.Blackfriday { + p.renderingConfigInit.Do(func() { + p.renderingConfig = p.s.ContentSpec.NewBlackfriday() + + if p.Language() == nil { + panic(fmt.Sprintf("nil language for %s with source lang %s", p.BaseFileName(), p.lang)) + } + + bfParam := p.GetParam("blackfriday") + if bfParam == nil { + return + } + + pageParam := cast.ToStringMap(bfParam) + if err := mapstructure.Decode(pageParam, &p.renderingConfig); err != nil { + p.s.Log.FATAL.Printf("Failed to get rendering config for %s:\n%s", p.BaseFileName(), err.Error()) + } + + }) + + return p.renderingConfig +} + +func (s *Site) newPage(filename string) *Page { + sp := source.NewSourceSpec(s.Cfg, s.Fs) + p := &Page{ + pageInit: &pageInit{}, + Kind: kindFromFilename(filename), + contentType: "", + Source: Source{File: *sp.NewFile(filename)}, + Keywords: []string{}, Sitemap: Sitemap{Priority: -1}, + Params: make(map[string]interface{}), + translations: make(Pages, 0), + sections: sectionsFromFilename(filename), + Site: &s.Info, + s: s, + } + + s.Log.DEBUG.Println("Reading from", p.File.Path()) + return p +} + +func (p *Page) IsRenderable() bool { + return p.renderable +} + +func (p *Page) Type() string { + if p.contentType != "" { + return p.contentType + } + + if x := p.Section(); x != "" { + return x + } + + return "page" +} + +// Section returns the first path element below the content root. Note that +// since Hugo 0.22 we support nested sections, but this will always be the first +// element of any nested path. +func (p *Page) Section() string { + if p.Kind == KindSection { + return p.sections[0] + } + return p.Source.Section() +} + +func (s *Site) NewPageFrom(buf io.Reader, name string) (*Page, error) { + p, err := s.NewPage(name) + if err != nil { + return p, err + } + _, err = p.ReadFrom(buf) + + return p, err +} + +func (s *Site) NewPage(name string) (*Page, error) { + if len(name) == 0 { + return nil, errors.New("Zero length page name") + } + + // Create new page + p := s.newPage(name) + p.s = s + p.Site = &s.Info + + return p, nil +} + +func (p *Page) ReadFrom(buf io.Reader) (int64, error) { + // Parse for metadata & body + if err := p.parse(buf); err != nil { + p.s.Log.ERROR.Print(err) + return 0, err + } + + return int64(len(p.rawContent)), nil +} + +func (p *Page) WordCount() int { + p.analyzePage() + return p.wordCount +} + +func (p *Page) ReadingTime() int { + p.analyzePage() + return p.readingTime +} + +func (p *Page) FuzzyWordCount() int { + p.analyzePage() + return p.fuzzyWordCount +} + +func (p *Page) analyzePage() { + p.pageMetaInit.Do(func() { + if p.isCJKLanguage { + p.wordCount = 0 + for _, word := range p.PlainWords() { + runeCount := utf8.RuneCountInString(word) + if len(word) == runeCount { + p.wordCount++ + } else { + p.wordCount += runeCount + } + } + } else { + p.wordCount = helpers.TotalWords(p.Plain()) + } + + // TODO(bep) is set in a test. Fix that. + if p.fuzzyWordCount == 0 { + p.fuzzyWordCount = (p.wordCount + 100) / 100 * 100 + } + + if p.isCJKLanguage { + p.readingTime = (p.wordCount + 500) / 501 + } else { + p.readingTime = (p.wordCount + 212) / 213 + } + }) +} + +func (p *Page) Extension() string { + // Remove in Hugo 0.22. + helpers.Deprecated("Page", "Extension", "See OutputFormats with its MediaType", true) + return p.extension +} + +// AllTranslations returns all translations, including the current Page. +func (p *Page) AllTranslations() Pages { + return p.translations +} + +// IsTranslated returns whether this content file is translated to +// other language(s). +func (p *Page) IsTranslated() bool { + return len(p.translations) > 1 +} + +// Translations returns the translations excluding the current Page. +func (p *Page) Translations() Pages { + translations := make(Pages, 0) + for _, t := range p.translations { + if t.Lang() != p.Lang() { + translations = append(translations, t) + } + } + return translations +} + +func (p *Page) LinkTitle() string { + if len(p.linkTitle) > 0 { + return p.linkTitle + } + return p.Title +} + +func (p *Page) shouldBuild() bool { + return shouldBuild(p.s.Cfg.GetBool("buildFuture"), p.s.Cfg.GetBool("buildExpired"), + p.s.Cfg.GetBool("buildDrafts"), p.Draft, p.PublishDate, p.ExpiryDate) +} + +func shouldBuild(buildFuture bool, buildExpired bool, buildDrafts bool, Draft bool, + publishDate time.Time, expiryDate time.Time) bool { + if !(buildDrafts || !Draft) { + return false + } + if !buildFuture && !publishDate.IsZero() && publishDate.After(time.Now()) { + return false + } + if !buildExpired && !expiryDate.IsZero() && expiryDate.Before(time.Now()) { + return false + } + return true +} + +func (p *Page) IsDraft() bool { + return p.Draft +} + +func (p *Page) IsFuture() bool { + if p.PublishDate.IsZero() { + return false + } + return p.PublishDate.After(time.Now()) +} + +func (p *Page) IsExpired() bool { + if p.ExpiryDate.IsZero() { + return false + } + return p.ExpiryDate.Before(time.Now()) +} + +func (p *Page) URL() string { + + if p.IsPage() && p.URLPath.URL != "" { + // This is the url set in front matter + return p.URLPath.URL + } + // Fall back to the relative permalink. + u := p.RelPermalink() + return u +} + +// Permalink returns the absolute URL to this Page. +func (p *Page) Permalink() string { + return p.permalink +} + +// RelPermalink gets a URL to the resource relative to the host. +func (p *Page) RelPermalink() string { + return p.relPermalink +} + +func (p *Page) initURLs() error { + if len(p.outputFormats) == 0 { + p.outputFormats = p.s.outputFormats[p.Kind] + } + rel := p.createRelativePermalink() + + var err error + p.permalink, err = p.s.permalinkForOutputFormat(rel, p.outputFormats[0]) + if err != nil { + return err + } + rel = p.s.PathSpec.PrependBasePath(rel) + p.relPermalink = rel + p.layoutDescriptor = p.createLayoutDescriptor() + return nil +} + +var ErrHasDraftAndPublished = errors.New("both draft and published parameters were found in page's frontmatter") + +func (p *Page) update(f interface{}) error { + if f == nil { + return errors.New("no metadata found") + } + m := f.(map[string]interface{}) + // Needed for case insensitive fetching of params values + helpers.ToLowerMap(m) + + var err error + var draft, published, isCJKLanguage *bool + for k, v := range m { + loki := strings.ToLower(k) + switch loki { + case "title": + p.Title = cast.ToString(v) + p.Params[loki] = p.Title + case "linktitle": + p.linkTitle = cast.ToString(v) + p.Params[loki] = p.linkTitle + case "description": + p.Description = cast.ToString(v) + p.Params[loki] = p.Description + case "slug": + p.Slug = cast.ToString(v) + p.Params[loki] = p.Slug + case "url": + if url := cast.ToString(v); strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") { + return fmt.Errorf("Only relative URLs are supported, %v provided", url) + } + p.URLPath.URL = cast.ToString(v) + p.Params[loki] = p.URLPath.URL + case "type": + p.contentType = cast.ToString(v) + p.Params[loki] = p.contentType + case "extension", "ext": + p.extension = cast.ToString(v) + p.Params[loki] = p.extension + case "keywords": + p.Keywords = cast.ToStringSlice(v) + p.Params[loki] = p.Keywords + case "date": + p.Date, err = cast.ToTimeE(v) + if err != nil { + p.s.Log.ERROR.Printf("Failed to parse date '%v' in page %s", v, p.File.Path()) + } + p.Params[loki] = p.Date + case "lastmod": + p.Lastmod, err = cast.ToTimeE(v) + if err != nil { + p.s.Log.ERROR.Printf("Failed to parse lastmod '%v' in page %s", v, p.File.Path()) + } + case "outputs": + o := cast.ToStringSlice(v) + if len(o) > 0 { + // Output formats are exlicitly set in front matter, use those. + outFormats, err := p.s.outputFormatsConfig.GetByNames(o...) + + if err != nil { + p.s.Log.ERROR.Printf("Failed to resolve output formats: %s", err) + } else { + p.outputFormats = outFormats + p.Params[loki] = outFormats + } + + } + //p.Params[loki] = p.Keywords + case "publishdate", "pubdate": + p.PublishDate, err = cast.ToTimeE(v) + if err != nil { + p.s.Log.ERROR.Printf("Failed to parse publishdate '%v' in page %s", v, p.File.Path()) + } + case "expirydate", "unpublishdate": + p.ExpiryDate, err = cast.ToTimeE(v) + if err != nil { + p.s.Log.ERROR.Printf("Failed to parse expirydate '%v' in page %s", v, p.File.Path()) + } + case "draft": + draft = new(bool) + *draft = cast.ToBool(v) + case "published": // Intentionally undocumented + published = new(bool) + *published = cast.ToBool(v) + case "layout": + p.Layout = cast.ToString(v) + p.Params[loki] = p.Layout + case "markup": + p.Markup = cast.ToString(v) + p.Params[loki] = p.Markup + case "weight": + p.Weight = cast.ToInt(v) + p.Params[loki] = p.Weight + case "aliases": + p.Aliases = cast.ToStringSlice(v) + for _, alias := range p.Aliases { + if strings.HasPrefix(alias, "http://") || strings.HasPrefix(alias, "https://") { + return fmt.Errorf("Only relative aliases are supported, %v provided", alias) + } + } + p.Params[loki] = p.Aliases + case "status": + p.Status = cast.ToString(v) + p.Params[loki] = p.Status + case "sitemap": + p.Sitemap = parseSitemap(cast.ToStringMap(v)) + p.Params[loki] = p.Sitemap + case "iscjklanguage": + isCJKLanguage = new(bool) + *isCJKLanguage = cast.ToBool(v) + default: + // If not one of the explicit values, store in Params + switch vv := v.(type) { + case bool: + p.Params[loki] = vv + case string: + p.Params[loki] = vv + case int64, int32, int16, int8, int: + p.Params[loki] = vv + case float64, float32: + p.Params[loki] = vv + case time.Time: + p.Params[loki] = vv + default: // handle array of strings as well + switch vvv := vv.(type) { + case []interface{}: + if len(vvv) > 0 { + switch vvv[0].(type) { + case map[interface{}]interface{}: // Proper parsing structured array from YAML based FrontMatter + p.Params[loki] = vvv + case map[string]interface{}: // Proper parsing structured array from JSON based FrontMatter + p.Params[loki] = vvv + case []interface{}: + p.Params[loki] = vvv + default: + a := make([]string, len(vvv)) + for i, u := range vvv { + a[i] = cast.ToString(u) + } + + p.Params[loki] = a + } + } else { + p.Params[loki] = []string{} + } + default: + p.Params[loki] = vv + } + } + } + } + + if draft != nil && published != nil { + p.Draft = *draft + p.s.Log.ERROR.Printf("page %s has both draft and published settings in its frontmatter. Using draft.", p.File.Path()) + return ErrHasDraftAndPublished + } else if draft != nil { + p.Draft = *draft + } else if published != nil { + p.Draft = !*published + } + p.Params["draft"] = p.Draft + + if p.Date.IsZero() && p.s.Cfg.GetBool("useModTimeAsFallback") { + fi, err := p.s.Fs.Source.Stat(filepath.Join(p.s.PathSpec.AbsPathify(p.s.Cfg.GetString("contentDir")), p.File.Path())) + if err == nil { + p.Date = fi.ModTime() + p.Params["date"] = p.Date + } + } + + if p.Lastmod.IsZero() { + p.Lastmod = p.Date + } + p.Params["lastmod"] = p.Lastmod + + if isCJKLanguage != nil { + p.isCJKLanguage = *isCJKLanguage + } else if p.s.Cfg.GetBool("hasCJKLanguage") { + if cjk.Match(p.rawContent) { + p.isCJKLanguage = true + } else { + p.isCJKLanguage = false + } + } + p.Params["iscjklanguage"] = p.isCJKLanguage + + return nil + +} + +func (p *Page) GetParam(key string) interface{} { + return p.getParam(key, true) +} + +func (p *Page) getParam(key string, stringToLower bool) interface{} { + v := p.Params[strings.ToLower(key)] + + if v == nil { + return nil + } + + switch val := v.(type) { + case bool: + return val + case string: + if stringToLower { + return strings.ToLower(val) + } + return val + case int64, int32, int16, int8, int: + return cast.ToInt(v) + case float64, float32: + return cast.ToFloat64(v) + case time.Time: + return val + case []string: + if stringToLower { + return helpers.SliceToLower(val) + } + return v + case map[string]interface{}: // JSON and TOML + return v + case map[interface{}]interface{}: // YAML + return v + } + + p.s.Log.ERROR.Printf("GetParam(\"%s\"): Unknown type %s\n", key, reflect.TypeOf(v)) + return nil +} + +func (p *Page) HasMenuCurrent(menuID string, me *MenuEntry) bool { + + sectionPagesMenu := p.Site.sectionPagesMenu + + // page is labeled as "shadow-member" of the menu with the same identifier as the section + if sectionPagesMenu != "" { + section := p.Section() + + if section != "" && sectionPagesMenu == menuID && section == me.Identifier { return true } } + + if !me.HasChildren() { + return false + } + + menus := p.Menus() + + if m, ok := menus[menuID]; ok { + + for _, child := range me.Children { + if child.IsEqual(m) { + return true + } + if p.HasMenuCurrent(menuID, child) { + return true + } + } + + } + + if p.IsPage() { + return false + } + + // The following logic is kept from back when Hugo had both Page and Node types. + // TODO(bep) consolidate / clean + nme := MenuEntry{Name: p.Title, URL: p.URL()} + + for _, child := range me.Children { + if nme.IsSameResource(child) { + return true + } + if p.HasMenuCurrent(menuID, child) { + return true + } + } + + return false + +} + +func (p *Page) IsMenuCurrent(menuID string, inme *MenuEntry) bool { + + menus := p.Menus() + + if me, ok := menus[menuID]; ok { + if me.IsEqual(inme) { + return true + } + } + + if p.IsPage() { + return false + } + + // The following logic is kept from back when Hugo had both Page and Node types. + // TODO(bep) consolidate / clean + me := MenuEntry{Name: p.Title, URL: p.URL()} + + if !me.IsSameResource(inme) { + return false + } + + // this resource may be included in several menus + // search for it to make sure that it is in the menu with the given menuId + if menu, ok := (*p.Site.Menus)[menuID]; ok { + for _, menuEntry := range *menu { + if menuEntry.IsSameResource(inme) { + return true + } + + descendantFound := p.isSameAsDescendantMenu(inme, menuEntry) + if descendantFound { + return descendantFound + } + + } + } + + return false +} + +func (p *Page) isSameAsDescendantMenu(inme *MenuEntry, parent *MenuEntry) bool { + if parent.HasChildren() { + for _, child := range parent.Children { + if child.IsSameResource(inme) { + return true + } + descendantFound := p.isSameAsDescendantMenu(inme, child) + if descendantFound { + return descendantFound + } + } + } return false } -func (p *pageState) isContentNodeBranch() bool { - return p.IsNode() -} +func (p *Page) Menus() PageMenus { + p.pageMenusInit.Do(func() { + p.pageMenus = PageMenus{} -// Eq returns whether the current page equals the given page. -// This is what's invoked when doing `{{ if eq $page $otherPage }}` -func (p *pageState) Eq(other any) bool { - pp, err := unwrapPage(other) - if err != nil { - return false - } + if ms, ok := p.Params["menu"]; ok { + link := p.RelPermalink() - return p == pp -} + me := MenuEntry{Name: p.LinkTitle(), Weight: p.Weight, URL: link} -func (p *pageState) HeadingsFiltered(context.Context) tableofcontents.Headings { - return nil -} + // Could be the name of the menu to attach it to + mname, err := cast.ToStringE(ms) -type pageHeadingsFiltered struct { - *pageState - headings tableofcontents.Headings -} + if err == nil { + me.Menu = mname + p.pageMenus[mname] = &me + return + } -func (p *pageHeadingsFiltered) HeadingsFiltered(context.Context) tableofcontents.Headings { - return p.headings -} + // Could be a slice of strings + mnames, err := cast.ToStringSliceE(ms) -func (p *pageHeadingsFiltered) page() page.Page { - return p.pageState -} + if err == nil { + for _, mname := range mnames { + me.Menu = mname + p.pageMenus[mname] = &me + } + return + } -// For internal use by the related content feature. -func (p *pageState) ApplyFilterToHeadings(ctx context.Context, fn func(*tableofcontents.Heading) bool) related.Document { - fragments := p.pageOutput.pco.c().Fragments(ctx) - headings := fragments.Headings.FilterBy(fn) - return &pageHeadingsFiltered{ - pageState: p, - headings: headings, - } -} + // Could be a structured menu entry + menus, err := cast.ToStringMapE(ms) -func (p *pageState) GitInfo() source.GitInfo { - return p.gitInfo -} + if err != nil { + p.s.Log.ERROR.Printf("unable to process menus for %q\n", p.Title) + } -func (p *pageState) CodeOwners() []string { - return p.codeowners -} + for name, menu := range menus { + menuEntry := MenuEntry{Name: p.LinkTitle(), URL: link, Weight: p.Weight, Menu: name} + if menu != nil { + p.s.Log.DEBUG.Printf("found menu: %q, in %q\n", name, p.Title) + ime, err := cast.ToStringMapE(menu) + if err != nil { + p.s.Log.ERROR.Printf("unable to process menus for %q: %s", p.Title, err) + } -// GetTerms gets the terms defined on this page in the given taxonomy. -// The pages returned will be ordered according to the front matter. -func (p *pageState) GetTerms(taxonomy string) page.Pages { - return p.s.pageMap.getTermsForPageInTaxonomy(p.Path(), taxonomy) -} + menuEntry.marshallMap(ime) + } + p.pageMenus[name] = &menuEntry -func (p *pageState) MarshalJSON() ([]byte, error) { - return page.MarshalPageToJSON(p) -} - -func (p *pageState) RegularPagesRecursive() page.Pages { - switch p.Kind() { - case kinds.KindSection, kinds.KindHome: - return p.s.pageMap.getPagesInSection( - pageMapQueryPagesInSection{ - pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{ - Path: p.Path(), - Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindPage), - }, - Recursive: true, - }, - ) - default: - return p.RegularPages() - } -} - -func (p *pageState) PagesRecursive() page.Pages { - return nil -} - -func (p *pageState) RegularPages() page.Pages { - switch p.Kind() { - case kinds.KindPage: - case kinds.KindSection, kinds.KindHome, kinds.KindTaxonomy: - return p.s.pageMap.getPagesInSection( - pageMapQueryPagesInSection{ - pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{ - Path: p.Path(), - Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindPage), - }, - }, - ) - case kinds.KindTerm: - return p.s.pageMap.getPagesWithTerm( - pageMapQueryPagesBelowPath{ - Path: p.Path(), - Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindPage), - }, - ) - default: - return p.s.RegularPages() - } - return nil -} - -func (p *pageState) Pages() page.Pages { - switch p.Kind() { - case kinds.KindPage: - case kinds.KindSection, kinds.KindHome: - return p.s.pageMap.getPagesInSection( - pageMapQueryPagesInSection{ - pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{ - Path: p.Path(), - KeyPart: "page-section", - Include: pagePredicates.ShouldListLocal.And( - pagePredicates.KindPage.Or(pagePredicates.KindSection), - ), - }, - }, - ) - case kinds.KindTerm: - return p.s.pageMap.getPagesWithTerm( - pageMapQueryPagesBelowPath{ - Path: p.Path(), - }, - ) - case kinds.KindTaxonomy: - return p.s.pageMap.getPagesInSection( - pageMapQueryPagesInSection{ - pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{ - Path: p.Path(), - KeyPart: "term", - Include: pagePredicates.ShouldListLocal.And(pagePredicates.KindTerm), - }, - Recursive: true, - }, - ) - default: - return p.s.Pages() - } - return nil -} - -// RawContent returns the un-rendered source content without -// any leading front matter. -func (p *pageState) RawContent() string { - if p.m.content.pi.itemsStep2 == nil { - return "" - } - start := p.m.content.pi.posMainContent - if start == -1 { - start = 0 - } - source, err := p.m.content.pi.contentSource(p.m.content) - if err != nil { - panic(err) - } - return string(source[start:]) -} - -func (p *pageState) Resources() resource.Resources { - return p.s.pageMap.getOrCreateResourcesForPage(p) -} - -func (p *pageState) HasShortcode(name string) bool { - if p.m.content.shortcodeState == nil { - return false - } - - return p.m.content.shortcodeState.hasName(name) -} - -func (p *pageState) Site() page.Site { - return p.sWrapped -} - -func (p *pageState) String() string { - var sb strings.Builder - if p.File() != nil { - // The forward slashes even on Windows is motivated by - // getting stable tests. - // This information is meant for getting positional information in logs, - // so the direction of the slashes should not matter. - sb.WriteString(filepath.ToSlash(p.File().Filename())) - if p.File().IsContentAdapter() { - // Also include the path. - sb.WriteString(":") - sb.WriteString(p.Path()) + } } + }) + + return p.pageMenus +} + +func (p *Page) shouldRenderTo(f output.Format) bool { + _, found := p.outputFormats.GetByName(f.Name) + return found +} + +func (p *Page) determineMarkupType() string { + // Try markup explicitly set in the frontmatter + p.Markup = helpers.GuessType(p.Markup) + if p.Markup == "unknown" { + // Fall back to file extension (might also return "unknown") + p.Markup = helpers.GuessType(p.Source.Ext()) + } + + return p.Markup +} + +func (p *Page) parse(reader io.Reader) error { + psr, err := parser.ReadFrom(reader) + if err != nil { + return err + } + + p.renderable = psr.IsRenderable() + p.frontmatter = psr.FrontMatter() + p.rawContent = psr.Content() + p.lang = p.Source.File.Lang() + + meta, err := psr.Metadata() + if err != nil { + return fmt.Errorf("failed to parse page metadata for %q: %s", p.File.Path(), err) + } + + if meta != nil { + if err = p.update(meta); err != nil { + return err + } + } + + return nil +} + +func (p *Page) RawContent() string { + return string(p.rawContent) +} + +func (p *Page) SetSourceContent(content []byte) { + p.Source.Content = content +} + +func (p *Page) SetSourceMetaData(in interface{}, mark rune) (err error) { + // See https://github.com/gohugoio/hugo/issues/2458 + defer func() { + if r := recover(); r != nil { + var ok bool + err, ok = r.(error) + if !ok { + err = fmt.Errorf("error from marshal: %v", r) + } + } + }() + + buf := bp.GetBuffer() + defer bp.PutBuffer(buf) + + err = parser.InterfaceToFrontMatter(in, mark, buf) + if err != nil { + return + } + + _, err = buf.WriteRune('\n') + if err != nil { + return + } + + p.Source.Frontmatter = buf.Bytes() + + return +} + +func (p *Page) SafeSaveSourceAs(path string) error { + return p.saveSourceAs(path, true) +} + +func (p *Page) SaveSourceAs(path string) error { + return p.saveSourceAs(path, false) +} + +func (p *Page) saveSourceAs(path string, safe bool) error { + b := bp.GetBuffer() + defer bp.PutBuffer(b) + + b.Write(p.Source.Frontmatter) + b.Write(p.Source.Content) + + bc := make([]byte, b.Len(), b.Len()) + copy(bc, b.Bytes()) + + return p.saveSource(bc, path, safe) +} + +func (p *Page) saveSource(by []byte, inpath string, safe bool) (err error) { + if !filepath.IsAbs(inpath) { + inpath = p.s.PathSpec.AbsPathify(inpath) + } + p.s.Log.INFO.Println("creating", inpath) + if safe { + err = helpers.SafeWriteToDisk(inpath, bytes.NewReader(by), p.s.Fs.Source) } else { - sb.WriteString(p.Path()) + err = helpers.WriteToDisk(inpath, bytes.NewReader(by), p.s.Fs.Source) } - return sb.String() -} - -// IsTranslated returns whether this content file is translated to -// other language(s). -func (p *pageState) IsTranslated() bool { - return len(p.Translations()) > 0 -} - -// TranslationKey returns the key used to identify a translation of this content. -func (p *pageState) TranslationKey() string { - if p.m.pageConfig.TranslationKey != "" { - return p.m.pageConfig.TranslationKey - } - return p.Path() -} - -// AllTranslations returns all translations, including the current Page. -func (p *pageState) AllTranslations() page.Pages { - key := p.Path() + "/" + "translations-all" - // This is called from Translations, so we need to use a different partition, cachePages2, - // to avoid potential deadlocks. - pages, err := p.s.pageMap.getOrCreatePagesFromCache(p.s.pageMap.cachePages2, key, func(string) (page.Pages, error) { - if p.m.pageConfig.TranslationKey != "" { - // translationKey set by user. - pas, _ := p.s.h.translationKeyPages.Get(p.m.pageConfig.TranslationKey) - pasc := make(page.Pages, len(pas)) - copy(pasc, pas) - page.SortByLanguage(pasc) - return pasc, nil - } - var pas page.Pages - p.s.pageMap.treePages.ForEeachInDimension(p.Path(), doctree.DimensionLanguage.Index(), - func(n contentNodeI) bool { - if n != nil { - pas = append(pas, n.(page.Page)) - } - return false - }, - ) - - pas = pagePredicates.ShouldLink.Filter(pas) - page.SortByLanguage(pas) - return pas, nil - }) if err != nil { - panic(err) - } - - return pages -} - -// Translations returns the translations excluding the current Page. -func (p *pageState) Translations() page.Pages { - key := p.Path() + "/" + "translations" - pages, err := p.s.pageMap.getOrCreatePagesFromCache(nil, key, func(string) (page.Pages, error) { - var pas page.Pages - for _, pp := range p.AllTranslations() { - if !pp.Eq(p) { - pas = append(pas, pp) - } - } - return pas, nil - }) - if err != nil { - panic(err) - } - return pages -} - -func (ps *pageState) initCommonProviders(pp pagePaths) error { - if ps.IsPage() { - ps.posNextPrev = &nextPrev{init: ps.s.init.prevNext} - ps.posNextPrevSection = &nextPrev{init: ps.s.init.prevNextInSection} - ps.InSectionPositioner = newPagePositionInSection(ps.posNextPrevSection) - ps.Positioner = newPagePosition(ps.posNextPrev) - } - - ps.OutputFormatsProvider = pp - ps.targetPathDescriptor = pp.targetPathDescriptor - ps.RefProvider = newPageRef(ps) - ps.SitesProvider = ps.s - - return nil -} - -// Exported so it can be used in integration tests. -func (po *pageOutput) GetInternalTemplateBasePathAndDescriptor() (string, tplimpl.TemplateDescriptor) { - p := po.p - f := po.f - base := p.PathInfo().BaseReTyped(p.m.pageConfig.Type) - return base, tplimpl.TemplateDescriptor{ - Kind: p.Kind(), - Lang: p.Language().Lang, - LayoutFromUser: p.Layout(), - OutputFormat: f.Name, - MediaType: f.MediaType.Type, - IsPlainText: f.IsPlainText, - } -} - -func (p *pageState) resolveTemplate(layouts ...string) (*tplimpl.TemplInfo, bool, error) { - dir, d := p.GetInternalTemplateBasePathAndDescriptor() - - if len(layouts) > 0 { - d.LayoutFromUser = layouts[0] - d.LayoutFromUserMustMatch = true - } - - q := tplimpl.TemplateQuery{ - Path: dir, - Category: tplimpl.CategoryLayout, - Desc: d, - } - - tinfo := p.s.TemplateStore.LookupPagesLayout(q) - if tinfo == nil { - return nil, false, nil - } - - return tinfo, true, nil -} - -// Must be run after the site section tree etc. is built and ready. -func (p *pageState) initPage() error { - if _, err := p.init.Do(context.Background()); err != nil { - return err + return } return nil } -func (p *pageState) renderResources() error { - for _, r := range p.Resources() { +func (p *Page) SaveSource() error { + return p.SaveSourceAs(p.FullFilePath()) +} - if _, ok := r.(page.Page); ok { - if p.s.h.buildCounter.Load() == 0 { - // Pages gets rendered with the owning page but we count them here. - p.s.PathSpec.ProcessingStats.Incr(&p.s.PathSpec.ProcessingStats.Pages) - } - continue - } - - if resources.IsPublished(r) { - continue - } - - src, ok := r.(resource.Source) - if !ok { - return fmt.Errorf("resource %T does not support resource.Source", r) - } - - if err := src.Publish(); err != nil { - if !herrors.IsNotExist(err) { - p.s.Log.Errorf("Failed to publish Resource for page %q: %s", p.pathOrTitle(), err) - } - } else { - p.s.PathSpec.ProcessingStats.Incr(&p.s.PathSpec.ProcessingStats.Files) - } +func (p *Page) processShortcodes() error { + p.shortcodeState = newShortcodeHandler(p) + tmpContent, err := p.shortcodeState.extractShortcodes(string(p.workContent), p) + if err != nil { + return err } + p.workContent = []byte(tmpContent) return nil + } -func (p *pageState) AlternativeOutputFormats() page.OutputFormats { - f := p.outputFormat() - var o page.OutputFormats - for _, of := range p.OutputFormats() { - if of.Format.NotAlternative || of.Format.Name == f.Name { - continue - } - - o = append(o, of) - } - return o +func (p *Page) FullFilePath() string { + return filepath.Join(p.Dir(), p.LogicalName()) } -type renderStringOpts struct { - Display string - Markup string -} +// Pre render prepare steps -var defaultRenderStringOpts = renderStringOpts{ - Display: "inline", - Markup: "", // Will inherit the page's value when not set. -} - -func (p *pageMeta) wrapError(err error, sourceFs afero.Fs) error { - if err == nil { - panic("wrapError with nil") - } - - if p.File() == nil { - // No more details to add. - return fmt.Errorf("%q: %w", p.Path(), err) - } - - return hugofs.AddFileInfoToError(err, p.File().FileInfo(), sourceFs) -} - -// wrapError adds some more context to the given error if possible/needed -func (p *pageState) wrapError(err error) error { - return p.m.wrapError(err, p.s.h.SourceFs) -} - -func (p *pageState) getPageInfoForError() string { - s := fmt.Sprintf("kind: %q, path: %q", p.Kind(), p.Path()) - if p.File() != nil { - s += fmt.Sprintf(", file: %q", p.File().Filename()) - } - return s -} - -func (p *pageState) getContentConverter() converter.Converter { - var err error - p.contentConverterInit.Do(func() { - if p.m.pageConfig.ContentMediaType.IsZero() { - panic("ContentMediaType not set") - } - markup := p.m.pageConfig.ContentMediaType.SubType - - if markup == "html" { - // Only used for shortcode inner content. - markup = "markdown" - } - p.contentConverter, err = p.m.newContentConverter(p, markup) - }) - - if err != nil { - p.s.Log.Errorln("Failed to create content converter:", err) - } - return p.contentConverter -} - -func (p *pageState) errorf(err error, format string, a ...any) error { - if herrors.UnwrapFileError(err) != nil { - // More isn't always better. - return err - } - args := append([]any{p.Language().Lang, p.pathOrTitle()}, a...) - args = append(args, err) - format = "[%s] page %q: " + format + ": %w" - if err == nil { - return fmt.Errorf(format, args...) - } - return fmt.Errorf(format, args...) -} - -func (p *pageState) outputFormat() (f output.Format) { - if p.pageOutput == nil { - panic("no pageOutput") - } - return p.pageOutput.f -} - -func (p *pageState) parseError(err error, input []byte, offset int) error { - pos := posFromInput("", input, offset) - return herrors.NewFileErrorFromName(err, p.File().Filename()).UpdatePosition(pos) -} - -func (p *pageState) pathOrTitle() string { - if p.File() != nil { - return p.File().Filename() - } - - if p.Path() != "" { - return p.Path() - } - - return p.Title() -} - -func (p *pageState) posFromInput(input []byte, offset int) text.Position { - return posFromInput(p.pathOrTitle(), input, offset) -} - -func (p *pageState) posOffset(offset int) text.Position { - return p.posFromInput(p.m.content.mustSource(), offset) -} - -// shiftToOutputFormat is serialized. The output format idx refers to the -// full set of output formats for all sites. -// This is serialized. -func (p *pageState) shiftToOutputFormat(isRenderingSite bool, idx int) error { - if err := p.initPage(); err != nil { - return err - } - - if len(p.pageOutputs) == 1 { - idx = 0 - } - - p.pageOutputIdx = idx - p.pageOutput = p.pageOutputs[idx] - if p.pageOutput == nil { - panic(fmt.Sprintf("pageOutput is nil for output idx %d", idx)) - } - - // Reset any built paginator. This will trigger when re-rendering pages in - // server mode. - if isRenderingSite && p.pageOutput.paginator != nil && p.pageOutput.paginator.current != nil { - p.pageOutput.paginator.reset() - } - - if isRenderingSite { - cp := p.pageOutput.pco - if cp == nil && p.canReusePageOutputContent() { - // Look for content to reuse. - for i := range p.pageOutputs { - if i == idx { - continue - } - po := p.pageOutputs[i] - - if po.pco != nil { - cp = po.pco - break - } - } - } - - if cp == nil { - var err error - cp, err = newPageContentOutput(p.pageOutput) +func (p *Page) prepareLayouts() error { + // TODO(bep): Check the IsRenderable logic. + if p.Kind == KindPage { + if !p.IsRenderable() { + self := "__" + p.UniqueID() + err := p.s.TemplateHandler().AddLateTemplate(self, string(p.Content)) if err != nil { return err } - } - p.pageOutput.setContentProvider(cp) - } else { - // We attempt to assign pageContentOutputs while preparing each site - // for rendering and before rendering each site. This lets us share - // content between page outputs to conserve resources. But if a template - // unexpectedly calls a method of a ContentProvider that is not yet - // initialized, we assign a LazyContentProvider that performs the - // initialization just in time. - if lcp, ok := (p.pageOutput.ContentProvider.(*page.LazyContentProvider)); ok { - lcp.Reset() - } else { - lcp = page.NewLazyContentProvider(func() (page.OutputFormatContentProvider, error) { - cp, err := newPageContentOutput(p.pageOutput) - if err != nil { - return nil, err - } - return cp, nil - }) - p.pageOutput.contentRenderer = lcp - p.pageOutput.ContentProvider = lcp - p.pageOutput.MarkupProvider = lcp - p.pageOutput.PageRenderProvider = lcp - p.pageOutput.TableOfContentsProvider = lcp + p.selfLayout = self } } return nil } -var ( - _ page.Page = (*pageWithOrdinal)(nil) - _ collections.Order = (*pageWithOrdinal)(nil) - _ pageWrapper = (*pageWithOrdinal)(nil) +func (p *Page) prepareData(s *Site) error { + if p.Kind != KindSection { + var pages Pages + p.Data = make(map[string]interface{}) + + switch p.Kind { + case KindPage: + case KindHome: + pages = s.RegularPages + case KindTaxonomy: + plural := p.sections[0] + term := p.sections[1] + + if s.Info.preserveTaxonomyNames { + if v, ok := s.taxonomiesOrigKey[fmt.Sprintf("%s-%s", plural, term)]; ok { + term = v + } + } + + singular := s.taxonomiesPluralSingular[plural] + taxonomy := s.Taxonomies[plural].Get(term) + + p.Data[singular] = taxonomy + p.Data["Singular"] = singular + p.Data["Plural"] = plural + p.Data["Term"] = term + pages = taxonomy.Pages() + case KindTaxonomyTerm: + plural := p.sections[0] + singular := s.taxonomiesPluralSingular[plural] + + p.Data["Singular"] = singular + p.Data["Plural"] = plural + p.Data["Terms"] = s.Taxonomies[plural] + // keep the following just for legacy reasons + p.Data["OrderedIndex"] = p.Data["Terms"] + p.Data["Index"] = p.Data["Terms"] + + // A list of all KindTaxonomy pages with matching plural + for _, p := range s.findPagesByKind(KindTaxonomy) { + if p.sections[0] == plural { + pages = append(pages, p) + } + } + } + + p.Data["Pages"] = pages + p.Pages = pages + } + + // Now we know enough to set missing dates on home page etc. + p.updatePageDates() + + return nil +} + +func (p *Page) updatePageDates() { + // TODO(bep) there is a potential issue with page sorting for home pages + // etc. without front matter dates set, but let us wrap the head around + // that in another time. + if !p.IsNode() { + return + } + + if !p.Date.IsZero() { + if p.Lastmod.IsZero() { + p.Lastmod = p.Date + } + return + } else if !p.Lastmod.IsZero() { + if p.Date.IsZero() { + p.Date = p.Lastmod + } + return + } + + // Set it to the first non Zero date in children + var foundDate, foundLastMod bool + + for _, child := range p.Pages { + if !child.Date.IsZero() { + p.Date = child.Date + foundDate = true + } + if !child.Lastmod.IsZero() { + p.Lastmod = child.Lastmod + foundLastMod = true + } + + if foundDate && foundLastMod { + break + } + } +} + +// copy creates a copy of this page with the lazy sync.Once vars reset +// so they will be evaluated again, for word count calculations etc. +func (p *Page) copy() *Page { + c := *p + c.pageInit = &pageInit{} + return &c +} + +func (p *Page) Now() time.Time { + // Delete in Hugo 0.22 + helpers.Deprecated("Page", "Now", "Use now (the template func)", true) + return time.Now() +} + +func (p *Page) Hugo() *HugoInfo { + return hugoInfo +} + +func (p *Page) Ref(refs ...string) (string, error) { + if len(refs) == 0 { + return "", nil + } + if len(refs) > 1 { + return p.Site.Ref(refs[0], nil, refs[1]) + } + return p.Site.Ref(refs[0], nil) +} + +func (p *Page) RelRef(refs ...string) (string, error) { + if len(refs) == 0 { + return "", nil + } + if len(refs) > 1 { + return p.Site.RelRef(refs[0], nil, refs[1]) + } + return p.Site.RelRef(refs[0], nil) +} + +func (p *Page) String() string { + return fmt.Sprintf("Page(%q)", p.Title) +} + +type URLPath struct { + URL string + Permalink string + Slug string + Section string +} + +// Scratch returns the writable context associated with this Page. +func (p *Page) Scratch() *Scratch { + if p.scratch == nil { + p.scratch = newScratch() + } + return p.scratch +} + +func (p *Page) Language() *helpers.Language { + p.initLanguage() + return p.language +} + +func (p *Page) Lang() string { + // When set, Language can be different from lang in the case where there is a + // content file (doc.sv.md) with language indicator, but there is no language + // config for that language. Then the language will fall back on the site default. + if p.Language() != nil { + return p.Language().Lang + } + return p.lang +} + +func (p *Page) isNewTranslation(candidate *Page) bool { + + if p.Kind != candidate.Kind { + return false + } + + if p.Kind == KindPage || p.Kind == kindUnknown { + panic("Node type not currently supported for this op") + } + + // At this point, we know that this is a traditional Node (home page, section, taxonomy) + // It represents the same node, but different language, if the sections is the same. + if len(p.sections) != len(candidate.sections) { + return false + } + + for i := 0; i < len(p.sections); i++ { + if p.sections[i] != candidate.sections[i] { + return false + } + } + + // Finally check that it is not already added. + for _, translation := range p.translations { + if candidate == translation { + return false + } + } + + return true + +} + +func (p *Page) shouldAddLanguagePrefix() bool { + if !p.Site.IsMultiLingual() { + return false + } + + if p.Lang() == "" { + return false + } + + if !p.Site.defaultContentLanguageInSubdir && p.Lang() == p.Site.multilingual.DefaultLang.Lang { + return false + } + + return true +} + +func (p *Page) initLanguage() { + p.languageInit.Do(func() { + if p.language != nil { + return + } + + ml := p.Site.multilingual + if ml == nil { + panic("Multilanguage not set") + } + if p.lang == "" { + p.lang = ml.DefaultLang.Lang + p.language = ml.DefaultLang + return + } + + language := ml.Language(p.lang) + + if language == nil { + // It can be a file named stefano.chiodino.md. + p.s.Log.WARN.Printf("Page language (if it is that) not found in multilang setup: %s.", p.lang) + language = ml.DefaultLang + } + + p.language = language + + }) +} + +func (p *Page) LanguagePrefix() string { + return p.Site.LanguagePrefix +} + +func (p *Page) addLangPathPrefix(outfile string) string { + return p.addLangPathPrefixIfFlagSet(outfile, p.shouldAddLanguagePrefix()) +} + +func (p *Page) addLangPathPrefixIfFlagSet(outfile string, should bool) string { + if helpers.IsAbsURL(outfile) { + return outfile + } + + if !should { + return outfile + } + + hadSlashSuffix := strings.HasSuffix(outfile, "/") + + outfile = "/" + path.Join(p.Lang(), outfile) + if hadSlashSuffix { + outfile += "/" + } + return outfile +} + +func sectionsFromFilename(filename string) []string { + var sections []string + dir, _ := filepath.Split(filename) + dir = strings.TrimSuffix(dir, helpers.FilePathSeparator) + if dir == "" { + return sections + } + sections = strings.Split(dir, helpers.FilePathSeparator) + return sections +} + +const ( + regularPageFileNameDoesNotStartWith = "_index" + + // There can be "my_regular_index_page.md but not /_index_file.md + regularPageFileNameDoesNotContain = helpers.FilePathSeparator + regularPageFileNameDoesNotStartWith ) -type pageWithOrdinal struct { - ordinal int - *pageState +func kindFromFilename(filename string) string { + if !strings.HasPrefix(filename, regularPageFileNameDoesNotStartWith) && !strings.Contains(filename, regularPageFileNameDoesNotContain) { + return KindPage + } + + if strings.HasPrefix(filename, "_index") { + return KindHome + } + + // We don't know enough yet to determine the type. + return kindUnknown } -func (p pageWithOrdinal) Ordinal() int { - return p.ordinal +func (p *Page) setValuesForKind(s *Site) { + if p.Kind == kindUnknown { + // This is either a taxonomy list, taxonomy term or a section + nodeType := s.kindFromSections(p.sections) + + if nodeType == kindUnknown { + panic(fmt.Sprintf("Unable to determine page kind from %q", p.sections)) + } + + p.Kind = nodeType + } + + switch p.Kind { + case KindHome: + p.URLPath.URL = "/" + case KindPage: + default: + p.URLPath.URL = "/" + path.Join(p.sections...) + "/" + } } -func (p pageWithOrdinal) page() page.Page { - return p.pageState -} - -type pageWithWeight0 struct { - weight0 int - *pageState -} - -func (p pageWithWeight0) Weight0() int { - return p.weight0 -} - -func (p pageWithWeight0) page() page.Page { - return p.pageState -} - -var _ types.Unwrapper = (*pageWithWeight0)(nil) - -func (p pageWithWeight0) Unwrapv() any { - return p.pageState +// Used in error logs. +func (p *Page) pathOrTitle() string { + if p.Path() != "" { + return p.Path() + } + return p.Title } diff --git a/hugolib/pageCache.go b/hugolib/pageCache.go new file mode 100644 index 000000000..e0a3a160b --- /dev/null +++ b/hugolib/pageCache.go @@ -0,0 +1,108 @@ +// Copyright 2015 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 hugolib + +import ( + "sync" +) + +type pageCache struct { + sync.RWMutex + m map[string][][2]Pages +} + +func newPageCache() *pageCache { + return &pageCache{m: make(map[string][][2]Pages)} +} + +// get gets a Pages slice from the cache matching the given key and Pages slice. +// If none found in cache, a copy of the supplied slice is created. +// +// If an apply func is provided, that func is applied to the newly created copy. +// +// The cache and the execution of the apply func is protected by a RWMutex. +func (c *pageCache) get(key string, p Pages, apply func(p Pages)) (Pages, bool) { + c.RLock() + if cached, ok := c.m[key]; ok { + for _, ps := range cached { + if probablyEqualPages(p, ps[0]) { + c.RUnlock() + return ps[1], true + } + } + + } + c.RUnlock() + + c.Lock() + defer c.Unlock() + + // double-check + if cached, ok := c.m[key]; ok { + for _, ps := range cached { + if probablyEqualPages(p, ps[0]) { + return ps[1], true + } + } + } + + pagesCopy := append(Pages(nil), p...) + + if apply != nil { + apply(pagesCopy) + } + + if v, ok := c.m[key]; ok { + c.m[key] = append(v, [2]Pages{p, pagesCopy}) + } else { + c.m[key] = [][2]Pages{{p, pagesCopy}} + } + + return pagesCopy, false + +} + +// "probably" as in: we do not compare every element for big slices, but that is +// good enough for our use case. +// TODO(bep) there is a similar method in pagination.go. DRY. +func probablyEqualPages(p1, p2 Pages) bool { + if p1 == nil && p2 == nil { + return true + } + + if p1 == nil || p2 == nil { + return false + } + + if p1.Len() != p2.Len() { + return false + } + + if p1.Len() == 0 { + return true + } + + step := 1 + + if len(p1) >= 50 { + step = len(p1) / 10 + } + + for i := 0; i < len(p1); i += step { + if p1[i] != p2[i] { + return false + } + } + return true +} diff --git a/hugolib/pageCache_test.go b/hugolib/pageCache_test.go new file mode 100644 index 000000000..62837394f --- /dev/null +++ b/hugolib/pageCache_test.go @@ -0,0 +1,73 @@ +// Copyright 2015 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 hugolib + +import ( + "sync" + "sync/atomic" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPageCache(t *testing.T) { + t.Parallel() + c1 := newPageCache() + + changeFirst := func(p Pages) { + p[0].Description = "changed" + } + + var o1 uint64 + var o2 uint64 + + var wg sync.WaitGroup + + var l1 sync.Mutex + var l2 sync.Mutex + + var testPageSets []Pages + + s := newTestSite(t) + + for i := 0; i < 50; i++ { + testPageSets = append(testPageSets, createSortTestPages(s, i+1)) + } + + for j := 0; j < 100; j++ { + wg.Add(1) + go func() { + defer wg.Done() + for k, pages := range testPageSets { + l1.Lock() + p, c := c1.get("k1", pages, nil) + assert.Equal(t, !atomic.CompareAndSwapUint64(&o1, uint64(k), uint64(k+1)), c) + l1.Unlock() + p2, c2 := c1.get("k1", p, nil) + assert.True(t, c2) + assert.True(t, probablyEqualPages(p, p2)) + assert.True(t, probablyEqualPages(p, pages)) + assert.NotNil(t, p) + + l2.Lock() + p3, c3 := c1.get("k2", pages, changeFirst) + assert.Equal(t, !atomic.CompareAndSwapUint64(&o2, uint64(k), uint64(k+1)), c3) + l2.Unlock() + assert.NotNil(t, p3) + assert.Equal(t, p3[0].Description, "changed") + } + }() + } + wg.Wait() +} diff --git a/hugolib/pageGroup.go b/hugolib/pageGroup.go new file mode 100644 index 000000000..343ecf52e --- /dev/null +++ b/hugolib/pageGroup.go @@ -0,0 +1,297 @@ +// Copyright 2015 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 hugolib + +import ( + "errors" + "reflect" + "sort" + "strings" + "time" +) + +// PageGroup represents a group of pages, grouped by the key. +// The key is typically a year or similar. +type PageGroup struct { + Key interface{} + Pages Pages +} + +type mapKeyValues []reflect.Value + +func (v mapKeyValues) Len() int { return len(v) } +func (v mapKeyValues) Swap(i, j int) { v[i], v[j] = v[j], v[i] } + +type mapKeyByInt struct{ mapKeyValues } + +func (s mapKeyByInt) Less(i, j int) bool { return s.mapKeyValues[i].Int() < s.mapKeyValues[j].Int() } + +type mapKeyByStr struct{ mapKeyValues } + +func (s mapKeyByStr) Less(i, j int) bool { + return s.mapKeyValues[i].String() < s.mapKeyValues[j].String() +} + +func sortKeys(v []reflect.Value, order string) []reflect.Value { + if len(v) <= 1 { + return v + } + + switch v[0].Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if order == "desc" { + sort.Sort(sort.Reverse(mapKeyByInt{v})) + } else { + sort.Sort(mapKeyByInt{v}) + } + case reflect.String: + if order == "desc" { + sort.Sort(sort.Reverse(mapKeyByStr{v})) + } else { + sort.Sort(mapKeyByStr{v}) + } + } + return v +} + +// PagesGroup represents a list of page groups. +// This is what you get when doing page grouping in the templates. +type PagesGroup []PageGroup + +// Reverse reverses the order of this list of page groups. +func (p PagesGroup) Reverse() PagesGroup { + for i, j := 0, len(p)-1; i < j; i, j = i+1, j-1 { + p[i], p[j] = p[j], p[i] + } + + return p +} + +var ( + errorType = reflect.TypeOf((*error)(nil)).Elem() + pagePtrType = reflect.TypeOf((*Page)(nil)) +) + +// GroupBy groups by the value in the given field or method name and with the given order. +// Valid values for order is asc, desc, rev and reverse. +func (p Pages) GroupBy(key string, order ...string) (PagesGroup, error) { + if len(p) < 1 { + return nil, nil + } + + direction := "asc" + + if len(order) > 0 && (strings.ToLower(order[0]) == "desc" || strings.ToLower(order[0]) == "rev" || strings.ToLower(order[0]) == "reverse") { + direction = "desc" + } + + var ft interface{} + m, ok := pagePtrType.MethodByName(key) + if ok { + if m.Type.NumIn() != 1 || m.Type.NumOut() == 0 || m.Type.NumOut() > 2 { + return nil, errors.New(key + " is a Page method but you can't use it with GroupBy") + } + if m.Type.NumOut() == 1 && m.Type.Out(0).Implements(errorType) { + return nil, errors.New(key + " is a Page method but you can't use it with GroupBy") + } + if m.Type.NumOut() == 2 && !m.Type.Out(1).Implements(errorType) { + return nil, errors.New(key + " is a Page method but you can't use it with GroupBy") + } + ft = m + } else { + ft, ok = pagePtrType.Elem().FieldByName(key) + if !ok { + return nil, errors.New(key + " is neither a field nor a method of Page") + } + } + + var tmp reflect.Value + switch e := ft.(type) { + case reflect.StructField: + tmp = reflect.MakeMap(reflect.MapOf(e.Type, reflect.SliceOf(pagePtrType))) + case reflect.Method: + tmp = reflect.MakeMap(reflect.MapOf(e.Type.Out(0), reflect.SliceOf(pagePtrType))) + } + + for _, e := range p { + ppv := reflect.ValueOf(e) + var fv reflect.Value + switch ft.(type) { + case reflect.StructField: + fv = ppv.Elem().FieldByName(key) + case reflect.Method: + fv = ppv.MethodByName(key).Call([]reflect.Value{})[0] + } + if !fv.IsValid() { + continue + } + if !tmp.MapIndex(fv).IsValid() { + tmp.SetMapIndex(fv, reflect.MakeSlice(reflect.SliceOf(pagePtrType), 0, 0)) + } + tmp.SetMapIndex(fv, reflect.Append(tmp.MapIndex(fv), ppv)) + } + + var r []PageGroup + for _, k := range sortKeys(tmp.MapKeys(), direction) { + r = append(r, PageGroup{Key: k.Interface(), Pages: tmp.MapIndex(k).Interface().([]*Page)}) + } + + return r, nil +} + +// GroupByParam groups by the given page parameter key's value and with the given order. +// Valid values for order is asc, desc, rev and reverse. +func (p Pages) GroupByParam(key string, order ...string) (PagesGroup, error) { + if len(p) < 1 { + return nil, nil + } + + direction := "asc" + + if len(order) > 0 && (strings.ToLower(order[0]) == "desc" || strings.ToLower(order[0]) == "rev" || strings.ToLower(order[0]) == "reverse") { + direction = "desc" + } + + var tmp reflect.Value + var keyt reflect.Type + for _, e := range p { + param := e.GetParam(key) + if param != nil { + if _, ok := param.([]string); !ok { + keyt = reflect.TypeOf(param) + tmp = reflect.MakeMap(reflect.MapOf(keyt, reflect.SliceOf(pagePtrType))) + break + } + } + } + if !tmp.IsValid() { + return nil, errors.New("There is no such a param") + } + + for _, e := range p { + param := e.getParam(key, false) + if param == nil || reflect.TypeOf(param) != keyt { + continue + } + v := reflect.ValueOf(param) + if !tmp.MapIndex(v).IsValid() { + tmp.SetMapIndex(v, reflect.MakeSlice(reflect.SliceOf(pagePtrType), 0, 0)) + } + tmp.SetMapIndex(v, reflect.Append(tmp.MapIndex(v), reflect.ValueOf(e))) + } + + var r []PageGroup + for _, k := range sortKeys(tmp.MapKeys(), direction) { + r = append(r, PageGroup{Key: k.Interface(), Pages: tmp.MapIndex(k).Interface().([]*Page)}) + } + + return r, nil +} + +func (p Pages) groupByDateField(sorter func(p Pages) Pages, formatter func(p *Page) string, order ...string) (PagesGroup, error) { + if len(p) < 1 { + return nil, nil + } + + sp := sorter(p) + + if !(len(order) > 0 && (strings.ToLower(order[0]) == "asc" || strings.ToLower(order[0]) == "rev" || strings.ToLower(order[0]) == "reverse")) { + sp = sp.Reverse() + } + + date := formatter(sp[0]) + var r []PageGroup + r = append(r, PageGroup{Key: date, Pages: make(Pages, 0)}) + r[0].Pages = append(r[0].Pages, sp[0]) + + i := 0 + for _, e := range sp[1:] { + date = formatter(e) + if r[i].Key.(string) != date { + r = append(r, PageGroup{Key: date}) + i++ + } + r[i].Pages = append(r[i].Pages, e) + } + return r, nil +} + +// GroupByDate groups by the given page's Date value in +// the given format and with the given order. +// Valid values for order is asc, desc, rev and reverse. +// For valid format strings, see https://golang.org/pkg/time/#Time.Format +func (p Pages) GroupByDate(format string, order ...string) (PagesGroup, error) { + sorter := func(p Pages) Pages { + return p.ByDate() + } + formatter := func(p *Page) string { + return p.Date.Format(format) + } + return p.groupByDateField(sorter, formatter, order...) +} + +// GroupByPublishDate groups by the given page's PublishDate value in +// the given format and with the given order. +// Valid values for order is asc, desc, rev and reverse. +// For valid format strings, see https://golang.org/pkg/time/#Time.Format +func (p Pages) GroupByPublishDate(format string, order ...string) (PagesGroup, error) { + sorter := func(p Pages) Pages { + return p.ByPublishDate() + } + formatter := func(p *Page) string { + return p.PublishDate.Format(format) + } + return p.groupByDateField(sorter, formatter, order...) +} + +// GroupByExpiryDate groups by the given page's ExpireDate value in +// the given format and with the given order. +// Valid values for order is asc, desc, rev and reverse. +// For valid format strings, see https://golang.org/pkg/time/#Time.Format +func (p Pages) GroupByExpiryDate(format string, order ...string) (PagesGroup, error) { + sorter := func(p Pages) Pages { + return p.ByExpiryDate() + } + formatter := func(p *Page) string { + return p.ExpiryDate.Format(format) + } + return p.groupByDateField(sorter, formatter, order...) +} + +// GroupByParamDate groups by a date set as a param on the page in +// the given format and with the given order. +// Valid values for order is asc, desc, rev and reverse. +// For valid format strings, see https://golang.org/pkg/time/#Time.Format +func (p Pages) GroupByParamDate(key string, format string, order ...string) (PagesGroup, error) { + sorter := func(p Pages) Pages { + var r Pages + for _, e := range p { + param := e.GetParam(key) + if param != nil { + if _, ok := param.(time.Time); ok { + r = append(r, e) + } + } + } + pdate := func(p1, p2 *Page) bool { + return p1.GetParam(key).(time.Time).Unix() < p2.GetParam(key).(time.Time).Unix() + } + pageBy(pdate).Sort(r) + return r + } + formatter := func(p *Page) string { + return p.GetParam(key).(time.Time).Format(format) + } + return p.groupByDateField(sorter, formatter, order...) +} diff --git a/hugolib/pageGroup_test.go b/hugolib/pageGroup_test.go new file mode 100644 index 000000000..8cc381b61 --- /dev/null +++ b/hugolib/pageGroup_test.go @@ -0,0 +1,457 @@ +// Copyright 2015 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 hugolib + +import ( + "errors" + "path/filepath" + "reflect" + "testing" + + "github.com/spf13/cast" +) + +type pageGroupTestObject struct { + path string + weight int + date string + param string +} + +var pageGroupTestSources = []pageGroupTestObject{ + {"/section1/testpage1.md", 3, "2012-04-06", "foo"}, + {"/section1/testpage2.md", 3, "2012-01-01", "bar"}, + {"/section1/testpage3.md", 2, "2012-04-06", "foo"}, + {"/section2/testpage4.md", 1, "2012-03-02", "bar"}, + {"/section2/testpage5.md", 1, "2012-04-06", "baz"}, +} + +func preparePageGroupTestPages(t *testing.T) Pages { + s := newTestSite(t) + var pages Pages + for _, src := range pageGroupTestSources { + p, err := s.NewPage(filepath.FromSlash(src.path)) + if err != nil { + t.Fatalf("failed to prepare test page %s", src.path) + } + p.Weight = src.weight + p.Date = cast.ToTime(src.date) + p.PublishDate = cast.ToTime(src.date) + p.ExpiryDate = cast.ToTime(src.date) + p.Params["custom_param"] = src.param + p.Params["custom_date"] = cast.ToTime(src.date) + pages = append(pages, p) + } + return pages +} + +func TestGroupByWithFieldNameArg(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + expect := PagesGroup{ + {Key: 1, Pages: Pages{pages[3], pages[4]}}, + {Key: 2, Pages: Pages{pages[2]}}, + {Key: 3, Pages: Pages{pages[0], pages[1]}}, + } + + groups, err := pages.GroupBy("Weight") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if !reflect.DeepEqual(groups, expect) { + t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups) + } +} + +func TestGroupByWithMethodNameArg(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + expect := PagesGroup{ + {Key: "section1", Pages: Pages{pages[0], pages[1], pages[2]}}, + {Key: "section2", Pages: Pages{pages[3], pages[4]}}, + } + + groups, err := pages.GroupBy("Type") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if !reflect.DeepEqual(groups, expect) { + t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups) + } +} + +func TestGroupByWithSectionArg(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + expect := PagesGroup{ + {Key: "section1", Pages: Pages{pages[0], pages[1], pages[2]}}, + {Key: "section2", Pages: Pages{pages[3], pages[4]}}, + } + + groups, err := pages.GroupBy("Section") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if !reflect.DeepEqual(groups, expect) { + t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups) + } +} + +func TestGroupByInReverseOrder(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + expect := PagesGroup{ + {Key: 3, Pages: Pages{pages[0], pages[1]}}, + {Key: 2, Pages: Pages{pages[2]}}, + {Key: 1, Pages: Pages{pages[3], pages[4]}}, + } + + groups, err := pages.GroupBy("Weight", "desc") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if !reflect.DeepEqual(groups, expect) { + t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups) + } +} + +func TestGroupByCalledWithEmptyPages(t *testing.T) { + t.Parallel() + var pages Pages + groups, err := pages.GroupBy("Weight") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if groups != nil { + t.Errorf("PagesGroup isn't empty. It should be %#v, got %#v", nil, groups) + } +} + +func TestGroupByCalledWithUnavailableKey(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + _, err := pages.GroupBy("UnavailableKey") + if err == nil { + t.Errorf("GroupByParam should return an error but didn't") + } +} + +func (page *Page) DummyPageMethodWithArgForTest(s string) string { + return s +} + +func (page *Page) DummyPageMethodReturnThreeValueForTest() (string, string, string) { + return "foo", "bar", "baz" +} + +func (page *Page) DummyPageMethodReturnErrorOnlyForTest() error { + return errors.New("some error occurred") +} + +func (page *Page) dummyPageMethodReturnTwoValueForTest() (string, string) { + return "foo", "bar" +} + +func TestGroupByCalledWithInvalidMethod(t *testing.T) { + t.Parallel() + var err error + pages := preparePageGroupTestPages(t) + + _, err = pages.GroupBy("DummyPageMethodWithArgForTest") + if err == nil { + t.Errorf("GroupByParam should return an error but didn't") + } + + _, err = pages.GroupBy("DummyPageMethodReturnThreeValueForTest") + if err == nil { + t.Errorf("GroupByParam should return an error but didn't") + } + + _, err = pages.GroupBy("DummyPageMethodReturnErrorOnlyForTest") + if err == nil { + t.Errorf("GroupByParam should return an error but didn't") + } + + _, err = pages.GroupBy("DummyPageMethodReturnTwoValueForTest") + if err == nil { + t.Errorf("GroupByParam should return an error but didn't") + } +} + +func TestReverse(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + + groups1, err := pages.GroupBy("Weight", "desc") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + + groups2, err := pages.GroupBy("Weight") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + groups2 = groups2.Reverse() + + if !reflect.DeepEqual(groups2, groups1) { + t.Errorf("PagesGroup is sorted in unexpected order. It should be %#v, got %#v", groups2, groups1) + } +} + +func TestGroupByParam(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + expect := PagesGroup{ + {Key: "bar", Pages: Pages{pages[1], pages[3]}}, + {Key: "baz", Pages: Pages{pages[4]}}, + {Key: "foo", Pages: Pages{pages[0], pages[2]}}, + } + + groups, err := pages.GroupByParam("custom_param") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if !reflect.DeepEqual(groups, expect) { + t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups) + } +} + +func TestGroupByParamInReverseOrder(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + expect := PagesGroup{ + {Key: "foo", Pages: Pages{pages[0], pages[2]}}, + {Key: "baz", Pages: Pages{pages[4]}}, + {Key: "bar", Pages: Pages{pages[1], pages[3]}}, + } + + groups, err := pages.GroupByParam("custom_param", "desc") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if !reflect.DeepEqual(groups, expect) { + t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups) + } +} + +func TestGroupByParamCalledWithCapitalLetterString(t *testing.T) { + testStr := "TestString" + f := "/section1/test_capital.md" + s := newTestSite(t) + p, err := s.NewPage(filepath.FromSlash(f)) + if err != nil { + t.Fatalf("failed to prepare test page %s", f) + } + p.Params["custom_param"] = testStr + pages := Pages{p} + + groups, err := pages.GroupByParam("custom_param") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if groups[0].Key != testStr { + t.Errorf("PagesGroup key is converted to a lower character string. It should be %#v, got %#v", testStr, groups[0].Key) + } +} + +func TestGroupByParamCalledWithSomeUnavailableParams(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + delete(pages[1].Params, "custom_param") + delete(pages[3].Params, "custom_param") + delete(pages[4].Params, "custom_param") + + expect := PagesGroup{ + {Key: "foo", Pages: Pages{pages[0], pages[2]}}, + } + + groups, err := pages.GroupByParam("custom_param") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if !reflect.DeepEqual(groups, expect) { + t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups) + } +} + +func TestGroupByParamCalledWithEmptyPages(t *testing.T) { + t.Parallel() + var pages Pages + groups, err := pages.GroupByParam("custom_param") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if groups != nil { + t.Errorf("PagesGroup isn't empty. It should be %#v, got %#v", nil, groups) + } +} + +func TestGroupByParamCalledWithUnavailableParam(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + _, err := pages.GroupByParam("unavailable_param") + if err == nil { + t.Errorf("GroupByParam should return an error but didn't") + } +} + +func TestGroupByDate(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + expect := PagesGroup{ + {Key: "2012-04", Pages: Pages{pages[4], pages[2], pages[0]}}, + {Key: "2012-03", Pages: Pages{pages[3]}}, + {Key: "2012-01", Pages: Pages{pages[1]}}, + } + + groups, err := pages.GroupByDate("2006-01") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if !reflect.DeepEqual(groups, expect) { + t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups) + } +} + +func TestGroupByDateInReverseOrder(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + expect := PagesGroup{ + {Key: "2012-01", Pages: Pages{pages[1]}}, + {Key: "2012-03", Pages: Pages{pages[3]}}, + {Key: "2012-04", Pages: Pages{pages[0], pages[2], pages[4]}}, + } + + groups, err := pages.GroupByDate("2006-01", "asc") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if !reflect.DeepEqual(groups, expect) { + t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups) + } +} + +func TestGroupByPublishDate(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + expect := PagesGroup{ + {Key: "2012-04", Pages: Pages{pages[4], pages[2], pages[0]}}, + {Key: "2012-03", Pages: Pages{pages[3]}}, + {Key: "2012-01", Pages: Pages{pages[1]}}, + } + + groups, err := pages.GroupByPublishDate("2006-01") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if !reflect.DeepEqual(groups, expect) { + t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups) + } +} + +func TestGroupByPublishDateInReverseOrder(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + expect := PagesGroup{ + {Key: "2012-01", Pages: Pages{pages[1]}}, + {Key: "2012-03", Pages: Pages{pages[3]}}, + {Key: "2012-04", Pages: Pages{pages[0], pages[2], pages[4]}}, + } + + groups, err := pages.GroupByDate("2006-01", "asc") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if !reflect.DeepEqual(groups, expect) { + t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups) + } +} + +func TestGroupByPublishDateWithEmptyPages(t *testing.T) { + t.Parallel() + var pages Pages + groups, err := pages.GroupByPublishDate("2006-01") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if groups != nil { + t.Errorf("PagesGroup isn't empty. It should be %#v, got %#v", nil, groups) + } +} + +func TestGroupByExpiryDate(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + expect := PagesGroup{ + {Key: "2012-04", Pages: Pages{pages[4], pages[2], pages[0]}}, + {Key: "2012-03", Pages: Pages{pages[3]}}, + {Key: "2012-01", Pages: Pages{pages[1]}}, + } + + groups, err := pages.GroupByExpiryDate("2006-01") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if !reflect.DeepEqual(groups, expect) { + t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups) + } +} + +func TestGroupByParamDate(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + expect := PagesGroup{ + {Key: "2012-04", Pages: Pages{pages[4], pages[2], pages[0]}}, + {Key: "2012-03", Pages: Pages{pages[3]}}, + {Key: "2012-01", Pages: Pages{pages[1]}}, + } + + groups, err := pages.GroupByParamDate("custom_date", "2006-01") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if !reflect.DeepEqual(groups, expect) { + t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups) + } +} + +func TestGroupByParamDateInReverseOrder(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + expect := PagesGroup{ + {Key: "2012-01", Pages: Pages{pages[1]}}, + {Key: "2012-03", Pages: Pages{pages[3]}}, + {Key: "2012-04", Pages: Pages{pages[0], pages[2], pages[4]}}, + } + + groups, err := pages.GroupByParamDate("custom_date", "2006-01", "asc") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if !reflect.DeepEqual(groups, expect) { + t.Errorf("PagesGroup has unexpected groups. It should be %#v, got %#v", expect, groups) + } +} + +func TestGroupByParamDateWithEmptyPages(t *testing.T) { + t.Parallel() + var pages Pages + groups, err := pages.GroupByParamDate("custom_date", "2006-01") + if err != nil { + t.Fatalf("Unable to make PagesGroup array: %s", err) + } + if groups != nil { + t.Errorf("PagesGroup isn't empty. It should be %#v, got %#v", nil, groups) + } +} diff --git a/hugolib/pageSort.go b/hugolib/pageSort.go new file mode 100644 index 000000000..6d2431cec --- /dev/null +++ b/hugolib/pageSort.go @@ -0,0 +1,303 @@ +// Copyright 2015 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 hugolib + +import ( + "github.com/spf13/cast" + "sort" +) + +var spc = newPageCache() + +/* + * Implementation of a custom sorter for Pages + */ + +// A pageSorter implements the sort interface for Pages +type pageSorter struct { + pages Pages + by pageBy +} + +// pageBy is a closure used in the Sort.Less method. +type pageBy func(p1, p2 *Page) bool + +// Sort stable sorts the pages given the receiver's sort order. +func (by pageBy) Sort(pages Pages) { + ps := &pageSorter{ + pages: pages, + by: by, // The Sort method's receiver is the function (closure) that defines the sort order. + } + sort.Stable(ps) +} + +// defaultPageSort is the default sort for pages in Hugo: +// Order by Weight, Date, LinkTitle and then full file path. +var defaultPageSort = func(p1, p2 *Page) bool { + if p1.Weight == p2.Weight { + if p1.Date.Unix() == p2.Date.Unix() { + if p1.LinkTitle() == p2.LinkTitle() { + return (p1.FullFilePath() < p2.FullFilePath()) + } + return (p1.LinkTitle() < p2.LinkTitle()) + } + return p1.Date.Unix() > p2.Date.Unix() + } + + if p2.Weight == 0 { + return true + } + + if p1.Weight == 0 { + return false + } + + return p1.Weight < p2.Weight +} + +var languagePageSort = func(p1, p2 *Page) bool { + if p1.Language().Weight == p2.Language().Weight { + if p1.Date.Unix() == p2.Date.Unix() { + if p1.LinkTitle() == p2.LinkTitle() { + return (p1.FullFilePath() < p2.FullFilePath()) + } + return (p1.LinkTitle() < p2.LinkTitle()) + } + return p1.Date.Unix() > p2.Date.Unix() + } + + if p2.Language().Weight == 0 { + return true + } + + if p1.Language().Weight == 0 { + return false + } + + return p1.Language().Weight < p2.Language().Weight +} + +func (ps *pageSorter) Len() int { return len(ps.pages) } +func (ps *pageSorter) Swap(i, j int) { ps.pages[i], ps.pages[j] = ps.pages[j], ps.pages[i] } + +// Less is part of sort.Interface. It is implemented by calling the "by" closure in the sorter. +func (ps *pageSorter) Less(i, j int) bool { return ps.by(ps.pages[i], ps.pages[j]) } + +// Sort sorts the pages by the default sort order defined: +// Order by Weight, Date, LinkTitle and then full file path. +func (p Pages) Sort() { + pageBy(defaultPageSort).Sort(p) +} + +// Limit limits the number of pages returned to n. +func (p Pages) Limit(n int) Pages { + if len(p) > n { + return p[0:n] + } + return p +} + +// ByWeight sorts the Pages by weight and returns a copy. +// +// Adjacent invocations on the same receiver will return a cached result. +// +// This may safely be executed in parallel. +func (p Pages) ByWeight() Pages { + key := "pageSort.ByWeight" + pages, _ := spc.get(key, p, pageBy(defaultPageSort).Sort) + return pages +} + +// ByTitle sorts the Pages by title and returns a copy. +// +// Adjacent invocations on the same receiver will return a cached result. +// +// This may safely be executed in parallel. +func (p Pages) ByTitle() Pages { + + key := "pageSort.ByTitle" + + title := func(p1, p2 *Page) bool { + return p1.Title < p2.Title + } + + pages, _ := spc.get(key, p, pageBy(title).Sort) + return pages +} + +// ByLinkTitle sorts the Pages by link title and returns a copy. +// +// Adjacent invocations on the same receiver will return a cached result. +// +// This may safely be executed in parallel. +func (p Pages) ByLinkTitle() Pages { + + key := "pageSort.ByLinkTitle" + + linkTitle := func(p1, p2 *Page) bool { + return p1.linkTitle < p2.linkTitle + } + + pages, _ := spc.get(key, p, pageBy(linkTitle).Sort) + + return pages +} + +// ByDate sorts the Pages by date and returns a copy. +// +// Adjacent invocations on the same receiver will return a cached result. +// +// This may safely be executed in parallel. +func (p Pages) ByDate() Pages { + + key := "pageSort.ByDate" + + date := func(p1, p2 *Page) bool { + return p1.Date.Unix() < p2.Date.Unix() + } + + pages, _ := spc.get(key, p, pageBy(date).Sort) + + return pages +} + +// ByPublishDate sorts the Pages by publish date and returns a copy. +// +// Adjacent invocations on the same receiver will return a cached result. +// +// This may safely be executed in parallel. +func (p Pages) ByPublishDate() Pages { + + key := "pageSort.ByPublishDate" + + pubDate := func(p1, p2 *Page) bool { + return p1.PublishDate.Unix() < p2.PublishDate.Unix() + } + + pages, _ := spc.get(key, p, pageBy(pubDate).Sort) + + return pages +} + +// ByExpiryDate sorts the Pages by publish date and returns a copy. +// +// Adjacent invocations on the same receiver will return a cached result. +// +// This may safely be executed in parallel. +func (p Pages) ByExpiryDate() Pages { + + key := "pageSort.ByExpiryDate" + + expDate := func(p1, p2 *Page) bool { + return p1.ExpiryDate.Unix() < p2.ExpiryDate.Unix() + } + + pages, _ := spc.get(key, p, pageBy(expDate).Sort) + + return pages +} + +// ByLastmod sorts the Pages by the last modification date and returns a copy. +// +// Adjacent invocations on the same receiver will return a cached result. +// +// This may safely be executed in parallel. +func (p Pages) ByLastmod() Pages { + + key := "pageSort.ByLastmod" + + date := func(p1, p2 *Page) bool { + return p1.Lastmod.Unix() < p2.Lastmod.Unix() + } + + pages, _ := spc.get(key, p, pageBy(date).Sort) + + return pages +} + +// ByLength sorts the Pages by length and returns a copy. +// +// Adjacent invocations on the same receiver will return a cached result. +// +// This may safely be executed in parallel. +func (p Pages) ByLength() Pages { + + key := "pageSort.ByLength" + + length := func(p1, p2 *Page) bool { + return len(p1.Content) < len(p2.Content) + } + + pages, _ := spc.get(key, p, pageBy(length).Sort) + + return pages +} + +// ByLanguage sorts the Pages by the language's Weight. +// +// Adjacent invocations on the same receiver will return a cached result. +// +// This may safely be executed in parallel. +func (p Pages) ByLanguage() Pages { + + key := "pageSort.ByLanguage" + + pages, _ := spc.get(key, p, pageBy(languagePageSort).Sort) + + return pages +} + +// Reverse reverses the order in Pages and returns a copy. +// +// Adjacent invocations on the same receiver will return a cached result. +// +// This may safely be executed in parallel. +func (p Pages) Reverse() Pages { + key := "pageSort.Reverse" + + reverseFunc := func(pages Pages) { + for i, j := 0, len(pages)-1; i < j; i, j = i+1, j-1 { + pages[i], pages[j] = pages[j], pages[i] + } + } + + pages, _ := spc.get(key, p, reverseFunc) + + return pages +} + +func (p Pages) ByParam(paramsKey interface{}) Pages { + paramsKeyStr := cast.ToString(paramsKey) + key := "pageSort.ByParam." + paramsKeyStr + + paramsKeyComparator := func(p1, p2 *Page) bool { + v1, _ := p1.Param(paramsKeyStr) + v2, _ := p2.Param(paramsKeyStr) + s1 := cast.ToString(v1) + s2 := cast.ToString(v2) + + // Sort nils last. + if s1 == "" { + return false + } else if s2 == "" { + return true + } + + return s1 < s2 + } + + pages, _ := spc.get(key, p, pageBy(paramsKeyComparator).Sort) + + return pages +} diff --git a/hugolib/pageSort_test.go b/hugolib/pageSort_test.go new file mode 100644 index 000000000..a17f53dc6 --- /dev/null +++ b/hugolib/pageSort_test.go @@ -0,0 +1,202 @@ +// Copyright 2015 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 hugolib + +import ( + "fmt" + "html/template" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestDefaultSort(t *testing.T) { + t.Parallel() + d1 := time.Now() + d2 := d1.Add(-1 * time.Hour) + d3 := d1.Add(-2 * time.Hour) + d4 := d1.Add(-3 * time.Hour) + + s := newTestSite(t) + + p := createSortTestPages(s, 4) + + // first by weight + setSortVals([4]time.Time{d1, d2, d3, d4}, [4]string{"b", "a", "c", "d"}, [4]int{4, 3, 2, 1}, p) + p.Sort() + + assert.Equal(t, 1, p[0].Weight) + + // Consider zero weight, issue #2673 + setSortVals([4]time.Time{d1, d2, d3, d4}, [4]string{"b", "a", "d", "c"}, [4]int{0, 0, 0, 1}, p) + p.Sort() + + assert.Equal(t, 1, p[0].Weight) + + // next by date + setSortVals([4]time.Time{d3, d4, d1, d2}, [4]string{"a", "b", "c", "d"}, [4]int{1, 1, 1, 1}, p) + p.Sort() + assert.Equal(t, d1, p[0].Date) + + // finally by link title + setSortVals([4]time.Time{d3, d3, d3, d3}, [4]string{"b", "c", "a", "d"}, [4]int{1, 1, 1, 1}, p) + p.Sort() + assert.Equal(t, "al", p[0].LinkTitle()) + assert.Equal(t, "bl", p[1].LinkTitle()) + assert.Equal(t, "cl", p[2].LinkTitle()) +} + +func TestSortByN(t *testing.T) { + t.Parallel() + s := newTestSite(t) + d1 := time.Now() + d2 := d1.Add(-2 * time.Hour) + d3 := d1.Add(-10 * time.Hour) + d4 := d1.Add(-20 * time.Hour) + + p := createSortTestPages(s, 4) + + for i, this := range []struct { + sortFunc func(p Pages) Pages + assertFunc func(p Pages) bool + }{ + {(Pages).ByWeight, func(p Pages) bool { return p[0].Weight == 1 }}, + {(Pages).ByTitle, func(p Pages) bool { return p[0].Title == "ab" }}, + {(Pages).ByLinkTitle, func(p Pages) bool { return p[0].LinkTitle() == "abl" }}, + {(Pages).ByDate, func(p Pages) bool { return p[0].Date == d4 }}, + {(Pages).ByPublishDate, func(p Pages) bool { return p[0].PublishDate == d4 }}, + {(Pages).ByExpiryDate, func(p Pages) bool { return p[0].ExpiryDate == d4 }}, + {(Pages).ByLastmod, func(p Pages) bool { return p[1].Lastmod == d3 }}, + {(Pages).ByLength, func(p Pages) bool { return p[0].Content == "b_content" }}, + } { + setSortVals([4]time.Time{d1, d2, d3, d4}, [4]string{"b", "ab", "cde", "fg"}, [4]int{0, 3, 2, 1}, p) + + sorted := this.sortFunc(p) + if !this.assertFunc(sorted) { + t.Errorf("[%d] sort error", i) + } + } + +} + +func TestLimit(t *testing.T) { + t.Parallel() + s := newTestSite(t) + p := createSortTestPages(s, 10) + firstFive := p.Limit(5) + assert.Equal(t, 5, len(firstFive)) + for i := 0; i < 5; i++ { + assert.Equal(t, p[i], firstFive[i]) + } + assert.Equal(t, p, p.Limit(10)) + assert.Equal(t, p, p.Limit(11)) +} + +func TestPageSortReverse(t *testing.T) { + t.Parallel() + s := newTestSite(t) + p1 := createSortTestPages(s, 10) + assert.Equal(t, 0, p1[0].fuzzyWordCount) + assert.Equal(t, 9, p1[9].fuzzyWordCount) + p2 := p1.Reverse() + assert.Equal(t, 9, p2[0].fuzzyWordCount) + assert.Equal(t, 0, p2[9].fuzzyWordCount) + // cached + assert.True(t, probablyEqualPages(p2, p1.Reverse())) +} + +func TestPageSortByParam(t *testing.T) { + t.Parallel() + var k interface{} = "arbitrarily.nested" + s := newTestSite(t) + + unsorted := createSortTestPages(s, 10) + delete(unsorted[9].Params, "arbitrarily") + + firstSetValue, _ := unsorted[0].Param(k) + secondSetValue, _ := unsorted[1].Param(k) + lastSetValue, _ := unsorted[8].Param(k) + unsetValue, _ := unsorted[9].Param(k) + + assert.Equal(t, "xyz100", firstSetValue) + assert.Equal(t, "xyz99", secondSetValue) + assert.Equal(t, "xyz92", lastSetValue) + assert.Equal(t, nil, unsetValue) + + sorted := unsorted.ByParam("arbitrarily.nested") + firstSetSortedValue, _ := sorted[0].Param(k) + secondSetSortedValue, _ := sorted[1].Param(k) + lastSetSortedValue, _ := sorted[8].Param(k) + unsetSortedValue, _ := sorted[9].Param(k) + + assert.Equal(t, firstSetValue, firstSetSortedValue) + assert.Equal(t, secondSetValue, lastSetSortedValue) + assert.Equal(t, lastSetValue, secondSetSortedValue) + assert.Equal(t, unsetValue, unsetSortedValue) +} + +func BenchmarkSortByWeightAndReverse(b *testing.B) { + s := newTestSite(b) + p := createSortTestPages(s, 300) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + p = p.ByWeight().Reverse() + } +} + +func setSortVals(dates [4]time.Time, titles [4]string, weights [4]int, pages Pages) { + for i := range dates { + pages[i].Date = dates[i] + pages[i].Lastmod = dates[i] + pages[i].Weight = weights[i] + pages[i].Title = titles[i] + // make sure we compare apples and ... apples ... + pages[len(dates)-1-i].linkTitle = pages[i].Title + "l" + pages[len(dates)-1-i].PublishDate = dates[i] + pages[len(dates)-1-i].ExpiryDate = dates[i] + pages[len(dates)-1-i].Content = template.HTML(titles[i] + "_content") + } + lastLastMod := pages[2].Lastmod + pages[2].Lastmod = pages[1].Lastmod + pages[1].Lastmod = lastLastMod +} + +func createSortTestPages(s *Site, num int) Pages { + pages := make(Pages, num) + + for i := 0; i < num; i++ { + p := s.newPage(filepath.FromSlash(fmt.Sprintf("/x/y/p%d.md", i))) + p.Params = map[string]interface{}{ + "arbitrarily": map[string]interface{}{ + "nested": ("xyz" + fmt.Sprintf("%v", 100-i)), + }, + } + + w := 5 + + if i%2 == 0 { + w = 10 + } + p.fuzzyWordCount = i + p.Weight = w + p.Description = "initial" + + pages[i] = p + } + + return pages +} diff --git a/hugolib/page__common.go b/hugolib/page__common.go deleted file mode 100644 index f6f01bbe2..000000000 --- a/hugolib/page__common.go +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright 2019 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 hugolib - -import ( - "sync" - - "github.com/gohugoio/hugo/common/maps" - "github.com/gohugoio/hugo/compare" - "github.com/gohugoio/hugo/lazy" - "github.com/gohugoio/hugo/markup/converter" - "github.com/gohugoio/hugo/navigation" - "github.com/gohugoio/hugo/resources/page" - "github.com/gohugoio/hugo/resources/resource" - "github.com/gohugoio/hugo/source" -) - -type nextPrevProvider interface { - getNextPrev() *nextPrev -} - -func (p *pageCommon) getNextPrev() *nextPrev { - return p.posNextPrev -} - -type nextPrevInSectionProvider interface { - getNextPrevInSection() *nextPrev -} - -func (p *pageCommon) getNextPrevInSection() *nextPrev { - return p.posNextPrevSection -} - -type pageCommon struct { - s *Site - m *pageMeta - - sWrapped page.Site - - // Lazily initialized dependencies. - init *lazy.Init - - // Store holds state that survives server rebuilds. - store *maps.Scratch - - // All of these represents the common parts of a page.Page - navigation.PageMenusProvider - page.AlternativeOutputFormatsProvider - page.ChildCareProvider - page.FileProvider - page.GetPageProvider - page.GitInfoProvider - page.InSectionPositioner - page.OutputFormatsProvider - page.PageMetaProvider - page.PageMetaInternalProvider - page.Positioner - page.RawContentProvider - page.RefProvider - page.ShortcodeInfoProvider - page.SitesProvider - page.TranslationsProvider - page.TreeProvider - resource.LanguageProvider - resource.ResourceDataProvider - resource.ResourceNameTitleProvider - resource.ResourceParamsProvider - resource.ResourceTypeProvider - resource.MediaTypeProvider - resource.TranslationKeyProvider - compare.Eqer - - // Describes how paths and URLs for this page and its descendants - // should look like. - targetPathDescriptor page.TargetPathDescriptor - - // Set if feature enabled and this is in a Git repo. - gitInfo source.GitInfo - codeowners []string - - // Positional navigation - posNextPrev *nextPrev - posNextPrevSection *nextPrev - - // Menus - pageMenus *pageMenus - - // Internal use - page.RelatedDocsHandlerProvider - - contentConverterInit sync.Once - contentConverter converter.Converter -} - -func (p *pageCommon) Store() *maps.Scratch { - return p.store -} - -// See issue 13016. -func (p *pageCommon) Scratch() *maps.Scratch { - return p.Store() -} diff --git a/hugolib/page__content.go b/hugolib/page__content.go deleted file mode 100644 index 20abb7884..000000000 --- a/hugolib/page__content.go +++ /dev/null @@ -1,1191 +0,0 @@ -// Copyright 2019 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 hugolib - -import ( - "context" - "errors" - "fmt" - "html/template" - "io" - "path/filepath" - "strconv" - "strings" - "unicode/utf8" - - maps0 "maps" - - "github.com/bep/logg" - "github.com/gohugoio/hugo/common/hcontext" - "github.com/gohugoio/hugo/common/herrors" - "github.com/gohugoio/hugo/common/hugio" - "github.com/gohugoio/hugo/common/hugo" - "github.com/gohugoio/hugo/common/maps" - "github.com/gohugoio/hugo/common/types/hstring" - "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/markup" - "github.com/gohugoio/hugo/markup/converter" - "github.com/gohugoio/hugo/markup/goldmark/hugocontext" - "github.com/gohugoio/hugo/markup/tableofcontents" - "github.com/gohugoio/hugo/parser/metadecoders" - "github.com/gohugoio/hugo/parser/pageparser" - "github.com/gohugoio/hugo/resources" - "github.com/gohugoio/hugo/resources/page" - "github.com/gohugoio/hugo/resources/resource" - "github.com/gohugoio/hugo/tpl" - "github.com/mitchellh/mapstructure" - "github.com/spf13/cast" -) - -const ( - internalSummaryDividerBase = "HUGOMORE42" -) - -var ( - internalSummaryDividerPreString = "\n\n" + internalSummaryDividerBase + "\n\n" - internalSummaryDividerPre = []byte(internalSummaryDividerPreString) -) - -type pageContentReplacement struct { - val []byte - - source pageparser.Item -} - -func (m *pageMeta) parseFrontMatter(h *HugoSites, pid uint64) (*contentParseInfo, error) { - var ( - sourceKey string - openSource hugio.OpenReadSeekCloser - isFromContentAdapter = m.pageConfig.IsFromContentAdapter - ) - - if m.f != nil && !isFromContentAdapter { - sourceKey = filepath.ToSlash(m.f.Filename()) - if !isFromContentAdapter { - meta := m.f.FileInfo().Meta() - openSource = func() (hugio.ReadSeekCloser, error) { - r, err := meta.Open() - if err != nil { - return nil, fmt.Errorf("failed to open file %q: %w", meta.Filename, err) - } - return r, nil - } - } - } else if isFromContentAdapter { - openSource = m.pageConfig.Content.ValueAsOpenReadSeekCloser() - } - - if sourceKey == "" { - sourceKey = strconv.FormatUint(pid, 10) - } - - pi := &contentParseInfo{ - h: h, - pid: pid, - sourceKey: sourceKey, - openSource: openSource, - } - - source, err := pi.contentSource(m) - if err != nil { - return nil, err - } - - items, err := pageparser.ParseBytes( - source, - pageparser.Config{ - NoFrontMatter: isFromContentAdapter, - }, - ) - if err != nil { - return nil, err - } - - pi.itemsStep1 = items - - if isFromContentAdapter { - // No front matter. - return pi, nil - } - - if err := pi.mapFrontMatter(source); err != nil { - return nil, err - } - - return pi, nil -} - -func (m *pageMeta) newCachedContent(h *HugoSites, pi *contentParseInfo) (*cachedContent, error) { - var filename string - if m.f != nil { - filename = m.f.Filename() - } - - c := &cachedContent{ - pm: m.s.pageMap, - StaleInfo: m, - shortcodeState: newShortcodeHandler(filename, m.s), - pi: pi, - enableEmoji: m.s.conf.EnableEmoji, - scopes: maps.NewCache[string, *cachedContentScope](), - } - - source, err := c.pi.contentSource(m) - if err != nil { - return nil, err - } - - if err := c.parseContentFile(source); err != nil { - return nil, err - } - - return c, nil -} - -type cachedContent struct { - pm *pageMap - - resource.StaleInfo - - shortcodeState *shortcodeHandler - - // Parsed content. - pi *contentParseInfo - - enableEmoji bool - - scopes *maps.Cache[string, *cachedContentScope] -} - -func (c *cachedContent) getOrCreateScope(scope string, pco *pageContentOutput) *cachedContentScope { - key := scope + pco.po.f.Name - cs, _ := c.scopes.GetOrCreate(key, func() (*cachedContentScope, error) { - return &cachedContentScope{ - cachedContent: c, - pco: pco, - scope: scope, - }, nil - }) - return cs -} - -type contentParseInfo struct { - h *HugoSites - - pid uint64 - sourceKey string - - // The source bytes. - openSource hugio.OpenReadSeekCloser - - frontMatter map[string]any - - // Whether the parsed content contains a summary separator. - hasSummaryDivider bool - - // Returns the position in bytes after any front matter. - posMainContent int - - // Indicates whether we must do placeholder replacements. - hasNonMarkdownShortcode bool - - // Items from the page parser. - // These maps directly to the source - itemsStep1 pageparser.Items - - // *shortcode, pageContentReplacement or pageparser.Item - itemsStep2 []any -} - -func (p *contentParseInfo) AddBytes(item pageparser.Item) { - p.itemsStep2 = append(p.itemsStep2, item) -} - -func (p *contentParseInfo) AddReplacement(val []byte, source pageparser.Item) { - p.itemsStep2 = append(p.itemsStep2, pageContentReplacement{val: val, source: source}) -} - -func (p *contentParseInfo) AddShortcode(s *shortcode) { - p.itemsStep2 = append(p.itemsStep2, s) - if s.insertPlaceholder() { - p.hasNonMarkdownShortcode = true - } -} - -// contentToRenderForItems returns the content to be processed by Goldmark or similar. -func (pi *contentParseInfo) contentToRender(ctx context.Context, source []byte, renderedShortcodes map[string]shortcodeRenderer) ([]byte, bool, error) { - var hasVariants bool - c := make([]byte, 0, len(source)+(len(source)/10)) - - for _, it := range pi.itemsStep2 { - switch v := it.(type) { - case pageparser.Item: - c = append(c, source[v.Pos():v.Pos()+len(v.Val(source))]...) - case pageContentReplacement: - c = append(c, v.val...) - case *shortcode: - if !v.insertPlaceholder() { - // Insert the rendered shortcode. - renderedShortcode, found := renderedShortcodes[v.placeholder] - if !found { - // This should never happen. - panic(fmt.Sprintf("rendered shortcode %q not found", v.placeholder)) - } - - b, more, err := renderedShortcode.renderShortcode(ctx) - if err != nil { - return nil, false, fmt.Errorf("failed to render shortcode: %w", err) - } - hasVariants = hasVariants || more - c = append(c, []byte(b)...) - - } else { - // Insert the placeholder so we can insert the content after - // markdown processing. - c = append(c, []byte(v.placeholder)...) - } - default: - panic(fmt.Sprintf("unknown item type %T", it)) - } - } - - return c, hasVariants, nil -} - -func (c *cachedContent) IsZero() bool { - return len(c.pi.itemsStep2) == 0 -} - -func (c *cachedContent) parseContentFile(source []byte) error { - if source == nil || c.pi.openSource == nil { - return nil - } - - return c.pi.mapItemsAfterFrontMatter(source, c.shortcodeState) -} - -func (c *contentParseInfo) parseFrontMatter(it pageparser.Item, iter *pageparser.Iterator, source []byte) error { - if c.frontMatter != nil { - return nil - } - - f := pageparser.FormatFromFrontMatterType(it.Type) - var err error - c.frontMatter, err = metadecoders.Default.UnmarshalToMap(it.Val(source), f) - if err != nil { - if fe, ok := err.(herrors.FileError); ok { - pos := fe.Position() - - // Offset the starting position of front matter. - offset := iter.LineNumber(source) - 1 - if f == metadecoders.YAML { - offset -= 1 - } - pos.LineNumber += offset - - fe.UpdatePosition(pos) - fe.SetFilename("") // It will be set later. - - return fe - } else { - return err - } - } - - return nil -} - -func (rn *contentParseInfo) failMap(source []byte, err error, i pageparser.Item) error { - if fe, ok := err.(herrors.FileError); ok { - return fe - } - - pos := posFromInput("", source, i.Pos()) - - return herrors.NewFileErrorFromPos(err, pos) -} - -func (rn *contentParseInfo) mapFrontMatter(source []byte) error { - if len(rn.itemsStep1) == 0 { - return nil - } - iter := pageparser.NewIterator(rn.itemsStep1) - -Loop: - for { - it := iter.Next() - switch { - case it.IsFrontMatter(): - if err := rn.parseFrontMatter(it, iter, source); err != nil { - return err - } - next := iter.Peek() - if !next.IsDone() { - rn.posMainContent = next.Pos() - } - // Done. - break Loop - case it.IsEOF(): - break Loop - case it.IsError(): - return rn.failMap(source, it.Err, it) - default: - - } - } - - return nil -} - -func (rn *contentParseInfo) mapItemsAfterFrontMatter( - source []byte, - s *shortcodeHandler, -) error { - if len(rn.itemsStep1) == 0 { - return nil - } - - fail := func(err error, i pageparser.Item) error { - if fe, ok := err.(herrors.FileError); ok { - return fe - } - - pos := posFromInput("", source, i.Pos()) - - return herrors.NewFileErrorFromPos(err, pos) - } - - iter := pageparser.NewIterator(rn.itemsStep1) - - // the parser is guaranteed to return items in proper order or fail, so … - // … it's safe to keep some "global" state - var ordinal int - -Loop: - for { - it := iter.Next() - - switch { - case it.Type == pageparser.TypeIgnore: - case it.IsFrontMatter(): - // Ignore. - case it.Type == pageparser.TypeLeadSummaryDivider: - posBody := -1 - f := func(item pageparser.Item) bool { - if posBody == -1 && !item.IsDone() { - posBody = item.Pos() - } - - if item.IsNonWhitespace(source) { - // Done - return false - } - return true - } - iter.PeekWalk(f) - - rn.hasSummaryDivider = true - - // The content may be rendered by Goldmark or similar, - // and we need to track the summary. - rn.AddReplacement(internalSummaryDividerPre, it) - - // Handle shortcode - case it.IsLeftShortcodeDelim(): - // let extractShortcode handle left delim (will do so recursively) - iter.Backup() - - currShortcode, err := s.extractShortcode(ordinal, 0, source, iter) - if err != nil { - return fail(err, it) - } - - currShortcode.pos = it.Pos() - currShortcode.length = iter.Current().Pos() - it.Pos() - if currShortcode.placeholder == "" { - currShortcode.placeholder = createShortcodePlaceholder("s", rn.pid, currShortcode.ordinal) - } - - if currShortcode.name != "" { - s.addName(currShortcode.name) - } - - if currShortcode.params == nil { - var s []string - currShortcode.params = s - } - - currShortcode.placeholder = createShortcodePlaceholder("s", rn.pid, ordinal) - ordinal++ - s.shortcodes = append(s.shortcodes, currShortcode) - - rn.AddShortcode(currShortcode) - - case it.IsEOF(): - break Loop - case it.IsError(): - return fail(it.Err, it) - default: - rn.AddBytes(it) - } - } - - return nil -} - -func (c *cachedContent) mustSource() []byte { - source, err := c.pi.contentSource(c) - if err != nil { - panic(err) - } - return source -} - -func (c *contentParseInfo) contentSource(s resource.StaleInfo) ([]byte, error) { - key := c.sourceKey - versionv := s.StaleVersion() - - v, err := c.h.cacheContentSource.GetOrCreate(key, func(string) (*resources.StaleValue[[]byte], error) { - b, err := c.readSourceAll() - if err != nil { - return nil, err - } - - return &resources.StaleValue[[]byte]{ - Value: b, - StaleVersionFunc: func() uint32 { - return s.StaleVersion() - versionv - }, - }, nil - }) - if err != nil { - return nil, err - } - - return v.Value, nil -} - -func (c *contentParseInfo) readSourceAll() ([]byte, error) { - if c.openSource == nil { - return []byte{}, nil - } - r, err := c.openSource() - if err != nil { - return nil, err - } - defer r.Close() - - return io.ReadAll(r) -} - -type contentTableOfContents struct { - // For Goldmark we split Parse and Render. - astDoc any - - tableOfContents *tableofcontents.Fragments - tableOfContentsHTML template.HTML - - // Temporary storage of placeholders mapped to their content. - // These are shortcodes etc. Some of these will need to be replaced - // after any markup is rendered, so they share a common prefix. - contentPlaceholders map[string]shortcodeRenderer - - contentToRender []byte -} - -type contentSummary struct { - content template.HTML - contentWithoutSummary template.HTML - summary page.Summary -} - -type contentPlainPlainWords struct { - plain string - plainWords []string - - wordCount int - fuzzyWordCount int - readingTime int -} - -func (c *cachedContentScope) keyScope(ctx context.Context) string { - return hugo.GetMarkupScope(ctx) + c.pco.po.f.Name -} - -func (c *cachedContentScope) contentRendered(ctx context.Context) (contentSummary, error) { - cp := c.pco - ctx = tpl.Context.DependencyScope.Set(ctx, pageDependencyScopeGlobal) - key := c.pi.sourceKey + "/" + c.keyScope(ctx) - versionv := c.version(cp) - - v, err := c.pm.cacheContentRendered.GetOrCreate(key, func(string) (*resources.StaleValue[contentSummary], error) { - cp.po.p.s.Log.Trace(logg.StringFunc(func() string { - return fmt.Sprintln("contentRendered", key) - })) - - cp.po.p.s.h.contentRenderCounter.Add(1) - cp.contentRendered.Store(true) - po := cp.po - - ct, err := c.contentToC(ctx) - if err != nil { - return nil, err - } - - rs, err := func() (*resources.StaleValue[contentSummary], error) { - rs := &resources.StaleValue[contentSummary]{ - StaleVersionFunc: func() uint32 { - return c.version(cp) - versionv - }, - } - - if len(c.pi.itemsStep2) == 0 { - // Nothing to do. - return rs, nil - } - - var b []byte - - if ct.astDoc != nil { - // The content is parsed, but not rendered. - r, ok, err := po.contentRenderer.RenderContent(ctx, ct.contentToRender, ct.astDoc) - if err != nil { - return nil, err - } - - if !ok { - return nil, errors.New("invalid state: astDoc is set but RenderContent returned false") - } - - b = r.Bytes() - - } else { - // Copy the content to be rendered. - b = make([]byte, len(ct.contentToRender)) - copy(b, ct.contentToRender) - } - - // There are one or more replacement tokens to be replaced. - var hasShortcodeVariants bool - tokenHandler := func(ctx context.Context, token string) ([]byte, error) { - if token == tocShortcodePlaceholder { - return []byte(ct.tableOfContentsHTML), nil - } - renderer, found := ct.contentPlaceholders[token] - if found { - repl, more, err := renderer.renderShortcode(ctx) - if err != nil { - return nil, err - } - hasShortcodeVariants = hasShortcodeVariants || more - return repl, nil - } - // This should never happen. - panic(fmt.Errorf("unknown shortcode token %q (number of tokens: %d)", token, len(ct.contentPlaceholders))) - } - - b, err = expandShortcodeTokens(ctx, b, tokenHandler) - if err != nil { - return nil, err - } - if hasShortcodeVariants { - cp.po.p.incrPageOutputTemplateVariation() - } - - var result contentSummary - if c.pi.hasSummaryDivider { - s := string(b) - summarized := page.ExtractSummaryFromHTMLWithDivider(cp.po.p.m.pageConfig.ContentMediaType, s, internalSummaryDividerBase) - result.summary = page.Summary{ - Text: template.HTML(summarized.Summary()), - Type: page.SummaryTypeManual, - Truncated: summarized.Truncated(), - } - result.contentWithoutSummary = template.HTML(summarized.ContentWithoutSummary()) - result.content = template.HTML(summarized.Content()) - } else { - result.content = template.HTML(string(b)) - } - - if !c.pi.hasSummaryDivider && cp.po.p.m.pageConfig.Summary == "" { - numWords := cp.po.p.s.conf.SummaryLength - isCJKLanguage := cp.po.p.m.pageConfig.IsCJKLanguage - summary := page.ExtractSummaryFromHTML(cp.po.p.m.pageConfig.ContentMediaType, string(result.content), numWords, isCJKLanguage) - result.summary = page.Summary{ - Text: template.HTML(summary.Summary()), - Type: page.SummaryTypeAuto, - Truncated: summary.Truncated(), - } - result.contentWithoutSummary = template.HTML(summary.ContentWithoutSummary()) - } - rs.Value = result - - return rs, nil - }() - if err != nil { - return rs, cp.po.p.wrapError(err) - } - - if rs.Value.summary.IsZero() { - b, err := cp.po.contentRenderer.ParseAndRenderContent(ctx, []byte(cp.po.p.m.pageConfig.Summary), false) - if err != nil { - return nil, err - } - html := cp.po.p.s.ContentSpec.TrimShortHTML(b.Bytes(), cp.po.p.m.pageConfig.Content.Markup) - rs.Value.summary = page.Summary{ - Text: helpers.BytesToHTML(html), - Type: page.SummaryTypeFrontMatter, - } - rs.Value.contentWithoutSummary = rs.Value.content - } - - return rs, err - }) - if err != nil { - return contentSummary{}, cp.po.p.wrapError(err) - } - - return v.Value, nil -} - -func (c *cachedContentScope) mustContentToC(ctx context.Context) contentTableOfContents { - ct, err := c.contentToC(ctx) - if err != nil { - panic(err) - } - return ct -} - -type contextKey uint8 - -const ( - contextKeyContentCallback contextKey = iota -) - -var setGetContentCallbackInContext = hcontext.NewContextDispatcher[func(*pageContentOutput, contentTableOfContents)](contextKeyContentCallback) - -func (c *cachedContentScope) contentToC(ctx context.Context) (contentTableOfContents, error) { - cp := c.pco - key := c.pi.sourceKey + "/" + c.keyScope(ctx) - versionv := c.version(cp) - - v, err := c.pm.contentTableOfContents.GetOrCreate(key, func(string) (*resources.StaleValue[contentTableOfContents], error) { - source, err := c.pi.contentSource(c) - if err != nil { - return nil, err - } - - var ct contentTableOfContents - if err := cp.initRenderHooks(); err != nil { - return nil, err - } - po := cp.po - p := po.p - ct.contentPlaceholders, err = c.shortcodeState.prepareShortcodesForPage(ctx, po, false) - if err != nil { - return nil, err - } - - // Callback called from below (e.g. in .RenderString) - ctxCallback := func(cp2 *pageContentOutput, ct2 contentTableOfContents) { - cp.otherOutputs.Set(cp2.po.p.pid, cp2) - - // Merge content placeholders - maps0.Copy(ct.contentPlaceholders, ct2.contentPlaceholders) - - if p.s.conf.Internal.Watch { - for _, s := range cp2.po.p.m.content.shortcodeState.shortcodes { - cp.trackDependency(s.templ) - } - } - - // Transfer shortcode names so HasShortcode works for shortcodes from included pages. - cp.po.p.m.content.shortcodeState.transferNames(cp2.po.p.m.content.shortcodeState) - if cp2.po.p.pageOutputTemplateVariationsState.Load() > 0 { - cp.po.p.incrPageOutputTemplateVariation() - } - } - - ctx = setGetContentCallbackInContext.Set(ctx, ctxCallback) - - var hasVariants bool - ct.contentToRender, hasVariants, err = c.pi.contentToRender(ctx, source, ct.contentPlaceholders) - if err != nil { - return nil, err - } - - if hasVariants { - p.incrPageOutputTemplateVariation() - } - - isHTML := cp.po.p.m.pageConfig.ContentMediaType.IsHTML() - - if !isHTML { - createAndSetToC := func(tocProvider converter.TableOfContentsProvider) error { - cfg := p.s.ContentSpec.Converters.GetMarkupConfig() - ct.tableOfContents = tocProvider.TableOfContents() - ct.tableOfContentsHTML, err = ct.tableOfContents.ToHTML( - cfg.TableOfContents.StartLevel, - cfg.TableOfContents.EndLevel, - cfg.TableOfContents.Ordered, - ) - return err - } - - // If the converter supports doing the parsing separately, we do that. - parseResult, ok, err := po.contentRenderer.ParseContent(ctx, ct.contentToRender) - if err != nil { - return nil, err - } - if ok { - // This is Goldmark. - // Store away the parse result for later use. - createAndSetToC(parseResult) - - ct.astDoc = parseResult.Doc() - - } else { - - // This is Asciidoctor etc. - r, err := po.contentRenderer.ParseAndRenderContent(ctx, ct.contentToRender, true) - if err != nil { - return nil, err - } - - ct.contentToRender = r.Bytes() - - if tocProvider, ok := r.(converter.TableOfContentsProvider); ok { - createAndSetToC(tocProvider) - } else { - tmpContent, tmpTableOfContents := helpers.ExtractTOC(ct.contentToRender) - ct.tableOfContentsHTML = helpers.BytesToHTML(tmpTableOfContents) - ct.tableOfContents = tableofcontents.Empty - ct.contentToRender = tmpContent - } - } - } - - return &resources.StaleValue[contentTableOfContents]{ - Value: ct, - StaleVersionFunc: func() uint32 { - return c.version(cp) - versionv - }, - }, nil - }) - if err != nil { - return contentTableOfContents{}, err - } - - return v.Value, nil -} - -func (c *cachedContent) version(cp *pageContentOutput) uint32 { - // Both of these gets incremented on change. - return c.StaleVersion() + cp.contentRenderedVersion -} - -func (c *cachedContentScope) contentPlain(ctx context.Context) (contentPlainPlainWords, error) { - cp := c.pco - key := c.pi.sourceKey + "/" + c.keyScope(ctx) - - versionv := c.version(cp) - - v, err := c.pm.cacheContentPlain.GetOrCreateWitTimeout(key, cp.po.p.s.Conf.Timeout(), func(string) (*resources.StaleValue[contentPlainPlainWords], error) { - var result contentPlainPlainWords - rs := &resources.StaleValue[contentPlainPlainWords]{ - StaleVersionFunc: func() uint32 { - return c.version(cp) - versionv - }, - } - - rendered, err := c.contentRendered(ctx) - if err != nil { - return nil, err - } - - result.plain = tpl.StripHTML(string(rendered.content)) - result.plainWords = strings.Fields(result.plain) - - isCJKLanguage := cp.po.p.m.pageConfig.IsCJKLanguage - - if isCJKLanguage { - result.wordCount = 0 - for _, word := range result.plainWords { - runeCount := utf8.RuneCountInString(word) - if len(word) == runeCount { - result.wordCount++ - } else { - result.wordCount += runeCount - } - } - } else { - result.wordCount = helpers.TotalWords(result.plain) - } - - // TODO(bep) is set in a test. Fix that. - if result.fuzzyWordCount == 0 { - result.fuzzyWordCount = (result.wordCount + 100) / 100 * 100 - } - - if isCJKLanguage { - result.readingTime = (result.wordCount + 500) / 501 - } else { - result.readingTime = (result.wordCount + 212) / 213 - } - - rs.Value = result - - return rs, nil - }) - if err != nil { - if herrors.IsTimeoutError(err) { - err = fmt.Errorf("timed out rendering the page content. Extend the `timeout` limit in your Hugo config file: %w", err) - } - return contentPlainPlainWords{}, err - } - return v.Value, nil -} - -type cachedContentScope struct { - *cachedContent - pco *pageContentOutput - scope string -} - -func (c *cachedContentScope) prepareContext(ctx context.Context) context.Context { - // A regular page's shortcode etc. may be rendered by e.g. the home page, - // so we need to track any changes to this content's page. - ctx = tpl.Context.DependencyManagerScopedProvider.Set(ctx, c.pco.po.p) - - // The markup scope is recursive, so if already set to a non zero value, preserve that value. - if s := hugo.GetMarkupScope(ctx); s != "" || s == c.scope { - return ctx - } - return hugo.SetMarkupScope(ctx, c.scope) -} - -func (c *cachedContentScope) Render(ctx context.Context) (page.Content, error) { - return c, nil -} - -func (c *cachedContentScope) Content(ctx context.Context) (template.HTML, error) { - ctx = c.prepareContext(ctx) - cr, err := c.contentRendered(ctx) - if err != nil { - return "", err - } - return cr.content, nil -} - -func (c *cachedContentScope) ContentWithoutSummary(ctx context.Context) (template.HTML, error) { - ctx = c.prepareContext(ctx) - cr, err := c.contentRendered(ctx) - if err != nil { - return "", err - } - return cr.contentWithoutSummary, nil -} - -func (c *cachedContentScope) Summary(ctx context.Context) (page.Summary, error) { - ctx = c.prepareContext(ctx) - rendered, err := c.contentRendered(ctx) - return rendered.summary, err -} - -func (c *cachedContentScope) RenderString(ctx context.Context, args ...any) (template.HTML, error) { - ctx = c.prepareContext(ctx) - - if len(args) < 1 || len(args) > 2 { - return "", errors.New("want 1 or 2 arguments") - } - - pco := c.pco - - var contentToRender string - opts := defaultRenderStringOpts - sidx := 1 - - if len(args) == 1 { - sidx = 0 - } else { - m, ok := args[0].(map[string]any) - if !ok { - return "", errors.New("first argument must be a map") - } - - if err := mapstructure.WeakDecode(m, &opts); err != nil { - return "", fmt.Errorf("failed to decode options: %w", err) - } - if opts.Markup != "" { - opts.Markup = markup.ResolveMarkup(opts.Markup) - } - } - - contentToRenderv := args[sidx] - - if _, ok := contentToRenderv.(hstring.HTML); ok { - // This content is already rendered, this is potentially - // a infinite recursion. - return "", errors.New("text is already rendered, repeating it may cause infinite recursion") - } - - var err error - contentToRender, err = cast.ToStringE(contentToRenderv) - if err != nil { - return "", err - } - - if err = pco.initRenderHooks(); err != nil { - return "", err - } - - conv := pco.po.p.getContentConverter() - - if opts.Markup != "" && opts.Markup != pco.po.p.m.pageConfig.ContentMediaType.SubType { - var err error - conv, err = pco.po.p.m.newContentConverter(pco.po.p, opts.Markup) - if err != nil { - return "", pco.po.p.wrapError(err) - } - } - - var rendered []byte - - parseInfo := &contentParseInfo{ - h: pco.po.p.s.h, - pid: pco.po.p.pid, - } - - if pageparser.HasShortcode(contentToRender) { - contentToRenderb := []byte(contentToRender) - // String contains a shortcode. - parseInfo.itemsStep1, err = pageparser.ParseBytes(contentToRenderb, pageparser.Config{ - NoFrontMatter: true, - NoSummaryDivider: true, - }) - if err != nil { - return "", err - } - - s := newShortcodeHandler(pco.po.p.pathOrTitle(), pco.po.p.s) - if err := parseInfo.mapItemsAfterFrontMatter(contentToRenderb, s); err != nil { - return "", err - } - - placeholders, err := s.prepareShortcodesForPage(ctx, pco.po, true) - if err != nil { - return "", err - } - - contentToRender, hasVariants, err := parseInfo.contentToRender(ctx, contentToRenderb, placeholders) - if err != nil { - return "", err - } - if hasVariants { - pco.po.p.incrPageOutputTemplateVariation() - } - b, err := pco.renderContentWithConverter(ctx, conv, contentToRender, false) - if err != nil { - return "", pco.po.p.wrapError(err) - } - rendered = b.Bytes() - - if parseInfo.hasNonMarkdownShortcode { - var hasShortcodeVariants bool - - tokenHandler := func(ctx context.Context, token string) ([]byte, error) { - if token == tocShortcodePlaceholder { - toc, err := c.contentToC(ctx) - if err != nil { - return nil, err - } - // The Page's TableOfContents was accessed in a shortcode. - return []byte(toc.tableOfContentsHTML), nil - } - renderer, found := placeholders[token] - if found { - repl, more, err := renderer.renderShortcode(ctx) - if err != nil { - return nil, err - } - hasShortcodeVariants = hasShortcodeVariants || more - return repl, nil - } - // This should not happen. - return nil, fmt.Errorf("unknown shortcode token %q", token) - } - - rendered, err = expandShortcodeTokens(ctx, rendered, tokenHandler) - if err != nil { - return "", err - } - if hasShortcodeVariants { - pco.po.p.incrPageOutputTemplateVariation() - } - } - - // We need a consolidated view in $page.HasShortcode - pco.po.p.m.content.shortcodeState.transferNames(s) - - } else { - c, err := pco.renderContentWithConverter(ctx, conv, []byte(contentToRender), false) - if err != nil { - return "", pco.po.p.wrapError(err) - } - - rendered = c.Bytes() - } - - if opts.Display == "inline" { - markup := pco.po.p.m.pageConfig.Content.Markup - if opts.Markup != "" { - markup = pco.po.p.s.ContentSpec.ResolveMarkup(opts.Markup) - } - rendered = pco.po.p.s.ContentSpec.TrimShortHTML(rendered, markup) - } - - return template.HTML(string(rendered)), nil -} - -func (c *cachedContentScope) RenderShortcodes(ctx context.Context) (template.HTML, error) { - ctx = c.prepareContext(ctx) - - pco := c.pco - content := pco.po.p.m.content - - source, err := content.pi.contentSource(content) - if err != nil { - return "", err - } - ct, err := c.contentToC(ctx) - if err != nil { - return "", err - } - - var insertPlaceholders bool - var hasVariants bool - cb := setGetContentCallbackInContext.Get(ctx) - if cb != nil { - insertPlaceholders = true - } - cc := make([]byte, 0, len(source)+(len(source)/10)) - for _, it := range content.pi.itemsStep2 { - switch v := it.(type) { - case pageparser.Item: - cc = append(cc, source[v.Pos():v.Pos()+len(v.Val(source))]...) - case pageContentReplacement: - // Ignore. - case *shortcode: - if !insertPlaceholders || !v.insertPlaceholder() { - // Insert the rendered shortcode. - renderedShortcode, found := ct.contentPlaceholders[v.placeholder] - if !found { - // This should never happen. - panic(fmt.Sprintf("rendered shortcode %q not found", v.placeholder)) - } - - b, more, err := renderedShortcode.renderShortcode(ctx) - if err != nil { - return "", fmt.Errorf("failed to render shortcode: %w", err) - } - hasVariants = hasVariants || more - cc = append(cc, []byte(b)...) - - } else { - // Insert the placeholder so we can insert the content after - // markdown processing. - cc = append(cc, []byte(v.placeholder)...) - } - default: - panic(fmt.Sprintf("unknown item type %T", it)) - } - } - - if hasVariants { - pco.po.p.incrPageOutputTemplateVariation() - } - - if cb != nil { - cb(pco, ct) - } - - if tpl.Context.IsInGoldmark.Get(ctx) { - // This content will be parsed and rendered by Goldmark. - // Wrap it in a special Hugo markup to assign the correct Page from - // the stack. - return template.HTML(hugocontext.Wrap(cc, pco.po.p.pid)), nil - } - - return helpers.BytesToHTML(cc), nil -} - -func (c *cachedContentScope) Plain(ctx context.Context) string { - ctx = c.prepareContext(ctx) - return c.mustContentPlain(ctx).plain -} - -func (c *cachedContentScope) PlainWords(ctx context.Context) []string { - ctx = c.prepareContext(ctx) - return c.mustContentPlain(ctx).plainWords -} - -func (c *cachedContentScope) WordCount(ctx context.Context) int { - ctx = c.prepareContext(ctx) - return c.mustContentPlain(ctx).wordCount -} - -func (c *cachedContentScope) FuzzyWordCount(ctx context.Context) int { - ctx = c.prepareContext(ctx) - return c.mustContentPlain(ctx).fuzzyWordCount -} - -func (c *cachedContentScope) ReadingTime(ctx context.Context) int { - ctx = c.prepareContext(ctx) - return c.mustContentPlain(ctx).readingTime -} - -func (c *cachedContentScope) Len(ctx context.Context) int { - ctx = c.prepareContext(ctx) - return len(c.mustContentRendered(ctx).content) -} - -func (c *cachedContentScope) Fragments(ctx context.Context) *tableofcontents.Fragments { - ctx = c.prepareContext(ctx) - toc := c.mustContentToC(ctx).tableOfContents - if toc == nil { - return nil - } - return toc -} - -func (c *cachedContentScope) fragmentsHTML(ctx context.Context) template.HTML { - ctx = c.prepareContext(ctx) - return c.mustContentToC(ctx).tableOfContentsHTML -} - -func (c *cachedContentScope) mustContentPlain(ctx context.Context) contentPlainPlainWords { - r, err := c.contentPlain(ctx) - if err != nil { - c.pco.fail(err) - } - return r -} - -func (c *cachedContentScope) mustContentRendered(ctx context.Context) contentSummary { - r, err := c.contentRendered(ctx) - if err != nil { - c.pco.fail(err) - } - return r -} diff --git a/hugolib/page__data.go b/hugolib/page__data.go deleted file mode 100644 index 256d8a97f..000000000 --- a/hugolib/page__data.go +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2019 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 hugolib - -import ( - "strings" - "sync" - - "github.com/gohugoio/hugo/resources/kinds" - "github.com/gohugoio/hugo/resources/page" -) - -type pageData struct { - *pageState - - dataInit sync.Once - data page.Data -} - -func (p *pageData) Data() any { - p.dataInit.Do(func() { - p.data = make(page.Data) - - if p.Kind() == kinds.KindPage { - return - } - - switch p.Kind() { - case kinds.KindTerm: - path := p.Path() - name := p.s.pageMap.cfg.getTaxonomyConfig(path) - term := p.s.Taxonomies()[name.plural].Get(strings.TrimPrefix(path, name.pluralTreeKey)) - p.data[name.singular] = term - p.data["Singular"] = name.singular - p.data["Plural"] = name.plural - p.data["Term"] = p.m.term - case kinds.KindTaxonomy: - viewCfg := p.s.pageMap.cfg.getTaxonomyConfig(p.Path()) - p.data["Singular"] = viewCfg.singular - p.data["Plural"] = viewCfg.plural - p.data["Terms"] = p.s.Taxonomies()[viewCfg.plural] - // keep the following just for legacy reasons - p.data["OrderedIndex"] = p.data["Terms"] - p.data["Index"] = p.data["Terms"] - } - - // Assign the function to the map to make sure it is lazily initialized - p.data["pages"] = p.Pages - }) - - return p.data -} diff --git a/hugolib/page__fragments_test.go b/hugolib/page__fragments_test.go deleted file mode 100644 index c30fa829e..000000000 --- a/hugolib/page__fragments_test.go +++ /dev/null @@ -1,110 +0,0 @@ -// 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 hugolib - -import "testing" - -// #10794 -func TestFragmentsAndToCCrossSiteAccess(t *testing.T) { - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableKinds = ["taxonomy", "term", "home"] -defaultContentLanguage = "en" -defaultContentLanguageInSubdir = true -[languages] -[languages.en] -weight = 1 -[languages.fr] -weight = 2 --- content/p1.en.md -- ---- -title: "P1" -outputs: ["HTML", "JSON"] ---- - -## Heading 1 EN - --- content/p1.fr.md -- ---- -title: "P1" -outputs: ["HTML", "JSON"] ---- - -## Heading 1 FR --- layouts/_default/single.html -- -HTML --- layouts/_default/single.json -- -{{ $secondSite := index .Sites 1 }} -{{ $p1 := $secondSite.GetPage "p1" }} -ToC: {{ $p1.TableOfContents }} -Fragments : {{ $p1.Fragments.Identifiers }} - - - - -` - - b := NewIntegrationTestBuilder( - IntegrationTestConfig{ - TxtarString: files, - T: t, - }, - ).Build() - - b.AssertFileContent("public/en/p1/index.html", "HTML") - b.AssertFileContent("public/en/p1/index.json", "ToC: <nav id=\"TableOfContents\">\n <ul>\n <li><a href=\"#heading-1-fr\">Heading 1 FR</a></li>\n </ul>\n</nav>\nFragments : [heading-1-fr]") -} - -// Issue #10866 -func TestTableOfContentsWithIncludedMarkdownFile(t *testing.T) { - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableKinds = ["taxonomy", "term", "home"] --- content/p1.md -- ---- -title: "P1" ---- - -## Heading P1 1 -{{% include "p2" %}} - --- content/p2.md -- ---- -title: "P2" ---- - -### Heading P2 1 -### Heading P2 2 - --- layouts/shortcodes/include.html -- -{{ with site.GetPage (.Get 0) }}{{ .RawContent }}{{ end }} --- layouts/_default/single.html -- -Fragments: {{ .Fragments.Identifiers }}| - - - -` - - b := NewIntegrationTestBuilder( - IntegrationTestConfig{ - TxtarString: files, - T: t, - }, - ).Build() - - b.AssertFileContent("public/p1/index.html", "Fragments: [heading-p1-1 heading-p2-1 heading-p2-2]|") - b.AssertFileContent("public/p2/index.html", "Fragments: [heading-p2-1 heading-p2-2]|") -} diff --git a/hugolib/page__menus.go b/hugolib/page__menus.go deleted file mode 100644 index 1666036ce..000000000 --- a/hugolib/page__menus.go +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2019 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 hugolib - -import ( - "context" - "sync" - - "github.com/gohugoio/hugo/navigation" -) - -type pageMenus struct { - p *pageState - - q navigation.MenuQueryProvider - - pmInit sync.Once - pm navigation.PageMenus -} - -func (p *pageMenus) HasMenuCurrent(menuID string, me *navigation.MenuEntry) bool { - p.p.s.init.menus.Do(context.Background()) - p.init() - return p.q.HasMenuCurrent(menuID, me) -} - -func (p *pageMenus) IsMenuCurrent(menuID string, inme *navigation.MenuEntry) bool { - p.p.s.init.menus.Do(context.Background()) - p.init() - return p.q.IsMenuCurrent(menuID, inme) -} - -func (p *pageMenus) Menus() navigation.PageMenus { - // There is a reverse dependency here. initMenus will, once, build the - // site menus and update any relevant page. - p.p.s.init.menus.Do(context.Background()) - - return p.menus() -} - -func (p *pageMenus) menus() navigation.PageMenus { - p.init() - return p.pm -} - -func (p *pageMenus) init() { - p.pmInit.Do(func() { - p.q = navigation.NewMenuQueryProvider( - p, - p.p.s, - p.p, - ) - - params := p.p.Params() - - var menus any - var ok bool - - if p.p.m.pageConfig.Menus != nil { - menus = p.p.m.pageConfig.Menus - } else { - menus, ok = params["menus"] - if !ok { - menus = params["menu"] - } - } - - var err error - p.pm, err = navigation.PageMenusFromPage(menus, p.p) - if err != nil { - p.p.s.Log.Errorln(p.p.wrapError(err)) - } - }) -} diff --git a/hugolib/page__meta.go b/hugolib/page__meta.go deleted file mode 100644 index 1af489f18..000000000 --- a/hugolib/page__meta.go +++ /dev/null @@ -1,948 +0,0 @@ -// 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 hugolib - -import ( - "context" - "fmt" - "path/filepath" - "regexp" - "strings" - "time" - - "github.com/bep/logg" - "github.com/gobuffalo/flect" - "github.com/gohugoio/hugo/langs" - "github.com/gohugoio/hugo/markup/converter" - xmaps "golang.org/x/exp/maps" - - "github.com/gohugoio/hugo/source" - - "github.com/gohugoio/hugo/common/hashing" - "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/config" - "github.com/gohugoio/hugo/helpers" - - "github.com/gohugoio/hugo/output" - "github.com/gohugoio/hugo/resources/kinds" - "github.com/gohugoio/hugo/resources/page" - "github.com/gohugoio/hugo/resources/page/pagemeta" - "github.com/gohugoio/hugo/resources/resource" - "github.com/spf13/cast" -) - -var cjkRe = regexp.MustCompile(`\p{Han}|\p{Hangul}|\p{Hiragana}|\p{Katakana}`) - -type pageMeta struct { - term string // Set for kind == KindTerm. - singular string // Set for kind == KindTerm and kind == KindTaxonomy. - - resource.Staler - *pageMetaParams - - // Set for standalone pages, e.g. robotsTXT. - standaloneOutputFormat output.Format - - resourcePath string // Set for bundled pages; path relative to its bundle root. - bundled bool // Set if this page is bundled inside another. - - pathInfo *paths.Path // Always set. This the canonical path to the Page. - f *source.File - - content *cachedContent // The source and the parsed page content. - - s *Site // The site this page belongs to. -} - -// Prepare for a rebuild of the data passed in from front matter. -func (m *pageMeta) setMetaPostPrepareRebuild() { - params := xmaps.Clone(m.paramsOriginal) - m.pageMetaParams.pageConfig = pagemeta.ClonePageConfigForRebuild(m.pageMetaParams.pageConfig, params) -} - -type pageMetaParams struct { - setMetaPostCount int - setMetaPostCascadeChanged bool - - pageConfig *pagemeta.PageConfig - - // These are only set in watch mode. - datesOriginal pagemeta.Dates - paramsOriginal map[string]any // contains the original params as defined in the front matter. - cascadeOriginal *maps.Ordered[page.PageMatcher, page.PageMatcherParamsConfig] // contains the original cascade as defined in the front matter. -} - -func (m *pageMetaParams) init(preserveOriginal bool) { - if preserveOriginal { - if m.pageConfig.IsFromContentAdapter { - m.paramsOriginal = xmaps.Clone(m.pageConfig.ContentAdapterData) - } else { - m.paramsOriginal = xmaps.Clone(m.pageConfig.Params) - } - m.cascadeOriginal = m.pageConfig.CascadeCompiled.Clone() - } -} - -func (p *pageMeta) Aliases() []string { - return p.pageConfig.Aliases -} - -func (p *pageMeta) BundleType() string { - switch p.pathInfo.Type() { - case paths.TypeLeaf: - return "leaf" - case paths.TypeBranch: - return "branch" - default: - return "" - } -} - -func (p *pageMeta) Date() time.Time { - return p.pageConfig.Dates.Date -} - -func (p *pageMeta) PublishDate() time.Time { - return p.pageConfig.Dates.PublishDate -} - -func (p *pageMeta) Lastmod() time.Time { - return p.pageConfig.Dates.Lastmod -} - -func (p *pageMeta) ExpiryDate() time.Time { - return p.pageConfig.Dates.ExpiryDate -} - -func (p *pageMeta) Description() string { - return p.pageConfig.Description -} - -func (p *pageMeta) Lang() string { - return p.s.Lang() -} - -func (p *pageMeta) Draft() bool { - return p.pageConfig.Draft -} - -func (p *pageMeta) File() *source.File { - return p.f -} - -func (p *pageMeta) IsHome() bool { - return p.Kind() == kinds.KindHome -} - -func (p *pageMeta) Keywords() []string { - return p.pageConfig.Keywords -} - -func (p *pageMeta) Kind() string { - return p.pageConfig.Kind -} - -func (p *pageMeta) Layout() string { - return p.pageConfig.Layout -} - -func (p *pageMeta) LinkTitle() string { - if p.pageConfig.LinkTitle != "" { - return p.pageConfig.LinkTitle - } - - return p.Title() -} - -func (p *pageMeta) Name() string { - if p.resourcePath != "" { - return p.resourcePath - } - if p.pageConfig.Kind == kinds.KindTerm { - return p.pathInfo.Unnormalized().BaseNameNoIdentifier() - } - return p.Title() -} - -func (p *pageMeta) IsNode() bool { - return !p.IsPage() -} - -func (p *pageMeta) IsPage() bool { - return p.Kind() == kinds.KindPage -} - -// Param is a convenience method to do lookups in Page's and Site's Params map, -// in that order. -// -// This method is also implemented on SiteInfo. -// TODO(bep) interface -func (p *pageMeta) Param(key any) (any, error) { - return resource.Param(p, p.s.Params(), key) -} - -func (p *pageMeta) Params() maps.Params { - return p.pageConfig.Params -} - -func (p *pageMeta) Path() string { - return p.pathInfo.Base() -} - -func (p *pageMeta) PathInfo() *paths.Path { - return p.pathInfo -} - -func (p *pageMeta) IsSection() bool { - return p.Kind() == kinds.KindSection -} - -func (p *pageMeta) Section() string { - return p.pathInfo.Section() -} - -func (p *pageMeta) Sitemap() config.SitemapConfig { - return p.pageConfig.Sitemap -} - -func (p *pageMeta) Title() string { - return p.pageConfig.Title -} - -const defaultContentType = "page" - -func (p *pageMeta) Type() string { - if p.pageConfig.Type != "" { - return p.pageConfig.Type - } - - if sect := p.Section(); sect != "" { - return sect - } - - return defaultContentType -} - -func (p *pageMeta) Weight() int { - return p.pageConfig.Weight -} - -func (p *pageMeta) setMetaPre(pi *contentParseInfo, logger loggers.Logger, conf config.AllProvider) error { - frontmatter := pi.frontMatter - - if frontmatter != nil { - pcfg := p.pageConfig - // Needed for case insensitive fetching of params values - maps.PrepareParams(frontmatter) - pcfg.Params = frontmatter - // Check for any cascade define on itself. - if cv, found := frontmatter["cascade"]; found { - var err error - cascade, err := page.DecodeCascade(logger, true, cv) - if err != nil { - return err - } - pcfg.CascadeCompiled = cascade - } - - // Look for path, lang and kind, all of which values we need early on. - if v, found := frontmatter["path"]; found { - pcfg.Path = paths.ToSlashPreserveLeading(cast.ToString(v)) - pcfg.Params["path"] = pcfg.Path - } - if v, found := frontmatter["lang"]; found { - lang := strings.ToLower(cast.ToString(v)) - if _, ok := conf.PathParser().LanguageIndex[lang]; ok { - pcfg.Lang = lang - pcfg.Params["lang"] = pcfg.Lang - } - } - if v, found := frontmatter["kind"]; found { - s := cast.ToString(v) - if s != "" { - pcfg.Kind = kinds.GetKindMain(s) - if pcfg.Kind == "" { - return fmt.Errorf("unknown kind %q in front matter", s) - } - pcfg.Params["kind"] = pcfg.Kind - } - } - } else if p.pageMetaParams.pageConfig.Params == nil { - p.pageConfig.Params = make(maps.Params) - } - - p.pageMetaParams.init(conf.Watching()) - - return nil -} - -func (ps *pageState) setMetaPost(cascade *maps.Ordered[page.PageMatcher, page.PageMatcherParamsConfig]) error { - ps.m.setMetaPostCount++ - var cascadeHashPre uint64 - if ps.m.setMetaPostCount > 1 { - cascadeHashPre = hashing.HashUint64(ps.m.pageConfig.CascadeCompiled) - ps.m.pageConfig.CascadeCompiled = ps.m.cascadeOriginal.Clone() - - } - - // Apply cascades first so they can be overridden later. - if cascade != nil { - if ps.m.pageConfig.CascadeCompiled != nil { - cascade.Range(func(k page.PageMatcher, v page.PageMatcherParamsConfig) bool { - vv, found := ps.m.pageConfig.CascadeCompiled.Get(k) - if !found { - ps.m.pageConfig.CascadeCompiled.Set(k, v) - } else { - // Merge - for ck, cv := range v.Params { - if _, found := vv.Params[ck]; !found { - vv.Params[ck] = cv - } - } - for ck, cv := range v.Fields { - if _, found := vv.Fields[ck]; !found { - vv.Fields[ck] = cv - } - } - } - return true - }) - cascade = ps.m.pageConfig.CascadeCompiled - } else { - ps.m.pageConfig.CascadeCompiled = cascade - } - } - - if cascade == nil { - cascade = ps.m.pageConfig.CascadeCompiled - } - - if ps.m.setMetaPostCount > 1 { - ps.m.setMetaPostCascadeChanged = cascadeHashPre != hashing.HashUint64(ps.m.pageConfig.CascadeCompiled) - if !ps.m.setMetaPostCascadeChanged { - - // No changes, restore any value that may be changed by aggregation. - ps.m.pageConfig.Dates = ps.m.datesOriginal - return nil - } - ps.m.setMetaPostPrepareRebuild() - - } - - // Cascade is also applied to itself. - var err error - cascade.Range(func(k page.PageMatcher, v page.PageMatcherParamsConfig) bool { - if !k.Matches(ps) { - return true - } - for kk, vv := range v.Params { - if _, found := ps.m.pageConfig.Params[kk]; !found { - ps.m.pageConfig.Params[kk] = vv - } - } - - for kk, vv := range v.Fields { - if ps.m.pageConfig.IsFromContentAdapter { - if _, found := ps.m.pageConfig.ContentAdapterData[kk]; !found { - ps.m.pageConfig.ContentAdapterData[kk] = vv - } - } else { - if _, found := ps.m.pageConfig.Params[kk]; !found { - ps.m.pageConfig.Params[kk] = vv - } - } - } - return true - }) - - if err != nil { - return err - } - - if err := ps.setMetaPostParams(); err != nil { - return err - } - - if err := ps.m.applyDefaultValues(); err != nil { - return err - } - - // Store away any original values that may be changed from aggregation. - ps.m.datesOriginal = ps.m.pageConfig.Dates - - return nil -} - -func (p *pageState) setMetaPostParams() error { - pm := p.m - var mtime time.Time - var contentBaseName string - var ext string - var isContentAdapter bool - if p.File() != nil { - isContentAdapter = p.File().IsContentAdapter() - contentBaseName = p.File().ContentBaseName() - if p.File().FileInfo() != nil { - mtime = p.File().FileInfo().ModTime() - } - if !isContentAdapter { - ext = p.File().Ext() - } - } - - var gitAuthorDate time.Time - if !p.gitInfo.IsZero() { - gitAuthorDate = p.gitInfo.AuthorDate - } - - descriptor := &pagemeta.FrontMatterDescriptor{ - PageConfig: pm.pageConfig, - BaseFilename: contentBaseName, - ModTime: mtime, - GitAuthorDate: gitAuthorDate, - Location: langs.GetLocation(pm.s.Language()), - PathOrTitle: p.pathOrTitle(), - } - - if isContentAdapter { - if err := pm.pageConfig.Compile(ext, p.s.Log, p.s.conf.OutputFormats.Config, p.s.conf.MediaTypes.Config); err != nil { - return err - } - } - - // Handle the date separately - // TODO(bep) we need to "do more" in this area so this can be split up and - // more easily tested without the Page, but the coupling is strong. - err := pm.s.frontmatterHandler.HandleDates(descriptor) - if err != nil { - p.s.Log.Errorf("Failed to handle dates for page %q: %s", p.pathOrTitle(), err) - } - - if isContentAdapter { - // Done. - return nil - } - - var buildConfig any - var isNewBuildKeyword bool - if v, ok := pm.pageConfig.Params["_build"]; ok { - hugo.Deprecate("The \"_build\" front matter key", "Use \"build\" instead. See https://gohugo.io/content-management/build-options.", "0.145.0") - buildConfig = v - } else { - buildConfig = pm.pageConfig.Params["build"] - isNewBuildKeyword = true - } - pm.pageConfig.Build, err = pagemeta.DecodeBuildConfig(buildConfig) - if err != nil { - var msgDetail string - if isNewBuildKeyword { - msgDetail = `. We renamed the _build keyword to build in Hugo 0.123.0. We recommend putting user defined params in the params section, e.g.: ---- -title: "My Title" -params: - build: "My Build" ---- -´ - -` - } - return fmt.Errorf("failed to decode build config in front matter: %s%s", err, msgDetail) - } - - var sitemapSet bool - - pcfg := pm.pageConfig - params := pcfg.Params - if params == nil { - panic("params not set for " + p.Title()) - } - - var draft, published, isCJKLanguage *bool - var userParams map[string]any - for k, v := range pcfg.Params { - loki := strings.ToLower(k) - - if loki == "params" { - vv, err := maps.ToStringMapE(v) - if err != nil { - return err - } - userParams = vv - delete(pcfg.Params, k) - continue - } - - if loki == "published" { // Intentionally undocumented - vv, err := cast.ToBoolE(v) - if err == nil { - published = &vv - } - // published may also be a date - continue - } - - if pm.s.frontmatterHandler.IsDateKey(loki) { - continue - } - - if loki == "path" || loki == "kind" || loki == "lang" { - // See issue 12484. - hugo.DeprecateLevelMin(loki+" in front matter", "", "v0.144.0", logg.LevelWarn) - } - - switch loki { - case "title": - pcfg.Title = cast.ToString(v) - params[loki] = pcfg.Title - case "linktitle": - pcfg.LinkTitle = cast.ToString(v) - params[loki] = pcfg.LinkTitle - case "summary": - pcfg.Summary = cast.ToString(v) - params[loki] = pcfg.Summary - case "description": - pcfg.Description = cast.ToString(v) - params[loki] = pcfg.Description - case "slug": - // Don't start or end with a - - pcfg.Slug = strings.Trim(cast.ToString(v), "-") - params[loki] = pm.Slug() - case "url": - url := cast.ToString(v) - if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") { - return fmt.Errorf("URLs with protocol (http*) not supported: %q. In page %q", url, p.pathOrTitle()) - } - pcfg.URL = url - params[loki] = url - case "type": - pcfg.Type = cast.ToString(v) - params[loki] = pcfg.Type - case "keywords": - pcfg.Keywords = cast.ToStringSlice(v) - params[loki] = pcfg.Keywords - case "headless": - // Legacy setting for leaf bundles. - // This is since Hugo 0.63 handled in a more general way for all - // pages. - isHeadless := cast.ToBool(v) - params[loki] = isHeadless - if isHeadless { - pm.pageConfig.Build.List = pagemeta.Never - pm.pageConfig.Build.Render = pagemeta.Never - } - case "outputs": - o := cast.ToStringSlice(v) - // lower case names: - for i, s := range o { - o[i] = strings.ToLower(s) - } - pm.pageConfig.Outputs = o - case "draft": - draft = new(bool) - *draft = cast.ToBool(v) - case "layout": - pcfg.Layout = cast.ToString(v) - params[loki] = pcfg.Layout - case "markup": - pcfg.Content.Markup = cast.ToString(v) - params[loki] = pcfg.Content.Markup - case "weight": - pcfg.Weight = cast.ToInt(v) - params[loki] = pcfg.Weight - case "aliases": - pcfg.Aliases = cast.ToStringSlice(v) - for i, alias := range pcfg.Aliases { - if strings.HasPrefix(alias, "http://") || strings.HasPrefix(alias, "https://") { - return fmt.Errorf("http* aliases not supported: %q", alias) - } - pcfg.Aliases[i] = filepath.ToSlash(alias) - } - params[loki] = pcfg.Aliases - case "sitemap": - pcfg.Sitemap, err = config.DecodeSitemap(p.s.conf.Sitemap, maps.ToStringMap(v)) - if err != nil { - return fmt.Errorf("failed to decode sitemap config in front matter: %s", err) - } - sitemapSet = true - case "iscjklanguage": - isCJKLanguage = new(bool) - *isCJKLanguage = cast.ToBool(v) - case "translationkey": - pcfg.TranslationKey = cast.ToString(v) - params[loki] = pcfg.TranslationKey - case "resources": - var resources []map[string]any - handled := true - - switch vv := v.(type) { - case []map[any]any: - for _, vvv := range vv { - resources = append(resources, maps.ToStringMap(vvv)) - } - case []map[string]any: - resources = append(resources, vv...) - case []any: - for _, vvv := range vv { - switch vvvv := vvv.(type) { - case map[any]any: - resources = append(resources, maps.ToStringMap(vvvv)) - case map[string]any: - resources = append(resources, vvvv) - } - } - default: - handled = false - } - - if handled { - pcfg.ResourcesMeta = resources - break - } - fallthrough - default: - // If not one of the explicit values, store in Params - switch vv := v.(type) { - case []any: - if len(vv) > 0 { - allStrings := true - for _, vvv := range vv { - if _, ok := vvv.(string); !ok { - allStrings = false - break - } - } - if allStrings { - // We need tags, keywords etc. to be []string, not []interface{}. - a := make([]string, len(vv)) - for i, u := range vv { - a[i] = cast.ToString(u) - } - params[loki] = a - } else { - params[loki] = vv - } - } else { - params[loki] = []string{} - } - - default: - params[loki] = vv - } - } - } - - for k, v := range userParams { - params[strings.ToLower(k)] = v - } - - if !sitemapSet { - pcfg.Sitemap = p.s.conf.Sitemap - } - - if draft != nil && published != nil { - pcfg.Draft = *draft - p.m.s.Log.Warnf("page %q has both draft and published settings in its frontmatter. Using draft.", p.File().Filename()) - } else if draft != nil { - pcfg.Draft = *draft - } else if published != nil { - pcfg.Draft = !*published - } - params["draft"] = pcfg.Draft - - if isCJKLanguage != nil { - pcfg.IsCJKLanguage = *isCJKLanguage - } else if p.s.conf.HasCJKLanguage && p.m.content.pi.openSource != nil { - if cjkRe.Match(p.m.content.mustSource()) { - pcfg.IsCJKLanguage = true - } else { - pcfg.IsCJKLanguage = false - } - } - - params["iscjklanguage"] = pcfg.IsCJKLanguage - - if err := pcfg.Validate(false); err != nil { - return err - } - - if err := pcfg.Compile(ext, p.s.Log, p.s.conf.OutputFormats.Config, p.s.conf.MediaTypes.Config); err != nil { - return err - } - - return nil -} - -// shouldList returns whether this page should be included in the list of pages. -// global indicates site.Pages etc. -func (p *pageMeta) shouldList(global bool) bool { - if p.isStandalone() { - // Never list 404, sitemap and similar. - return false - } - - switch p.pageConfig.Build.List { - case pagemeta.Always: - return true - case pagemeta.Never: - return false - case pagemeta.ListLocally: - return !global - } - return false -} - -func (p *pageMeta) shouldListAny() bool { - return p.shouldList(true) || p.shouldList(false) -} - -func (p *pageMeta) isStandalone() bool { - return !p.standaloneOutputFormat.IsZero() -} - -func (p *pageMeta) shouldBeCheckedForMenuDefinitions() bool { - if !p.shouldList(false) { - return false - } - - return p.pageConfig.Kind == kinds.KindHome || p.pageConfig.Kind == kinds.KindSection || p.pageConfig.Kind == kinds.KindPage -} - -func (p *pageMeta) noRender() bool { - return p.pageConfig.Build.Render != pagemeta.Always -} - -func (p *pageMeta) noLink() bool { - return p.pageConfig.Build.Render == pagemeta.Never -} - -func (p *pageMeta) applyDefaultValues() error { - if p.pageConfig.Build.IsZero() { - p.pageConfig.Build, _ = pagemeta.DecodeBuildConfig(nil) - } - - if !p.s.conf.IsKindEnabled(p.Kind()) { - (&p.pageConfig.Build).Disable() - } - - if p.pageConfig.Content.Markup == "" { - if p.File() != nil { - // Fall back to file extension - p.pageConfig.Content.Markup = p.s.ContentSpec.ResolveMarkup(p.File().Ext()) - } - if p.pageConfig.Content.Markup == "" { - p.pageConfig.Content.Markup = "markdown" - } - } - - if p.pageConfig.Title == "" && p.f == nil { - switch p.Kind() { - case kinds.KindHome: - p.pageConfig.Title = p.s.Title() - case kinds.KindSection: - sectionName := p.pathInfo.Unnormalized().BaseNameNoIdentifier() - if p.s.conf.PluralizeListTitles { - sectionName = flect.Pluralize(sectionName) - } - if p.s.conf.CapitalizeListTitles { - sectionName = p.s.conf.C.CreateTitle(sectionName) - } - p.pageConfig.Title = sectionName - case kinds.KindTerm: - if p.term != "" { - if p.s.conf.CapitalizeListTitles { - p.pageConfig.Title = p.s.conf.C.CreateTitle(p.term) - } else { - p.pageConfig.Title = p.term - } - } else { - panic("term not set") - } - case kinds.KindTaxonomy: - if p.s.conf.CapitalizeListTitles { - p.pageConfig.Title = strings.Replace(p.s.conf.C.CreateTitle(p.pathInfo.Unnormalized().BaseNameNoIdentifier()), "-", " ", -1) - } else { - p.pageConfig.Title = strings.Replace(p.pathInfo.Unnormalized().BaseNameNoIdentifier(), "-", " ", -1) - } - case kinds.KindStatus404: - p.pageConfig.Title = "404 Page not found" - } - } - - return nil -} - -func (p *pageMeta) newContentConverter(ps *pageState, markup string) (converter.Converter, error) { - if ps == nil { - panic("no Page provided") - } - cp := p.s.ContentSpec.Converters.Get(markup) - if cp == nil { - return converter.NopConverter, fmt.Errorf("no content renderer found for markup %q, page: %s", markup, ps.getPageInfoForError()) - } - - var id string - var filename string - var path string - if p.f != nil { - id = p.f.UniqueID() - filename = p.f.Filename() - path = p.f.Path() - } else { - path = p.Path() - } - - doc := newPageForRenderHook(ps) - - documentLookup := func(id uint64) any { - if id == ps.pid { - // This prevents infinite recursion in some cases. - return doc - } - if v, ok := ps.pageOutput.pco.otherOutputs.Get(id); ok { - return v.po.p - } - return nil - } - - cpp, err := cp.New( - converter.DocumentContext{ - Document: doc, - DocumentLookup: documentLookup, - DocumentID: id, - DocumentName: path, - Filename: filename, - }, - ) - if err != nil { - return converter.NopConverter, err - } - - return cpp, nil -} - -// The output formats this page will be rendered to. -func (m *pageMeta) outputFormats() output.Formats { - if len(m.pageConfig.ConfiguredOutputFormats) > 0 { - return m.pageConfig.ConfiguredOutputFormats - } - return m.s.conf.C.KindOutputFormats[m.Kind()] -} - -func (p *pageMeta) Slug() string { - return p.pageConfig.Slug -} - -func getParam(m resource.ResourceParamsProvider, key string, stringToLower bool) any { - v := m.Params()[strings.ToLower(key)] - - if v == nil { - return nil - } - - switch val := v.(type) { - case bool: - return val - case string: - if stringToLower { - return strings.ToLower(val) - } - return val - case int64, int32, int16, int8, int: - return cast.ToInt(v) - case float64, float32: - return cast.ToFloat64(v) - case time.Time: - return val - case []string: - if stringToLower { - return helpers.SliceToLower(val) - } - return v - default: - return v - } -} - -func getParamToLower(m resource.ResourceParamsProvider, key string) any { - return getParam(m, key, true) -} - -func (ps *pageState) initLazyProviders() error { - ps.init.Add(func(ctx context.Context) (any, error) { - pp, err := newPagePaths(ps) - if err != nil { - return nil, err - } - - var outputFormatsForPage output.Formats - var renderFormats output.Formats - - if ps.m.standaloneOutputFormat.IsZero() { - outputFormatsForPage = ps.m.outputFormats() - renderFormats = ps.s.h.renderFormats - } else { - // One of the fixed output format pages, e.g. 404. - outputFormatsForPage = output.Formats{ps.m.standaloneOutputFormat} - renderFormats = outputFormatsForPage - } - - // Prepare output formats for all sites. - // We do this even if this page does not get rendered on - // its own. It may be referenced via one of the site collections etc. - // it will then need an output format. - ps.pageOutputs = make([]*pageOutput, len(renderFormats)) - created := make(map[string]*pageOutput) - shouldRenderPage := !ps.m.noRender() - - for i, f := range renderFormats { - - if po, found := created[f.Name]; found { - ps.pageOutputs[i] = po - continue - } - - render := shouldRenderPage - if render { - _, render = outputFormatsForPage.GetByName(f.Name) - } - - po := newPageOutput(ps, pp, f, render) - - // Create a content provider for the first, - // we may be able to reuse it. - if i == 0 { - contentProvider, err := newPageContentOutput(po) - if err != nil { - return nil, err - } - po.setContentProvider(contentProvider) - } - - ps.pageOutputs[i] = po - created[f.Name] = po - - } - - if err := ps.initCommonProviders(pp); err != nil { - return nil, err - } - - return nil, nil - }) - - return nil -} diff --git a/hugolib/page__meta_test.go b/hugolib/page__meta_test.go deleted file mode 100644 index 58d29464d..000000000 --- a/hugolib/page__meta_test.go +++ /dev/null @@ -1,82 +0,0 @@ -// 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 hugolib - -import ( - "strings" - "testing" -) - -// Issue 9793 -// Issue 12115 -func TestListTitles(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ['home','rss','sitemap'] -capitalizeListTitles = true -pluralizeListTitles = true -[taxonomies] -tag = 'tags' --- content/section-1/page-1.md -- ---- -title: page-1 -tags: 'tag-a' ---- --- layouts/_default/list.html -- -{{ .Title }} --- layouts/_default/single.html -- -{{ .Title }} - ` - - b := Test(t, files) - - b.AssertFileContent("public/section-1/index.html", "Section-1s") - b.AssertFileContent("public/tags/index.html", "Tags") - b.AssertFileContent("public/tags/tag-a/index.html", "Tag-A") - - files = strings.Replace(files, "true", "false", -1) - - b = Test(t, files) - - b.AssertFileContent("public/section-1/index.html", "section-1") - b.AssertFileContent("public/tags/index.html", "tags") - b.AssertFileContent("public/tags/tag-a/index.html", "tag-a") -} - -func TestDraftNonDefaultContentLanguage(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -defaultContentLanguage = "en" -[languages] -[languages.en] -weight = 1 -[languages.nn] -weight = 2 --- content/p1.md -- --- content/p2.nn.md -- ---- -title: "p2" -draft: true ---- --- layouts/_default/single.html -- -{{ .Title }} -` - b := Test(t, files) - - b.AssertFileExists("public/nn/p2/index.html", false) -} diff --git a/hugolib/page__new.go b/hugolib/page__new.go deleted file mode 100644 index 80115cc72..000000000 --- a/hugolib/page__new.go +++ /dev/null @@ -1,265 +0,0 @@ -// Copyright 2019 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 hugolib - -import ( - "fmt" - "strings" - "sync/atomic" - - "github.com/gohugoio/hugo/hugofs/files" - "github.com/gohugoio/hugo/resources" - - "github.com/gohugoio/hugo/common/constants" - "github.com/gohugoio/hugo/common/maps" - "github.com/gohugoio/hugo/common/paths" - - "github.com/gohugoio/hugo/lazy" - - "github.com/gohugoio/hugo/resources/kinds" - "github.com/gohugoio/hugo/resources/page" - "github.com/gohugoio/hugo/resources/page/pagemeta" -) - -var pageIDCounter atomic.Uint64 - -func (h *HugoSites) newPage(m *pageMeta) (*pageState, *paths.Path, error) { - p, pth, err := h.doNewPage(m) - if err != nil { - // Make sure that any partially created page part is marked as stale. - m.MarkStale() - } - - if p != nil && pth != nil && p.IsHome() && pth.IsLeafBundle() { - msg := "Using %s in your content's root directory is usually incorrect for your home page. " - msg += "You should use %s instead. If you don't rename this file, your home page will be " - msg += "treated as a leaf bundle, meaning it won't be able to have any child pages or sections." - h.Log.Warnidf(constants.WarnHomePageIsLeafBundle, msg, pth.PathNoLeadingSlash(), strings.ReplaceAll(pth.PathNoLeadingSlash(), "index", "_index")) - } - - return p, pth, err -} - -func (h *HugoSites) doNewPage(m *pageMeta) (*pageState, *paths.Path, error) { - m.Staler = &resources.AtomicStaler{} - if m.pageMetaParams == nil { - m.pageMetaParams = &pageMetaParams{ - pageConfig: &pagemeta.PageConfig{}, - } - } - if m.pageConfig.Params == nil { - m.pageConfig.Params = maps.Params{} - } - - pid := pageIDCounter.Add(1) - pi, err := m.parseFrontMatter(h, pid) - if err != nil { - return nil, nil, err - } - - if err := m.setMetaPre(pi, h.Log, h.Conf); err != nil { - return nil, nil, m.wrapError(err, h.BaseFs.SourceFs) - } - pcfg := m.pageConfig - if pcfg.Lang != "" { - if h.Conf.IsLangDisabled(pcfg.Lang) { - return nil, nil, nil - } - } - - if pcfg.Path != "" { - s := m.pageConfig.Path - // Paths from content adapters should never have any extension. - if pcfg.IsFromContentAdapter || !paths.HasExt(s) { - var ( - isBranch bool - isBranchSet bool - ext string = m.pageConfig.ContentMediaType.FirstSuffix.Suffix - ) - if pcfg.Kind != "" { - isBranch = kinds.IsBranch(pcfg.Kind) - isBranchSet = true - } - - if !pcfg.IsFromContentAdapter { - if m.pathInfo != nil { - if !isBranchSet { - isBranch = m.pathInfo.IsBranchBundle() - } - if m.pathInfo.Ext() != "" { - ext = m.pathInfo.Ext() - } - } else if m.f != nil { - pi := m.f.FileInfo().Meta().PathInfo - if !isBranchSet { - isBranch = pi.IsBranchBundle() - } - if pi.Ext() != "" { - ext = pi.Ext() - } - } - } - - if isBranch { - s += "/_index." + ext - } else { - s += "/index." + ext - } - - } - m.pathInfo = h.Conf.PathParser().Parse(files.ComponentFolderContent, s) - } else if m.pathInfo == nil { - if m.f != nil { - m.pathInfo = m.f.FileInfo().Meta().PathInfo - } - - if m.pathInfo == nil { - panic(fmt.Sprintf("missing pathInfo in %v", m)) - } - } - - ps, err := func() (*pageState, error) { - if m.s == nil { - // Identify the Site/language to associate this Page with. - var lang string - if pcfg.Lang != "" { - lang = pcfg.Lang - } else if m.f != nil { - meta := m.f.FileInfo().Meta() - lang = meta.Lang - } else { - lang = m.pathInfo.Lang() - } - - m.s = h.resolveSite(lang) - - if m.s == nil { - return nil, fmt.Errorf("no site found for language %q", lang) - } - } - - var tc viewName - // Identify Page Kind. - if m.pageConfig.Kind == "" { - m.pageConfig.Kind = kinds.KindSection - if m.pathInfo.Base() == "/" { - m.pageConfig.Kind = kinds.KindHome - } else if m.pathInfo.IsBranchBundle() { - // A section, taxonomy or term. - tc = m.s.pageMap.cfg.getTaxonomyConfig(m.Path()) - if !tc.IsZero() { - // Either a taxonomy or a term. - if tc.pluralTreeKey == m.Path() { - m.pageConfig.Kind = kinds.KindTaxonomy - } else { - m.pageConfig.Kind = kinds.KindTerm - } - } - } else if m.f != nil { - m.pageConfig.Kind = kinds.KindPage - } - } - - if m.pageConfig.Kind == kinds.KindTerm || m.pageConfig.Kind == kinds.KindTaxonomy { - if tc.IsZero() { - tc = m.s.pageMap.cfg.getTaxonomyConfig(m.Path()) - } - if tc.IsZero() { - return nil, fmt.Errorf("no taxonomy configuration found for %q", m.Path()) - } - m.singular = tc.singular - if m.pageConfig.Kind == kinds.KindTerm { - m.term = paths.TrimLeading(strings.TrimPrefix(m.pathInfo.Unnormalized().Base(), tc.pluralTreeKey)) - } - } - - if m.pageConfig.Kind == kinds.KindPage && !m.s.conf.IsKindEnabled(m.pageConfig.Kind) { - return nil, nil - } - - // Parse the rest of the page content. - m.content, err = m.newCachedContent(h, pi) - if err != nil { - return nil, m.wrapError(err, h.SourceFs) - } - - ps := &pageState{ - pid: pid, - pageOutput: nopPageOutput, - pageOutputTemplateVariationsState: &atomic.Uint32{}, - Staler: m, - dependencyManager: m.s.Conf.NewIdentityManager(m.Path()), - pageCommon: &pageCommon{ - FileProvider: m, - store: maps.NewScratch(), - Positioner: page.NopPage, - InSectionPositioner: page.NopPage, - ResourceNameTitleProvider: m, - ResourceParamsProvider: m, - PageMetaProvider: m, - PageMetaInternalProvider: m, - OutputFormatsProvider: page.NopPage, - ResourceTypeProvider: pageTypesProvider, - MediaTypeProvider: pageTypesProvider, - RefProvider: page.NopPage, - ShortcodeInfoProvider: page.NopPage, - LanguageProvider: m.s, - - RelatedDocsHandlerProvider: m.s, - init: lazy.New(), - m: m, - s: m.s, - sWrapped: page.WrapSite(m.s), - }, - } - - if m.f != nil { - gi, err := m.s.h.gitInfoForPage(ps) - if err != nil { - return nil, fmt.Errorf("failed to load Git data: %w", err) - } - ps.gitInfo = gi - owners, err := m.s.h.codeownersForPage(ps) - if err != nil { - return nil, fmt.Errorf("failed to load CODEOWNERS: %w", err) - } - ps.codeowners = owners - } - - ps.pageMenus = &pageMenus{p: ps} - ps.PageMenusProvider = ps.pageMenus - ps.GetPageProvider = pageSiteAdapter{s: m.s, p: ps} - ps.GitInfoProvider = ps - ps.TranslationsProvider = ps - ps.ResourceDataProvider = &pageData{pageState: ps} - ps.RawContentProvider = ps - ps.ChildCareProvider = ps - ps.TreeProvider = pageTree{p: ps} - ps.Eqer = ps - ps.TranslationKeyProvider = ps - ps.ShortcodeInfoProvider = ps - ps.AlternativeOutputFormatsProvider = ps - - if err := ps.initLazyProviders(); err != nil { - return nil, ps.wrapError(err) - } - return ps, nil - }() - - if ps == nil { - return nil, nil, err - } - - return ps, ps.PathInfo(), err -} diff --git a/hugolib/page__output.go b/hugolib/page__output.go deleted file mode 100644 index b8086bb48..000000000 --- a/hugolib/page__output.go +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright 2019 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 hugolib - -import ( - "fmt" - - "github.com/gohugoio/hugo/identity" - "github.com/gohugoio/hugo/output" - "github.com/gohugoio/hugo/resources/page" - "github.com/gohugoio/hugo/resources/resource" -) - -func newPageOutput( - ps *pageState, - pp pagePaths, - f output.Format, - render bool, -) *pageOutput { - var targetPathsProvider targetPathsHolder - var linksProvider resource.ResourceLinksProvider - - ft, found := pp.targetPaths[f.Name] - if !found { - // Link to the main output format - ft = pp.targetPaths[pp.firstOutputFormat.Format.Name] - } - targetPathsProvider = ft - linksProvider = ft - - var paginatorProvider page.PaginatorProvider - var pag *pagePaginator - - if render && ps.IsNode() { - pag = newPagePaginator(ps) - paginatorProvider = pag - } else { - paginatorProvider = page.PaginatorNotSupportedFunc(func() error { - return fmt.Errorf("pagination not supported for this page: %s", ps.getPageInfoForError()) - }) - } - - providers := struct { - page.PaginatorProvider - resource.ResourceLinksProvider - targetPather - }{ - paginatorProvider, - linksProvider, - targetPathsProvider, - } - - po := &pageOutput{ - p: ps, - f: f, - pagePerOutputProviders: providers, - MarkupProvider: page.NopPage, - ContentProvider: page.NopPage, - PageRenderProvider: page.NopPage, - TableOfContentsProvider: page.NopPage, - render: render, - paginator: pag, - dependencyManagerOutput: ps.s.Conf.NewIdentityManager((ps.Path() + "/" + f.Name)), - } - - return po -} - -// We create a pageOutput for every output format combination, even if this -// particular page isn't configured to be rendered to that format. -type pageOutput struct { - p *pageState - - // Set if this page isn't configured to be rendered to this format. - render bool - - f output.Format - - // Only set if render is set. - // Note that this will be lazily initialized, so only used if actually - // used in template(s). - paginator *pagePaginator - - // These interface provides the functionality that is specific for this - // output format. - contentRenderer page.ContentRenderer - pagePerOutputProviders - page.MarkupProvider - page.ContentProvider - page.PageRenderProvider - page.TableOfContentsProvider - page.RenderShortcodesProvider - - // May be nil. - pco *pageContentOutput - - dependencyManagerOutput identity.Manager - - renderState int // Reset when it needs to be rendered again. - renderOnce bool // To make sure we at least try to render it once. -} - -func (po *pageOutput) incrRenderState() { - po.renderState++ - po.renderOnce = true -} - -// isRendered reports whether this output format or its content has been rendered. -func (po *pageOutput) isRendered() bool { - if po.renderState > 0 { - return true - } - if po.pco != nil && po.pco.contentRendered.Load() { - return true - } - return false -} - -func (po *pageOutput) IdentifierBase() string { - return po.p.f.Name -} - -func (po *pageOutput) GetDependencyManager() identity.Manager { - return po.dependencyManagerOutput -} - -func (p *pageOutput) setContentProvider(cp *pageContentOutput) { - if cp == nil { - return - } - p.contentRenderer = cp - p.ContentProvider = cp - p.MarkupProvider = cp - p.PageRenderProvider = cp - p.TableOfContentsProvider = cp - p.RenderShortcodesProvider = cp - p.pco = cp -} diff --git a/hugolib/page__paginator.go b/hugolib/page__paginator.go deleted file mode 100644 index b6a778a21..000000000 --- a/hugolib/page__paginator.go +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright 2019 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 hugolib - -import ( - "sync" - - "github.com/gohugoio/hugo/resources/kinds" - "github.com/gohugoio/hugo/resources/page" -) - -func newPagePaginator(source *pageState) *pagePaginator { - return &pagePaginator{ - source: source, - pagePaginatorInit: &pagePaginatorInit{}, - } -} - -type pagePaginator struct { - *pagePaginatorInit - source *pageState -} - -type pagePaginatorInit struct { - init sync.Once - current *page.Pager -} - -// reset resets the paginator to allow for a rebuild. -func (p *pagePaginator) reset() { - p.pagePaginatorInit = &pagePaginatorInit{} -} - -func (p *pagePaginator) Paginate(seq any, options ...any) (*page.Pager, error) { - var initErr error - p.init.Do(func() { - pagerSize, err := page.ResolvePagerSize(p.source.s.Conf, options...) - if err != nil { - initErr = err - return - } - - pd := p.source.targetPathDescriptor - pd.Type = p.source.outputFormat() - paginator, err := page.Paginate(pd, seq, pagerSize) - if err != nil { - initErr = err - return - } - - p.current = paginator.Pagers()[0] - }) - - if initErr != nil { - return nil, initErr - } - - return p.current, nil -} - -func (p *pagePaginator) Paginator(options ...any) (*page.Pager, error) { - var initErr error - p.init.Do(func() { - pagerSize, err := page.ResolvePagerSize(p.source.s.Conf, options...) - if err != nil { - initErr = err - return - } - - pd := p.source.targetPathDescriptor - pd.Type = p.source.outputFormat() - - var pages page.Pages - - switch p.source.Kind() { - case kinds.KindHome: - // From Hugo 0.57 we made home.Pages() work like any other - // section. To avoid the default paginators for the home page - // changing in the wild, we make this a special case. - pages = p.source.s.RegularPages() - case kinds.KindTerm, kinds.KindTaxonomy: - pages = p.source.Pages() - default: - pages = p.source.RegularPages() - } - - paginator, err := page.Paginate(pd, pages, pagerSize) - if err != nil { - initErr = err - return - } - - p.current = paginator.Pagers()[0] - }) - - if initErr != nil { - return nil, initErr - } - - return p.current, nil -} diff --git a/hugolib/page__paths.go b/hugolib/page__paths.go deleted file mode 100644 index 62206cb15..000000000 --- a/hugolib/page__paths.go +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright 2019 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 hugolib - -import ( - "net/url" - "strings" - - "github.com/gohugoio/hugo/output" - - "github.com/gohugoio/hugo/resources/kinds" - "github.com/gohugoio/hugo/resources/page" -) - -func newPagePaths(ps *pageState) (pagePaths, error) { - s := ps.s - pm := ps.m - - targetPathDescriptor, err := createTargetPathDescriptor(ps) - if err != nil { - return pagePaths{}, err - } - - var outputFormats output.Formats - - if ps.m.isStandalone() { - outputFormats = output.Formats{ps.m.standaloneOutputFormat} - } else { - outputFormats = pm.outputFormats() - if len(outputFormats) == 0 { - return pagePaths{}, nil - } - - if pm.noRender() { - outputFormats = outputFormats[:1] - } - } - - pageOutputFormats := make(page.OutputFormats, len(outputFormats)) - targets := make(map[string]targetPathsHolder) - - for i, f := range outputFormats { - desc := targetPathDescriptor - desc.Type = f - paths := page.CreateTargetPaths(desc) - - var relPermalink, permalink string - - // If a page is headless or bundled in another, - // it will not get published on its own and it will have no links. - // We also check the build options if it's set to not render or have - // a link. - if !pm.noLink() && !pm.bundled { - relPermalink = paths.RelPermalink(s.PathSpec) - permalink = paths.PermalinkForOutputFormat(s.PathSpec, f) - } - - pageOutputFormats[i] = page.NewOutputFormat(relPermalink, permalink, len(outputFormats) == 1, f) - - // Use the main format for permalinks, usually HTML. - permalinksIndex := 0 - if f.Permalinkable { - // Unless it's permalinkable. - permalinksIndex = i - } - - targets[f.Name] = targetPathsHolder{ - relURL: relPermalink, - paths: paths, - OutputFormat: pageOutputFormats[permalinksIndex], - } - - } - - var out page.OutputFormats - if !pm.noLink() { - out = pageOutputFormats - } - - return pagePaths{ - outputFormats: out, - firstOutputFormat: pageOutputFormats[0], - targetPaths: targets, - targetPathDescriptor: targetPathDescriptor, - }, nil -} - -type pagePaths struct { - outputFormats page.OutputFormats - firstOutputFormat page.OutputFormat - - targetPaths map[string]targetPathsHolder - targetPathDescriptor page.TargetPathDescriptor -} - -func (l pagePaths) OutputFormats() page.OutputFormats { - return l.outputFormats -} - -func createTargetPathDescriptor(p *pageState) (page.TargetPathDescriptor, error) { - s := p.s - d := s.Deps - pm := p.m - alwaysInSubDir := p.Kind() == kinds.KindSitemap - - pageInfoPage := p.PathInfo() - pageInfoCurrentSection := p.CurrentSection().PathInfo() - if p.s.Conf.DisablePathToLower() { - pageInfoPage = pageInfoPage.Unnormalized() - pageInfoCurrentSection = pageInfoCurrentSection.Unnormalized() - } - - desc := page.TargetPathDescriptor{ - PathSpec: d.PathSpec, - Kind: p.Kind(), - Path: pageInfoPage, - Section: pageInfoCurrentSection, - UglyURLs: s.h.Conf.IsUglyURLs(p.Section()), - ForcePrefix: s.h.Conf.IsMultihost() || alwaysInSubDir, - URL: pm.pageConfig.URL, - } - - if pm.Slug() != "" { - desc.BaseName = pm.Slug() - } else if pm.isStandalone() && pm.standaloneOutputFormat.BaseName != "" { - desc.BaseName = pm.standaloneOutputFormat.BaseName - } else { - desc.BaseName = pageInfoPage.BaseNameNoIdentifier() - } - - desc.PrefixFilePath = s.getLanguageTargetPathLang(alwaysInSubDir) - desc.PrefixLink = s.getLanguagePermalinkLang(alwaysInSubDir) - - if desc.URL != "" && strings.IndexByte(desc.URL, ':') >= 0 { - // Attempt to parse and expand an url - opath, err := d.ResourceSpec.Permalinks.ExpandPattern(desc.URL, p) - if err != nil { - return desc, err - } - - if opath != "" { - opath, _ = url.QueryUnescape(opath) - desc.URL = opath - } - } - - opath, err := d.ResourceSpec.Permalinks.Expand(p.Section(), p) - if err != nil { - return desc, err - } - - if opath != "" { - opath, _ = url.QueryUnescape(opath) - if strings.HasSuffix(opath, "//") { - // When rewriting the _index of the section the permalink config is applied to, - // we get double slashes at the end sometimes; clear them up here - opath = strings.TrimSuffix(opath, "/") - } - - desc.ExpandedPermalink = opath - - if p.File() != nil { - s.Log.Debugf("Set expanded permalink path for %s %s to %#v", p.Kind(), p.File().Path(), opath) - } else { - s.Log.Debugf("Set expanded permalink path for %s in %v to %#v", p.Kind(), desc.Section.Path(), opath) - } - } - - return desc, nil -} diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go deleted file mode 100644 index 1f7f3411e..000000000 --- a/hugolib/page__per_output.go +++ /dev/null @@ -1,493 +0,0 @@ -// Copyright 2019 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 hugolib - -import ( - "bytes" - "context" - "errors" - "fmt" - "html/template" - "sync" - "sync/atomic" - - "github.com/gohugoio/hugo/common/maps" - "github.com/gohugoio/hugo/common/text" - "github.com/gohugoio/hugo/identity" - "github.com/gohugoio/hugo/tpl/tplimpl" - "github.com/spf13/cast" - - "github.com/gohugoio/hugo/markup/converter/hooks" - "github.com/gohugoio/hugo/markup/tableofcontents" - - "github.com/gohugoio/hugo/markup/converter" - - bp "github.com/gohugoio/hugo/bufferpool" - - "github.com/gohugoio/hugo/output" - "github.com/gohugoio/hugo/resources/page" - "github.com/gohugoio/hugo/resources/resource" -) - -var ( - nopTargetPath = targetPathsHolder{} - nopPagePerOutput = struct { - resource.ResourceLinksProvider - page.ContentProvider - page.PageRenderProvider - page.PaginatorProvider - page.TableOfContentsProvider - page.AlternativeOutputFormatsProvider - - targetPather - }{ - page.NopPage, - page.NopPage, - page.NopPage, - page.NopPage, - page.NopPage, - page.NopPage, - nopTargetPath, - } -) - -func newPageContentOutput(po *pageOutput) (*pageContentOutput, error) { - cp := &pageContentOutput{ - po: po, - renderHooks: &renderHooks{}, - otherOutputs: maps.NewCache[uint64, *pageContentOutput](), - } - return cp, nil -} - -type renderHooks struct { - getRenderer hooks.GetRendererFunc - init sync.Once -} - -// pageContentOutput represents the Page content for a given output format. -type pageContentOutput struct { - po *pageOutput - - // Other pages involved in rendering of this page, - // typically included with .RenderShortcodes. - otherOutputs *maps.Cache[uint64, *pageContentOutput] - - contentRenderedVersion uint32 // Incremented on reset. - contentRendered atomic.Bool // Set on content render. - - // Renders Markdown hooks. - renderHooks *renderHooks -} - -func (pco *pageContentOutput) trackDependency(idp identity.IdentityProvider) { - pco.po.p.dependencyManagerOutput.AddIdentity(idp.GetIdentity()) -} - -func (pco *pageContentOutput) Reset() { - if pco == nil { - return - } - pco.contentRenderedVersion++ - pco.contentRendered.Store(false) - pco.renderHooks = &renderHooks{} -} - -func (pco *pageContentOutput) Render(ctx context.Context, layout ...string) (template.HTML, error) { - if len(layout) == 0 { - return "", errors.New("no layout given") - } - templ, found, err := pco.po.p.resolveTemplate(layout...) - if err != nil { - return "", pco.po.p.wrapError(err) - } - - if !found { - return "", nil - } - - // Make sure to send the *pageState and not the *pageContentOutput to the template. - res, err := executeToString(ctx, pco.po.p.s.GetTemplateStore(), templ, pco.po.p) - if err != nil { - return "", pco.po.p.wrapError(fmt.Errorf("failed to execute template %s: %w", templ.Name(), err)) - } - return template.HTML(res), nil -} - -func (pco *pageContentOutput) Fragments(ctx context.Context) *tableofcontents.Fragments { - return pco.c().Fragments(ctx) -} - -func (pco *pageContentOutput) RenderShortcodes(ctx context.Context) (template.HTML, error) { - return pco.c().RenderShortcodes(ctx) -} - -func (pco *pageContentOutput) Markup(opts ...any) page.Markup { - if len(opts) > 1 { - panic("too many arguments, expected 0 or 1") - } - var scope string - if len(opts) == 1 { - scope = cast.ToString(opts[0]) - } - return pco.po.p.m.content.getOrCreateScope(scope, pco) -} - -func (pco *pageContentOutput) c() page.Markup { - return pco.po.p.m.content.getOrCreateScope("", pco) -} - -func (pco *pageContentOutput) Content(ctx context.Context) (any, error) { - r, err := pco.c().Render(ctx) - if err != nil { - return nil, err - } - return r.Content(ctx) -} - -func (pco *pageContentOutput) ContentWithoutSummary(ctx context.Context) (template.HTML, error) { - r, err := pco.c().Render(ctx) - if err != nil { - return "", err - } - return r.ContentWithoutSummary(ctx) -} - -func (pco *pageContentOutput) TableOfContents(ctx context.Context) template.HTML { - return pco.c().(*cachedContentScope).fragmentsHTML(ctx) -} - -func (pco *pageContentOutput) Len(ctx context.Context) int { - return pco.mustRender(ctx).Len(ctx) -} - -func (pco *pageContentOutput) mustRender(ctx context.Context) page.Content { - c, err := pco.c().Render(ctx) - if err != nil { - pco.fail(err) - } - return c -} - -func (pco *pageContentOutput) fail(err error) { - pco.po.p.s.h.FatalError(pco.po.p.wrapError(err)) -} - -func (pco *pageContentOutput) Plain(ctx context.Context) string { - return pco.mustRender(ctx).Plain(ctx) -} - -func (pco *pageContentOutput) PlainWords(ctx context.Context) []string { - return pco.mustRender(ctx).PlainWords(ctx) -} - -func (pco *pageContentOutput) ReadingTime(ctx context.Context) int { - return pco.mustRender(ctx).ReadingTime(ctx) -} - -func (pco *pageContentOutput) WordCount(ctx context.Context) int { - return pco.mustRender(ctx).WordCount(ctx) -} - -func (pco *pageContentOutput) FuzzyWordCount(ctx context.Context) int { - return pco.mustRender(ctx).FuzzyWordCount(ctx) -} - -func (pco *pageContentOutput) Summary(ctx context.Context) template.HTML { - summary, err := pco.mustRender(ctx).Summary(ctx) - if err != nil { - pco.fail(err) - } - return summary.Text -} - -func (pco *pageContentOutput) Truncated(ctx context.Context) bool { - summary, err := pco.mustRender(ctx).Summary(ctx) - if err != nil { - pco.fail(err) - } - return summary.Truncated -} - -func (pco *pageContentOutput) RenderString(ctx context.Context, args ...any) (template.HTML, error) { - return pco.c().RenderString(ctx, args...) -} - -func (pco *pageContentOutput) initRenderHooks() error { - if pco == nil { - return nil - } - - pco.renderHooks.init.Do(func() { - if pco.po.p.pageOutputTemplateVariationsState.Load() == 0 { - pco.po.p.pageOutputTemplateVariationsState.Store(1) - } - - type cacheKey struct { - tp hooks.RendererType - id any - f output.Format - } - - renderCache := make(map[cacheKey]any) - var renderCacheMu sync.Mutex - - resolvePosition := func(ctx any) text.Position { - source := pco.po.p.m.content.mustSource() - var offset int - - switch v := ctx.(type) { - case hooks.PositionerSourceTargetProvider: - offset = bytes.Index(source, v.PositionerSourceTarget()) - } - - pos := pco.po.p.posFromInput(source, offset) - - if pos.LineNumber > 0 { - // Move up to the code fence delimiter. - // This is in line with how we report on shortcodes. - pos.LineNumber = pos.LineNumber - 1 - } - - return pos - } - - pco.renderHooks.getRenderer = func(tp hooks.RendererType, id any) any { - renderCacheMu.Lock() - defer renderCacheMu.Unlock() - - key := cacheKey{tp: tp, id: id, f: pco.po.f} - if r, ok := renderCache[key]; ok { - return r - } - - // Inherit the descriptor from the page/current output format. - // This allows for fine-grained control of the template used for - // rendering of e.g. links. - base, layoutDescriptor := pco.po.p.GetInternalTemplateBasePathAndDescriptor() - - switch tp { - case hooks.LinkRendererType: - layoutDescriptor.Variant1 = "link" - case hooks.ImageRendererType: - layoutDescriptor.Variant1 = "image" - case hooks.HeadingRendererType: - layoutDescriptor.Variant1 = "heading" - case hooks.PassthroughRendererType: - layoutDescriptor.Variant1 = "passthrough" - if id != nil { - layoutDescriptor.Variant2 = id.(string) - } - case hooks.BlockquoteRendererType: - layoutDescriptor.Variant1 = "blockquote" - if id != nil { - layoutDescriptor.Variant2 = id.(string) - } - case hooks.TableRendererType: - layoutDescriptor.Variant1 = "table" - case hooks.CodeBlockRendererType: - layoutDescriptor.Variant1 = "codeblock" - if id != nil { - layoutDescriptor.Variant2 = id.(string) - } - } - - renderHookConfig := pco.po.p.s.conf.Markup.Goldmark.RenderHooks - var ignoreInternal bool - switch layoutDescriptor.Variant1 { - case "link": - ignoreInternal = !renderHookConfig.Link.IsEnableDefault() - case "image": - ignoreInternal = !renderHookConfig.Image.IsEnableDefault() - } - - candidates := pco.po.p.s.renderFormats - var numCandidatesFound int - consider := func(candidate *tplimpl.TemplInfo) bool { - if layoutDescriptor.Variant1 != candidate.D.Variant1 { - return false - } - - if layoutDescriptor.Variant2 != "" && candidate.D.Variant2 != "" && layoutDescriptor.Variant2 != candidate.D.Variant2 { - return false - } - - if ignoreInternal && candidate.SubCategory() == tplimpl.SubCategoryEmbedded { - // Don't consider the internal hook templates. - return false - } - - if pco.po.p.pageOutputTemplateVariationsState.Load() > 1 { - return true - } - - if candidate.D.OutputFormat == "" { - numCandidatesFound++ - } else if _, found := candidates.GetByName(candidate.D.OutputFormat); found { - numCandidatesFound++ - } - - return true - } - - getHookTemplate := func() (*tplimpl.TemplInfo, bool) { - q := tplimpl.TemplateQuery{ - Path: base, - Category: tplimpl.CategoryMarkup, - Desc: layoutDescriptor, - Consider: consider, - } - - v := pco.po.p.s.TemplateStore.LookupPagesLayout(q) - return v, v != nil - } - - templ, found1 := getHookTemplate() - if found1 && templ == nil { - panic("found1 is true, but templ is nil") - } - - if !found1 && layoutDescriptor.OutputFormat == pco.po.p.s.conf.DefaultOutputFormat { - numCandidatesFound++ - } - - if numCandidatesFound > 1 { - // More than one output format candidate found for this hook temoplate, - // so we cannot reuse the same rendered content. - pco.po.p.incrPageOutputTemplateVariation() - } - - if !found1 { - if tp == hooks.CodeBlockRendererType { - // No user provided template for code blocks, so we use the native Go version -- which is also faster. - r := pco.po.p.s.ContentSpec.Converters.GetHighlighter() - renderCache[key] = r - return r - } - return nil - } - - r := hookRendererTemplate{ - templateHandler: pco.po.p.s.GetTemplateStore(), - templ: templ, - resolvePosition: resolvePosition, - } - renderCache[key] = r - return r - } - }) - - return nil -} - -func (pco *pageContentOutput) getContentConverter() (converter.Converter, error) { - if err := pco.initRenderHooks(); err != nil { - return nil, err - } - return pco.po.p.getContentConverter(), nil -} - -func (cp *pageContentOutput) ParseAndRenderContent(ctx context.Context, content []byte, renderTOC bool) (converter.ResultRender, error) { - c, err := cp.getContentConverter() - if err != nil { - return nil, err - } - return cp.renderContentWithConverter(ctx, c, content, renderTOC) -} - -func (pco *pageContentOutput) ParseContent(ctx context.Context, content []byte) (converter.ResultParse, bool, error) { - c, err := pco.getContentConverter() - if err != nil { - return nil, false, err - } - p, ok := c.(converter.ParseRenderer) - if !ok { - return nil, ok, nil - } - rctx := converter.RenderContext{ - Ctx: ctx, - Src: content, - RenderTOC: true, - GetRenderer: pco.renderHooks.getRenderer, - } - r, err := p.Parse(rctx) - return r, ok, err -} - -func (pco *pageContentOutput) RenderContent(ctx context.Context, content []byte, doc any) (converter.ResultRender, bool, error) { - c, err := pco.getContentConverter() - if err != nil { - return nil, false, err - } - p, ok := c.(converter.ParseRenderer) - if !ok { - return nil, ok, nil - } - rctx := converter.RenderContext{ - Ctx: ctx, - Src: content, - RenderTOC: true, - GetRenderer: pco.renderHooks.getRenderer, - } - r, err := p.Render(rctx, doc) - return r, ok, err -} - -func (pco *pageContentOutput) renderContentWithConverter(ctx context.Context, c converter.Converter, content []byte, renderTOC bool) (converter.ResultRender, error) { - r, err := c.Convert( - converter.RenderContext{ - Ctx: ctx, - Src: content, - RenderTOC: renderTOC, - GetRenderer: pco.renderHooks.getRenderer, - }) - return r, err -} - -// these will be shifted out when rendering a given output format. -type pagePerOutputProviders interface { - targetPather - page.PaginatorProvider - resource.ResourceLinksProvider -} - -type targetPather interface { - targetPaths() page.TargetPaths - getRelURL() string -} - -type targetPathsHolder struct { - // relURL is usually the same as OutputFormat.RelPermalink, but can be different - // for non-permalinkable output formats. These shares RelPermalink with the main (first) output format. - relURL string - paths page.TargetPaths - page.OutputFormat -} - -func (t targetPathsHolder) getRelURL() string { - return t.relURL -} - -func (t targetPathsHolder) targetPaths() page.TargetPaths { - return t.paths -} - -func executeToString(ctx context.Context, h *tplimpl.TemplateStore, templ *tplimpl.TemplInfo, data any) (string, error) { - b := bp.GetBuffer() - defer bp.PutBuffer(b) - if err := h.ExecuteWithContext(ctx, templ, b, data); err != nil { - return "", err - } - return b.String(), nil -} diff --git a/hugolib/page__position.go b/hugolib/page__position.go deleted file mode 100644 index d55ebeb07..000000000 --- a/hugolib/page__position.go +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2019 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 hugolib - -import ( - "context" - - "github.com/gohugoio/hugo/common/hugo" - "github.com/gohugoio/hugo/lazy" - "github.com/gohugoio/hugo/resources/page" -) - -func newPagePosition(n *nextPrev) pagePosition { - return pagePosition{nextPrev: n} -} - -func newPagePositionInSection(n *nextPrev) pagePositionInSection { - return pagePositionInSection{nextPrev: n} -} - -type nextPrev struct { - init *lazy.Init - prevPage page.Page - nextPage page.Page -} - -func (n *nextPrev) next() page.Page { - n.init.Do(context.Background()) - return n.nextPage -} - -func (n *nextPrev) prev() page.Page { - n.init.Do(context.Background()) - return n.prevPage -} - -type pagePosition struct { - *nextPrev -} - -func (p pagePosition) Next() page.Page { - return p.next() -} - -// Deprecated: Use Next instead. -func (p pagePosition) NextPage() page.Page { - hugo.Deprecate(".Page.NextPage", "Use .Page.Next instead.", "v0.123.0") - return p.Next() -} - -func (p pagePosition) Prev() page.Page { - return p.prev() -} - -// Deprecated: Use Prev instead. -func (p pagePosition) PrevPage() page.Page { - hugo.Deprecate(".Page.PrevPage", "Use .Page.Prev instead.", "v0.123.0") - return p.Prev() -} - -type pagePositionInSection struct { - *nextPrev -} - -func (p pagePositionInSection) NextInSection() page.Page { - return p.next() -} - -func (p pagePositionInSection) PrevInSection() page.Page { - return p.prev() -} diff --git a/hugolib/page__ref.go b/hugolib/page__ref.go deleted file mode 100644 index e55a8a3e4..000000000 --- a/hugolib/page__ref.go +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright 2019 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 hugolib - -import ( - "fmt" - - "github.com/gohugoio/hugo/common/text" - - "github.com/mitchellh/mapstructure" -) - -func newPageRef(p *pageState) pageRef { - return pageRef{p: p} -} - -type pageRef struct { - p *pageState -} - -func (p pageRef) Ref(argsm map[string]any) (string, error) { - return p.ref(argsm, p.p) -} - -func (p pageRef) RefFrom(argsm map[string]any, source any) (string, error) { - return p.ref(argsm, source) -} - -func (p pageRef) RelRef(argsm map[string]any) (string, error) { - return p.relRef(argsm, p.p) -} - -func (p pageRef) RelRefFrom(argsm map[string]any, source any) (string, error) { - return p.relRef(argsm, source) -} - -func (p pageRef) decodeRefArgs(args map[string]any) (refArgs, *Site, error) { - var ra refArgs - err := mapstructure.WeakDecode(args, &ra) - if err != nil { - return ra, nil, nil - } - - s := p.p.s - - if ra.Lang != "" && ra.Lang != p.p.s.Language().Lang { - // Find correct site - found := false - for _, ss := range p.p.s.h.Sites { - if ss.Lang() == ra.Lang { - found = true - s = ss - } - } - - if !found { - p.p.s.siteRefLinker.logNotFound(ra.Path, fmt.Sprintf("no site found with lang %q", ra.Lang), nil, text.Position{}) - return ra, nil, nil - } - } - - return ra, s, nil -} - -func (p pageRef) ref(argsm map[string]any, source any) (string, error) { - args, s, err := p.decodeRefArgs(argsm) - if err != nil { - return "", fmt.Errorf("invalid arguments to Ref: %w", err) - } - - if s == nil { - return p.p.s.siteRefLinker.notFoundURL, nil - } - - if args.Path == "" { - return "", nil - } - - return s.refLink(args.Path, source, false, args.OutputFormat) -} - -func (p pageRef) relRef(argsm map[string]any, source any) (string, error) { - args, s, err := p.decodeRefArgs(argsm) - if err != nil { - return "", fmt.Errorf("invalid arguments to Ref: %w", err) - } - - if s == nil { - return p.p.s.siteRefLinker.notFoundURL, nil - } - - if args.Path == "" { - return "", nil - } - - return s.refLink(args.Path, source, true, args.OutputFormat) -} - -type refArgs struct { - Path string - Lang string - OutputFormat string -} diff --git a/hugolib/page__tree.go b/hugolib/page__tree.go deleted file mode 100644 index cccfb8904..000000000 --- a/hugolib/page__tree.go +++ /dev/null @@ -1,203 +0,0 @@ -// Copyright 2019 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 hugolib - -import ( - "context" - "fmt" - "strings" - - "github.com/gohugoio/hugo/common/paths" - "github.com/gohugoio/hugo/common/types" - "github.com/gohugoio/hugo/hugolib/doctree" - "github.com/gohugoio/hugo/resources/kinds" - "github.com/gohugoio/hugo/resources/page" -) - -// pageTree holds the treen navigational method for a Page. -type pageTree struct { - p *pageState -} - -func (pt pageTree) IsAncestor(other any) bool { - n, ok := other.(contentNodeI) - if !ok { - return false - } - - if n.Path() == pt.p.Path() { - return false - } - - return strings.HasPrefix(n.Path(), paths.AddTrailingSlash(pt.p.Path())) -} - -func (pt pageTree) IsDescendant(other any) bool { - n, ok := other.(contentNodeI) - if !ok { - return false - } - - if n.Path() == pt.p.Path() { - return false - } - - return strings.HasPrefix(pt.p.Path(), paths.AddTrailingSlash(n.Path())) -} - -func (pt pageTree) CurrentSection() page.Page { - if kinds.IsBranch(pt.p.Kind()) { - return pt.p - } - - dir := pt.p.m.pathInfo.Dir() - if dir == "/" { - return pt.p.s.home - } - - _, n := pt.p.s.pageMap.treePages.LongestPrefix(dir, true, func(n contentNodeI) bool { return n.isContentNodeBranch() }) - if n != nil { - return n.(page.Page) - } - - panic(fmt.Sprintf("CurrentSection not found for %q in lang %s", pt.p.Path(), pt.p.Lang())) -} - -func (pt pageTree) FirstSection() page.Page { - s := pt.p.m.pathInfo.Dir() - if s == "/" { - return pt.p.s.home - } - - for { - k, n := pt.p.s.pageMap.treePages.LongestPrefix(s, true, func(n contentNodeI) bool { return n.isContentNodeBranch() }) - if n == nil { - return nil - } - - // /blog - if strings.Count(k, "/") < 2 { - return n.(page.Page) - } - - if s == "" { - return nil - } - - s = paths.Dir(s) - - } -} - -func (pt pageTree) InSection(other any) bool { - if pt.p == nil || types.IsNil(other) { - return false - } - - p, ok := other.(page.Page) - if !ok { - return false - } - - return pt.CurrentSection() == p.CurrentSection() -} - -func (pt pageTree) Parent() page.Page { - if pt.p.IsHome() { - return nil - } - - dir := pt.p.m.pathInfo.ContainerDir() - - if dir == "" { - return pt.p.s.home - } - - for { - _, n := pt.p.s.pageMap.treePages.LongestPrefix(dir, true, nil) - if n == nil { - return pt.p.s.home - } - if pt.p.m.bundled || n.isContentNodeBranch() { - return n.(page.Page) - } - dir = paths.Dir(dir) - } -} - -func (pt pageTree) Ancestors() page.Pages { - var ancestors page.Pages - parent := pt.Parent() - for parent != nil { - ancestors = append(ancestors, parent) - parent = parent.Parent() - } - return ancestors -} - -func (pt pageTree) Sections() page.Pages { - var ( - pages page.Pages - currentBranchPrefix string - s = pt.p.Path() - prefix = paths.AddTrailingSlash(s) - tree = pt.p.s.pageMap.treePages - ) - - w := &doctree.NodeShiftTreeWalker[contentNodeI]{ - Tree: tree, - Prefix: prefix, - } - w.Handle = func(ss string, n contentNodeI, match doctree.DimensionFlag) (bool, error) { - if !n.isContentNodeBranch() { - return false, nil - } - if currentBranchPrefix == "" || !strings.HasPrefix(ss, currentBranchPrefix) { - if p, ok := n.(*pageState); ok && p.IsSection() && p.m.shouldList(false) && p.Parent() == pt.p { - pages = append(pages, p) - } else { - w.SkipPrefix(ss + "/") - } - } - currentBranchPrefix = ss + "/" - return false, nil - } - - if err := w.Walk(context.Background()); err != nil { - panic(err) - } - - page.SortByDefault(pages) - return pages -} - -func (pt pageTree) Page() page.Page { - return pt.p -} - -func (p pageTree) SectionsEntries() []string { - sp := p.SectionsPath() - if sp == "/" { - return nil - } - entries := strings.Split(sp[1:], "/") - if len(entries) == 0 { - return nil - } - return entries -} - -func (p pageTree) SectionsPath() string { - return p.CurrentSection().Path() -} diff --git a/hugolib/page_collections.go b/hugolib/page_collections.go new file mode 100644 index 000000000..e72f9a731 --- /dev/null +++ b/hugolib/page_collections.go @@ -0,0 +1,181 @@ +// Copyright 2016 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 hugolib + +import ( + "path" + "path/filepath" + + "github.com/gohugoio/hugo/cache" +) + +// PageCollections contains the page collections for a site. +type PageCollections struct { + // Includes only pages of all types, and only pages in the current language. + Pages Pages + + // Includes all pages in all languages, including the current one. + // Includes pages of all types. + AllPages Pages + + // A convenience cache for the traditional index types, taxonomies, home page etc. + // This is for the current language only. + indexPages Pages + + // A convenience cache for the regular pages. + // This is for the current language only. + RegularPages Pages + + // A convenience cache for the all the regular pages. + AllRegularPages Pages + + // Includes absolute all pages (of all types), including drafts etc. + rawAllPages Pages + + pageCache *cache.PartitionedLazyCache +} + +func (c *PageCollections) refreshPageCaches() { + c.indexPages = c.findPagesByKindNotIn(KindPage, c.Pages) + c.RegularPages = c.findPagesByKindIn(KindPage, c.Pages) + c.AllRegularPages = c.findPagesByKindIn(KindPage, c.AllPages) + + cacheLoader := func(kind string) func() (map[string]interface{}, error) { + return func() (map[string]interface{}, error) { + cache := make(map[string]interface{}) + switch kind { + case KindPage: + // Note that we deliberately use the pages from all sites + // in this cache, as we intend to use this in the ref and relref + // shortcodes. If the user says "sect/doc1.en.md", he/she knows + // what he/she is looking for. + for _, p := range c.AllRegularPages { + cache[filepath.ToSlash(p.Source.Path())] = p + // Ref/Relref supports this potentially ambiguous lookup. + cache[p.Source.LogicalName()] = p + } + default: + for _, p := range c.indexPages { + key := path.Join(p.sections...) + cache[key] = p + } + } + + return cache, nil + } + } + + var partitions []cache.Partition + + for _, kind := range allKindsInPages { + partitions = append(partitions, cache.Partition{Key: kind, Load: cacheLoader(kind)}) + } + + c.pageCache = cache.NewPartitionedLazyCache(partitions...) +} + +func newPageCollections() *PageCollections { + return &PageCollections{} +} + +func newPageCollectionsFromPages(pages Pages) *PageCollections { + return &PageCollections{rawAllPages: pages} +} + +func (c *PageCollections) getPage(typ string, sections ...string) *Page { + var key string + if len(sections) == 1 { + key = filepath.ToSlash(sections[0]) + } else { + key = path.Join(sections...) + } + + p, _ := c.pageCache.Get(typ, key) + if p == nil { + return nil + } + return p.(*Page) + +} + +func (*PageCollections) findPagesByKindIn(kind string, inPages Pages) Pages { + var pages Pages + for _, p := range inPages { + if p.Kind == kind { + pages = append(pages, p) + } + } + return pages +} + +func (*PageCollections) findPagesByKindNotIn(kind string, inPages Pages) Pages { + var pages Pages + for _, p := range inPages { + if p.Kind != kind { + pages = append(pages, p) + } + } + return pages +} + +func (c *PageCollections) findPagesByKind(kind string) Pages { + return c.findPagesByKindIn(kind, c.Pages) +} + +func (c *PageCollections) addPage(page *Page) { + c.rawAllPages = append(c.rawAllPages, page) +} + +// When we get a REMOVE event we're not always getting all the individual files, +// so we need to remove all below a given path. +func (c *PageCollections) removePageByPathPrefix(path string) { + for { + i := c.rawAllPages.findFirstPagePosByFilePathPrefix(path) + if i == -1 { + break + } + c.rawAllPages = append(c.rawAllPages[:i], c.rawAllPages[i+1:]...) + } +} + +func (c *PageCollections) removePageByPath(path string) { + if i := c.rawAllPages.findPagePosByFilePath(path); i >= 0 { + c.rawAllPages = append(c.rawAllPages[:i], c.rawAllPages[i+1:]...) + } +} + +func (c *PageCollections) removePage(page *Page) { + if i := c.rawAllPages.findPagePos(page); i >= 0 { + c.rawAllPages = append(c.rawAllPages[:i], c.rawAllPages[i+1:]...) + } +} + +func (c *PageCollections) findPagesByShortcode(shortcode string) Pages { + var pages Pages + + for _, p := range c.rawAllPages { + if p.shortcodeState != nil { + if _, ok := p.shortcodeState.nameSet[shortcode]; ok { + pages = append(pages, p) + } + } + } + return pages +} + +func (c *PageCollections) replacePage(page *Page) { + // will find existing page that matches filepath and remove it + c.removePage(page) + c.addPage(page) +} diff --git a/hugolib/page_collections_test.go b/hugolib/page_collections_test.go new file mode 100644 index 000000000..aee99040c --- /dev/null +++ b/hugolib/page_collections_test.go @@ -0,0 +1,140 @@ +// Copyright 2017 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 hugolib + +import ( + "fmt" + "math/rand" + "path" + "path/filepath" + "testing" + "time" + + "github.com/gohugoio/hugo/deps" + "github.com/stretchr/testify/require" +) + +const pageCollectionsPageTemplate = `--- +title: "%s" +categories: +- Hugo +--- +# Doc +` + +func BenchmarkGetPage(b *testing.B) { + var ( + cfg, fs = newTestCfg() + r = rand.New(rand.NewSource(time.Now().UnixNano())) + ) + + for i := 0; i < 10; i++ { + for j := 0; j < 100; j++ { + writeSource(b, fs, filepath.Join("content", fmt.Sprintf("sect%d", i), fmt.Sprintf("page%d.md", j)), "CONTENT") + } + } + + s := buildSingleSite(b, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) + + pagePaths := make([]string, b.N) + + for i := 0; i < b.N; i++ { + pagePaths[i] = fmt.Sprintf("sect%d", r.Intn(10)) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + home := s.getPage(KindHome) + if home == nil { + b.Fatal("Home is nil") + } + + p := s.getPage(KindSection, pagePaths[i]) + if p == nil { + b.Fatal("Section is nil") + } + + } +} + +func BenchmarkGetPageRegular(b *testing.B) { + var ( + cfg, fs = newTestCfg() + r = rand.New(rand.NewSource(time.Now().UnixNano())) + ) + + for i := 0; i < 10; i++ { + for j := 0; j < 100; j++ { + content := fmt.Sprintf(pageCollectionsPageTemplate, fmt.Sprintf("Title%d_%d", i, j)) + writeSource(b, fs, filepath.Join("content", fmt.Sprintf("sect%d", i), fmt.Sprintf("page%d.md", j)), content) + } + } + + s := buildSingleSite(b, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) + + pagePaths := make([]string, b.N) + + for i := 0; i < b.N; i++ { + pagePaths[i] = path.Join(fmt.Sprintf("sect%d", r.Intn(10)), fmt.Sprintf("page%d.md", r.Intn(100))) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + page := s.getPage(KindPage, pagePaths[i]) + require.NotNil(b, page) + } +} + +func TestGetPage(t *testing.T) { + + var ( + assert = require.New(t) + cfg, fs = newTestCfg() + ) + + for i := 0; i < 10; i++ { + for j := 0; j < 10; j++ { + content := fmt.Sprintf(pageCollectionsPageTemplate, fmt.Sprintf("Title%d_%d", i, j)) + writeSource(t, fs, filepath.Join("content", fmt.Sprintf("sect%d", i), fmt.Sprintf("page%d.md", j)), content) + } + } + + content := fmt.Sprintf(pageCollectionsPageTemplate, "UniqueBase") + writeSource(t, fs, filepath.Join("content", "sect3", "unique.md"), content) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) + + tests := []struct { + kind string + path []string + expectedTitle string + }{ + {KindHome, []string{}, ""}, + {KindSection, []string{"sect3"}, "Sect3s"}, + {KindPage, []string{"sect3", "page1.md"}, "Title3_1"}, + {KindPage, []string{"sect4/page2.md"}, "Title4_2"}, + {KindPage, []string{filepath.FromSlash("sect5/page3.md")}, "Title5_3"}, + // Ref/Relref supports this potentially ambiguous lookup. + {KindPage, []string{"unique.md"}, "UniqueBase"}, + } + + for i, test := range tests { + errorMsg := fmt.Sprintf("Test %d", i) + page := s.getPage(test.kind, test.path...) + assert.NotNil(page, errorMsg) + assert.Equal(test.kind, page.Kind) + assert.Equal(test.expectedTitle, page.Title) + } + +} diff --git a/hugolib/page_kinds.go b/hugolib/page_kinds.go deleted file mode 100644 index 9bdf689d7..000000000 --- a/hugolib/page_kinds.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2019 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 hugolib - -const ( - pageResourceType = "page" -) diff --git a/hugolib/page_output.go b/hugolib/page_output.go new file mode 100644 index 000000000..6ea466b4f --- /dev/null +++ b/hugolib/page_output.go @@ -0,0 +1,273 @@ +// Copyright 2017 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 hugolib + +import ( + "fmt" + "html/template" + "strings" + "sync" + + "github.com/gohugoio/hugo/media" + + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/output" +) + +// PageOutput represents one of potentially many output formats of a given +// Page. +type PageOutput struct { + *Page + + // Pagination + paginator *Pager + paginatorInit sync.Once + + // Keep this to create URL/path variations, i.e. paginators. + targetPathDescriptor targetPathDescriptor + + outputFormat output.Format +} + +func (p *PageOutput) targetPath(addends ...string) (string, error) { + tp, err := p.createTargetPath(p.outputFormat, addends...) + if err != nil { + return "", err + } + return tp, nil +} + +func newPageOutput(p *Page, createCopy bool, f output.Format) (*PageOutput, error) { + // TODO(bep) This is only needed for tests and we should get rid of it. + if p.targetPathDescriptorPrototype == nil { + if err := p.initTargetPathDescriptor(); err != nil { + return nil, err + } + if err := p.initURLs(); err != nil { + return nil, err + } + } + + if createCopy { + p = p.copy() + } + + td, err := p.createTargetPathDescriptor(f) + + if err != nil { + return nil, err + } + + return &PageOutput{ + Page: p, + outputFormat: f, + targetPathDescriptor: td, + }, nil +} + +// copy creates a copy of this PageOutput with the lazy sync.Once vars reset +// so they will be evaluated again, for word count calculations etc. +func (p *PageOutput) copyWithFormat(f output.Format) (*PageOutput, error) { + c, err := newPageOutput(p.Page, true, f) + if err != nil { + return nil, err + } + c.paginator = p.paginator + return c, nil +} + +func (p *PageOutput) copy() (*PageOutput, error) { + return p.copyWithFormat(p.outputFormat) +} + +func (p *PageOutput) layouts(layouts ...string) ([]string, error) { + if len(layouts) == 0 && p.selfLayout != "" { + return []string{p.selfLayout}, nil + } + + layoutOverride := "" + if len(layouts) > 0 { + layoutOverride = layouts[0] + } + + return p.s.layoutHandler.For( + p.layoutDescriptor, + layoutOverride, + p.outputFormat) +} + +func (p *PageOutput) Render(layout ...string) template.HTML { + if !p.checkRender() { + return "" + } + + l, err := p.layouts(layout...) + if err != nil { + helpers.DistinctErrorLog.Printf("in .Render: Failed to resolve layout %q for page %q", layout, p.pathOrTitle()) + return "" + } + + for _, layout := range l { + templ := p.s.Tmpl.Lookup(layout) + if templ == nil { + // This is legacy from when we had only one output format and + // HTML templates only. Some have references to layouts without suffix. + // We default to good old HTML. + templ = p.s.Tmpl.Lookup(layout + ".html") + } + if templ != nil { + res, err := templ.ExecuteToString(p) + if err != nil { + helpers.DistinctErrorLog.Printf("in .Render: Failed to execute template %q for page %q", layout, p.pathOrTitle()) + return template.HTML("") + } + return template.HTML(res) + } + } + + return "" + +} + +func (p *Page) Render(layout ...string) template.HTML { + if !p.checkRender() { + return "" + } + + p.pageOutputInit.Do(func() { + if p.mainPageOutput != nil { + return + } + // If Render is called in a range loop, the page output isn't available. + // So, create one. + outFormat := p.outputFormats[0] + pageOutput, err := newPageOutput(p, true, outFormat) + + if err != nil { + p.s.Log.ERROR.Printf("Failed to create output page for type %q for page %q: %s", outFormat.Name, p.pathOrTitle(), err) + return + } + + p.mainPageOutput = pageOutput + + }) + + return p.mainPageOutput.Render(layout...) +} + +// We may fix this in the future, but the layout handling in Render isn't built +// for list pages. +func (p *Page) checkRender() bool { + if p.Kind != KindPage { + helpers.DistinctWarnLog.Printf(".Render only available for regular pages, not for of kind %q. You probably meant .Site.RegularPages and not.Site.Pages.", p.Kind) + return false + } + return true +} + +// OutputFormats holds a list of the relevant output formats for a given resource. +type OutputFormats []*OutputFormat + +// And OutputFormat links to a representation of a resource. +type OutputFormat struct { + // Rel constains a value that can be used to construct a rel link. + // This is value is fetched from the output format definition. + // Note that for pages with only one output format, + // this method will always return "canonical". + // As an example, the AMP output format will, by default, return "amphtml". + // + // See: + // https://www.ampproject.org/docs/guides/deploy/discovery + // + // Most other output formats will have "alternate" as value for this. + Rel string + + // It may be tempting to export this, but let us hold on to that horse for a while. + f output.Format + + p *Page +} + +// Name returns this OutputFormat's name, i.e. HTML, AMP, JSON etc. +func (o OutputFormat) Name() string { + return o.f.Name +} + +// MediaType returns this OutputFormat's MediaType (MIME type). +func (o OutputFormat) MediaType() media.Type { + return o.f.MediaType +} + +// OutputFormats gives the output formats for this Page. +func (p *Page) OutputFormats() OutputFormats { + var o OutputFormats + for _, f := range p.outputFormats { + o = append(o, newOutputFormat(p, f)) + } + return o +} + +func newOutputFormat(p *Page, f output.Format) *OutputFormat { + rel := f.Rel + isCanonical := len(p.outputFormats) == 1 + if isCanonical { + rel = "canonical" + } + return &OutputFormat{Rel: rel, f: f, p: p} +} + +// OutputFormats gives the alternative output formats for this PageOutput. +// Note that we use the term "alternative" and not "alternate" here, as it +// does not necessarily replace the other format, it is an alternative representation. +func (p *PageOutput) AlternativeOutputFormats() (OutputFormats, error) { + var o OutputFormats + for _, of := range p.OutputFormats() { + if of.f.NotAlternative || of.f == p.outputFormat { + continue + } + o = append(o, of) + } + return o, nil +} + +// AlternativeOutputFormats is only available on the top level rendering +// entry point, and not inside range loops on the Page collections. +// This method is just here to inform users of that restriction. +func (p *Page) AlternativeOutputFormats() (OutputFormats, error) { + return nil, fmt.Errorf("AlternativeOutputFormats only available from the top level template context for page %q", p.Path()) +} + +// Get gets a OutputFormat given its name, i.e. json, html etc. +// It returns nil if not found. +func (o OutputFormats) Get(name string) *OutputFormat { + for _, f := range o { + if strings.EqualFold(f.f.Name, name) { + return f + } + } + return nil +} + +// Permalink returns the absolute permalink to this output format. +func (o *OutputFormat) Permalink() string { + rel := o.p.createRelativePermalinkForOutputFormat(o.f) + perm, _ := o.p.s.permalinkForOutputFormat(rel, o.f) + return perm +} + +// Permalink returns the relative permalink to this output format. +func (o *OutputFormat) RelPermalink() string { + rel := o.p.createRelativePermalinkForOutputFormat(o.f) + return o.p.s.PathSpec.PrependBasePath(rel) +} diff --git a/hugolib/page_paths.go b/hugolib/page_paths.go new file mode 100644 index 000000000..73fd62278 --- /dev/null +++ b/hugolib/page_paths.go @@ -0,0 +1,256 @@ +// Copyright 2017 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 hugolib + +import ( + "fmt" + "path/filepath" + + "net/url" + "strings" + + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/output" +) + +// targetPathDescriptor describes how a file path for a given resource +// should look like on the file system. The same descriptor is then later used to +// create both the permalinks and the relative links, paginator URLs etc. +// +// The big motivating behind this is to have only one source of truth for URLs, +// and by that also get rid of most of the fragile string parsing/encoding etc. +// +// Page.createTargetPathDescriptor is the Page adapter. +// +type targetPathDescriptor struct { + PathSpec *helpers.PathSpec + + Type output.Format + Kind string + + Sections []string + + // For regular content pages this is either + // 1) the Slug, if set, + // 2) the file base name (TranslationBaseName). + BaseName string + + // Source directory. + Dir string + + // Language prefix, set if multilingual and if page should be placed in its + // language subdir. + LangPrefix string + + // Page.URLPath.URL. Will override any Slug etc. for regular pages. + URL string + + // Used to create paginator links. + Addends string + + // The expanded permalink if defined for the section, ready to use. + ExpandedPermalink string + + // Some types cannot have uglyURLs, even if globally enabled, RSS being one example. + UglyURLs bool +} + +// createTargetPathDescriptor adapts a Page and the given output.Format into +// a targetPathDescriptor. This descriptor can then be used to create paths +// and URLs for this Page. +func (p *Page) createTargetPathDescriptor(t output.Format) (targetPathDescriptor, error) { + if p.targetPathDescriptorPrototype == nil { + panic(fmt.Sprintf("Must run initTargetPathDescriptor() for page %q, kind %q", p.Title, p.Kind)) + } + d := *p.targetPathDescriptorPrototype + d.Type = t + return d, nil +} + +func (p *Page) initTargetPathDescriptor() error { + + d := &targetPathDescriptor{ + PathSpec: p.s.PathSpec, + Kind: p.Kind, + Sections: p.sections, + UglyURLs: p.s.Info.uglyURLs, + Dir: filepath.ToSlash(p.Source.Dir()), + URL: p.URLPath.URL, + } + + if p.Slug != "" { + d.BaseName = p.Slug + } else { + d.BaseName = p.TranslationBaseName() + } + + if p.shouldAddLanguagePrefix() { + d.LangPrefix = p.Lang() + } + + if override, ok := p.Site.Permalinks[p.Section()]; ok { + opath, err := override.Expand(p) + if err != nil { + return err + } + + opath, _ = url.QueryUnescape(opath) + opath = filepath.FromSlash(opath) + d.ExpandedPermalink = opath + } + + p.targetPathDescriptorPrototype = d + return nil + +} + +// createTargetPath creates the target filename for this Page for the given +// output.Format. Some additional URL parts can also be provided, the typical +// use case being pagination. +func (p *Page) createTargetPath(t output.Format, addends ...string) (string, error) { + d, err := p.createTargetPathDescriptor(t) + if err != nil { + return "", nil + } + + if len(addends) > 0 { + d.Addends = filepath.Join(addends...) + } + + return createTargetPath(d), nil +} + +func createTargetPath(d targetPathDescriptor) string { + + pagePath := helpers.FilePathSeparator + + // The top level index files, i.e. the home page etc., needs + // the index base even when uglyURLs is enabled. + needsBase := true + + isUgly := d.UglyURLs && !d.Type.NoUgly + + // If the page output format's base name is the same as the page base name, + // we treat it as an ugly path, i.e. + // my-blog-post-1/index.md => my-blog-post-1/index.html + // (given the default values for that content file, i.e. no slug set etc.). + // This introduces the behaviour from < Hugo 0.20, see issue #3396. + if d.BaseName != "" && d.BaseName == d.Type.BaseName { + isUgly = true + } + + if d.Kind != KindPage && len(d.Sections) > 0 { + pagePath = filepath.Join(d.Sections...) + needsBase = false + } + + if d.Type.Path != "" { + pagePath = filepath.Join(pagePath, d.Type.Path) + } + + if d.Kind == KindPage { + // Always use URL if it's specified + if d.URL != "" { + pagePath = filepath.Join(pagePath, d.URL) + if strings.HasSuffix(d.URL, "/") || !strings.Contains(d.URL, ".") { + pagePath = filepath.Join(pagePath, d.Type.BaseName+d.Type.MediaType.FullSuffix()) + } + } else { + if d.ExpandedPermalink != "" { + pagePath = filepath.Join(pagePath, d.ExpandedPermalink) + + } else { + if d.Dir != "" { + pagePath = filepath.Join(pagePath, d.Dir) + } + if d.BaseName != "" { + pagePath = filepath.Join(pagePath, d.BaseName) + } + } + + if d.Addends != "" { + pagePath = filepath.Join(pagePath, d.Addends) + } + + if isUgly { + pagePath += d.Type.MediaType.Delimiter + d.Type.MediaType.Suffix + } else { + pagePath = filepath.Join(pagePath, d.Type.BaseName+d.Type.MediaType.FullSuffix()) + } + + if d.LangPrefix != "" { + pagePath = filepath.Join(d.LangPrefix, pagePath) + } + } + } else { + if d.Addends != "" { + pagePath = filepath.Join(pagePath, d.Addends) + } + + needsBase = needsBase && d.Addends == "" + + // No permalink expansion etc. for node type pages (for now) + base := "" + + if needsBase || !isUgly { + base = helpers.FilePathSeparator + d.Type.BaseName + } + + pagePath += base + d.Type.MediaType.FullSuffix() + + if d.LangPrefix != "" { + pagePath = filepath.Join(d.LangPrefix, pagePath) + } + } + + pagePath = filepath.Join(helpers.FilePathSeparator, pagePath) + + // Note: MakePathSanitized will lower case the path if + // disablePathToLower isn't set. + return d.PathSpec.MakePathSanitized(pagePath) +} + +func (p *Page) createRelativePermalink() string { + + if len(p.outputFormats) == 0 { + panic(fmt.Sprintf("Page %q missing output format(s)", p.Title)) + } + + // Choose the main output format. In most cases, this will be HTML. + f := p.outputFormats[0] + + return p.createRelativePermalinkForOutputFormat(f) + +} + +func (p *Page) createRelativePermalinkForOutputFormat(f output.Format) string { + tp, err := p.createTargetPath(f) + + if err != nil { + p.s.Log.ERROR.Printf("Failed to create permalink for page %q: %s", p.FullFilePath(), err) + return "" + } + // For /index.json etc. we must use the full path. + if strings.HasSuffix(f.BaseFilename(), "html") { + tp = strings.TrimSuffix(tp, f.BaseFilename()) + } + + return p.s.PathSpec.URLizeFilename(tp) +} + +func (p *Page) TargetPath() (outfile string) { + // Delete in Hugo 0.22 + helpers.Deprecated("Page", "TargetPath", "This method does not make sanse any more.", true) + return "" +} diff --git a/hugolib/page_paths_test.go b/hugolib/page_paths_test.go new file mode 100644 index 000000000..80dc390cc --- /dev/null +++ b/hugolib/page_paths_test.go @@ -0,0 +1,190 @@ +// Copyright 2017 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 hugolib + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/gohugoio/hugo/media" + + "fmt" + + "github.com/gohugoio/hugo/output" +) + +func TestPageTargetPath(t *testing.T) { + + pathSpec := newTestDefaultPathSpec() + + noExtNoDelimMediaType := media.TextType + noExtNoDelimMediaType.Suffix = "" + noExtNoDelimMediaType.Delimiter = "" + + // Netlify style _redirects + noExtDelimFormat := output.Format{ + Name: "NER", + MediaType: noExtNoDelimMediaType, + BaseName: "_redirects", + } + + for _, langPrefix := range []string{"", "no"} { + for _, uglyURLs := range []bool{false, true} { + t.Run(fmt.Sprintf("langPrefix=%q,uglyURLs=%t", langPrefix, uglyURLs), + func(t *testing.T) { + + tests := []struct { + name string + d targetPathDescriptor + expected string + }{ + {"JSON home", targetPathDescriptor{Kind: KindHome, Type: output.JSONFormat}, "/index.json"}, + {"AMP home", targetPathDescriptor{Kind: KindHome, Type: output.AMPFormat}, "/amp/index.html"}, + {"HTML home", targetPathDescriptor{Kind: KindHome, BaseName: "_index", Type: output.HTMLFormat}, "/index.html"}, + {"Netlify redirects", targetPathDescriptor{Kind: KindHome, BaseName: "_index", Type: noExtDelimFormat}, "/_redirects"}, + {"HTML section list", targetPathDescriptor{ + Kind: KindSection, + Sections: []string{"sect1"}, + BaseName: "_index", + Type: output.HTMLFormat}, "/sect1/index.html"}, + {"HTML taxonomy list", targetPathDescriptor{ + Kind: KindTaxonomy, + Sections: []string{"tags", "hugo"}, + BaseName: "_index", + Type: output.HTMLFormat}, "/tags/hugo/index.html"}, + {"HTML taxonomy term", targetPathDescriptor{ + Kind: KindTaxonomy, + Sections: []string{"tags"}, + BaseName: "_index", + Type: output.HTMLFormat}, "/tags/index.html"}, + { + "HTML page", targetPathDescriptor{ + Kind: KindPage, + Dir: "/a/b", + BaseName: "mypage", + Sections: []string{"a"}, + Type: output.HTMLFormat}, "/a/b/mypage/index.html"}, + + { + // Issue #3396 + "HTML page with index as base", targetPathDescriptor{ + Kind: KindPage, + Dir: "/a/b", + BaseName: "index", + Sections: []string{"a"}, + Type: output.HTMLFormat}, "/a/b/index.html"}, + + { + "HTML page with special chars", targetPathDescriptor{ + Kind: KindPage, + Dir: "/a/b", + BaseName: "My Page!", + Type: output.HTMLFormat}, "/a/b/My-Page/index.html"}, + {"RSS home", targetPathDescriptor{Kind: kindRSS, Type: output.RSSFormat}, "/index.xml"}, + {"RSS section list", targetPathDescriptor{ + Kind: kindRSS, + Sections: []string{"sect1"}, + Type: output.RSSFormat}, "/sect1/index.xml"}, + { + "AMP page", targetPathDescriptor{ + Kind: KindPage, + Dir: "/a/b/c", + BaseName: "myamp", + Type: output.AMPFormat}, "/amp/a/b/c/myamp/index.html"}, + { + "AMP page with URL with suffix", targetPathDescriptor{ + Kind: KindPage, + Dir: "/sect/", + BaseName: "mypage", + URL: "/some/other/url.xhtml", + Type: output.HTMLFormat}, "/some/other/url.xhtml"}, + { + "JSON page with URL without suffix", targetPathDescriptor{ + Kind: KindPage, + Dir: "/sect/", + BaseName: "mypage", + URL: "/some/other/path/", + Type: output.JSONFormat}, "/some/other/path/index.json"}, + { + "JSON page with URL without suffix and no trailing slash", targetPathDescriptor{ + Kind: KindPage, + Dir: "/sect/", + BaseName: "mypage", + URL: "/some/other/path", + Type: output.JSONFormat}, "/some/other/path/index.json"}, + { + "HTML page with expanded permalink", targetPathDescriptor{ + Kind: KindPage, + Dir: "/a/b", + BaseName: "mypage", + ExpandedPermalink: "/2017/10/my-title", + Type: output.HTMLFormat}, "/2017/10/my-title/index.html"}, + { + "Paginated HTML home", targetPathDescriptor{ + Kind: KindHome, + BaseName: "_index", + Type: output.HTMLFormat, + Addends: "page/3"}, "/page/3/index.html"}, + { + "Paginated Taxonomy list", targetPathDescriptor{ + Kind: KindTaxonomy, + BaseName: "_index", + Sections: []string{"tags", "hugo"}, + Type: output.HTMLFormat, + Addends: "page/3"}, "/tags/hugo/page/3/index.html"}, + { + "Regular page with addend", targetPathDescriptor{ + Kind: KindPage, + Dir: "/a/b", + BaseName: "mypage", + Addends: "c/d/e", + Type: output.HTMLFormat}, "/a/b/mypage/c/d/e/index.html"}, + } + + for i, test := range tests { + test.d.PathSpec = pathSpec + test.d.UglyURLs = uglyURLs + test.d.LangPrefix = langPrefix + test.d.Dir = filepath.FromSlash(test.d.Dir) + isUgly := uglyURLs && !test.d.Type.NoUgly + + expected := test.expected + + // TODO(bep) simplify + if test.d.Kind == KindPage && test.d.BaseName == test.d.Type.BaseName { + + } else if test.d.Kind == KindHome && test.d.Type.Path != "" { + } else if (!strings.HasPrefix(expected, "/index") || test.d.Addends != "") && test.d.URL == "" && isUgly { + expected = strings.Replace(expected, + "/"+test.d.Type.BaseName+"."+test.d.Type.MediaType.Suffix, + "."+test.d.Type.MediaType.Suffix, -1) + } + + if test.d.LangPrefix != "" && !(test.d.Kind == KindPage && test.d.URL != "") { + expected = "/" + test.d.LangPrefix + expected + } + + expected = filepath.FromSlash(expected) + + pagePath := createTargetPath(test.d) + + if pagePath != expected { + t.Fatalf("[%d] [%s] targetPath expected %q, got: %q", i, test.name, expected, pagePath) + } + } + }) + } + } +} diff --git a/hugolib/page_permalink_test.go b/hugolib/page_permalink_test.go index d8fd99d79..6f899efae 100644 --- a/hugolib/page_permalink_test.go +++ b/hugolib/page_permalink_test.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Hugo Authors. All rights reserved. +// Copyright 2015 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,11 +16,12 @@ package hugolib import ( "fmt" "html/template" + "path/filepath" "testing" - qt "github.com/frankban/quicktest" + "github.com/stretchr/testify/require" - "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/deps" ) func TestPermalink(t *testing.T) { @@ -54,108 +55,46 @@ func TestPermalink(t *testing.T) { {"x/y/z/boofar.md", "http://barnew/boo/", "booslug", "", false, false, "http://barnew/boo/x/y/z/booslug/", "/boo/x/y/z/booslug/"}, {"x/y/z/boofar.md", "http://barnew/boo/", "booslug", "", true, true, "http://barnew/boo/x/y/z/booslug.html", "/x/y/z/booslug.html"}, {"x/y/z/boofar.md", "http://barnew/boo", "booslug", "", true, true, "http://barnew/boo/x/y/z/booslug.html", "/x/y/z/booslug.html"}, - // Issue #4666 - {"x/y/z/boo-makeindex.md", "http://barnew/boo", "", "", true, true, "http://barnew/boo/x/y/z/boo-makeindex.html", "/x/y/z/boo-makeindex.html"}, // test URL overrides {"x/y/z/boofar.md", "", "", "/z/y/q/", false, false, "/z/y/q/", "/z/y/q/"}, - // test URL override with expands - {"x/y/z/boofar.md", "", "test", "/z/:slug/", false, false, "/z/test/", "/z/test/"}, } for i, test := range tests { - i := i - test := test - t.Run(fmt.Sprintf("%s-%d", test.file, i), func(t *testing.T) { - t.Parallel() - c := qt.New(t) - cfg := config.New() - cfg.Set("uglyURLs", test.uglyURLs) - cfg.Set("canonifyURLs", test.canonifyURLs) - files := fmt.Sprintf(` --- hugo.toml -- -baseURL = %q --- content/%s -- ---- + cfg, fs := newTestCfg() + + cfg.Set("uglyURLs", test.uglyURLs) + cfg.Set("canonifyURLs", test.canonifyURLs) + cfg.Set("baseURL", test.base) + + pageContent := fmt.Sprintf(`--- title: Page slug: %q -url: %q -output: ["HTML"] ---- -`, test.base, test.file, test.slug, test.url) - - if i > 0 { - t.Skip() - } - - b := NewIntegrationTestBuilder( - IntegrationTestConfig{ - T: t, - TxtarString: files, - BaseCfg: cfg, - }, - ) - - b.Build() - s := b.H.Sites[0] - c.Assert(len(s.RegularPages()), qt.Equals, 1) - p := s.RegularPages()[0] - u := p.Permalink() - - expected := test.expectedAbs - if u != expected { - t.Fatalf("[%d] Expected abs url: %s, got: %s", i, expected, u) - } - - u = p.RelPermalink() - - expected = test.expectedRel - if u != expected { - t.Errorf("[%d] Expected rel url: %s, got: %s", i, expected, u) - } - }) - } -} - -func TestRelativeURLInFrontMatter(t *testing.T) { - config := ` -baseURL = "https://example.com" -defaultContentLanguage = "en" -defaultContentLanguageInSubdir = false - -[Languages] -[Languages.en] -weight = 10 -contentDir = "content/en" -[Languages.nn] -weight = 20 -contentDir = "content/nn" - -` - - pageTempl := `--- -title: "A page" url: %q --- +Content +`, test.slug, test.url) -Some content. -` + writeSource(t, fs, filepath.Join("content", filepath.FromSlash(test.file)), pageContent) - b := newTestSitesBuilder(t).WithConfigFile("toml", config) - b.WithContent("content/en/blog/page1.md", fmt.Sprintf(pageTempl, "myblog/p1/")) - b.WithContent("content/en/blog/page2.md", fmt.Sprintf(pageTempl, "../../../../../myblog/p2/")) - b.WithContent("content/en/blog/page3.md", fmt.Sprintf(pageTempl, "../myblog/../myblog/p3/")) - b.WithContent("content/en/blog/_index.md", fmt.Sprintf(pageTempl, "this-is-my-english-blog")) - b.WithContent("content/nn/blog/page1.md", fmt.Sprintf(pageTempl, "myblog/p1/")) - b.WithContent("content/nn/blog/_index.md", fmt.Sprintf(pageTempl, "this-is-my-blog")) + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) + require.Len(t, s.RegularPages, 1) - b.Build(BuildCfg{}) + p := s.RegularPages[0] - b.AssertFileContent("public/nn/myblog/p1/index.html", "Single: A page|Hello|nn|RelPermalink: /nn/myblog/p1/|") - b.AssertFileContent("public/nn/this-is-my-blog/index.html", "List Page 1|A page|Hello|https://example.com/nn/this-is-my-blog/|") - b.AssertFileContent("public/this-is-my-english-blog/index.html", "List Page 1|A page|Hello|https://example.com/this-is-my-english-blog/|") - b.AssertFileContent("public/myblog/p1/index.html", "Single: A page|Hello|en|RelPermalink: /myblog/p1/|Permalink: https://example.com/myblog/p1/|") - b.AssertFileContent("public/myblog/p2/index.html", "Single: A page|Hello|en|RelPermalink: /myblog/p2/|Permalink: https://example.com/myblog/p2/|") - b.AssertFileContent("public/myblog/p3/index.html", "Single: A page|Hello|en|RelPermalink: /myblog/p3/|Permalink: https://example.com/myblog/p3/|") + u := p.Permalink() + + expected := test.expectedAbs + if u != expected { + t.Fatalf("[%d] Expected abs url: %s, got: %s", i, expected, u) + } + + u = p.RelPermalink() + + expected = test.expectedRel + if u != expected { + t.Errorf("[%d] Expected rel url: %s, got: %s", i, expected, u) + } + } } diff --git a/hugolib/page_taxonomy_test.go b/hugolib/page_taxonomy_test.go new file mode 100644 index 000000000..e0dc1ffbc --- /dev/null +++ b/hugolib/page_taxonomy_test.go @@ -0,0 +1,96 @@ +// Copyright 2015 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 hugolib + +import ( + "reflect" + "strings" + "testing" +) + +var pageYamlWithTaxonomiesA = `--- +tags: ['a', 'B', 'c'] +categories: 'd' +--- +YAML frontmatter with tags and categories taxonomy.` + +var pageYamlWithTaxonomiesB = `--- +tags: + - "a" + - "B" + - "c" +categories: 'd' +--- +YAML frontmatter with tags and categories taxonomy.` + +var pageYamlWithTaxonomiesC = `--- +tags: 'E' +categories: 'd' +--- +YAML frontmatter with tags and categories taxonomy.` + +var pageJSONWithTaxonomies = `{ + "categories": "D", + "tags": [ + "a", + "b", + "c" + ] +} +JSON Front Matter with tags and categories` + +var pageTomlWithTaxonomies = `+++ +tags = [ "a", "B", "c" ] +categories = "d" ++++ +TOML Front Matter with tags and categories` + +func TestParseTaxonomies(t *testing.T) { + t.Parallel() + for _, test := range []string{pageTomlWithTaxonomies, + pageJSONWithTaxonomies, + pageYamlWithTaxonomiesA, + pageYamlWithTaxonomiesB, + pageYamlWithTaxonomiesC, + } { + + s := newTestSite(t) + p, _ := s.NewPage("page/with/taxonomy") + _, err := p.ReadFrom(strings.NewReader(test)) + if err != nil { + t.Fatalf("Failed parsing %q: %s", test, err) + } + + param := p.GetParam("tags") + + if params, ok := param.([]string); ok { + expected := []string{"a", "b", "c"} + if !reflect.DeepEqual(params, expected) { + t.Errorf("Expected %s: got: %s", expected, params) + } + } else if params, ok := param.(string); ok { + expected := "e" + if params != expected { + t.Errorf("Expected %s: got: %s", expected, params) + } + } + + param = p.GetParam("categories") + singleparam := param.(string) + + if singleparam != "d" { + t.Fatalf("Expected: d, got: %s", singleparam) + } + } +} diff --git a/hugolib/page_test.go b/hugolib/page_test.go index 1da67e58f..88724cd1c 100644 --- a/hugolib/page_test.go +++ b/hugolib/page_test.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Hugo Authors. All rights reserved. +// Copyright 2015 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,46 +14,80 @@ package hugolib import ( - "context" + "bytes" "fmt" "html/template" + "os" "path/filepath" + "reflect" + "sort" "strings" "testing" "time" - "github.com/bep/clocks" - "github.com/gohugoio/hugo/markup/asciidocext" - "github.com/gohugoio/hugo/markup/rst" - "github.com/gohugoio/hugo/tpl" - - "github.com/gohugoio/hugo/config" - - "github.com/gohugoio/hugo/common/hashing" - "github.com/gohugoio/hugo/common/htime" - "github.com/gohugoio/hugo/common/loggers" - - "github.com/gohugoio/hugo/resources/page" - "github.com/gohugoio/hugo/resources/resource" - - qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/helpers" + "github.com/spf13/cast" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) +var emptyPage = "" + const ( - homePage = "---\ntitle: Home\n---\nHome Page Content\n" - simplePage = "---\ntitle: Simple\n---\nSimple Page\n" - - simplePageRFC3339Date = "---\ntitle: RFC3339 Date\ndate: \"2013-05-17T16:59:30Z\"\n---\nrfc3339 content" - - simplePageWithoutSummaryDelimiter = `--- -title: SimpleWithoutSummaryDelimiter + simplePage = "---\ntitle: Simple\n---\nSimple Page\n" + invalidFrontMatterMissing = "This is a test" + renderNoFrontmatter = "<!doctype><html><head></head><body>This is a test</body></html>" + contentWithCommentedFrontmatter = "<!--\n+++\ntitle = \"Network configuration\"\ndescription = \"Docker networking\"\nkeywords = [\"network\"]\n[menu.main]\nparent= \"smn_administrate\"\n+++\n-->\n\n# Network configuration\n\n##\nSummary" + contentWithCommentedTextFrontmatter = "<!--[metaData]>\n+++\ntitle = \"Network configuration\"\ndescription = \"Docker networking\"\nkeywords = [\"network\"]\n[menu.main]\nparent= \"smn_administrate\"\n+++\n<![end-metadata]-->\n\n# Network configuration\n\n##\nSummary" + contentWithCommentedLongFrontmatter = "<!--[metaData123456789012345678901234567890]>\n+++\ntitle = \"Network configuration\"\ndescription = \"Docker networking\"\nkeywords = [\"network\"]\n[menu.main]\nparent= \"smn_administrate\"\n+++\n<![end-metadata]-->\n\n# Network configuration\n\n##\nSummary" + contentWithCommentedLong2Frontmatter = "<!--[metaData]>\n+++\ntitle = \"Network configuration\"\ndescription = \"Docker networking\"\nkeywords = [\"network\"]\n[menu.main]\nparent= \"smn_administrate\"\n+++\n<![end-metadata123456789012345678901234567890]-->\n\n# Network configuration\n\n##\nSummary" + invalidFrontmatterShortDelim = ` +-- +title: Short delim start --- -[Lorem ipsum](https://lipsum.com/) dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +Short Delim +` -Additional text. + invalidFrontmatterShortDelimEnding = ` +--- +title: Short delim ending +-- +Short Delim +` -Further text. + invalidFrontmatterLadingWs = ` + + --- +title: Leading WS +--- +Leading +` + + simplePageJSON = ` +{ +"title": "spf13-vim 3.0 release and new website", +"description": "spf13-vim is a cross platform distribution of vim plugins and resources for Vim.", +"tags": [ ".vimrc", "plugins", "spf13-vim", "VIm" ], +"date": "2012-04-06", +"categories": [ + "Development", + "VIM" +], +"slug": "-spf13-vim-3-0-release-and-new-website-" +} + +Content of the file goes Here +` + + simplePageRFC3339Date = "---\ntitle: RFC3339 Date\ndate: \"2013-05-17T16:59:30Z\"\n---\nrfc3339 content" + simplePageJSONMultiple = ` +{ + "title": "foobar", + "customData": { "foo": "bar" }, + "date": "2012-08-06" +} +Some text ` simplePageWithSummaryDelimiter = `--- @@ -63,16 +97,6 @@ Summary Next Line <!--more--> Some more text -` - - simplePageWithSummaryParameter = `--- -title: SimpleWithSummaryParameter -summary: "Page with summary parameter and [a link](http://www.example.com/)" ---- - -Some text. - -Some more text. ` simplePageWithSummaryDelimiterAndMarkdownThatCrossesBorder = `--- @@ -90,6 +114,12 @@ Summary Next Line. {{<figure src="/not/real" >}}. More text here. Some more text +` + + simplePageWithEmbeddedScript = `--- +title: Simple +--- +<script type='text/javascript'>alert('the script tags are still there, right?');</script> ` simplePageWithSummaryDelimiterSameLine = `--- @@ -98,6 +128,14 @@ title: Simple Summary Same Line<!--more--> Some more text +` + + simplePageWithSummaryDelimiterOnlySummary = `--- +title: Simple +--- +Summary text + +<!--more--> ` simplePageWithAllCJKRunes = `--- @@ -235,6 +273,16 @@ the cylinder and strike me down. ## BB ### BBB "You're a great Granser," he cried delightedly, "always making believe them little marks mean something." +` + + simplePageWithAdditionalExtension = `+++ +[blackfriday] + extensions = ["hardLineBreak"] ++++ +first line. +second line. + +fourth line. ` simplePageWithURL = `--- @@ -279,16 +327,154 @@ date: '2013-10-15T06:16:13' UTF8 Page With Date` ) -func checkPageTitle(t *testing.T, page page.Page, title string) { - if page.Title() != title { - t.Fatalf("Page title is: %s. Expected %s", page.Title(), title) +var pageWithVariousFrontmatterTypes = `+++ +a_string = "bar" +an_integer = 1 +a_float = 1.3 +a_bool = false +a_date = 1979-05-27T07:32:00Z + +[a_table] +a_key = "a_value" ++++ +Front Matter with various frontmatter types` + +var pageWithCalendarYAMLFrontmatter = `--- +type: calendar +weeks: + - + start: "Jan 5" + days: + - activity: class + room: EN1000 + - activity: lab + - activity: class + - activity: lab + - activity: class + - + start: "Jan 12" + days: + - activity: class + - activity: lab + - activity: class + - activity: lab + - activity: exam +--- + +Hi. +` + +var pageWithCalendarJSONFrontmatter = `{ + "type": "calendar", + "weeks": [ + { + "start": "Jan 5", + "days": [ + { "activity": "class", "room": "EN1000" }, + { "activity": "lab" }, + { "activity": "class" }, + { "activity": "lab" }, + { "activity": "class" } + ] + }, + { + "start": "Jan 12", + "days": [ + { "activity": "class" }, + { "activity": "lab" }, + { "activity": "class" }, + { "activity": "lab" }, + { "activity": "exam" } + ] + } + ] +} + +Hi. +` + +var pageWithCalendarTOMLFrontmatter = `+++ +type = "calendar" + +[[weeks]] +start = "Jan 5" + +[[weeks.days]] +activity = "class" +room = "EN1000" + +[[weeks.days]] +activity = "lab" + +[[weeks.days]] +activity = "class" + +[[weeks.days]] +activity = "lab" + +[[weeks.days]] +activity = "class" + +[[weeks]] +start = "Jan 12" + +[[weeks.days]] +activity = "class" + +[[weeks.days]] +activity = "lab" + +[[weeks.days]] +activity = "class" + +[[weeks.days]] +activity = "lab" + +[[weeks.days]] +activity = "exam" ++++ + +Hi. +` + +func checkError(t *testing.T, err error, expected string) { + if err == nil { + t.Fatalf("err is nil. Expected: %s", expected) + } + if err.Error() != expected { + t.Errorf("err.Error() returned: '%s'. Expected: '%s'", err.Error(), expected) } } -func checkPageContent(t *testing.T, page page.Page, expected string, msg ...any) { - t.Helper() - a := normalizeContent(expected) - b := normalizeContent(content(page)) +func TestDegenerateEmptyPageZeroLengthName(t *testing.T) { + t.Parallel() + s := newTestSite(t) + _, err := s.NewPage("") + if err == nil { + t.Fatalf("A zero length page name must return an error") + } + + checkError(t, err, "Zero length page name") +} + +func TestDegenerateEmptyPage(t *testing.T) { + t.Parallel() + s := newTestSite(t) + _, err := s.NewPageFrom(strings.NewReader(emptyPage), "test") + if err != nil { + t.Fatalf("Empty files should not trigger an error. Should be able to touch a file while watching without erroring out.") + } +} + +func checkPageTitle(t *testing.T, page *Page, title string) { + if page.Title != title { + t.Fatalf("Page title is: %s. Expected %s", page.Title, title) + } +} + +func checkPageContent(t *testing.T, page *Page, content string, msg ...interface{}) { + a := normalizeContent(content) + b := normalizeContent(string(page.Content)) if a != b { t.Fatalf("Page content is:\n%q\nExpected:\n%q (%q)", b, a, msg) } @@ -305,31 +491,42 @@ func normalizeContent(c string) string { return strings.TrimSpace(norm) } -func checkPageTOC(t *testing.T, page page.Page, toc string) { - t.Helper() - if page.TableOfContents(context.Background()) != template.HTML(toc) { - t.Fatalf("Page TableOfContents is:\n%q.\nExpected %q", page.TableOfContents(context.Background()), toc) +func checkPageTOC(t *testing.T, page *Page, toc string) { + if page.TableOfContents != template.HTML(toc) { + t.Fatalf("Page TableOfContents is: %q.\nExpected %q", page.TableOfContents, toc) } } -func checkPageSummary(t *testing.T, page page.Page, summary string, msg ...any) { - s := string(page.Summary(context.Background())) - a := normalizeContent(s) +func checkPageSummary(t *testing.T, page *Page, summary string, msg ...interface{}) { + a := normalizeContent(string(page.Summary)) b := normalizeContent(summary) if a != b { t.Fatalf("Page summary is:\n%q.\nExpected\n%q (%q)", a, b, msg) } } -func checkPageType(t *testing.T, page page.Page, pageType string) { +func checkPageType(t *testing.T, page *Page, pageType string) { if page.Type() != pageType { t.Fatalf("Page type is: %s. Expected: %s", page.Type(), pageType) } } -func checkPageDate(t *testing.T, page page.Page, time time.Time) { - if page.Date() != time { - t.Fatalf("Page date is: %s. Expected: %s", page.Date(), time) +func checkPageDate(t *testing.T, page *Page, time time.Time) { + if page.Date != time { + t.Fatalf("Page date is: %s. Expected: %s", page.Date, time) + } +} + +func checkTruncation(t *testing.T, page *Page, shouldBe bool, msg string) { + if page.Summary == "" { + t.Fatal("page has no summary, can not check truncation") + } + if page.Truncated != shouldBe { + if shouldBe { + t.Fatalf("page wasn't truncated: %s", msg) + } else { + t.Fatalf("page was truncated: %s", msg) + } } } @@ -339,7 +536,7 @@ func normalizeExpected(ext, str string) string { default: return str case "html": - return strings.Trim(tpl.StripHTML(str), " ") + return strings.Trim(helpers.StripHTML(str), " ") case "ad": paragraphs := strings.Split(str, "</p>") expected := "" @@ -349,26 +546,24 @@ func normalizeExpected(ext, str string) string { } expected += fmt.Sprintf("<div class=\"paragraph\">\n%s</p></div>\n", para) } - return expected case "rst": - if str == "" { - return "<div class=\"document\"></div>" - } return fmt.Sprintf("<div class=\"document\">\n\n\n%s</div>", str) } } func testAllMarkdownEnginesForPages(t *testing.T, - assertFunc func(t *testing.T, ext string, pages page.Pages), settings map[string]any, pageSources ...string, -) { + assertFunc func(t *testing.T, ext string, pages Pages), settings map[string]interface{}, pageSources ...string) { + engines := []struct { ext string shouldExecute func() bool }{ {"md", func() bool { return true }}, - {"ad", func() bool { return asciidocext.Supports() }}, - {"rst", func() bool { return rst.Supports() }}, + {"mmark", func() bool { return true }}, + {"ad", func() bool { return helpers.HasAsciidoc() }}, + // TODO(bep) figure a way to include this without too much work.{"html", func() bool { return true }}, + {"rst", func() bool { return helpers.HasRst() }}, } for _, e := range engines { @@ -376,51 +571,125 @@ func testAllMarkdownEnginesForPages(t *testing.T, continue } - t.Run(e.ext, func(t *testing.T) { - cfg := config.New() + cfg, fs := newTestCfg() + + if settings != nil { for k, v := range settings { cfg.Set(k, v) } + } - if s := cfg.GetString("contentDir"); s != "" && s != "content" { - panic("contentDir must be set to 'content' for this test") - } + contentDir := "content" - files := ` --- hugo.toml -- -[security] -[security.exec] -allow = ['^python$', '^rst2html.*', '^asciidoctor$'] -` + if s := cfg.GetString("contentDir"); s != "" { + contentDir = s + } - for i, source := range pageSources { - files += fmt.Sprintf("-- content/p%d.%s --\n%s\n", i, e.ext, source) - } - homePath := fmt.Sprintf("_index.%s", e.ext) - files += fmt.Sprintf("-- content/%s --\n%s\n", homePath, homePage) + var fileSourcePairs []string - b := NewIntegrationTestBuilder( - IntegrationTestConfig{ - T: t, - TxtarString: files, - NeedsOsFS: true, - BaseCfg: cfg, - }, - ).Build() + for i, source := range pageSources { + fileSourcePairs = append(fileSourcePairs, fmt.Sprintf("p%d.%s", i, e.ext), source) + } - s := b.H.Sites[0] + for i := 0; i < len(fileSourcePairs); i += 2 { + writeSource(t, fs, filepath.Join(contentDir, fileSourcePairs[i]), fileSourcePairs[i+1]) + } - b.Assert(len(s.RegularPages()), qt.Equals, len(pageSources)) + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) - assertFunc(t, e.ext, s.RegularPages()) + require.Len(t, s.RegularPages, len(pageSources)) - home := s.Home() - b.Assert(home, qt.Not(qt.IsNil)) - b.Assert(home.File().Path(), qt.Equals, homePath) - b.Assert(content(home), qt.Contains, "Home Page Content") - }) + assertFunc(t, e.ext, s.RegularPages) } + +} + +func TestCreateNewPage(t *testing.T) { + t.Parallel() + assertFunc := func(t *testing.T, ext string, pages Pages) { + p := pages[0] + + // issue #2290: Path is relative to the content dir and will continue to be so. + require.Equal(t, filepath.FromSlash(fmt.Sprintf("p0.%s", ext)), p.Path()) + assert.False(t, p.IsHome()) + checkPageTitle(t, p, "Simple") + checkPageContent(t, p, normalizeExpected(ext, "<p>Simple Page</p>\n")) + checkPageSummary(t, p, "Simple Page") + checkPageType(t, p, "page") + checkTruncation(t, p, false, "simple short page") + } + + settings := map[string]interface{}{ + "contentDir": "mycontent", + } + + testAllMarkdownEnginesForPages(t, assertFunc, settings, simplePage) +} + +func TestSplitSummaryAndContent(t *testing.T) { + t.Parallel() + for i, this := range []struct { + markup string + content string + expectedSummary string + expectedContent string + }{ + {"markdown", `<p>Summary Same LineHUGOMORE42</p> + +<p>Some more text</p>`, "<p>Summary Same Line</p>", "<p>Summary Same Line</p>\n\n<p>Some more text</p>"}, + {"asciidoc", `<div class="paragraph"><p>sn</p></div><div class="paragraph"><p>HUGOMORE42Some more text</p></div>`, + "<div class=\"paragraph\"><p>sn</p></div>", + "<div class=\"paragraph\"><p>sn</p></div><div class=\"paragraph\"><p>Some more text</p></div>"}, + {"rst", + "<div class=\"document\"><p>Summary Next Line</p><p>HUGOMORE42Some more text</p></div>", + "<div class=\"document\"><p>Summary Next Line</p></div>", + "<div class=\"document\"><p>Summary Next Line</p><p>Some more text</p></div>"}, + {"markdown", "<p>a</p><p>b</p><p>HUGOMORE42c</p>", "<p>a</p><p>b</p>", "<p>a</p><p>b</p><p>c</p>"}, + {"markdown", "<p>a</p><p>b</p><p>cHUGOMORE42</p>", "<p>a</p><p>b</p><p>c</p>", "<p>a</p><p>b</p><p>c</p>"}, + {"markdown", "<p>a</p><p>bHUGOMORE42</p><p>c</p>", "<p>a</p><p>b</p>", "<p>a</p><p>b</p><p>c</p>"}, + {"markdown", "<p>aHUGOMORE42</p><p>b</p><p>c</p>", "<p>a</p>", "<p>a</p><p>b</p><p>c</p>"}, + {"markdown", " HUGOMORE42 ", "", ""}, + {"markdown", "HUGOMORE42", "", ""}, + {"markdown", "<p>HUGOMORE42", "<p>", "<p>"}, + {"markdown", "HUGOMORE42<p>", "", "<p>"}, + {"markdown", "\n\n<p>HUGOMORE42</p>\n", "<p></p>", "<p></p>"}, + // Issue #2586 + // Note: Hugo will not split mid-sentence but will look for the closest + // paragraph end marker. This may be a change from Hugo 0.16, but it makes sense. + {"markdown", `<p>this is an example HUGOMORE42of the issue.</p>`, + "<p>this is an example of the issue.</p>", + "<p>this is an example of the issue.</p>"}, + // Issue: #2538 + {"markdown", fmt.Sprintf(` <p class="lead">%s</p>HUGOMORE42<p>%s</p> +`, + strings.Repeat("A", 10), strings.Repeat("B", 31)), + fmt.Sprintf(`<p class="lead">%s</p>`, strings.Repeat("A", 10)), + fmt.Sprintf(`<p class="lead">%s</p><p>%s</p>`, strings.Repeat("A", 10), strings.Repeat("B", 31)), + }, + } { + + sc, err := splitUserDefinedSummaryAndContent(this.markup, []byte(this.content)) + + require.NoError(t, err) + require.NotNil(t, sc, fmt.Sprintf("[%d] Nil %s", i, this.markup)) + require.Equal(t, this.expectedSummary, string(sc.summary), fmt.Sprintf("[%d] Summary markup %s", i, this.markup)) + require.Equal(t, this.expectedContent, string(sc.content), fmt.Sprintf("[%d] Content markup %s", i, this.markup)) + } +} + +func TestPageWithDelimiter(t *testing.T) { + t.Parallel() + assertFunc := func(t *testing.T, ext string, pages Pages) { + p := pages[0] + checkPageTitle(t, p, "Simple") + checkPageContent(t, p, normalizeExpected(ext, "<p>Summary Next Line</p>\n\n<p>Some more text</p>\n"), ext) + checkPageSummary(t, p, normalizeExpected(ext, "<p>Summary Next Line</p>"), ext) + checkPageType(t, p, "page") + checkTruncation(t, p, true, "page with summary delimiter") + } + + testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithSummaryDelimiter) } // Issue #1076 @@ -428,390 +697,133 @@ func TestPageWithDelimiterForMarkdownThatCrossesBorder(t *testing.T) { t.Parallel() cfg, fs := newTestCfg() - c := qt.New(t) - configs, err := loadTestConfigFromProvider(cfg) - c.Assert(err, qt.IsNil) - writeSource(t, fs, filepath.Join("content", "simple.md"), simplePageWithSummaryDelimiterAndMarkdownThatCrossesBorder) - s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{SkipRender: true}) + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) - c.Assert(len(s.RegularPages()), qt.Equals, 1) + require.Len(t, s.RegularPages, 1) - p := s.RegularPages()[0] + p := s.RegularPages[0] - if p.Summary(context.Background()) != template.HTML( - "<p>The <a href=\"http://gohugo.io/\">best static site generator</a>.<sup id=\"fnref:1\"><a href=\"#fn:1\" class=\"footnote-ref\" role=\"doc-noteref\">1</a></sup></p>") { - t.Fatalf("Got summary:\n%q", p.Summary(context.Background())) + if p.Summary != template.HTML("<p>The <a href=\"http://gohugo.io/\">best static site generator</a>.<sup class=\"footnote-ref\" id=\"fnref:1\"><a rel=\"footnote\" href=\"#fn:1\">1</a></sup>\n</p>") { + t.Fatalf("Got summary:\n%q", p.Summary) } - cnt := content(p) - if cnt != "<p>The <a href=\"http://gohugo.io/\">best static site generator</a>.<sup id=\"fnref:1\"><a href=\"#fn:1\" class=\"footnote-ref\" role=\"doc-noteref\">1</a></sup></p>\n<div class=\"footnotes\" role=\"doc-endnotes\">\n<hr>\n<ol>\n<li id=\"fn:1\">\n<p>Many people say so. <a href=\"#fnref:1\" class=\"footnote-backref\" role=\"doc-backlink\">↩︎</a></p>\n</li>\n</ol>\n</div>" { - t.Fatalf("Got content:\n%q", cnt) + if p.Content != template.HTML("<p>The <a href=\"http://gohugo.io/\">best static site generator</a>.<sup class=\"footnote-ref\" id=\"fnref:1\"><a rel=\"footnote\" href=\"#fn:1\">1</a></sup>\n</p>\n<div class=\"footnotes\">\n\n<hr />\n\n<ol>\n<li id=\"fn:1\">Many people say so.\n <a class=\"footnote-return\" href=\"#fnref:1\"><sup>[return]</sup></a></li>\n</ol>\n</div>") { + t.Fatalf("Got content:\n%q", p.Content) } } -func TestPageDatesTerms(t *testing.T) { +// Issue #2601 +func TestPageRawContent(t *testing.T) { t.Parallel() + cfg, fs := newTestCfg() - files := ` --- hugo.toml -- -baseURL = "http://example.com/" --- content/p1.md -- + writeSource(t, fs, filepath.Join("content", "raw.md"), `--- +title: Raw --- -title: p1 -date: 2022-01-15 -lastMod: 2022-01-16 -tags: ["a", "b"] -categories: ["c", "d"] ---- -p1 --- content/p2.md -- ---- -title: p2 -date: 2017-01-16 -lastMod: 2017-01-17 -tags: ["a", "c"] -categories: ["c", "e"] ---- -p2 --- layouts/_default/list.html -- -{{ .Title }}|Date: {{ .Date.Format "2006-01-02" }}|Lastmod: {{ .Lastmod.Format "2006-01-02" }}| +**Raw**`) -` - b := Test(t, files) + writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), `{{ .RawContent }}`) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) + + require.Len(t, s.RegularPages, 1) + p := s.RegularPages[0] + + require.Contains(t, p.RawContent(), "**Raw**") - b.AssertFileContent("public/categories/index.html", "Categories|Date: 2022-01-15|Lastmod: 2022-01-16|") - b.AssertFileContent("public/categories/c/index.html", "C|Date: 2022-01-15|Lastmod: 2022-01-16|") - b.AssertFileContent("public/categories/e/index.html", "E|Date: 2017-01-16|Lastmod: 2017-01-17|") - b.AssertFileContent("public/tags/index.html", "Tags|Date: 2022-01-15|Lastmod: 2022-01-16|") - b.AssertFileContent("public/tags/a/index.html", "A|Date: 2022-01-15|Lastmod: 2022-01-16|") - b.AssertFileContent("public/tags/c/index.html", "C|Date: 2017-01-16|Lastmod: 2017-01-17|") } -func TestPageDatesAllKinds(t *testing.T) { +func TestPageWithShortCodeInSummary(t *testing.T) { t.Parallel() - - pageContent := ` ---- -title: Page -date: 2017-01-15 -tags: ["hugo"] -categories: ["cool stuff"] ---- -` - - b := newTestSitesBuilder(t) - b.WithSimpleConfigFile().WithContent("page.md", pageContent) - b.WithContent("blog/page.md", pageContent) - - b.CreateSites().Build(BuildCfg{}) - - b.Assert(len(b.H.Sites), qt.Equals, 1) - s := b.H.Sites[0] - - checkDate := func(t time.Time, msg string) { - b.Helper() - b.Assert(t.Year(), qt.Equals, 2017, qt.Commentf(msg)) - } - - checkDated := func(d resource.Dated, msg string) { - b.Helper() - checkDate(d.Date(), "date: "+msg) - checkDate(d.Lastmod(), "lastmod: "+msg) - } - for _, p := range s.Pages() { - checkDated(p, p.Kind()) - } - checkDate(s.Lastmod(), "site") -} - -func TestPageDatesSections(t *testing.T) { - t.Parallel() - - b := newTestSitesBuilder(t) - b.WithSimpleConfigFile().WithContent("no-index/page.md", ` ---- -title: Page -date: 2017-01-15 ---- -`, "with-index-no-date/_index.md", `--- -title: No Date ---- - -`, - // https://github.com/gohugoio/hugo/issues/5854 - "with-index-date/_index.md", `--- -title: Date -date: 2018-01-15 ---- - -`, "with-index-date/p1.md", `--- -title: Date -date: 2018-01-15 ---- - -`, "with-index-date/p1.md", `--- -title: Date -date: 2018-01-15 ---- - -`) - - for i := 1; i <= 20; i++ { - b.WithContent(fmt.Sprintf("main-section/p%d.md", i), `--- -title: Date -date: 2012-01-12 ---- - -`) - } - - b.CreateSites().Build(BuildCfg{}) - - b.Assert(len(b.H.Sites), qt.Equals, 1) - s := b.H.Sites[0] - - checkDate := func(p page.Page, year int) { - b.Assert(p.Date().Year(), qt.Equals, year) - b.Assert(p.Lastmod().Year(), qt.Equals, year) - } - - checkDate(s.getPageOldVersion("/"), 2018) - checkDate(s.getPageOldVersion("/no-index"), 2017) - b.Assert(s.getPageOldVersion("/with-index-no-date").Date().IsZero(), qt.Equals, true) - checkDate(s.getPageOldVersion("/with-index-date"), 2018) - - b.Assert(s.Site().Lastmod().Year(), qt.Equals, 2018) -} - -func TestPageSummary(t *testing.T) { - t.Parallel() - assertFunc := func(t *testing.T, ext string, pages page.Pages) { - p := pages[0] - checkPageTitle(t, p, "SimpleWithoutSummaryDelimiter") - // Source is not Asciidoctor- or RST-compatible so don't test them - if ext != "ad" && ext != "rst" { - checkPageContent(t, p, normalizeExpected(ext, "<p><a href=\"https://lipsum.com/\">Lorem ipsum</a> dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>\n\n<p>Additional text.</p>\n\n<p>Further text.</p>\n"), ext) - checkPageSummary(t, p, normalizeExpected(ext, "<p><a href=\"https://lipsum.com/\">Lorem ipsum</a> dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p><p>Additional text.</p>"), ext) - } - checkPageType(t, p, "page") - } - - testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithoutSummaryDelimiter) -} - -func TestPageWithDelimiter(t *testing.T) { - t.Parallel() - assertFunc := func(t *testing.T, ext string, pages page.Pages) { + assertFunc := func(t *testing.T, ext string, pages Pages) { p := pages[0] checkPageTitle(t, p, "Simple") - checkPageContent(t, p, normalizeExpected(ext, "<p>Summary Next Line</p>\n\n<p>Some more text</p>\n"), ext) - checkPageSummary(t, p, normalizeExpected(ext, "<p>Summary Next Line</p>"), ext) + checkPageContent(t, p, normalizeExpected(ext, "<p>Summary Next Line. \n<figure >\n \n <img src=\"/not/real\" />\n \n \n</figure>\n.\nMore text here.</p>\n\n<p>Some more text</p>\n")) + checkPageSummary(t, p, "Summary Next Line. . More text here. Some more text") checkPageType(t, p, "page") } - testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithSummaryDelimiter) + testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithShortcodeInSummary) } -func TestPageWithSummaryParameter(t *testing.T) { +func TestPageWithEmbeddedScriptTag(t *testing.T) { t.Parallel() - assertFunc := func(t *testing.T, ext string, pages page.Pages) { + assertFunc := func(t *testing.T, ext string, pages Pages) { p := pages[0] - checkPageTitle(t, p, "SimpleWithSummaryParameter") - checkPageContent(t, p, normalizeExpected(ext, "<p>Some text.</p>\n\n<p>Some more text.</p>\n"), ext) - // Summary is not Asciidoctor- or RST-compatible so don't test them - if ext != "ad" && ext != "rst" { - checkPageSummary(t, p, normalizeExpected(ext, "Page with summary parameter and <a href=\"http://www.example.com/\">a link</a>"), ext) + if ext == "ad" || ext == "rst" { + // TOD(bep) + return } - checkPageType(t, p, "page") + checkPageContent(t, p, "<script type='text/javascript'>alert('the script tags are still there, right?');</script>\n", ext) } - testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithSummaryParameter) + testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithEmbeddedScript) } -// Issue #3854 -// Also see https://github.com/gohugoio/hugo/issues/3977 -func TestPageWithDateFields(t *testing.T) { - c := qt.New(t) - pageWithDate := `--- -title: P%d -weight: %d -%s: 2017-10-13 ---- -Simple Page With Some Date` - - hasDate := func(p page.Page) bool { - return p.Date().Year() == 2017 - } - - datePage := func(field string, weight int) string { - return fmt.Sprintf(pageWithDate, weight, weight, field) - } - +func TestPageWithAdditionalExtension(t *testing.T) { t.Parallel() - assertFunc := func(t *testing.T, ext string, pages page.Pages) { - c.Assert(len(pages) > 0, qt.Equals, true) - for _, p := range pages { - c.Assert(hasDate(p), qt.Equals, true) - } - } + cfg, fs := newTestCfg() - fields := []string{"date", "publishdate", "pubdate", "published"} - pageContents := make([]string, len(fields)) - for i, field := range fields { - pageContents[i] = datePage(field, i+1) - } + writeSource(t, fs, filepath.Join("content", "simple.md"), simplePageWithAdditionalExtension) - testAllMarkdownEnginesForPages(t, assertFunc, nil, pageContents...) -} + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) -func TestPageRawContent(t *testing.T) { - files := ` --- hugo.toml -- --- content/basic.md -- ---- -title: "basic" ---- -**basic** --- content/empty.md -- ---- -title: "empty" ---- --- layouts/_default/single.html -- -|{{ .RawContent }}| -` + require.Len(t, s.RegularPages, 1) - b := Test(t, files) + p := s.RegularPages[0] - b.AssertFileContent("public/basic/index.html", "|**basic**|") - b.AssertFileContent("public/empty/index.html", "! title") + checkPageContent(t, p, "<p>first line.<br />\nsecond line.</p>\n\n<p>fourth line.</p>\n") } func TestTableOfContents(t *testing.T) { - c := qt.New(t) + cfg, fs := newTestCfg() - configs, err := loadTestConfigFromProvider(cfg) - c.Assert(err, qt.IsNil) writeSource(t, fs, filepath.Join("content", "tocpage.md"), pageWithToC) - s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{SkipRender: true}) + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) - c.Assert(len(s.RegularPages()), qt.Equals, 1) + require.Len(t, s.RegularPages, 1) - p := s.RegularPages()[0] + p := s.RegularPages[0] - checkPageContent(t, p, "<p>For some moments the old man did not reply. He stood with bowed head, buried in deep thought. But at last he spoke.</p><h2 id=\"aa\">AA</h2> <p>I have no idea, of course, how long it took me to reach the limit of the plain, but at last I entered the foothills, following a pretty little canyon upward toward the mountains. Beside me frolicked a laughing brooklet, hurrying upon its noisy way down to the silent sea. In its quieter pools I discovered many small fish, of four-or five-pound weight I should imagine. In appearance, except as to size and color, they were not unlike the whale of our own seas. As I watched them playing about I discovered, not only that they suckled their young, but that at intervals they rose to the surface to breathe as well as to feed upon certain grasses and a strange, scarlet lichen which grew upon the rocks just above the water line.</p><h3 id=\"aaa\">AAA</h3> <p>I remember I felt an extraordinary persuasion that I was being played with, that presently, when I was upon the very verge of safety, this mysterious death–as swift as the passage of light–would leap after me from the pit about the cylinder and strike me down. ## BB</p><h3 id=\"bbb\">BBB</h3> <p>“You’re a great Granser,” he cried delightedly, “always making believe them little marks mean something.”</p>") - checkPageTOC(t, p, "<nav id=\"TableOfContents\">\n <ul>\n <li><a href=\"#aa\">AA</a>\n <ul>\n <li><a href=\"#aaa\">AAA</a></li>\n <li><a href=\"#bbb\">BBB</a></li>\n </ul>\n </li>\n </ul>\n</nav>") + checkPageContent(t, p, "\n\n<p>For some moments the old man did not reply. He stood with bowed head, buried in deep thought. But at last he spoke.</p>\n\n<h2 id=\"aa\">AA</h2>\n\n<p>I have no idea, of course, how long it took me to reach the limit of the plain,\nbut at last I entered the foothills, following a pretty little canyon upward\ntoward the mountains. Beside me frolicked a laughing brooklet, hurrying upon\nits noisy way down to the silent sea. In its quieter pools I discovered many\nsmall fish, of four-or five-pound weight I should imagine. In appearance,\nexcept as to size and color, they were not unlike the whale of our own seas. As\nI watched them playing about I discovered, not only that they suckled their\nyoung, but that at intervals they rose to the surface to breathe as well as to\nfeed upon certain grasses and a strange, scarlet lichen which grew upon the\nrocks just above the water line.</p>\n\n<h3 id=\"aaa\">AAA</h3>\n\n<p>I remember I felt an extraordinary persuasion that I was being played with,\nthat presently, when I was upon the very verge of safety, this mysterious\ndeath–as swift as the passage of light–would leap after me from the pit about\nthe cylinder and strike me down. ## BB</p>\n\n<h3 id=\"bbb\">BBB</h3>\n\n<p>“You’re a great Granser,” he cried delightedly, “always making believe them little marks mean something.”</p>\n") + checkPageTOC(t, p, "<nav id=\"TableOfContents\">\n<ul>\n<li>\n<ul>\n<li><a href=\"#aa\">AA</a>\n<ul>\n<li><a href=\"#aaa\">AAA</a></li>\n<li><a href=\"#bbb\">BBB</a></li>\n</ul></li>\n</ul></li>\n</ul>\n</nav>") } func TestPageWithMoreTag(t *testing.T) { t.Parallel() - assertFunc := func(t *testing.T, ext string, pages page.Pages) { + assertFunc := func(t *testing.T, ext string, pages Pages) { p := pages[0] checkPageTitle(t, p, "Simple") checkPageContent(t, p, normalizeExpected(ext, "<p>Summary Same Line</p>\n\n<p>Some more text</p>\n")) checkPageSummary(t, p, normalizeExpected(ext, "<p>Summary Same Line</p>")) checkPageType(t, p, "page") + } testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithSummaryDelimiterSameLine) } -func TestSummaryInFrontMatter(t *testing.T) { - t.Parallel() - Test(t, ` --- hugo.toml -- --- content/simple.md -- ---- -title: Simple -summary: "Front **matter** summary" ---- -Simple Page --- layouts/_default/single.html -- -Summary: {{ .Summary }}|Truncated: {{ .Truncated }}| +func TestPageWithMoreTagOnlySummary(t *testing.T) { -`).AssertFileContent("public/simple/index.html", "Summary: Front <strong>matter</strong> summary|", "Truncated: false") -} + assertFunc := func(t *testing.T, ext string, pages Pages) { + p := pages[0] + checkTruncation(t, p, false, "page with summary delimiter at end") + } -func TestSummaryManualSplit(t *testing.T) { - t.Parallel() - Test(t, ` --- hugo.toml -- --- content/simple.md -- ---- -title: Simple ---- -This is **summary**. -<!--more--> -This is **content**. --- layouts/_default/single.html -- -Summary: {{ .Summary }}|Truncated: {{ .Truncated }}| -Content: {{ .Content }}| - -`).AssertFileContent("public/simple/index.html", - "Summary: <p>This is <strong>summary</strong>.</p>|", - "Truncated: true|", - "Content: <p>This is <strong>summary</strong>.</p>\n<p>This is <strong>content</strong>.</p>|", - ) -} - -func TestSummaryManualSplitHTML(t *testing.T) { - t.Parallel() - Test(t, ` --- hugo.toml -- --- content/simple.html -- ---- -title: Simple ---- -<div> -This is <b>summary</b>. -</div> -<!--more--> -<div> -This is <b>content</b>. -</div> --- layouts/_default/single.html -- -Summary: {{ .Summary }}|Truncated: {{ .Truncated }}| -Content: {{ .Content }}| - -`).AssertFileContent("public/simple/index.html", "Summary: <div>\nThis is <b>summary</b>.\n</div>\n|Truncated: true|\nContent: \n\n<div>\nThis is <b>content</b>.\n</div>|") -} - -func TestSummaryAuto(t *testing.T) { - t.Parallel() - Test(t, ` --- hugo.toml -- -summaryLength = 10 --- content/simple.md -- ---- -title: Simple ---- -This is **summary**. -This is **more summary**. -This is *even more summary**. -This is **more summary**. - -This is **content**. --- layouts/_default/single.html -- -Summary: {{ .Summary }}|Truncated: {{ .Truncated }}| -Content: {{ .Content }}| - -`).AssertFileContent("public/simple/index.html", - "Summary: <p>This is <strong>summary</strong>.\nThis is <strong>more summary</strong>.\nThis is <em>even more summary</em>*.\nThis is <strong>more summary</strong>.</p>|", - "Truncated: true|", - "Content: <p>This is <strong>summary</strong>.") + testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithSummaryDelimiterOnlySummary) } // #2973 func TestSummaryWithHTMLTagsOnNextLine(t *testing.T) { - assertFunc := func(t *testing.T, ext string, pages page.Pages) { - c := qt.New(t) + + assertFunc := func(t *testing.T, ext string, pages Pages) { p := pages[0] - s := string(p.Summary(context.Background())) - c.Assert(s, qt.Contains, "Happy new year everyone!") - c.Assert(s, qt.Not(qt.Contains), "User interface") + require.Contains(t, p.Summary, "Happy new year everyone!") + require.NotContains(t, p.Summary, "User interface") } testAllMarkdownEnginesForPages(t, assertFunc, nil, `--- @@ -828,338 +840,28 @@ Here is the last report for commits in the year 2016. It covers hrev50718-hrev50 `) } -// Issue 9383 -func TestRenderStringForRegularPageTranslations(t *testing.T) { - c := qt.New(t) - b := newTestSitesBuilder(t) - b.WithLogger(loggers.NewDefault()) - - b.WithConfigFile("toml", - `baseurl = "https://example.org/" -title = "My Site" - -defaultContentLanguage = "ru" -defaultContentLanguageInSubdir = true - -[languages.ru] -contentDir = 'content/ru' -weight = 1 - -[languages.en] -weight = 2 -contentDir = 'content/en' - -[outputs] -home = ["HTML", "JSON"]`) - - b.WithTemplates("index.html", ` -{{- range .Site.Home.Translations -}} - <p>{{- .RenderString "foo" -}}</p> -{{- end -}} -{{- range .Site.Home.AllTranslations -}} - <p>{{- .RenderString "bar" -}}</p> -{{- end -}} -`, "_default/single.html", - `{{ .Content }}`, - "index.json", - `{"Title": "My Site"}`, - ) - - b.WithContent( - "ru/a.md", - "", - "en/a.md", - "", - ) - - err := b.BuildE(BuildCfg{}) - c.Assert(err, qt.Equals, nil) - - b.AssertFileContent("public/ru/index.html", ` -<p>foo</p> -<p>foo</p> -<p>bar</p> -<p>bar</p> -`) - - b.AssertFileContent("public/en/index.html", ` -<p>foo</p> -<p>foo</p> -<p>bar</p> -<p>bar</p> -`) -} - -// Issue 8919 -func TestContentProviderWithCustomOutputFormat(t *testing.T) { - b := newTestSitesBuilder(t) - b.WithLogger(loggers.NewDefault()) - b.WithConfigFile("toml", `baseURL = 'http://example.org/' -title = 'My New Hugo Site' - -timeout = 600000 # ten minutes in case we want to pause and debug - -defaultContentLanguage = "en" - -[languages] - [languages.en] - title = "Repro" - languageName = "English" - contentDir = "content/en" - - [languages.zh_CN] - title = "Repro" - languageName = "简体中文" - contentDir = "content/zh_CN" - -[outputFormats] - [outputFormats.metadata] - baseName = "metadata" - mediaType = "text/html" - isPlainText = true - notAlternative = true - -[outputs] - home = ["HTML", "metadata"]`) - - b.WithTemplates("home.metadata.html", `<h2>Translations metadata</h2> -<ul> -{{ $p := .Page }} -{{ range $p.Translations}} -<li>Title: {{ .Title }}, {{ .Summary }}</li> -<li>Content: {{ .Content }}</li> -<li>Plain: {{ .Plain }}</li> -<li>PlainWords: {{ .PlainWords }}</li> -<li>Summary: {{ .Summary }}</li> -<li>Truncated: {{ .Truncated }}</li> -<li>FuzzyWordCount: {{ .FuzzyWordCount }}</li> -<li>ReadingTime: {{ .ReadingTime }}</li> -<li>Len: {{ .Len }}</li> -{{ end }} -</ul>`) - - b.WithTemplates("_default/baseof.html", `<html> - -<body> - {{ block "main" . }}{{ end }} -</body> - -</html>`) - - b.WithTemplates("_default/home.html", `{{ define "main" }} -<h2>Translations</h2> -<ul> -{{ $p := .Page }} -{{ range $p.Translations}} -<li>Title: {{ .Title }}, {{ .Summary }}</li> -<li>Content: {{ .Content }}</li> -<li>Plain: {{ .Plain }}</li> -<li>PlainWords: {{ .PlainWords }}</li> -<li>Summary: {{ .Summary }}</li> -<li>Truncated: {{ .Truncated }}</li> -<li>FuzzyWordCount: {{ .FuzzyWordCount }}</li> -<li>ReadingTime: {{ .ReadingTime }}</li> -<li>Len: {{ .Len }}</li> -{{ end }} -</ul> -{{ end }}`) - - b.WithContent("en/_index.md", `--- -title: Title (en) -summary: Summary (en) ---- - -Here is some content. -`) - - b.WithContent("zh_CN/_index.md", `--- -title: Title (zh) -summary: Summary (zh) ---- - -这是一些内容 -`) - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", `<html> - -<body> - -<h2>Translations</h2> -<ul> - - -<li>Title: Title (zh), Summary (zh)</li> -<li>Content: <p>这是一些内容</p> -</li> -<li>Plain: 这是一些内容 -</li> -<li>PlainWords: [这是一些内容]</li> -<li>Summary: Summary (zh)</li> -<li>Truncated: false</li> -<li>FuzzyWordCount: 100</li> -<li>ReadingTime: 1</li> -<li>Len: 26</li> - -</ul> - -</body> - -</html>`) - b.AssertFileContent("public/metadata.html", `<h2>Translations metadata</h2> -<ul> - - -<li>Title: Title (zh), Summary (zh)</li> -<li>Content: <p>这是一些内容</p> -</li> -<li>Plain: 这是一些内容 -</li> -<li>PlainWords: [这是一些内容]</li> -<li>Summary: Summary (zh)</li> -<li>Truncated: false</li> -<li>FuzzyWordCount: 100</li> -<li>ReadingTime: 1</li> -<li>Len: 26</li> - -</ul>`) - b.AssertFileContent("public/zh_cn/index.html", `<html> - -<body> - -<h2>Translations</h2> -<ul> - - -<li>Title: Title (en), Summary (en)</li> -<li>Content: <p>Here is some content.</p> -</li> -<li>Plain: Here is some content. -</li> -<li>PlainWords: [Here is some content.]</li> -<li>Summary: Summary (en)</li> -<li>Truncated: false</li> -<li>FuzzyWordCount: 100</li> -<li>ReadingTime: 1</li> -<li>Len: 29</li> - -</ul> - -</body> - -</html>`) - b.AssertFileContent("public/zh_cn/metadata.html", `<h2>Translations metadata</h2> -<ul> - - -<li>Title: Title (en), Summary (en)</li> -<li>Content: <p>Here is some content.</p> -</li> -<li>Plain: Here is some content. -</li> -<li>PlainWords: [Here is some content.]</li> -<li>Summary: Summary (en)</li> -<li>Truncated: false</li> -<li>FuzzyWordCount: 100</li> -<li>ReadingTime: 1</li> -<li>Len: 29</li> - -</ul>`) -} - func TestPageWithDate(t *testing.T) { t.Parallel() - c := qt.New(t) cfg, fs := newTestCfg() - configs, err := loadTestConfigFromProvider(cfg) - c.Assert(err, qt.IsNil) writeSource(t, fs, filepath.Join("content", "simple.md"), simplePageRFC3339Date) - s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{SkipRender: true}) + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) - c.Assert(len(s.RegularPages()), qt.Equals, 1) + require.Len(t, s.RegularPages, 1) - p := s.RegularPages()[0] + p := s.RegularPages[0] d, _ := time.Parse(time.RFC3339, "2013-05-17T16:59:30Z") checkPageDate(t, p, d) } -func TestPageWithFrontMatterConfig(t *testing.T) { - for _, dateHandler := range []string{":filename", ":fileModTime"} { - dateHandler := dateHandler - t.Run(fmt.Sprintf("dateHandler=%q", dateHandler), func(t *testing.T) { - t.Parallel() - c := qt.New(t) - cfg, fs := newTestCfg() - - pageTemplate := ` ---- -title: Page -weight: %d -lastMod: 2018-02-28 -%s ---- -Content -` - - cfg.Set("frontmatter", map[string]any{ - "date": []string{dateHandler, "date"}, - }) - configs, err := loadTestConfigFromProvider(cfg) - c.Assert(err, qt.IsNil) - - c1 := filepath.Join("content", "section", "2012-02-21-noslug.md") - c2 := filepath.Join("content", "section", "2012-02-22-slug.md") - - writeSource(t, fs, c1, fmt.Sprintf(pageTemplate, 1, "")) - writeSource(t, fs, c2, fmt.Sprintf(pageTemplate, 2, "slug: aslug")) - - c1fi, err := fs.Source.Stat(c1) - c.Assert(err, qt.IsNil) - c2fi, err := fs.Source.Stat(c2) - c.Assert(err, qt.IsNil) - - b := newTestSitesBuilderFromDepsCfg(t, deps.DepsCfg{Fs: fs, Configs: configs}).WithNothingAdded() - b.Build(BuildCfg{SkipRender: true}) - - s := b.H.Sites[0] - c.Assert(len(s.RegularPages()), qt.Equals, 2) - - noSlug := s.RegularPages()[0] - slug := s.RegularPages()[1] - - c.Assert(noSlug.Lastmod().Day(), qt.Equals, 28) - - switch strings.ToLower(dateHandler) { - case ":filename": - c.Assert(noSlug.Date().IsZero(), qt.Equals, false) - c.Assert(slug.Date().IsZero(), qt.Equals, false) - c.Assert(noSlug.Date().Year(), qt.Equals, 2012) - c.Assert(slug.Date().Year(), qt.Equals, 2012) - c.Assert(noSlug.Slug(), qt.Equals, "noslug") - c.Assert(slug.Slug(), qt.Equals, "aslug") - case ":filemodtime": - c.Assert(noSlug.Date().Year(), qt.Equals, c1fi.ModTime().Year()) - c.Assert(slug.Date().Year(), qt.Equals, c2fi.ModTime().Year()) - fallthrough - default: - c.Assert(noSlug.Slug(), qt.Equals, "") - c.Assert(slug.Slug(), qt.Equals, "aslug") - - } - }) - } -} - func TestWordCountWithAllCJKRunesWithoutHasCJKLanguage(t *testing.T) { t.Parallel() - assertFunc := func(t *testing.T, ext string, pages page.Pages) { + assertFunc := func(t *testing.T, ext string, pages Pages) { p := pages[0] - if p.WordCount(context.Background()) != 8 { - t.Fatalf("[%s] incorrect word count. expected %v, got %v", ext, 8, p.WordCount(context.Background())) + if p.WordCount() != 8 { + t.Fatalf("[%s] incorrect word count for content '%s'. expected %v, got %v", ext, p.plain, 8, p.WordCount()) } } @@ -1168,12 +870,12 @@ func TestWordCountWithAllCJKRunesWithoutHasCJKLanguage(t *testing.T) { func TestWordCountWithAllCJKRunesHasCJKLanguage(t *testing.T) { t.Parallel() - settings := map[string]any{"hasCJKLanguage": true} + settings := map[string]interface{}{"hasCJKLanguage": true} - assertFunc := func(t *testing.T, ext string, pages page.Pages) { + assertFunc := func(t *testing.T, ext string, pages Pages) { p := pages[0] - if p.WordCount(context.Background()) != 15 { - t.Fatalf("[%s] incorrect word count, expected %v, got %v", ext, 15, p.WordCount(context.Background())) + if p.WordCount() != 15 { + t.Fatalf("[%s] incorrect word count for content '%s'. expected %v, got %v", ext, p.plain, 15, p.WordCount()) } } testAllMarkdownEnginesForPages(t, assertFunc, settings, simplePageWithAllCJKRunes) @@ -1181,12 +883,17 @@ func TestWordCountWithAllCJKRunesHasCJKLanguage(t *testing.T) { func TestWordCountWithMainEnglishWithCJKRunes(t *testing.T) { t.Parallel() - settings := map[string]any{"hasCJKLanguage": true} + settings := map[string]interface{}{"hasCJKLanguage": true} - assertFunc := func(t *testing.T, ext string, pages page.Pages) { + assertFunc := func(t *testing.T, ext string, pages Pages) { p := pages[0] - if p.WordCount(context.Background()) != 74 { - t.Fatalf("[%s] incorrect word count, expected %v, got %v", ext, 74, p.WordCount(context.Background())) + if p.WordCount() != 74 { + t.Fatalf("[%s] incorrect word count for content '%s'. expected %v, got %v", ext, p.plain, 74, p.WordCount()) + } + + if p.Summary != simplePageWithMainEnglishWithCJKRunesSummary { + t.Fatalf("[%s] incorrect Summary for content '%s'. expected %v, got %v", ext, p.plain, + simplePageWithMainEnglishWithCJKRunesSummary, p.Summary) } } @@ -1195,43 +902,202 @@ func TestWordCountWithMainEnglishWithCJKRunes(t *testing.T) { func TestWordCountWithIsCJKLanguageFalse(t *testing.T) { t.Parallel() - settings := map[string]any{ + settings := map[string]interface{}{ "hasCJKLanguage": true, } - assertFunc := func(t *testing.T, ext string, pages page.Pages) { + assertFunc := func(t *testing.T, ext string, pages Pages) { p := pages[0] - if p.WordCount(context.Background()) != 75 { - t.Fatalf("[%s] incorrect word count for content '%s'. expected %v, got %v", ext, p.Plain(context.Background()), 74, p.WordCount(context.Background())) + if p.WordCount() != 75 { + t.Fatalf("[%s] incorrect word count for content '%s'. expected %v, got %v", ext, p.plain, 74, p.WordCount()) + } + + if p.Summary != simplePageWithIsCJKLanguageFalseSummary { + t.Fatalf("[%s] incorrect Summary for content '%s'. expected %v, got %v", ext, p.plain, + simplePageWithIsCJKLanguageFalseSummary, p.Summary) } } testAllMarkdownEnginesForPages(t, assertFunc, settings, simplePageWithIsCJKLanguageFalse) + } func TestWordCount(t *testing.T) { t.Parallel() - assertFunc := func(t *testing.T, ext string, pages page.Pages) { + assertFunc := func(t *testing.T, ext string, pages Pages) { p := pages[0] - if p.WordCount(context.Background()) != 483 { - t.Fatalf("[%s] incorrect word count. expected %v, got %v", ext, 483, p.WordCount(context.Background())) + if p.WordCount() != 483 { + t.Fatalf("[%s] incorrect word count. expected %v, got %v", ext, 483, p.WordCount()) } - if p.FuzzyWordCount(context.Background()) != 500 { - t.Fatalf("[%s] incorrect word count. expected %v, got %v", ext, 500, p.FuzzyWordCount(context.Background())) + if p.FuzzyWordCount() != 500 { + t.Fatalf("[%s] incorrect word count. expected %v, got %v", ext, 500, p.WordCount()) } - if p.ReadingTime(context.Background()) != 3 { - t.Fatalf("[%s] incorrect min read. expected %v, got %v", ext, 3, p.ReadingTime(context.Background())) + if p.ReadingTime() != 3 { + t.Fatalf("[%s] incorrect min read. expected %v, got %v", ext, 3, p.ReadingTime()) } + + checkTruncation(t, p, true, "long page") } testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithLongContent) } +func TestCreatePage(t *testing.T) { + t.Parallel() + var tests = []struct { + r string + }{ + {simplePageJSON}, + {simplePageJSONMultiple}, + //{strings.NewReader(SIMPLE_PAGE_JSON_COMPACT)}, + } + + for i, test := range tests { + s := newTestSite(t) + p, _ := s.NewPage("page") + if _, err := p.ReadFrom(strings.NewReader(test.r)); err != nil { + t.Fatalf("[%d] Unable to parse page: %s", i, err) + } + } +} + +func TestDegenerateInvalidFrontMatterShortDelim(t *testing.T) { + t.Parallel() + var tests = []struct { + r string + err string + }{ + {invalidFrontmatterShortDelimEnding, "unable to read frontmatter at filepos 45: EOF"}, + } + for _, test := range tests { + s := newTestSite(t) + p, _ := s.NewPage("invalid/front/matter/short/delim") + _, err := p.ReadFrom(strings.NewReader(test.r)) + checkError(t, err, test.err) + } +} + +func TestShouldRenderContent(t *testing.T) { + t.Parallel() + var tests = []struct { + text string + render bool + }{ + {invalidFrontMatterMissing, true}, + // TODO how to deal with malformed frontmatter. In this case it'll be rendered as markdown. + {invalidFrontmatterShortDelim, true}, + {renderNoFrontmatter, false}, + {contentWithCommentedFrontmatter, true}, + {contentWithCommentedTextFrontmatter, true}, + {contentWithCommentedLongFrontmatter, false}, + {contentWithCommentedLong2Frontmatter, true}, + } + + for _, test := range tests { + s := newTestSite(t) + p, _ := s.NewPage("render/front/matter") + _, err := p.ReadFrom(strings.NewReader(test.text)) + p = pageMust(p, err) + if p.IsRenderable() != test.render { + t.Errorf("expected p.IsRenderable() == %t, got %t", test.render, p.IsRenderable()) + } + } +} + +// Issue #768 +func TestCalendarParamsVariants(t *testing.T) { + t.Parallel() + s := newTestSite(t) + pageJSON, _ := s.NewPage("test/fileJSON.md") + _, _ = pageJSON.ReadFrom(strings.NewReader(pageWithCalendarJSONFrontmatter)) + + pageYAML, _ := s.NewPage("test/fileYAML.md") + _, _ = pageYAML.ReadFrom(strings.NewReader(pageWithCalendarYAMLFrontmatter)) + + pageTOML, _ := s.NewPage("test/fileTOML.md") + _, _ = pageTOML.ReadFrom(strings.NewReader(pageWithCalendarTOMLFrontmatter)) + + assert.True(t, compareObjects(pageJSON.Params, pageYAML.Params)) + assert.True(t, compareObjects(pageJSON.Params, pageTOML.Params)) + +} + +func TestDifferentFrontMatterVarTypes(t *testing.T) { + t.Parallel() + s := newTestSite(t) + page, _ := s.NewPage("test/file1.md") + _, _ = page.ReadFrom(strings.NewReader(pageWithVariousFrontmatterTypes)) + + dateval, _ := time.Parse(time.RFC3339, "1979-05-27T07:32:00Z") + if page.GetParam("a_string") != "bar" { + t.Errorf("frontmatter not handling strings correctly should be %s, got: %s", "bar", page.GetParam("a_string")) + } + if page.GetParam("an_integer") != 1 { + t.Errorf("frontmatter not handling ints correctly should be %s, got: %s", "1", page.GetParam("an_integer")) + } + if page.GetParam("a_float") != 1.3 { + t.Errorf("frontmatter not handling floats correctly should be %f, got: %s", 1.3, page.GetParam("a_float")) + } + if page.GetParam("a_bool") != false { + t.Errorf("frontmatter not handling bools correctly should be %t, got: %s", false, page.GetParam("a_bool")) + } + if page.GetParam("a_date") != dateval { + t.Errorf("frontmatter not handling dates correctly should be %s, got: %s", dateval, page.GetParam("a_date")) + } + param := page.GetParam("a_table") + if param == nil { + t.Errorf("frontmatter not handling tables correctly should be type of %v, got: type of %v", reflect.TypeOf(page.Params["a_table"]), reflect.TypeOf(param)) + } + if cast.ToStringMap(param)["a_key"] != "a_value" { + t.Errorf("frontmatter not handling values inside a table correctly should be %s, got: %s", "a_value", cast.ToStringMap(page.Params["a_table"])["a_key"]) + } +} + +func TestDegenerateInvalidFrontMatterLeadingWhitespace(t *testing.T) { + t.Parallel() + s := newTestSite(t) + p, _ := s.NewPage("invalid/front/matter/leading/ws") + _, err := p.ReadFrom(strings.NewReader(invalidFrontmatterLadingWs)) + if err != nil { + t.Fatalf("Unable to parse front matter given leading whitespace: %s", err) + } +} + +func TestSectionEvaluation(t *testing.T) { + t.Parallel() + s := newTestSite(t) + page, _ := s.NewPage(filepath.FromSlash("blue/file1.md")) + page.ReadFrom(strings.NewReader(simplePage)) + if page.Section() != "blue" { + t.Errorf("Section should be %s, got: %s", "blue", page.Section()) + } +} + +func TestSliceToLower(t *testing.T) { + t.Parallel() + tests := []struct { + value []string + expected []string + }{ + {[]string{"a", "b", "c"}, []string{"a", "b", "c"}}, + {[]string{"a", "B", "c"}, []string{"a", "b", "c"}}, + {[]string{"A", "B", "C"}, []string{"a", "b", "c"}}, + } + + for _, test := range tests { + res := helpers.SliceToLower(test.value) + for i, val := range res { + if val != test.expected[i] { + t.Errorf("Case mismatch. Expected %s, got %s", test.expected[i], res[i]) + } + } + } +} + func TestPagePaths(t *testing.T) { t.Parallel() - c := qt.New(t) siteParmalinksSetting := map[string]string{ "post": ":year/:month/:day/:title/", @@ -1255,8 +1121,6 @@ func TestPagePaths(t *testing.T) { for _, test := range tests { cfg, fs := newTestCfg() - configs, err := loadTestConfigFromProvider(cfg) - c.Assert(err, qt.IsNil) if test.hasPermalink { cfg.Set("permalinks", siteParmalinksSetting) @@ -1264,274 +1128,263 @@ func TestPagePaths(t *testing.T) { writeSource(t, fs, filepath.Join("content", filepath.FromSlash(test.path)), test.content) - s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{SkipRender: true}) - c.Assert(len(s.RegularPages()), qt.Equals, 1) + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) + require.Len(t, s.RegularPages, 1) } } -func TestTranslationKey(t *testing.T) { - files := ` --- hugo.toml -- -disableKinds = ["taxonomy", "term"] -defaultContentLanguage = "en" -defaultContentLanguageInSubdir = true -[languages] -[languages.en] -weight = 1 -[languages.nn] -weight = 2 --- content/sect/p1.en.md -- +var pageWithDraftAndPublished = `--- +title: broken +published: false +draft: true --- -translationkey: "adfasdf" -title: "p1 en" ---- --- content/sect/p1.nn.md -- ---- -translationkey: "adfasdf" -title: "p1 nn" ---- --- layouts/_default/single.html -- -Title: {{ .Title }}|TranslationKey: {{ .TranslationKey }}| -Translations: {{ range .Translations }}{{ .Language.Lang }}|{{ end }}| -AllTranslations: {{ range .AllTranslations }}{{ .Language.Lang }}|{{ end }}| - +some content ` - b := Test(t, files) - b.AssertFileContent("public/en/sect/p1/index.html", - "TranslationKey: adfasdf|", - "AllTranslations: en|nn||", - "Translations: nn||", - ) - b.AssertFileContent("public/nn/sect/p1/index.html", - "TranslationKey: adfasdf|", - "Translations: en||", - "AllTranslations: en|nn||", - ) -} - -func TestTranslationKeyTermPages(t *testing.T) { +func TestDraftAndPublishedFrontMatterError(t *testing.T) { t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ['home','rss','section','sitemap','taxonomy'] -defaultContentLanguage = 'en' -defaultContentLanguageInSubdir = true -[languages.en] -weight = 1 -[languages.pt] -weight = 2 -[taxonomies] -category = 'categories' --- layouts/_default/list.html -- -{{ .IsTranslated }}|{{ range .Translations }}{{ .RelPermalink }}|{{ end }} --- layouts/_default/single.html -- -{{ .Title }}| --- content/p1.en.md -- ---- -title: p1 (en) -categories: [music] ---- --- content/p1.pt.md -- ---- -title: p1 (pt) -categories: [música] ---- --- content/categories/music/_index.en.md -- ---- -title: music -translationKey: foo ---- --- content/categories/música/_index.pt.md -- ---- -title: música -translationKey: foo ---- -` - - b := Test(t, files) - b.AssertFileContent("public/en/categories/music/index.html", "true|/pt/categories/m%C3%BAsica/|") - b.AssertFileContent("public/pt/categories/música/index.html", "true|/en/categories/music/|") + s := newTestSite(t) + _, err := s.NewPageFrom(strings.NewReader(pageWithDraftAndPublished), "content/post/broken.md") + if err != ErrHasDraftAndPublished { + t.Errorf("expected ErrHasDraftAndPublished, was %#v", err) + } } -// Issue #11540. -func TestTranslationKeyResourceSharing(t *testing.T) { - files := ` --- hugo.toml -- -disableKinds = ["taxonomy", "term"] -defaultContentLanguage = "en" -defaultContentLanguageInSubdir = true -[languages] -[languages.en] -weight = 1 -[languages.nn] -weight = 2 --- content/sect/mybundle_en/index.en.md -- +var pagesWithPublishedFalse = `--- +title: okay +published: false --- -translationkey: "adfasdf" -title: "mybundle en" ---- --- content/sect/mybundle_en/f1.txt -- -f1.en --- content/sect/mybundle_en/f2.txt -- -f2.en --- content/sect/mybundle_nn/index.nn.md -- ---- -translationkey: "adfasdf" -title: "mybundle nn" ---- --- content/sect/mybundle_nn/f2.nn.txt -- -f2.nn --- layouts/_default/single.html -- -Title: {{ .Title }}|TranslationKey: {{ .TranslationKey }}| -Resources: {{ range .Resources }}{{ .RelPermalink }}|{{ .Content }}|{{ end }}| - +some content +` +var pageWithPublishedTrue = `--- +title: okay +published: true +--- +some content ` - b := Test(t, files) - b.AssertFileContent("public/en/sect/mybundle_en/index.html", - "TranslationKey: adfasdf|", - "Resources: /en/sect/mybundle_en/f1.txt|f1.en|/en/sect/mybundle_en/f2.txt|f2.en||", - ) - b.AssertFileContent("public/nn/sect/mybundle_nn/index.html", - "TranslationKey: adfasdf|", - "Title: mybundle nn|TranslationKey: adfasdf|\nResources: /en/sect/mybundle_en/f1.txt|f1.en|/nn/sect/mybundle_nn/f2.nn.txt|f2.nn||", - ) +func TestPublishedFrontMatter(t *testing.T) { + t.Parallel() + s := newTestSite(t) + p, err := s.NewPageFrom(strings.NewReader(pagesWithPublishedFalse), "content/post/broken.md") + if err != nil { + t.Fatalf("err during parse: %s", err) + } + if !p.Draft { + t.Errorf("expected true, got %t", p.Draft) + } + p, err = s.NewPageFrom(strings.NewReader(pageWithPublishedTrue), "content/post/broken.md") + if err != nil { + t.Fatalf("err during parse: %s", err) + } + if p.Draft { + t.Errorf("expected false, got %t", p.Draft) + } +} + +var pagesDraftTemplate = []string{`--- +title: "okay" +draft: %t +--- +some content +`, + `+++ +title = "okay" +draft = %t ++++ + +some content +`, +} + +func TestDraft(t *testing.T) { + t.Parallel() + s := newTestSite(t) + for _, draft := range []bool{true, false} { + for i, templ := range pagesDraftTemplate { + pageContent := fmt.Sprintf(templ, draft) + p, err := s.NewPageFrom(strings.NewReader(pageContent), "content/post/broken.md") + if err != nil { + t.Fatalf("err during parse: %s", err) + } + if p.Draft != draft { + t.Errorf("[%d] expected %t, got %t", i, draft, p.Draft) + } + } + } +} + +var pagesParamsTemplate = []string{`+++ +title = "okay" +draft = false +tags = [ "hugo", "web" ] +social= [ + [ "a", "#" ], + [ "b", "#" ], +] ++++ +some content +`, + `--- +title: "okay" +draft: false +tags: + - hugo + - web +social: + - - a + - "#" + - - b + - "#" +--- +some content +`, + `{ + "title": "okay", + "draft": false, + "tags": [ "hugo", "web" ], + "social": [ + [ "a", "#" ], + [ "b", "#" ] + ] +} +some content +`, +} + +func TestPageParams(t *testing.T) { + t.Parallel() + s := newTestSite(t) + wantedMap := map[string]interface{}{ + "tags": []string{"hugo", "web"}, + // Issue #2752 + "social": []interface{}{ + []interface{}{"a", "#"}, + []interface{}{"b", "#"}, + }, + } + + for i, c := range pagesParamsTemplate { + p, err := s.NewPageFrom(strings.NewReader(c), "content/post/params.md") + require.NoError(t, err, "err during parse", "#%d", i) + for key := range wantedMap { + assert.Equal(t, wantedMap[key], p.Params[key], "#%d", key) + } + } +} + +func TestTraverse(t *testing.T) { + exampleParams := `--- +rating: "5 stars" +tags: + - hugo + - web +social: + twitter: "@jxxf" + facebook: "https://example.com" +---` + t.Parallel() + s := newTestSite(t) + p, _ := s.NewPageFrom(strings.NewReader(exampleParams), "content/post/params.md") + + topLevelKeyValue, _ := p.Param("rating") + assert.Equal(t, "5 stars", topLevelKeyValue) + + nestedStringKeyValue, _ := p.Param("social.twitter") + assert.Equal(t, "@jxxf", nestedStringKeyValue) + + nonexistentKeyValue, _ := p.Param("doesn't.exist") + assert.Nil(t, nonexistentKeyValue) +} + +func TestPageSimpleMethods(t *testing.T) { + t.Parallel() + s := newTestSite(t) + for i, this := range []struct { + assertFunc func(p *Page) bool + }{ + {func(p *Page) bool { return !p.IsNode() }}, + {func(p *Page) bool { return p.IsPage() }}, + {func(p *Page) bool { return p.Plain() == "Do Be Do Be Do" }}, + {func(p *Page) bool { return strings.Join(p.PlainWords(), " ") == "Do Be Do Be Do" }}, + } { + + p, _ := s.NewPage("Test") + p.Content = "<h1>Do Be Do Be Do</h1>" + if !this.assertFunc(p) { + t.Errorf("[%d] Page method error", i) + } + } +} + +func TestIndexPageSimpleMethods(t *testing.T) { + s := newTestSite(t) + t.Parallel() + for i, this := range []struct { + assertFunc func(n *Page) bool + }{ + {func(n *Page) bool { return n.IsNode() }}, + {func(n *Page) bool { return !n.IsPage() }}, + {func(n *Page) bool { return n.Scratch() != nil }}, + {func(n *Page) bool { return n.Hugo() != nil }}, + {func(n *Page) bool { return n.Now().Unix() == time.Now().Unix() }}, + } { + + n := s.newHomePage() + + if !this.assertFunc(n) { + t.Errorf("[%d] Node method error", i) + } + } +} + +func TestKind(t *testing.T) { + t.Parallel() + // Add tests for these constants to make sure they don't change + require.Equal(t, "page", KindPage) + require.Equal(t, "home", KindHome) + require.Equal(t, "section", KindSection) + require.Equal(t, "taxonomy", KindTaxonomy) + require.Equal(t, "taxonomyTerm", KindTaxonomyTerm) + } func TestChompBOM(t *testing.T) { t.Parallel() - c := qt.New(t) const utf8BOM = "\xef\xbb\xbf" cfg, fs := newTestCfg() - configs, err := loadTestConfigFromProvider(cfg) - c.Assert(err, qt.IsNil) writeSource(t, fs, filepath.Join("content", "simple.md"), utf8BOM+simplePage) - s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{SkipRender: true}) + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) - c.Assert(len(s.RegularPages()), qt.Equals, 1) + require.Len(t, s.RegularPages, 1) - p := s.RegularPages()[0] + p := s.RegularPages[0] checkPageTitle(t, p, "Simple") } -// https://github.com/gohugoio/hugo/issues/5381 -func TestPageManualSummary(t *testing.T) { - b := newTestSitesBuilder(t) - b.WithSimpleConfigFile() +// TODO(bep) this may be useful for other tests. +func compareObjects(a interface{}, b interface{}) bool { + aStr := strings.Split(fmt.Sprintf("%v", a), "") + sort.Strings(aStr) - b.WithContent("page-md-shortcode.md", `--- -title: "Hugo" ---- -This is a {{< sc >}}. -<!--more--> -Content. -`) + bStr := strings.Split(fmt.Sprintf("%v", b), "") + sort.Strings(bStr) - // https://github.com/gohugoio/hugo/issues/5464 - b.WithContent("page-md-only-shortcode.md", `--- -title: "Hugo" ---- -{{< sc >}} -<!--more--> -{{< sc >}} -`) - - b.WithContent("page-md-shortcode-same-line.md", `--- -title: "Hugo" ---- -This is a {{< sc >}}<!--more-->Same line. -`) - - b.WithContent("page-md-shortcode-same-line-after.md", `--- -title: "Hugo" ---- -Summary<!--more-->{{< sc >}} -`) - - b.WithContent("page-org-shortcode.org", `#+TITLE: T1 -#+AUTHOR: A1 -#+DESCRIPTION: D1 -This is a {{< sc >}}. -# more -Content. -`) - - b.WithContent("page-org-variant1.org", `#+TITLE: T1 -Summary. - -# more - -Content. -`) - - b.WithTemplatesAdded("layouts/shortcodes/sc.html", "a shortcode") - b.WithTemplatesAdded("layouts/_default/single.html", ` -SUMMARY:{{ .Summary }}:END --------------------------- -CONTENT:{{ .Content }} -`) - - b.CreateSites().Build(BuildCfg{}) - - b.AssertFileContent("public/page-md-shortcode/index.html", - "SUMMARY:<p>This is a a shortcode.</p>:END", - "CONTENT:<p>This is a a shortcode.</p>\n\n<p>Content.</p>\n", - ) - - b.AssertFileContent("public/page-md-shortcode-same-line/index.html", - "SUMMARY:<p>This is a a shortcode</p>:END", - "CONTENT:<p>This is a a shortcode</p>\n\n<p>Same line.</p>\n", - ) - - b.AssertFileContent("public/page-md-shortcode-same-line-after/index.html", - "SUMMARY:<p>Summary</p>:END", - "CONTENT:<p>Summary</p>\n\na shortcode", - ) - - b.AssertFileContent("public/page-org-shortcode/index.html", - "SUMMARY:<p>\nThis is a a shortcode.\n</p>:END", - "CONTENT:<p>\nThis is a a shortcode.\n</p>\n<p>\nContent.\t\n</p>\n", - ) - b.AssertFileContent("public/page-org-variant1/index.html", - "SUMMARY:<p>\nSummary.\n</p>:END", - "CONTENT:<p>\nSummary.\n</p>\n<p>\nContent.\t\n</p>\n", - ) - - b.AssertFileContent("public/page-md-only-shortcode/index.html", - "SUMMARY:a shortcode:END", - "CONTENT:a shortcode\n\na shortcode\n", - ) -} - -func TestHomePageWithNoTitle(t *testing.T) { - b := newTestSitesBuilder(t).WithConfigFile("toml", ` -title = "Site Title" -`) - b.WithTemplatesAdded("index.html", "Title|{{ with .Title }}{{ . }}{{ end }}|") - b.WithContent("_index.md", `--- -description: "No title for you!" ---- - -Content. -`) - - b.Build(BuildCfg{}) - b.AssertFileContent("public/index.html", "Title||") + return strings.Join(aStr, "") == strings.Join(bStr, "") } func TestShouldBuild(t *testing.T) { - past := time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC) - future := time.Date(2037, 11, 17, 20, 34, 58, 651387237, time.UTC) - zero := time.Time{} + t.Parallel() + var past = time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC) + var future = time.Date(2037, 11, 17, 20, 34, 58, 651387237, time.UTC) + var zero = time.Time{} - publishSettings := []struct { + var publishSettings = []struct { buildFuture bool buildExpired bool buildDrafts bool @@ -1572,431 +1425,101 @@ func TestShouldBuild(t *testing.T) { } } -func TestShouldBuildWithClock(t *testing.T) { - htime.Clock = clocks.Start(time.Date(2021, 11, 17, 20, 34, 58, 651387237, time.UTC)) - t.Cleanup(func() { htime.Clock = clocks.System() }) - past := time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC) - future := time.Date(2037, 11, 17, 20, 34, 58, 651387237, time.UTC) - zero := time.Time{} +// "dot" in path: #1885 and #2110 +// disablePathToLower regression: #3374 +func TestPathIssues(t *testing.T) { + t.Parallel() + for _, disablePathToLower := range []bool{false, true} { + for _, uglyURLs := range []bool{false, true} { + t.Run(fmt.Sprintf("disablePathToLower=%t,uglyURLs=%t", disablePathToLower, uglyURLs), func(t *testing.T) { - publishSettings := []struct { - buildFuture bool - buildExpired bool - buildDrafts bool - draft bool - publishDate time.Time - expiryDate time.Time - out bool - }{ - // publishDate and expiryDate - {false, false, false, false, zero, zero, true}, - {false, false, false, false, zero, future, true}, - {false, false, false, false, past, zero, true}, - {false, false, false, false, past, future, true}, - {false, false, false, false, past, past, false}, - {false, false, false, false, future, future, false}, - {false, false, false, false, future, past, false}, + cfg, fs := newTestCfg() + th := testHelper{cfg, fs, t} - // buildFuture and buildExpired - {false, true, false, false, past, past, true}, - {true, true, false, false, past, past, true}, - {true, false, false, false, past, past, false}, - {true, false, false, false, future, future, true}, - {true, true, false, false, future, future, true}, - {false, true, false, false, future, past, false}, + cfg.Set("permalinks", map[string]string{ + "post": ":section/:title", + }) - // buildDrafts and draft - {true, true, false, true, past, future, false}, - {true, true, true, true, past, future, true}, - {true, true, true, true, past, future, true}, - } + cfg.Set("uglyURLs", uglyURLs) + cfg.Set("disablePathToLower", disablePathToLower) + cfg.Set("paginate", 1) - for _, ps := range publishSettings { - s := shouldBuild(ps.buildFuture, ps.buildExpired, ps.buildDrafts, ps.draft, - ps.publishDate, ps.expiryDate) - if s != ps.out { - t.Errorf("AssertShouldBuildWithClock unexpected output with params: %+v", ps) + writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), "<html><body>{{.Content}}</body></html>") + writeSource(t, fs, filepath.Join("layouts", "_default", "list.html"), + "<html><body>P{{.Paginator.PageNumber}}|URL: {{.Paginator.URL}}|{{ if .Paginator.HasNext }}Next: {{.Paginator.Next.URL }}{{ end }}</body></html>") + + for i := 0; i < 3; i++ { + writeSource(t, fs, filepath.Join("content", "post", fmt.Sprintf("doc%d.md", i)), + fmt.Sprintf(`--- +title: "test%d.dot" +tags: +- ".net" +--- +# doc1 +*some content*`, i)) + } + + writeSource(t, fs, filepath.Join("content", "Blog", "Blog1.md"), + fmt.Sprintf(`--- +title: "testBlog" +tags: +- "Blog" +--- +# doc1 +*some blog content*`)) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + require.Len(t, s.RegularPages, 4) + + pathFunc := func(s string) string { + if uglyURLs { + return strings.Replace(s, "/index.html", ".html", 1) + } + return s + } + + blog := "blog" + + if disablePathToLower { + blog = "Blog" + } + + th.assertFileContent(pathFunc("public/"+blog+"/"+blog+"1/index.html"), "some blog content") + + th.assertFileContent(pathFunc("public/post/test0.dot/index.html"), "some content") + + if uglyURLs { + th.assertFileContent("public/post/page/1.html", `canonical" href="/post.html"/`) + th.assertFileContent("public/post.html", `<body>P1|URL: /post.html|Next: /post/page/2.html</body>`) + th.assertFileContent("public/post/page/2.html", `<body>P2|URL: /post/page/2.html|Next: /post/page/3.html</body>`) + } else { + th.assertFileContent("public/post/page/1/index.html", `canonical" href="/post/"/`) + th.assertFileContent("public/post/index.html", `<body>P1|URL: /post/|Next: /post/page/2/</body>`) + th.assertFileContent("public/post/page/2/index.html", `<body>P2|URL: /post/page/2/|Next: /post/page/3/</body>`) + th.assertFileContent("public/tags/.net/index.html", `<body>P1|URL: /tags/.net/|Next: /tags/.net/page/2/</body>`) + + } + + p := s.RegularPages[0] + if uglyURLs { + require.Equal(t, "/post/test0.dot.html", p.RelPermalink()) + } else { + require.Equal(t, "/post/test0.dot/", p.RelPermalink()) + } + + }) } } } -// See https://github.com/gohugoio/hugo/issues/9171 -// We redefined disablePathToLower in v0.121.0. -func TestPagePathDisablePathToLower(t *testing.T) { - files := ` --- hugo.toml -- -baseURL = "http://example.com" -disablePathToLower = true -[permalinks] -sect2 = "/:section/:filename/" -sect3 = "/:section/:title/" --- content/sect/p1.md -- ---- -title: "Page1" ---- -p1. --- content/sect/p2.md -- ---- -title: "Page2" -slug: "PaGe2" ---- -p2. --- content/sect2/PaGe3.md -- ---- -title: "Page3" ---- --- content/seCt3/p4.md -- ---- -title: "Pag.E4" -slug: "PaGe4" ---- -p4. --- layouts/_default/single.html -- -Single: {{ .Title}}|{{ .RelPermalink }}|{{ .Path }}| -` - b := Test(t, files) - b.AssertFileContent("public/sect/p1/index.html", "Single: Page1|/sect/p1/|/sect/p1") - b.AssertFileContent("public/sect/PaGe2/index.html", "Single: Page2|/sect/PaGe2/|/sect/p2") - b.AssertFileContent("public/sect2/PaGe3/index.html", "Single: Page3|/sect2/PaGe3/|/sect2/page3|") - b.AssertFileContent("public/sect3/Pag.E4/index.html", "Single: Pag.E4|/sect3/Pag.E4/|/sect3/p4|") -} - -func TestScratch(t *testing.T) { - t.Parallel() - - b := newTestSitesBuilder(t) - b.WithSimpleConfigFile().WithTemplatesAdded("index.html", ` -{{ .Scratch.Set "b" "bv" }} -B: {{ .Scratch.Get "b" }} -`, - "shortcodes/scratch.html", ` -{{ .Scratch.Set "c" "cv" }} -C: {{ .Scratch.Get "c" }} -`, - ) - - b.WithContentAdded("scratchme.md", ` ---- -title: Scratch Me! ---- - -{{< scratch >}} -`) - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", "B: bv") - b.AssertFileContent("public/scratchme/index.html", "C: cv") -} - -// Issue 13016. -func TestScratchAliasToStore(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ["taxonomy", "term", "page", "section"] -disableLiveReload = true --- layouts/index.html -- -{{ .Scratch.Set "a" "b" }} -{{ .Store.Set "c" "d" }} -.Scratch eq .Store: {{ eq .Scratch .Store }} -a: {{ .Store.Get "a" }} -c: {{ .Scratch.Get "c" }} - -` - - b := Test(t, files) - - b.AssertFileContent("public/index.html", - ".Scratch eq .Store: true", - "a: b", - "c: d", - ) -} - -func TestPageParam(t *testing.T) { - t.Parallel() - - b := newTestSitesBuilder(t).WithConfigFile("toml", ` - -baseURL = "https://example.org" - -[params] -[params.author] - name = "Kurt Vonnegut" - -`) - b.WithTemplatesAdded("index.html", ` - -{{ $withParam := .Site.GetPage "withparam" }} -{{ $noParam := .Site.GetPage "noparam" }} -{{ $withStringParam := .Site.GetPage "withstringparam" }} - -Author page: {{ $withParam.Param "author.name" }} -Author name page string: {{ $withStringParam.Param "author.name" }}| -Author page string: {{ $withStringParam.Param "author" }}| -Author site config: {{ $noParam.Param "author.name" }} - -`, - ) - - b.WithContent("withparam.md", ` -+++ -title = "With Param!" -[author] - name = "Ernest Miller Hemingway" - -+++ - -`, - - "noparam.md", ` ---- -title: "No Param!" ---- -`, "withstringparam.md", ` -+++ -title = "With string Param!" -author = "Jo Nesbø" - -+++ - -`) - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", - "Author page: Ernest Miller Hemingway", - "Author name page string: Kurt Vonnegut|", - "Author page string: Jo Nesbø|", - "Author site config: Kurt Vonnegut") -} - -func TestGoldmark(t *testing.T) { - t.Parallel() - - b := newTestSitesBuilder(t).WithConfigFile("toml", ` -baseURL = "https://example.org" - -[markup] -defaultMarkdownHandler="goldmark" -[markup.goldmark] -[markup.goldmark.renderer] -unsafe = false -[markup.highlight] -noClasses=false - - -`) - b.WithTemplatesAdded("_default/single.html", ` -Title: {{ .Title }} -ToC: {{ .TableOfContents }} -Content: {{ .Content }} - -`, "shortcodes/t.html", `T-SHORT`, "shortcodes/s.html", `## Code -{{ .Inner }} -`) - - content := ` -+++ -title = "A Page!" -+++ - -## Shortcode {{% t %}} in header - -## Code Fense in Shortcode - -{{% s %}} -$$$bash {hl_lines=[1]} -SHORT -$$$ -{{% /s %}} - -## Code Fence - -$$$bash {hl_lines=[1]} -MARKDOWN -$$$ - -Link with URL as text - -[https://google.com](https://google.com) - - -` - content = strings.ReplaceAll(content, "$$$", "```") - - b.WithContent("page.md", content) - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/page/index.html", - `<nav id="TableOfContents"> -<li><a href="#shortcode-t-short-in-header">Shortcode T-SHORT in header</a></li> -<code class="language-bash" data-lang="bash"><span class="line hl"><span class="cl">SHORT -<code class="language-bash" data-lang="bash"><span class="line hl"><span class="cl">MARKDOWN -<p><a href="https://google.com">https://google.com</a></p> -`) -} - -func TestPageHashString(t *testing.T) { - files := ` --- config.toml -- -baseURL = "https://example.org" -[languages] -[languages.en] -weight = 1 -title = "English" -[languages.no] -weight = 2 -title = "Norsk" --- content/p1.md -- ---- -title: "p1" ---- --- content/p2.md -- ---- -title: "p2" ---- -` - - b := NewIntegrationTestBuilder(IntegrationTestConfig{ - T: t, - TxtarString: files, - }).Build() - - p1 := b.H.Sites[0].RegularPages()[0] - p2 := b.H.Sites[0].RegularPages()[1] - sites := p1.Sites() - - b.Assert(p1, qt.Not(qt.Equals), p2) - - b.Assert(hashing.HashString(p1), qt.Not(qt.Equals), hashing.HashString(p2)) - b.Assert(hashing.HashString(sites[0]), qt.Not(qt.Equals), hashing.HashString(sites[1])) -} - -// Issue #11243 -func TestRenderWithoutArgument(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- --- layouts/index.html -- -{{ .Render }} -` - - b, err := NewIntegrationTestBuilder( - IntegrationTestConfig{ - T: t, - TxtarString: files, - }, - ).BuildE() - - b.Assert(err, qt.IsNotNil) -} - -// Issue #13021 -func TestAllStores(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ["taxonomy", "term", "page", "section"] -disableLiveReload = true --- content/_index.md -- ---- -title: "Home" ---- -{{< s >}} --- layouts/shortcodes/s.html -- -{{ if not (.Store.Get "Shortcode") }}{{ .Store.Set "Shortcode" (printf "sh-%s" $.Page.Title) }}{{ end }} -Shortcode: {{ .Store.Get "Shortcode" }}| --- layouts/index.html -- -{{ .Content }} -{{ if not (.Store.Get "Page") }}{{ .Store.Set "Page" (printf "p-%s" $.Title) }}{{ end }} -{{ if not (hugo.Store.Get "Hugo") }}{{ hugo.Store.Set "Hugo" (printf "h-%s" $.Title) }}{{ end }} -{{ if not (site.Store.Get "Site") }}{{ site.Store.Set "Site" (printf "s-%s" $.Title) }}{{ end }} -Page: {{ .Store.Get "Page" }}| -Hugo: {{ hugo.Store.Get "Hugo" }}| -Site: {{ site.Store.Get "Site" }}| -` - - b := TestRunning(t, files) - - b.AssertFileContent("public/index.html", - ` -Shortcode: sh-Home| -Page: p-Home| -Site: s-Home| -Hugo: h-Home| -`, - ) - - b.EditFileReplaceAll("content/_index.md", "Home", "Homer").Build() - - b.AssertFileContent("public/index.html", - ` -Shortcode: sh-Homer| -Page: p-Homer| -Site: s-Home| -Hugo: h-Home| -`, - ) -} - -// See #12484 -func TestPageFrontMatterDeprecatePathKindLang(t *testing.T) { - // This cannot be parallel as it depends on output from the global logger. - - files := ` --- hugo.toml -- -disableKinds = ["taxonomy", "term", "home", "section"] --- content/p1.md -- ---- -title: "p1" -kind: "page" -lang: "en" -path: "mypath" ---- --- layouts/_default/single.html -- -Title: {{ .Title }} -` - b := Test(t, files, TestOptWarn()) - b.AssertFileContent("public/mypath/index.html", "p1") - b.AssertLogContains( - "deprecated: kind in front matter was deprecated", - "deprecated: lang in front matter was deprecated", - "deprecated: path in front matter was deprecated", - ) -} - -// Issue 13538 -func TestHomePageIsLeafBundle(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -defaultContentLanguage = 'de' -defaultContentLanguageInSubdir = true -[languages.de] -weight = 1 -[languages.en] -weight = 2 --- layouts/all.html -- -{{ .Title }} --- content/index.de.md -- ---- -title: home de ---- --- content/index.en.org -- ---- -title: home en ---- -` - - b := Test(t, files, TestOptWarn()) - - b.AssertFileContent("public/de/index.html", "home de") - b.AssertFileContent("public/en/index.html", "home en") - b.AssertLogContains("Using index.de.md in your content's root directory is usually incorrect for your home page. You should use _index.de.md instead.") - b.AssertLogContains("Using index.en.org in your content's root directory is usually incorrect for your home page. You should use _index.en.org instead.") +func BenchmarkParsePage(b *testing.B) { + s := newTestSite(b) + f, _ := os.Open("testdata/redis.cn.md") + var buf bytes.Buffer + buf.ReadFrom(f) + b.ResetTimer() + for i := 0; i < b.N; i++ { + page, _ := s.NewPage("bench") + page.ReadFrom(bytes.NewReader(buf.Bytes())) + } } diff --git a/hugolib/page_time_integration_test.go b/hugolib/page_time_integration_test.go new file mode 100644 index 000000000..1bf83bdca --- /dev/null +++ b/hugolib/page_time_integration_test.go @@ -0,0 +1,183 @@ +// Copyright 2015 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 hugolib + +import ( + "fmt" + "os" + "strings" + "sync" + "testing" + "time" + + "github.com/spf13/cast" +) + +const ( + pageWithInvalidDate = `--- +date: 2010-05-02_15:29:31+08:00 +--- +Page With Invalid Date (replace T with _ for RFC 3339)` + + pageWithDateRFC3339 = `--- +date: 2010-05-02T15:29:31+08:00 +--- +Page With Date RFC3339` + + pageWithDateRFC3339NoT = `--- +date: 2010-05-02 15:29:31+08:00 +--- +Page With Date RFC3339_NO_T` + + pageWithRFC1123 = `--- +date: Sun, 02 May 2010 15:29:31 PST +--- +Page With Date RFC1123` + + pageWithDateRFC1123Z = `--- +date: Sun, 02 May 2010 15:29:31 +0800 +--- +Page With Date RFC1123Z` + + pageWithDateRFC822 = `--- +date: 02 May 10 15:29 PST +--- +Page With Date RFC822` + + pageWithDateRFC822Z = `--- +date: 02 May 10 15:29 +0800 +--- +Page With Date RFC822Z` + + pageWithDateANSIC = `--- +date: Sun May 2 15:29:31 2010 +--- +Page With Date ANSIC` + + pageWithDateUnixDate = `--- +date: Sun May 2 15:29:31 PST 2010 +--- +Page With Date UnixDate` + + pageWithDateRubyDate = `--- +date: Sun May 02 15:29:31 +0800 2010 +--- +Page With Date RubyDate` + + pageWithDateHugoYearNumeric = `--- +date: 2010-05-02 +--- +Page With Date HugoYearNumeric` + + pageWithDateHugoYear = `--- +date: 02 May 2010 +--- +Page With Date HugoYear` + + pageWithDateHugoLong = `--- +date: 02 May 2010 15:29 PST +--- +Page With Date HugoLong` +) + +func TestDegenerateDateFrontMatter(t *testing.T) { + t.Parallel() + s := newTestSite(t) + p, _ := s.NewPageFrom(strings.NewReader(pageWithInvalidDate), "page/with/invalid/date") + if p.Date != *new(time.Time) { + t.Fatalf("Date should be set to time.Time zero value. Got: %s", p.Date) + } +} + +func TestParsingDateInFrontMatter(t *testing.T) { + t.Parallel() + s := newTestSite(t) + tests := []struct { + buf string + dt string + }{ + {pageWithDateRFC3339, "2010-05-02T15:29:31+08:00"}, + {pageWithDateRFC3339NoT, "2010-05-02T15:29:31+08:00"}, + {pageWithDateRFC1123Z, "2010-05-02T15:29:31+08:00"}, + {pageWithDateRFC822Z, "2010-05-02T15:29:00+08:00"}, + {pageWithDateANSIC, "2010-05-02T15:29:31Z"}, + {pageWithDateRubyDate, "2010-05-02T15:29:31+08:00"}, + {pageWithDateHugoYearNumeric, "2010-05-02T00:00:00Z"}, + {pageWithDateHugoYear, "2010-05-02T00:00:00Z"}, + } + + tzShortCodeTests := []struct { + buf string + dt string + }{ + {pageWithRFC1123, "2010-05-02T15:29:31-08:00"}, + {pageWithDateRFC822, "2010-05-02T15:29:00-08:00Z"}, + {pageWithDateUnixDate, "2010-05-02T15:29:31-08:00"}, + {pageWithDateHugoLong, "2010-05-02T15:21:00+08:00"}, + } + + if _, err := time.LoadLocation("PST"); err == nil { + tests = append(tests, tzShortCodeTests...) + } else { + fmt.Fprintf(os.Stderr, "Skipping shortname timezone tests.\n") + } + + for _, test := range tests { + dt, e := time.Parse(time.RFC3339, test.dt) + if e != nil { + t.Fatalf("Unable to parse date time (RFC3339) for running the test: %s", e) + } + p, err := s.NewPageFrom(strings.NewReader(test.buf), "page/with/date") + if err != nil { + t.Fatalf("Expected to be able to parse page.") + } + if !dt.Equal(p.Date) { + t.Errorf("Date does not equal frontmatter:\n%s\nExpecting: %s\n Got: %s. Diff: %s\n internal: %#v\n %#v", test.buf, dt, p.Date, dt.Sub(p.Date), dt, p.Date) + } + } +} + +// Temp test https://github.com/gohugoio/hugo/issues/3059 +func TestParsingDateParallel(t *testing.T) { + t.Parallel() + + var wg sync.WaitGroup + + for j := 0; j < 100; j++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 100; j++ { + dateStr := "2010-05-02 15:29:31 +08:00" + + dt, err := time.Parse("2006-01-02 15:04:05 -07:00", dateStr) + if err != nil { + t.Fatal(err) + } + + if dt.Year() != 2010 { + t.Fatal("time.Parse: Invalid date:", dt) + } + + dt2 := cast.ToTime(dateStr) + + if dt2.Year() != 2010 { + t.Fatal("cast.ToTime: Invalid date:", dt2.Year()) + } + } + }() + } + wg.Wait() + +} diff --git a/hugolib/page_unwrap.go b/hugolib/page_unwrap.go deleted file mode 100644 index c22ff2174..000000000 --- a/hugolib/page_unwrap.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2019 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 hugolib - -import ( - "fmt" - - "github.com/gohugoio/hugo/common/types" - "github.com/gohugoio/hugo/resources/page" -) - -// Wraps a Page. -type pageWrapper interface { - page() page.Page -} - -// unwrapPage is used in equality checks and similar. -func unwrapPage(in any) (page.Page, error) { - switch v := in.(type) { - case *pageState: - return v, nil - case pageWrapper: - return v.page(), nil - case types.Unwrapper: - return unwrapPage(v.Unwrapv()) - case page.Page: - return v, nil - case nil: - return nil, nil - default: - return nil, fmt.Errorf("unwrapPage: %T not supported", in) - } -} - -func mustUnwrapPage(in any) page.Page { - p, err := unwrapPage(in) - if err != nil { - panic(err) - } - - return p -} diff --git a/hugolib/page_unwrap_test.go b/hugolib/page_unwrap_test.go deleted file mode 100644 index 2d9b5e17f..000000000 --- a/hugolib/page_unwrap_test.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2019 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 hugolib - -import ( - "testing" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/resources/page" -) - -func TestUnwrapPage(t *testing.T) { - c := qt.New(t) - - p := &pageState{} - - c.Assert(mustUnwrap(newPageForShortcode(p)), qt.Equals, p) - c.Assert(mustUnwrap(newPageForRenderHook(p)), qt.Equals, p) -} - -func mustUnwrap(v any) page.Page { - p, err := unwrapPage(v) - if err != nil { - panic(err) - } - return p -} diff --git a/hugolib/pagebundler_test.go b/hugolib/pagebundler_test.go deleted file mode 100644 index e5521412b..000000000 --- a/hugolib/pagebundler_test.go +++ /dev/null @@ -1,959 +0,0 @@ -// Copyright 2019 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 hugolib - -import ( - "fmt" - "os" - "path/filepath" - "testing" - - "github.com/gohugoio/hugo/common/hashing" - "github.com/gohugoio/hugo/common/loggers" - - "github.com/gohugoio/hugo/config" - - "github.com/gohugoio/hugo/hugofs" - - "github.com/gohugoio/hugo/resources/kinds" - "github.com/gohugoio/hugo/resources/page" - - "github.com/gohugoio/hugo/htesting" - - "github.com/gohugoio/hugo/deps" - - qt "github.com/frankban/quicktest" -) - -func TestPageBundlerBundleInRoot(t *testing.T) { - t.Parallel() - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableKinds = ["taxonomy", "term"] --- content/root/index.md -- ---- -title: "Root" ---- --- layouts/_default/single.html -- -Basic: {{ .Title }}|{{ .Kind }}|{{ .BundleType }}|{{ .RelPermalink }}| -Tree: Section: {{ .Section }}|CurrentSection: {{ .CurrentSection.RelPermalink }}|Parent: {{ .Parent.RelPermalink }}|FirstSection: {{ .FirstSection.RelPermalink }} -` - b := Test(t, files) - - b.AssertFileContent("public/root/index.html", - "Basic: Root|page|leaf|/root/|", - "Tree: Section: |CurrentSection: /|Parent: /|FirstSection: /", - ) -} - -func TestPageBundlerShortcodeInBundledPage(t *testing.T) { - t.Parallel() - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableKinds = ["taxonomy", "term"] --- content/section/mybundle/index.md -- ---- -title: "Mybundle" ---- --- content/section/mybundle/p1.md -- ---- -title: "P1" ---- - -P1 content. - -{{< myShort >}} - --- layouts/_default/single.html -- -Bundled page: {{ .RelPermalink}}|{{ with .Resources.Get "p1.md" }}Title: {{ .Title }}|Content: {{ .Content }}{{ end }}| --- layouts/shortcodes/myShort.html -- -MyShort. - -` - b := Test(t, files) - - b.AssertFileContent("public/section/mybundle/index.html", - "Bundled page: /section/mybundle/|Title: P1|Content: <p>P1 content.</p>\nMyShort.", - ) -} - -func TestPageBundlerResourceMultipleOutputFormatsWithDifferentPaths(t *testing.T) { - t.Parallel() - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableKinds = ["taxonomy", "term"] -[outputformats] -[outputformats.cpath] -mediaType = "text/html" -path = "cpath" --- content/section/mybundle/index.md -- ---- -title: "My Bundle" -outputs: ["html", "cpath"] ---- --- content/section/mybundle/hello.txt -- -Hello. --- content/section/mybundle/p1.md -- ---- -title: "P1" ---- -P1. - -{{< hello >}} - --- layouts/shortcodes/hello.html -- -Hello HTML. --- layouts/_default/single.html -- -Basic: {{ .Title }}|{{ .Kind }}|{{ .BundleType }}|{{ .RelPermalink }}| -Resources: {{ range .Resources }}RelPermalink: {{ .RelPermalink }}|Content: {{ .Content }}|{{ end }}| --- layouts/shortcodes/hello.cpath -- -Hello CPATH. --- layouts/_default/single.cpath -- -Basic: {{ .Title }}|{{ .Kind }}|{{ .BundleType }}|{{ .RelPermalink }}| -Resources: {{ range .Resources }}RelPermalink: {{ .RelPermalink }}|Content: {{ .Content }}|{{ end }}| -` - - b := Test(t, files) - - b.AssertFileContent("public/section/mybundle/index.html", - "Basic: My Bundle|page|leaf|/section/mybundle/|", - "Resources: RelPermalink: |Content: <p>P1.</p>\nHello HTML.\n|RelPermalink: /section/mybundle/hello.txt|Content: Hello.||", - ) - - b.AssertFileContent("public/cpath/section/mybundle/index.html", "Basic: My Bundle|page|leaf|/section/mybundle/|\nResources: RelPermalink: |Content: <p>P1.</p>\nHello CPATH.\n|RelPermalink: /section/mybundle/hello.txt|Content: Hello.||") -} - -func TestPageBundlerMultilingualTextResource(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableKinds = ["taxonomy", "term"] -defaultContentLanguage = "en" -defaultContentLanguageInSubdir = true -[languages] -[languages.en] -weight = 1 -[languages.en.permalinks] -"/" = "/enpages/:slug/" -[languages.nn] -weight = 2 --- content/mybundle/index.md -- ---- -title: "My Bundle" ---- --- content/mybundle/index.nn.md -- ---- -title: "My Bundle NN" ---- --- content/mybundle/f1.txt -- -F1 --- content/mybundle/f2.txt -- -F2 --- content/mybundle/f2.nn.txt -- -F2 nn. --- layouts/_default/single.html -- -{{ .Title }}|{{ .RelPermalink }}|{{ .Lang }}| -Resources: {{ range .Resources }}RelPermalink: {{ .RelPermalink }}|Content: {{ .Content }}|{{ end }}| - -` - b := Test(t, files) - - b.AssertFileContent("public/en/enpages/my-bundle/index.html", "My Bundle|/en/enpages/my-bundle/|en|\nResources: RelPermalink: /en/enpages/my-bundle/f1.txt|Content: F1|RelPermalink: /en/enpages/my-bundle/f2.txt|Content: F2||") - b.AssertFileContent("public/nn/mybundle/index.html", "My Bundle NN|/nn/mybundle/|nn|\nResources: RelPermalink: /en/enpages/my-bundle/f1.txt|Content: F1|RelPermalink: /nn/mybundle/f2.nn.txt|Content: F2 nn.||") -} - -func TestMultilingualDisableLanguage(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableKinds = ["taxonomy", "term"] -defaultContentLanguage = "en" -defaultContentLanguageInSubdir = true -[languages] -[languages.en] -weight = 1 -[languages.nn] -weight = 2 -disabled = true --- content/mysect/_index.md -- ---- -title: "My Sect En" ---- --- content/mysect/p1/index.md -- ---- -title: "P1" ---- -P1 --- content/mysect/_index.nn.md -- ---- -title: "My Sect Nn" ---- --- content/mysect/p1/index.nn.md -- ---- -title: "P1nn" ---- -P1nn --- layouts/index.html -- -Len RegularPages: {{ len .Site.RegularPages }}|RegularPages: {{ range site.RegularPages }}{{ .RelPermalink }}: {{ .Title }}|{{ end }}| -Len Pages: {{ len .Site.Pages }}| -Len Sites: {{ len .Site.Sites }}| --- layouts/_default/single.html -- -{{ .Title }}|{{ .Content }}|{{ .Lang }}| - -` - b := Test(t, files) - - b.AssertFileContent("public/en/index.html", "Len RegularPages: 1|") - b.AssertFileContent("public/en/mysect/p1/index.html", "P1|<p>P1</p>\n|en|") - b.AssertFileExists("public/public/nn/mysect/p1/index.html", false) - b.Assert(len(b.H.Sites), qt.Equals, 1) -} - -func TestPageBundlerHeadless(t *testing.T) { - t.Parallel() - - cfg, fs := newTestCfg() - c := qt.New(t) - - workDir := "/work" - cfg.Set("workingDir", workDir) - cfg.Set("contentDir", "base") - cfg.Set("baseURL", "https://example.com") - configs, err := loadTestConfigFromProvider(cfg) - c.Assert(err, qt.IsNil) - - pageContent := `--- -title: "Bundle Galore" -slug: s1 -date: 2017-01-23 ---- - -TheContent. - -{{< myShort >}} -` - - writeSource(t, fs, filepath.Join(workDir, "layouts", "_default", "single.html"), "single {{ .Content }}") - writeSource(t, fs, filepath.Join(workDir, "layouts", "_default", "list.html"), "list") - writeSource(t, fs, filepath.Join(workDir, "layouts", "shortcodes", "myShort.html"), "SHORTCODE") - - writeSource(t, fs, filepath.Join(workDir, "base", "a", "index.md"), pageContent) - writeSource(t, fs, filepath.Join(workDir, "base", "a", "l1.png"), "PNG image") - writeSource(t, fs, filepath.Join(workDir, "base", "a", "l2.png"), "PNG image") - - writeSource(t, fs, filepath.Join(workDir, "base", "b", "index.md"), `--- -title: "Headless Bundle in Topless Bar" -slug: s2 -headless: true -date: 2017-01-23 ---- - -TheContent. -HEADLESS {{< myShort >}} -`) - writeSource(t, fs, filepath.Join(workDir, "base", "b", "l1.png"), "PNG image") - writeSource(t, fs, filepath.Join(workDir, "base", "b", "l2.png"), "PNG image") - writeSource(t, fs, filepath.Join(workDir, "base", "b", "p1.md"), pageContent) - - s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{}) - - c.Assert(len(s.RegularPages()), qt.Equals, 1) - - regular := s.getPageOldVersion(kinds.KindPage, "a/index") - c.Assert(regular.RelPermalink(), qt.Equals, "/s1/") - - headless := s.getPageOldVersion(kinds.KindPage, "b/index") - c.Assert(headless, qt.Not(qt.IsNil)) - c.Assert(headless.Title(), qt.Equals, "Headless Bundle in Topless Bar") - c.Assert(headless.RelPermalink(), qt.Equals, "") - c.Assert(headless.Permalink(), qt.Equals, "") - c.Assert(content(headless), qt.Contains, "HEADLESS SHORTCODE") - - headlessResources := headless.Resources() - c.Assert(len(headlessResources), qt.Equals, 3) - res := headlessResources.Match("l*") - c.Assert(len(res), qt.Equals, 2) - pageResource := headlessResources.GetMatch("p*") - c.Assert(pageResource, qt.Not(qt.IsNil)) - p := pageResource.(page.Page) - c.Assert(content(p), qt.Contains, "SHORTCODE") - c.Assert(p.Name(), qt.Equals, "p1.md") - - th := newTestHelper(s.conf, s.Fs, t) - - th.assertFileContent(filepath.FromSlash("public/s1/index.html"), "TheContent") - th.assertFileContent(filepath.FromSlash("public/s1/l1.png"), "PNG") - - th.assertFileNotExist("public/s2/index.html") - // But the bundled resources needs to be published - th.assertFileContent(filepath.FromSlash("public/s2/l1.png"), "PNG") - - // No headless bundles here, please. - // https://github.com/gohugoio/hugo/issues/6492 - c.Assert(s.RegularPages(), qt.HasLen, 1) - c.Assert(s.Pages(), qt.HasLen, 4) - c.Assert(s.home.RegularPages(), qt.HasLen, 1) - c.Assert(s.home.Pages(), qt.HasLen, 1) -} - -func TestPageBundlerHeadlessIssue6552(t *testing.T) { - t.Parallel() - - b := newTestSitesBuilder(t) - b.WithContent("headless/h1/index.md", ` ---- -title: My Headless Bundle1 -headless: true ---- -`, "headless/h1/p1.md", ` ---- -title: P1 ---- -`, "headless/h2/index.md", ` ---- -title: My Headless Bundle2 -headless: true ---- -`) - - b.WithTemplatesAdded("index.html", ` -{{ $headless1 := .Site.GetPage "headless/h1" }} -{{ $headless2 := .Site.GetPage "headless/h2" }} - -HEADLESS1: {{ $headless1.Title }}|{{ $headless1.RelPermalink }}|{{ len $headless1.Resources }}| -HEADLESS2: {{ $headless2.Title }}{{ $headless2.RelPermalink }}|{{ len $headless2.Resources }}| - -`) - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", ` -HEADLESS1: My Headless Bundle1||1| -HEADLESS2: My Headless Bundle2|0| -`) -} - -func TestMultiSiteBundles(t *testing.T) { - c := qt.New(t) - b := newTestSitesBuilder(t) - b.WithConfigFile("toml", ` - -baseURL = "http://example.com/" - -defaultContentLanguage = "en" - -[languages] -[languages.en] -weight = 10 -contentDir = "content/en" -[languages.nn] -weight = 20 -contentDir = "content/nn" - - -`) - - b.WithContent("en/mybundle/index.md", ` ---- -headless: true ---- - -`) - - b.WithContent("nn/mybundle/index.md", ` ---- -headless: true ---- - -`) - - b.WithContent("en/mybundle/data.yaml", `data en`) - b.WithContent("en/mybundle/forms.yaml", `forms en`) - b.WithContent("nn/mybundle/data.yaml", `data nn`) - - b.WithContent("en/_index.md", ` ---- -Title: Home ---- - -Home content. - -`) - - b.WithContent("en/section-not-bundle/_index.md", ` ---- -Title: Section Page ---- - -Section content. - -`) - - b.WithContent("en/section-not-bundle/single.md", ` ---- -Title: Section Single -Date: 2018-02-01 ---- - -Single content. - -`) - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/nn/mybundle/data.yaml", "data nn") - b.AssertFileContent("public/mybundle/data.yaml", "data en") - b.AssertFileContent("public/mybundle/forms.yaml", "forms en") - - c.Assert(b.CheckExists("public/nn/nn/mybundle/data.yaml"), qt.Equals, false) - c.Assert(b.CheckExists("public/en/mybundle/data.yaml"), qt.Equals, false) - - homeEn := b.H.Sites[0].home - c.Assert(homeEn, qt.Not(qt.IsNil)) - c.Assert(homeEn.Date().Year(), qt.Equals, 2018) - - b.AssertFileContent("public/section-not-bundle/index.html", "Section Page", "Content: <p>Section content.</p>") - b.AssertFileContent("public/section-not-bundle/single/index.html", "Section Single", "|<p>Single content.</p>") -} - -func TestBundledResourcesMultilingualDuplicateResourceFiles(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.com/" -[markup] -[markup.goldmark] -duplicateResourceFiles = true -[languages] -[languages.en] -weight = 1 -[languages.en.permalinks] -"/" = "/enpages/:slug/" -[languages.nn] -weight = 2 -[languages.nn.permalinks] -"/" = "/nnpages/:slug/" --- content/mybundle/index.md -- ---- -title: "My Bundle" ---- -{{< getresource "f1.txt" >}} -{{< getresource "f2.txt" >}} --- content/mybundle/index.nn.md -- ---- -title: "My Bundle NN" ---- -{{< getresource "f1.txt" >}} -f2.nn.txt is the original name. -{{< getresource "f2.nn.txt" >}} -{{< getresource "f2.txt" >}} -{{< getresource "sub/f3.txt" >}} --- content/mybundle/f1.txt -- -F1 en. --- content/mybundle/sub/f3.txt -- -F1 en. --- content/mybundle/f2.txt -- -F2 en. --- content/mybundle/f2.nn.txt -- -F2 nn. --- layouts/shortcodes/getresource.html -- -{{ $r := .Page.Resources.Get (.Get 0)}} -Resource: {{ (.Get 0) }}|{{ with $r }}{{ .RelPermalink }}|{{ .Content }}|{{ else }}Not found.{{ end}} --- layouts/_default/single.html -- -{{ .Title }}|{{ .RelPermalink }}|{{ .Lang }}|{{ .Content }}| -` - b := Test(t, files) - - // helpers.PrintFs(b.H.Fs.PublishDir, "", os.Stdout) - b.AssertFileContent("public/nn/nnpages/my-bundle-nn/index.html", ` -My Bundle NN -Resource: f1.txt|/nn/nnpages/my-bundle-nn/f1.txt| -Resource: f2.txt|/nn/nnpages/my-bundle-nn/f2.nn.txt|F2 nn.| -Resource: f2.nn.txt|/nn/nnpages/my-bundle-nn/f2.nn.txt|F2 nn.| -Resource: sub/f3.txt|/nn/nnpages/my-bundle-nn/sub/f3.txt|F1 en.| -`) - - b.AssertFileContent("public/enpages/my-bundle/f2.txt", "F2 en.") - b.AssertFileContent("public/nn/nnpages/my-bundle-nn/f2.nn.txt", "F2 nn") - - b.AssertFileContent("public/enpages/my-bundle/index.html", ` -Resource: f1.txt|/enpages/my-bundle/f1.txt|F1 en.| -Resource: f2.txt|/enpages/my-bundle/f2.txt|F2 en.| -`) - b.AssertFileContent("public/enpages/my-bundle/f1.txt", "F1 en.") - - // Should be duplicated to the nn bundle. - b.AssertFileContent("public/nn/nnpages/my-bundle-nn/f1.txt", "F1 en.") -} - -// https://github.com/gohugoio/hugo/issues/5858 -func TestBundledResourcesWhenMultipleOutputFormats(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.org" -disableKinds = ["taxonomy", "term"] -disableLiveReload = true -[outputs] -# This looks odd, but it triggers the behavior in #5858 -# The total output formats list gets sorted, so CSS before HTML. -home = [ "CSS" ] --- content/mybundle/index.md -- ---- -title: Page ---- --- content/mybundle/data.json -- -MyData --- layouts/_default/single.html -- -{{ range .Resources }} -{{ .ResourceType }}|{{ .Title }}| -{{ end }} -` - - b := TestRunning(t, files) - - b.AssertFileContent("public/mybundle/data.json", "MyData") - - b.EditFileReplaceAll("content/mybundle/data.json", "MyData", "My changed data").Build() - - b.AssertFileContent("public/mybundle/data.json", "My changed data") -} - -// https://github.com/gohugoio/hugo/issues/5858 - -// https://github.com/gohugoio/hugo/issues/4870 -func TestBundleSlug(t *testing.T) { - t.Parallel() - c := qt.New(t) - - const pageTemplate = `--- -title: Title -slug: %s ---- -` - - b := newTestSitesBuilder(t) - - b.WithTemplatesAdded("index.html", `{{ range .Site.RegularPages }}|{{ .RelPermalink }}{{ end }}|`) - b.WithSimpleConfigFile(). - WithContent("about/services1/misc.md", fmt.Sprintf(pageTemplate, "this-is-the-slug")). - WithContent("about/services2/misc/index.md", fmt.Sprintf(pageTemplate, "this-is-another-slug")) - - b.CreateSites().Build(BuildCfg{}) - - b.AssertHome( - "|/about/services1/this-is-the-slug/|/", - "|/about/services2/this-is-another-slug/|") - - c.Assert(b.CheckExists("public/about/services1/this-is-the-slug/index.html"), qt.Equals, true) - c.Assert(b.CheckExists("public/about/services2/this-is-another-slug/index.html"), qt.Equals, true) -} - -// See #11663 -func TestPageBundlerPartialTranslations(t *testing.T) { - t.Parallel() - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableKinds = ["taxonomy", "term"] -defaultContentLanguage = "en" -defaultContentLanguageInSubDir = true -[languages] -[languages.nn] -weight = 2 -[languages.en] -weight = 1 --- content/section/mybundle/index.md -- ---- -title: "Mybundle" ---- --- content/section/mybundle/bundledpage.md -- ---- -title: "Bundled page en" ---- --- content/section/mybundle/bundledpage.nn.md -- ---- -title: "Bundled page nn" ---- - --- layouts/_default/single.html -- -Bundled page: {{ .RelPermalink}}|Len resources: {{ len .Resources }}| - - -` - b := Test(t, files) - - b.AssertFileContent("public/en/section/mybundle/index.html", - "Bundled page: /en/section/mybundle/|Len resources: 1|", - ) - - b.AssertFileExists("public/nn/section/mybundle/index.html", false) -} - -// #6208 -func TestBundleIndexInSubFolder(t *testing.T) { - config := ` -baseURL = "https://example.com" - -` - - const pageContent = `--- -title: %q ---- -` - createPage := func(s string) string { - return fmt.Sprintf(pageContent, s) - } - - b := newTestSitesBuilder(t).WithConfigFile("toml", config) - b.WithLogger(loggers.NewDefault()) - - b.WithTemplates("_default/single.html", `{{ range .Resources }} -{{ .ResourceType }}|{{ .Title }}| -{{ end }} - - -`) - - b.WithContent("bundle/index.md", createPage("bundle index")) - b.WithContent("bundle/p1.md", createPage("bundle p1")) - b.WithContent("bundle/sub/p2.md", createPage("bundle sub p2")) - b.WithContent("bundle/sub/index.md", createPage("bundle sub index")) - b.WithContent("bundle/sub/data.json", "data") - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/bundle/index.html", ` - application|sub/data.json| - page|bundle p1| - page|bundle sub index| - page|bundle sub p2| -`) -} - -func TestBundleTransformMany(t *testing.T) { - b := newTestSitesBuilder(t).WithSimpleConfigFile().Running() - - for i := 1; i <= 50; i++ { - b.WithContent(fmt.Sprintf("bundle%d/index.md", i), fmt.Sprintf(` ---- -title: "Page" -weight: %d ---- - -`, i)) - b.WithSourceFile(fmt.Sprintf("content/bundle%d/data.yaml", i), fmt.Sprintf(`data: v%d`, i)) - b.WithSourceFile(fmt.Sprintf("content/bundle%d/data.json", i), fmt.Sprintf(`{ "data": "v%d" }`, i)) - b.WithSourceFile(fmt.Sprintf("assets/data%d/data.yaml", i), fmt.Sprintf(`vdata: v%d`, i)) - - } - - b.WithTemplatesAdded("_default/single.html", ` -{{ $bundleYaml := .Resources.GetMatch "*.yaml" }} -{{ $bundleJSON := .Resources.GetMatch "*.json" }} -{{ $assetsYaml := resources.GetMatch (printf "data%d/*.yaml" .Weight) }} -{{ $data1 := $bundleYaml | transform.Unmarshal }} -{{ $data2 := $assetsYaml | transform.Unmarshal }} -{{ $bundleFingerprinted := $bundleYaml | fingerprint "md5" }} -{{ $assetsFingerprinted := $assetsYaml | fingerprint "md5" }} -{{ $jsonMin := $bundleJSON | minify }} -{{ $jsonMinMin := $jsonMin | minify }} -{{ $jsonMinMinMin := $jsonMinMin | minify }} - -data content unmarshaled: {{ $data1.data }} -data assets content unmarshaled: {{ $data2.vdata }} -bundle fingerprinted: {{ $bundleFingerprinted.RelPermalink }} -assets fingerprinted: {{ $assetsFingerprinted.RelPermalink }} - -bundle min min min: {{ $jsonMinMinMin.RelPermalink }} -bundle min min key: {{ $jsonMinMin.Key }} - -`) - - for range 3 { - - b.Build(BuildCfg{}) - - for i := 1; i <= 50; i++ { - index := fmt.Sprintf("public/bundle%d/index.html", i) - b.AssertFileContent(fmt.Sprintf("public/bundle%d/data.yaml", i), fmt.Sprintf("data: v%d", i)) - b.AssertFileContent(index, fmt.Sprintf("data content unmarshaled: v%d", i)) - b.AssertFileContent(index, fmt.Sprintf("data assets content unmarshaled: v%d", i)) - - md5Asset := hashing.MD5FromStringHexEncoded(fmt.Sprintf(`vdata: v%d`, i)) - b.AssertFileContent(index, fmt.Sprintf("assets fingerprinted: /data%d/data.%s.yaml", i, md5Asset)) - - // The original is not used, make sure it's not published. - b.Assert(b.CheckExists(fmt.Sprintf("public/data%d/data.yaml", i)), qt.Equals, false) - - md5Bundle := hashing.MD5FromStringHexEncoded(fmt.Sprintf(`data: v%d`, i)) - b.AssertFileContent(index, fmt.Sprintf("bundle fingerprinted: /bundle%d/data.%s.yaml", i, md5Bundle)) - - b.AssertFileContent(index, - fmt.Sprintf("bundle min min min: /bundle%d/data.min.min.min.json", i), - fmt.Sprintf("bundle min min key: /bundle%d/data.min.min.json", i), - ) - b.Assert(b.CheckExists(fmt.Sprintf("public/bundle%d/data.min.min.min.json", i)), qt.Equals, true) - b.Assert(b.CheckExists(fmt.Sprintf("public/bundle%d/data.min.json", i)), qt.Equals, false) - b.Assert(b.CheckExists(fmt.Sprintf("public/bundle%d/data.min.min.json", i)), qt.Equals, false) - - } - - b.EditFiles("assets/data/foo.yaml", "FOO") - - } -} - -func TestPageBundlerHome(t *testing.T) { - t.Parallel() - c := qt.New(t) - - workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-bundler-home") - c.Assert(err, qt.IsNil) - - cfg := config.New() - cfg.Set("workingDir", workDir) - cfg.Set("publishDir", "public") - fs := hugofs.NewFromOld(hugofs.Os, cfg) - - os.MkdirAll(filepath.Join(workDir, "content"), 0o777) - - defer clean() - - b := newTestSitesBuilder(t) - b.Fs = fs - - b.WithWorkingDir(workDir).WithViper(cfg) - - b.WithContent("_index.md", "---\ntitle: Home\n---\n![Alt text](image.jpg)") - b.WithSourceFile("content/data.json", "DATA") - - b.WithTemplates("index.html", `Title: {{ .Title }}|First Resource: {{ index .Resources 0 }}|Content: {{ .Content }}`) - b.WithTemplates("_default/_markup/render-image.html", `Hook Len Page Resources {{ len .Page.Resources }}`) - - b.Build(BuildCfg{}) - b.AssertFileContent("public/index.html", ` -Title: Home|First Resource: data.json|Content: <p>Hook Len Page Resources 1</p> -`) -} - -func TestHTMLFilesIsue11999(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ["taxonomy", "term", "rss", "sitemap", "robotsTXT", "404"] -[permalinks] -posts = "/myposts/:slugorfilename" --- content/posts/markdown-without-frontmatter.md -- --- content/posts/html-without-frontmatter.html -- -<html>hello</html> --- content/posts/html-with-frontmatter.html -- ---- -title: "HTML with frontmatter" ---- -<html>hello</html> --- content/posts/html-with-commented-out-frontmatter.html -- -<!-- ---- -title: "HTML with commented out frontmatter" ---- ---> -<html>hello</html> --- content/posts/markdown-with-frontmatter.md -- ---- -title: "Markdown" ---- --- content/posts/mybundle/index.md -- ---- -title: My Bundle ---- --- content/posts/mybundle/data.txt -- -Data.txt --- content/posts/mybundle/html-in-bundle-without-frontmatter.html -- -<html>hell</html> --- content/posts/mybundle/html-in-bundle-with-frontmatter.html -- ---- -title: Hello ---- -<html>hello</html> --- content/posts/mybundle/html-in-bundle-with-commented-out-frontmatter.html -- -<!-- ---- -title: "HTML with commented out frontmatter" ---- ---> -<html>hello</html> --- layouts/index.html -- -{{ range site.RegularPages }}{{ .RelPermalink }}|{{ end }}$ --- layouts/_default/single.html -- -{{ .Title }}|{{ .RelPermalink }}Resources: {{ range .Resources }}{{ .Name }}|{{ end }}$ - -` - b := Test(t, files) - - b.AssertFileContent("public/index.html", "/myposts/html-with-commented-out-frontmatter/|/myposts/html-without-frontmatter/|/myposts/markdown-without-frontmatter/|/myposts/html-with-frontmatter/|/myposts/markdown-with-frontmatter/|/myposts/mybundle/|$") - - b.AssertFileContent("public/myposts/mybundle/index.html", - "My Bundle|/myposts/mybundle/Resources: html-in-bundle-with-commented-out-frontmatter.html|html-in-bundle-without-frontmatter.html|html-in-bundle-with-frontmatter.html|data.txt|$") - - b.AssertPublishDir(` -index.html -myposts/html-with-commented-out-frontmatter -myposts/html-with-commented-out-frontmatter/index.html -myposts/html-with-frontmatter -myposts/html-with-frontmatter/index.html -myposts/html-without-frontmatter -myposts/html-without-frontmatter/index.html -myposts/markdown-with-frontmatter -myposts/markdown-with-frontmatter/index.html -myposts/markdown-without-frontmatter -myposts/markdown-without-frontmatter/index.html -myposts/mybundle/data.txt -myposts/mybundle/index.html -! myposts/mybundle/html-in-bundle-with-frontmatter.html -`) -} - -func TestBundleDuplicatePagesAndResources(t *testing.T) { - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableKinds = ["taxonomy", "term"] --- content/mysection/mybundle/index.md -- --- content/mysection/mybundle/index.html -- --- content/mysection/mybundle/p1.md -- --- content/mysection/mybundle/p1.html -- --- content/mysection/mybundle/foo/p1.html -- --- content/mysection/mybundle/data.txt -- -Data txt. --- content/mysection/mybundle/data.en.txt -- -Data en txt. --- content/mysection/mybundle/data.json -- -Data JSON. --- content/mysection/_index.md -- --- content/mysection/_index.html -- --- content/mysection/sectiondata.json -- -Secion data JSON. --- content/mysection/sectiondata.txt -- -Section data TXT. --- content/mysection/p2.md -- --- content/mysection/p2.html -- --- content/mysection/foo/p2.md -- --- layouts/_default/single.html -- -Single:{{ .Title }}|{{ .Path }}|File LogicalName: {{ with .File }}{{ .LogicalName }}{{ end }}||{{ .RelPermalink }}|{{ .Kind }}|Resources: {{ range .Resources}}{{ .Name }}: {{ .Content }}|{{ end }}$ --- layouts/_default/list.html -- -List: {{ .Title }}|{{ .Path }}|File LogicalName: {{ with .File }}{{ .LogicalName }}{{ end }}|{{ .RelPermalink }}|{{ .Kind }}|Resources: {{ range .Resources}}{{ .Name }}: {{ .Content }}|{{ end }}$ -RegularPages: {{ range .RegularPages }}{{ .RelPermalink }}|File LogicalName: {{ with .File }}{{ .LogicalName }}|{{ end }}{{ end }}$ -` - - b := Test(t, files) - - // Note that the sort order gives us the most specific data file for the en language (the data.en.json). - b.AssertFileContent("public/mysection/mybundle/index.html", `Single:|/mysection/mybundle|File LogicalName: index.md||/mysection/mybundle/|page|Resources: data.en.txt: Data en txt.|data.json: Data JSON.|foo/p1.html: |p1.html: |p1.md: |$`) - b.AssertFileContent("public/mysection/index.html", - "List: |/mysection|File LogicalName: _index.md|/mysection/|section|Resources: sectiondata.json: Secion data JSON.|sectiondata.txt: Section data TXT.|$", - "RegularPages: /mysection/foo/p2/|File LogicalName: p2.md|/mysection/mybundle/|File LogicalName: index.md|/mysection/p2/|File LogicalName: p2.md|$") -} - -func TestBundleResourcesGetMatchOriginalName(t *testing.T) { - files := ` --- hugo.toml -- -baseURL = "https://example.com" --- content/mybundle/index.md -- --- content/mybundle/f1.en.txt -- -F1. --- layouts/_default/single.html -- -GetMatch: {{ with .Resources.GetMatch "f1.en.*" }}{{ .Name }}: {{ .Content }}|{{ end }} -Match: {{ range .Resources.Match "f1.En.*" }}{{ .Name }}: {{ .Content }}|{{ end }} -` - - b := Test(t, files) - - b.AssertFileContent("public/mybundle/index.html", "GetMatch: f1.en.txt: F1.|", "Match: f1.en.txt: F1.|") -} - -func TestBundleResourcesWhenLanguageVariantIsDraft(t *testing.T) { - files := ` --- hugo.toml -- -baseURL = "https://example.com" -defaultContentLanguage = "en" -[languages] -[languages.en] -weight = 1 -[languages.nn] -weight = 2 --- content/mybundle/index.en.md -- --- content/mybundle/index.nn.md -- ---- -draft: true ---- --- content/mybundle/f1.en.txt -- -F1. --- layouts/_default/single.html -- -GetMatch: {{ with .Resources.GetMatch "f1.*" }}{{ .Name }}: {{ .Content }}|{{ end }}$ -` - - b := Test(t, files) - - b.AssertFileContent("public/mybundle/index.html", "GetMatch: f1.en.txt: F1.|") -} - -func TestBundleBranchIssue12320(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ['rss','sitemap','taxonomy','term'] -defaultContentLanguage = 'en' -defaultContentLanguageInSubdir = true -[languages.en] -baseURL = "https://en.example.org/" -contentDir = "content/en" -[languages.fr] -baseURL = "https://fr.example.org/" -contentDir = "content/fr" --- content/en/s1/p1.md -- ---- -title: p1 ---- --- content/en/s1/p1.txt -- ---- -p1.txt ---- --- layouts/_default/single.html -- -{{ .Title }}| --- layouts/_default/list.html -- -{{ .Title }}| -` - - b := Test(t, files) - - b.AssertFileExists("public/en/s1/index.html", true) - b.AssertFileExists("public/en/s1/p1/index.html", true) - b.AssertFileExists("public/en/s1/p1.txt", true) - - b.AssertFileExists("public/fr/s1/index.html", false) - b.AssertFileExists("public/fr/s1/p1/index.html", false) - b.AssertFileExists("public/fr/s1/p1.txt", false) // failing test -} diff --git a/hugolib/pagecollections.go b/hugolib/pagecollections.go deleted file mode 100644 index f1038deff..000000000 --- a/hugolib/pagecollections.go +++ /dev/null @@ -1,256 +0,0 @@ -// 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 hugolib - -import ( - "fmt" - "path" - "path/filepath" - "strings" - - "github.com/gohugoio/hugo/hugofs" - "github.com/gohugoio/hugo/hugofs/files" - - "github.com/gohugoio/hugo/common/paths" - - "github.com/gohugoio/hugo/resources/kinds" - "github.com/gohugoio/hugo/resources/page" -) - -// pageFinder provides ways to find a Page in a Site. -type pageFinder struct { - pageMap *pageMap -} - -func newPageFinder(m *pageMap) *pageFinder { - if m == nil { - panic("must provide a pageMap") - } - c := &pageFinder{pageMap: m} - return c -} - -// getPageRef resolves a Page from ref/relRef, with a slightly more comprehensive -// search path than getPage. -func (c *pageFinder) getPageRef(context page.Page, ref string) (page.Page, error) { - n, err := c.getContentNode(context, true, ref) - if err != nil { - return nil, err - } - - if p, ok := n.(page.Page); ok { - return p, nil - } - return nil, nil -} - -func (c *pageFinder) getPage(context page.Page, ref string) (page.Page, error) { - n, err := c.getContentNode(context, false, ref) - if err != nil { - return nil, err - } - if p, ok := n.(page.Page); ok { - return p, nil - } - return nil, nil -} - -// Only used in tests. -func (c *pageFinder) getPageOldVersion(kind string, sections ...string) page.Page { - refs := append([]string{kind}, path.Join(sections...)) - p, _ := c.getPageForRefs(refs...) - return p -} - -// This is an adapter func for the old API with Kind as first argument. -// This is invoked when you do .Site.GetPage. We drop the Kind and fails -// if there are more than 2 arguments, which would be ambiguous. -func (c *pageFinder) getPageForRefs(ref ...string) (page.Page, error) { - var refs []string - for _, r := range ref { - // A common construct in the wild is - // .Site.GetPage "home" "" or - // .Site.GetPage "home" "/" - if r != "" && r != "/" { - refs = append(refs, r) - } - } - - var key string - - if len(refs) > 2 { - // This was allowed in Hugo <= 0.44, but we cannot support this with the - // new API. This should be the most unusual case. - return nil, fmt.Errorf(`too many arguments to .Site.GetPage: %v. Use lookups on the form {{ .Site.GetPage "/posts/mypage-md" }}`, ref) - } - - if len(refs) == 0 || refs[0] == kinds.KindHome { - key = "/" - } else if len(refs) == 1 { - if len(ref) == 2 && refs[0] == kinds.KindSection { - // This is an old style reference to the "Home Page section". - // Typically fetched via {{ .Site.GetPage "section" .Section }} - // See https://github.com/gohugoio/hugo/issues/4989 - key = "/" - } else { - key = refs[0] - } - } else { - key = refs[1] - } - - return c.getPage(nil, key) -} - -const defaultContentExt = ".md" - -func (c *pageFinder) getContentNode(context page.Page, isReflink bool, ref string) (contentNodeI, error) { - ref = paths.ToSlashTrimTrailing(ref) - inRef := ref - if ref == "" { - ref = "/" - } - - if paths.HasExt(ref) { - return c.getContentNodeForRef(context, isReflink, true, inRef, ref) - } - - // We are always looking for a content file and having an extension greatly simplifies the code that follows, - // even in the case where the extension does not match this one. - if ref == "/" { - if n, err := c.getContentNodeForRef(context, isReflink, false, inRef, "/_index"+defaultContentExt); n != nil || err != nil { - return n, err - } - } else if strings.HasSuffix(ref, "/index") { - if n, err := c.getContentNodeForRef(context, isReflink, false, inRef, ref+"/index"+defaultContentExt); n != nil || err != nil { - return n, err - } - if n, err := c.getContentNodeForRef(context, isReflink, false, inRef, ref+defaultContentExt); n != nil || err != nil { - return n, err - } - } else { - if n, err := c.getContentNodeForRef(context, isReflink, false, inRef, ref+defaultContentExt); n != nil || err != nil { - return n, err - } - } - - return nil, nil -} - -func (c *pageFinder) getContentNodeForRef(context page.Page, isReflink, hadExtension bool, inRef, ref string) (contentNodeI, error) { - s := c.pageMap.s - contentPathParser := s.Conf.PathParser() - - if context != nil && !strings.HasPrefix(ref, "/") { - // Try the page-relative path first. - // Branch pages: /mysection, "./mypage" => /mysection/mypage - // Regular pages: /mysection/mypage.md, Path=/mysection/mypage, "./someotherpage" => /mysection/mypage/../someotherpage - // Regular leaf bundles: /mysection/mypage/index.md, Path=/mysection/mypage, "./someotherpage" => /mysection/mypage/../someotherpage - // Given the above, for regular pages we use the containing folder. - var baseDir string - if pi := context.PathInfo(); pi != nil { - if pi.IsBranchBundle() || (hadExtension && strings.HasPrefix(ref, "../")) { - baseDir = pi.Dir() - } else { - baseDir = pi.ContainerDir() - } - } - - rel := path.Join(baseDir, ref) - - relPath, _ := contentPathParser.ParseBaseAndBaseNameNoIdentifier(files.ComponentFolderContent, rel) - - n, err := c.getContentNodeFromPath(relPath, ref) - if n != nil || err != nil { - return n, err - } - - if hadExtension && context.File() != nil { - if n, err := c.getContentNodeFromRefReverseLookup(inRef, context.File().FileInfo()); n != nil || err != nil { - return n, err - } - } - - } - - if strings.HasPrefix(ref, ".") { - // Page relative, no need to look further. - return nil, nil - } - - relPath, nameNoIdentifier := contentPathParser.ParseBaseAndBaseNameNoIdentifier(files.ComponentFolderContent, ref) - - n, err := c.getContentNodeFromPath(relPath, ref) - - if n != nil || err != nil { - return n, err - } - - if hadExtension && s.home != nil && s.home.File() != nil { - if n, err := c.getContentNodeFromRefReverseLookup(inRef, s.home.File().FileInfo()); n != nil || err != nil { - return n, err - } - } - - var doSimpleLookup bool - if isReflink || context == nil { - slashCount := strings.Count(inRef, "/") - doSimpleLookup = slashCount == 0 - } - - if !doSimpleLookup { - return nil, nil - } - - n = c.pageMap.pageReverseIndex.Get(nameNoIdentifier) - if n == ambiguousContentNode { - return nil, fmt.Errorf("page reference %q is ambiguous", inRef) - } - - return n, nil -} - -func (c *pageFinder) getContentNodeFromRefReverseLookup(ref string, fi hugofs.FileMetaInfo) (contentNodeI, error) { - s := c.pageMap.s - meta := fi.Meta() - dir := meta.Filename - if !fi.IsDir() { - dir = filepath.Dir(meta.Filename) - } - - realFilename := filepath.Join(dir, ref) - - pcs, err := s.BaseFs.Content.ReverseLookup(realFilename, true) - if err != nil { - return nil, err - } - - // There may be multiple matches, but we will only use the first one. - for _, pc := range pcs { - pi := s.Conf.PathParser().Parse(pc.Component, pc.Path) - if n := c.pageMap.treePages.Get(pi.Base()); n != nil { - return n, nil - } - } - return nil, nil -} - -func (c *pageFinder) getContentNodeFromPath(s string, ref string) (contentNodeI, error) { - n := c.pageMap.treePages.Get(s) - if n != nil { - return n, nil - } - - return nil, nil -} diff --git a/hugolib/pagecollections_test.go b/hugolib/pagecollections_test.go deleted file mode 100644 index 10c973b7e..000000000 --- a/hugolib/pagecollections_test.go +++ /dev/null @@ -1,757 +0,0 @@ -// Copyright 2019 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 hugolib - -import ( - "fmt" - "math/rand" - "path" - "path/filepath" - "testing" - "time" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/resources/kinds" - "github.com/gohugoio/hugo/resources/page" - - "github.com/gohugoio/hugo/deps" -) - -const pageCollectionsPageTemplate = `--- -title: "%s" -categories: -- Hugo ---- -# Doc -` - -func BenchmarkGetPage(b *testing.B) { - var ( - cfg, fs = newTestCfg() - r = rand.New(rand.NewSource(time.Now().UnixNano())) - ) - - configs, err := loadTestConfigFromProvider(cfg) - if err != nil { - b.Fatal(err) - } - - for i := range 10 { - for j := range 100 { - writeSource(b, fs, filepath.Join("content", fmt.Sprintf("sect%d", i), fmt.Sprintf("page%d.md", j)), "CONTENT") - } - } - - s := buildSingleSite(b, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{SkipRender: true}) - - pagePaths := make([]string, b.N) - - for i := 0; i < b.N; i++ { - pagePaths[i] = fmt.Sprintf("sect%d", r.Intn(10)) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - home, _ := s.getPage(nil, "/") - if home == nil { - b.Fatal("Home is nil") - } - - p, _ := s.getPage(nil, pagePaths[i]) - if p == nil { - b.Fatal("Section is nil") - } - - } -} - -func createGetPageRegularBenchmarkSite(t testing.TB) *Site { - var ( - c = qt.New(t) - cfg, fs = newTestCfg() - ) - - configs, err := loadTestConfigFromProvider(cfg) - if err != nil { - t.Fatal(err) - } - - pc := func(title string) string { - return fmt.Sprintf(pageCollectionsPageTemplate, title) - } - - for i := range 10 { - for j := range 100 { - content := pc(fmt.Sprintf("Title%d_%d", i, j)) - writeSource(c, fs, filepath.Join("content", fmt.Sprintf("sect%d", i), fmt.Sprintf("page%d.md", j)), content) - } - } - - return buildSingleSite(c, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{SkipRender: true}) -} - -func TestBenchmarkGetPageRegular(t *testing.T) { - c := qt.New(t) - s := createGetPageRegularBenchmarkSite(t) - - for i := range 10 { - pp := path.Join("/", fmt.Sprintf("sect%d", i), fmt.Sprintf("page%d.md", i)) - page, _ := s.getPage(nil, pp) - c.Assert(page, qt.Not(qt.IsNil), qt.Commentf(pp)) - } -} - -func BenchmarkGetPageRegular(b *testing.B) { - r := rand.New(rand.NewSource(time.Now().UnixNano())) - - b.Run("From root", func(b *testing.B) { - s := createGetPageRegularBenchmarkSite(b) - c := qt.New(b) - - pagePaths := make([]string, b.N) - - for i := 0; i < b.N; i++ { - pagePaths[i] = path.Join(fmt.Sprintf("/sect%d", r.Intn(10)), fmt.Sprintf("page%d.md", r.Intn(100))) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - page, _ := s.getPage(nil, pagePaths[i]) - c.Assert(page, qt.Not(qt.IsNil)) - } - }) - - b.Run("Page relative", func(b *testing.B) { - s := createGetPageRegularBenchmarkSite(b) - c := qt.New(b) - allPages := s.RegularPages() - - pagePaths := make([]string, b.N) - pages := make([]page.Page, b.N) - - for i := 0; i < b.N; i++ { - pagePaths[i] = fmt.Sprintf("page%d.md", r.Intn(100)) - pages[i] = allPages[r.Intn(len(allPages)/3)] - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - page, _ := s.getPage(pages[i], pagePaths[i]) - c.Assert(page, qt.Not(qt.IsNil)) - } - }) -} - -type getPageTest struct { - name string - kind string - context page.Page - pathVariants []string - expectedTitle string -} - -func (t *getPageTest) check(p page.Page, err error, errorMsg string, c *qt.C) { - c.Helper() - errorComment := qt.Commentf(errorMsg) - switch t.kind { - case "Ambiguous": - c.Assert(err, qt.Not(qt.IsNil)) - c.Assert(p, qt.IsNil, errorComment) - case "NoPage": - c.Assert(err, qt.IsNil) - c.Assert(p, qt.IsNil, errorComment) - default: - c.Assert(err, qt.IsNil, errorComment) - c.Assert(p, qt.Not(qt.IsNil), errorComment) - c.Assert(p.Kind(), qt.Equals, t.kind, errorComment) - c.Assert(p.Title(), qt.Equals, t.expectedTitle, errorComment) - } -} - -func TestGetPage(t *testing.T) { - var ( - cfg, fs = newTestCfg() - c = qt.New(t) - ) - - configs, err := loadTestConfigFromProvider(cfg) - c.Assert(err, qt.IsNil) - - pc := func(title string) string { - return fmt.Sprintf(pageCollectionsPageTemplate, title) - } - - for i := range 10 { - for j := range 10 { - content := pc(fmt.Sprintf("Title%d_%d", i, j)) - writeSource(t, fs, filepath.Join("content", fmt.Sprintf("sect%d", i), fmt.Sprintf("page%d.md", j)), content) - } - } - - content := pc("home page") - writeSource(t, fs, filepath.Join("content", "_index.md"), content) - - content = pc("about page") - writeSource(t, fs, filepath.Join("content", "about.md"), content) - - content = pc("section 3") - writeSource(t, fs, filepath.Join("content", "sect3", "_index.md"), content) - - writeSource(t, fs, filepath.Join("content", "sect3", "unique.md"), pc("UniqueBase")) - writeSource(t, fs, filepath.Join("content", "sect3", "Unique2.md"), pc("UniqueBase2")) - - content = pc("another sect7") - writeSource(t, fs, filepath.Join("content", "sect3", "sect7", "_index.md"), content) - - content = pc("deep page") - writeSource(t, fs, filepath.Join("content", "sect3", "subsect", "deep.md"), content) - - // Bundle variants - writeSource(t, fs, filepath.Join("content", "sect3", "b1", "index.md"), pc("b1 bundle")) - writeSource(t, fs, filepath.Join("content", "sect3", "index", "index.md"), pc("index bundle")) - - writeSource(t, fs, filepath.Join("content", "section_bundle_overlap", "_index.md"), pc("index overlap section")) - writeSource(t, fs, filepath.Join("content", "section_bundle_overlap_bundle", "index.md"), pc("index overlap bundle")) - - s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{SkipRender: true}) - - sec3, err := s.getPage(nil, "/sect3") - c.Assert(err, qt.IsNil) - c.Assert(sec3, qt.Not(qt.IsNil)) - - tests := []getPageTest{ - // legacy content root relative paths - {"Root relative, no slash, home", kinds.KindHome, nil, []string{""}, "home page"}, - {"Root relative, no slash, root page", kinds.KindPage, nil, []string{"about.md", "ABOUT.md"}, "about page"}, - {"Root relative, no slash, section", kinds.KindSection, nil, []string{"sect3"}, "section 3"}, - {"Root relative, no slash, section page", kinds.KindPage, nil, []string{"sect3/page1.md"}, "Title3_1"}, - {"Root relative, no slash, sub section", kinds.KindSection, nil, []string{"sect3/sect7"}, "another sect7"}, - {"Root relative, no slash, nested page", kinds.KindPage, nil, []string{"sect3/subsect/deep.md"}, "deep page"}, - {"Root relative, no slash, OS slashes", kinds.KindPage, nil, []string{filepath.FromSlash("sect5/page3.md")}, "Title5_3"}, - - {"Short ref, unique", kinds.KindPage, nil, []string{"unique.md", "unique"}, "UniqueBase"}, - {"Short ref, unique, upper case", kinds.KindPage, nil, []string{"Unique2.md", "unique2.md", "unique2"}, "UniqueBase2"}, - {"Short ref, ambiguous", "Ambiguous", nil, []string{"page1.md"}, ""}, - - // ISSUE: This is an ambiguous ref, but because we have to support the legacy - // content root relative paths without a leading slash, the lookup - // returns /sect7. This undermines ambiguity detection, but we have no choice. - //{"Ambiguous", nil, []string{"sect7"}, ""}, - {"Section, ambiguous", kinds.KindSection, nil, []string{"sect7"}, "Sect7s"}, - - {"Absolute, home", kinds.KindHome, nil, []string{"/", ""}, "home page"}, - {"Absolute, page", kinds.KindPage, nil, []string{"/about.md", "/about"}, "about page"}, - {"Absolute, sect", kinds.KindSection, nil, []string{"/sect3"}, "section 3"}, - {"Absolute, page in subsection", kinds.KindPage, nil, []string{"/sect3/page1.md", "/Sect3/Page1.md"}, "Title3_1"}, - {"Absolute, section, subsection with same name", kinds.KindSection, nil, []string{"/sect3/sect7"}, "another sect7"}, - {"Absolute, page, deep", kinds.KindPage, nil, []string{"/sect3/subsect/deep.md"}, "deep page"}, - {"Absolute, page, OS slashes", kinds.KindPage, nil, []string{filepath.FromSlash("/sect5/page3.md")}, "Title5_3"}, // test OS-specific path - {"Absolute, unique", kinds.KindPage, nil, []string{"/sect3/unique.md"}, "UniqueBase"}, - {"Absolute, unique, case", kinds.KindPage, nil, []string{"/sect3/Unique2.md", "/sect3/unique2.md", "/sect3/unique2", "/sect3/Unique2"}, "UniqueBase2"}, - // next test depends on this page existing - // {"NoPage", nil, []string{"/unique.md"}, ""}, // ISSUE #4969: this is resolving to /sect3/unique.md - {"Absolute, missing page", "NoPage", nil, []string{"/missing-page.md"}, ""}, - {"Absolute, missing section", "NoPage", nil, []string{"/missing-section"}, ""}, - - // relative paths - {"Dot relative, home", kinds.KindHome, sec3, []string{".."}, "home page"}, - {"Dot relative, home, slash", kinds.KindHome, sec3, []string{"../"}, "home page"}, - {"Dot relative about", kinds.KindPage, sec3, []string{"../about.md"}, "about page"}, - {"Dot", kinds.KindSection, sec3, []string{"."}, "section 3"}, - {"Dot slash", kinds.KindSection, sec3, []string{"./"}, "section 3"}, - {"Page relative, no dot", kinds.KindPage, sec3, []string{"page1.md"}, "Title3_1"}, - {"Page relative, dot", kinds.KindPage, sec3, []string{"./page1.md"}, "Title3_1"}, - {"Up and down another section", kinds.KindPage, sec3, []string{"../sect4/page2.md"}, "Title4_2"}, - {"Rel sect7", kinds.KindSection, sec3, []string{"sect7"}, "another sect7"}, - {"Rel sect7 dot", kinds.KindSection, sec3, []string{"./sect7"}, "another sect7"}, - {"Dot deep", kinds.KindPage, sec3, []string{"./subsect/deep.md"}, "deep page"}, - {"Dot dot inner", kinds.KindPage, sec3, []string{"./subsect/../../sect7/page9.md"}, "Title7_9"}, - {"Dot OS slash", kinds.KindPage, sec3, []string{filepath.FromSlash("../sect5/page3.md")}, "Title5_3"}, // test OS-specific path - {"Dot unique", kinds.KindPage, sec3, []string{"./unique.md"}, "UniqueBase"}, - {"Dot sect", "NoPage", sec3, []string{"./sect2"}, ""}, - //{"NoPage", sec3, []string{"sect2"}, ""}, // ISSUE: /sect3 page relative query is resolving to /sect2 - - {"Abs, ignore context, home", kinds.KindHome, sec3, []string{"/"}, "home page"}, - {"Abs, ignore context, about", kinds.KindPage, sec3, []string{"/about.md"}, "about page"}, - {"Abs, ignore context, page in section", kinds.KindPage, sec3, []string{"/sect4/page2.md"}, "Title4_2"}, - {"Abs, ignore context, page subsect deep", kinds.KindPage, sec3, []string{"/sect3/subsect/deep.md"}, "deep page"}, // next test depends on this page existing - {"Abs, ignore context, page deep", "NoPage", sec3, []string{"/subsect/deep.md"}, ""}, - - // Taxonomies - {"Taxonomy term", kinds.KindTaxonomy, nil, []string{"categories"}, "Categories"}, - {"Taxonomy", kinds.KindTerm, nil, []string{"categories/hugo", "categories/Hugo"}, "Hugo"}, - - // Bundle variants - {"Bundle regular", kinds.KindPage, nil, []string{"sect3/b1", "sect3/b1/index.md", "sect3/b1/index.en.md"}, "b1 bundle"}, - {"Bundle index name", kinds.KindPage, nil, []string{"sect3/index/index.md", "sect3/index"}, "index bundle"}, - - // https://github.com/gohugoio/hugo/issues/7301 - {"Section and bundle overlap", kinds.KindPage, nil, []string{"section_bundle_overlap_bundle"}, "index overlap bundle"}, - } - - for _, test := range tests { - c.Run(test.name, func(c *qt.C) { - errorMsg := fmt.Sprintf("Test case %v %v -> %s", test.context, test.pathVariants, test.expectedTitle) - - // test legacy public Site.GetPage (which does not support page context relative queries) - if test.context == nil { - for _, ref := range test.pathVariants { - args := append([]string{test.kind}, ref) - page, err := s.GetPage(args...) - test.check(page, err, errorMsg, c) - } - } - - // test new internal Site.getPage - for _, ref := range test.pathVariants { - page2, err := s.getPage(test.context, ref) - test.check(page2, err, errorMsg, c) - } - }) - } -} - -// #11664 -func TestGetPageIndexIndex(t *testing.T) { - files := ` --- hugo.toml -- -disableKinds = ["taxonomy", "term"] --- content/mysect/index/index.md -- ---- -title: "Mysect Index" ---- --- layouts/index.html -- -GetPage 1: {{ with site.GetPage "mysect/index/index.md" }}{{ .Title }}|{{ .RelPermalink }}|{{ .Path }}{{ end }}| -GetPage 2: {{ with site.GetPage "mysect/index" }}{{ .Title }}|{{ .RelPermalink }}|{{ .Path }}{{ end }}| -` - - b := Test(t, files) - b.AssertFileContent("public/index.html", - "GetPage 1: Mysect Index|/mysect/index/|/mysect/index|", - "GetPage 2: Mysect Index|/mysect/index/|/mysect/index|", - ) -} - -// https://github.com/gohugoio/hugo/issues/6034 -func TestGetPageRelative(t *testing.T) { - b := newTestSitesBuilder(t) - for i, section := range []string{"what", "where", "who"} { - isDraft := i == 2 - b.WithContent( - section+"/_index.md", fmt.Sprintf("---title: %s\n---", section), - section+"/members.md", fmt.Sprintf("---title: members %s\ndraft: %t\n---", section, isDraft), - ) - } - - b.WithTemplates("_default/list.html", ` -{{ with .GetPage "members.md" }} - Members: {{ .Title }} -{{ else }} -NOT FOUND -{{ end }} -`) - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/what/index.html", `Members: members what`) - b.AssertFileContent("public/where/index.html", `Members: members where`) - b.AssertFileContent("public/who/index.html", `NOT FOUND`) -} - -func TestGetPageIssue11883(t *testing.T) { - files := ` --- hugo.toml -- --- p1/index.md -- ---- -title: p1 ---- --- p1/p1.xyz -- -xyz. --- layouts/index.html -- -Home. {{ with .Page.GetPage "p1.xyz" }}{{ else }}OK 1{{ end }} {{ with .Site.GetPage "p1.xyz" }}{{ else }}OK 2{{ end }} -` - - b := Test(t, files) - b.AssertFileContent("public/index.html", "Home. OK 1 OK 2") -} - -func TestGetPageIssue12120(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ['home','rss','section','sitemap','taxonomy','term'] --- content/s1/p1/index.md -- ---- -title: p1 -layout: p1 ---- --- content/s1/p2.md -- ---- -title: p2 -layout: p2 ---- --- layouts/_default/p1.html -- -{{ (.GetPage "p2.md").Title }}| --- layouts/_default/p2.html -- -{{ (.GetPage "p1").Title }}| -` - - b := Test(t, files) - b.AssertFileContent("public/s1/p1/index.html", "p2") // failing test - b.AssertFileContent("public/s1/p2/index.html", "p1") -} - -func TestGetPageNewsVsTagsNewsIssue12638(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ['rss','section','sitemap'] -[taxonomies] - tag = "tags" --- content/p1.md -- ---- -title: p1 -tags: [news] ---- --- layouts/index.html -- -/tags/news: {{ with .Site.GetPage "/tags/news" }}{{ .Title }}{{ end }}| -news: {{ with .Site.GetPage "news" }}{{ .Title }}{{ end }}| -/news: {{ with .Site.GetPage "/news" }}{{ .Title }}{{ end }}| - -` - - b := Test(t, files) - - b.AssertFileContent("public/index.html", - "/tags/news: News|", - "news: News|", - "/news: |", - ) -} - -func TestGetPageBundleToRegular(t *testing.T) { - files := ` --- hugo.toml -- --- content/s1/p1/index.md -- ---- -title: p1 ---- --- content/s1/p2.md -- ---- -title: p2 ---- --- layouts/_default/single.html -- -{{ with .GetPage "p2" }} - OK: {{ .LinkTitle }} -{{ else }} - Unable to get p2. -{{ end }} -` - - b := Test(t, files) - b.AssertFileContent("public/s1/p1/index.html", "OK: p2") - b.AssertFileContent("public/s1/p2/index.html", "OK: p2") -} - -func TestPageGetPageVariations(t *testing.T) { - files := ` --- hugo.toml -- --- content/s1/_index.md -- ---- -title: s1 section ---- --- content/s1/p1/index.md -- ---- -title: p1 ---- --- content/s1/p2.md -- ---- -title: p2 ---- --- content/s2/p3/index.md -- ---- -title: p3 ---- --- content/p2.md -- ---- -title: p2_root ---- --- layouts/index.html -- -/s1: {{ with .GetPage "/s1" }}{{ .Title }}{{ end }}| -/s1/: {{ with .GetPage "/s1/" }}{{ .Title }}{{ end }}| -/s1/p2.md: {{ with .GetPage "/s1/p2.md" }}{{ .Title }}{{ end }}| -/s1/p2: {{ with .GetPage "/s1/p2" }}{{ .Title }}{{ end }}| -/s1/p1/index.md: {{ with .GetPage "/s1/p1/index.md" }}{{ .Title }}{{ end }}| -/s1/p1: {{ with .GetPage "/s1/p1" }}{{ .Title }}{{ end }}| --- layouts/_default/single.html -- -../p2: {{ with .GetPage "../p2" }}{{ .Title }}{{ end }}| -../p2.md: {{ with .GetPage "../p2.md" }}{{ .Title }}{{ end }}| -p1/index.md: {{ with .GetPage "p1/index.md" }}{{ .Title }}{{ end }}| -../s2/p3/index.md: {{ with .GetPage "../s2/p3/index.md" }}{{ .Title }}{{ end }}| -` - - b := Test(t, files) - - b.AssertFileContent("public/index.html", ` -/s1: s1 section| -/s1/: s1 section| -/s1/p2.md: p2| -/s1/p2: p2| -/s1/p1/index.md: p1| -/s1/p1: p1| -`) - - b.AssertFileContent("public/s1/p1/index.html", ` -../p2: p2_root| -../p2.md: p2| - -`) - - b.AssertFileContent("public/s1/p2/index.html", ` -../p2: p2_root| -../p2.md: p2_root| -p1/index.md: p1| -../s2/p3/index.md: p3| - -`) -} - -func TestPageGetPageMountsReverseLookup(t *testing.T) { - tempDir := t.TempDir() - - files := ` --- README.md -- ---- -title: README ---- --- blog/b1.md -- ---- -title: b1 ---- -{{< ref "../docs/d1.md" >}} --- blog/b2/index.md -- ---- -title: b2 ---- -{{< ref "../../docs/d1.md" >}} --- docs/d1.md -- ---- -title: d1 ---- --- hugo.toml -- -baseURL = "https://example.com/" -[module] -[[module.mounts]] -source = "layouts" -target = "layouts" -[[module.mounts]] -source = "README.md" -target = "content/_index.md" -[[module.mounts]] -source = "blog" -target = "content/posts" -[[module.mounts]] -source = "docs" -target = "content/mydocs" --- layouts/shortcodes/ref.html -- -{{ $ref := .Get 0 }} -.Page.GetPage({{ $ref }}).Title: {{ with .Page.GetPage $ref }}{{ .Title }}{{ end }}| --- layouts/index.html -- -Home. -/blog/b1.md: {{ with .GetPage "/blog/b1.md" }}{{ .Title }}{{ end }}| -/blog/b2/index.md: {{ with .GetPage "/blog/b2/index.md" }}{{ .Title }}{{ end }}| -/docs/d1.md: {{ with .GetPage "/docs/d1.md" }}{{ .Title }}{{ end }}| -/README.md: {{ with .GetPage "/README.md" }}{{ .Title }}{{ end }}| --- layouts/_default/single.html -- -Single. -/README.md: {{ with .GetPage "/README.md" }}{{ .Title }}{{ end }}| -{{ .Content }} - - -` - b := Test(t, files, TestOptWithConfig(func(cfg *IntegrationTestConfig) { cfg.WorkingDir = tempDir })) - - b.AssertFileContent("public/index.html", - ` -/blog/b1.md: b1| -/blog/b2/index.md: b2| -/docs/d1.md: d1| -/README.md: README -`, - ) - - b.AssertFileContent("public/mydocs/d1/index.html", `README.md: README|`) - - b.AssertFileContent("public/posts/b1/index.html", `.Page.GetPage(../docs/d1.md).Title: d1|`) - b.AssertFileContent("public/posts/b2/index.html", `.Page.GetPage(../../docs/d1.md).Title: d1|`) -} - -// https://github.com/gohugoio/hugo/issues/7016 -func TestGetPageMultilingual(t *testing.T) { - b := newTestSitesBuilder(t) - - b.WithConfigFile("yaml", ` -baseURL: "http://example.org/" -languageCode: "en-us" -defaultContentLanguage: ru -title: "My New Hugo Site" -uglyurls: true - -languages: - ru: {} - en: {} -`) - - b.WithContent( - "docs/1.md", "\n---title: p1\n---", - "news/1.md", "\n---title: p1\n---", - "news/1.en.md", "\n---title: p1en\n---", - "news/about/1.md", "\n---title: about1\n---", - "news/about/1.en.md", "\n---title: about1en\n---", - ) - - b.WithTemplates("index.html", ` -{{ with site.GetPage "docs/1" }} - Docs p1: {{ .Title }} -{{ else }} -NOT FOUND -{{ end }} -`) - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", `Docs p1: p1`) - b.AssertFileContent("public/en/index.html", `NOT FOUND`) -} - -func TestRegularPagesRecursive(t *testing.T) { - b := newTestSitesBuilder(t) - - b.WithConfigFile("yaml", ` -baseURL: "http://example.org/" -title: "My New Hugo Site" - -`) - - b.WithContent( - "docs/1.md", "\n---title: docs1\n---", - "docs/sect1/_index.md", "\n---title: docs_sect1\n---", - "docs/sect1/ps1.md", "\n---title: docs_sect1_ps1\n---", - "docs/sect1/ps2.md", "\n---title: docs_sect1_ps2\n---", - "docs/sect1/sect1_s2/_index.md", "\n---title: docs_sect1_s2\n---", - "docs/sect1/sect1_s2/ps2_1.md", "\n---title: docs_sect1_s2_1\n---", - "docs/sect2/_index.md", "\n---title: docs_sect2\n---", - "docs/sect2/ps1.md", "\n---title: docs_sect2_ps1\n---", - "docs/sect2/ps2.md", "\n---title: docs_sect2_ps2\n---", - "news/1.md", "\n---title: news1\n---", - ) - - b.WithTemplates("index.html", ` -{{ $sect1 := site.GetPage "sect1" }} - -Sect1 RegularPagesRecursive: {{ range $sect1.RegularPagesRecursive }}{{ .Kind }}:{{ .RelPermalink}}|{{ end }}|End. - -`) - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", ` -Sect1 RegularPagesRecursive: page:/docs/sect1/ps1/|page:/docs/sect1/ps2/|page:/docs/sect1/sect1_s2/ps2_1/||End. - - -`) -} - -func TestRegularPagesRecursiveHome(t *testing.T) { - files := ` --- hugo.toml -- --- content/p1.md -- --- content/post/p2.md -- --- layouts/index.html -- -RegularPagesRecursive: {{ range .RegularPagesRecursive }}{{ .Kind }}:{{ .RelPermalink}}|{{ end }}|End. -` - - b := NewIntegrationTestBuilder( - IntegrationTestConfig{ - T: t, - TxtarString: files, - }).Build() - - b.AssertFileContent("public/index.html", `RegularPagesRecursive: page:/p1/|page:/post/p2/||End.`) -} - -// Issue #12169. -func TestPagesSimilarSectionNames(t *testing.T) { - files := ` --- hugo.toml -- --- content/draftsection/_index.md -- ---- -draft: true ---- --- content/draftsection/sub/_index.md --got --- content/draftsection/sub/d1.md -- --- content/s1/_index.md -- --- content/s1/p1.md -- --- content/s1-foo/_index.md -- --- content/s1-foo/p2.md -- --- content/s1-foo/s2/_index.md -- --- content/s1-foo/s2/p3.md -- --- content/s1-foo/s2-foo/_index.md -- --- content/s1-foo/s2-foo/p4.md -- --- layouts/_default/list.html -- -{{ .RelPermalink }}: Pages: {{ range .Pages }}{{ .RelPermalink }}|{{ end }}$ - -` - b := Test(t, files) - - b.AssertFileContent("public/index.html", "/: Pages: /s1-foo/|/s1/|$") - b.AssertFileContent("public/s1/index.html", "/s1/: Pages: /s1/p1/|$") - b.AssertFileContent("public/s1-foo/index.html", "/s1-foo/: Pages: /s1-foo/p2/|/s1-foo/s2-foo/|/s1-foo/s2/|$") - b.AssertFileContent("public/s1-foo/s2/index.html", "/s1-foo/s2/: Pages: /s1-foo/s2/p3/|$") -} - -func TestGetPageContentAdapterBaseIssue12561(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ['rss','section','sitemap','taxonomy','term'] --- layouts/index.html -- -Test A: {{ (site.GetPage "/s1/p1").Title }} -Test B: {{ (site.GetPage "p1").Title }} -Test C: {{ (site.GetPage "/s2/p2").Title }} -Test D: {{ (site.GetPage "p2").Title }} --- layouts/_default/single.html -- -{{ .Title }} --- content/s1/p1.md -- ---- -title: p1 ---- --- content/s2/_content.gotmpl -- -{{ .AddPage (dict "path" "p2" "title" "p2") }} -` - - b := Test(t, files) - - b.AssertFileExists("public/s1/p1/index.html", true) - b.AssertFileExists("public/s2/p2/index.html", true) - b.AssertFileContent("public/index.html", - "Test A: p1", - "Test B: p1", - "Test C: p2", - "Test D: p2", // fails - ) -} diff --git a/hugolib/pagesPrevNext.go b/hugolib/pagesPrevNext.go new file mode 100644 index 000000000..bb474c499 --- /dev/null +++ b/hugolib/pagesPrevNext.go @@ -0,0 +1,40 @@ +// Copyright 2015 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 hugolib + +// Prev returns the previous page reletive to the given page. +func (p Pages) Prev(cur *Page) *Page { + for x, c := range p { + if c.UniqueID() == cur.UniqueID() { + if x == 0 { + return p[len(p)-1] + } + return p[x-1] + } + } + return nil +} + +// Next returns the next page reletive to the given page. +func (p Pages) Next(cur *Page) *Page { + for x, c := range p { + if c.UniqueID() == cur.UniqueID() { + if x < len(p)-1 { + return p[x+1] + } + return p[0] + } + } + return nil +} diff --git a/hugolib/pagesPrevNext_test.go b/hugolib/pagesPrevNext_test.go new file mode 100644 index 000000000..5945d8fe5 --- /dev/null +++ b/hugolib/pagesPrevNext_test.go @@ -0,0 +1,86 @@ +// Copyright 2015 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 hugolib + +import ( + "testing" + + "github.com/spf13/cast" + "github.com/stretchr/testify/assert" +) + +type pagePNTestObject struct { + path string + weight int + date string +} + +var pagePNTestSources = []pagePNTestObject{ + {"/section1/testpage1.md", 5, "2012-04-06"}, + {"/section1/testpage2.md", 4, "2012-01-01"}, + {"/section1/testpage3.md", 3, "2012-04-06"}, + {"/section2/testpage4.md", 2, "2012-03-02"}, + {"/section2/testpage5.md", 1, "2012-04-06"}, +} + +func TestPrev(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + assert.Equal(t, pages.Prev(pages[0]), pages[4]) + assert.Equal(t, pages.Prev(pages[1]), pages[0]) + assert.Equal(t, pages.Prev(pages[4]), pages[3]) +} + +func TestNext(t *testing.T) { + t.Parallel() + pages := preparePageGroupTestPages(t) + assert.Equal(t, pages.Next(pages[0]), pages[1]) + assert.Equal(t, pages.Next(pages[1]), pages[2]) + assert.Equal(t, pages.Next(pages[4]), pages[0]) +} + +func prepareWeightedPagesPrevNext(t *testing.T) WeightedPages { + s := newTestSite(t) + w := WeightedPages{} + + for _, src := range pagePNTestSources { + p, err := s.NewPage(src.path) + if err != nil { + t.Fatalf("failed to prepare test page %s", src.path) + } + p.Weight = src.weight + p.Date = cast.ToTime(src.date) + p.PublishDate = cast.ToTime(src.date) + w = append(w, WeightedPage{p.Weight, p}) + } + + w.Sort() + return w +} + +func TestWeightedPagesPrev(t *testing.T) { + t.Parallel() + w := prepareWeightedPagesPrevNext(t) + assert.Equal(t, w.Prev(w[0].Page), w[4].Page) + assert.Equal(t, w.Prev(w[1].Page), w[0].Page) + assert.Equal(t, w.Prev(w[4].Page), w[3].Page) +} + +func TestWeightedPagesNext(t *testing.T) { + t.Parallel() + w := prepareWeightedPagesPrevNext(t) + assert.Equal(t, w.Next(w[0].Page), w[1].Page) + assert.Equal(t, w.Next(w[1].Page), w[2].Page) + assert.Equal(t, w.Next(w[4].Page), w[0].Page) +} diff --git a/hugolib/pages_capture.go b/hugolib/pages_capture.go deleted file mode 100644 index 50900e585..000000000 --- a/hugolib/pages_capture.go +++ /dev/null @@ -1,425 +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 hugolib - -import ( - "context" - "fmt" - "os" - "path/filepath" - "strings" - "sync" - "sync/atomic" - "time" - - "github.com/bep/logg" - "github.com/gohugoio/hugo/common/hstrings" - "github.com/gohugoio/hugo/common/paths" - "github.com/gohugoio/hugo/common/rungroup" - "github.com/spf13/afero" - - "github.com/gohugoio/hugo/source" - - "github.com/gohugoio/hugo/common/loggers" - "github.com/gohugoio/hugo/hugofs" -) - -func newPagesCollector( - ctx context.Context, - h *HugoSites, - sp *source.SourceSpec, - logger loggers.Logger, - infoLogger logg.LevelLogger, - m *pageMap, - buildConfig *BuildCfg, - ids []pathChange, -) *pagesCollector { - return &pagesCollector{ - ctx: ctx, - h: h, - fs: sp.BaseFs.Content.Fs, - m: m, - sp: sp, - logger: logger, - infoLogger: infoLogger, - buildConfig: buildConfig, - ids: ids, - seenDirs: make(map[string]bool), - } -} - -type pagesCollector struct { - ctx context.Context - h *HugoSites - sp *source.SourceSpec - logger loggers.Logger - infoLogger logg.LevelLogger - - m *pageMap - - fs afero.Fs - - buildConfig *BuildCfg - - // List of paths that have changed. Used in partial builds. - ids []pathChange - seenDirs map[string]bool - - g rungroup.Group[hugofs.FileMetaInfo] -} - -// Collect collects content by walking the file system and storing -// it in the content tree. -// It may be restricted by filenames set on the collector (partial build). -func (c *pagesCollector) Collect() (collectErr error) { - var ( - numWorkers = c.h.numWorkers - numFilesProcessedTotal atomic.Uint64 - numPagesProcessedTotal atomic.Uint64 - numResourcesProcessed atomic.Uint64 - numFilesProcessedLast uint64 - fileBatchTimer = time.Now() - fileBatchTimerMu sync.Mutex - ) - - l := c.infoLogger.WithField("substep", "collect") - - logFilesProcessed := func(force bool) { - fileBatchTimerMu.Lock() - if force || time.Since(fileBatchTimer) > 3*time.Second { - numFilesProcessedBatch := numFilesProcessedTotal.Load() - numFilesProcessedLast - numFilesProcessedLast = numFilesProcessedTotal.Load() - loggers.TimeTrackf(l, fileBatchTimer, - logg.Fields{ - logg.Field{Name: "files", Value: numFilesProcessedBatch}, - logg.Field{Name: "files_total", Value: numFilesProcessedTotal.Load()}, - logg.Field{Name: "pages_total", Value: numPagesProcessedTotal.Load()}, - logg.Field{Name: "resources_total", Value: numResourcesProcessed.Load()}, - }, - "", - ) - fileBatchTimer = time.Now() - } - fileBatchTimerMu.Unlock() - } - - defer func() { - logFilesProcessed(true) - }() - - c.g = rungroup.Run[hugofs.FileMetaInfo](c.ctx, rungroup.Config[hugofs.FileMetaInfo]{ - NumWorkers: numWorkers, - Handle: func(ctx context.Context, fi hugofs.FileMetaInfo) error { - numPages, numResources, err := c.m.AddFi(fi, c.buildConfig) - if err != nil { - return hugofs.AddFileInfoToError(err, fi, c.fs) - } - numFilesProcessedTotal.Add(1) - numPagesProcessedTotal.Add(numPages) - numResourcesProcessed.Add(numResources) - if numFilesProcessedTotal.Load()%1000 == 0 { - logFilesProcessed(false) - } - return nil - }, - }) - - if c.ids == nil { - // Collect everything. - collectErr = c.collectDir(nil, false, nil) - } else { - for _, s := range c.h.Sites { - s.pageMap.cfg.isRebuild = true - } - - var hasStructuralChange bool - for _, id := range c.ids { - if id.isStructuralChange() { - hasStructuralChange = true - break - } - } - - for _, id := range c.ids { - if id.p.IsLeafBundle() { - collectErr = c.collectDir( - id.p, - false, - func(fim hugofs.FileMetaInfo) bool { - if hasStructuralChange { - return true - } - fimp := fim.Meta().PathInfo - if fimp == nil { - return true - } - - return fimp.Path() == id.p.Path() - }, - ) - } else if id.p.IsBranchBundle() { - collectErr = c.collectDir( - id.p, - false, - func(fim hugofs.FileMetaInfo) bool { - if fim.IsDir() { - return id.isStructuralChange() - } - fimp := fim.Meta().PathInfo - if fimp == nil { - return false - } - - return strings.HasPrefix(fimp.Path(), paths.AddTrailingSlash(id.p.Dir())) - }, - ) - } else { - // We always start from a directory. - collectErr = c.collectDir(id.p, id.isDir, func(fim hugofs.FileMetaInfo) bool { - if id.isStructuralChange() { - if id.isDir && fim.Meta().PathInfo.IsLeafBundle() { - return strings.HasPrefix(fim.Meta().PathInfo.Path(), paths.AddTrailingSlash(id.p.Path())) - } - - return id.p.Dir() == fim.Meta().PathInfo.Dir() - } - - if fim.Meta().PathInfo.IsLeafBundle() && id.p.Type() == paths.TypeContentSingle { - return id.p.Dir() == fim.Meta().PathInfo.Dir() - } - - return id.p.Path() == fim.Meta().PathInfo.Path() - }) - } - - if collectErr != nil { - break - } - } - - } - - werr := c.g.Wait() - if collectErr == nil { - collectErr = werr - } - - return -} - -func (c *pagesCollector) collectDir(dirPath *paths.Path, isDir bool, inFilter func(fim hugofs.FileMetaInfo) bool) error { - var dpath string - if dirPath != nil { - if isDir { - dpath = filepath.FromSlash(dirPath.Unnormalized().Path()) - } else { - dpath = filepath.FromSlash(dirPath.Unnormalized().Dir()) - } - } - - if c.seenDirs[dpath] { - return nil - } - c.seenDirs[dpath] = true - - root, err := c.fs.Stat(dpath) - if err != nil { - if os.IsNotExist(err) { - return nil - } - return err - } - - rootm := root.(hugofs.FileMetaInfo) - - if err := c.collectDirDir(dpath, rootm, inFilter); err != nil { - return err - } - - return nil -} - -func (c *pagesCollector) collectDirDir(path string, root hugofs.FileMetaInfo, inFilter func(fim hugofs.FileMetaInfo) bool) error { - filter := func(fim hugofs.FileMetaInfo) bool { - if inFilter != nil { - return inFilter(fim) - } - return true - } - - preHook := func(dir hugofs.FileMetaInfo, path string, readdir []hugofs.FileMetaInfo) ([]hugofs.FileMetaInfo, error) { - filtered := readdir[:0] - for _, fi := range readdir { - if filter(fi) { - filtered = append(filtered, fi) - } - } - readdir = filtered - if len(readdir) == 0 { - return nil, nil - } - - n := 0 - for _, fi := range readdir { - if fi.Meta().PathInfo.IsContentData() { - // _content.json - // These are not part of any bundle, so just add them directly and remove them from the readdir slice. - if err := c.g.Enqueue(fi); err != nil { - return nil, err - } - } else { - readdir[n] = fi - n++ - } - } - readdir = readdir[:n] - - // Pick the first regular file. - var first hugofs.FileMetaInfo - for _, fi := range readdir { - if fi.IsDir() { - continue - } - first = fi - break - } - - if first == nil { - // Only dirs, keep walking. - return readdir, nil - } - - // Any bundle file will always be first. - firstPi := first.Meta().PathInfo - - if firstPi == nil { - panic(fmt.Sprintf("collectDirDir: no path info for %q", first.Meta().Filename)) - } - - if firstPi.IsLeafBundle() { - if err := c.handleBundleLeaf(dir, first, path, readdir); err != nil { - return nil, err - } - return nil, filepath.SkipDir - } - - seen := map[hstrings.Strings2]hugofs.FileMetaInfo{} - for _, fi := range readdir { - if fi.IsDir() { - continue - } - - pi := fi.Meta().PathInfo - meta := fi.Meta() - - // Filter out duplicate page or resource. - // These would eventually have been filtered out as duplicates when - // inserting them into the document store, - // but doing it here will preserve a consistent ordering. - baseLang := hstrings.Strings2{pi.Base(), meta.Lang} - if fi2, ok := seen[baseLang]; ok { - if c.h.Configs.Base.PrintPathWarnings && !c.h.isRebuild() { - c.logger.Warnf("Duplicate content path: %q file: %q file: %q", pi.Base(), fi2.Meta().Filename, meta.Filename) - } - continue - } - seen[baseLang] = fi - - if pi == nil { - panic(fmt.Sprintf("no path info for %q", meta.Filename)) - } - - if meta.Lang == "" { - panic("lang not set") - } - - if err := c.g.Enqueue(fi); err != nil { - return nil, err - } - } - - // Keep walking. - return readdir, nil - } - - var postHook hugofs.WalkHook - - wfn := func(path string, fi hugofs.FileMetaInfo) error { - return nil - } - - w := hugofs.NewWalkway( - hugofs.WalkwayConfig{ - Logger: c.logger, - Root: path, - Info: root, - Fs: c.fs, - IgnoreFile: c.h.SourceSpec.IgnoreFile, - PathParser: c.h.Conf.PathParser(), - HookPre: preHook, - HookPost: postHook, - WalkFn: wfn, - }) - - return w.Walk() -} - -func (c *pagesCollector) handleBundleLeaf(dir, bundle hugofs.FileMetaInfo, inPath string, readdir []hugofs.FileMetaInfo) error { - bundlePi := bundle.Meta().PathInfo - seen := map[hstrings.Strings2]bool{} - - walk := func(path string, info hugofs.FileMetaInfo) error { - if info.IsDir() { - return nil - } - - pi := info.Meta().PathInfo - - if info != bundle { - // Everything inside a leaf bundle is a Resource, - // even the content pages. - // Note that we do allow index.md as page resources, but not in the bundle root. - if !pi.IsLeafBundle() || pi.Dir() != bundlePi.Dir() { - paths.ModifyPathBundleTypeResource(pi) - } - } - - // Filter out duplicate page or resource. - // These would eventually have been filtered out as duplicates when - // inserting them into the document store, - // but doing it here will preserve a consistent ordering. - baseLang := hstrings.Strings2{pi.Base(), info.Meta().Lang} - if seen[baseLang] { - return nil - } - seen[baseLang] = true - - return c.g.Enqueue(info) - } - - // Start a new walker from the given path. - w := hugofs.NewWalkway( - hugofs.WalkwayConfig{ - Root: inPath, - Fs: c.fs, - Logger: c.logger, - Info: dir, - DirEntries: readdir, - IgnoreFile: c.h.SourceSpec.IgnoreFile, - PathParser: c.h.Conf.PathParser(), - WalkFn: walk, - }) - - return w.Walk() -} diff --git a/hugolib/pages_language_merge_test.go b/hugolib/pages_language_merge_test.go deleted file mode 100644 index ba1ed83de..000000000 --- a/hugolib/pages_language_merge_test.go +++ /dev/null @@ -1,196 +0,0 @@ -// Copyright 2019 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 hugolib - -import ( - "fmt" - "testing" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/resources/resource" -) - -// TODO(bep) move and rewrite in resource/page. - -func TestMergeLanguages(t *testing.T) { - t.Parallel() - c := qt.New(t) - - b := newTestSiteForLanguageMerge(t, 30) - b.CreateSites() - - b.Build(BuildCfg{SkipRender: true}) - - h := b.H - - enSite := h.Sites[0] - frSite := h.Sites[1] - nnSite := h.Sites[2] - - c.Assert(len(enSite.RegularPages()), qt.Equals, 31) - c.Assert(len(frSite.RegularPages()), qt.Equals, 6) - c.Assert(len(nnSite.RegularPages()), qt.Equals, 12) - - for range 2 { - mergedNN := nnSite.RegularPages().MergeByLanguage(enSite.RegularPages()) - c.Assert(len(mergedNN), qt.Equals, 31) - for i := 1; i <= 31; i++ { - expectedLang := "en" - if i == 2 || i%3 == 0 || i == 31 { - expectedLang = "nn" - } - p := mergedNN[i-1] - c.Assert(p.Language().Lang, qt.Equals, expectedLang) - } - } - - mergedFR := frSite.RegularPages().MergeByLanguage(enSite.RegularPages()) - c.Assert(len(mergedFR), qt.Equals, 31) - for i := 1; i <= 31; i++ { - expectedLang := "en" - if i%5 == 0 { - expectedLang = "fr" - } - p := mergedFR[i-1] - c.Assert(p.Language().Lang, qt.Equals, expectedLang) - } - - firstNN := nnSite.RegularPages()[0] - c.Assert(len(firstNN.Sites()), qt.Equals, 4) - c.Assert(firstNN.Sites().Default().Language().Lang, qt.Equals, "en") - - nnBundle := nnSite.getPageOldVersion("page", "bundle") - enBundle := enSite.getPageOldVersion("page", "bundle") - - c.Assert(len(enBundle.Resources()), qt.Equals, 6) - c.Assert(len(nnBundle.Resources()), qt.Equals, 2) - - var ri any = nnBundle.Resources() - - // This looks less ugly in the templates ... - mergedNNResources := ri.(resource.ResourcesLanguageMerger).MergeByLanguage(enBundle.Resources()) - c.Assert(len(mergedNNResources), qt.Equals, 6) - - unchanged, err := nnSite.RegularPages().MergeByLanguageInterface(nil) - c.Assert(err, qt.IsNil) - c.Assert(unchanged, deepEqualsPages, nnSite.RegularPages()) -} - -func TestMergeLanguagesTemplate(t *testing.T) { - t.Parallel() - - b := newTestSiteForLanguageMerge(t, 15) - b.WithTemplates("home.html", ` -{{ $pages := .Site.RegularPages }} -{{ .Scratch.Set "pages" $pages }} -{{ $enSite := index .Sites 0 }} -{{ $frSite := index .Sites 1 }} -{{ if eq .Language.Lang "nn" }}: -{{ $nnBundle := .Site.GetPage "page" "bundle" }} -{{ $enBundle := $enSite.GetPage "page" "bundle" }} -{{ .Scratch.Set "pages" ($pages | lang.Merge $frSite.RegularPages| lang.Merge $enSite.RegularPages) }} -{{ .Scratch.Set "pages2" (sort ($nnBundle.Resources | lang.Merge $enBundle.Resources) "Title") }} -{{ end }} -{{ $pages := .Scratch.Get "pages" }} -{{ $pages2 := .Scratch.Get "pages2" }} -Pages1: {{ range $i, $p := $pages }}{{ add $i 1 }}: {{ .File.Path }} {{ .Language.Lang }} | {{ end }} -Pages2: {{ range $i, $p := $pages2 }}{{ add $i 1 }}: {{ .Title }} {{ .Language.Lang }} | {{ end }} -{{ $nil := resources.Get "asdfasdfasdf" }} -Pages3: {{ $frSite.RegularPages | lang.Merge $nil }} -Pages4: {{ $nil | lang.Merge $frSite.RegularPages }} - - -`, - "shortcodes/shortcode.html", "MyShort", - "shortcodes/lingo.html", "MyLingo", - ) - - b.CreateSites() - b.Build(BuildCfg{}) - - b.AssertFileContent("public/nn/index.html", "Pages1: 1: p1.md en | 2: p2.nn.md nn | 3: p3.nn.md nn | 4: p4.md en | 5: p5.fr.md fr | 6: p6.nn.md nn | 7: p7.md en | 8: p8.md en | 9: p9.nn.md nn | 10: p10.fr.md fr | 11: p11.md en | 12: p12.nn.md nn | 13: p13.md en | 14: p14.md en | 15: p15.nn.md nn") - b.AssertFileContent("public/nn/index.html", "Pages2: 1: doc100 en | 2: doc101 nn | 3: doc102 nn | 4: doc103 en | 5: doc104 en | 6: doc105 en") - b.AssertFileContent("public/nn/index.html", ` -Pages3: Pages(3) -Pages4: Pages(3) - `) -} - -func newTestSiteForLanguageMerge(t testing.TB, count int) *sitesBuilder { - contentTemplate := `--- -title: doc%d -weight: %d -date: "2018-02-28" ---- -# doc -*some "content"* - -{{< shortcode >}} - -{{< lingo >}} -` - - builder := newTestSitesBuilder(t).WithDefaultMultiSiteConfig() - - // We need some content with some missing translations. - // "en" is the main language, so add some English content + some Norwegian (nn, nynorsk) content. - var contentPairs []string - for i := 1; i <= count; i++ { - content := fmt.Sprintf(contentTemplate, i, i) - contentPairs = append(contentPairs, []string{fmt.Sprintf("p%d.md", i), content}...) - if i == 2 || i%3 == 0 { - // Add page 2,3, 6, 9 ... to both languages - contentPairs = append(contentPairs, []string{fmt.Sprintf("p%d.nn.md", i), content}...) - } - if i%5 == 0 { - // Add some French content, too. - contentPairs = append(contentPairs, []string{fmt.Sprintf("p%d.fr.md", i), content}...) - } - } - - // See https://github.com/gohugoio/hugo/issues/4644 - // Add a bundles - j := 100 - contentPairs = append(contentPairs, []string{"bundle/index.md", fmt.Sprintf(contentTemplate, j, j)}...) - for i := range 6 { - contentPairs = append(contentPairs, []string{fmt.Sprintf("bundle/pb%d.md", i), fmt.Sprintf(contentTemplate, i+j, i+j)}...) - } - contentPairs = append(contentPairs, []string{"bundle/index.nn.md", fmt.Sprintf(contentTemplate, j, j)}...) - for i := 1; i < 3; i++ { - contentPairs = append(contentPairs, []string{fmt.Sprintf("bundle/pb%d.nn.md", i), fmt.Sprintf(contentTemplate, i+j, i+j)}...) - } - - builder.WithContent(contentPairs...) - return builder -} - -func BenchmarkMergeByLanguage(b *testing.B) { - const count = 100 - - // newTestSiteForLanguageMerge creates count+1 pages. - builder := newTestSiteForLanguageMerge(b, count-1) - builder.CreateSites() - builder.Build(BuildCfg{SkipRender: true}) - h := builder.H - - enSite := h.Sites[0] - nnSite := h.Sites[2] - - for i := 0; i < b.N; i++ { - merged := nnSite.RegularPages().MergeByLanguage(enSite.RegularPages()) - if len(merged) != count { - b.Fatal("Count mismatch") - } - } -} diff --git a/hugolib/pages_test.go b/hugolib/pages_test.go deleted file mode 100644 index 30e9e59d2..000000000 --- a/hugolib/pages_test.go +++ /dev/null @@ -1,119 +0,0 @@ -package hugolib - -import ( - "fmt" - "math/rand" - "testing" - - "github.com/gohugoio/hugo/resources/page" - - qt "github.com/frankban/quicktest" -) - -func newPagesPrevNextTestSite(t testing.TB, numPages int) *sitesBuilder { - categories := []string{"blue", "green", "red", "orange", "indigo", "amber", "lime"} - cat1, cat2 := categories[rand.Intn(len(categories))], categories[rand.Intn(len(categories))] - categoriesSlice := fmt.Sprintf("[%q,%q]", cat1, cat2) - pageTemplate := ` ---- -title: "Page %d" -weight: %d -categories: %s ---- - -` - b := newTestSitesBuilder(t) - - for i := 1; i <= numPages; i++ { - b.WithContent(fmt.Sprintf("page%d.md", i), fmt.Sprintf(pageTemplate, i, rand.Intn(numPages), categoriesSlice)) - } - - return b -} - -func TestPagesPrevNext(t *testing.T) { - b := newPagesPrevNextTestSite(t, 100) - b.Build(BuildCfg{SkipRender: true}) - - pages := b.H.Sites[0].RegularPages() - - b.Assert(pages, qt.HasLen, 100) - - for _, p := range pages { - msg := qt.Commentf("w=%d", p.Weight()) - b.Assert(pages.Next(p), qt.Equals, p.Next(), msg) - b.Assert(pages.Prev(p), qt.Equals, p.Prev(), msg) - } -} - -func BenchmarkPagesPrevNext(b *testing.B) { - type Variant struct { - name string - preparePages func(pages page.Pages) page.Pages - run func(p page.Page, pages page.Pages) - } - - shufflePages := func(pages page.Pages) page.Pages { - rand.Shuffle(len(pages), func(i, j int) { pages[i], pages[j] = pages[j], pages[i] }) - return pages - } - - for _, variant := range []Variant{ - {".Next", nil, func(p page.Page, pages page.Pages) { p.Next() }}, - {".Prev", nil, func(p page.Page, pages page.Pages) { p.Prev() }}, - {"Pages.Next", nil, func(p page.Page, pages page.Pages) { pages.Next(p) }}, - {"Pages.Prev", nil, func(p page.Page, pages page.Pages) { pages.Prev(p) }}, - {"Pages.Shuffled.Next", shufflePages, func(p page.Page, pages page.Pages) { pages.Next(p) }}, - {"Pages.Shuffled.Prev", shufflePages, func(p page.Page, pages page.Pages) { pages.Prev(p) }}, - {"Pages.ByTitle.Next", func(pages page.Pages) page.Pages { return pages.ByTitle() }, func(p page.Page, pages page.Pages) { pages.Next(p) }}, - } { - for _, numPages := range []int{300, 5000} { - b.Run(fmt.Sprintf("%s-pages-%d", variant.name, numPages), func(b *testing.B) { - b.StopTimer() - builder := newPagesPrevNextTestSite(b, numPages) - builder.Build(BuildCfg{SkipRender: true}) - pages := builder.H.Sites[0].RegularPages() - if variant.preparePages != nil { - pages = variant.preparePages(pages) - } - b.StartTimer() - for i := 0; i < b.N; i++ { - p := pages[rand.Intn(len(pages))] - variant.run(p, pages) - } - }) - } - } -} - -func BenchmarkPagePageCollections(b *testing.B) { - type Variant struct { - name string - run func(p page.Page) - } - - for _, variant := range []Variant{ - {".Pages", func(p page.Page) { p.Pages() }}, - {".RegularPages", func(p page.Page) { p.RegularPages() }}, - {".RegularPagesRecursive", func(p page.Page) { p.RegularPagesRecursive() }}, - } { - for _, numPages := range []int{300, 5000} { - b.Run(fmt.Sprintf("%s-%d", variant.name, numPages), func(b *testing.B) { - b.StopTimer() - builder := newPagesPrevNextTestSite(b, numPages) - builder.Build(BuildCfg{SkipRender: true}) - var pages page.Pages - for _, p := range builder.H.Sites[0].Pages() { - if !p.IsPage() { - pages = append(pages, p) - } - } - b.StartTimer() - for i := 0; i < b.N; i++ { - p := pages[rand.Intn(len(pages))] - variant.run(p) - } - }) - } - } -} diff --git a/hugolib/pagesfromdata/pagesfromgotmpl.go b/hugolib/pagesfromdata/pagesfromgotmpl.go deleted file mode 100644 index 72909a40b..000000000 --- a/hugolib/pagesfromdata/pagesfromgotmpl.go +++ /dev/null @@ -1,340 +0,0 @@ -// 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 pagesfromdata - -import ( - "context" - "fmt" - "io" - "path/filepath" - - "github.com/gohugoio/hugo/common/hashing" - "github.com/gohugoio/hugo/common/maps" - "github.com/gohugoio/hugo/common/paths" - "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/hugofs" - "github.com/gohugoio/hugo/identity" - "github.com/gohugoio/hugo/resources/page" - "github.com/gohugoio/hugo/resources/page/pagemeta" - "github.com/gohugoio/hugo/resources/resource" - "github.com/gohugoio/hugo/tpl" - "github.com/gohugoio/hugo/tpl/tplimpl" - "github.com/mitchellh/mapstructure" - "github.com/spf13/cast" -) - -type PagesFromDataTemplateContext interface { - // AddPage adds a new page to the site. - // The first return value will always be an empty string. - AddPage(any) (string, error) - - // AddResource adds a new resource to the site. - // The first return value will always be an empty string. - AddResource(any) (string, error) - - // The site to which the pages will be added. - Site() page.Site - - // The same template may be executed multiple times for multiple languages. - // The Store can be used to store state between these invocations. - Store() *maps.Scratch - - // By default, the template will be executed for the language - // defined by the _content.gotmpl file (e.g. its mount definition). - // This method can be used to activate the template for all languages. - // The return value will always be an empty string. - EnableAllLanguages() string -} - -var _ PagesFromDataTemplateContext = (*pagesFromDataTemplateContext)(nil) - -type pagesFromDataTemplateContext struct { - p *PagesFromTemplate -} - -func (p *pagesFromDataTemplateContext) toPathMap(v any) (string, map[string]any, error) { - m, err := maps.ToStringMapE(v) - if err != nil { - return "", nil, err - } - pathv, ok := m["path"] - if !ok { - return "", nil, fmt.Errorf("path not set") - } - path, err := cast.ToStringE(pathv) - if err != nil || path == "" { - return "", nil, fmt.Errorf("invalid path %q", path) - } - return path, m, nil -} - -func (p *pagesFromDataTemplateContext) AddPage(v any) (string, error) { - path, m, err := p.toPathMap(v) - if err != nil { - return "", err - } - - if !p.p.buildState.checkHasChangedAndSetSourceInfo(path, m) { - return "", nil - } - - pd := pagemeta.DefaultPageConfig - pd.IsFromContentAdapter = true - pd.ContentAdapterData = m - - // The rest will be handled after the cascade is calculated and applied. - if err := mapstructure.WeakDecode(pd.ContentAdapterData, &pd.PageConfigEarly); err != nil { - err = fmt.Errorf("failed to decode page map: %w", err) - return "", err - } - - if err := pd.Validate(true); err != nil { - return "", err - } - - p.p.buildState.NumPagesAdded++ - - return "", p.p.HandlePage(p.p, &pd) -} - -func (p *pagesFromDataTemplateContext) AddResource(v any) (string, error) { - path, m, err := p.toPathMap(v) - if err != nil { - return "", err - } - - if !p.p.buildState.checkHasChangedAndSetSourceInfo(path, m) { - return "", nil - } - - var rd pagemeta.ResourceConfig - if err := mapstructure.WeakDecode(m, &rd); err != nil { - return "", err - } - - p.p.buildState.NumResourcesAdded++ - - if err := rd.Validate(); err != nil { - return "", err - } - - return "", p.p.HandleResource(p.p, &rd) -} - -func (p *pagesFromDataTemplateContext) Site() page.Site { - return p.p.Site -} - -func (p *pagesFromDataTemplateContext) Store() *maps.Scratch { - return p.p.store -} - -func (p *pagesFromDataTemplateContext) EnableAllLanguages() string { - p.p.buildState.EnableAllLanguages = true - return "" -} - -func NewPagesFromTemplate(opts PagesFromTemplateOptions) *PagesFromTemplate { - return &PagesFromTemplate{ - PagesFromTemplateOptions: opts, - PagesFromTemplateDeps: opts.DepsFromSite(opts.Site), - buildState: &BuildState{ - sourceInfosCurrent: maps.NewCache[string, *sourceInfo](), - }, - store: maps.NewScratch(), - } -} - -type PagesFromTemplateOptions struct { - Site page.Site - DepsFromSite func(page.Site) PagesFromTemplateDeps - - DependencyManager identity.Manager - - Watching bool - - HandlePage func(pt *PagesFromTemplate, p *pagemeta.PageConfig) error - HandleResource func(pt *PagesFromTemplate, p *pagemeta.ResourceConfig) error - - GoTmplFi hugofs.FileMetaInfo -} - -type PagesFromTemplateDeps struct { - TemplateStore *tplimpl.TemplateStore -} - -var _ resource.Staler = (*PagesFromTemplate)(nil) - -type PagesFromTemplate struct { - PagesFromTemplateOptions - PagesFromTemplateDeps - buildState *BuildState - store *maps.Scratch -} - -func (b *PagesFromTemplate) AddChange(id identity.Identity) { - b.buildState.ChangedIdentities = append(b.buildState.ChangedIdentities, id) -} - -func (b *PagesFromTemplate) MarkStale() { - b.buildState.StaleVersion++ -} - -func (b *PagesFromTemplate) StaleVersion() uint32 { - return b.buildState.StaleVersion -} - -type BuildInfo struct { - NumPagesAdded uint64 - NumResourcesAdded uint64 - EnableAllLanguages bool - ChangedIdentities []identity.Identity - DeletedPaths []string - Path *paths.Path -} - -type BuildState struct { - StaleVersion uint32 - - EnableAllLanguages bool - - // Paths deleted in the current build. - DeletedPaths []string - - // Changed identities in the current build. - ChangedIdentities []identity.Identity - - NumPagesAdded uint64 - NumResourcesAdded uint64 - - sourceInfosCurrent *maps.Cache[string, *sourceInfo] - sourceInfosPrevious *maps.Cache[string, *sourceInfo] -} - -func (b *BuildState) hash(v any) uint64 { - return hashing.HashUint64(v) -} - -func (b *BuildState) checkHasChangedAndSetSourceInfo(changedPath string, v any) bool { - h := b.hash(v) - si, found := b.sourceInfosPrevious.Get(changedPath) - if found { - b.sourceInfosCurrent.Set(changedPath, si) - if si.hash == h { - return false - } - } else { - si = &sourceInfo{} - b.sourceInfosCurrent.Set(changedPath, si) - } - si.hash = h - return true -} - -func (b *BuildState) resolveDeletedPaths() { - if b.sourceInfosPrevious == nil { - b.DeletedPaths = nil - return - } - var paths []string - b.sourceInfosPrevious.ForEeach(func(k string, _ *sourceInfo) bool { - if _, found := b.sourceInfosCurrent.Get(k); !found { - paths = append(paths, k) - } - return true - }) - - b.DeletedPaths = paths -} - -func (b *BuildState) PrepareNextBuild() { - b.sourceInfosPrevious = b.sourceInfosCurrent - b.sourceInfosCurrent = maps.NewCache[string, *sourceInfo]() - b.StaleVersion = 0 - b.DeletedPaths = nil - b.ChangedIdentities = nil - b.NumPagesAdded = 0 - b.NumResourcesAdded = 0 -} - -type sourceInfo struct { - hash uint64 -} - -func (p PagesFromTemplate) CloneForSite(s page.Site) *PagesFromTemplate { - // We deliberately make them share the same DependencyManager and Store. - p.PagesFromTemplateOptions.Site = s - p.PagesFromTemplateDeps = p.PagesFromTemplateOptions.DepsFromSite(s) - p.buildState = &BuildState{ - sourceInfosCurrent: maps.NewCache[string, *sourceInfo](), - } - return &p -} - -func (p PagesFromTemplate) CloneForGoTmpl(fi hugofs.FileMetaInfo) *PagesFromTemplate { - p.PagesFromTemplateOptions.GoTmplFi = fi - return &p -} - -func (p *PagesFromTemplate) GetDependencyManagerForScope(scope int) identity.Manager { - return p.DependencyManager -} - -func (p *PagesFromTemplate) GetDependencyManagerForScopesAll() []identity.Manager { - return []identity.Manager{p.DependencyManager} -} - -func (p *PagesFromTemplate) Execute(ctx context.Context) (BuildInfo, error) { - defer func() { - p.buildState.PrepareNextBuild() - }() - - f, err := p.GoTmplFi.Meta().Open() - if err != nil { - return BuildInfo{}, err - } - defer f.Close() - - tmpl, err := p.TemplateStore.TextParse(filepath.ToSlash(p.GoTmplFi.Meta().Filename), helpers.ReaderToString(f)) - if err != nil { - return BuildInfo{}, err - } - - data := &pagesFromDataTemplateContext{ - p: p, - } - - ctx = tpl.Context.DependencyManagerScopedProvider.Set(ctx, p) - - if err := p.TemplateStore.ExecuteWithContext(ctx, tmpl, io.Discard, data); err != nil { - return BuildInfo{}, err - } - - if p.Watching { - p.buildState.resolveDeletedPaths() - } - - bi := BuildInfo{ - NumPagesAdded: p.buildState.NumPagesAdded, - NumResourcesAdded: p.buildState.NumResourcesAdded, - EnableAllLanguages: p.buildState.EnableAllLanguages, - ChangedIdentities: p.buildState.ChangedIdentities, - DeletedPaths: p.buildState.DeletedPaths, - Path: p.GoTmplFi.Meta().PathInfo, - } - - return bi, nil -} - -////////////// diff --git a/hugolib/pagesfromdata/pagesfromgotmpl_integration_test.go b/hugolib/pagesfromdata/pagesfromgotmpl_integration_test.go deleted file mode 100644 index db06fb4a4..000000000 --- a/hugolib/pagesfromdata/pagesfromgotmpl_integration_test.go +++ /dev/null @@ -1,917 +0,0 @@ -// Copyright 2025 The Hugo Authors. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package pagesfromdata_test - -import ( - "fmt" - "strings" - "testing" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/hugolib" - "github.com/gohugoio/hugo/markup/asciidocext" - "github.com/gohugoio/hugo/markup/pandoc" - "github.com/gohugoio/hugo/markup/rst" - "github.com/gohugoio/hugo/related" -) - -const filesPagesFromDataTempleBasic = ` --- hugo.toml -- -disableKinds = ["taxonomy", "term", "rss", "sitemap"] -baseURL = "https://example.com" -disableLiveReload = true --- assets/a/pixel.png -- -iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg== --- assets/mydata.yaml -- -p1: "p1" -draft: false --- layouts/partials/get-value.html -- -{{ $val := "p1" }} -{{ return $val }} --- layouts/_default/baseof.html -- -Baseof: -{{ block "main" . }}{{ end }} --- layouts/_default/single.html -- -{{ define "main" }} -Single: {{ .Title }}|{{ .Content }}|Params: {{ .Params.param1 }}|Path: {{ .Path }}| -Dates: Date: {{ .Date.Format "2006-01-02" }}|Lastmod: {{ .Lastmod.Format "2006-01-02" }}|PublishDate: {{ .PublishDate.Format "2006-01-02" }}|ExpiryDate: {{ .ExpiryDate.Format "2006-01-02" }}| -Len Resources: {{ .Resources | len }} -Resources: {{ range .Resources }}RelPermalink: {{ .RelPermalink }}|Name: {{ .Name }}|Title: {{ .Title }}|Params: {{ .Params }}|{{ end }}$ -{{ with .Resources.Get "featured.png" }} -Featured Image: {{ .RelPermalink }}|{{ .Name }}| -{{ with .Resize "10x10" }} -Resized Featured Image: {{ .RelPermalink }}|{{ .Width }}| -{{ end}} -{{ end }} -{{ end }} --- layouts/_default/list.html -- -List: {{ .Title }}|{{ .Content }}| -RegularPagesRecursive: {{ range .RegularPagesRecursive }}{{ .Title }}:{{ .Path }}|{{ end }}$ -Sections: {{ range .Sections }}{{ .Title }}:{{ .Path }}|{{ end }}$ --- content/docs/pfile.md -- ---- -title: "pfile" -date: 2023-03-01 ---- -Pfile Content --- content/docs/_content.gotmpl -- -{{ $pixel := resources.Get "a/pixel.png" }} -{{ $dataResource := resources.Get "mydata.yaml" }} -{{ $data := $dataResource | transform.Unmarshal }} -{{ $pd := $data.p1 }} -{{ $pp := partial "get-value.html" }} -{{ $title := printf "%s:%s" $pd $pp }} -{{ $date := "2023-03-01" | time.AsTime }} -{{ $dates := dict "date" $date }} -{{ $keywords := slice "foo" "Bar"}} -{{ $contentMarkdown := dict "value" "**Hello World**" "mediaType" "text/markdown" }} -{{ $contentMarkdownDefault := dict "value" "**Hello World Default**" }} -{{ $contentHTML := dict "value" "<b>Hello World!</b> No **markdown** here." "mediaType" "text/html" }} -{{ $.AddPage (dict "kind" "page" "path" "P1" "title" $title "dates" $dates "keywords" $keywords "content" $contentMarkdown "params" (dict "param1" "param1v" ) ) }} -{{ $.AddPage (dict "kind" "page" "path" "p2" "title" "p2title" "dates" $dates "content" $contentHTML ) }} -{{ $.AddPage (dict "kind" "page" "path" "p3" "title" "p3title" "dates" $dates "content" $contentMarkdownDefault "draft" false ) }} -{{ $.AddPage (dict "kind" "page" "path" "p4" "title" "p4title" "dates" $dates "content" $contentMarkdownDefault "draft" $data.draft ) }} -ADD_MORE_PLACEHOLDER - - -{{ $resourceContent := dict "value" $dataResource }} -{{ $.AddResource (dict "path" "p1/data1.yaml" "content" $resourceContent) }} -{{ $.AddResource (dict "path" "p1/mytext.txt" "content" (dict "value" "some text") "name" "textresource" "title" "My Text Resource" "params" (dict "param1" "param1v") )}} -{{ $.AddResource (dict "path" "p1/sub/mytex2.txt" "content" (dict "value" "some text") "title" "My Text Sub Resource" ) }} -{{ $.AddResource (dict "path" "P1/Sub/MyMixCaseText2.txt" "content" (dict "value" "some text") "title" "My Text Sub Mixed Case Path Resource" ) }} -{{ $.AddResource (dict "path" "p1/sub/data1.yaml" "content" $resourceContent "title" "Sub data") }} -{{ $resourceParams := dict "data2ParaM1" "data2Param1v" }} -{{ $.AddResource (dict "path" "p1/data2.yaml" "name" "data2.yaml" "title" "My data 2" "params" $resourceParams "content" $resourceContent) }} -{{ $.AddResource (dict "path" "p1/featuredimage.png" "name" "featured.png" "title" "My Featured Image" "params" $resourceParams "content" (dict "value" $pixel ))}} -` - -func TestPagesFromGoTmplMisc(t *testing.T) { - t.Parallel() - b := hugolib.Test(t, filesPagesFromDataTempleBasic, hugolib.TestOptWarn()) - b.AssertLogContains("! WARN") - b.AssertPublishDir(` -docs/p1/mytext.txt -docs/p1/sub/mytex2.tx -docs/p1/sub/mymixcasetext2.txt - `) - - // Page from markdown file. - b.AssertFileContent("public/docs/pfile/index.html", "Dates: Date: 2023-03-01|Lastmod: 2023-03-01|PublishDate: 2023-03-01|ExpiryDate: 0001-01-01|") - // Pages from gotmpl. - b.AssertFileContent("public/docs/p1/index.html", - "Single: p1:p1|", - "Path: /docs/p1|", - "<strong>Hello World</strong>", - "Params: param1v|", - "Len Resources: 7", - "RelPermalink: /mydata.yaml|Name: data1.yaml|Title: data1.yaml|Params: map[]|", - "RelPermalink: /mydata.yaml|Name: data2.yaml|Title: My data 2|Params: map[data2param1:data2Param1v]|", - "RelPermalink: /a/pixel.png|Name: featured.png|Title: My Featured Image|Params: map[data2param1:data2Param1v]|", - "RelPermalink: /docs/p1/sub/mytex2.txt|Name: sub/mytex2.txt|", - "RelPermalink: /docs/p1/sub/mymixcasetext2.txt|Name: sub/mymixcasetext2.txt|", - "RelPermalink: /mydata.yaml|Name: sub/data1.yaml|Title: Sub data|Params: map[]|", - "Featured Image: /a/pixel.png|featured.png|", - "Resized Featured Image: /a/pixel_hu_a32b3e361d55df1.png|10|", - // Resource from string - "RelPermalink: /docs/p1/mytext.txt|Name: textresource|Title: My Text Resource|Params: map[param1:param1v]|", - // Dates - "Dates: Date: 2023-03-01|Lastmod: 2023-03-01|PublishDate: 2023-03-01|ExpiryDate: 0001-01-01|", - ) - b.AssertFileContent("public/docs/p2/index.html", "Single: p2title|", "<b>Hello World!</b> No **markdown** here.") - b.AssertFileContent("public/docs/p3/index.html", "<strong>Hello World Default</strong>") -} - -func TestPagesFromGoTmplAsciidocAndSimilar(t *testing.T) { - files := ` --- hugo.toml -- -disableKinds = ["taxonomy", "term", "rss", "sitemap"] -baseURL = "https://example.com" -[security] -[security.exec] -allow = ['asciidoctor', 'pandoc','rst2html', 'python'] --- layouts/_default/single.html -- -|Content: {{ .Content }}|Title: {{ .Title }}|Path: {{ .Path }}| --- content/docs/_content.gotmpl -- -{{ $.AddPage (dict "path" "asciidoc" "content" (dict "value" "Mark my words, #automation is essential#." "mediaType" "text/asciidoc" )) }} -{{ $.AddPage (dict "path" "pandoc" "content" (dict "value" "This ~~is deleted text.~~" "mediaType" "text/pandoc" )) }} -{{ $.AddPage (dict "path" "rst" "content" (dict "value" "This is *bold*." "mediaType" "text/rst" )) }} -{{ $.AddPage (dict "path" "org" "content" (dict "value" "the ability to use +strikethrough+ is a plus" "mediaType" "text/org" )) }} -{{ $.AddPage (dict "path" "nocontent" "title" "No Content" ) }} - - ` - - b := hugolib.Test(t, files) - - if asciidocext.Supports() { - b.AssertFileContent("public/docs/asciidoc/index.html", - "Mark my words, <mark>automation is essential</mark>", - "Path: /docs/asciidoc|", - ) - } - if pandoc.Supports() { - b.AssertFileContent("public/docs/pandoc/index.html", - "This <del>is deleted text.</del>", - "Path: /docs/pandoc|", - ) - } - - if rst.Supports() { - b.AssertFileContent("public/docs/rst/index.html", - "This is <em>bold</em>", - "Path: /docs/rst|", - ) - } - - b.AssertFileContent("public/docs/org/index.html", - "the ability to use <del>strikethrough</del> is a plus", - "Path: /docs/org|", - ) - - b.AssertFileContent("public/docs/nocontent/index.html", "|Content: |Title: No Content|Path: /docs/nocontent|") -} - -func TestPagesFromGoTmplAddPageErrors(t *testing.T) { - filesTemplate := ` --- hugo.toml -- -disableKinds = ["taxonomy", "term", "rss", "sitemap"] -baseURL = "https://example.com" --- content/docs/_content.gotmpl -- -{{ $.AddPage DICT }} -` - - t.Run("AddPage, missing Path", func(t *testing.T) { - files := strings.ReplaceAll(filesTemplate, "DICT", `(dict "kind" "page" "title" "p1")`) - b, err := hugolib.TestE(t, files) - b.Assert(err, qt.IsNotNil) - b.Assert(err.Error(), qt.Contains, "_content.gotmpl:1:4") - b.Assert(err.Error(), qt.Contains, "error calling AddPage: path not set") - }) - - t.Run("AddPage, path starting with slash", func(t *testing.T) { - files := strings.ReplaceAll(filesTemplate, "DICT", `(dict "kind" "page" "title" "p1" "path" "/foo")`) - b, err := hugolib.TestE(t, files) - b.Assert(err, qt.IsNotNil) - b.Assert(err.Error(), qt.Contains, `path "/foo" must not start with a /`) - }) - - t.Run("AddPage, lang set", func(t *testing.T) { - files := strings.ReplaceAll(filesTemplate, "DICT", `(dict "kind" "page" "path" "p1" "lang" "en")`) - b, err := hugolib.TestE(t, files) - b.Assert(err, qt.IsNotNil) - b.Assert(err.Error(), qt.Contains, "_content.gotmpl:1:4") - b.Assert(err.Error(), qt.Contains, "error calling AddPage: lang must not be set") - }) - - t.Run("Site methods not ready", func(t *testing.T) { - filesTemplate := ` --- hugo.toml -- -disableKinds = ["taxonomy", "term", "rss", "sitemap"] -baseURL = "https://example.com" --- content/docs/_content.gotmpl -- -{{ .Site.METHOD }} -` - - for _, method := range []string{"RegularPages", "Pages", "AllPages", "AllRegularPages", "Home", "Sections", "GetPage", "Menus", "MainSections", "Taxonomies"} { - t.Run(method, func(t *testing.T) { - files := strings.ReplaceAll(filesTemplate, "METHOD", method) - b, err := hugolib.TestE(t, files) - b.Assert(err, qt.IsNotNil) - b.Assert(err.Error(), qt.Contains, fmt.Sprintf("error calling %s: this method cannot be called before the site is fully initialized", method)) - }) - } - }) -} - -func TestPagesFromGoTmplAddResourceErrors(t *testing.T) { - filesTemplate := ` --- hugo.toml -- -disableKinds = ["taxonomy", "term", "rss", "sitemap"] -baseURL = "https://example.com" --- content/docs/_content.gotmpl -- -{{ $.AddResource DICT }} -` - - t.Run("missing Path", func(t *testing.T) { - files := strings.ReplaceAll(filesTemplate, "DICT", `(dict "name" "r1")`) - b, err := hugolib.TestE(t, files) - b.Assert(err, qt.IsNotNil) - b.Assert(err.Error(), qt.Contains, "error calling AddResource: path not set") - }) -} - -func TestPagesFromGoTmplEditGoTmpl(t *testing.T) { - t.Parallel() - b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic) - b.EditFileReplaceAll("content/docs/_content.gotmpl", `"title" "p2title"`, `"title" "p2titleedited"`).Build() - b.AssertFileContent("public/docs/p2/index.html", "Single: p2titleedited|") - b.AssertFileContent("public/docs/index.html", "p2titleedited") -} - -func TestPagesFromGoTmplEditDataResource(t *testing.T) { - t.Parallel() - b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic) - b.AssertRenderCountPage(7) - b.EditFileReplaceAll("assets/mydata.yaml", "p1: \"p1\"", "p1: \"p1edited\"").Build() - b.AssertFileContent("public/docs/p1/index.html", "Single: p1edited:p1|") - b.AssertFileContent("public/docs/index.html", "p1edited") - b.AssertRenderCountPage(3) -} - -func TestPagesFromGoTmplEditPartial(t *testing.T) { - t.Parallel() - b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic) - b.EditFileReplaceAll("layouts/partials/get-value.html", "p1", "p1edited").Build() - b.AssertFileContent("public/docs/p1/index.html", "Single: p1:p1edited|") - b.AssertFileContent("public/docs/index.html", "p1edited") -} - -func TestPagesFromGoTmplRemovePage(t *testing.T) { - t.Parallel() - b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic) - b.EditFileReplaceAll("content/docs/_content.gotmpl", `{{ $.AddPage (dict "kind" "page" "path" "p2" "title" "p2title" "dates" $dates "content" $contentHTML ) }}`, "").Build() - b.AssertFileContent("public/index.html", "RegularPagesRecursive: p1:p1:/docs/p1|p3title:/docs/p3|p4title:/docs/p4|pfile:/docs/pfile|$") -} - -func TestPagesFromGoTmplAddPage(t *testing.T) { - t.Parallel() - b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic) - b.EditFileReplaceAll("content/docs/_content.gotmpl", "ADD_MORE_PLACEHOLDER", `{{ $.AddPage (dict "kind" "page" "path" "page_added" "title" "page_added_title" "dates" $dates "content" $contentHTML ) }}`).Build() - b.AssertFileExists("public/docs/page_added/index.html", true) - b.AssertFileContent("public/index.html", "RegularPagesRecursive: p1:p1:/docs/p1|p2title:/docs/p2|p3title:/docs/p3|p4title:/docs/p4|page_added_title:/docs/page_added|pfile:/docs/pfile|$") -} - -func TestPagesFromGoTmplDraftPage(t *testing.T) { - t.Parallel() - b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic) - b.EditFileReplaceAll("content/docs/_content.gotmpl", `"draft" false`, `"draft" true`).Build() - b.AssertFileContent("public/index.html", "RegularPagesRecursive: p1:p1:/docs/p1|p2title:/docs/p2|p4title:/docs/p4|pfile:/docs/pfile|$") -} - -func TestPagesFromGoTmplDraftFlagFromResource(t *testing.T) { - t.Parallel() - b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic) - b.EditFileReplaceAll("assets/mydata.yaml", `draft: false`, `draft: true`).Build() - b.AssertFileContent("public/index.html", "RegularPagesRecursive: p1:p1:/docs/p1|p2title:/docs/p2|p3title:/docs/p3|pfile:/docs/pfile|$") - b.EditFileReplaceAll("assets/mydata.yaml", `draft: true`, `draft: false`).Build() - b.AssertFileContent("public/index.html", "RegularPagesRecursive: p1:p1:/docs/p1|p2title:/docs/p2|p3title:/docs/p3|p4title:/docs/p4|pfile:/docs/pfile|$") -} - -func TestPagesFromGoTmplMovePage(t *testing.T) { - t.Parallel() - b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic) - b.AssertFileContent("public/index.html", "RegularPagesRecursive: p1:p1:/docs/p1|p2title:/docs/p2|p3title:/docs/p3|p4title:/docs/p4|pfile:/docs/pfile|$") - b.EditFileReplaceAll("content/docs/_content.gotmpl", `"path" "p2"`, `"path" "p2moved"`).Build() - b.AssertFileContent("public/index.html", "RegularPagesRecursive: p1:p1:/docs/p1|p2title:/docs/p2moved|p3title:/docs/p3|p4title:/docs/p4|pfile:/docs/pfile|$") -} - -func TestPagesFromGoTmplRemoveGoTmpl(t *testing.T) { - t.Parallel() - b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic) - b.AssertFileContent("public/index.html", - "RegularPagesRecursive: p1:p1:/docs/p1|p2title:/docs/p2|p3title:/docs/p3|p4title:/docs/p4|pfile:/docs/pfile|$", - "Sections: Docs:/docs|", - ) - b.AssertFileContent("public/docs/index.html", "RegularPagesRecursive: p1:p1:/docs/p1|p2title:/docs/p2|p3title:/docs/p3|p4title:/docs/p4|pfile:/docs/pfile|$") - b.RemoveFiles("content/docs/_content.gotmpl").Build() - // One regular page left. - b.AssertFileContent("public/index.html", - "RegularPagesRecursive: pfile:/docs/pfile|$", - "Sections: Docs:/docs|", - ) - b.AssertFileContent("public/docs/index.html", "RegularPagesRecursive: pfile:/docs/pfile|$") -} - -// Issue #13443. -func TestPagesFromGoRelatedKeywords(t *testing.T) { - t.Parallel() - b := hugolib.Test(t, filesPagesFromDataTempleBasic) - - p1 := b.H.Sites[0].RegularPages()[0] - icfg := related.IndexConfig{ - Name: "keywords", - } - k, err := p1.RelatedKeywords(icfg) - b.Assert(err, qt.IsNil) - b.Assert(k, qt.DeepEquals, icfg.StringsToKeywords("foo", "Bar")) - icfg.Name = "title" - k, err = p1.RelatedKeywords(icfg) - b.Assert(err, qt.IsNil) - b.Assert(k, qt.DeepEquals, icfg.StringsToKeywords("p1:p1")) -} - -func TestPagesFromGoTmplLanguagePerFile(t *testing.T) { - filesTemplate := ` --- hugo.toml -- -defaultContentLanguage = "en" -defaultContentLanguageInSubdir = true -[languages] -[languages.en] -weight = 1 -title = "Title" -[languages.fr] -weight = 2 -title = "Titre" -disabled = DISABLE --- layouts/_default/single.html -- -Single: {{ .Title }}|{{ .Content }}| --- content/docs/_content.gotmpl -- -{{ $.AddPage (dict "kind" "page" "path" "p1" "title" "Title" ) }} --- content/docs/_content.fr.gotmpl -- -{{ $.AddPage (dict "kind" "page" "path" "p1" "title" "Titre" ) }} -` - - for _, disable := range []bool{false, true} { - t.Run(fmt.Sprintf("disable=%t", disable), func(t *testing.T) { - b := hugolib.Test(t, strings.ReplaceAll(filesTemplate, "DISABLE", fmt.Sprintf("%t", disable))) - b.AssertFileContent("public/en/docs/p1/index.html", "Single: Title||") - b.AssertFileExists("public/fr/docs/p1/index.html", !disable) - if !disable { - b.AssertFileContent("public/fr/docs/p1/index.html", "Single: Titre||") - } - }) - } -} - -func TestPagesFromGoTmplDefaultPageSort(t *testing.T) { - t.Parallel() - files := ` --- hugo.toml -- -defaultContentLanguage = "en" --- layouts/index.html -- -{{ range site.RegularPages }}{{ .RelPermalink }}|{{ end}} --- content/_content.gotmpl -- -{{ $.AddPage (dict "kind" "page" "path" "docs/_p22" "title" "A" ) }} -{{ $.AddPage (dict "kind" "page" "path" "docs/p12" "title" "A" ) }} -{{ $.AddPage (dict "kind" "page" "path" "docs/_p12" "title" "A" ) }} --- content/docs/_content.gotmpl -- -{{ $.AddPage (dict "kind" "page" "path" "_p21" "title" "A" ) }} -{{ $.AddPage (dict "kind" "page" "path" "p11" "title" "A" ) }} -{{ $.AddPage (dict "kind" "page" "path" "_p11" "title" "A" ) }} -` - - b := hugolib.Test(t, files) - - b.AssertFileContent("public/index.html", "/docs/_p11/|/docs/_p12/|/docs/_p21/|/docs/_p22/|/docs/p11/|/docs/p12/|") -} - -func TestPagesFromGoTmplEnableAllLanguages(t *testing.T) { - t.Parallel() - - filesTemplate := ` --- hugo.toml -- -defaultContentLanguage = "en" -defaultContentLanguageInSubdir = true -[languages] -[languages.en] -weight = 1 -title = "Title" -[languages.fr] -title = "Titre" -weight = 2 -disabled = DISABLE --- i18n/en.yaml -- -title: Title --- i18n/fr.yaml -- -title: Titre --- content/docs/_content.gotmpl -- -{{ .EnableAllLanguages }} -{{ $titleFromStore := .Store.Get "title" }} -{{ if not $titleFromStore }} - {{ $titleFromStore = "notfound"}} - {{ .Store.Set "title" site.Title }} -{{ end }} -{{ $title := printf "%s:%s:%s" site.Title (i18n "title") $titleFromStore }} -{{ $.AddPage (dict "kind" "page" "path" "p1" "title" $title ) }} --- layouts/_default/single.html -- -Single: {{ .Title }}|{{ .Content }}| - -` - - for _, disable := range []bool{false, true} { - t.Run(fmt.Sprintf("disable=%t", disable), func(t *testing.T) { - b := hugolib.Test(t, strings.ReplaceAll(filesTemplate, "DISABLE", fmt.Sprintf("%t", disable))) - b.AssertFileExists("public/fr/docs/p1/index.html", !disable) - if !disable { - b.AssertFileContent("public/en/docs/p1/index.html", "Single: Title:Title:notfound||") - b.AssertFileContent("public/fr/docs/p1/index.html", "Single: Titre:Titre:Title||") - } - }) - } -} - -func TestPagesFromGoTmplMarkdownify(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ["taxonomy", "term", "rss", "sitemap"] -baseURL = "https://example.com" --- layouts/_default/single.html -- -|Content: {{ .Content }}|Title: {{ .Title }}|Path: {{ .Path }}| --- content/docs/_content.gotmpl -- -{{ $content := "**Hello World**" | markdownify }} -{{ $.AddPage (dict "path" "p1" "content" (dict "value" $content "mediaType" "text/html" )) }} -` - - b, err := hugolib.TestE(t, files) - - // This currently fails. We should fix this, but that is not a trivial task, so do it later. - b.Assert(err, qt.IsNotNil) - b.Assert(err.Error(), qt.Contains, "error calling markdownify: this method cannot be called before the site is fully initialized") -} - -func TestPagesFromGoTmplResourceWithoutExtensionWithMediaTypeProvided(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ["taxonomy", "term", "rss", "sitemap"] -baseURL = "https://example.com" --- layouts/_default/single.html -- -|Content: {{ .Content }}|Title: {{ .Title }}|Path: {{ .Path }}| -{{ range .Resources }} -|RelPermalink: {{ .RelPermalink }}|Name: {{ .Name }}|Title: {{ .Title }}|Params: {{ .Params }}|MediaType: {{ .MediaType }}| -{{ end }} --- content/docs/_content.gotmpl -- -{{ $.AddPage (dict "path" "p1" "content" (dict "value" "**Hello World**" "mediaType" "text/markdown" )) }} -{{ $.AddResource (dict "path" "p1/myresource" "content" (dict "value" "abcde" "mediaType" "text/plain" )) }} -` - - b := hugolib.Test(t, files) - - b.AssertFileContent("public/docs/p1/index.html", "RelPermalink: /docs/p1/myresource|Name: myresource|Title: myresource|Params: map[]|MediaType: text/plain|") -} - -func TestPagesFromGoTmplCascade(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ["taxonomy", "term", "rss", "sitemap"] -baseURL = "https://example.com" --- layouts/_default/single.html -- -|Content: {{ .Content }}|Title: {{ .Title }}|Path: {{ .Path }}|Params: {{ .Params }}| --- content/_content.gotmpl -- -{{ $cascade := dict "params" (dict "cascadeparam1" "cascadeparam1value" ) }} -{{ $.AddPage (dict "path" "docs" "kind" "section" "cascade" $cascade ) }} -{{ $.AddPage (dict "path" "docs/p1" "content" (dict "value" "**Hello World**" "mediaType" "text/markdown" )) }} - -` - b := hugolib.Test(t, files) - - b.AssertFileContent("public/docs/p1/index.html", "|Path: /docs/p1|Params: map[cascadeparam1:cascadeparam1value") -} - -func TestPagesFromGoBuildOptions(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ["taxonomy", "term", "rss", "sitemap"] -baseURL = "https://example.com" --- layouts/_default/single.html -- -|Content: {{ .Content }}|Title: {{ .Title }}|Path: {{ .Path }}|Params: {{ .Params }}| --- content/_content.gotmpl -- -{{ $.AddPage (dict "path" "docs/p1" "content" (dict "value" "**Hello World**" "mediaType" "text/markdown" )) }} -{{ $never := dict "list" "never" "publishResources" false "render" "never" }} -{{ $.AddPage (dict "path" "docs/p2" "content" (dict "value" "**Hello World**" "mediaType" "text/markdown" ) "build" $never ) }} - - -` - b := hugolib.Test(t, files) - - b.AssertFileExists("public/docs/p1/index.html", true) - b.AssertFileExists("public/docs/p2/index.html", false) -} - -func TestPagesFromGoPathsWithDotsIssue12493(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ['home','section','rss','sitemap','taxonomy','term'] --- content/_content.gotmpl -- -{{ .AddPage (dict "path" "s-1.2.3/p-4.5.6" "title" "p-4.5.6") }} --- layouts/_default/single.html -- -{{ .Title }} -` - - b := hugolib.Test(t, files) - - b.AssertFileExists("public/s-1.2.3/p-4.5.6/index.html", true) -} - -func TestPagesFromGoParamsIssue12497(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ['home','section','rss','sitemap','taxonomy','term'] --- content/_content.gotmpl -- -{{ .AddPage (dict "path" "p1" "title" "p1" "params" (dict "paraM1" "param1v" )) }} -{{ .AddResource (dict "path" "p1/data1.yaml" "content" (dict "value" "data1" ) "params" (dict "paraM1" "param1v" )) }} --- layouts/_default/single.html -- -{{ .Title }}|{{ .Params.paraM1 }} -{{ range .Resources }} -{{ .Name }}|{{ .Params.paraM1 }} -{{ end }} -` - - b := hugolib.Test(t, files) - - b.AssertFileContent("public/p1/index.html", - "p1|param1v", - "data1.yaml|param1v", - ) -} - -func TestPagesFromGoTmplPathWarningsPathPage(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableKinds = ['home','section','rss','sitemap','taxonomy','term'] -printPathWarnings = true --- content/_content.gotmpl -- -{{ .AddPage (dict "path" "p1" "title" "p1" ) }} -{{ .AddPage (dict "path" "p2" "title" "p2" ) }} --- content/p1.md -- ---- -title: "p1" ---- --- layouts/_default/single.html -- -{{ .Title }}| -` - - b := hugolib.Test(t, files, hugolib.TestOptWarn()) - - b.AssertFileContent("public/p1/index.html", "p1|") - - b.AssertLogContains("Duplicate content path") - - files = strings.ReplaceAll(files, `"path" "p1"`, `"path" "p1new"`) - - b = hugolib.Test(t, files, hugolib.TestOptWarn()) - - b.AssertLogContains("! WARN") -} - -func TestPagesFromGoTmplPathWarningsPathResource(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableKinds = ['home','section','rss','sitemap','taxonomy','term'] -printPathWarnings = true --- content/_content.gotmpl -- -{{ .AddResource (dict "path" "p1/data1.yaml" "content" (dict "value" "data1" ) ) }} -{{ .AddResource (dict "path" "p1/data2.yaml" "content" (dict "value" "data2" ) ) }} - --- content/p1/index.md -- ---- -title: "p1" ---- --- content/p1/data1.yaml -- -value: data1 --- layouts/_default/single.html -- -{{ .Title }}| -` - - b := hugolib.Test(t, files, hugolib.TestOptWarn()) - - b.AssertFileContent("public/p1/index.html", "p1|") - - b.AssertLogContains("Duplicate resource path") - - files = strings.ReplaceAll(files, `"path" "p1/data1.yaml"`, `"path" "p1/data1new.yaml"`) - - b = hugolib.Test(t, files, hugolib.TestOptWarn()) - - b.AssertLogContains("! WARN") -} - -func TestPagesFromGoTmplShortcodeNoPreceddingCharacterIssue12544(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ['home','rss','section','sitemap','taxonomy','term'] --- content/_content.gotmpl -- -{{ $content := dict "mediaType" "text/html" "value" "x{{< sc >}}" }} -{{ .AddPage (dict "content" $content "path" "a") }} - -{{ $content := dict "mediaType" "text/html" "value" "{{< sc >}}" }} -{{ .AddPage (dict "content" $content "path" "b") }} --- layouts/_default/single.html -- -|{{ .Content }}| --- layouts/shortcodes/sc.html -- -foo -{{- /**/ -}} -` - - b := hugolib.Test(t, files) - - b.AssertFileContent("public/a/index.html", "|xfoo|") - b.AssertFileContent("public/b/index.html", "|foo|") // fails -} - -func TestPagesFromGoTmplMenus(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ['rss','section','sitemap','taxonomy','term'] - -[menus] -[[menus.main]] -name = "Main" -[[menus.footer]] -name = "Footer" --- content/_content.gotmpl -- -{{ .AddPage (dict "path" "p1" "title" "p1" "menus" "main" ) }} -{{ .AddPage (dict "path" "p2" "title" "p2" "menus" (slice "main" "footer")) }} --- layouts/index.html -- -Main: {{ range index site.Menus.main }}{{ .Name }}|{{ end }}| -Footer: {{ range index site.Menus.footer }}{{ .Name }}|{{ end }}| - -` - b := hugolib.Test(t, files) - - b.AssertFileContent("public/index.html", - "Main: Main|p1|p2||", - "Footer: Footer|p2||", - ) -} - -// Issue 13384. -func TestPagesFromGoTmplMenusMap(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ['rss','section','sitemap','taxonomy','term'] --- content/_content.gotmpl -- -{{ $menu1 := dict - "parent" "main-page" - "identifier" "id1" -}} -{{ $menu2 := dict - "parent" "main-page" - "identifier" "id2" -}} -{{ $menus := dict "m1" $menu1 "m2" $menu2 }} -{{ .AddPage (dict "path" "p1" "title" "p1" "menus" $menus ) }} - --- layouts/index.html -- -Menus: {{ range $k, $v := site.Menus }}{{ $k }}|{{ end }} - -` - b := hugolib.Test(t, files) - - b.AssertFileContent("public/index.html", "Menus: m1|m2|") -} - -func TestPagesFromGoTmplMore(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ['home','rss','section','sitemap','taxonomy','term'] -[markup.goldmark.renderer] -unsafe = true --- content/s1/_content.gotmpl -- -{{ $page := dict - "content" (dict "mediaType" "text/markdown" "value" "aaa <!--more--> bbb") - "title" "p1" - "path" "p1" - }} - {{ .AddPage $page }} --- layouts/_default/single.html -- -summary: {{ .Summary }}|content: {{ .Content}} -` - - b := hugolib.Test(t, files) - - b.AssertFileContent("public/s1/p1/index.html", - "<p>aaa</p>|content: <p>aaa</p>\n<p>bbb</p>", - ) -} - -// Issue 13063. -func TestPagesFromGoTmplTermIsEmpty(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableKinds = ['section', 'home', 'rss','sitemap'] -printPathWarnings = true -[taxonomies] -tag = "tags" --- content/mypost.md -- ---- -title: "My Post" -tags: ["mytag"] ---- --- content/tags/_content.gotmpl -- -{{ .AddPage (dict "path" "mothertag" "title" "My title" "kind" "term") }} --- --- layouts/_default/taxonomy.html -- -Terms: {{ range .Data.Terms.ByCount }}{{ .Name }}: {{ .Count }}|{{ end }}§s --- layouts/_default/single.html -- -Single. -` - - b := hugolib.Test(t, files, hugolib.TestOptWarn()) - - b.AssertFileContent("public/tags/index.html", "Terms: mytag: 1|§s") -} - -func TestContentAdapterOutputsIssue13689(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ['home','rss','section','sitemap','taxonomy','term'] -[outputs] -page = ['html','json'] --- layouts/page.html -- -html: {{ .Title }} --- layouts/page.json -- -json: {{ .Title }} --- content/p1.md -- ---- -title: p1 ---- --- content/p2.md -- ---- -title: p2 -outputs: - - html ---- --- content/_content.gotmpl -- -{{ $page := dict "path" "p3" "title" "p3" }} -{{ $.AddPage $page }} - -{{ $page := dict "path" "p4" "title" "p4" "outputs" (slice "html") }} -{{ $.AddPage $page }} -` - - b := hugolib.Test(t, files) - - b.AssertFileExists("public/p1/index.html", true) - b.AssertFileExists("public/p1/index.json", true) - b.AssertFileExists("public/p2/index.html", true) - b.AssertFileExists("public/p2/index.json", false) - b.AssertFileExists("public/p3/index.html", true) - b.AssertFileExists("public/p3/index.json", true) - b.AssertFileExists("public/p4/index.html", true) - b.AssertFileExists("public/p4/index.json", false) // currently returns true -} - -func TestContentAdapterOutputsIssue13692(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ['page','home','sitemap','taxonomy','term'] -[[cascade]] -outputs = ['html','json'] -[cascade.target] -path = '{/s2,/s4}' --- layouts/section.html -- -html: {{ .Title }} --- layouts/section.json -- -json: {{ .Title }} --- content/s1/_index.md -- ---- -title: s1 ---- --- content/s2/_index.md -- ---- -title: s2 ---- --- content/_content.gotmpl -- -{{ $page := dict "path" "s3" "title" "s3" "kind" "section" }} -{{ $.AddPage $page }} - -{{ $page := dict "path" "s4" "title" "s4" "kind" "section" }} -{{ $.AddPage $page }} - -{{ $page := dict "path" "s5" "title" "s5" "kind" "section" "outputs" (slice "html") }} - {{ $.AddPage $page }} -` - - b := hugolib.Test(t, files) - - b.AssertFileExists("public/s1/index.html", true) - b.AssertFileExists("public/s1/index.json", false) - b.AssertFileExists("public/s1/index.xml", true) - - b.AssertFileExists("public/s2/index.html", true) - b.AssertFileExists("public/s2/index.json", true) - b.AssertFileExists("public/s2/index.xml", false) - - b.AssertFileExists("public/s3/index.html", true) - b.AssertFileExists("public/s3/index.json", false) - b.AssertFileExists("public/s3/index.xml", true) - - b.AssertFileExists("public/s4/index.html", true) - b.AssertFileExists("public/s4/index.json", true) - b.AssertFileExists("public/s4/index.xml", false) - - b.AssertFileExists("public/s5/index.html", true) - b.AssertFileExists("public/s5/index.json", false) - b.AssertFileExists("public/s5/index.xml", false) -} - -func TestContentAdapterCascadeBasic(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableLiveReload = true --- content/_index.md -- ---- -cascade: - - title: foo - target: - path: "**" ---- --- layouts/all.html -- -Title: {{ .Title }}|Content: {{ .Content }}| --- content/_content.gotmpl -- -{{ $content := dict - "mediaType" "text/markdown" - "value" "The _Hunchback of Notre Dame_ was written by Victor Hugo." -}} - -{{ $page := dict "path" "s1" "kind" "page" }} -{{ $.AddPage $page }} - {{ $page := dict "path" "s2" "kind" "page" "title" "bar" "content" $content }} -{{ $.AddPage $page }} - -` - - b := hugolib.TestRunning(t, files) - - b.AssertFileContent("public/s1/index.html", "Title: foo|") - b.AssertFileContent("public/s2/index.html", "Title: bar|", "Content: <p>The <em>Hunchback of Notre Dame</em> was written by Victor Hugo.</p>") - - b.EditFileReplaceAll("content/_index.md", "foo", "baz").Build() - - b.AssertFileContent("public/s1/index.html", "Title: baz|") -} diff --git a/hugolib/pagesfromdata/pagesfromgotmpl_test.go b/hugolib/pagesfromdata/pagesfromgotmpl_test.go deleted file mode 100644 index c60b56dbf..000000000 --- a/hugolib/pagesfromdata/pagesfromgotmpl_test.go +++ /dev/null @@ -1,32 +0,0 @@ -// 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 pagesfromdata - -import "testing" - -func BenchmarkHash(b *testing.B) { - m := map[string]any{ - "foo": "bar", - "bar": "foo", - "stringSlice": []any{"a", "b", "c"}, - "intSlice": []any{1, 2, 3}, - "largeText": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit.", - } - - bs := BuildState{} - - for i := 0; i < b.N; i++ { - bs.hash(m) - } -} diff --git a/hugolib/pagination.go b/hugolib/pagination.go new file mode 100644 index 000000000..4733cf7c8 --- /dev/null +++ b/hugolib/pagination.go @@ -0,0 +1,535 @@ +// Copyright 2015 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 hugolib + +import ( + "errors" + "fmt" + "html/template" + "math" + "reflect" + "strings" + + "github.com/gohugoio/hugo/config" + + "github.com/spf13/cast" +) + +// Pager represents one of the elements in a paginator. +// The number, starting on 1, represents its place. +type Pager struct { + number int + *paginator +} + +func (p Pager) String() string { + return fmt.Sprintf("Pager %d", p.number) +} + +type paginatedElement interface { + Len() int +} + +// Len returns the number of pages in the list. +func (p Pages) Len() int { + return len(p) +} + +// Len returns the number of pages in the page group. +func (psg PagesGroup) Len() int { + l := 0 + for _, pg := range psg { + l += len(pg.Pages) + } + return l +} + +type pagers []*Pager + +var ( + paginatorEmptyPages Pages + paginatorEmptyPageGroups PagesGroup +) + +type paginator struct { + paginatedElements []paginatedElement + pagers + paginationURLFactory + total int + size int + source interface{} + options []interface{} +} + +type paginationURLFactory func(int) string + +// PageNumber returns the current page's number in the pager sequence. +func (p *Pager) PageNumber() int { + return p.number +} + +// URL returns the URL to the current page. +func (p *Pager) URL() template.HTML { + return template.HTML(p.paginationURLFactory(p.PageNumber())) +} + +// Pages returns the Pages on this page. +// Note: If this return a non-empty result, then PageGroups() will return empty. +func (p *Pager) Pages() Pages { + if len(p.paginatedElements) == 0 { + return paginatorEmptyPages + } + + if pages, ok := p.element().(Pages); ok { + return pages + } + + return paginatorEmptyPages +} + +// PageGroups return Page groups for this page. +// Note: If this return non-empty result, then Pages() will return empty. +func (p *Pager) PageGroups() PagesGroup { + if len(p.paginatedElements) == 0 { + return paginatorEmptyPageGroups + } + + if groups, ok := p.element().(PagesGroup); ok { + return groups + } + + return paginatorEmptyPageGroups +} + +func (p *Pager) element() paginatedElement { + if len(p.paginatedElements) == 0 { + return paginatorEmptyPages + } + return p.paginatedElements[p.PageNumber()-1] +} + +// page returns the Page with the given index +func (p *Pager) page(index int) (*Page, error) { + + if pages, ok := p.element().(Pages); ok { + if pages != nil && len(pages) > index { + return pages[index], nil + } + return nil, nil + } + + // must be PagesGroup + // this construction looks clumsy, but ... + // ... it is the difference between 99.5% and 100% test coverage :-) + groups := p.element().(PagesGroup) + + i := 0 + for _, v := range groups { + for _, page := range v.Pages { + if i == index { + return page, nil + } + i++ + } + } + return nil, nil +} + +// NumberOfElements gets the number of elements on this page. +func (p *Pager) NumberOfElements() int { + return p.element().Len() +} + +// HasPrev tests whether there are page(s) before the current. +func (p *Pager) HasPrev() bool { + return p.PageNumber() > 1 +} + +// Prev returns the pager for the previous page. +func (p *Pager) Prev() *Pager { + if !p.HasPrev() { + return nil + } + return p.pagers[p.PageNumber()-2] +} + +// HasNext tests whether there are page(s) after the current. +func (p *Pager) HasNext() bool { + return p.PageNumber() < len(p.paginatedElements) +} + +// Next returns the pager for the next page. +func (p *Pager) Next() *Pager { + if !p.HasNext() { + return nil + } + return p.pagers[p.PageNumber()] +} + +// First returns the pager for the first page. +func (p *Pager) First() *Pager { + return p.pagers[0] +} + +// Last returns the pager for the last page. +func (p *Pager) Last() *Pager { + return p.pagers[len(p.pagers)-1] +} + +// Pagers returns a list of pagers that can be used to build a pagination menu. +func (p *paginator) Pagers() pagers { + return p.pagers +} + +// PageSize returns the size of each paginator page. +func (p *paginator) PageSize() int { + return p.size +} + +// TotalPages returns the number of pages in the paginator. +func (p *paginator) TotalPages() int { + return len(p.paginatedElements) +} + +// TotalNumberOfElements returns the number of elements on all pages in this paginator. +func (p *paginator) TotalNumberOfElements() int { + return p.total +} + +func splitPages(pages Pages, size int) []paginatedElement { + var split []paginatedElement + for low, j := 0, len(pages); low < j; low += size { + high := int(math.Min(float64(low+size), float64(len(pages)))) + split = append(split, pages[low:high]) + } + + return split +} + +func splitPageGroups(pageGroups PagesGroup, size int) []paginatedElement { + + type keyPage struct { + key interface{} + page *Page + } + + var ( + split []paginatedElement + flattened []keyPage + ) + + for _, g := range pageGroups { + for _, p := range g.Pages { + flattened = append(flattened, keyPage{g.Key, p}) + } + } + + numPages := len(flattened) + + for low, j := 0, numPages; low < j; low += size { + high := int(math.Min(float64(low+size), float64(numPages))) + + var ( + pg PagesGroup + key interface{} + groupIndex = -1 + ) + + for k := low; k < high; k++ { + kp := flattened[k] + if key == nil || key != kp.key { + key = kp.key + pg = append(pg, PageGroup{Key: key}) + groupIndex++ + } + pg[groupIndex].Pages = append(pg[groupIndex].Pages, kp.page) + } + split = append(split, pg) + } + + return split +} + +// Paginator get this Page's main output's paginator. +func (p *Page) Paginator(options ...interface{}) (*Pager, error) { + return p.mainPageOutput.Paginator(options...) +} + +// Paginator gets this PageOutput's paginator if it's already created. +// If it's not, one will be created with all pages in Data["Pages"]. +func (p *PageOutput) Paginator(options ...interface{}) (*Pager, error) { + if !p.IsNode() { + return nil, fmt.Errorf("Paginators not supported for pages of type %q (%q)", p.Kind, p.Title) + } + pagerSize, err := resolvePagerSize(p.s.Cfg, options...) + + if err != nil { + return nil, err + } + + var initError error + + p.paginatorInit.Do(func() { + if p.paginator != nil { + return + } + + pagers, err := paginatePages(p.targetPathDescriptor, p.Data["Pages"], pagerSize) + + if err != nil { + initError = err + } + + if len(pagers) > 0 { + // the rest of the nodes will be created later + p.paginator = pagers[0] + p.paginator.source = "paginator" + p.paginator.options = options + p.Site.addToPaginationPageCount(uint64(p.paginator.TotalPages())) + } + + }) + + if initError != nil { + return nil, initError + } + + return p.paginator, nil +} + +// Paginate invokes this Page's main output's Paginate method. +func (p *Page) Paginate(seq interface{}, options ...interface{}) (*Pager, error) { + return p.mainPageOutput.Paginate(seq, options...) +} + +// Paginate gets this PageOutput's paginator if it's already created. +// If it's not, one will be created with the qiven sequence. +// Note that repeated calls will return the same result, even if the sequence is different. +func (p *PageOutput) Paginate(seq interface{}, options ...interface{}) (*Pager, error) { + if !p.IsNode() { + return nil, fmt.Errorf("Paginators not supported for pages of type %q (%q)", p.Kind, p.Title) + } + + pagerSize, err := resolvePagerSize(p.s.Cfg, options...) + + if err != nil { + return nil, err + } + + var initError error + + p.paginatorInit.Do(func() { + if p.paginator != nil { + return + } + pagers, err := paginatePages(p.targetPathDescriptor, seq, pagerSize) + + if err != nil { + initError = err + } + + if len(pagers) > 0 { + // the rest of the nodes will be created later + p.paginator = pagers[0] + p.paginator.source = seq + p.paginator.options = options + p.Site.addToPaginationPageCount(uint64(p.paginator.TotalPages())) + } + + }) + + if initError != nil { + return nil, initError + } + + if p.paginator.source == "paginator" { + return nil, errors.New("a Paginator was previously built for this Node without filters; look for earlier .Paginator usage") + } + + if !reflect.DeepEqual(options, p.paginator.options) || !probablyEqualPageLists(p.paginator.source, seq) { + return nil, errors.New("invoked multiple times with different arguments") + } + + return p.paginator, nil +} + +func resolvePagerSize(cfg config.Provider, options ...interface{}) (int, error) { + if len(options) == 0 { + return cfg.GetInt("paginate"), nil + } + + if len(options) > 1 { + return -1, errors.New("too many arguments, 'pager size' is currently the only option") + } + + pas, err := cast.ToIntE(options[0]) + + if err != nil || pas <= 0 { + return -1, errors.New(("'pager size' must be a positive integer")) + } + + return pas, nil +} + +func paginatePages(td targetPathDescriptor, seq interface{}, pagerSize int) (pagers, error) { + + if pagerSize <= 0 { + return nil, errors.New("'paginate' configuration setting must be positive to paginate") + } + + urlFactory := newPaginationURLFactory(td) + + var paginator *paginator + + if groups, ok := seq.(PagesGroup); ok { + paginator, _ = newPaginatorFromPageGroups(groups, pagerSize, urlFactory) + } else { + pages, err := toPages(seq) + if err != nil { + return nil, err + } + paginator, _ = newPaginatorFromPages(pages, pagerSize, urlFactory) + } + + pagers := paginator.Pagers() + + return pagers, nil +} + +func toPages(seq interface{}) (Pages, error) { + switch seq.(type) { + case Pages: + return seq.(Pages), nil + case *Pages: + return *(seq.(*Pages)), nil + case WeightedPages: + return (seq.(WeightedPages)).Pages(), nil + case PageGroup: + return (seq.(PageGroup)).Pages, nil + default: + return nil, fmt.Errorf("unsupported type in paginate, got %T", seq) + } +} + +// probablyEqual checks page lists for probable equality. +// It may return false positives. +// The motivation behind this is to avoid potential costly reflect.DeepEqual +// when "probably" is good enough. +func probablyEqualPageLists(a1 interface{}, a2 interface{}) bool { + + if a1 == nil || a2 == nil { + return a1 == a2 + } + + t1 := reflect.TypeOf(a1) + t2 := reflect.TypeOf(a2) + + if t1 != t2 { + return false + } + + if g1, ok := a1.(PagesGroup); ok { + g2 := a2.(PagesGroup) + if len(g1) != len(g2) { + return false + } + if len(g1) == 0 { + return true + } + if g1.Len() != g2.Len() { + return false + } + + return g1[0].Pages[0] == g2[0].Pages[0] + } + + p1, err1 := toPages(a1) + p2, err2 := toPages(a2) + + // probably the same wrong type + if err1 != nil && err2 != nil { + return true + } + + if len(p1) != len(p2) { + return false + } + + if len(p1) == 0 { + return true + } + + return p1[0] == p2[0] +} + +func newPaginatorFromPages(pages Pages, size int, urlFactory paginationURLFactory) (*paginator, error) { + + if size <= 0 { + return nil, errors.New("Paginator size must be positive") + } + + split := splitPages(pages, size) + + return newPaginator(split, len(pages), size, urlFactory) +} + +func newPaginatorFromPageGroups(pageGroups PagesGroup, size int, urlFactory paginationURLFactory) (*paginator, error) { + + if size <= 0 { + return nil, errors.New("Paginator size must be positive") + } + + split := splitPageGroups(pageGroups, size) + + return newPaginator(split, pageGroups.Len(), size, urlFactory) +} + +func newPaginator(elements []paginatedElement, total, size int, urlFactory paginationURLFactory) (*paginator, error) { + p := &paginator{total: total, paginatedElements: elements, size: size, paginationURLFactory: urlFactory} + + var ps pagers + + if len(elements) > 0 { + ps = make(pagers, len(elements)) + for i := range p.paginatedElements { + ps[i] = &Pager{number: (i + 1), paginator: p} + } + } else { + ps = make(pagers, 1) + ps[0] = &Pager{number: 1, paginator: p} + } + + p.pagers = ps + + return p, nil +} + +func newPaginationURLFactory(d targetPathDescriptor) paginationURLFactory { + + return func(page int) string { + pathDescriptor := d + var rel string + if page > 1 { + rel = fmt.Sprintf("/%s/%d/", d.PathSpec.PaginatePath(), page) + pathDescriptor.Addends = rel + } + + targetPath := createTargetPath(pathDescriptor) + targetPath = strings.TrimSuffix(targetPath, d.Type.BaseFilename()) + link := d.PathSpec.PrependBasePath(targetPath) + + // Note: The targetPath is massaged with MakePathSanitized + return d.PathSpec.URLizeFilename(link) + } +} diff --git a/hugolib/pagination_test.go b/hugolib/pagination_test.go new file mode 100644 index 000000000..edfac3f3e --- /dev/null +++ b/hugolib/pagination_test.go @@ -0,0 +1,579 @@ +// Copyright 2015 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 hugolib + +import ( + "fmt" + "html/template" + "path/filepath" + "strings" + "testing" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/output" + "github.com/stretchr/testify/require" +) + +func TestSplitPages(t *testing.T) { + t.Parallel() + s := newTestSite(t) + + pages := createTestPages(s, 21) + chunks := splitPages(pages, 5) + require.Equal(t, 5, len(chunks)) + + for i := 0; i < 4; i++ { + require.Equal(t, 5, chunks[i].Len()) + } + + lastChunk := chunks[4] + require.Equal(t, 1, lastChunk.Len()) + +} + +func TestSplitPageGroups(t *testing.T) { + t.Parallel() + s := newTestSite(t) + pages := createTestPages(s, 21) + groups, _ := pages.GroupBy("Weight", "desc") + chunks := splitPageGroups(groups, 5) + require.Equal(t, 5, len(chunks)) + + firstChunk := chunks[0] + + // alternate weight 5 and 10 + if groups, ok := firstChunk.(PagesGroup); ok { + require.Equal(t, 5, groups.Len()) + for _, pg := range groups { + // first group 10 in weight + require.Equal(t, 10, pg.Key) + for _, p := range pg.Pages { + require.True(t, p.fuzzyWordCount%2 == 0) // magic test + } + } + } else { + t.Fatal("Excepted PageGroup") + } + + lastChunk := chunks[4] + + if groups, ok := lastChunk.(PagesGroup); ok { + require.Equal(t, 1, groups.Len()) + for _, pg := range groups { + // last should have 5 in weight + require.Equal(t, 5, pg.Key) + for _, p := range pg.Pages { + require.True(t, p.fuzzyWordCount%2 != 0) // magic test + } + } + } else { + t.Fatal("Excepted PageGroup") + } + +} + +func TestPager(t *testing.T) { + t.Parallel() + s := newTestSite(t) + pages := createTestPages(s, 21) + groups, _ := pages.GroupBy("Weight", "desc") + + urlFactory := func(page int) string { + return fmt.Sprintf("page/%d/", page) + } + + _, err := newPaginatorFromPages(pages, -1, urlFactory) + require.NotNil(t, err) + + _, err = newPaginatorFromPageGroups(groups, -1, urlFactory) + require.NotNil(t, err) + + pag, err := newPaginatorFromPages(pages, 5, urlFactory) + require.Nil(t, err) + doTestPages(t, pag) + first := pag.Pagers()[0].First() + require.Equal(t, "Pager 1", first.String()) + require.NotEmpty(t, first.Pages()) + require.Empty(t, first.PageGroups()) + + pag, err = newPaginatorFromPageGroups(groups, 5, urlFactory) + require.Nil(t, err) + doTestPages(t, pag) + first = pag.Pagers()[0].First() + require.NotEmpty(t, first.PageGroups()) + require.Empty(t, first.Pages()) + +} + +func doTestPages(t *testing.T, paginator *paginator) { + + paginatorPages := paginator.Pagers() + + require.Equal(t, 5, len(paginatorPages)) + require.Equal(t, 21, paginator.TotalNumberOfElements()) + require.Equal(t, 5, paginator.PageSize()) + require.Equal(t, 5, paginator.TotalPages()) + + first := paginatorPages[0] + require.Equal(t, template.HTML("page/1/"), first.URL()) + require.Equal(t, first, first.First()) + require.True(t, first.HasNext()) + require.Equal(t, paginatorPages[1], first.Next()) + require.False(t, first.HasPrev()) + require.Nil(t, first.Prev()) + require.Equal(t, 5, first.NumberOfElements()) + require.Equal(t, 1, first.PageNumber()) + + third := paginatorPages[2] + require.True(t, third.HasNext()) + require.True(t, third.HasPrev()) + require.Equal(t, paginatorPages[1], third.Prev()) + + last := paginatorPages[4] + require.Equal(t, template.HTML("page/5/"), last.URL()) + require.Equal(t, last, last.Last()) + require.False(t, last.HasNext()) + require.Nil(t, last.Next()) + require.True(t, last.HasPrev()) + require.Equal(t, 1, last.NumberOfElements()) + require.Equal(t, 5, last.PageNumber()) +} + +func TestPagerNoPages(t *testing.T) { + t.Parallel() + s := newTestSite(t) + pages := createTestPages(s, 0) + groups, _ := pages.GroupBy("Weight", "desc") + + urlFactory := func(page int) string { + return fmt.Sprintf("page/%d/", page) + } + + paginator, _ := newPaginatorFromPages(pages, 5, urlFactory) + doTestPagerNoPages(t, paginator) + + first := paginator.Pagers()[0].First() + require.Empty(t, first.PageGroups()) + require.Empty(t, first.Pages()) + + paginator, _ = newPaginatorFromPageGroups(groups, 5, urlFactory) + doTestPagerNoPages(t, paginator) + + first = paginator.Pagers()[0].First() + require.Empty(t, first.PageGroups()) + require.Empty(t, first.Pages()) + +} + +func doTestPagerNoPages(t *testing.T, paginator *paginator) { + paginatorPages := paginator.Pagers() + + require.Equal(t, 1, len(paginatorPages)) + require.Equal(t, 0, paginator.TotalNumberOfElements()) + require.Equal(t, 5, paginator.PageSize()) + require.Equal(t, 0, paginator.TotalPages()) + + // pageOne should be nothing but the first + pageOne := paginatorPages[0] + require.NotNil(t, pageOne.First()) + require.False(t, pageOne.HasNext()) + require.False(t, pageOne.HasPrev()) + require.Nil(t, pageOne.Next()) + require.Equal(t, 1, len(pageOne.Pagers())) + require.Equal(t, 0, pageOne.Pages().Len()) + require.Equal(t, 0, pageOne.NumberOfElements()) + require.Equal(t, 0, pageOne.TotalNumberOfElements()) + require.Equal(t, 0, pageOne.TotalPages()) + require.Equal(t, 1, pageOne.PageNumber()) + require.Equal(t, 5, pageOne.PageSize()) + +} + +func TestPaginationURLFactory(t *testing.T) { + t.Parallel() + cfg, fs := newTestCfg() + cfg.Set("paginatePath", "zoo") + + for _, uglyURLs := range []bool{false, true} { + for _, canonifyURLs := range []bool{false, true} { + t.Run(fmt.Sprintf("uglyURLs=%t,canonifyURLs=%t", uglyURLs, canonifyURLs), func(t *testing.T) { + + tests := []struct { + name string + d targetPathDescriptor + baseURL string + page int + expected string + }{ + {"HTML home page 32", + targetPathDescriptor{Kind: KindHome, Type: output.HTMLFormat}, "http://example.com/", 32, "/zoo/32/"}, + {"JSON home page 42", + targetPathDescriptor{Kind: KindHome, Type: output.JSONFormat}, "http://example.com/", 42, "/zoo/42/"}, + // Issue #1252 + {"BaseURL with sub path", + targetPathDescriptor{Kind: KindHome, Type: output.HTMLFormat}, "http://example.com/sub/", 999, "/sub/zoo/999/"}, + } + + for _, test := range tests { + d := test.d + cfg.Set("baseURL", test.baseURL) + cfg.Set("canonifyURLs", canonifyURLs) + cfg.Set("uglyURLs", uglyURLs) + d.UglyURLs = uglyURLs + + expected := test.expected + + if canonifyURLs { + expected = strings.Replace(expected, "/sub", "", 1) + } + + if uglyURLs { + expected = expected[:len(expected)-1] + "." + test.d.Type.MediaType.Suffix + } + + pathSpec := newTestPathSpec(fs, cfg) + d.PathSpec = pathSpec + + factory := newPaginationURLFactory(d) + + got := factory(test.page) + + require.Equal(t, expected, got) + + } + }) + } + } +} + +func TestPaginator(t *testing.T) { + t.Parallel() + for _, useViper := range []bool{false, true} { + doTestPaginator(t, useViper) + } +} + +func doTestPaginator(t *testing.T, useViper bool) { + + cfg, fs := newTestCfg() + + pagerSize := 5 + if useViper { + cfg.Set("paginate", pagerSize) + } else { + cfg.Set("paginate", -1) + } + + s, err := NewSiteForCfg(deps.DepsCfg{Cfg: cfg, Fs: fs}) + require.NoError(t, err) + + pages := createTestPages(s, 12) + n1, _ := newPageOutput(s.newHomePage(), false, output.HTMLFormat) + n2, _ := newPageOutput(s.newHomePage(), false, output.HTMLFormat) + n1.Data["Pages"] = pages + + var paginator1 *Pager + + if useViper { + paginator1, err = n1.Paginator() + } else { + paginator1, err = n1.Paginator(pagerSize) + } + + require.Nil(t, err) + require.NotNil(t, paginator1) + require.Equal(t, 3, paginator1.TotalPages()) + require.Equal(t, 12, paginator1.TotalNumberOfElements()) + + n2.paginator = paginator1.Next() + paginator2, err := n2.Paginator() + require.Nil(t, err) + require.Equal(t, paginator2, paginator1.Next()) + + n1.Data["Pages"] = createTestPages(s, 1) + samePaginator, _ := n1.Paginator() + require.Equal(t, paginator1, samePaginator) + + pp, _ := s.NewPage("test") + p, _ := newPageOutput(pp, false, output.HTMLFormat) + + _, err = p.Paginator() + require.NotNil(t, err) +} + +func TestPaginatorWithNegativePaginate(t *testing.T) { + t.Parallel() + s := newTestSite(t, "paginate", -1) + n1, _ := newPageOutput(s.newHomePage(), false, output.HTMLFormat) + _, err := n1.Paginator() + require.Error(t, err) +} + +func TestPaginate(t *testing.T) { + t.Parallel() + for _, useViper := range []bool{false, true} { + doTestPaginate(t, useViper) + } +} + +func TestPaginatorURL(t *testing.T) { + t.Parallel() + cfg, fs := newTestCfg() + + cfg.Set("paginate", 2) + cfg.Set("paginatePath", "testing") + + for i := 0; i < 10; i++ { + // Issue #2177, do not double encode URLs + writeSource(t, fs, filepath.Join("content", "阅读", fmt.Sprintf("page%d.md", (i+1))), + fmt.Sprintf(`--- +title: Page%d +--- +Conten%d +`, (i+1), i+1)) + + } + writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), "<html><body>{{.Content}}</body></html>") + writeSource(t, fs, filepath.Join("layouts", "_default", "list.html"), + ` +<html><body> +Count: {{ .Paginator.TotalNumberOfElements }} +Pages: {{ .Paginator.TotalPages }} +{{ range .Paginator.Pagers -}} + {{ .PageNumber }}: {{ .URL }} +{{ end }} +</body></html>`) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + + th := testHelper{s.Cfg, s.Fs, t} + + th.assertFileContent(filepath.Join("public", "阅读", "testing", "2", "index.html"), "2: /%E9%98%85%E8%AF%BB/testing/2/") + +} + +func doTestPaginate(t *testing.T, useViper bool) { + pagerSize := 5 + + var ( + s *Site + err error + ) + + if useViper { + s = newTestSite(t, "paginate", pagerSize) + } else { + s = newTestSite(t, "paginate", -1) + } + + pages := createTestPages(s, 6) + n1, _ := newPageOutput(s.newHomePage(), false, output.HTMLFormat) + n2, _ := newPageOutput(s.newHomePage(), false, output.HTMLFormat) + + var paginator1, paginator2 *Pager + + if useViper { + paginator1, err = n1.Paginate(pages) + } else { + paginator1, err = n1.Paginate(pages, pagerSize) + } + + require.Nil(t, err) + require.NotNil(t, paginator1) + require.Equal(t, 2, paginator1.TotalPages()) + require.Equal(t, 6, paginator1.TotalNumberOfElements()) + + n2.paginator = paginator1.Next() + if useViper { + paginator2, err = n2.Paginate(pages) + } else { + paginator2, err = n2.Paginate(pages, pagerSize) + } + require.Nil(t, err) + require.Equal(t, paginator2, paginator1.Next()) + + pp, err := s.NewPage("test") + p, _ := newPageOutput(pp, false, output.HTMLFormat) + + _, err = p.Paginate(pages) + require.NotNil(t, err) +} + +func TestInvalidOptions(t *testing.T) { + t.Parallel() + s := newTestSite(t) + n1, _ := newPageOutput(s.newHomePage(), false, output.HTMLFormat) + + _, err := n1.Paginate(createTestPages(s, 1), 1, 2) + require.NotNil(t, err) + _, err = n1.Paginator(1, 2) + require.NotNil(t, err) + _, err = n1.Paginator(-1) + require.NotNil(t, err) +} + +func TestPaginateWithNegativePaginate(t *testing.T) { + t.Parallel() + cfg, fs := newTestCfg() + cfg.Set("paginate", -1) + + s, err := NewSiteForCfg(deps.DepsCfg{Cfg: cfg, Fs: fs}) + require.NoError(t, err) + + n, _ := newPageOutput(s.newHomePage(), false, output.HTMLFormat) + + _, err = n.Paginate(createTestPages(s, 2)) + require.NotNil(t, err) +} + +func TestPaginatePages(t *testing.T) { + t.Parallel() + s := newTestSite(t) + + groups, _ := createTestPages(s, 31).GroupBy("Weight", "desc") + pd := targetPathDescriptor{Kind: KindHome, Type: output.HTMLFormat, PathSpec: s.PathSpec, Addends: "t"} + + for i, seq := range []interface{}{createTestPages(s, 11), groups, WeightedPages{}, PageGroup{}, &Pages{}} { + v, err := paginatePages(pd, seq, 11) + require.NotNil(t, v, "Val %d", i) + require.Nil(t, err, "Err %d", i) + } + _, err := paginatePages(pd, Site{}, 11) + require.NotNil(t, err) + +} + +// Issue #993 +func TestPaginatorFollowedByPaginateShouldFail(t *testing.T) { + t.Parallel() + s := newTestSite(t, "paginate", 10) + n1, _ := newPageOutput(s.newHomePage(), false, output.HTMLFormat) + n2, _ := newPageOutput(s.newHomePage(), false, output.HTMLFormat) + + _, err := n1.Paginator() + require.Nil(t, err) + _, err = n1.Paginate(createTestPages(s, 2)) + require.NotNil(t, err) + + _, err = n2.Paginate(createTestPages(s, 2)) + require.Nil(t, err) + +} + +func TestPaginateFollowedByDifferentPaginateShouldFail(t *testing.T) { + t.Parallel() + s := newTestSite(t, "paginate", 10) + + n1, _ := newPageOutput(s.newHomePage(), false, output.HTMLFormat) + n2, _ := newPageOutput(s.newHomePage(), false, output.HTMLFormat) + + p1 := createTestPages(s, 2) + p2 := createTestPages(s, 10) + + _, err := n1.Paginate(p1) + require.Nil(t, err) + + _, err = n1.Paginate(p1) + require.Nil(t, err) + + _, err = n1.Paginate(p2) + require.NotNil(t, err) + + _, err = n2.Paginate(p2) + require.Nil(t, err) +} + +func TestProbablyEqualPageLists(t *testing.T) { + t.Parallel() + s := newTestSite(t) + fivePages := createTestPages(s, 5) + zeroPages := createTestPages(s, 0) + zeroPagesByWeight, _ := createTestPages(s, 0).GroupBy("Weight", "asc") + fivePagesByWeight, _ := createTestPages(s, 5).GroupBy("Weight", "asc") + ninePagesByWeight, _ := createTestPages(s, 9).GroupBy("Weight", "asc") + + for i, this := range []struct { + v1 interface{} + v2 interface{} + expect bool + }{ + {nil, nil, true}, + {"a", "b", true}, + {"a", fivePages, false}, + {fivePages, "a", false}, + {fivePages, createTestPages(s, 2), false}, + {fivePages, fivePages, true}, + {zeroPages, zeroPages, true}, + {fivePagesByWeight, fivePagesByWeight, true}, + {zeroPagesByWeight, fivePagesByWeight, false}, + {zeroPagesByWeight, zeroPagesByWeight, true}, + {fivePagesByWeight, fivePages, false}, + {fivePagesByWeight, ninePagesByWeight, false}, + } { + result := probablyEqualPageLists(this.v1, this.v2) + + if result != this.expect { + t.Errorf("[%d] got %t but expected %t", i, result, this.expect) + + } + } +} + +func TestPage(t *testing.T) { + t.Parallel() + urlFactory := func(page int) string { + return fmt.Sprintf("page/%d/", page) + } + + s := newTestSite(t) + + fivePages := createTestPages(s, 7) + fivePagesFuzzyWordCount, _ := createTestPages(s, 7).GroupBy("FuzzyWordCount", "asc") + + p1, _ := newPaginatorFromPages(fivePages, 2, urlFactory) + p2, _ := newPaginatorFromPageGroups(fivePagesFuzzyWordCount, 2, urlFactory) + + f1 := p1.pagers[0].First() + f2 := p2.pagers[0].First() + + page11, _ := f1.page(1) + page1Nil, _ := f1.page(3) + + page21, _ := f2.page(1) + page2Nil, _ := f2.page(3) + + require.Equal(t, 3, page11.fuzzyWordCount) + require.Nil(t, page1Nil) + + require.Equal(t, 3, page21.fuzzyWordCount) + require.Nil(t, page2Nil) +} + +func createTestPages(s *Site, num int) Pages { + pages := make(Pages, num) + + for i := 0; i < num; i++ { + p := s.newPage(filepath.FromSlash(fmt.Sprintf("/x/y/z/p%d.md", i))) + w := 5 + if i%2 == 0 { + w = 10 + } + p.fuzzyWordCount = i + 2 + p.Weight = w + pages[i] = p + + } + + return pages +} diff --git a/hugolib/paginator_test.go b/hugolib/paginator_test.go deleted file mode 100644 index 2fb87956f..000000000 --- a/hugolib/paginator_test.go +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright 2019 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 hugolib - -import ( - "fmt" - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestPaginator(t *testing.T) { - configFile := ` -baseURL = "https://example.com/foo/" - -[pagination] -pagerSize = 3 -path = "thepage" - -[languages.en] -weight = 1 -contentDir = "content/en" - -[languages.nn] -weight = 2 -contentDir = "content/nn" - -` - b := newTestSitesBuilder(t).WithConfigFile("toml", configFile) - var content []string - for i := range 9 { - for _, contentDir := range []string{"content/en", "content/nn"} { - content = append(content, fmt.Sprintf(contentDir+"/blog/page%d.md", i), fmt.Sprintf(`--- -title: Page %d ---- - -Content. -`, i)) - } - } - - b.WithContent(content...) - - pagTemplate := ` -{{ $pag := $.Paginator }} -Total: {{ $pag.TotalPages }} -First: {{ $pag.First.URL }} -Page Number: {{ $pag.PageNumber }} -URL: {{ $pag.URL }} -{{ with $pag.Next }}Next: {{ .URL }}{{ end }} -{{ with $pag.Prev }}Prev: {{ .URL }}{{ end }} -{{ range $i, $e := $pag.Pagers }} -{{ printf "%d: %d/%d %t" $i $pag.PageNumber .PageNumber (eq . $pag) -}} -{{ end }} -` - - b.WithTemplatesAdded("index.html", pagTemplate) - b.WithTemplatesAdded("index.xml", pagTemplate) - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", - "Page Number: 1", - "0: 1/1 true") - - b.AssertFileContent("public/thepage/2/index.html", - "Total: 3", - "Page Number: 2", - "URL: /foo/thepage/2/", - "Next: /foo/thepage/3/", - "Prev: /foo/", - "1: 2/2 true", - ) - - b.AssertFileContent("public/index.xml", - "Page Number: 1", - "0: 1/1 true") - b.AssertFileContent("public/thepage/2/index.xml", - "Page Number: 2", - "1: 2/2 true") - - b.AssertFileContent("public/nn/index.html", - "Page Number: 1", - "0: 1/1 true") - - b.AssertFileContent("public/nn/index.xml", - "Page Number: 1", - "0: 1/1 true") -} - -// Issue 6023 -func TestPaginateWithSort(t *testing.T) { - files := ` --- hugo.toml -- --- content/a/a.md -- --- content/z/b.md -- --- content/x/b.md -- --- content/x/a.md -- --- layouts/home.html -- -Paginate: {{ range (.Paginate (sort .Site.RegularPages ".File.Filename" "desc")).Pages }}|{{ .Path }}{{ end }} -` - b := Test(t, files) - - b.AssertFileContent("public/index.html", "Paginate: |/z/b|/x/b|/x/a|/a/a") -} - -// https://github.com/gohugoio/hugo/issues/6797 -func TestPaginateOutputFormat(t *testing.T) { - b := newTestSitesBuilder(t).WithSimpleConfigFile() - b.WithContent("_index.md", `--- -title: "Home" -cascade: - outputs: - - JSON ----`) - - for i := range 22 { - b.WithContent(fmt.Sprintf("p%d.md", i+1), fmt.Sprintf(`--- -title: "Page" -weight: %d ----`, i+1)) - } - - b.WithTemplatesAdded("index.json", `JSON: {{ .Paginator.TotalNumberOfElements }}: {{ range .Paginator.Pages }}|{{ .RelPermalink }}{{ end }}:DONE`) - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.json", - `JSON: 22 -|/p1/index.json|/p2/index.json| -`) - - // This looks odd, so are most bugs. - b.Assert(b.CheckExists("public/page/1/index.json/index.html"), qt.Equals, false) - b.Assert(b.CheckExists("public/page/1/index.json"), qt.Equals, false) - b.AssertFileContent("public/page/2/index.json", `JSON: 22: |/p11/index.json|/p12/index.json`) -} - -// Issue 10802 -func TestPaginatorEmptyPageGroups(t *testing.T) { - t.Parallel() - - files := ` --- config.toml -- -baseURL = "https://example.com/" --- content/p1.md -- --- content/p2.md -- --- layouts/index.html -- -{{ $empty := site.RegularPages | complement site.RegularPages }} -Len: {{ len $empty }}: Type: {{ printf "%T" $empty }} -{{ $pgs := $empty.GroupByPublishDate "January 2006" }} -{{ $pag := .Paginate $pgs }} -Len Pag: {{ len $pag.Pages }} -` - b := Test(t, files) - - b.AssertFileContent("public/index.html", "Len: 0", "Len Pag: 0") -} - -func TestPaginatorNodePagesOnly(t *testing.T) { - files := ` --- hugo.toml -- -[pagination] -pagerSize = 1 --- content/p1.md -- --- layouts/_default/single.html -- -Paginator: {{ .Paginator }} -` - b, err := TestE(t, files) - b.Assert(err, qt.IsNotNil) - b.Assert(err.Error(), qt.Contains, `error calling Paginator: pagination not supported for this page: kind: "page"`) -} - -func TestNilPointerErrorMessage(t *testing.T) { - files := ` --- hugo.toml -- --- content/p1.md -- --- layouts/_default/single.html -- -Home Filename: {{ site.Home.File.Filename }} -` - b, err := TestE(t, files) - b.Assert(err, qt.IsNotNil) - b.Assert(err.Error(), qt.Contains, `single.html:1:22: executing "single.html" – File is nil; wrap it in if or with: {{ with site.Home.File }}{{ .Filename }}{{ end }}`) -} diff --git a/hugolib/params_test.go b/hugolib/params_test.go deleted file mode 100644 index 7f7566024..000000000 --- a/hugolib/params_test.go +++ /dev/null @@ -1,143 +0,0 @@ -// 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 hugolib - -import ( - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestFrontMatterParamsInItsOwnSection(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.org/" --- content/_index.md -- -+++ -title = "Home" -[[cascade]] -background = 'yosemite.jpg' -[cascade.params] -a = "home-a" -b = "home-b" -[cascade._target] -kind = 'page' -+++ --- content/p1.md -- ---- -title: "P1" -summary: "frontmatter.summary" -params: - a: "p1-a" - summary: "params.summary" ---- --- layouts/_default/single.html -- -Params: {{ range $k, $v := .Params }}{{ $k }}: {{ $v }}|{{ end }}$ -Summary: {{ .Summary }}| -` - - b := Test(t, files) - - b.AssertFileContent("public/p1/index.html", - "Params: a: p1-a|b: home-b|background: yosemite.jpg|draft: false|iscjklanguage: false|summary: params.summary|title: P1|$", - "Summary: frontmatter.summary|", - ) -} - -func TestFrontMatterParamsPath(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.org/" -disableKinds = ["taxonomy", "term"] - --- content/p1.md -- ---- -title: "P1" -date: 2019-08-07 -path: "/a/b/c" -slug: "s1" ---- --- content/mysection/_index.md -- ---- -title: "My Section" -date: 2022-08-07 -path: "/a/b" ---- --- layouts/index.html -- -RegularPages: {{ range site.RegularPages }}{{ .Path }}|{{ .RelPermalink }}|{{ .Title }}|{{ .Date.Format "2006-02-01" }}| Slug: {{ .Params.slug }}|{{ end }}$ -Sections: {{ range site.Sections }}{{ .Path }}|{{ .RelPermalink }}|{{ .Title }}|{{ .Date.Format "2006-02-01" }}| Slug: {{ .Params.slug }}|{{ end }}$ -{{ $ab := site.GetPage "a/b" }} -a/b pages: {{ range $ab.RegularPages }}{{ .Path }}|{{ .RelPermalink }}|{{ end }}$ -` - - b := Test(t, files) - - b.AssertFileContent("public/index.html", - "RegularPages: /a/b/c|/a/b/s1/|P1|2019-07-08| Slug: s1|$", - "Sections: /a|/a/|As", - "a/b pages: /a/b/c|/a/b/s1/|$", - ) -} - -func TestFrontMatterParamsLangNoCascade(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.org/" -disableKinds = ["taxonomy", "term"] -defaultContentLanguage = "en" -defaultContentLanguageInSubdir = true -[languages] -[languages.en] -weight = 1 -[languages.nn] -weight = 2 --- content/_index.md -- -+++ -[[cascade]] -background = 'yosemite.jpg' -lang = 'nn' -+++ - -` - - b, err := TestE(t, files) - b.Assert(err, qt.IsNotNil) -} - -// Issue 11970. -func TestFrontMatterBuildIsHugoKeyword(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.org/" --- content/p1.md -- ---- -title: "P1" -build: "foo" ---- --- layouts/_default/single.html -- -Params: {{ range $k, $v := .Params }}{{ $k }}: {{ $v }}|{{ end }}$ -` - b, err := TestE(t, files) - - b.Assert(err, qt.IsNotNil) - b.Assert(err.Error(), qt.Contains, "We renamed the _build keyword") -} diff --git a/hugolib/path_separators_test.go b/hugolib/path_separators_test.go new file mode 100644 index 000000000..3a73869ad --- /dev/null +++ b/hugolib/path_separators_test.go @@ -0,0 +1,38 @@ +// Copyright 2015 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 hugolib + +import ( + "path/filepath" + "strings" + "testing" +) + +var simplePageYAML = `--- +contenttype: "" +--- +Sample Text +` + +func TestDegenerateMissingFolderInPageFilename(t *testing.T) { + t.Parallel() + s := newTestSite(t) + p, err := s.NewPageFrom(strings.NewReader(simplePageYAML), filepath.Join("foobar")) + if err != nil { + t.Fatalf("Error in NewPageFrom") + } + if p.Section() != "" { + t.Fatalf("No section should be set for a file path: foobar") + } +} diff --git a/hugolib/paths/paths.go b/hugolib/paths/paths.go deleted file mode 100644 index 60ec873f9..000000000 --- a/hugolib/paths/paths.go +++ /dev/null @@ -1,133 +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 paths - -import ( - "path/filepath" - "strings" - - hpaths "github.com/gohugoio/hugo/common/paths" - - "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/modules" - - "github.com/gohugoio/hugo/hugofs" -) - -var FilePathSeparator = string(filepath.Separator) - -type Paths struct { - Fs *hugofs.Fs - Cfg config.AllProvider - - // Directories to store Resource related artifacts. - AbsResourcesDir string - - AbsPublishDir string - - // When in multihost mode, this returns a list of base paths below PublishDir - // for each language. - MultihostTargetBasePaths []string -} - -func New(fs *hugofs.Fs, cfg config.AllProvider) (*Paths, error) { - bcfg := cfg.BaseConfig() - publishDir := bcfg.PublishDir - if publishDir == "" { - panic("publishDir not set") - } - - absPublishDir := hpaths.AbsPathify(bcfg.WorkingDir, publishDir) - if !strings.HasSuffix(absPublishDir, FilePathSeparator) { - absPublishDir += FilePathSeparator - } - // If root, remove the second '/' - if absPublishDir == "//" { - absPublishDir = FilePathSeparator - } - absResourcesDir := hpaths.AbsPathify(bcfg.WorkingDir, cfg.Dirs().ResourceDir) - if !strings.HasSuffix(absResourcesDir, FilePathSeparator) { - absResourcesDir += FilePathSeparator - } - if absResourcesDir == "//" { - absResourcesDir = FilePathSeparator - } - - var multihostTargetBasePaths []string - if cfg.IsMultihost() && len(cfg.Languages()) > 1 { - for _, l := range cfg.Languages() { - multihostTargetBasePaths = append(multihostTargetBasePaths, hpaths.ToSlashPreserveLeading(l.Lang)) - } - } - - p := &Paths{ - Fs: fs, - Cfg: cfg, - AbsResourcesDir: absResourcesDir, - AbsPublishDir: absPublishDir, - MultihostTargetBasePaths: multihostTargetBasePaths, - } - - return p, nil -} - -func (p *Paths) AllModules() modules.Modules { - return p.Cfg.GetConfigSection("allModules").(modules.Modules) -} - -// GetBasePath returns any path element in baseURL if needed. -// The path returned will have a leading, but no trailing slash. -func (p *Paths) GetBasePath(isRelativeURL bool) string { - if isRelativeURL && p.Cfg.CanonifyURLs() { - // The baseURL will be prepended later. - return "" - } - return p.Cfg.BaseURL().BasePathNoTrailingSlash -} - -func (p *Paths) Lang() string { - if p == nil || p.Cfg.Language() == nil { - return "" - } - return p.Cfg.Language().Lang -} - -func (p *Paths) GetTargetLanguageBasePath() string { - if p.Cfg.IsMultihost() { - // In a multihost configuration all assets will be published below the language code. - return p.Lang() - } - return p.GetLanguagePrefix() -} - -func (p *Paths) GetLanguagePrefix() string { - return p.Cfg.LanguagePrefix() -} - -// AbsPathify creates an absolute path if given a relative path. If already -// absolute, the path is just cleaned. -func (p *Paths) AbsPathify(inPath string) string { - return hpaths.AbsPathify(p.Cfg.BaseConfig().WorkingDir, inPath) -} - -// RelPathify trims any WorkingDir prefix from the given filename. If -// the filename is not considered to be absolute, the path is just cleaned. -func (p *Paths) RelPathify(filename string) string { - filename = filepath.Clean(filename) - if !filepath.IsAbs(filename) { - return filename - } - - return strings.TrimPrefix(strings.TrimPrefix(filename, p.Cfg.BaseConfig().WorkingDir), FilePathSeparator) -} diff --git a/hugolib/permalinker.go b/hugolib/permalinker.go index aeaa673f7..5e7a13a02 100644 --- a/hugolib/permalinker.go +++ b/hugolib/permalinker.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Hugo Authors. All rights reserved. +// Copyright 2017-present 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,7 +13,10 @@ package hugolib -var _ Permalinker = (*pageState)(nil) +var ( + _ Permalinker = (*Page)(nil) + _ Permalinker = (*OutputFormat)(nil) +) // Permalinker provides permalinks of both the relative and absolute kind. type Permalinker interface { diff --git a/hugolib/permalinks.go b/hugolib/permalinks.go new file mode 100644 index 000000000..14d0cba5e --- /dev/null +++ b/hugolib/permalinks.go @@ -0,0 +1,209 @@ +// Copyright 2015 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 hugolib + +import ( + "errors" + "fmt" + "path" + "regexp" + "strconv" + "strings" +) + +// pathPattern represents a string which builds up a URL from attributes +type pathPattern string + +// pageToPermaAttribute is the type of a function which, given a page and a tag +// can return a string to go in that position in the page (or an error) +type pageToPermaAttribute func(*Page, string) (string, error) + +// PermalinkOverrides maps a section name to a PathPattern +type PermalinkOverrides map[string]pathPattern + +// knownPermalinkAttributes maps :tags in a permalink specification to a +// function which, given a page and the tag, returns the resulting string +// to be used to replace that tag. +var knownPermalinkAttributes map[string]pageToPermaAttribute + +var attributeRegexp *regexp.Regexp + +// validate determines if a PathPattern is well-formed +func (pp pathPattern) validate() bool { + fragments := strings.Split(string(pp[1:]), "/") + var bail = false + for i := range fragments { + if bail { + return false + } + if len(fragments[i]) == 0 { + bail = true + continue + } + + matches := attributeRegexp.FindAllStringSubmatch(fragments[i], -1) + if matches == nil { + continue + } + + for _, match := range matches { + k := strings.ToLower(match[0][1:]) + if _, ok := knownPermalinkAttributes[k]; !ok { + return false + } + } + } + return true +} + +type permalinkExpandError struct { + pattern pathPattern + section string + err error +} + +func (pee *permalinkExpandError) Error() string { + return fmt.Sprintf("error expanding %q section %q: %s", string(pee.pattern), pee.section, pee.err) +} + +var ( + errPermalinkIllFormed = errors.New("permalink ill-formed") + errPermalinkAttributeUnknown = errors.New("permalink attribute not recognised") +) + +// Expand on a PathPattern takes a Page and returns the fully expanded Permalink +// or an error explaining the failure. +func (pp pathPattern) Expand(p *Page) (string, error) { + if !pp.validate() { + return "", &permalinkExpandError{pattern: pp, section: "<all>", err: errPermalinkIllFormed} + } + sections := strings.Split(string(pp), "/") + for i, field := range sections { + if len(field) == 0 { + continue + } + + matches := attributeRegexp.FindAllStringSubmatch(field, -1) + + if matches == nil { + continue + } + + newField := field + + for _, match := range matches { + attr := match[0][1:] + callback, ok := knownPermalinkAttributes[attr] + + if !ok { + return "", &permalinkExpandError{pattern: pp, section: strconv.Itoa(i), err: errPermalinkAttributeUnknown} + } + + newAttr, err := callback(p, attr) + + if err != nil { + return "", &permalinkExpandError{pattern: pp, section: strconv.Itoa(i), err: err} + } + + newField = strings.Replace(newField, match[0], newAttr, 1) + } + + sections[i] = newField + } + return strings.Join(sections, "/"), nil +} + +func pageToPermalinkDate(p *Page, dateField string) (string, error) { + // a Page contains a Node which provides a field Date, time.Time + switch dateField { + case "year": + return strconv.Itoa(p.Date.Year()), nil + case "month": + return fmt.Sprintf("%02d", int(p.Date.Month())), nil + case "monthname": + return p.Date.Month().String(), nil + case "day": + return fmt.Sprintf("%02d", p.Date.Day()), nil + case "weekday": + return strconv.Itoa(int(p.Date.Weekday())), nil + case "weekdayname": + return p.Date.Weekday().String(), nil + case "yearday": + return strconv.Itoa(p.Date.YearDay()), nil + } + //TODO: support classic strftime escapes too + // (and pass those through despite not being in the map) + panic("coding error: should not be here") +} + +// pageToPermalinkTitle returns the URL-safe form of the title +func pageToPermalinkTitle(p *Page, _ string) (string, error) { + // Page contains Node which has Title + // (also contains URLPath which has Slug, sometimes) + return p.s.PathSpec.URLize(p.Title), nil +} + +// pageToPermalinkFilename returns the URL-safe form of the filename +func pageToPermalinkFilename(p *Page, _ string) (string, error) { + //var extension = p.Source.Ext + //var name = p.Source.Path()[0 : len(p.Source.Path())-len(extension)] + return p.s.PathSpec.URLize(p.Source.TranslationBaseName()), nil +} + +// if the page has a slug, return the slug, else return the title +func pageToPermalinkSlugElseTitle(p *Page, a string) (string, error) { + if p.Slug != "" { + // Don't start or end with a - + // TODO(bep) this doesn't look good... Set the Slug once. + if strings.HasPrefix(p.Slug, "-") { + p.Slug = p.Slug[1:len(p.Slug)] + } + + if strings.HasSuffix(p.Slug, "-") { + p.Slug = p.Slug[0 : len(p.Slug)-1] + } + return p.s.PathSpec.URLize(p.Slug), nil + } + return pageToPermalinkTitle(p, a) +} + +func pageToPermalinkSection(p *Page, _ string) (string, error) { + // Page contains Node contains URLPath which has Section + return p.Section(), nil +} + +func pageToPermalinkSections(p *Page, _ string) (string, error) { + // TODO(bep) we have some superflous URLize in this file, but let's + // deal with that later. + return path.Join(p.CurrentSection().sections...), nil +} + +func init() { + knownPermalinkAttributes = map[string]pageToPermaAttribute{ + "year": pageToPermalinkDate, + "month": pageToPermalinkDate, + "monthname": pageToPermalinkDate, + "day": pageToPermalinkDate, + "weekday": pageToPermalinkDate, + "weekdayname": pageToPermalinkDate, + "yearday": pageToPermalinkDate, + "section": pageToPermalinkSection, + "sections": pageToPermalinkSections, + "title": pageToPermalinkTitle, + "slug": pageToPermalinkSlugElseTitle, + "filename": pageToPermalinkFilename, + } + + attributeRegexp = regexp.MustCompile(`:\w+`) +} diff --git a/hugolib/permalinks_test.go b/hugolib/permalinks_test.go new file mode 100644 index 000000000..7a4bf78c2 --- /dev/null +++ b/hugolib/permalinks_test.go @@ -0,0 +1,94 @@ +// Copyright 2015 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 hugolib + +import ( + "strings" + "testing" +) + +// testdataPermalinks is used by a couple of tests; the expandsTo content is +// subject to the data in SIMPLE_PAGE_JSON. +var testdataPermalinks = []struct { + spec string + valid bool + expandsTo string +}{ + //{"/:year/:month/:title/", true, "/2012/04/spf13-vim-3.0-release-and-new-website/"}, + //{"/:title", true, "/spf13-vim-3.0-release-and-new-website"}, + //{":title", true, "spf13-vim-3.0-release-and-new-website"}, + //{"/blog/:year/:yearday/:title", true, "/blog/2012/97/spf13-vim-3.0-release-and-new-website"}, + {"/:year-:month-:title", true, "/2012-04-spf13-vim-3.0-release-and-new-website"}, + {"/blog/:year-:month-:title", true, "/blog/2012-04-spf13-vim-3.0-release-and-new-website"}, + {"/blog-:year-:month-:title", true, "/blog-2012-04-spf13-vim-3.0-release-and-new-website"}, + //{"/blog/:fred", false, ""}, + //{"/:year//:title", false, ""}, + //{ + //"/:section/:year/:month/:day/:weekdayname/:yearday/:title", + //true, + //"/blue/2012/04/06/Friday/97/spf13-vim-3.0-release-and-new-website", + //}, + //{ + //"/:weekday/:weekdayname/:month/:monthname", + //true, + //"/5/Friday/04/April", + //}, + //{ + //"/:slug/:title", + //true, + //"/spf13-vim-3-0-release-and-new-website/spf13-vim-3.0-release-and-new-website", + //}, +} + +func TestPermalinkValidation(t *testing.T) { + t.Parallel() + for _, item := range testdataPermalinks { + pp := pathPattern(item.spec) + have := pp.validate() + if have == item.valid { + continue + } + var howBad string + if have { + howBad = "validates but should not have" + } else { + howBad = "should have validated but did not" + } + t.Errorf("permlink spec %q %s", item.spec, howBad) + } +} + +func TestPermalinkExpansion(t *testing.T) { + t.Parallel() + s := newTestSite(t) + page, err := s.NewPageFrom(strings.NewReader(simplePageJSON), "blue/test-page.md") + + if err != nil { + t.Fatalf("failed before we began, could not parse SIMPLE_PAGE_JSON: %s", err) + } + for _, item := range testdataPermalinks { + if !item.valid { + continue + } + pp := pathPattern(item.spec) + result, err := pp.Expand(page) + if err != nil { + t.Errorf("failed to expand page: %s", err) + continue + } + if result != item.expandsTo { + t.Errorf("expansion mismatch!\n\tExpected: %q\n\tReceived: %q", item.expandsTo, result) + } + } +} diff --git a/hugolib/prune_resources.go b/hugolib/prune_resources.go deleted file mode 100644 index 50868e872..000000000 --- a/hugolib/prune_resources.go +++ /dev/null @@ -1,19 +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 hugolib - -// GC requires a build first and must run on it's own. It is not thread safe. -func (h *HugoSites) GC() (int, error) { - return h.Deps.ResourceSpec.FileCaches.Prune() -} diff --git a/hugolib/rebuild_test.go b/hugolib/rebuild_test.go deleted file mode 100644 index d4a15fb5b..000000000 --- a/hugolib/rebuild_test.go +++ /dev/null @@ -1,1968 +0,0 @@ -package hugolib - -import ( - "fmt" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/fortytw2/leaktest" - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/common/types" - "github.com/gohugoio/hugo/htesting" - "github.com/gohugoio/hugo/markup/asciidocext" - "github.com/gohugoio/hugo/resources/resource_transformers/tocss/dartsass" - "github.com/gohugoio/hugo/resources/resource_transformers/tocss/scss" -) - -const rebuildFilesSimple = ` --- hugo.toml -- -baseURL = "https://example.com" -disableKinds = ["term", "taxonomy", "sitemap", "robotstxt", "404"] -disableLiveReload = true -[outputFormats] - [outputFormats.rss] - weight = 10 - [outputFormats.html] - weight = 20 -[outputs] -home = ["rss", "html"] -section = ["html"] -page = ["html"] --- content/mysection/_index.md -- ---- -title: "My Section" ---- --- content/mysection/mysectionbundle/index.md -- ---- -title: "My Section Bundle" ---- -My Section Bundle Content. --- content/mysection/mysectionbundle/mysectionbundletext.txt -- -My Section Bundle Text 2 Content. --- content/mysection/mysectionbundle/mysectionbundlecontent.md -- ---- -title: "My Section Bundle Content" ---- -My Section Bundle Content Content. --- content/mysection/_index.md -- ---- -title: "My Section" ---- --- content/mysection/mysectiontext.txt -- -Content. --- content/_index.md -- ---- -title: "Home" ---- -Home Content. --- content/hometext.txt -- -Home Text Content. --- content/myothersection/myothersectionpage.md -- ---- -title: "myothersectionpage" ---- -myothersectionpage Content. --- content/mythirdsection/mythirdsectionpage.md -- ---- -title: "mythirdsectionpage" ---- -mythirdsectionpage Content. -{{< myshortcodetext >}} -§§§ myothertext -foo -§§§ --- assets/mytext.txt -- -Assets My Text. --- assets/myshortcodetext.txt -- -Assets My Shortcode Text. --- assets/myothertext.txt -- -Assets My Other Text. --- layouts/_default/single.html -- -Single: {{ .Title }}|{{ .Content }}$ -Resources: {{ range $i, $e := .Resources }}{{ $i }}:{{ .RelPermalink }}|{{ .Content }}|{{ end }}$ -Len Resources: {{ len .Resources }}| --- layouts/_default/list.html -- -List: {{ .Title }}|{{ .Content }}$ -Len Resources: {{ len .Resources }}| -Resources: {{ range $i, $e := .Resources }}{{ $i }}:{{ .RelPermalink }}|{{ .Content }}|{{ end }}$ --- layouts/shortcodes/foo.html -- -Foo. --- layouts/shortcodes/myshortcodetext.html -- -{{ warnf "mytext %s" now}} -{{ $r := resources.Get "myshortcodetext.txt" }} -My Shortcode Text: {{ $r.Content }}|{{ $r.Permalink }}| --- layouts/_default/_markup/render-codeblock-myothertext.html -- -{{ $r := resources.Get "myothertext.txt" }} -My Other Text: {{ $r.Content }}|{{ $r.Permalink }}| - -` - -func TestRebuildEditLeafBundleHeaderOnly(t *testing.T) { - t.Parallel() - for i := 0; i < 3; i++ { - b := TestRunning(t, rebuildFilesSimple) - b.AssertFileContent("public/mysection/mysectionbundle/index.html", - "My Section Bundle Content Content.") - b.EditFileReplaceAll("content/mysection/mysectionbundle/index.md", "My Section Bundle Content.", "My Section Bundle Content Edited.").Build() - b.AssertFileContent("public/mysection/mysectionbundle/index.html", - "My Section Bundle Content Edited.") - b.AssertRenderCountPage(2) // home (rss) + bundle. - b.AssertRenderCountContent(1) - } -} - -func TestRebuildEditTextFileInLeafBundle(t *testing.T) { - b := TestRunning(t, rebuildFilesSimple) - b.AssertFileContent("public/mysection/mysectionbundle/index.html", - "Resources: 0:/mysection/mysectionbundle/mysectionbundletext.txt|My Section Bundle Text 2 Content.|1:|<p>My Section Bundle Content Content.</p>\n|$") - - b.EditFileReplaceAll("content/mysection/mysectionbundle/mysectionbundletext.txt", "Content.", "Content Edited.").Build() - b.AssertFileContent("public/mysection/mysectionbundle/index.html", - "Text 2 Content Edited") - b.AssertRenderCountPage(1) - b.AssertRenderCountContent(0) -} - -func TestRebuildEditTextFileInShortcode(t *testing.T) { - t.Parallel() - for range 3 { - b := TestRunning(t, rebuildFilesSimple) - b.AssertFileContent("public/mythirdsection/mythirdsectionpage/index.html", - "Text: Assets My Shortcode Text.") - b.EditFileReplaceAll("assets/myshortcodetext.txt", "My Shortcode Text", "My Shortcode Text Edited").Build() - fmt.Println(b.LogString()) - b.AssertFileContent("public/mythirdsection/mythirdsectionpage/index.html", - "Text: Assets My Shortcode Text Edited.") - - } -} - -func TestRebuildEditTextFileInHook(t *testing.T) { - t.Parallel() - for range 3 { - b := TestRunning(t, rebuildFilesSimple) - b.AssertFileContent("public/mythirdsection/mythirdsectionpage/index.html", - "Text: Assets My Other Text.") - b.AssertFileContent("public/myothertext.txt", "Assets My Other Text.") - b.EditFileReplaceAll("assets/myothertext.txt", "My Other Text", "My Other Text Edited").Build() - b.AssertFileContent("public/mythirdsection/mythirdsectionpage/index.html", - "Text: Assets My Other Text Edited.") - - } -} - -func TestRebuiEditUnmarshaledYamlFileInLeafBundle(t *testing.T) { - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableLiveReload = true -disableKinds = ["taxonomy", "term", "sitemap", "robotsTXT", "404", "rss"] --- content/mybundle/index.md -- --- content/mybundle/mydata.yml -- -foo: bar --- layouts/_default/single.html -- -MyData: {{ .Resources.Get "mydata.yml" | transform.Unmarshal }}| -` - b := TestRunning(t, files) - - b.AssertFileContent("public/mybundle/index.html", "MyData: map[foo:bar]") - - b.EditFileReplaceAll("content/mybundle/mydata.yml", "bar", "bar edited").Build() - - b.AssertFileContent("public/mybundle/index.html", "MyData: map[foo:bar edited]") -} - -func TestRebuildEditTextFileInHomeBundle(t *testing.T) { - b := TestRunning(t, rebuildFilesSimple) - b.AssertFileContent("public/index.html", "Home Content.") - b.AssertFileContent("public/index.html", "Home Text Content.") - - b.EditFileReplaceAll("content/hometext.txt", "Content.", "Content Edited.").Build() - b.AssertFileContent("public/index.html", "Home Content.") - b.AssertFileContent("public/index.html", "Home Text Content Edited.") - b.AssertRenderCountPage(1) - b.AssertRenderCountContent(0) -} - -func TestRebuildEditTextFileInBranchBundle(t *testing.T) { - b := TestRunning(t, rebuildFilesSimple) - b.AssertFileContent("public/mysection/index.html", "My Section", "0:/mysection/mysectiontext.txt|Content.|") - - b.EditFileReplaceAll("content/mysection/mysectiontext.txt", "Content.", "Content Edited.").Build() - b.AssertFileContent("public/mysection/index.html", "My Section", "0:/mysection/mysectiontext.txt|Content Edited.|") - b.AssertRenderCountPage(1) - b.AssertRenderCountContent(0) -} - -func testRebuildBothWatchingAndRunning(t *testing.T, files string, withB func(b *IntegrationTestBuilder)) { - t.Helper() - for _, opt := range []TestOpt{TestOptWatching(), TestOptRunning()} { - b := Test(t, files, opt) - withB(b) - } -} - -func TestRebuildRenameTextFileInLeafBundle(t *testing.T) { - testRebuildBothWatchingAndRunning(t, rebuildFilesSimple, func(b *IntegrationTestBuilder) { - b.AssertFileContent("public/mysection/mysectionbundle/index.html", "My Section Bundle Text 2 Content.", "Len Resources: 2|") - - b.RenameFile("content/mysection/mysectionbundle/mysectionbundletext.txt", "content/mysection/mysectionbundle/mysectionbundletext2.txt").Build() - b.AssertFileContent("public/mysection/mysectionbundle/index.html", "mysectionbundletext2", "My Section Bundle Text 2 Content.", "Len Resources: 2|") - b.AssertRenderCountPage(8) - b.AssertRenderCountContent(9) - }) -} - -func TestRebuilEditContentFileInLeafBundle(t *testing.T) { - b := TestRunning(t, rebuildFilesSimple) - b.AssertFileContent("public/mysection/mysectionbundle/index.html", "My Section Bundle Content Content.") - b.EditFileReplaceAll("content/mysection/mysectionbundle/mysectionbundlecontent.md", "Content Content.", "Content Content Edited.").Build() - b.AssertFileContent("public/mysection/mysectionbundle/index.html", "My Section Bundle Content Content Edited.") -} - -func TestRebuilEditContentFileThenAnother(t *testing.T) { - b := TestRunning(t, rebuildFilesSimple) - b.EditFileReplaceAll("content/mysection/mysectionbundle/mysectionbundlecontent.md", "Content Content.", "Content Content Edited.").Build() - b.AssertFileContent("public/mysection/mysectionbundle/index.html", "My Section Bundle Content Content Edited.") - b.AssertRenderCountPage(1) - b.AssertRenderCountContent(2) - - b.EditFileReplaceAll("content/myothersection/myothersectionpage.md", "myothersectionpage Content.", "myothersectionpage Content Edited.").Build() - b.AssertFileContent("public/myothersection/myothersectionpage/index.html", "myothersectionpage Content Edited") - b.AssertRenderCountPage(2) - b.AssertRenderCountContent(2) -} - -func TestRebuildRenameTextFileInBranchBundle(t *testing.T) { - b := TestRunning(t, rebuildFilesSimple) - b.AssertFileContent("public/mysection/index.html", "My Section") - - b.RenameFile("content/mysection/mysectiontext.txt", "content/mysection/mysectiontext2.txt").Build() - b.AssertFileContent("public/mysection/index.html", "mysectiontext2", "My Section") - b.AssertRenderCountPage(3) - b.AssertRenderCountContent(2) -} - -func TestRebuildRenameTextFileInHomeBundle(t *testing.T) { - b := TestRunning(t, rebuildFilesSimple) - b.AssertFileContent("public/index.html", "Home Text Content.") - - b.RenameFile("content/hometext.txt", "content/hometext2.txt").Build() - b.AssertFileContent("public/index.html", "hometext2", "Home Text Content.") - b.AssertRenderCountPage(5) -} - -func TestRebuildRenameDirectoryWithLeafBundle(t *testing.T) { - b := TestRunning(t, rebuildFilesSimple) - b.RenameDir("content/mysection/mysectionbundle", "content/mysection/mysectionbundlerenamed").Build() - b.AssertFileContent("public/mysection/mysectionbundlerenamed/index.html", "My Section Bundle") - b.AssertRenderCountPage(2) -} - -func TestRebuildRenameDirectoryWithBranchBundle(t *testing.T) { - b := TestRunning(t, rebuildFilesSimple) - b.RenameDir("content/mysection", "content/mysectionrenamed").Build() - b.AssertFileContent("public/mysectionrenamed/index.html", "My Section") - b.AssertFileContent("public/mysectionrenamed/mysectionbundle/index.html", "My Section Bundle") - b.AssertFileContent("public/mysectionrenamed/mysectionbundle/mysectionbundletext.txt", "My Section Bundle Text 2 Content.") - b.AssertRenderCountPage(5) -} - -func TestRebuildRenameDirectoryWithRegularPageUsedInHome(t *testing.T) { - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableLiveReload = true --- content/foo/p1.md -- ---- -title: "P1" ---- --- layouts/index.html -- -Pages: {{ range .Site.RegularPages }}{{ .RelPermalink }}|{{ end }}$ -` - b := TestRunning(t, files) - - b.AssertFileContent("public/index.html", "Pages: /foo/p1/|$") - - b.RenameDir("content/foo", "content/bar").Build() - - b.AssertFileContent("public/index.html", "Pages: /bar/p1/|$") -} - -func TestRebuildAddRegularFileRegularPageUsedInHomeMultilingual(t *testing.T) { - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableLiveReload = true -[languages] -[languages.en] -weight = 1 -[languages.nn] -weight = 2 -[languages.fr] -weight = 3 -[languages.a] -weight = 4 -[languages.b] -weight = 5 -[languages.c] -weight = 6 -[languages.d] -weight = 7 -[languages.e] -weight = 8 -[languages.f] -weight = 9 -[languages.g] -weight = 10 -[languages.h] -weight = 11 -[languages.i] -weight = 12 -[languages.j] -weight = 13 --- content/foo/_index.md -- --- content/foo/data.txt -- --- content/foo/p1.md -- --- content/foo/p1.nn.md -- --- content/foo/p1.fr.md -- --- content/foo/p1.a.md -- --- content/foo/p1.b.md -- --- content/foo/p1.c.md -- --- content/foo/p1.d.md -- --- content/foo/p1.e.md -- --- content/foo/p1.f.md -- --- content/foo/p1.g.md -- --- content/foo/p1.h.md -- --- content/foo/p1.i.md -- --- content/foo/p1.j.md -- --- layouts/index.html -- -RegularPages: {{ range .Site.RegularPages }}{{ .RelPermalink }}|{{ end }}$ -` - b := TestRunning(t, files) - - b.AssertFileContent("public/index.html", "RegularPages: /foo/p1/|$") - b.AssertFileContent("public/nn/index.html", "RegularPages: /nn/foo/p1/|$") - b.AssertFileContent("public/i/index.html", "RegularPages: /i/foo/p1/|$") - - b.AddFiles("content/foo/p2.md", ``).Build() - - b.AssertFileContent("public/index.html", "RegularPages: /foo/p1/|/foo/p2/|$") - b.AssertFileContent("public/fr/index.html", "RegularPages: /fr/foo/p1/|$") - - b.AddFiles("content/foo/p2.fr.md", ``).Build() - b.AssertFileContent("public/fr/index.html", "RegularPages: /fr/foo/p1/|/fr/foo/p2/|$") - - b.AddFiles("content/foo/p2.i.md", ``).Build() - b.AssertFileContent("public/i/index.html", "RegularPages: /i/foo/p1/|/i/foo/p2/|$") -} - -func TestRebuildRenameDirectoryWithBranchBundleFastRender(t *testing.T) { - recentlyVisited := types.NewEvictingQueue[string](10).Add("/a/b/c/") - b := TestRunning(t, rebuildFilesSimple, func(cfg *IntegrationTestConfig) { cfg.BuildCfg = BuildCfg{RecentlyTouched: recentlyVisited} }) - b.RenameDir("content/mysection", "content/mysectionrenamed").Build() - b.AssertFileContent("public/mysectionrenamed/index.html", "My Section") - b.AssertFileContent("public/mysectionrenamed/mysectionbundle/index.html", "My Section Bundle") - b.AssertFileContent("public/mysectionrenamed/mysectionbundle/mysectionbundletext.txt", "My Section Bundle Text 2 Content.") - b.AssertRenderCountPage(5) -} - -func TestRebuilErrorRecovery(t *testing.T) { - b := TestRunning(t, rebuildFilesSimple) - _, err := b.EditFileReplaceAll("content/mysection/mysectionbundle/index.md", "My Section Bundle Content.", "My Section Bundle Content\n\n\n\n{{< foo }}.").BuildE() - - b.Assert(err, qt.Not(qt.IsNil)) - b.Assert(err.Error(), qt.Contains, filepath.FromSlash(`"/content/mysection/mysectionbundle/index.md:8:9": unrecognized character`)) - - // Fix the error - b.EditFileReplaceAll("content/mysection/mysectionbundle/index.md", "{{< foo }}", "{{< foo >}}").Build() -} - -func TestRebuildAddPageListPagesInHome(t *testing.T) { - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableLiveReload = true --- content/asection/s1.md -- --- content/p1.md -- ---- -title: "P1" -weight: 1 ---- --- layouts/_default/single.html -- -Single: {{ .Title }}|{{ .Content }}| --- layouts/index.html -- -Pages: {{ range .RegularPages }}{{ .RelPermalink }}|{{ end }}$ -` - - b := TestRunning(t, files) - b.AssertFileContent("public/index.html", "Pages: /p1/|$") - b.AddFiles("content/p2.md", ``).Build() - b.AssertFileContent("public/index.html", "Pages: /p1/|/p2/|$") -} - -func TestRebuildAddPageWithSpaceListPagesInHome(t *testing.T) { - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableLiveReload = true --- content/asection/s1.md -- --- content/p1.md -- ---- -title: "P1" -weight: 1 ---- --- layouts/_default/single.html -- -Single: {{ .Title }}|{{ .Content }}| --- layouts/index.html -- -Pages: {{ range .RegularPages }}{{ .RelPermalink }}|{{ end }}$ -` - - b := TestRunning(t, files) - b.AssertFileContent("public/index.html", "Pages: /p1/|$") - b.AddFiles("content/test test/index.md", ``).Build() - b.AssertFileContent("public/index.html", "Pages: /p1/|/test-test/|$") -} - -func TestRebuildScopedToOutputFormat(t *testing.T) { - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableKinds = ["term", "taxonomy", "sitemap", "robotstxt", "404"] -disableLiveReload = true --- content/p1.md -- ---- -title: "P1" -outputs: ["html", "json"] ---- -P1 Content. - -{{< myshort >}} --- layouts/_default/single.html -- -Single HTML: {{ .Title }}|{{ .Content }}| --- layouts/_default/single.json -- -Single JSON: {{ .Title }}|{{ .Content }}| --- layouts/shortcodes/myshort.html -- -My short. -` - b := Test(t, files, TestOptRunning()) - b.AssertRenderCountPage(3) - b.AssertRenderCountContent(1) - b.AssertFileContent("public/p1/index.html", "Single HTML: P1|<p>P1 Content.</p>\n") - b.AssertFileContent("public/p1/index.json", "Single JSON: P1|<p>P1 Content.</p>\n") - b.EditFileReplaceAll("layouts/_default/single.html", "Single HTML", "Single HTML Edited").Build() - b.AssertFileContent("public/p1/index.html", "Single HTML Edited: P1|<p>P1 Content.</p>\n") - b.AssertRenderCountPage(1) - - // Edit shortcode. Note that this is reused across all output formats. - b.EditFileReplaceAll("layouts/shortcodes/myshort.html", "My short", "My short edited").Build() - b.AssertFileContent("public/p1/index.html", "My short edited") - b.AssertFileContent("public/p1/index.json", "My short edited") - b.AssertRenderCountPage(3) // rss (uses .Content) + 2 single pages. -} - -func TestRebuildBaseof(t *testing.T) { - files := ` --- hugo.toml -- -title = "Hugo Site" -baseURL = "https://example.com" -disableKinds = ["term", "taxonomy"] -disableLiveReload = true --- layouts/_default/baseof.html -- -Baseof: {{ .Title }}| -{{ block "main" . }}default{{ end }} --- layouts/index.html -- -{{ define "main" }} -Home: {{ .Title }}|{{ .Content }}| -{{ end }} -` - testRebuildBothWatchingAndRunning(t, files, func(b *IntegrationTestBuilder) { - b.AssertFileContent("public/index.html", "Baseof: Hugo Site|", "Home: Hugo Site||") - b.EditFileReplaceFunc("layouts/_default/baseof.html", func(s string) string { - return strings.Replace(s, "Baseof", "Baseof Edited", 1) - }).Build() - b.AssertFileContent("public/index.html", "Baseof Edited: Hugo Site|", "Home: Hugo Site||") - }) -} - -func TestRebuildSingle(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -title = "Hugo Site" -baseURL = "https://example.com" -disableKinds = ["term", "taxonomy", "sitemap", "robotstxt", "404"] -disableLiveReload = true --- content/p1.md -- ---- -title: "P1" ---- -P1 Content. --- layouts/index.html -- -Home. --- layouts/single.html -- -Single: {{ .Title }}|{{ .Content }}| -{{ with (templates.Defer (dict "key" "global")) }} -Defer. -{{ end }} -` - b := Test(t, files, TestOptRunning()) - b.AssertFileContent("public/p1/index.html", "Single: P1|", "Defer.") - b.AssertRenderCountPage(3) - b.AssertRenderCountContent(1) - b.EditFileReplaceFunc("layouts/single.html", func(s string) string { - s = strings.Replace(s, "Single", "Single Edited", 1) - s = strings.Replace(s, "Defer.", "Defer Edited.", 1) - return s - }).Build() - b.AssertFileContent("public/p1/index.html", "Single Edited: P1|", "Defer Edited.") - b.AssertRenderCountPage(1) - b.AssertRenderCountContent(0) -} - -func TestRebuildSingleWithBaseofEditSingle(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -title = "Hugo Site" -baseURL = "https://example.com" -disableKinds = ["term", "taxonomy"] -disableLiveReload = true --- content/p1.md -- ---- -title: "P1" ---- -P1 Content. -[foo](/foo) --- layouts/_default/baseof.html -- -Baseof: {{ .Title }}| -{{ block "main" . }}default{{ end }} -{{ with (templates.Defer (dict "foo" "bar")) }} -Defer. -{{ end }} --- layouts/index.html -- -Home. --- layouts/_default/single.html -- -{{ define "main" }} -Single: {{ .Title }}|{{ .Content }}| -{{ end }} -` - b := Test(t, files, TestOptRunning()) - b.AssertFileContent("public/p1/index.html", "Single: P1|") - b.EditFileReplaceFunc("layouts/_default/single.html", func(s string) string { - return strings.Replace(s, "Single", "Single Edited", 1) - }).Build() - b.AssertFileContent("public/p1/index.html", "Single Edited") -} - -func TestRebuildSingleWithBaseofEditBaseof(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -title = "Hugo Site" -baseURL = "https://example.com" -disableKinds = ["term", "taxonomy"] -disableLiveReload = true --- content/p1.md -- ---- -title: "P1" ---- -P1 Content. -[foo](/foo) --- layouts/_default/baseof.html -- -Baseof: {{ .Title }}| -{{ block "main" . }}default{{ end }} -{{ with (templates.Defer (dict "foo" "bar")) }} -Defer. -{{ end }} --- layouts/index.html -- -Home. --- layouts/_default/single.html -- -{{ define "main" }} -Single: {{ .Title }}|{{ .Content }}| -{{ end }} -` - b := Test(t, files, TestOptRunning()) - b.AssertFileContent("public/p1/index.html", "Single: P1|") - fmt.Println("===============") - b.EditFileReplaceAll("layouts/_default/baseof.html", "Baseof", "Baseof Edited").Build() - b.AssertFileContent("public/p1/index.html", "Baseof Edited") -} - -func TestRebuildWithDeferEditRenderHook(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -title = "Hugo Site" -baseURL = "https://example.com" -disableKinds = ["term", "taxonomy"] -disableLiveReload = true --- content/p1.md -- ---- -title: "P1" ---- -P1 Content. -[foo](/foo) --- layouts/_default/baseof.html -- -Baseof: {{ .Title }}| -{{ block "main" . }}default{{ end }} - {{ with (templates.Defer (dict "foo" "bar")) }} -Defer. -{{ end }} --- layouts/single.html -- -{{ define "main" }} -Single: {{ .Title }}|{{ .Content }}| -{{ end }} --- layouts/_default/_markup/render-link.html -- -Render Link. -` - b := Test(t, files, TestOptRunning()) - // Edit render hook. - b.EditFileReplaceAll("layouts/_default/_markup/render-link.html", "Render Link", "Render Link Edited").Build() - - b.AssertFileContent("public/p1/index.html", "Render Link Edited") -} - -func TestRebuildFromString(t *testing.T) { - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableKinds = ["term", "taxonomy", "sitemap", "robotstxt", "404"] -disableLiveReload = true --- content/p1.md -- ---- -title: "P1" -layout: "l1" ---- -P1 Content. --- content/p2.md -- ---- -title: "P2" -layout: "l2" ---- -P2 Content. --- assets/mytext.txt -- -My Text --- layouts/_default/l1.html -- -{{ $r := partial "get-resource.html" . }} -L1: {{ .Title }}|{{ .Content }}|R: {{ $r.Content }}| --- layouts/_default/l2.html -- -L2. --- layouts/partials/get-resource.html -- -{{ $mytext := resources.Get "mytext.txt" }} -{{ $txt := printf "Text: %s" $mytext.Content }} -{{ $r := resources.FromString "r.txt" $txt }} -{{ return $r }} - -` - b := TestRunning(t, files) - - b.AssertFileContent("public/p1/index.html", "L1: P1|<p>P1 Content.</p>\n|R: Text: My Text|") - - b.EditFileReplaceAll("assets/mytext.txt", "My Text", "My Text Edited").Build() - - b.AssertFileContent("public/p1/index.html", "L1: P1|<p>P1 Content.</p>\n|R: Text: My Text Edited|") - - b.AssertRenderCountPage(1) -} - -func TestRebuildDeeplyNestedLink(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.com/" -disableKinds = ["term", "taxonomy", "sitemap", "robotstxt", "404"] -disableLiveReload = true --- content/s/p1.md -- ---- -title: "P1" ---- --- content/s/p2.md -- ---- -title: "P2" ---- --- content/s/p3.md -- ---- -title: "P3" ---- --- content/s/p4.md -- ---- -title: "P4" ---- --- content/s/p5.md -- ---- -title: "P5" ---- --- content/s/p6.md -- ---- -title: "P6" ---- --- content/s/p7.md -- ---- -title: "P7" ---- --- layouts/_default/list.html -- -List. --- layouts/_default/single.html -- -Single. --- layouts/_default/single.html -- -Next: {{ with .PrevInSection }}{{ .Title }}{{ end }}| -Prev: {{ with .NextInSection }}{{ .Title }}{{ end }}| - - -` - - b := TestRunning(t, files) - - b.AssertFileContent("public/s/p1/index.html", "Next: P2|") - b.EditFileReplaceAll("content/s/p7.md", "P7", "P7 Edited").Build() - b.AssertFileContent("public/s/p6/index.html", "Next: P7 Edited|") -} - -func TestRebuildVariations(t *testing.T) { - // t.Parallel() not supported, see https://github.com/fortytw2/leaktest/issues/4 - // This leaktest seems to be a little bit shaky on Travis. - if !htesting.IsCI() { - defer leaktest.CheckTimeout(t, 10*time.Second)() - } - - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableKinds = ["term", "taxonomy"] -disableLiveReload = true -defaultContentLanguage = "nn" -[pagination] -pagerSize = 20 -[security] -enableInlineShortcodes = true -[languages] -[languages.en] -weight = 1 -[languages.nn] -weight = 2 --- content/mysect/p1/index.md -- ---- -title: "P1" ---- -P1 Content. -{{< include "mysect/p2" >}} -§§§go { page="mysect/p3" } -hello -§§§ - -{{< foo.inline >}}Foo{{< /foo.inline >}} --- content/mysect/p2/index.md -- ---- -title: "P2" ---- -P2 Content. --- content/mysect/p3/index.md -- ---- -title: "P3" ---- -P3 Content. --- content/mysect/sub/_index.md -- --- content/mysect/sub/p4/index.md -- ---- -title: "P4" ---- -P4 Content. --- content/mysect/sub/p5/index.md -- ---- -title: "P5" -lastMod: 2019-03-02 ---- -P5 Content. --- content/myothersect/_index.md -- ---- -cascade: -- _target: - cascadeparam: "cascadevalue" ---- --- content/myothersect/sub/_index.md -- --- content/myothersect/sub/p6/index.md -- ---- -title: "P6" ---- -P6 Content. --- content/translations/p7.en.md -- ---- -title: "P7 EN" ---- -P7 EN Content. --- content/translations/p7.nn.md -- ---- -title: "P7 NN" ---- -P7 NN Content. --- layouts/index.html -- -Home: {{ .Title }}|{{ .Content }}| -RegularPages: {{ range .RegularPages }}{{ .RelPermalink }}|{{ end }}$ -Len RegularPagesRecursive: {{ len .RegularPagesRecursive }} -Site.Lastmod: {{ .Site.Lastmod.Format "2006-01-02" }}| -Paginate: {{ range (.Paginate .Site.RegularPages).Pages }}{{ .RelPermalink }}|{{ .Title }}|{{ end }}$ --- layouts/_default/single.html -- -Single: {{ .Title }}|{{ .Content }}| -Single Partial Cached: {{ partialCached "pcached" . }}| -Page.Lastmod: {{ .Lastmod.Format "2006-01-02" }}| -Cascade param: {{ .Params.cascadeparam }}| --- layouts/_default/list.html -- -List: {{ .Title }}|{{ .Content }}| -RegularPages: {{ range .RegularPages }}{{ .Title }}|{{ end }}$ -Len RegularPagesRecursive: {{ len .RegularPagesRecursive }} -RegularPagesRecursive: {{ range .RegularPagesRecursive }}{{ .RelPermalink }}|{{ end }}$ -List Partial P1: {{ partial "p1" . }}| -Page.Lastmod: {{ .Lastmod.Format "2006-01-02" }}| -Cascade param: {{ .Params.cascadeparam }}| --- layouts/partials/p1.html -- -Partial P1. --- layouts/partials/pcached.html -- -Partial Pcached. --- layouts/shortcodes/include.html -- -{{ $p := site.GetPage (.Get 0)}} -{{ with $p }} -Shortcode Include: {{ .Title }}| -{{ end }} -Shortcode .Page.Title: {{ .Page.Title }}| -Shortcode Partial P1: {{ partial "p1" . }}| --- layouts/_default/_markup/render-codeblock.html -- -{{ $p := site.GetPage (.Attributes.page)}} -{{ with $p }} -Codeblock Include: {{ .Title }}| -{{ end }} - - - -` - - b := NewIntegrationTestBuilder( - IntegrationTestConfig{ - T: t, - TxtarString: files, - Running: true, - BuildCfg: BuildCfg{ - testCounters: &buildCounters{}, - }, - // Verbose: true, - // LogLevel: logg.LevelTrace, - }, - ).Build() - - // When running the server, this is done on shutdown. - // Do this here to satisfy the leak detector above. - defer func() { - b.Assert(b.H.Close(), qt.IsNil) - }() - - contentRenderCount := b.counters.contentRenderCounter.Load() - pageRenderCount := b.counters.pageRenderCounter.Load() - - b.Assert(contentRenderCount > 0, qt.IsTrue) - b.Assert(pageRenderCount > 0, qt.IsTrue) - - // Test cases: - // - Edit content file direct - // - Edit content file transitive shortcode - // - Edit content file transitive render hook - // - Rename one language version of a content file - // - Delete content file, check site.RegularPages and section.RegularPagesRecursive (length) - // - Add content file (see above). - // - Edit shortcode - // - Edit inline shortcode - // - Edit render hook - // - Edit partial used in template - // - Edit partial used in shortcode - // - Edit partial cached. - // - Edit lastMod date in content file, check site.Lastmod. - editFile := func(filename string, replacementFunc func(s string) string) { - b.EditFileReplaceFunc(filename, replacementFunc).Build() - b.Assert(b.counters.contentRenderCounter.Load() < contentRenderCount, qt.IsTrue, qt.Commentf("count %d < %d", b.counters.contentRenderCounter.Load(), contentRenderCount)) - b.Assert(b.counters.pageRenderCounter.Load() < pageRenderCount, qt.IsTrue, qt.Commentf("count %d < %d", b.counters.pageRenderCounter.Load(), pageRenderCount)) - } - - b.AssertFileContent("public/index.html", "RegularPages: $", "Len RegularPagesRecursive: 7", "Site.Lastmod: 2019-03-02") - - b.AssertFileContent("public/mysect/p1/index.html", - "Single: P1|<p>P1 Content.", - "Shortcode Include: P2|", - "Codeblock Include: P3|") - - editFile("content/mysect/p1/index.md", func(s string) string { - return strings.ReplaceAll(s, "P1", "P1 Edited") - }) - - b.AssertFileContent("public/mysect/p1/index.html", "Single: P1 Edited|<p>P1 Edited Content.") - b.AssertFileContent("public/index.html", "RegularPages: $", "Len RegularPagesRecursive: 7", "Paginate: /mysect/sub/p5/|P5|/mysect/p1/|P1 Edited") - b.AssertFileContent("public/mysect/index.html", "RegularPages: P1 Edited|P2|P3|$", "Len RegularPagesRecursive: 5") - - // p2 is included in p1 via shortcode. - editFile("content/mysect/p2/index.md", func(s string) string { - return strings.ReplaceAll(s, "P2", "P2 Edited") - }) - - b.AssertFileContent("public/mysect/p1/index.html", "Shortcode Include: P2 Edited|") - - // p3 is included in p1 via codeblock hook. - editFile("content/mysect/p3/index.md", func(s string) string { - return strings.ReplaceAll(s, "P3", "P3 Edited") - }) - - b.AssertFileContent("public/mysect/p1/index.html", "Codeblock Include: P3 Edited|") - - // Remove a content file in a nested section. - b.RemoveFiles("content/mysect/sub/p4/index.md").Build() - b.AssertFileContent("public/mysect/index.html", "RegularPages: P1 Edited|P2 Edited|P3 Edited", "Len RegularPagesRecursive: 4") - b.AssertFileContent("public/mysect/sub/index.html", "RegularPages: P5|$", "RegularPagesRecursive: 1") - - // Rename one of the translations. - b.AssertFileContent("public/translations/index.html", "RegularPagesRecursive: /translations/p7/") - b.AssertFileContent("public/en/translations/index.html", "RegularPagesRecursive: /en/translations/p7/") - b.RenameFile("content/translations/p7.nn.md", "content/translations/p7rename.nn.md").Build() - b.AssertFileContent("public/translations/index.html", "RegularPagesRecursive: /translations/p7rename/") - b.AssertFileContent("public/en/translations/index.html", "RegularPagesRecursive: /en/translations/p7/") - - // Edit shortcode - editFile("layouts/shortcodes/include.html", func(s string) string { - return s + "\nShortcode Include Edited." - }) - b.AssertFileContent("public/mysect/p1/index.html", "Shortcode Include Edited.") - - // Edit render hook - editFile("layouts/_default/_markup/render-codeblock.html", func(s string) string { - return s + "\nCodeblock Include Edited." - }) - b.AssertFileContent("public/mysect/p1/index.html", "Codeblock Include Edited.") - - // Edit partial p1 - editFile("layouts/partials/p1.html", func(s string) string { - return strings.Replace(s, "Partial P1", "Partial P1 Edited", 1) - }) - b.AssertFileContent("public/mysect/index.html", "List Partial P1: Partial P1 Edited.") - b.AssertFileContent("public/mysect/p1/index.html", "Shortcode Partial P1: Partial P1 Edited.") - - // Edit partial cached. - editFile("layouts/partials/pcached.html", func(s string) string { - return strings.Replace(s, "Partial Pcached", "Partial Pcached Edited", 1) - }) - b.AssertFileContent("public/mysect/p1/index.html", "Pcached Edited.") - - // Edit lastMod date in content file, check site.Lastmod. - editFile("content/mysect/sub/p5/index.md", func(s string) string { - return strings.Replace(s, "2019-03-02", "2020-03-10", 1) - }) - b.AssertFileContent("public/index.html", "Site.Lastmod: 2020-03-10|") - b.AssertFileContent("public/mysect/index.html", "Page.Lastmod: 2020-03-10|") - - // Adjust the date back a few days. - editFile("content/mysect/sub/p5/index.md", func(s string) string { - return strings.Replace(s, "2020-03-10", "2019-03-08", 1) - }) - b.AssertFileContent("public/mysect/index.html", "Page.Lastmod: 2019-03-08|") - b.AssertFileContent("public/index.html", "Site.Lastmod: 2019-03-08|") - - // Check cascade mods. - b.AssertFileContent("public/myothersect/index.html", "Cascade param: cascadevalue|") - b.AssertFileContent("public/myothersect/sub/index.html", "Cascade param: cascadevalue|") - b.AssertFileContent("public/myothersect/sub/p6/index.html", "Cascade param: cascadevalue|") - - editFile("content/myothersect/_index.md", func(s string) string { - return strings.Replace(s, "cascadevalue", "cascadevalue edited", 1) - }) - b.AssertFileContent("public/myothersect/index.html", "Cascade param: cascadevalue edited|") - b.AssertFileContent("public/myothersect/sub/p6/index.html", "Cascade param: cascadevalue edited|") - - // Repurpose the cascadeparam to set the title. - editFile("content/myothersect/_index.md", func(s string) string { - return strings.Replace(s, "cascadeparam:", "title:", 1) - }) - b.AssertFileContent("public/myothersect/sub/index.html", "Cascade param: |", "List: cascadevalue edited|") - - // Revert it. - editFile("content/myothersect/_index.md", func(s string) string { - return strings.Replace(s, "title:", "cascadeparam:", 1) - }) - b.AssertFileContent("public/myothersect/sub/index.html", "Cascade param: cascadevalue edited|", "List: |") -} - -func TestRebuildVariationsJSNoneFingerprinted(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.com/" -disableKinds = ["term", "taxonomy", "sitemap", "robotsTXT", "404", "rss"] -disableLiveReload = true --- content/p1/index.md -- ---- -title: "P1" ---- -P1. --- content/p2/index.md -- ---- -title: "P2" ---- -P2. --- content/p3/index.md -- ---- -title: "P3" ---- -P3. --- content/p4/index.md -- ---- -title: "P4" ---- -P4. --- assets/main.css -- -body { - background: red; -} --- layouts/default/list.html -- -List. --- layouts/_default/single.html -- -Single. -{{ $css := resources.Get "main.css" | minify }} -RelPermalink: {{ $css.RelPermalink }}| - -` - - b := TestRunning(t, files) - - b.AssertFileContent("public/p1/index.html", "RelPermalink: /main.min.css|") - b.AssertFileContent("public/main.min.css", "body{background:red}") - - b.EditFileReplaceAll("assets/main.css", "red", "blue") - b.RemoveFiles("content/p2/index.md") - b.RemoveFiles("content/p3/index.md") - b.Build() - - b.AssertFileContent("public/main.min.css", "body{background:blue}") -} - -func TestRebuildVariationsJSInNestedCachedPartialFingerprinted(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.com/" -disableKinds = ["term", "taxonomy", "sitemap", "robotsTXT", "404", "rss"] -disableLiveReload = true --- content/p1/index.md -- ---- -title: "P1" ---- -P1. --- content/p2/index.md -- ---- -title: "P2" ---- -P2. --- content/p3/index.md -- ---- -title: "P3" ---- -P3. --- content/p4/index.md -- ---- -title: "P4" ---- -P4. --- assets/js/main.js -- -console.log("Hello"); --- layouts/_default/list.html -- -List. {{ partial "head.html" . }}$ --- layouts/_default/single.html -- -Single. {{ partial "head.html" . }}$ --- layouts/partials/head.html -- -{{ partialCached "js.html" . }}$ --- layouts/partials/js.html -- -{{ $js := resources.Get "js/main.js" | js.Build | fingerprint }} -RelPermalink: {{ $js.RelPermalink }}| -` - - b := TestRunning(t, files, TestOptOsFs()) - - b.AssertFileContent("public/p1/index.html", "/js/main.712a50b59d0f0dedb4e3606eaa3860b1f1a5305f6c42da30a2985e473ba314eb.js") - b.AssertFileContent("public/index.html", "/js/main.712a50b59d0f0dedb4e3606eaa3860b1f1a5305f6c42da30a2985e473ba314eb.js") - - b.EditFileReplaceAll("assets/js/main.js", "Hello", "Hello is Edited").Build() - - for i := 1; i < 5; i++ { - b.AssertFileContent(fmt.Sprintf("public/p%d/index.html", i), "/js/main.6535698cec9a21875f40ae03e96f30c4bee41a01e979224761e270b9034b2424.js") - } - - b.AssertFileContent("public/index.html", "/js/main.6535698cec9a21875f40ae03e96f30c4bee41a01e979224761e270b9034b2424.js") -} - -func TestRebuildVariationsJSInNestedPartialFingerprintedInBase(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.com/" -disableKinds = ["term", "taxonomy", "sitemap", "robotsTXT", "404", "rss"] -disableLiveReload = true --- assets/js/main.js -- -console.log("Hello"); --- layouts/_default/baseof.html -- -Base. {{ partial "common/head.html" . }}$ -{{ block "main" . }}default{{ end }} --- layouts/_default/list.html -- -{{ define "main" }}main{{ end }} --- layouts/partials/common/head.html -- -{{ partial "myfiles/js.html" . }}$ --- layouts/partials/myfiles/js.html -- -{{ $js := resources.Get "js/main.js" | js.Build | fingerprint }} -RelPermalink: {{ $js.RelPermalink }}| -` - - b := TestRunning(t, files, TestOptOsFs()) - - b.AssertFileContent("public/index.html", "/js/main.712a50b59d0f0dedb4e3606eaa3860b1f1a5305f6c42da30a2985e473ba314eb.js") - - b.EditFileReplaceAll("assets/js/main.js", "Hello", "Hello is Edited").Build() - - b.AssertFileContent("public/index.html", "/js/main.6535698cec9a21875f40ae03e96f30c4bee41a01e979224761e270b9034b2424.js") -} - -func TestRebuildVariationsJSBundled(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableKinds = ["term", "taxonomy", "sitemap", "robotsTXT", "404", "rss"] -disableLiveReload = true --- content/_index.md -- ---- -title: "Home" ---- --- content/p1.md -- ---- -title: "P1" -layout: "main" ---- --- content/p2.md -- ---- -title: "P2" ---- -{{< jsfingerprinted >}} --- content/p3.md -- ---- -title: "P3" -layout: "plain" ---- -{{< jsfingerprinted >}} --- content/main.js -- -console.log("Hello"); --- content/foo.js -- -console.log("Foo"); --- layouts/index.html -- -Home. -{{ $js := site.Home.Resources.Get "main.js" }} -{{ with $js }} -<script src="{{ .RelPermalink }}"></script> -{{ end }} --- layouts/_default/single.html -- -Single. Deliberately no .Content in here. --- layouts/_default/plain.html -- -Content: {{ .Content }}| --- layouts/_default/main.html -- -{{ $js := site.Home.Resources.Get "main.js" }} -{{ with $js }} -<script> -{{ .Content }} -</script> -{{ end }} --- layouts/shortcodes/jsfingerprinted.html -- -{{ $js := site.Home.Resources.Get "foo.js" | fingerprint }} -<script src="{{ $js.RelPermalink }}"></script> -` - - testCounters := &buildCounters{} - - b := NewIntegrationTestBuilder( - IntegrationTestConfig{ - T: t, - TxtarString: files, - Running: true, - // LogLevel: logg.LevelTrace, - // Verbose: true, - BuildCfg: BuildCfg{ - testCounters: testCounters, - }, - }, - ).Build() - - b.AssertFileContent("public/index.html", `<script src="/main.js"></script>`) - b.AssertFileContent("public/p1/index.html", "<script>\n\"console.log(\\\"Hello\\\");\"\n</script>") - b.AssertFileContent("public/p2/index.html", "Single. Deliberately no .Content in here.") - b.AssertFileContent("public/p3/index.html", "foo.57b4465b908531b43d4e4680ab1063d856b475cb1ae81ad43e0064ecf607bec1.js") - b.AssertRenderCountPage(4) - - // Edit JS file. - b.EditFileReplaceFunc("content/main.js", func(s string) string { - return strings.Replace(s, "Hello", "Hello is Edited", 1) - }).Build() - - b.AssertFileContent("public/p1/index.html", "<script>\n\"console.log(\\\"Hello is Edited\\\");\"\n</script>") - // The p1 (the one inlining the JS) should be rebuilt. - b.AssertRenderCountPage(2) - // But not the content file. - b.AssertRenderCountContent(0) - - // This is included with RelPermalink in a shortcode used in p3, but it's fingerprinted - // so we need to rebuild on change. - b.EditFileReplaceFunc("content/foo.js", func(s string) string { - return strings.Replace(s, "Foo", "Foo Edited", 1) - }).Build() - - // Verify that the hash has changed. - b.AssertFileContent("public/p3/index.html", "foo.3a332a088521231e5fc9bd22f15e0ccf507faa7b373fbff22959005b9a80481c.js") - - b.AssertRenderCountPage(1) - b.AssertRenderCountContent(1) -} - -func TestRebuildEditData(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableLiveReload = true -[security] -enableInlineShortcodes=true --- data/mydata.yaml -- -foo: bar --- content/_index.md -- ---- -title: "Home" ---- -{{< data "mydata.foo" >}}} --- content/p1.md -- ---- -title: "P1" ---- - -Foo inline: {{< foo.inline >}}{{ site.Data.mydata.foo }}|{{< /foo.inline >}} --- layouts/shortcodes/data.html -- -{{ $path := split (.Get 0) "." }} -{{ $data := index site.Data $path }} -Foo: {{ $data }}| --- layouts/index.html -- -Content: {{ .Content }}| --- layouts/_default/single.html -- -Single: {{ .Content }}| -` - b := TestRunning(t, files) - - b.AssertFileContent("public/index.html", "Foo: bar|") - b.AssertFileContent("public/p1/index.html", "Foo inline: bar|") - b.EditFileReplaceFunc("data/mydata.yaml", func(s string) string { - return strings.Replace(s, "bar", "bar edited", 1) - }).Build() - b.AssertFileContent("public/index.html", "Foo: bar edited|") - b.AssertFileContent("public/p1/index.html", "Foo inline: bar edited|") -} - -func TestRebuildEditHomeContent(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableLiveReload = true --- content/_index.md -- ---- -title: "Home" ---- -Home. --- layouts/index.html -- -Content: {{ .Content }} -` - b := TestRunning(t, files) - - b.AssertFileContent("public/index.html", "Content: <p>Home.</p>") - b.EditFileReplaceAll("content/_index.md", "Home.", "Home").Build() - b.AssertFileContent("public/index.html", "Content: <p>Home</p>") -} - -// Issue #13014. -func TestRebuildEditNotPermalinkableCustomOutputFormatTemplateInFastRenderMode(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.com/docs/" -disableLiveReload = true -[internal] -fastRenderMode = true -disableKinds = ["taxonomy", "term", "sitemap", "robotsTXT", "404"] -[outputFormats] - [outputFormats.SearchIndex] - baseName = 'Search' - isPlainText = true - mediaType = 'text/plain' - noAlternative = true - permalinkable = false - -[outputs] - home = ['HTML', 'SearchIndex'] --- content/_index.md -- ---- -title: "Home" ---- -Home. --- layouts/index.html -- -Home. --- layouts/_default/index.searchindex.txt -- -Text. {{ .Title }}|{{ .RelPermalink }}| - -` - b := TestRunning(t, files, TestOptInfo()) - - b.AssertFileContent("public/search.txt", "Text.") - - b.EditFileReplaceAll("layouts/_default/index.searchindex.txt", "Text.", "Text Edited.").Build() - - b.BuildPartial("/docs/search.txt") - - b.AssertFileContent("public/search.txt", "Text Edited.") -} - -func TestRebuildVariationsAssetsJSImport(t *testing.T) { - t.Parallel() - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableKinds = ["term", "taxonomy"] -disableLiveReload = true --- layouts/index.html -- -Home. {{ now }} -{{ with (resources.Get "js/main.js" | js.Build | fingerprint) }} -<script>{{ .Content | safeJS }}</script> -{{ end }} --- assets/js/lib/foo.js -- -export function foo() { - console.log("Foo"); -} --- assets/js/main.js -- -import { foo } from "./lib/foo.js"; -console.log("Hello"); -foo(); -` - - b := NewIntegrationTestBuilder( - IntegrationTestConfig{ - T: t, - TxtarString: files, - Running: true, - // LogLevel: logg.LevelTrace, - NeedsOsFS: true, - }, - ).Build() - - b.AssertFileContent("public/index.html", "Home.", "Hello", "Foo") - // Edit the imported file. - b.EditFileReplaceAll("assets/js/lib/foo.js", "Foo", "Foo Edited").Build() - b.AssertFileContent("public/index.html", "Home.", "Hello", "Foo Edited") -} - -func TestRebuildVariationsAssetsPostCSSImport(t *testing.T) { - if !htesting.IsCI() { - t.Skip("skip CI only") - } - - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableKinds = ["term", "taxonomy", "sitemap", "rss"] -disableLiveReload = true --- assets/css/lib/foo.css -- -body { - background: red; -} --- assets/css/main.css -- -@import "lib/foo.css"; --- package.json -- -{ - "devDependencies": { - "postcss-cli": "^9.0.1" - } -} --- content/p1.md -- ---- -title: "P1" ---- --- content/p2.md -- ---- -title: "P2" -layout: "foo" ---- -{{< fingerprinted >}} --- content/p3.md -- ---- -title: "P3" -layout: "foo" ---- -{{< notfingerprinted >}} --- layouts/shortcodes/fingerprinted.html -- -Fingerprinted. -{{ $opts := dict "inlineImports" true "noMap" true }} -{{ with (resources.Get "css/main.css" | postCSS $opts | fingerprint) }} -<style src="{{ .RelPermalink }}"></style> -{{ end }} --- layouts/shortcodes/notfingerprinted.html -- -Fingerprinted. -{{ $opts := dict "inlineImports" true "noMap" true }} -{{ with (resources.Get "css/main.css" | postCSS $opts) }} -<style src="{{ .RelPermalink }}"></style> -{{ end }} --- layouts/index.html -- -Home. -{{ $opts := dict "inlineImports" true "noMap" true }} -{{ with (resources.Get "css/main.css" | postCSS $opts) }} -<style>{{ .Content | safeCSS }}</style> -{{ end }} --- layouts/_default/foo.html -- -Foo. -{{ .Title }}|{{ .Content }}| --- layouts/_default/single.html -- -Single. -{{ $opts := dict "inlineImports" true "noMap" true }} -{{ with (resources.Get "css/main.css" | postCSS $opts) }} -<style src="{{ .RelPermalink }}"></style> -{{ end }} -` - - b := NewIntegrationTestBuilder( - IntegrationTestConfig{ - T: t, - TxtarString: files, - Running: true, - NeedsOsFS: true, - NeedsNpmInstall: true, - // LogLevel: logg.LevelDebug, - }, - ).Build() - - b.AssertFileContent("public/index.html", "Home.", "<style>body {\n\tbackground: red;\n}</style>") - b.AssertFileContent("public/p1/index.html", "Single.", "/css/main.css") - b.AssertRenderCountPage(4) - - // Edit the imported file. - b.EditFileReplaceFunc("assets/css/lib/foo.css", func(s string) string { - return strings.Replace(s, "red", "blue", 1) - }).Build() - - b.AssertRenderCountPage(4) - - b.AssertFileContent("public/index.html", "Home.", "<style>body {\n\tbackground: blue;\n}</style>") -} - -func TestRebuildI18n(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableLiveReload = true --- i18n/en.toml -- -hello = "Hello" --- layouts/index.html -- -Hello: {{ i18n "hello" }} -` - - b := TestRunning(t, files) - - b.AssertFileContent("public/index.html", "Hello: Hello") - - b.EditFileReplaceAll("i18n/en.toml", "Hello", "Hugo").Build() - - b.AssertFileContent("public/index.html", "Hello: Hugo") -} - -func TestRebuildEditContentNonDefaultLanguage(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableLiveReload = true -defaultContentLanguage = "en" -defaultContentLanguageInSubdir = true -[languages] -[languages.en] -weight = 1 -[languages.nn] -weight = 2 --- content/p1/index.en.md -- ---- -title: "P1 en" ---- -P1 en. --- content/p1/b.en.md -- ---- -title: "B en" ---- -B en. --- content/p1/f1.en.txt -- -F1 en --- content/p1/index.nn.md -- ---- -title: "P1 nn" ---- -P1 nn. --- content/p1/b.nn.md -- ---- -title: "B nn" ---- -B nn. --- content/p1/f1.nn.txt -- -F1 nn --- layouts/_default/single.html -- -Single: {{ .Title }}|{{ .Content }}|Bundled File: {{ with .Resources.GetMatch "f1.*" }}{{ .Content }}{{ end }}|Bundled Page: {{ with .Resources.GetMatch "b.*" }}{{ .Content }}{{ end }}| -` - - b := TestRunning(t, files) - - b.AssertFileContent("public/nn/p1/index.html", "Single: P1 nn|<p>P1 nn.</p>", "F1 nn|") - b.EditFileReplaceAll("content/p1/index.nn.md", "P1 nn.", "P1 nn edit.").Build() - b.AssertFileContent("public/nn/p1/index.html", "Single: P1 nn|<p>P1 nn edit.</p>\n|") - b.EditFileReplaceAll("content/p1/f1.nn.txt", "F1 nn", "F1 nn edit.").Build() - b.AssertFileContent("public/nn/p1/index.html", "Bundled File: F1 nn edit.") - b.EditFileReplaceAll("content/p1/b.nn.md", "B nn.", "B nn edit.").Build() - b.AssertFileContent("public/nn/p1/index.html", "B nn edit.") -} - -func TestRebuildEditContentNonDefaultLanguageDifferentBundles(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableLiveReload = true -defaultContentLanguage = "en" -defaultContentLanguageInSubdir = true -[languages] -[languages.en] -weight = 1 -contentDir = "content/en" -[languages.nn] -weight = 2 -contentDir = "content/nn" --- content/en/p1en/index.md -- ---- -title: "P1 en" ---- --- content/nn/p1nn/index.md -- ---- -title: "P1 nn" ---- -P1 nn. --- layouts/_default/single.html -- -Single: {{ .Title }}|{{ .Content }}| -` - - b := TestRunning(t, files) - - b.AssertFileContent("public/nn/p1nn/index.html", "Single: P1 nn|<p>P1 nn.</p>") - b.EditFileReplaceAll("content/nn/p1nn/index.md", "P1 nn.", "P1 nn edit.").Build() - b.AssertFileContent("public/nn/p1nn/index.html", "Single: P1 nn|<p>P1 nn edit.</p>\n|") - b.AssertFileContent("public/nn/p1nn/index.html", "P1 nn edit.") -} - -func TestRebuildVariationsAssetsSassImport(t *testing.T) { - if !htesting.IsCI() { - t.Skip("skip CI only") - } - - filesTemplate := ` --- hugo.toml -- -baseURL = "https://example.com" -disableKinds = ["term", "taxonomy"] -disableLiveReload = true --- assets/css/lib/foo.scss -- -body { - background: red; -} --- assets/css/main.scss -- -@import "lib/foo"; --- layouts/index.html -- -Home. -{{ $opts := dict "transpiler" "TRANSPILER" }} -{{ with (resources.Get "css/main.scss" | toCSS $opts) }} -<style>{{ .Content | safeCSS }}</style> -{{ end }} -` - - runTest := func(transpiler string) { - t.Run(transpiler, func(t *testing.T) { - files := strings.Replace(filesTemplate, "TRANSPILER", transpiler, 1) - b := NewIntegrationTestBuilder( - IntegrationTestConfig{ - T: t, - TxtarString: files, - Running: true, - NeedsOsFS: true, - }, - ).Build() - - b.AssertFileContent("public/index.html", "Home.", "background: red") - - // Edit the imported file. - b.EditFileReplaceFunc("assets/css/lib/foo.scss", func(s string) string { - return strings.Replace(s, "red", "blue", 1) - }).Build() - - b.AssertFileContent("public/index.html", "Home.", "background: blue") - }) - } - - if scss.Supports() { - runTest("libsass") - } - - if dartsass.Supports() { - runTest("dartsass") - } -} - -func benchmarkFilesEdit(count int) string { - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableKinds = ["term", "taxonomy"] -disableLiveReload = true --- layouts/_default/single.html -- -Single: {{ .Title }}|{{ .Content }}| --- layouts/_default/list.html -- -List: {{ .Title }}|{{ .Content }}| --- content/mysect/_index.md -- ---- -title: "My Sect" ---- - ` - - contentTemplate := ` ---- -title: "P%d" ---- -P%d Content. -` - - for i := range count { - files += fmt.Sprintf("-- content/mysect/p%d/index.md --\n%s", i, fmt.Sprintf(contentTemplate, i, i)) - } - - return files -} - -func BenchmarkRebuildContentFileChange(b *testing.B) { - files := benchmarkFilesEdit(500) - - cfg := IntegrationTestConfig{ - T: b, - TxtarString: files, - Running: true, - // Verbose: true, - // LogLevel: logg.LevelInfo, - } - builders := make([]*IntegrationTestBuilder, b.N) - - for i := range builders { - builders[i] = NewIntegrationTestBuilder(cfg) - builders[i].Build() - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - bb := builders[i] - bb.EditFileReplaceFunc("content/mysect/p123/index.md", func(s string) string { - return s + "... Edited" - }).Build() - // fmt.Println(bb.LogString()) - } -} - -func TestRebuildConcat(t *testing.T) { - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableLiveReload = true -disableKinds = ["taxonomy", "term", "sitemap", "robotsTXT", "404", "rss"] --- assets/a.css -- -a --- assets/b.css -- -b --- assets/c.css -- -c --- assets/common/c1.css -- -c1 --- assets/common/c2.css -- -c2 --- layouts/index.html -- -{{ $a := resources.Get "a.css" }} -{{ $b := resources.Get "b.css" }} -{{ $common := resources.Match "common/*.css" | resources.Concat "common.css" | minify }} -{{ $ab := slice $a $b $common | resources.Concat "ab.css" }} -all: {{ $ab.RelPermalink }} -` - b := TestRunning(t, files) - - b.AssertFileContent("public/ab.css", "abc1c2") - b.EditFileReplaceAll("assets/common/c2.css", "c2", "c2 edited").Build() - b.AssertFileContent("public/ab.css", "abc1c2 edited") - b.AddFiles("assets/common/c3.css", "c3").Build() - b.AssertFileContent("public/ab.css", "abc1c2 editedc3") -} - -func TestRebuildEditArchetypeFile(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableLiveReload = true --- archetypes/default.md -- ---- -title: "Default" ---- -` - - b := TestRunning(t, files) - // Just make sure that it doesn't panic. - b.EditFileReplaceAll("archetypes/default.md", "Default", "Default Edited").Build() -} - -func TestRebuildEditMixedCaseTemplateFileIssue12165(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableLiveReload = true --- layouts/partials/MyTemplate.html -- -MyTemplate --- layouts/index.html -- -MyTemplate: {{ partial "MyTemplate.html" . }}| - - -` - - b := TestRunning(t, files) - - b.AssertFileContent("public/index.html", "MyTemplate: MyTemplate") - - b.EditFileReplaceAll("layouts/partials/MyTemplate.html", "MyTemplate", "MyTemplate Edited").Build() - - b.AssertFileContent("public/index.html", "MyTemplate: MyTemplate Edited") -} - -func TestRebuildEditInlinePartial13723(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableLiveReload = true -title = "Foo" --- layouts/baseof.html -- -{{ block "main" . }}Main.{{ end }} -{{ partial "myinlinepartialinbaseof.html" . }}| - {{- define "_partials/myinlinepartialinbaseof.html" }} - My inline partial in baseof. - {{ end }} --- layouts/_partials/mypartial.html -- -Mypartial. -{{ partial "myinlinepartial.html" . }}| -{{- define "_partials/myinlinepartial.html" }} -Mypartial Inline.|{{ .Title }}| -{{ end }} --- layouts/_partials/myotherpartial.html -- -Myotherpartial. -{{ partial "myotherinlinepartial.html" . }}| -{{- define "_partials/myotherinlinepartial.html" }} -Myotherpartial Inline.|{{ .Title }}| -{{ return "myotherinlinepartial" }} -{{ end }} --- layouts/all.html -- -{{ define "main" }} -{{ partial "mypartial.html" . }}| -{{ partial "myotherpartial.html" . }}| - {{ partial "myinlinepartialinall.html" . }}| -{{ end }} - {{- define "_partials/myinlinepartialinall.html" }} - My inline partial in all. - {{ end }} - -` - b := TestRunning(t, files) - b.AssertFileContent("public/index.html", "Mypartial.", "Mypartial Inline.|Foo") - - // Edit inline partial in partial. - b.EditFileReplaceAll("layouts/_partials/mypartial.html", "Mypartial Inline.", "Mypartial Inline Edited.").Build() - b.AssertFileContent("public/index.html", "Mypartial Inline Edited.|Foo") - - // Edit inline partial in baseof. - b.EditFileReplaceAll("layouts/baseof.html", "My inline partial in baseof.", "My inline partial in baseof Edited.").Build() - b.AssertFileContent("public/index.html", "My inline partial in baseof Edited.") - - // Edit inline partial in all. - b.EditFileReplaceAll("layouts/all.html", "My inline partial in all.", "My inline partial in all Edited.").Build() - b.AssertFileContent("public/index.html", "My inline partial in all Edited.") -} - -func TestRebuildEditAsciidocContentFile(t *testing.T) { - if !asciidocext.Supports() { - t.Skip("skip asciidoc") - } - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableLiveReload = true -disableKinds = ["taxonomy", "term", "sitemap", "robotsTXT", "404", "rss", "home", "section"] -[security] -[security.exec] -allow = ['^python$', '^rst2html.*', '^asciidoctor$'] --- content/posts/p1.adoc -- ---- -title: "P1" ---- -P1 Content. --- content/posts/p2.adoc -- ---- -title: "P2" ---- -P2 Content. --- layouts/_default/single.html -- -Single: {{ .Title }}|{{ .Content }}| -` - b := TestRunning(t, files) - b.AssertFileContent("public/posts/p1/index.html", - "Single: P1|<div class=\"paragraph\">\n<p>P1 Content.</p>\n</div>\n|") - b.AssertRenderCountPage(2) - b.AssertRenderCountContent(2) - - b.EditFileReplaceAll("content/posts/p1.adoc", "P1 Content.", "P1 Content Edited.").Build() - - b.AssertFileContent("public/posts/p1/index.html", "Single: P1|<div class=\"paragraph\">\n<p>P1 Content Edited.</p>\n</div>\n|") - b.AssertRenderCountPage(1) - b.AssertRenderCountContent(1) -} - -func TestRebuildEditSingleListChangeUbuntuIssue12362(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ['rss','section','sitemap','taxonomy','term'] -disableLiveReload = true --- layouts/_default/list.html -- -{{ range .Pages }}{{ .Title }}|{{ end }} --- layouts/_default/single.html -- -{{ .Title }} --- content/p1.md -- ---- -title: p1 ---- -` - - b := TestRunning(t, files) - b.AssertFileContent("public/index.html", "p1|") - - b.AddFiles("content/p2.md", "---\ntitle: p2\n---").Build() - b.AssertFileContent("public/index.html", "p1|p2|") // this test passes, which doesn't match reality -} - -func TestRebuildHomeThenPageIssue12436(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableKinds = ['sitemap','taxonomy','term'] -disableLiveReload = true --- layouts/_default/list.html -- -{{ .Content }} --- layouts/_default/single.html -- -{{ .Content }} --- content/_index.md -- ---- -title: home ---- -home-content| --- content/p1/index.md -- ---- -title: p1 ---- -p1-content| -` - - b := TestRunning(t, files) - - b.AssertFileContent("public/index.html", "home-content|") - b.AssertFileContent("public/p1/index.html", "p1-content|") - b.AssertRenderCountPage(3) - - b.EditFileReplaceAll("content/_index.md", "home-content", "home-content-foo").Build() - b.AssertFileContent("public/index.html", "home-content-foo") - b.AssertRenderCountPage(2) // Home page rss + html - - b.EditFileReplaceAll("content/p1/index.md", "p1-content", "p1-content-foo").Build() - b.AssertFileContent("public/p1/index.html", "p1-content-foo") -} - -func TestRebuildEditTagIssue13648(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableLiveReload = true --- layouts/all.html -- -All. {{ range .Pages }}{{ .Title }}|{{ end }} --- content/p1.md -- ---- -title: "P1" -tags: ["tag1"] ---- - -` - b := TestRunning(t, files) - - b.AssertFileContent("public/tags/index.html", "All. Tag1|") - b.EditFileReplaceAll("content/p1.md", "tag1", "tag2").Build() - - // Note that the below is still not correct, as this is effectively a rename, and - // Tag2 should be removed from the list. - // But that is a harder problem to tackle. - b.AssertFileContent("public/tags/index.html", "All. Tag1|Tag2|") -} - -func TestRebuildEditNonReferencedResourceIssue13748(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableLiveReload = true --- content/mybundle/index.md -- --- content/mybundle/resource.txt -- -This is a resource file. --- layouts/all.html -- -All. -` - b := TestRunning(t, files) - - b.AssertFileContent("public/mybundle/resource.txt", "This is a resource file.") - b.EditFileReplaceAll("content/mybundle/resource.txt", "This is a resource file.", "This is an edited resource file.").Build() - b.AssertFileContent("public/mybundle/resource.txt", "This is an edited resource file.") -} diff --git a/hugolib/rendershortcodes_test.go b/hugolib/rendershortcodes_test.go deleted file mode 100644 index d8b51d3ed..000000000 --- a/hugolib/rendershortcodes_test.go +++ /dev/null @@ -1,529 +0,0 @@ -// 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 hugolib - -import ( - "path/filepath" - "strings" - "testing" -) - -func TestRenderShortcodesBasic(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ["home", "taxonomy", "term"] --- content/p1.md -- ---- -title: "p1" ---- -## p1-h1 -{{% include "p2" %}} --- content/p2.md -- ---- -title: "p2" ---- -### p2-h1 -{{< withhtml >}} -### p2-h2 -{{% withmarkdown %}} -### p2-h3 -{{% include "p3" %}} --- content/p3.md -- ---- -title: "p3" ---- -### p3-h1 -{{< withhtml >}} -### p3-h2 -{{% withmarkdown %}} -{{< level3 >}} --- layouts/shortcodes/include.html -- -{{ $p := site.GetPage (.Get 0) }} -{{ $p.RenderShortcodes }} --- layouts/shortcodes/withhtml.html -- -<div>{{ .Page.Title }} withhtml</div> --- layouts/shortcodes/withmarkdown.html -- -#### {{ .Page.Title }} withmarkdown --- layouts/shortcodes/level3.html -- -Level 3: {{ .Page.Title }} --- layouts/_default/single.html -- -Fragments: {{ .Fragments.Identifiers }}| -HasShortcode Level 1: {{ .HasShortcode "include" }}| -HasShortcode Level 2: {{ .HasShortcode "withmarkdown" }}| -HasShortcode Level 3: {{ .HasShortcode "level3" }}| -HasShortcode not found: {{ .HasShortcode "notfound" }}| -Content: {{ .Content }}| -` - - b := Test(t, files) - - b.AssertNoRenderShortcodesArtifacts() - b.AssertFileContent("public/p1/index.html", - "Fragments: [p1-h1 p2-h1 p2-h2 p2-h3 p2-withmarkdown p3-h1 p3-h2 p3-withmarkdown]|", - "HasShortcode Level 1: true|", - "HasShortcode Level 2: true|", - "HasShortcode Level 3: true|", - "HasShortcode not found: false|", - ) -} - -func TestRenderShortcodesNestedMultipleOutputFormatTemplates(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ["home", "taxonomy", "term", "section", "rss", "sitemap", "robotsTXT", "404"] -[outputs] -page = ["html", "json"] --- content/p1.md -- ---- -title: "p1" ---- -## p1-h1 -{{% include "p2" %}} --- content/p2.md -- ---- -title: "p2" ---- -### p2-h1 -{{% myshort %}} --- layouts/shortcodes/include.html -- -{{ $p := site.GetPage (.Get 0) }} -{{ $p.RenderShortcodes }} --- layouts/shortcodes/myshort.html -- -Myshort HTML. --- layouts/shortcodes/myshort.json -- -Myshort JSON. --- layouts/_default/single.html -- -HTML: {{ .Content }} --- layouts/_default/single.json -- -JSON: {{ .Content }} - - -` - - b := Test(t, files) - - b.AssertNoRenderShortcodesArtifacts() - b.AssertFileContent("public/p1/index.html", "Myshort HTML") - b.AssertFileContent("public/p1/index.json", "Myshort JSON") -} - -func TestRenderShortcodesEditNested(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableLiveReload = true -disableKinds = ["home", "taxonomy", "term", "section", "rss", "sitemap", "robotsTXT", "404"] --- content/p1.md -- ---- -title: "p1" ---- -## p1-h1 -{{% include "p2" %}} --- content/p2.md -- ---- -title: "p2" ---- -### p2-h1 -{{% myshort %}} --- layouts/shortcodes/include.html -- -{{ $p := site.GetPage (.Get 0) }} -{{ $p.RenderShortcodes }} --- layouts/shortcodes/myshort.html -- -Myshort Original. --- layouts/_default/single.html -- - {{ .Content }} -` - b := TestRunning(t, files) - b.AssertNoRenderShortcodesArtifacts() - b.AssertFileContent("public/p1/index.html", "Myshort Original.") - - b.EditFileReplaceAll("layouts/shortcodes/myshort.html", "Original", "Edited").Build() - b.AssertNoRenderShortcodesArtifacts() - b.AssertFileContent("public/p1/index.html", "Myshort Edited.") -} - -func TestRenderShortcodesEditIncludedPage(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableLiveReload = true -disableKinds = ["home", "taxonomy", "term", "section", "rss", "sitemap", "robotsTXT", "404"] --- content/p1.md -- ---- -title: "p1" ---- -## p1-h1 -{{% include "p2" %}} --- content/p2.md -- ---- -title: "p2" ---- -### Original -{{% myshort %}} --- layouts/shortcodes/include.html -- -{{ $p := site.GetPage (.Get 0) }} -{{ $p.RenderShortcodes }} --- layouts/shortcodes/myshort.html -- -Myshort Original. --- layouts/_default/single.html -- - {{ .Content }} - - - -` - - b := NewIntegrationTestBuilder( - IntegrationTestConfig{ - T: t, - TxtarString: files, - Running: true, - }, - ).Build() - - b.AssertNoRenderShortcodesArtifacts() - b.AssertFileContent("public/p1/index.html", "Original") - - b.EditFileReplaceFunc("content/p2.md", func(s string) string { - return strings.Replace(s, "Original", "Edited", 1) - }) - b.Build() - b.AssertNoRenderShortcodesArtifacts() - b.AssertFileContent("public/p1/index.html", "Edited") -} - -func TestRenderShortcodesEditSectionContentWithShortcodeInIncludedPageIssue12458(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableLiveReload = true -disableKinds = ["home", "taxonomy", "term", "rss", "sitemap", "robotsTXT", "404"] --- content/mysection/_index.md -- ---- -title: "My Section" ---- -## p1-h1 -{{% include "p2" %}} --- content/mysection/p2.md -- ---- -title: "p2" ---- -### Original -{{% myshort %}} --- layouts/shortcodes/include.html -- -{{ $p := .Page.GetPage (.Get 0) }} -{{ $p.RenderShortcodes }} --- layouts/shortcodes/myshort.html -- -Myshort Original. --- layouts/_default/list.html -- - {{ .Content }} - - - -` - b := TestRunning(t, files) - - b.AssertNoRenderShortcodesArtifacts() - b.AssertFileContent("public/mysection/index.html", "p1-h1") - b.EditFileReplaceAll("content/mysection/_index.md", "p1-h1", "p1-h1 Edited").Build() - b.AssertNoRenderShortcodesArtifacts() - b.AssertFileContent("public/mysection/index.html", "p1-h1 Edited") -} - -func TestRenderShortcodesNestedPageContextIssue12356(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ["taxonomy", "term", "rss", "sitemap", "robotsTXT", "404"] --- layouts/_default/_markup/render-image.html -- -{{- with .PageInner.Resources.Get .Destination -}}Image: {{ .RelPermalink }}|{{- end -}} --- layouts/_default/_markup/render-link.html -- -{{- with .PageInner.GetPage .Destination -}}Link: {{ .RelPermalink }}|{{- end -}} --- layouts/_default/_markup/render-heading.html -- -Heading: {{ .PageInner.Title }}: {{ .PlainText }}| --- layouts/_default/_markup/render-codeblock.html -- -CodeBlock: {{ .PageInner.Title }}: {{ .Type }}| --- layouts/_default/list.html -- -Content:{{ .Content }}| -Fragments: {{ with .Fragments }}{{.Identifiers }}{{ end }}| --- layouts/_default/single.html -- -Content:{{ .Content }}| --- layouts/shortcodes/include.html -- -{{ with site.GetPage (.Get 0) }} - {{ .RenderShortcodes }} -{{ end }} --- content/markdown/_index.md -- ---- -title: "Markdown" ---- -# H1 -|{{% include "/posts/p1" %}}| -![kitten](pixel3.png "Pixel 3") - -§§§go -fmt.Println("Hello") -§§§ - --- content/markdown2/_index.md -- ---- -title: "Markdown 2" ---- -|{{< include "/posts/p1" >}}| --- content/html/_index.html -- ---- -title: "HTML" ---- -|{{% include "/posts/p1" %}}| - --- content/posts/p1/index.md -- ---- -title: "p1" ---- -## H2-p1 -![kitten](pixel1.png "Pixel 1") -![kitten](pixel2.png "Pixel 2") -[p2](p2) - -§§§bash -echo "Hello" -§§§ - --- content/posts/p2/index.md -- ---- -title: "p2" ---- --- content/posts/p1/pixel1.png -- -iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg== --- content/posts/p1/pixel2.png -- -iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg== --- content/markdown/pixel3.png -- -iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg== --- content/html/pixel4.png -- -iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg== - -` - - b := Test(t, files) - - b.AssertNoRenderShortcodesArtifacts() - - b.AssertFileContent("public/markdown/index.html", - // Images. - "Image: /posts/p1/pixel1.png|\nImage: /posts/p1/pixel2.png|\n|\nImage: /markdown/pixel3.png|</p>\n|", - // Links. - "Link: /posts/p2/|", - // Code blocks - "CodeBlock: p1: bash|", "CodeBlock: Markdown: go|", - // Headings. - "Heading: Markdown: H1|", "Heading: p1: H2-p1|", - // Fragments. - "Fragments: [h1 h2-p1]|", - // Check that the special context markup is not rendered. - "! hugo_ctx", - ) - - b.AssertFileContent("public/markdown2/index.html", "! hugo_ctx", "Content:<p>|\n ![kitten](pixel1.png \"Pixel 1\")\n![kitten](pixel2.png \"Pixel 2\")\n|</p>\n|") - - b.AssertFileContent("public/html/index.html", "! hugo_ctx") -} - -// Issue 12854. -func TestRenderShortcodesWithHTML(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableLiveReload = true -disableKinds = ["home", "taxonomy", "term"] -markup.goldmark.renderer.unsafe = true --- content/p1.md -- ---- -title: "p1" ---- -{{% include "p2" %}} --- content/p2.md -- ---- -title: "p2" ---- -Hello <b>world</b>. Some **bold** text. Some Unicode: 神真美好. --- layouts/shortcodes/include.html -- -{{ with site.GetPage (.Get 0) }} -<div>{{ .RenderShortcodes }}</div> -{{ end }} --- layouts/_default/single.html -- -{{ .Content }} -` - - b := TestRunning(t, files, TestOptWarn()) - - b.AssertNoRenderShortcodesArtifacts() - b.AssertLogContains(filepath.ToSlash("WARN .RenderShortcodes detected inside HTML block in \"/content/p1.md\"; this may not be what you intended, see https://gohugo.io/methods/page/rendershortcodes/#limitations\nYou can suppress this warning by adding the following to your site configuration:\nignoreLogs = ['warning-rendershortcodes-in-html']")) - b.AssertFileContent("public/p1/index.html", "<div>Hello <b>world</b>. Some **bold** text. Some Unicode: 神真美好.\n</div>") - b.EditFileReplaceAll("content/p2.md", "Hello", "Hello Edited").Build() - b.AssertNoRenderShortcodesArtifacts() - b.AssertFileContent("public/p1/index.html", "<div>Hello Edited <b>world</b>. Some **bold** text. Some Unicode: 神真美好.\n</div>") -} - -func TestRenderShortcodesIncludeMarkdownFileWithoutTrailingNewline(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableLiveReload = true -disableKinds = ["home", "taxonomy", "term"] -markup.goldmark.renderer.unsafe = true --- content/p1.md -- ---- -title: "p1" ---- -Content p1 id-1000.{{% include "p2" %}}{{% include "p3" %}} - -§§§ go -code_p1 -§§§ -§§§ go -code_p1_2 -§§§ - -§§§ go -code_p1_3 -§§§ --- content/p2.md -- ---- -title: "p2" ---- -§§§ bash -code_p2 -§§§ -Foo. --- content/p3.md -- ---- -title: "p3" ---- -§§§ php -code_p3 -§§§ --- layouts/shortcodes/include.html -- -{{ with site.GetPage (.Get 0) -}} -{{ .RenderShortcodes -}} -{{ end -}} --- layouts/_default/single.html -- -{{ .Content }} --- layouts/_default/_markup/render-codeblock.html -- -<code>{{ .Inner | safeHTML }}</code> -` - - b := TestRunning(t, files, TestOptWarn()) - - b.AssertNoRenderShortcodesArtifacts() - b.AssertFileContentEquals("public/p1/index.html", "<p>Content p1 id-1000.</p>\n<code>code_p2</code><p>Foo.</p>\n<code>code_p3</code><code>code_p1</code><code>code_p1_2</code><code>code_p1_3</code>") - b.EditFileReplaceAll("content/p1.md", "id-1000.", "id-100.").Build() - b.AssertNoRenderShortcodesArtifacts() - b.AssertFileContentEquals("public/p1/index.html", "<p>Content p1 id-100.</p>\n<code>code_p2</code><p>Foo.</p>\n<code>code_p3</code><code>code_p1</code><code>code_p1_2</code><code>code_p1_3</code>") - b.EditFileReplaceAll("content/p2.md", "code_p2", "codep2").Build() - b.AssertNoRenderShortcodesArtifacts() - b.AssertFileContentEquals("public/p1/index.html", "<p>Content p1 id-100.</p>\n<code>codep2</code><p>Foo.</p>\n<code>code_p3</code><code>code_p1</code><code>code_p1_2</code><code>code_p1_3</code>") - b.EditFileReplaceAll("content/p3.md", "code_p3", "code_p3_edited").Build() - b.AssertNoRenderShortcodesArtifacts() - b.AssertFileContentEquals("public/p1/index.html", "<p>Content p1 id-100.</p>\n<code>codep2</code><p>Foo.</p>\n<code>code_p3_edited</code><code>code_p1</code><code>code_p1_2</code><code>code_p1_3</code>") -} - -// Issue 13004. -func TestRenderShortcodesIncludeShortRefEdit(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableLiveReload = true -disableKinds = ["home", "taxonomy", "term", "section", "rss", "sitemap", "robotsTXT", "404"] --- content/first/p1.md -- ---- -title: "p1" ---- -## p1-h1 -{{% include "p2" %}} --- content/second/p2.md -- ---- -title: "p2" ---- -### p2-h1 - -This is some **markup**. --- layouts/shortcodes/include.html -- -{{ $p := site.GetPage (.Get 0) -}} -{{ $p.RenderShortcodes -}} --- layouts/_default/single.html -- -{{ .Content }} -` - b := TestRunning(t, files) - b.AssertNoRenderShortcodesArtifacts() - b.AssertFileContentEquals("public/first/p1/index.html", "<h2 id=\"p1-h1\">p1-h1</h2>\n<h3 id=\"p2-h1\">p2-h1</h3>\n<p>This is some <strong>markup</strong>.</p>\n") - b.EditFileReplaceAll("content/second/p2.md", "p2-h1", "p2-h1-edited").Build() - b.AssertNoRenderShortcodesArtifacts() - b.AssertFileContentEquals("public/first/p1/index.html", "<h2 id=\"p1-h1\">p1-h1</h2>\n<h3 id=\"p2-h1-edited\">p2-h1-edited</h3>\n<p>This is some <strong>markup</strong>.</p>\n") -} - -// Issue 13051. -func TestRenderShortcodesEmptyParagraph(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ['section','rss','sitemap','taxonomy','term'] --- layouts/_default/home.html -- -{{ .Content }} --- layouts/_default/single.html -- -{{ .Content }} --- layouts/shortcodes/include.html -- - {{ with site.GetPage (.Get 0) }} - {{ .RenderShortcodes }} -{{ end }} --- content/_index.md -- ---- -title: home ---- - -a - -{{% include "/snippet" %}} - -b - --- content/snippet.md -- ---- -title: snippet -build: - render: never - list: never ---- - -_emphasized_ - -not emphasized - -` - - b := Test(t, files) - b.AssertNoRenderShortcodesArtifacts() - b.AssertFileContentEquals("public/index.html", - "<p>a</p>\n<p><em>emphasized</em></p>\n<p>not emphasized</p>\n<p>b</p>\n", - ) -} diff --git a/hugolib/renderstring_test.go b/hugolib/renderstring_test.go deleted file mode 100644 index 413943698..000000000 --- a/hugolib/renderstring_test.go +++ /dev/null @@ -1,214 +0,0 @@ -// 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 requiredF 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 hugolib - -import ( - "testing" - - "github.com/bep/logg" - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/common/loggers" -) - -func TestRenderString(t *testing.T) { - b := newTestSitesBuilder(t) - - b.WithTemplates("index.html", ` -{{ $p := site.GetPage "p1.md" }} -{{ $optBlock := dict "display" "block" }} -{{ $optOrg := dict "markup" "org" }} -RSTART:{{ "**Bold Markdown**" | $p.RenderString }}:REND -RSTART:{{ "**Bold Block Markdown**" | $p.RenderString $optBlock }}:REND -RSTART:{{ "/italic org mode/" | $p.RenderString $optOrg }}:REND -RSTART:{{ "## Header2" | $p.RenderString }}:REND - - -`, "_default/_markup/render-heading.html", "Hook Heading: {{ .Level }}") - - b.WithContent("p1.md", `--- -title: "p1" ---- -`, - ) - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", ` -RSTART:<strong>Bold Markdown</strong>:REND -RSTART:<p><strong>Bold Block Markdown</strong></p> -RSTART:<em>italic org mode</em>:REND -RSTART:Hook Heading: 2:REND -`) -} - -// https://github.com/gohugoio/hugo/issues/6882 -func TestRenderStringOnListPage(t *testing.T) { - renderStringTempl := ` -{{ .RenderString "**Hello**" }} -` - b := newTestSitesBuilder(t) - b.WithContent("mysection/p1.md", `FOO`) - b.WithTemplates( - "index.html", renderStringTempl, - "_default/list.html", renderStringTempl, - "_default/single.html", renderStringTempl, - ) - - b.Build(BuildCfg{}) - - for _, filename := range []string{ - "index.html", - "mysection/index.html", - "categories/index.html", - "tags/index.html", - "mysection/p1/index.html", - } { - b.AssertFileContent("public/"+filename, `<strong>Hello</strong>`) - } -} - -// Issue 9433 -func TestRenderStringOnPageNotBackedByAFile(t *testing.T) { - t.Parallel() - logger := loggers.NewDefault() - b := newTestSitesBuilder(t).WithLogger(logger).WithConfigFile("toml", ` -disableKinds = ["page", "section", "taxonomy", "term"] -`) - b.WithTemplates("index.html", `{{ .RenderString "**Hello**" }}`).WithContent("p1.md", "") - b.BuildE(BuildCfg{}) - b.Assert(logger.LoggCount(logg.LevelWarn), qt.Equals, 0) -} - -func TestRenderStringWithShortcode(t *testing.T) { - t.Parallel() - - filesTemplate := ` --- config.toml -- -title = "Hugo Rocks!" -enableInlineShortcodes = true --- content/p1/index.md -- ---- -title: "P1" ---- -## First --- layouts/shortcodes/mark1.md -- -{{ .Inner }} --- layouts/shortcodes/mark2.md -- -1. Item Mark2 1 -1. Item Mark2 2 - 1. Item Mark2 2-1 -1. Item Mark2 3 --- layouts/shortcodes/myhthml.html -- -Title: {{ .Page.Title }} -TableOfContents: {{ .Page.TableOfContents }} -Page Type: {{ printf "%T" .Page }} --- layouts/_default/single.html -- -{{ .RenderString "Markdown: {{% mark2 %}}|HTML: {{< myhthml >}}|Inline: {{< foo.inline >}}{{ site.Title }}{{< /foo.inline >}}|" }} -HasShortcode: mark2:{{ .HasShortcode "mark2" }}:true -HasShortcode: foo:{{ .HasShortcode "foo" }}:false - -` - - t.Run("Basic", func(t *testing.T) { - b := NewIntegrationTestBuilder( - IntegrationTestConfig{ - T: t, - TxtarString: filesTemplate, - }, - ).Build() - - b.AssertFileContent("public/p1/index.html", - "<p>Markdown: 1. Item Mark2 1</p>\n<ol>\n<li>Item Mark2 2\n<ol>\n<li>Item Mark2 2-1</li>\n</ol>\n</li>\n<li>Item Mark2 3|", - "<a href=\"#first\">First</a>", // ToC - ` -HTML: Title: P1 -Inline: Hugo Rocks! -HasShortcode: mark2:true:true -HasShortcode: foo:false:false -Page Type: *hugolib.pageForShortcode`, - ) - }) - - t.Run("Edit shortcode", func(t *testing.T) { - b := NewIntegrationTestBuilder( - IntegrationTestConfig{ - T: t, - TxtarString: filesTemplate, - Running: true, - }, - ).Build() - - b.EditFiles("layouts/shortcodes/myhthml.html", "Edit shortcode").Build() - - b.AssertFileContent("public/p1/index.html", - `Edit shortcode`, - ) - }) -} - -// Issue 9959 -func TestRenderStringWithShortcodeInPageWithNoContentFile(t *testing.T) { - t.Parallel() - - files := ` --- config.toml -- --- layouts/shortcodes/myshort.html -- -Page Kind: {{ .Page.Kind }} --- layouts/index.html -- -Short: {{ .RenderString "{{< myshort >}}" }} -Has myshort: {{ .HasShortcode "myshort" }} -Has other: {{ .HasShortcode "other" }} - - ` - - b := Test(t, files) - - b.AssertFileContent("public/index.html", - ` -Page Kind: home -Has myshort: true -Has other: false -`) -} - -func TestRenderStringWithShortcodeIssue10654(t *testing.T) { - t.Parallel() - - files := ` --- config.toml -- -timeout = '300ms' --- content/p1.md -- ---- -title: "P1" ---- -{{< toc >}} - -## Heading 1 - -{{< noop >}} - {{ not a shortcode -{{< /noop >}} -} --- layouts/shortcodes/noop.html -- -{{ .Inner | $.Page.RenderString }} --- layouts/shortcodes/toc.html -- -{{ .Page.TableOfContents }} --- layouts/_default/single.html -- -{{ .Content }} -` - - b := Test(t, files) - - b.AssertFileContent("public/p1/index.html", `TableOfContents`) -} diff --git a/hugolib/resource_chain_test.go b/hugolib/resource_chain_test.go deleted file mode 100644 index 00e4c0060..000000000 --- a/hugolib/resource_chain_test.go +++ /dev/null @@ -1,745 +0,0 @@ -// Copyright 2019 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 hugolib - -import ( - "fmt" - "io" - "math/rand" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "strings" - "testing" - "time" - - qt "github.com/frankban/quicktest" - - "github.com/gohugoio/hugo/common/hashing" - "github.com/gohugoio/hugo/common/loggers" - "github.com/gohugoio/hugo/resources/resource_transformers/tocss/scss" -) - -func TestResourceChainBasic(t *testing.T) { - failIfHandler := func(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/fail.jpg" { - http.Error(w, "{ msg: failed }", http.StatusNotImplemented) - return - } - h.ServeHTTP(w, r) - }) - } - ts := httptest.NewServer( - failIfHandler(http.FileServer(http.Dir("testdata/"))), - ) - t.Cleanup(func() { - ts.Close() - }) - - b := newTestSitesBuilder(t) - b.WithTemplatesAdded("index.html", fmt.Sprintf(` -{{ $hello := "<h1> Hello World! </h1>" | resources.FromString "hello.html" | fingerprint "sha512" | minify | fingerprint }} -{{ $cssFingerprinted1 := "body { background-color: lightblue; }" | resources.FromString "styles.css" | minify | fingerprint }} -{{ $cssFingerprinted2 := "body { background-color: orange; }" | resources.FromString "styles2.css" | minify | fingerprint }} - - -HELLO: {{ $hello.Name }}|{{ $hello.RelPermalink }}|{{ $hello.Content | safeHTML }} - -{{ $img := resources.Get "images/sunset.jpg" }} -{{ $fit := $img.Fit "200x200" }} -{{ $fit2 := $fit.Fit "100x200" }} -{{ $img = $img | fingerprint }} -SUNSET: {{ $img.Name }}|{{ $img.RelPermalink }}|{{ $img.Width }}|{{ len $img.Content }} -FIT: {{ $fit.Name }}|{{ $fit.RelPermalink }}|{{ $fit.Width }} -CSS integrity Data first: {{ $cssFingerprinted1.Data.Integrity }} {{ $cssFingerprinted1.RelPermalink }} -CSS integrity Data last: {{ $cssFingerprinted2.RelPermalink }} {{ $cssFingerprinted2.Data.Integrity }} - -{{ $failedImg := try (resources.GetRemote "%[1]s/fail.jpg") }} -{{ $rimg := resources.GetRemote "%[1]s/sunset.jpg" }} -{{ $remotenotfound := resources.GetRemote "%[1]s/notfound.jpg" }} -{{ $localnotfound := resources.Get "images/notfound.jpg" }} -{{ $gopherprotocol := try (resources.GetRemote "gopher://example.org") }} -{{ $rfit := $rimg.Fit "200x200" }} -{{ $rfit2 := $rfit.Fit "100x200" }} -{{ $rimg = $rimg | fingerprint }} -SUNSET REMOTE: {{ $rimg.Name }}|{{ $rimg.RelPermalink }}|{{ $rimg.Width }}|{{ len $rimg.Content }} -FIT REMOTE: {{ $rfit.Name }}|{{ $rfit.RelPermalink }}|{{ $rfit.Width }} -REMOTE NOT FOUND: {{ if $remotenotfound }}FAILED{{ else}}OK{{ end }} -LOCAL NOT FOUND: {{ if $localnotfound }}FAILED{{ else}}OK{{ end }} -PRINT PROTOCOL ERROR1: {{ with $gopherprotocol }}{{ .Value | safeHTML }}{{ end }} -PRINT PROTOCOL ERROR2: {{ with $gopherprotocol }}{{ .Err | safeHTML }}{{ end }} -PRINT PROTOCOL ERROR DETAILS: {{ with $gopherprotocol }}{{ with .Err }}Err: {{ . | safeHTML }}{{ with .Cause }}|{{ with .Data }}Body: {{ .Body }}|StatusCode: {{ .StatusCode }}{{ end }}|{{ end }}{{ end }}{{ end }} -FAILED REMOTE ERROR DETAILS CONTENT: {{ with $failedImg }}{{ with .Err }}{{ with .Cause }}{{ . }}|{{ with .Data }}Body: {{ .Body }}|StatusCode: {{ .StatusCode }}|ContentLength: {{ .ContentLength }}|ContentType: {{ .ContentType }}{{ end }}{{ end }}{{ end }}{{ end }}| -`, ts.URL)) - - fs := b.Fs.Source - - imageDir := filepath.Join("assets", "images") - b.Assert(os.MkdirAll(imageDir, 0o777), qt.IsNil) - src, err := os.Open("testdata/sunset.jpg") - b.Assert(err, qt.IsNil) - out, err := fs.Create(filepath.Join(imageDir, "sunset.jpg")) - b.Assert(err, qt.IsNil) - _, err = io.Copy(out, src) - b.Assert(err, qt.IsNil) - out.Close() - - b.Running() - - for i := range 2 { - b.Logf("Test run %d", i) - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", - fmt.Sprintf(` -SUNSET: /images/sunset.jpg|/images/sunset.a9bf1d944e19c0f382e0d8f51de690f7d0bc8fa97390c4242a86c3e5c0737e71.jpg|900|90587 -FIT: /images/sunset.jpg|/images/sunset_hu_f2aae87288f3c13b.jpg|200 -CSS integrity Data first: sha256-od9YaHw8nMOL8mUy97Sy8sKwMV3N4hI3aVmZXATxH+8= /styles.min.a1df58687c3c9cc38bf26532f7b4b2f2c2b0315dcde212376959995c04f11fef.css -CSS integrity Data last: /styles2.min.1cfc52986836405d37f9998a63fd6dd8608e8c410e5e3db1daaa30f78bc273ba.css sha256-HPxSmGg2QF03+ZmKY/1t2GCOjEEOXj2x2qow94vCc7o= - -SUNSET REMOTE: /sunset_%[1]s.jpg|/sunset_%[1]s.a9bf1d944e19c0f382e0d8f51de690f7d0bc8fa97390c4242a86c3e5c0737e71.jpg|900|90587 -FIT REMOTE: /sunset_%[1]s.jpg|/sunset_%[1]s_hu_f2aae87288f3c13b.jpg|200 -REMOTE NOT FOUND: OK -LOCAL NOT FOUND: OK -PRINT PROTOCOL ERROR DETAILS: Err: template: index.html:22:36: executing "index.html" at <resources.GetRemote>: error calling GetRemote: Get "gopher://example.org": unsupported protocol scheme "gopher"| -FAILED REMOTE ERROR DETAILS CONTENT: failed to fetch remote resource from '%[2]s/fail.jpg': Not Implemented|Body: { msg: failed } -|StatusCode: 501|ContentLength: 16|ContentType: text/plain; charset=utf-8| - - -`, hashing.HashString(ts.URL+"/sunset.jpg", map[string]any{}), ts.URL)) - - b.AssertFileContent("public/styles.min.a1df58687c3c9cc38bf26532f7b4b2f2c2b0315dcde212376959995c04f11fef.css", "body{background-color:#add8e6}") - b.AssertFileContent("public//styles2.min.1cfc52986836405d37f9998a63fd6dd8608e8c410e5e3db1daaa30f78bc273ba.css", "body{background-color:orange}") - - b.EditFiles("content/_index.md", ` ---- -title: "Home edit" -summary: "Edited summary" ---- - -Edited content. - -`) - - } -} - -func TestResourceChainPostProcess(t *testing.T) { - t.Parallel() - - rnd := rand.New(rand.NewSource(time.Now().UnixNano())) - - b := newTestSitesBuilder(t) - b.WithConfigFile("toml", ` -disableLiveReload = true -[minify] - minifyOutput = true - [minify.tdewolff] - [minify.tdewolff.html] - keepQuotes = false - keepWhitespace = false`) - b.WithContent("page1.md", "---\ntitle: Page1\n---") - b.WithContent("page2.md", "---\ntitle: Page2\n---") - - b.WithTemplates( - "_default/single.html", `{{ $hello := "<h1> Hello World! </h1>" | resources.FromString "hello.html" | minify | fingerprint "md5" | resources.PostProcess }} -HELLO: {{ $hello.RelPermalink }} -`, - "index.html", `Start. -{{ $hello := "<h1> Hello World! </h1>" | resources.FromString "hello.html" | minify | fingerprint "md5" | resources.PostProcess }} - -HELLO: {{ $hello.RelPermalink }}|Integrity: {{ $hello.Data.Integrity }}|MediaType: {{ $hello.MediaType.Type }} -HELLO2: Name: {{ $hello.Name }}|Content: {{ $hello.Content }}|Title: {{ $hello.Title }}|ResourceType: {{ $hello.ResourceType }} - -// Issue #10269 -{{ $m := dict "relPermalink" $hello.RelPermalink "integrity" $hello.Data.Integrity "mediaType" $hello.MediaType.Type }} -{{ $json := jsonify (dict "indent" " ") $m | resources.FromString "hello.json" -}} -JSON: {{ $json.RelPermalink }} - -// Issue #8884 -<a href="hugo.rocks">foo</a> -<a href="{{ $hello.RelPermalink }}" integrity="{{ $hello.Data.Integrity}}">Hello</a> -`+strings.Repeat("a b", rnd.Intn(10)+1)+` - - -End.`) - - b.Running() - b.Build(BuildCfg{}) - b.AssertFileContent("public/index.html", - `Start. -HELLO: /hello.min.a2d1cb24f24b322a7dad520414c523e9.html|Integrity: md5-otHLJPJLMip9rVIEFMUj6Q==|MediaType: text/html -HELLO2: Name: /hello.html|Content: <h1>Hello World!</h1>|Title: /hello.html|ResourceType: text -<a href=hugo.rocks>foo</a> -<a href="/hello.min.a2d1cb24f24b322a7dad520414c523e9.html" integrity="md5-otHLJPJLMip9rVIEFMUj6Q==">Hello</a> -End.`) - - b.AssertFileContent("public/page1/index.html", `HELLO: /hello.min.a2d1cb24f24b322a7dad520414c523e9.html`) - b.AssertFileContent("public/page2/index.html", `HELLO: /hello.min.a2d1cb24f24b322a7dad520414c523e9.html`) - b.AssertFileContent("public/hello.json", ` -integrity": "md5-otHLJPJLMip9rVIEFMUj6Q== -mediaType": "text/html -relPermalink": "/hello.min.a2d1cb24f24b322a7dad520414c523e9.html" -`) -} - -func BenchmarkResourceChainPostProcess(b *testing.B) { - for i := 0; i < b.N; i++ { - b.StopTimer() - s := newTestSitesBuilder(b) - for i := range 300 { - s.WithContent(fmt.Sprintf("page%d.md", i+1), "---\ntitle: Page\n---") - } - s.WithTemplates("_default/single.html", `Start. -Some text. - - -{{ $hello1 := "<h1> Hello World 2! </h1>" | resources.FromString "hello.html" | minify | fingerprint "md5" | resources.PostProcess }} -{{ $hello2 := "<h1> Hello World 2! </h1>" | resources.FromString (printf "%s.html" .Path) | minify | fingerprint "md5" | resources.PostProcess }} - -Some more text. - -HELLO: {{ $hello1.RelPermalink }}|Integrity: {{ $hello1.Data.Integrity }}|MediaType: {{ $hello1.MediaType.Type }} - -Some more text. - -HELLO2: Name: {{ $hello2.Name }}|Content: {{ $hello2.Content }}|Title: {{ $hello2.Title }}|ResourceType: {{ $hello2.ResourceType }} - -Some more text. - -HELLO2_2: Name: {{ $hello2.Name }}|Content: {{ $hello2.Content }}|Title: {{ $hello2.Title }}|ResourceType: {{ $hello2.ResourceType }} - -End. -`) - - b.StartTimer() - s.Build(BuildCfg{}) - - } -} - -func TestResourceChains(t *testing.T) { - t.Parallel() - - c := qt.New(t) - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case "/css/styles1.css": - w.Header().Set("Content-Type", "text/css") - w.Write([]byte(`h1 { - font-style: bold; - }`)) - return - - case "/js/script1.js": - w.Write([]byte(`var x; x = 5, document.getElementById("demo").innerHTML = x * 10`)) - return - - case "/mydata/json1.json": - w.Write([]byte(`{ - "employees": [ - { - "firstName": "John", - "lastName": "Doe" - }, - { - "firstName": "Anna", - "lastName": "Smith" - }, - { - "firstName": "Peter", - "lastName": "Jones" - } - ] - }`)) - return - - case "/mydata/xml1.xml": - w.Write([]byte(` - <hello> - <world>Hugo Rocks!</<world> - </hello>`)) - return - - case "/mydata/svg1.svg": - w.Header().Set("Content-Disposition", `attachment; filename="image.svg"`) - w.Write([]byte(` - <svg height="100" width="100"> - <path d="M1e2 1e2H3e2 2e2z"/> - </svg>`)) - return - - case "/mydata/html1.html": - w.Write([]byte(` - <html> - <a href=#>Cool</a> - </html>`)) - return - - case "/authenticated/": - w.Header().Set("Content-Type", "text/plain") - if r.Header.Get("Authorization") == "Bearer abcd" { - w.Write([]byte(`Welcome`)) - return - } - http.Error(w, "Forbidden", http.StatusForbidden) - return - - case "/post": - w.Header().Set("Content-Type", "text/plain") - if r.Method == http.MethodPost { - body, err := io.ReadAll(r.Body) - if err != nil { - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - w.Write(body) - return - } - http.Error(w, "Bad request", http.StatusBadRequest) - return - } - - http.Error(w, "Not found", http.StatusNotFound) - })) - t.Cleanup(func() { - ts.Close() - }) - - tests := []struct { - name string - shouldRun func() bool - prepare func(b *sitesBuilder) - verify func(b *sitesBuilder) - }{ - {"tocss", func() bool { return scss.Supports() }, func(b *sitesBuilder) { - b.WithTemplates("home.html", ` -{{ $scss := resources.Get "scss/styles2.scss" | toCSS }} -{{ $sass := resources.Get "sass/styles3.sass" | toCSS }} -{{ $scssCustomTarget := resources.Get "scss/styles2.scss" | toCSS (dict "targetPath" "styles/main.css") }} -{{ $scssCustomTargetString := resources.Get "scss/styles2.scss" | toCSS "styles/main.css" }} -{{ $scssMin := resources.Get "scss/styles2.scss" | toCSS | minify }} -{{ $scssFromTempl := ".{{ .Kind }} { color: blue; }" | resources.FromString "kindofblue.templ" | resources.ExecuteAsTemplate "kindofblue.scss" . | toCSS (dict "targetPath" "styles/templ.css") | minify }} -{{ $bundle1 := slice $scssFromTempl $scssMin | resources.Concat "styles/bundle1.css" }} -T1: Len Content: {{ len $scss.Content }}|RelPermalink: {{ $scss.RelPermalink }}|Permalink: {{ $scss.Permalink }}|MediaType: {{ $scss.MediaType.Type }} -T2: Content: {{ $scssMin.Content }}|RelPermalink: {{ $scssMin.RelPermalink }} -T3: Content: {{ len $scssCustomTarget.Content }}|RelPermalink: {{ $scssCustomTarget.RelPermalink }}|MediaType: {{ $scssCustomTarget.MediaType.Type }} -T4: Content: {{ len $scssCustomTargetString.Content }}|RelPermalink: {{ $scssCustomTargetString.RelPermalink }}|MediaType: {{ $scssCustomTargetString.MediaType.Type }} -T5: Content: {{ $sass.Content }}|T5 RelPermalink: {{ $sass.RelPermalink }}| -T6: {{ $bundle1.Permalink }} -`) - }, func(b *sitesBuilder) { - b.AssertFileContent("public/index.html", `T1: Len Content: 24|RelPermalink: /scss/styles2.css|Permalink: http://example.com/scss/styles2.css|MediaType: text/css`) - b.AssertFileContent("public/index.html", `T2: Content: body{color:#333}|RelPermalink: /scss/styles2.min.css`) - b.AssertFileContent("public/index.html", `T3: Content: 24|RelPermalink: /styles/main.css|MediaType: text/css`) - b.AssertFileContent("public/index.html", `T4: Content: 24|RelPermalink: /styles/main.css|MediaType: text/css`) - b.AssertFileContent("public/index.html", `T5: Content: .content-navigation {`) - b.AssertFileContent("public/index.html", `T5 RelPermalink: /sass/styles3.css|`) - b.AssertFileContent("public/index.html", `T6: http://example.com/styles/bundle1.css`) - - c.Assert(b.CheckExists("public/styles/templ.min.css"), qt.Equals, false) - b.AssertFileContent("public/styles/bundle1.css", `.home{color:blue}body{color:#333}`) - }}, - - {"minify", func() bool { return true }, func(b *sitesBuilder) { - b.WithConfigFile("toml", `[minify] - [minify.tdewolff] - [minify.tdewolff.html] - keepWhitespace = false -`) - b.WithTemplates("home.html", fmt.Sprintf(` -Min CSS: {{ ( resources.Get "css/styles1.css" | minify ).Content }} -Min CSS Remote: {{ ( resources.GetRemote "%[1]s/css/styles1.css" | minify ).Content }} -Min JS: {{ ( resources.Get "js/script1.js" | resources.Minify ).Content | safeJS }} -Min JS Remote: {{ ( resources.GetRemote "%[1]s/js/script1.js" | minify ).Content }} -Min JSON: {{ ( resources.Get "mydata/json1.json" | resources.Minify ).Content | safeHTML }} -Min JSON Remote: {{ ( resources.GetRemote "%[1]s/mydata/json1.json" | resources.Minify ).Content | safeHTML }} -Min XML: {{ ( resources.Get "mydata/xml1.xml" | resources.Minify ).Content | safeHTML }} -Min XML Remote: {{ ( resources.GetRemote "%[1]s/mydata/xml1.xml" | resources.Minify ).Content | safeHTML }} -Min SVG: {{ ( resources.Get "mydata/svg1.svg" | resources.Minify ).Content | safeHTML }} -Min SVG Remote: {{ ( resources.GetRemote "%[1]s/mydata/svg1.svg" | resources.Minify ).Content | safeHTML }} -Min SVG again: {{ ( resources.Get "mydata/svg1.svg" | resources.Minify ).Content | safeHTML }} -Min HTML: {{ ( resources.Get "mydata/html1.html" | resources.Minify ).Content | safeHTML }} -Min HTML Remote: {{ ( resources.GetRemote "%[1]s/mydata/html1.html" | resources.Minify ).Content | safeHTML }} -`, ts.URL)) - }, func(b *sitesBuilder) { - b.AssertFileContent("public/index.html", `Min CSS: h1{font-style:bold}`) - b.AssertFileContent("public/index.html", `Min CSS Remote: h1{font-style:bold}`) - b.AssertFileContent("public/index.html", `Min JS: var x=5;document.getElementById("demo").innerHTML=x*10`) - b.AssertFileContent("public/index.html", `Min JS Remote: var x=5;document.getElementById("demo").innerHTML=x*10`) - b.AssertFileContent("public/index.html", `Min JSON: {"employees":[{"firstName":"John","lastName":"Doe"},{"firstName":"Anna","lastName":"Smith"},{"firstName":"Peter","lastName":"Jones"}]}`) - b.AssertFileContent("public/index.html", `Min JSON Remote: {"employees":[{"firstName":"John","lastName":"Doe"},{"firstName":"Anna","lastName":"Smith"},{"firstName":"Peter","lastName":"Jones"}]}`) - b.AssertFileContent("public/index.html", `Min XML: <hello><world>Hugo Rocks!</<world></hello>`) - b.AssertFileContent("public/index.html", `Min XML Remote: <hello><world>Hugo Rocks!</<world></hello>`) - b.AssertFileContent("public/index.html", `Min SVG: <svg height="100" width="100"><path d="M1e2 1e2H3e2 2e2z"/></svg>`) - b.AssertFileContent("public/index.html", `Min SVG Remote: <svg height="100" width="100"><path d="M1e2 1e2H3e2 2e2z"/></svg>`) - b.AssertFileContent("public/index.html", `Min SVG again: <svg height="100" width="100"><path d="M1e2 1e2H3e2 2e2z"/></svg>`) - b.AssertFileContent("public/index.html", `Min HTML: <html><a href=#>Cool</a></html>`) - b.AssertFileContent("public/index.html", `Min HTML Remote: <html><a href=#>Cool</a></html>`) - }}, - - {"remote", func() bool { return true }, func(b *sitesBuilder) { - b.WithTemplates("home.html", fmt.Sprintf(` -{{$js := resources.GetRemote "%[1]s/js/script1.js" }} -Remote Filename: {{ $js.RelPermalink }} -{{$svg := resources.GetRemote "%[1]s/mydata/svg1.svg" }} -Remote Content-Disposition: {{ $svg.RelPermalink }} -{{$auth := resources.GetRemote "%[1]s/authenticated/" (dict "headers" (dict "Authorization" "Bearer abcd")) }} -Remote Authorization: {{ $auth.Content }} -{{$post := resources.GetRemote "%[1]s/post" (dict "method" "post" "body" "Request body") }} -Remote POST: {{ $post.Content }} -`, ts.URL)) - }, func(b *sitesBuilder) { - b.AssertFileContent("public/index.html", `Remote Filename: /script1_`) - b.AssertFileContent("public/index.html", `Remote Content-Disposition: /image_`) - b.AssertFileContent("public/index.html", `Remote Authorization: Welcome`) - b.AssertFileContent("public/index.html", `Remote POST: Request body`) - }}, - - {"concat", func() bool { return true }, func(b *sitesBuilder) { - b.WithTemplates("home.html", ` -{{ $a := "A" | resources.FromString "a.txt"}} -{{ $b := "B" | resources.FromString "b.txt"}} -{{ $c := "C" | resources.FromString "c.txt"}} -{{ $textResources := .Resources.Match "*.txt" }} -{{ $combined := slice $a $b $c | resources.Concat "bundle/concat.txt" }} -T1: Content: {{ $combined.Content }}|RelPermalink: {{ $combined.RelPermalink }}|Permalink: {{ $combined.Permalink }}|MediaType: {{ $combined.MediaType.Type }} -{{ with $textResources }} -{{ $combinedText := . | resources.Concat "bundle/concattxt.txt" }} -T2: Content: {{ $combinedText.Content }}|{{ $combinedText.RelPermalink }} -{{ end }} -{{/* https://github.com/gohugoio/hugo/issues/5269 */}} -{{ $css := "body { color: blue; }" | resources.FromString "styles.css" }} -{{ $minified := resources.Get "css/styles1.css" | minify }} -{{ slice $css $minified | resources.Concat "bundle/mixed.css" }} -{{/* https://github.com/gohugoio/hugo/issues/5403 */}} -{{ $d := "function D {} // A comment" | resources.FromString "d.js"}} -{{ $e := "(function E {})" | resources.FromString "e.js"}} -{{ $f := "(function F {})()" | resources.FromString "f.js"}} -{{ $jsResources := .Resources.Match "*.js" }} -{{ $combinedJs := slice $d $e $f | resources.Concat "bundle/concatjs.js" }} -T3: Content: {{ $combinedJs.Content }}|{{ $combinedJs.RelPermalink }} -`) - }, func(b *sitesBuilder) { - b.AssertFileContent("public/index.html", `T1: Content: ABC|RelPermalink: /bundle/concat.txt|Permalink: http://example.com/bundle/concat.txt|MediaType: text/plain`) - b.AssertFileContent("public/bundle/concat.txt", "ABC") - - b.AssertFileContent("public/index.html", `T2: Content: t1t|t2t|`) - b.AssertFileContent("public/bundle/concattxt.txt", "t1t|t2t|") - - b.AssertFileContent("public/index.html", `T3: Content: function D {} // A comment -; -(function E {}) -; -(function F {})()|`) - b.AssertFileContent("public/bundle/concatjs.js", `function D {} // A comment -; -(function E {}) -; -(function F {})()`) - }}, - - {"concat and fingerprint", func() bool { return true }, func(b *sitesBuilder) { - b.WithTemplates("home.html", ` -{{ $a := "A" | resources.FromString "a.txt"}} -{{ $b := "B" | resources.FromString "b.txt"}} -{{ $c := "C" | resources.FromString "c.txt"}} -{{ $combined := slice $a $b $c | resources.Concat "bundle/concat.txt" }} -{{ $fingerprinted := $combined | fingerprint }} -Fingerprinted: {{ $fingerprinted.RelPermalink }} -`) - }, func(b *sitesBuilder) { - b.AssertFileContent("public/index.html", "Fingerprinted: /bundle/concat.b5d4045c3f466fa91fe2cc6abe79232a1a57cdf104f7a26e716e0a1e2789df78.txt") - b.AssertFileContent("public/bundle/concat.b5d4045c3f466fa91fe2cc6abe79232a1a57cdf104f7a26e716e0a1e2789df78.txt", "ABC") - }}, - - {"fromstring", func() bool { return true }, func(b *sitesBuilder) { - b.WithTemplates("home.html", ` -{{ $r := "Hugo Rocks!" | resources.FromString "rocks/hugo.txt" }} -{{ $r.Content }}|{{ $r.RelPermalink }}|{{ $r.Permalink }}|{{ $r.MediaType.Type }} -`) - }, func(b *sitesBuilder) { - b.AssertFileContent("public/index.html", `Hugo Rocks!|/rocks/hugo.txt|http://example.com/rocks/hugo.txt|text/plain`) - b.AssertFileContent("public/rocks/hugo.txt", "Hugo Rocks!") - }}, - {"execute-as-template", func() bool { - return true - }, func(b *sitesBuilder) { - b.WithTemplates("home.html", ` -{{ $var := "Hugo Page" }} -{{ if .IsHome }} -{{ $var = "Hugo Home" }} -{{ end }} -T1: {{ $var }} -{{ $result := "{{ .Kind | upper }}" | resources.FromString "mytpl.txt" | resources.ExecuteAsTemplate "result.txt" . }} -T2: {{ $result.Content }}|{{ $result.RelPermalink}}|{{$result.MediaType.Type }} -`) - }, func(b *sitesBuilder) { - b.AssertFileContent("public/index.html", `T2: HOME|/result.txt|text/plain`, `T1: Hugo Home`) - }}, - {"fingerprint", func() bool { return true }, func(b *sitesBuilder) { - b.WithTemplates("home.html", ` -{{ $r := "ab" | resources.FromString "rocks/hugo.txt" }} -{{ $result := $r | fingerprint }} -{{ $result512 := $r | fingerprint "sha512" }} -{{ $resultMD5 := $r | fingerprint "md5" }} -T1: {{ $result.Content }}|{{ $result.RelPermalink}}|{{$result.MediaType.Type }}|{{ $result.Data.Integrity }}| -T2: {{ $result512.Content }}|{{ $result512.RelPermalink}}|{{$result512.MediaType.Type }}|{{ $result512.Data.Integrity }}| -T3: {{ $resultMD5.Content }}|{{ $resultMD5.RelPermalink}}|{{$resultMD5.MediaType.Type }}|{{ $resultMD5.Data.Integrity }}| -{{ $r2 := "bc" | resources.FromString "rocks/hugo2.txt" | fingerprint }} -{{/* https://github.com/gohugoio/hugo/issues/5296 */}} -T4: {{ $r2.Data.Integrity }}| - - -`) - }, func(b *sitesBuilder) { - b.AssertFileContent("public/index.html", `T1: ab|/rocks/hugo.fb8e20fc2e4c3f248c60c39bd652f3c1347298bb977b8b4d5903b85055620603.txt|text/plain|sha256-+44g/C5MPySMYMOb1lLzwTRymLuXe4tNWQO4UFViBgM=|`) - b.AssertFileContent("public/index.html", `T2: ab|/rocks/hugo.2d408a0717ec188158278a796c689044361dc6fdde28d6f04973b80896e1823975cdbf12eb63f9e0591328ee235d80e9b5bf1aa6a44f4617ff3caf6400eb172d.txt|text/plain|sha512-LUCKBxfsGIFYJ4p5bGiQRDYdxv3eKNbwSXO4CJbhgjl1zb8S62P54FkTKO4jXYDptb8apqRPRhf/PK9kAOsXLQ==|`) - b.AssertFileContent("public/index.html", `T3: ab|/rocks/hugo.187ef4436122d1cc2f40dc2b92f0eba0.txt|text/plain|md5-GH70Q2Ei0cwvQNwrkvDroA==|`) - b.AssertFileContent("public/index.html", `T4: sha256-Hgu9bGhroFC46wP/7txk/cnYCUf86CGrvl1tyNJSxaw=|`) - }}, - // https://github.com/gohugoio/hugo/issues/5226 - {"baseurl-path", func() bool { return true }, func(b *sitesBuilder) { - b.WithSimpleConfigFileAndBaseURL("https://example.com/hugo/") - b.WithTemplates("home.html", ` -{{ $r1 := "ab" | resources.FromString "rocks/hugo.txt" }} -T1: {{ $r1.Permalink }}|{{ $r1.RelPermalink }} -`) - }, func(b *sitesBuilder) { - b.AssertFileContent("public/index.html", `T1: https://example.com/hugo/rocks/hugo.txt|/hugo/rocks/hugo.txt`) - }}, - - // https://github.com/gohugoio/hugo/issues/4944 - {"Prevent resource publish on .Content only", func() bool { return true }, func(b *sitesBuilder) { - b.WithTemplates("home.html", ` -{{ $cssInline := "body { color: green; }" | resources.FromString "inline.css" | minify }} -{{ $cssPublish1 := "body { color: blue; }" | resources.FromString "external1.css" | minify }} -{{ $cssPublish2 := "body { color: orange; }" | resources.FromString "external2.css" | minify }} - -Inline: {{ $cssInline.Content }} -Publish 1: {{ $cssPublish1.Content }} {{ $cssPublish1.RelPermalink }} -Publish 2: {{ $cssPublish2.Permalink }} -`) - }, func(b *sitesBuilder) { - b.AssertFileContent("public/index.html", - `Inline: body{color:green}`, - "Publish 1: body{color:blue} /external1.min.css", - "Publish 2: http://example.com/external2.min.css", - ) - b.Assert(b.CheckExists("public/external2.css"), qt.Equals, false) - b.Assert(b.CheckExists("public/external1.css"), qt.Equals, false) - b.Assert(b.CheckExists("public/external2.min.css"), qt.Equals, true) - b.Assert(b.CheckExists("public/external1.min.css"), qt.Equals, true) - b.Assert(b.CheckExists("public/inline.min.css"), qt.Equals, false) - }}, - - {"unmarshal", func() bool { return true }, func(b *sitesBuilder) { - b.WithTemplates("home.html", ` -{{ $toml := "slogan = \"Hugo Rocks!\"" | resources.FromString "slogan.toml" | transform.Unmarshal }} -{{ $csv1 := "\"Hugo Rocks\",\"Hugo is Fast!\"" | resources.FromString "slogans.csv" | transform.Unmarshal }} -{{ $csv2 := "a;b;c" | transform.Unmarshal (dict "delimiter" ";") }} -{{ $xml := "<?xml version=\"1.0\" encoding=\"UTF-8\"?><note><to>You</to><from>Me</from><heading>Reminder</heading><body>Do not forget XML</body></note>" | transform.Unmarshal }} - -Slogan: {{ $toml.slogan }} -CSV1: {{ $csv1 }} {{ len (index $csv1 0) }} -CSV2: {{ $csv2 }} -XML: {{ $xml.body }} -`) - }, func(b *sitesBuilder) { - b.AssertFileContent("public/index.html", - `Slogan: Hugo Rocks!`, - `[[Hugo Rocks Hugo is Fast!]] 2`, - `CSV2: [[a b c]]`, - `XML: Do not forget XML`, - ) - }}, - {"resources.Get", func() bool { return true }, func(b *sitesBuilder) { - b.WithTemplates("home.html", `NOT FOUND: {{ if (resources.Get "this-does-not-exist") }}FAILED{{ else }}OK{{ end }}`) - }, func(b *sitesBuilder) { - b.AssertFileContent("public/index.html", "NOT FOUND: OK") - }}, - - {"template", func() bool { return true }, func(b *sitesBuilder) {}, func(b *sitesBuilder) { - }}, - } - - for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { - if !test.shouldRun() { - t.Skip() - } - t.Parallel() - - b := newTestSitesBuilder(t).WithLogger(loggers.NewDefault()) - b.WithContent("_index.md", ` ---- -title: Home ---- - -Home. - -`, - "page1.md", ` ---- -title: Hello1 ---- - -Hello1 -`, - "page2.md", ` ---- -title: Hello2 ---- - -Hello2 -`, - "t1.txt", "t1t|", - "t2.txt", "t2t|", - ) - - b.WithSourceFile(filepath.Join("assets", "css", "styles1.css"), ` -h1 { - font-style: bold; -} -`) - - b.WithSourceFile(filepath.Join("assets", "js", "script1.js"), ` -var x; -x = 5; -document.getElementById("demo").innerHTML = x * 10; -`) - - b.WithSourceFile(filepath.Join("assets", "mydata", "json1.json"), ` -{ -"employees":[ - {"firstName":"John", "lastName":"Doe"}, - {"firstName":"Anna", "lastName":"Smith"}, - {"firstName":"Peter", "lastName":"Jones"} -] -} -`) - - b.WithSourceFile(filepath.Join("assets", "mydata", "svg1.svg"), ` -<svg height="100" width="100"> - <path d="M 100 100 L 300 100 L 200 100 z"/> -</svg> -`) - - b.WithSourceFile(filepath.Join("assets", "mydata", "xml1.xml"), ` -<hello> -<world>Hugo Rocks!</<world> -</hello> -`) - - b.WithSourceFile(filepath.Join("assets", "mydata", "html1.html"), ` -<html> -<a href="#"> -Cool -</a > -</html> -`) - - b.WithSourceFile(filepath.Join("assets", "scss", "styles2.scss"), ` -$color: #333; - -body { - color: $color; -} -`) - - b.WithSourceFile(filepath.Join("assets", "sass", "styles3.sass"), ` -$color: #333; - -.content-navigation - border-color: $color - -`) - - test.prepare(b) - b.Build(BuildCfg{}) - test.verify(b) - }) - } -} - -func TestResourcesMatch(t *testing.T) { - t.Parallel() - - b := newTestSitesBuilder(t) - - b.WithContent("page.md", "") - - b.WithSourceFile( - "assets/images/img1.png", "png", - "assets/images/img2.jpg", "jpg", - "assets/jsons/data1.json", "json1 content", - "assets/jsons/data2.json", "json2 content", - "assets/jsons/data3.xml", "xml content", - ) - - b.WithTemplates("index.html", ` -{{ $jsons := (resources.Match "jsons/*.json") }} -{{ $json := (resources.GetMatch "jsons/*.json") }} -{{ printf "jsonsMatch: %d" (len $jsons) }} -{{ printf "imagesByType: %d" (len (resources.ByType "image") ) }} -{{ printf "applicationByType: %d" (len (resources.ByType "application") ) }} -JSON: {{ $json.RelPermalink }}: {{ $json.Content }} -{{ range $jsons }} -{{- .RelPermalink }}: {{ .Content }} -{{ end }} -`) - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", - "JSON: /jsons/data1.json: json1 content", - "jsonsMatch: 2", - "imagesByType: 2", - "applicationByType: 3", - "/jsons/data1.json: json1 content") -} - -func TestResourceMinifyDisabled(t *testing.T) { - t.Parallel() - - b := newTestSitesBuilder(t).WithConfigFile("toml", ` -baseURL = "https://example.org" - -[minify] -disableXML=true - - -`) - - b.WithContent("page.md", "") - - b.WithSourceFile( - "assets/xml/data.xml", "<root> <foo> asdfasdf </foo> </root>", - ) - - b.WithTemplates("index.html", ` -{{ $xml := resources.Get "xml/data.xml" | minify | fingerprint }} -XML: {{ $xml.Content | safeHTML }}|{{ $xml.RelPermalink }} -`) - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", ` -XML: <root> <foo> asdfasdf </foo> </root>|/xml/data.min.3be4fddd19aaebb18c48dd6645215b822df74701957d6d36e59f203f9c30fd9f.xml -`) -} diff --git a/hugolib/robotstxt_test.go b/hugolib/robotstxt_test.go index c901ce662..03332cbce 100644 --- a/hugolib/robotstxt_test.go +++ b/hugolib/robotstxt_test.go @@ -14,9 +14,10 @@ package hugolib import ( + "path/filepath" "testing" - "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/deps" ) const robotTxtTemplate = `User-agent: Googlebot @@ -27,28 +28,19 @@ const robotTxtTemplate = `User-agent: Googlebot func TestRobotsTXTOutput(t *testing.T) { t.Parallel() + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) - cfg := config.New() cfg.Set("baseURL", "http://auth/bub/") cfg.Set("enableRobotsTXT", true) - b := newTestSitesBuilder(t).WithViper(cfg) - b.WithTemplatesAdded("layouts/robots.txt", robotTxtTemplate) + writeSource(t, fs, filepath.Join("layouts", "robots.txt"), robotTxtTemplate) + writeSourcesToSource(t, "content", fs, weightedSources...) - b.Build(BuildCfg{}) + buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + + th.assertFileContent("public/robots.txt", "User-agent: Googlebot") - b.AssertFileContent("public/robots.txt", "User-agent: Googlebot") -} - -func TestRobotsTXTDefaultTemplate(t *testing.T) { - t.Parallel() - files := ` --- hugo.toml -- -baseURL = "http://auth/bub/" -enableRobotsTXT = true -` - - b := Test(t, files) - - b.AssertFileContent("public/robots.txt", "User-agent: *") } diff --git a/hugolib/rss_test.go b/hugolib/rss_test.go index 34c2be393..268b13073 100644 --- a/hugolib/rss_test.go +++ b/hugolib/rss_test.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Hugo Authors. All rights reserved. +// Copyright 2016 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,22 +23,25 @@ import ( func TestRSSOutput(t *testing.T) { t.Parallel() + var ( + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + ) rssLimit := len(weightedSources) - 1 - cfg, fs := newTestCfg() + rssURI := "customrss.xml" + cfg.Set("baseURL", "http://auth/bub/") + cfg.Set("rssURI", rssURI) cfg.Set("title", "RSSTest") cfg.Set("rssLimit", rssLimit) - th, configs := newTestHelperFromProvider(cfg, fs, t) - - rssURI := "index.xml" for _, src := range weightedSources { - writeSource(t, fs, filepath.Join("content", "sect", src[0]), src[1]) + writeSource(t, fs, filepath.Join("content", "sect", src.Name), string(src.Content)) } - buildSingleSite(t, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{}) + buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) // Home RSS th.assertFileContent(filepath.Join("public", rssURI), "<?xml", "rss version", "RSSTest") @@ -48,99 +51,9 @@ func TestRSSOutput(t *testing.T) { th.assertFileContent(filepath.Join("public", "categories", "hugo", rssURI), "<?xml", "rss version", "Hugo on RSSTest") // RSS Item Limit - content := readWorkingDir(t, fs, filepath.Join("public", rssURI)) + content := readDestination(t, fs, filepath.Join("public", rssURI)) c := strings.Count(content, "<item>") if c != rssLimit { t.Errorf("incorrect RSS item count: expected %d, got %d", rssLimit, c) } - - // Encoded summary - th.assertFileContent(filepath.Join("public", rssURI), "<?xml", "description", "A <em>custom</em> summary") -} - -// Before Hugo 0.49 we set the pseudo page kind RSS on the page when output to RSS. -// This had some unintended side effects, esp. when the only output format for that page -// was RSS. -// For the page kinds that can have multiple output formats, the Kind should be one of the -// standard home, page etc. -// This test has this single purpose: Check that the Kind is that of the source page. -// See https://github.com/gohugoio/hugo/issues/5138 -func TestRSSKind(t *testing.T) { - t.Parallel() - - b := newTestSitesBuilder(t) - b.WithSimpleConfigFile().WithTemplatesAdded("index.rss.xml", `RSS Kind: {{ .Kind }}`) - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.xml", "RSS Kind: home") -} - -func TestRSSCanonifyURLs(t *testing.T) { - t.Parallel() - - b := newTestSitesBuilder(t) - b.WithSimpleConfigFile().WithTemplatesAdded("index.rss.xml", `<rss>{{ range .Pages }}<item>{{ .Content | html }}</item>{{ end }}</rss>`) - b.WithContent("page.md", `--- -Title: My Page ---- - -Figure: - -{{< figure src="/images/sunset.jpg" title="Sunset" >}} - - - -`) - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.xml", "img src="http://example.com/images/sunset.jpg") -} - -// Issue 13332. -func TestRSSCanonifyURLsSubDir(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = 'https://example.org/subdir' -disableKinds = ['section','sitemap','taxonomy','term'] -[markup.goldmark.renderHooks.image] -enableDefault = true -[markup.goldmark.renderHooks.link] -enableDefault = true --- layouts/_default/_markup/render-image.html -- -{{- $u := urls.Parse .Destination -}} -{{- $src := $u.String | relURL -}} -<img srcset="{{ $src }}" src="{{ $src }} 2x"> -<img src="{{ $src }}"> -{{- /**/ -}} --- layouts/_default/home.html -- -{{ .Content }}| --- layouts/_default/single.html -- -{{ .Content }}| --- layouts/_default/rss.xml -- -{{ with site.GetPage "/s1/p2" }} - {{ .Content | transform.XMLEscape | safeHTML }} -{{ end }} --- content/s1/p1.md -- ---- -title: p1 ---- --- content/s1/p2/index.md -- ---- -title: p2 ---- -![alt](a.jpg) - -[p1](/s1/p1) --- content/s1/p2/a.jpg -- -` - - b := Test(t, files) - - b.AssertFileContent("public/index.xml", "https://example.org/subdir/s1/p1/") - b.AssertFileContent("public/index.xml", - "img src="https://example.org/subdir/a.jpg", - "img srcset="https://example.org/subdir/a.jpg" src="https://example.org/subdir/a.jpg 2x") } diff --git a/hugolib/scratch.go b/hugolib/scratch.go new file mode 100644 index 000000000..ca2c9d6a8 --- /dev/null +++ b/hugolib/scratch.go @@ -0,0 +1,127 @@ +// Copyright 2015 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 hugolib + +import ( + "reflect" + "sort" + "sync" + + "github.com/gohugoio/hugo/tpl/math" +) + +// Scratch is a writable context used for stateful operations in Page/Node rendering. +type Scratch struct { + values map[string]interface{} + mu sync.RWMutex +} + +// Add will, for single values, add (using the + operator) the addend to the existing addend (if found). +// Supports numeric values and strings. +// +// If the first add for a key is an array or slice, then the next value(s) will be appended. +func (c *Scratch) Add(key string, newAddend interface{}) (string, error) { + + var newVal interface{} + c.mu.RLock() + existingAddend, found := c.values[key] + c.mu.RUnlock() + if found { + var err error + + addendV := reflect.ValueOf(existingAddend) + + if addendV.Kind() == reflect.Slice || addendV.Kind() == reflect.Array { + nav := reflect.ValueOf(newAddend) + if nav.Kind() == reflect.Slice || nav.Kind() == reflect.Array { + newVal = reflect.AppendSlice(addendV, nav).Interface() + } else { + newVal = reflect.Append(addendV, nav).Interface() + } + } else { + newVal, err = math.DoArithmetic(existingAddend, newAddend, '+') + if err != nil { + return "", err + } + } + } else { + newVal = newAddend + } + c.mu.Lock() + c.values[key] = newVal + c.mu.Unlock() + return "", nil // have to return something to make it work with the Go templates +} + +// Set stores a value with the given key in the Node context. +// This value can later be retrieved with Get. +func (c *Scratch) Set(key string, value interface{}) string { + c.mu.Lock() + c.values[key] = value + c.mu.Unlock() + return "" +} + +// Get returns a value previously set by Add or Set +func (c *Scratch) Get(key string) interface{} { + c.mu.RLock() + val := c.values[key] + c.mu.RUnlock() + + return val +} + +// SetInMap stores a value to a map with the given key in the Node context. +// This map can later be retrieved with GetSortedMapValues. +func (c *Scratch) SetInMap(key string, mapKey string, value interface{}) string { + c.mu.Lock() + _, found := c.values[key] + if !found { + c.values[key] = make(map[string]interface{}) + } + + c.values[key].(map[string]interface{})[mapKey] = value + c.mu.Unlock() + return "" +} + +// GetSortedMapValues returns a sorted map previously filled with SetInMap +func (c *Scratch) GetSortedMapValues(key string) interface{} { + c.mu.RLock() + + if c.values[key] == nil { + c.mu.RUnlock() + return nil + } + + unsortedMap := c.values[key].(map[string]interface{}) + c.mu.RUnlock() + var keys []string + for mapKey := range unsortedMap { + keys = append(keys, mapKey) + } + + sort.Strings(keys) + + sortedArray := make([]interface{}, len(unsortedMap)) + for i, mapKey := range keys { + sortedArray[i] = unsortedMap[mapKey] + } + + return sortedArray +} + +func newScratch() *Scratch { + return &Scratch{values: make(map[string]interface{})} +} diff --git a/hugolib/scratch_test.go b/hugolib/scratch_test.go new file mode 100644 index 000000000..f65c2ddfe --- /dev/null +++ b/hugolib/scratch_test.go @@ -0,0 +1,161 @@ +// Copyright 2015 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 hugolib + +import ( + "reflect" + "sync" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestScratchAdd(t *testing.T) { + t.Parallel() + scratch := newScratch() + scratch.Add("int1", 10) + scratch.Add("int1", 20) + scratch.Add("int2", 20) + + assert.Equal(t, int64(30), scratch.Get("int1")) + assert.Equal(t, 20, scratch.Get("int2")) + + scratch.Add("float1", float64(10.5)) + scratch.Add("float1", float64(20.1)) + + assert.Equal(t, float64(30.6), scratch.Get("float1")) + + scratch.Add("string1", "Hello ") + scratch.Add("string1", "big ") + scratch.Add("string1", "World!") + + assert.Equal(t, "Hello big World!", scratch.Get("string1")) + + scratch.Add("scratch", scratch) + _, err := scratch.Add("scratch", scratch) + + if err == nil { + t.Errorf("Expected error from invalid arithmetic") + } + +} + +func TestScratchAddSlice(t *testing.T) { + t.Parallel() + scratch := newScratch() + + _, err := scratch.Add("intSlice", []int{1, 2}) + assert.Nil(t, err) + _, err = scratch.Add("intSlice", 3) + assert.Nil(t, err) + + sl := scratch.Get("intSlice") + expected := []int{1, 2, 3} + + if !reflect.DeepEqual(expected, sl) { + t.Errorf("Slice difference, go %q expected %q", sl, expected) + } + + _, err = scratch.Add("intSlice", []int{4, 5}) + + assert.Nil(t, err) + + sl = scratch.Get("intSlice") + expected = []int{1, 2, 3, 4, 5} + + if !reflect.DeepEqual(expected, sl) { + t.Errorf("Slice difference, go %q expected %q", sl, expected) + } + +} + +func TestScratchSet(t *testing.T) { + t.Parallel() + scratch := newScratch() + scratch.Set("key", "val") + assert.Equal(t, "val", scratch.Get("key")) +} + +// Issue #2005 +func TestScratchInParallel(t *testing.T) { + var wg sync.WaitGroup + scratch := newScratch() + key := "counter" + scratch.Set(key, int64(1)) + for i := 1; i <= 10; i++ { + wg.Add(1) + go func(j int) { + for k := 0; k < 10; k++ { + newVal := int64(k + j) + + _, err := scratch.Add(key, newVal) + if err != nil { + t.Errorf("Got err %s", err) + } + + scratch.Set(key, newVal) + + val := scratch.Get(key) + + if counter, ok := val.(int64); ok { + if counter < 1 { + t.Errorf("Got %d", counter) + } + } else { + t.Errorf("Got %T", val) + } + } + wg.Done() + }(i) + } + wg.Wait() +} + +func TestScratchGet(t *testing.T) { + t.Parallel() + scratch := newScratch() + nothing := scratch.Get("nothing") + if nothing != nil { + t.Errorf("Should not return anything, but got %v", nothing) + } +} + +func TestScratchSetInMap(t *testing.T) { + t.Parallel() + scratch := newScratch() + scratch.SetInMap("key", "lux", "Lux") + scratch.SetInMap("key", "abc", "Abc") + scratch.SetInMap("key", "zyx", "Zyx") + scratch.SetInMap("key", "abc", "Abc (updated)") + scratch.SetInMap("key", "def", "Def") + assert.Equal(t, []interface{}{0: "Abc (updated)", 1: "Def", 2: "Lux", 3: "Zyx"}, scratch.GetSortedMapValues("key")) +} + +func TestScratchGetSortedMapValues(t *testing.T) { + t.Parallel() + scratch := newScratch() + nothing := scratch.GetSortedMapValues("nothing") + if nothing != nil { + t.Errorf("Should not return anything, but got %v", nothing) + } +} + +func BenchmarkScratchGet(b *testing.B) { + scratch := newScratch() + scratch.Add("A", 1) + b.ResetTimer() + for i := 0; i < b.N; i++ { + scratch.Get("A") + } +} diff --git a/hugolib/securitypolicies_test.go b/hugolib/securitypolicies_test.go deleted file mode 100644 index facda80eb..000000000 --- a/hugolib/securitypolicies_test.go +++ /dev/null @@ -1,191 +0,0 @@ -// Copyright 2019 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 hugolib - -import ( - "fmt" - "net/http" - "net/http/httptest" - "runtime" - "testing" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/markup/asciidocext" - "github.com/gohugoio/hugo/markup/pandoc" - "github.com/gohugoio/hugo/markup/rst" - "github.com/gohugoio/hugo/resources/resource_transformers/tocss/dartsass" -) - -func TestSecurityPolicies(t *testing.T) { - c := qt.New(t) - - testVariant := func(c *qt.C, withBuilder func(b *sitesBuilder), expectErr string) { - c.Helper() - b := newTestSitesBuilder(c) - withBuilder(b) - - if expectErr != "" { - err := b.BuildE(BuildCfg{}) - b.Assert(err, qt.IsNotNil) - b.Assert(err, qt.ErrorMatches, expectErr) - } else { - b.Build(BuildCfg{}) - } - } - - httpTestVariant := func(c *qt.C, templ, expectErr string, withBuilder func(b *sitesBuilder)) { - ts := httptest.NewServer(http.FileServer(http.Dir("testdata/"))) - c.Cleanup(func() { - ts.Close() - }) - cb := func(b *sitesBuilder) { - b.WithTemplatesAdded("index.html", fmt.Sprintf(templ, ts.URL)) - if withBuilder != nil { - withBuilder(b) - } - } - testVariant(c, cb, expectErr) - } - - c.Run("os.GetEnv, denied", func(c *qt.C) { - c.Parallel() - cb := func(b *sitesBuilder) { - b.WithTemplatesAdded("index.html", `{{ os.Getenv "FOOBAR" }}`) - } - testVariant(c, cb, `(?s).*"FOOBAR" is not whitelisted in policy "security\.funcs\.getenv".*`) - }) - - c.Run("os.GetEnv, OK", func(c *qt.C) { - c.Parallel() - cb := func(b *sitesBuilder) { - b.WithTemplatesAdded("index.html", `{{ os.Getenv "HUGO_FOO" }}`) - } - testVariant(c, cb, "") - }) - - c.Run("Asciidoc, denied", func(c *qt.C) { - c.Parallel() - if !asciidocext.Supports() { - c.Skip() - } - - cb := func(b *sitesBuilder) { - b.WithContent("page.ad", "foo") - } - - testVariant(c, cb, `(?s).*"asciidoctor" is not whitelisted in policy "security\.exec\.allow".*`) - }) - - c.Run("RST, denied", func(c *qt.C) { - c.Parallel() - if !rst.Supports() { - c.Skip() - } - - cb := func(b *sitesBuilder) { - b.WithContent("page.rst", "foo") - } - - if runtime.GOOS == "windows" { - testVariant(c, cb, `(?s).*python(\.exe)?" is not whitelisted in policy "security\.exec\.allow".*`) - } else { - testVariant(c, cb, `(?s).*"rst2html(\.py)?" is not whitelisted in policy "security\.exec\.allow".*`) - } - }) - - c.Run("Pandoc, denied", func(c *qt.C) { - c.Parallel() - if !pandoc.Supports() { - c.Skip() - } - - cb := func(b *sitesBuilder) { - b.WithContent("page.pdc", "foo") - } - - testVariant(c, cb, `(?s).*pandoc" is not whitelisted in policy "security\.exec\.allow".*`) - }) - - c.Run("Dart SASS, OK", func(c *qt.C) { - c.Parallel() - if !dartsass.Supports() { - c.Skip() - } - cb := func(b *sitesBuilder) { - b.WithTemplatesAdded("index.html", `{{ $scss := "body { color: #333; }" | resources.FromString "foo.scss" | css.Sass (dict "transpiler" "dartsass") }}`) - } - testVariant(c, cb, "") - }) - - c.Run("Dart SASS, denied", func(c *qt.C) { - c.Parallel() - if !dartsass.Supports() { - c.Skip() - } - cb := func(b *sitesBuilder) { - b.WithConfigFile("toml", ` -[security] -[security.exec] -allow="none" - - `) - b.WithTemplatesAdded("index.html", `{{ $scss := "body { color: #333; }" | resources.FromString "foo.scss" | css.Sass (dict "transpiler" "dartsass") }}`) - } - testVariant(c, cb, `(?s).*sass(-embedded)?" is not whitelisted in policy "security\.exec\.allow".*`) - }) - - c.Run("resources.GetRemote, OK", func(c *qt.C) { - c.Parallel() - httpTestVariant(c, `{{ $json := resources.GetRemote "%[1]s/fruits.json" }}{{ $json.Content }}`, "", nil) - }) - - c.Run("resources.GetRemote, denied method", func(c *qt.C) { - c.Parallel() - httpTestVariant(c, `{{ $json := resources.GetRemote "%[1]s/fruits.json" (dict "method" "DELETE" ) }}{{ $json.Content }}`, `(?s).*"DELETE" is not whitelisted in policy "security\.http\.method".*`, nil) - }) - - c.Run("resources.GetRemote, denied URL", func(c *qt.C) { - c.Parallel() - httpTestVariant(c, `{{ $json := resources.GetRemote "%[1]s/fruits.json" }}{{ $json.Content }}`, `(?s).*is not whitelisted in policy "security\.http\.urls".*`, - func(b *sitesBuilder) { - b.WithConfigFile("toml", ` -[security] -[security.http] -urls="none" -`) - }) - }) - - c.Run("resources.GetRemote, fake JSON", func(c *qt.C) { - c.Parallel() - httpTestVariant(c, `{{ $json := resources.GetRemote "%[1]s/fakejson.json" }}{{ $json.Content }}`, `(?s).*failed to resolve media type.*`, - func(b *sitesBuilder) { - b.WithConfigFile("toml", ` -`) - }) - }) - - c.Run("resources.GetRemote, fake JSON whitelisted", func(c *qt.C) { - c.Parallel() - httpTestVariant(c, `{{ $json := resources.GetRemote "%[1]s/fakejson.json" }}{{ $json.Content }}`, ``, - func(b *sitesBuilder) { - b.WithConfigFile("toml", ` -[security] -[security.http] -mediaTypes=["application/json"] - -`) - }) - }) -} diff --git a/hugolib/segments/segments.go b/hugolib/segments/segments.go deleted file mode 100644 index 941c4ea5c..000000000 --- a/hugolib/segments/segments.go +++ /dev/null @@ -1,257 +0,0 @@ -// 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 segments - -import ( - "fmt" - - "github.com/gobwas/glob" - "github.com/gohugoio/hugo/common/maps" - "github.com/gohugoio/hugo/common/predicate" - "github.com/gohugoio/hugo/config" - hglob "github.com/gohugoio/hugo/hugofs/glob" - "github.com/mitchellh/mapstructure" -) - -// Segments is a collection of named segments. -type Segments struct { - s map[string]excludeInclude -} - -type excludeInclude struct { - exclude predicate.P[SegmentMatcherFields] - include predicate.P[SegmentMatcherFields] -} - -// ShouldExcludeCoarse returns whether the given fields should be excluded. -// This is used for the coarser grained checks, e.g. language and output format. -// Note that ShouldExcludeCoarse(fields) == ShouldExcludeFine(fields) may -// not always be true, but ShouldExcludeCoarse(fields) == true == ShouldExcludeFine(fields) -// will always be truthful. -func (e excludeInclude) ShouldExcludeCoarse(fields SegmentMatcherFields) bool { - return e.exclude != nil && e.exclude(fields) -} - -// ShouldExcludeFine returns whether the given fields should be excluded. -// This is used for the finer grained checks, e.g. on individual pages. -func (e excludeInclude) ShouldExcludeFine(fields SegmentMatcherFields) bool { - if e.exclude != nil && e.exclude(fields) { - return true - } - return e.include != nil && !e.include(fields) -} - -type SegmentFilter interface { - // ShouldExcludeCoarse returns whether the given fields should be excluded on a coarse level. - ShouldExcludeCoarse(SegmentMatcherFields) bool - - // ShouldExcludeFine returns whether the given fields should be excluded on a fine level. - ShouldExcludeFine(SegmentMatcherFields) bool -} - -type segmentFilter struct { - coarse predicate.P[SegmentMatcherFields] - fine predicate.P[SegmentMatcherFields] -} - -func (f segmentFilter) ShouldExcludeCoarse(field SegmentMatcherFields) bool { - return f.coarse(field) -} - -func (f segmentFilter) ShouldExcludeFine(fields SegmentMatcherFields) bool { - return f.fine(fields) -} - -var ( - matchAll = func(SegmentMatcherFields) bool { return true } - matchNothing = func(SegmentMatcherFields) bool { return false } -) - -// Get returns a SegmentFilter for the given segments. -func (sms Segments) Get(onNotFound func(s string), ss ...string) SegmentFilter { - if ss == nil { - return segmentFilter{coarse: matchNothing, fine: matchNothing} - } - var sf segmentFilter - for _, s := range ss { - if seg, ok := sms.s[s]; ok { - if sf.coarse == nil { - sf.coarse = seg.ShouldExcludeCoarse - } else { - sf.coarse = sf.coarse.Or(seg.ShouldExcludeCoarse) - } - if sf.fine == nil { - sf.fine = seg.ShouldExcludeFine - } else { - sf.fine = sf.fine.Or(seg.ShouldExcludeFine) - } - } else if onNotFound != nil { - onNotFound(s) - } - } - - if sf.coarse == nil { - sf.coarse = matchAll - } - if sf.fine == nil { - sf.fine = matchAll - } - - return sf -} - -type SegmentConfig struct { - Excludes []SegmentMatcherFields - Includes []SegmentMatcherFields -} - -// SegmentMatcherFields is a matcher for a segment include or exclude. -// All of these are Glob patterns. -type SegmentMatcherFields struct { - Kind string - Path string - Lang string - Output string -} - -func getGlob(s string) (glob.Glob, error) { - if s == "" { - return nil, nil - } - g, err := hglob.GetGlob(s) - if err != nil { - return nil, fmt.Errorf("failed to compile Glob %q: %w", s, err) - } - return g, nil -} - -func compileSegments(f []SegmentMatcherFields) (predicate.P[SegmentMatcherFields], error) { - if f == nil { - return func(SegmentMatcherFields) bool { return false }, nil - } - var ( - result predicate.P[SegmentMatcherFields] - section predicate.P[SegmentMatcherFields] - ) - - addToSection := func(matcherFields SegmentMatcherFields, f func(fields SegmentMatcherFields) string) error { - s1 := f(matcherFields) - g, err := getGlob(s1) - if err != nil { - return err - } - matcher := func(fields SegmentMatcherFields) bool { - s2 := f(fields) - if s2 == "" { - return false - } - return g.Match(s2) - } - if section == nil { - section = matcher - } else { - section = section.And(matcher) - } - return nil - } - - for _, fields := range f { - if fields.Kind != "" { - if err := addToSection(fields, func(fields SegmentMatcherFields) string { return fields.Kind }); err != nil { - return result, err - } - } - if fields.Path != "" { - if err := addToSection(fields, func(fields SegmentMatcherFields) string { return fields.Path }); err != nil { - return result, err - } - } - if fields.Lang != "" { - if err := addToSection(fields, func(fields SegmentMatcherFields) string { return fields.Lang }); err != nil { - return result, err - } - } - if fields.Output != "" { - if err := addToSection(fields, func(fields SegmentMatcherFields) string { return fields.Output }); err != nil { - return result, err - } - } - - if result == nil { - result = section - } else { - result = result.Or(section) - } - section = nil - - } - - return result, nil -} - -func DecodeSegments(in map[string]any) (*config.ConfigNamespace[map[string]SegmentConfig, Segments], error) { - buildConfig := func(in any) (Segments, any, error) { - sms := Segments{ - s: map[string]excludeInclude{}, - } - m, err := maps.ToStringMapE(in) - if err != nil { - return sms, nil, err - } - if m == nil { - m = map[string]any{} - } - m = maps.CleanConfigStringMap(m) - - var scfgm map[string]SegmentConfig - if err := mapstructure.Decode(m, &scfgm); err != nil { - return sms, nil, err - } - - for k, v := range scfgm { - var ( - include predicate.P[SegmentMatcherFields] - exclude predicate.P[SegmentMatcherFields] - err error - ) - if v.Excludes != nil { - exclude, err = compileSegments(v.Excludes) - if err != nil { - return sms, nil, err - } - } - if v.Includes != nil { - include, err = compileSegments(v.Includes) - if err != nil { - return sms, nil, err - } - } - - ei := excludeInclude{ - exclude: exclude, - include: include, - } - sms.s[k] = ei - - } - - return sms, nil, nil - } - - ns, err := config.DecodeNamespace[map[string]SegmentConfig](in, buildConfig) - if err != nil { - return nil, fmt.Errorf("failed to decode segments: %w", err) - } - return ns, nil -} diff --git a/hugolib/segments/segments_integration_test.go b/hugolib/segments/segments_integration_test.go deleted file mode 100644 index 465a7abe0..000000000 --- a/hugolib/segments/segments_integration_test.go +++ /dev/null @@ -1,76 +0,0 @@ -// 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 segments_test - -import ( - "testing" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/hugolib" -) - -func TestSegments(t *testing.T) { - files := ` --- hugo.toml -- -baseURL = "https://example.org/" -renderSegments = ["docs"] -[languages] -[languages.en] -weight = 1 -[languages.no] -weight = 2 -[languages.nb] -weight = 3 -[segments] -[segments.docs] -[[segments.docs.includes]] -kind = "{home,taxonomy,term}" -[[segments.docs.includes]] -path = "{/docs,/docs/**}" -[[segments.docs.excludes]] -path = "/blog/**" -[[segments.docs.excludes]] -lang = "n*" -output = "rss" -[[segments.docs.excludes]] -output = "json" --- layouts/_default/single.html -- -Single: {{ .Title }}|{{ .RelPermalink }}| --- layouts/_default/list.html -- -List: {{ .Title }}|{{ .RelPermalink }}| --- content/docs/_index.md -- --- content/docs/section1/_index.md -- --- content/docs/section1/page1.md -- ---- -title: "Docs Page 1" -tags: ["tag1", "tag2"] ---- --- content/blog/_index.md -- --- content/blog/section1/page1.md -- ---- -title: "Blog Page 1" -tags: ["tag1", "tag2"] ---- -` - - b := hugolib.Test(t, files) - b.Assert(b.H.Configs.Base.RootConfig.RenderSegments, qt.DeepEquals, []string{"docs"}) - - b.AssertFileContent("public/docs/section1/page1/index.html", "Docs Page 1") - b.AssertFileExists("public/blog/section1/page1/index.html", false) - b.AssertFileExists("public/index.html", true) - b.AssertFileExists("public/index.xml", true) - b.AssertFileExists("public/no/index.html", true) - b.AssertFileExists("public/no/index.xml", false) -} diff --git a/hugolib/segments/segments_test.go b/hugolib/segments/segments_test.go deleted file mode 100644 index 1a2dfb97b..000000000 --- a/hugolib/segments/segments_test.go +++ /dev/null @@ -1,115 +0,0 @@ -package segments - -import ( - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestCompileSegments(t *testing.T) { - c := qt.New(t) - - c.Run("excludes", func(c *qt.C) { - fields := []SegmentMatcherFields{ - { - Lang: "n*", - Output: "rss", - }, - } - - match, err := compileSegments(fields) - c.Assert(err, qt.IsNil) - - check := func() { - c.Assert(match, qt.IsNotNil) - c.Assert(match(SegmentMatcherFields{Lang: "no"}), qt.Equals, false) - c.Assert(match(SegmentMatcherFields{Lang: "no", Kind: "page"}), qt.Equals, false) - c.Assert(match(SegmentMatcherFields{Lang: "no", Output: "rss"}), qt.Equals, true) - c.Assert(match(SegmentMatcherFields{Lang: "no", Output: "html"}), qt.Equals, false) - c.Assert(match(SegmentMatcherFields{Kind: "page"}), qt.Equals, false) - c.Assert(match(SegmentMatcherFields{Lang: "no", Output: "rss", Kind: "page"}), qt.Equals, true) - } - - check() - - fields = []SegmentMatcherFields{ - { - Path: "/blog/**", - }, - { - Lang: "n*", - Output: "rss", - }, - } - - match, err = compileSegments(fields) - c.Assert(err, qt.IsNil) - check() - c.Assert(match(SegmentMatcherFields{Path: "/blog/foo"}), qt.Equals, true) - }) - - c.Run("includes", func(c *qt.C) { - fields := []SegmentMatcherFields{ - { - Path: "/docs/**", - }, - { - Lang: "no", - Output: "rss", - }, - } - - match, err := compileSegments(fields) - c.Assert(err, qt.IsNil) - c.Assert(match, qt.IsNotNil) - c.Assert(match(SegmentMatcherFields{Lang: "no"}), qt.Equals, false) - c.Assert(match(SegmentMatcherFields{Kind: "page"}), qt.Equals, false) - c.Assert(match(SegmentMatcherFields{Kind: "page", Path: "/blog/foo"}), qt.Equals, false) - c.Assert(match(SegmentMatcherFields{Lang: "en"}), qt.Equals, false) - c.Assert(match(SegmentMatcherFields{Lang: "no", Output: "rss"}), qt.Equals, true) - c.Assert(match(SegmentMatcherFields{Lang: "no", Output: "html"}), qt.Equals, false) - c.Assert(match(SegmentMatcherFields{Kind: "page", Path: "/docs/foo"}), qt.Equals, true) - }) - - c.Run("includes variant1", func(c *qt.C) { - c.Skip() - - fields := []SegmentMatcherFields{ - { - Kind: "home", - }, - { - Path: "{/docs,/docs/**}", - }, - } - - match, err := compileSegments(fields) - c.Assert(err, qt.IsNil) - c.Assert(match, qt.IsNotNil) - c.Assert(match(SegmentMatcherFields{Path: "/blog/foo"}), qt.Equals, false) - c.Assert(match(SegmentMatcherFields{Kind: "page", Path: "/docs/foo"}), qt.Equals, true) - c.Assert(match(SegmentMatcherFields{Kind: "home", Path: "/"}), qt.Equals, true) - }) -} - -func BenchmarkSegmentsMatch(b *testing.B) { - fields := []SegmentMatcherFields{ - { - Path: "/docs/**", - }, - { - Lang: "no", - Output: "rss", - }, - } - - match, err := compileSegments(fields) - if err != nil { - b.Fatal(err) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - match(SegmentMatcherFields{Lang: "no", Output: "rss"}) - } -} diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go index 56bf1ff9e..3cf472f82 100644 --- a/hugolib/shortcode.go +++ b/hugolib/shortcode.go @@ -1,4 +1,4 @@ -// Copyright 2025 The Hugo Authors. All rights reserved. +// Copyright 2017 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,133 +15,60 @@ package hugolib import ( "bytes" - "context" "errors" "fmt" "html/template" - "path" "reflect" "regexp" "sort" - "strconv" "strings" "sync" - "github.com/gohugoio/hugo/common/herrors" - "github.com/gohugoio/hugo/common/types" - "github.com/gohugoio/hugo/tpl/tplimpl" + "github.com/gohugoio/hugo/output" - "github.com/gohugoio/hugo/parser/pageparser" - "github.com/gohugoio/hugo/resources/page" - - "github.com/gohugoio/hugo/common/maps" - "github.com/gohugoio/hugo/common/text" - "github.com/gohugoio/hugo/common/urls" + "github.com/gohugoio/hugo/media" bp "github.com/gohugoio/hugo/bufferpool" + "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/tpl" ) -var ( - _ urls.RefLinker = (*ShortcodeWithPage)(nil) - _ types.Unwrapper = (*ShortcodeWithPage)(nil) - _ text.Positioner = (*ShortcodeWithPage)(nil) - _ maps.StoreProvider = (*ShortcodeWithPage)(nil) -) - // ShortcodeWithPage is the "." context in a shortcode template. type ShortcodeWithPage struct { - Params any + Params interface{} Inner template.HTML - Page page.Page + Page *Page Parent *ShortcodeWithPage - Name string IsNamedParams bool - - // Zero-based ordinal in relation to its parent. If the parent is the page itself, - // this ordinal will represent the position of this shortcode in the page content. - Ordinal int - - // Indentation before the opening shortcode in the source. - indentation string - - innerDeindentInit sync.Once - innerDeindent template.HTML - - // pos is the position in bytes in the source file. Used for error logging. - posInit sync.Once - posOffset int - pos text.Position - - store *maps.Scratch -} - -// InnerDeindent returns the (potentially de-indented) inner content of the shortcode. -func (scp *ShortcodeWithPage) InnerDeindent() template.HTML { - if scp.indentation == "" { - return scp.Inner - } - scp.innerDeindentInit.Do(func() { - b := bp.GetBuffer() - text.VisitLinesAfter(string(scp.Inner), func(s string) { - if strings.HasPrefix(s, scp.indentation) { - b.WriteString(strings.TrimPrefix(s, scp.indentation)) - } else { - b.WriteString(s) - } - }) - scp.innerDeindent = template.HTML(b.String()) - bp.PutBuffer(b) - }) - - return scp.innerDeindent -} - -// Position returns this shortcode's detailed position. Note that this information -// may be expensive to calculate, so only use this in error situations. -func (scp *ShortcodeWithPage) Position() text.Position { - scp.posInit.Do(func() { - if p, ok := mustUnwrapPage(scp.Page).(pageContext); ok { - scp.pos = p.posOffset(scp.posOffset) - } - }) - return scp.pos + scratch *Scratch } // Site returns information about the current site. -func (scp *ShortcodeWithPage) Site() page.Site { - return scp.Page.Site() +func (scp *ShortcodeWithPage) Site() *SiteInfo { + return scp.Page.Site } -// Ref is a shortcut to the Ref method on Page. It passes itself as a context -// to get better error messages. -func (scp *ShortcodeWithPage) Ref(args map[string]any) (string, error) { - return scp.Page.RefFrom(args, scp) +// Ref is a shortcut to the Ref method on Page. +func (scp *ShortcodeWithPage) Ref(ref string) (string, error) { + return scp.Page.Ref(ref) } -// RelRef is a shortcut to the RelRef method on Page. It passes itself as a context -// to get better error messages. -func (scp *ShortcodeWithPage) RelRef(args map[string]any) (string, error) { - return scp.Page.RelRefFrom(args, scp) -} - -// Store returns this shortcode's Store. -func (scp *ShortcodeWithPage) Store() *maps.Scratch { - if scp.store == nil { - scp.store = maps.NewScratch() - } - return scp.store +// RelRef is a shortcut to the RelRef method on Page. +func (scp *ShortcodeWithPage) RelRef(ref string) (string, error) { + return scp.Page.RelRef(ref) } // Scratch returns a scratch-pad scoped for this shortcode. This can be used // as a temporary storage for variables, counters etc. -// Deprecated: Use Store instead. Note that from the templates this should be considered a "soft deprecation". -func (scp *ShortcodeWithPage) Scratch() *maps.Scratch { - return scp.Store() +func (scp *ShortcodeWithPage) Scratch() *Scratch { + if scp.scratch == nil { + scp.scratch = newScratch() + } + return scp.scratch } // Get is a convenience method to look up shortcode parameters by its key. -func (scp *ShortcodeWithPage) Get(key any) any { +func (scp *ShortcodeWithPage) Get(key interface{}) interface{} { if scp.Params == nil { return nil } @@ -154,15 +81,13 @@ func (scp *ShortcodeWithPage) Get(key any) any { switch key.(type) { case int64, int32, int16, int8, int: if reflect.TypeOf(scp.Params).Kind() == reflect.Map { - // We treat this as a non error, so people can do similar to - // {{ $myParam := .Get "myParam" | default .Get 0 }} - // Without having to do additional checks. - return nil + return "error: cannot access named params by position" } else if reflect.TypeOf(scp.Params).Kind() == reflect.Slice { idx := int(reflect.ValueOf(key).Int()) ln := reflect.ValueOf(scp.Params).Len() if idx > ln-1 { - return "" + helpers.DistinctErrorLog.Printf("No shortcode param at .Get %d in page %s, have params: %v", idx, scp.Page.FullFilePath(), scp.Params) + return fmt.Sprintf("error: index out of range for positional param at position %d", idx) } x = reflect.ValueOf(scp.Params).Index(idx) } @@ -173,98 +98,50 @@ func (scp *ShortcodeWithPage) Get(key any) any { return "" } } else if reflect.TypeOf(scp.Params).Kind() == reflect.Slice { - // We treat this as a non error, so people can do similar to - // {{ $myParam := .Get "myParam" | default .Get 0 }} - // Without having to do additional checks. - return nil + if reflect.ValueOf(scp.Params).Len() == 1 && reflect.ValueOf(scp.Params).Index(0).String() == "" { + return nil + } + return "error: cannot access positional params by string name" } } - return x.Interface() -} + switch x.Kind() { + case reflect.String: + return x.String() + case reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int: + return x.Int() + default: + return x + } -// For internal use only. -func (scp *ShortcodeWithPage) Unwrapv() any { - return scp.Page } // Note - this value must not contain any markup syntax -const shortcodePlaceholderPrefix = "HAHAHUGOSHORTCODE" - -func createShortcodePlaceholder(sid string, id uint64, ordinal int) string { - return shortcodePlaceholderPrefix + strconv.FormatUint(id, 10) + sid + strconv.Itoa(ordinal) + "HBHB" -} +const shortcodePlaceholderPrefix = "HUGOSHORTCODE" type shortcode struct { - name string - isInline bool // inline shortcode. Any inner will be a Go template. - isClosing bool // whether a closing tag was provided - inner []any // string or nested shortcode - params any // map or array - ordinal int - - indentation string // indentation from source. - - templ *tplimpl.TemplInfo - - // If set, the rendered shortcode is sent as part of the surrounding content - // to Goldmark and similar. - // Before Hug0 0.55 we didn't send any shortcode output to the markup - // renderer, and this flag told Hugo to process the {{ .Inner }} content - // separately. - // The old behavior can be had by starting your shortcode template with: - // {{ $_hugo_config := `{ "version": 1 }`}} + name string + inner []interface{} // string or nested shortcode + params interface{} // map or array + err error doMarkup bool - - // the placeholder in the source when passed to Goldmark etc. - // This also identifies the rendered shortcode. - placeholder string - - pos int // the position in bytes in the source file - length int // the length in bytes in the source file -} - -func (s shortcode) insertPlaceholder() bool { - return !s.doMarkup || s.configVersion() == 1 -} - -func (s shortcode) needsInner() bool { - return s.templ != nil && s.templ.ParseInfo.IsInner -} - -func (s shortcode) configVersion() int { - if s.templ == nil { - // Not set for inline shortcodes. - return 2 - } - return s.templ.ParseInfo.Config.Version -} - -func (s shortcode) innerString() string { - var sb strings.Builder - - for _, inner := range s.inner { - sb.WriteString(inner.(string)) - } - - return sb.String() } func (sc shortcode) String() string { // for testing (mostly), so any change here will break tests! - var params any + var params interface{} switch v := sc.params.(type) { - case map[string]any: + case map[string]string: // sort the keys so test assertions won't fail var keys []string for k := range v { keys = append(keys, k) } sort.Strings(keys) - tmp := make(map[string]any) + var tmp = make([]string, len(keys)) - for _, k := range keys { - tmp[k] = v[k] + for i, k := range keys { + tmp[i] = k + ":" + v[k] } params = tmp @@ -276,157 +153,131 @@ func (sc shortcode) String() string { return fmt.Sprintf("%s(%q, %t){%s}", sc.name, params, sc.doMarkup, sc.inner) } -type shortcodeHandler struct { - filename string - s *Site +// We may have special shortcode templates for AMP etc. +// Note that in the below, OutputFormat may be empty. +// We will try to look for the most specific shortcode template available. +type scKey struct { + Lang string + OutputFormat string + Suffix string + ShortcodePlaceholder string +} - // Ordered list of shortcodes for a page. - shortcodes []*shortcode +func newScKey(m media.Type, shortcodeplaceholder string) scKey { + return scKey{Suffix: m.Suffix, ShortcodePlaceholder: shortcodeplaceholder} +} + +func newScKeyFromLangAndOutputFormat(lang string, o output.Format, shortcodeplaceholder string) scKey { + return scKey{Lang: lang, Suffix: o.MediaType.Suffix, OutputFormat: o.Name, ShortcodePlaceholder: shortcodeplaceholder} +} + +func newDefaultScKey(shortcodeplaceholder string) scKey { + return newScKey(media.HTMLType, shortcodeplaceholder) +} + +type shortcodeHandler struct { + init sync.Once + + p *Page + + // This is all shortcode rendering funcs for all potential output formats. + contentShortcodes map[scKey]func() (string, error) + + // This map contains the new or changed set of shortcodes that need + // to be rendered for the current output format. + contentShortcodesDelta map[scKey]func() (string, error) + + // This maps the shorcode placeholders with the rendered content. + // We will do (potential) partial re-rendering per output format, + // so keep this for the unchanged. + renderedShortcodes map[string]string + + // Maps the shortcodeplaceholder with the actual shortcode. + shortcodes map[string]shortcode // All the shortcode names in this set. - nameSet map[string]bool - nameSetMu sync.RWMutex - - // Configuration - enableInlineShortcodes bool + nameSet map[string]bool } -func newShortcodeHandler(filename string, s *Site) *shortcodeHandler { - sh := &shortcodeHandler{ - filename: filename, - s: s, - enableInlineShortcodes: s.ExecHelper.Sec().EnableInlineShortcodes, - shortcodes: make([]*shortcode, 0, 4), - nameSet: make(map[string]bool), +func newShortcodeHandler(p *Page) *shortcodeHandler { + return &shortcodeHandler{ + p: p, + contentShortcodes: make(map[scKey]func() (string, error)), + shortcodes: make(map[string]shortcode), + nameSet: make(map[string]bool), + renderedShortcodes: make(map[string]string), + } +} + +// TODO(bep) make it non-global +var isInnerShortcodeCache = struct { + sync.RWMutex + m map[string]bool +}{m: make(map[string]bool)} + +// to avoid potential costly look-aheads for closing tags we look inside the template itself +// we could change the syntax to self-closing tags, but that would make users cry +// the value found is cached +func isInnerShortcode(t tpl.TemplateExecutor) (bool, error) { + isInnerShortcodeCache.RLock() + m, ok := isInnerShortcodeCache.m[t.Name()] + isInnerShortcodeCache.RUnlock() + + if ok { + return m, nil } - return sh + isInnerShortcodeCache.Lock() + defer isInnerShortcodeCache.Unlock() + match, _ := regexp.MatchString("{{.*?\\.Inner.*?}}", t.Tree()) + isInnerShortcodeCache.m[t.Name()] = match + + return match, nil } -const ( - innerNewlineRegexp = "\n" - innerCleanupRegexp = `\A<p>(.*)</p>\n\z` - innerCleanupExpand = "$1" -) +func clearIsInnerShortcodeCache() { + isInnerShortcodeCache.Lock() + defer isInnerShortcodeCache.Unlock() + isInnerShortcodeCache.m = make(map[string]bool) +} -func prepareShortcode( - ctx context.Context, - level int, - s *Site, - sc *shortcode, +func createShortcodePlaceholder(id int) string { + return fmt.Sprintf("HAHA%s-%dHBHB", shortcodePlaceholderPrefix, id) +} + +const innerNewlineRegexp = "\n" +const innerCleanupRegexp = `\A<p>(.*)</p>\n\z` +const innerCleanupExpand = "$1" + +func prepareShortcodeForPage(placeholder string, sc shortcode, parent *ShortcodeWithPage, p *Page) map[scKey]func() (string, error) { + + m := make(map[scKey]func() (string, error)) + lang := p.Lang() + + for _, f := range p.outputFormats { + // The most specific template will win. + key := newScKeyFromLangAndOutputFormat(lang, f, placeholder) + m[key] = func() (string, error) { + return renderShortcode(key, sc, nil, p), nil + } + } + + return m +} + +func renderShortcode( + tmplKey scKey, + sc shortcode, parent *ShortcodeWithPage, - po *pageOutput, - isRenderString bool, -) (shortcodeRenderer, error) { - p := po.p - toParseErr := func(err error) error { - source := p.m.content.mustSource() - return p.parseError(fmt.Errorf("failed to render shortcode %q: %w", sc.name, err), source, sc.pos) - } - - // Allow the caller to delay the rendering of the shortcode if needed. - var fn shortcodeRenderFunc = func(ctx context.Context) ([]byte, bool, error) { - if p.m.pageConfig.ContentMediaType.IsMarkdown() && sc.doMarkup { - // Signal downwards that the content rendered will be - // parsed and rendered by Goldmark. - ctx = tpl.Context.IsInGoldmark.Set(ctx, true) - } - r, err := doRenderShortcode(ctx, level, s, sc, parent, po, isRenderString) - if err != nil { - return nil, false, toParseErr(err) - } - - b, hasVariants, err := r.renderShortcode(ctx) - if err != nil { - return nil, false, toParseErr(err) - } - return b, hasVariants, nil - } - - return fn, nil -} - -func doRenderShortcode( - ctx context.Context, - level int, - s *Site, - sc *shortcode, - parent *ShortcodeWithPage, - po *pageOutput, - isRenderString bool, -) (shortcodeRenderer, error) { - var tmpl *tplimpl.TemplInfo - p := po.p - - // Tracks whether this shortcode or any of its children has template variations - // in other languages or output formats. We are currently only interested in - // the output formats. - var hasVariants bool - - if sc.isInline { - if !p.s.ExecHelper.Sec().EnableInlineShortcodes { - return zeroShortcode, nil - } - templatePath := path.Join("_inline_shortcode", p.Path(), sc.name) - if sc.isClosing { - templStr := sc.innerString() - - var err error - tmpl, err = s.TemplateStore.TextParse(templatePath, templStr) - if err != nil { - if isRenderString { - return zeroShortcode, p.wrapError(err) - } - fe := herrors.NewFileErrorFromName(err, p.File().Filename()) - pos := fe.Position() - pos.LineNumber += p.posOffset(sc.pos).LineNumber - fe = fe.UpdatePosition(pos) - return zeroShortcode, p.wrapError(fe) - } - - } else { - // Re-use of shortcode defined earlier in the same page. - tmpl = s.TemplateStore.TextLookup(templatePath) - if tmpl == nil { - return zeroShortcode, fmt.Errorf("no earlier definition of shortcode %q found", sc.name) - } - } - } else { - ofCount := map[string]int{} - include := func(match *tplimpl.TemplInfo) bool { - ofCount[match.D.OutputFormat]++ - return true - } - base, layoutDescriptor := po.GetInternalTemplateBasePathAndDescriptor() - - // With shortcodes/mymarkdown.md (only), this allows {{% mymarkdown %}} when rendering HTML, - // but will not resolve any template when doing {{< mymarkdown >}}. - layoutDescriptor.AlwaysAllowPlainText = sc.doMarkup - q := tplimpl.TemplateQuery{ - Path: base, - Name: sc.name, - Category: tplimpl.CategoryShortcode, - Desc: layoutDescriptor, - Consider: include, - } - v, err := s.TemplateStore.LookupShortcode(q) - if v == nil { - return zeroShortcode, err - } - tmpl = v - hasVariants = hasVariants || len(ofCount) > 1 - } - - data := &ShortcodeWithPage{ - Ordinal: sc.ordinal, - posOffset: sc.pos, - indentation: sc.indentation, - Params: sc.params, - Page: newPageForShortcode(p), - Parent: parent, - Name: sc.name, + p *Page) string { + + tmpl := getShortcodeTemplateForTemplateKey(tmplKey, sc.name, p.s.Tmpl) + if tmpl == nil { + p.s.Log.ERROR.Printf("Unable to locate template for shortcode %q in page %q", sc.name, p.Path()) + return "" } + data := &ShortcodeWithPage{Params: sc.params, Page: p, Parent: parent} if sc.params != nil { data.IsNamedParams = reflect.TypeOf(sc.params).Kind() == reflect.Map } @@ -434,39 +285,27 @@ func doRenderShortcode( if len(sc.inner) > 0 { var inner string for _, innerData := range sc.inner { - switch innerData := innerData.(type) { + switch innerData.(type) { case string: - inner += innerData - case *shortcode: - s, err := prepareShortcode(ctx, level+1, s, innerData, data, po, isRenderString) - if err != nil { - return zeroShortcode, err - } - ss, more, err := s.renderShortcodeString(ctx) - hasVariants = hasVariants || more - if err != nil { - return zeroShortcode, err - } - inner += ss + inner += innerData.(string) + case shortcode: + inner += renderShortcode(tmplKey, innerData.(shortcode), data, p) default: - s.Log.Errorf("Illegal state on shortcode rendering of %q in page %q. Illegal type in inner data: %s ", - sc.name, p.File().Path(), reflect.TypeOf(innerData)) - return zeroShortcode, nil + p.s.Log.ERROR.Printf("Illegal state on shortcode rendering of %q in page %q. Illegal type in inner data: %s ", + sc.name, p.Path(), reflect.TypeOf(innerData)) + return "" } } - // Pre Hugo 0.55 this was the behavior even for the outer-most - // shortcode. - if sc.doMarkup && (level > 0 || sc.configVersion() == 1) { - var err error - b, err := p.pageOutput.contentRenderer.ParseAndRenderContent(ctx, []byte(inner), false) - if err != nil { - return zeroShortcode, err - } + if sc.doMarkup { + newInner := p.s.ContentSpec.RenderBytes(&helpers.RenderingContext{ + Content: []byte(inner), PageFmt: p.determineMarkupType(), + Cfg: p.Language(), + DocumentID: p.UniqueID(), + DocumentName: p.Path(), + Config: p.getRenderingConfig()}) - newInner := b.Bytes() - - // If the type is “” (unknown) or “markdown”, we assume the markdown + // If the type is “unknown” or “markdown”, we assume the markdown // generation has been performed. Given the input: `a line`, markdown // specifies the HTML `<p>a line</p>\n`. When dealing with documents as a // whole, this is OK. When dealing with an `{{ .Inner }}` block in Hugo, @@ -475,9 +314,12 @@ func doRenderShortcode( // 1. Check to see if inner has a newline in it. If so, the Inner data is // unchanged. // 2 If inner does not have a newline, strip the wrapping <p> block and - // the newline. - switch p.m.pageConfig.Content.Markup { - case "", "markdown": + // the newline. This was previously tricked out by wrapping shortcode + // substitutions in <div>HUGOSHORTCODE-1</div> which prevents the + // generation, but means that you can’t use shortcodes inside of + // markdown structures itself (e.g., `[foo]({{% ref foo.md %}})`). + switch p.determineMarkupType() { + case "unknown", "markdown": if match, _ := regexp.MatchString(innerNewlineRegexp, inner); !match { cleaner, err := regexp.Compile(innerCleanupRegexp) @@ -495,137 +337,128 @@ func doRenderShortcode( } - result, err := renderShortcodeWithPage(ctx, s.GetTemplateStore(), tmpl, data) + return renderShortcodeWithPage(tmpl, data) +} - if err != nil && sc.isInline { - fe := herrors.NewFileErrorFromName(err, p.File().Filename()) - pos := fe.Position() - pos.LineNumber += p.posOffset(sc.pos).LineNumber - fe = fe.UpdatePosition(pos) - return zeroShortcode, fe +// The delta represents new output format-versions of the shortcodes, +// which, combined with the ones that do not have alternative representations, +// builds a complete set ready for a full rebuild of the Page content. +// This method returns false if there are no new shortcode variants in the +// current rendering context's output format. This mean we can safely reuse +// the content from the previous output format, if any. +func (s *shortcodeHandler) updateDelta() bool { + s.init.Do(func() { + s.contentShortcodes = createShortcodeRenderers(s.shortcodes, s.p) + }) + + contentShortcodes := s.contentShortcodesForOutputFormat(s.p.s.rc.Format) + + if s.contentShortcodesDelta == nil || len(s.contentShortcodesDelta) == 0 { + s.contentShortcodesDelta = contentShortcodes + return true } - if len(sc.inner) == 0 && len(sc.indentation) > 0 { - b := bp.GetBuffer() - i := 0 - text.VisitLinesAfter(result, func(line string) { - // The first line is correctly indented. - if i > 0 { - b.WriteString(sc.indentation) - } - i++ - b.WriteString(line) - }) + delta := make(map[scKey]func() (string, error)) - result = b.String() - bp.PutBuffer(b) + for k, v := range contentShortcodes { + if _, found := s.contentShortcodesDelta[k]; !found { + delta[k] = v + } } - return prerenderedShortcode{s: result, hasVariants: hasVariants}, err + s.contentShortcodesDelta = delta + + return len(delta) > 0 } -func (s *shortcodeHandler) addName(name string) { - s.nameSetMu.Lock() - defer s.nameSetMu.Unlock() - s.nameSet[name] = true -} +func (s *shortcodeHandler) contentShortcodesForOutputFormat(f output.Format) map[scKey]func() (string, error) { + contentShortcodesForOuputFormat := make(map[scKey]func() (string, error)) + lang := s.p.Lang() -func (s *shortcodeHandler) transferNames(in *shortcodeHandler) { - s.nameSetMu.Lock() - defer s.nameSetMu.Unlock() - for k := range in.nameSet { - s.nameSet[k] = true + for shortcodePlaceholder := range s.shortcodes { + + key := newScKeyFromLangAndOutputFormat(lang, f, shortcodePlaceholder) + renderFn, found := s.contentShortcodes[key] + + if !found { + key.OutputFormat = "" + renderFn, found = s.contentShortcodes[key] + } + + // Fall back to HTML + if !found && key.Suffix != "html" { + key.Suffix = "html" + renderFn, found = s.contentShortcodes[key] + } + + if !found { + panic(fmt.Sprintf("Shortcode %q could not be found", shortcodePlaceholder)) + } + contentShortcodesForOuputFormat[newScKeyFromLangAndOutputFormat(lang, f, shortcodePlaceholder)] = renderFn } + + return contentShortcodesForOuputFormat } -func (s *shortcodeHandler) hasName(name string) bool { - s.nameSetMu.RLock() - defer s.nameSetMu.RUnlock() - _, ok := s.nameSet[name] - return ok -} +func (s *shortcodeHandler) executeShortcodesForDelta(p *Page) error { -func (s *shortcodeHandler) prepareShortcodesForPage(ctx context.Context, po *pageOutput, isRenderString bool) (map[string]shortcodeRenderer, error) { - rendered := make(map[string]shortcodeRenderer) - - for _, v := range s.shortcodes { - s, err := prepareShortcode(ctx, 0, s.s, v, nil, po, isRenderString) + for k, render := range s.contentShortcodesDelta { + renderedShortcode, err := render() if err != nil { - return nil, err + return fmt.Errorf("Failed to execute shortcode in page %q: %s", p.Path(), err) } - rendered[v.placeholder] = s + s.renderedShortcodes[k.ShortcodePlaceholder] = renderedShortcode } - return rendered, nil + return nil + } -func posFromInput(filename string, input []byte, offset int) text.Position { - if offset < 0 { - return text.Position{ - Filename: filename, +func createShortcodeRenderers(shortcodes map[string]shortcode, p *Page) map[scKey]func() (string, error) { + + shortcodeRenderers := make(map[scKey]func() (string, error)) + + for k, v := range shortcodes { + prepared := prepareShortcodeForPage(k, v, nil, p) + for kk, vv := range prepared { + shortcodeRenderers[kk] = vv } } - lf := []byte("\n") - input = input[:offset] - lineNumber := bytes.Count(input, lf) + 1 - endOfLastLine := bytes.LastIndex(input, lf) - return text.Position{ - Filename: filename, - LineNumber: lineNumber, - ColumnNumber: offset - endOfLastLine, - Offset: offset, - } + return shortcodeRenderers } +var errShortCodeIllegalState = errors.New("Illegal shortcode state") + // pageTokens state: // - before: positioned just before the shortcode start // - after: shortcode(s) consumed (plural when they are nested) -func (s *shortcodeHandler) extractShortcode(ordinal, level int, source []byte, pt *pageparser.Iterator) (*shortcode, error) { - if s == nil { - panic("handler nil") - } - sc := &shortcode{ordinal: ordinal} +func (s *shortcodeHandler) extractShortcode(pt *pageTokens, p *Page) (shortcode, error) { + sc := shortcode{} + var isInner = false - // Back up one to identify any indentation. - if pt.Pos() > 0 { - pt.Backup() - item := pt.Next() - if item.IsIndentation() { - sc.indentation = item.ValStr(source) - } - } - - cnt := 0 - nestedOrdinal := 0 - nextLevel := level + 1 - closed := false - const errorPrefix = "failed to extract shortcode" + var currItem item + var cnt = 0 Loop: for { - currItem := pt.Next() - switch { - case currItem.IsLeftShortcodeDelim(): - next := pt.Peek() - if next.IsRightShortcodeDelim() { - // no name: {{< >}} or {{% %}} - return sc, errors.New("shortcode has no name") - } - if next.IsShortcodeClose() { + currItem = pt.next() + + switch currItem.typ { + case tLeftDelimScWithMarkup, tLeftDelimScNoMarkup: + next := pt.peek() + if next.typ == tScClose { continue } if cnt > 0 { // nested shortcode; append it to inner content - pt.Backup() - nested, err := s.extractShortcode(nestedOrdinal, nextLevel, source, pt) - nestedOrdinal++ - if nested != nil && nested.name != "" { - s.addName(nested.name) + pt.backup3(currItem, next) + nested, err := s.extractShortcode(pt, p) + if nested.name != "" { + s.nameSet[nested.name] = true } - if err == nil { sc.inner = append(sc.inner, nested) } else { @@ -633,100 +466,89 @@ Loop: } } else { - sc.doMarkup = currItem.IsShortcodeMarkupDelimiter() + sc.doMarkup = currItem.typ == tLeftDelimScWithMarkup } cnt++ - case currItem.IsRightShortcodeDelim(): + case tRightDelimScWithMarkup, tRightDelimScNoMarkup: // we trust the template on this: // if there's no inner, we're done - if !sc.isInline { - if !sc.templ.ParseInfo.IsInner { - return sc, nil - } + if !isInner { + return sc, nil } - case currItem.IsShortcodeClose(): - closed = true - next := pt.Peek() - if !sc.isInline { - if !sc.needsInner() { - if next.IsError() { - // return that error, more specific - continue - } - name := sc.name - if name == "" { - name = next.ValStr(source) - } - return nil, fmt.Errorf("%s: shortcode %q does not evaluate .Inner or .InnerDeindent, yet a closing tag was provided", errorPrefix, name) + case tScClose: + next := pt.peek() + if !isInner { + if next.typ == tError { + // return that error, more specific + continue } + return sc, fmt.Errorf("Shortcode '%s' in page '%s' has no .Inner, yet a closing tag was provided", next.val, p.FullFilePath()) } - if next.IsRightShortcodeDelim() { + if next.typ == tRightDelimScWithMarkup || next.typ == tRightDelimScNoMarkup { // self-closing - pt.Consume(1) + pt.consume(1) } else { - sc.isClosing = true - pt.Consume(2) + pt.consume(2) } return sc, nil - case currItem.IsText(): - sc.inner = append(sc.inner, currItem.ValStr(source)) - case currItem.IsShortcodeName(): - - sc.name = currItem.ValStr(source) - - // Used to check if the template expects inner content, - // so just pick one arbitrarily with the same name. - templ := s.s.TemplateStore.LookupShortcodeByName(sc.name) - if templ == nil { - return nil, fmt.Errorf("%s: template for shortcode %q not found", errorPrefix, sc.name) + case tText: + sc.inner = append(sc.inner, currItem.val) + case tScName: + sc.name = currItem.val + // We pick the first template for an arbitrary output format + // if more than one. It is "all inner or no inner". + tmpl := getShortcodeTemplateForTemplateKey(scKey{}, sc.name, p.s.Tmpl) + if tmpl == nil { + return sc, fmt.Errorf("Unable to locate template for shortcode %q in page %q", sc.name, p.Path()) } - sc.templ = templ - case currItem.IsInlineShortcodeName(): - sc.name = currItem.ValStr(source) - sc.isInline = true - case currItem.IsShortcodeParam(): - if !pt.IsValueNext() { + + var err error + isInner, err = isInnerShortcode(tmpl) + if err != nil { + return sc, fmt.Errorf("Failed to handle template for shortcode %q for page %q: %s", sc.name, p.Path(), err) + } + + case tScParam: + if !pt.isValueNext() { continue - } else if pt.Peek().IsShortcodeParamVal() { + } else if pt.peek().typ == tScParamVal { // named params if sc.params == nil { - params := make(map[string]any) - params[currItem.ValStr(source)] = pt.Next().ValTyped(source) + params := make(map[string]string) + params[currItem.val] = pt.next().val sc.params = params } else { - if params, ok := sc.params.(map[string]any); ok { - params[currItem.ValStr(source)] = pt.Next().ValTyped(source) + if params, ok := sc.params.(map[string]string); ok { + params[currItem.val] = pt.next().val } else { - return sc, fmt.Errorf("%s: invalid state: invalid param type %T for shortcode %q, expected a map", errorPrefix, params, sc.name) + return sc, errShortCodeIllegalState } + } } else { // positional params if sc.params == nil { - var params []any - params = append(params, currItem.ValTyped(source)) + var params []string + params = append(params, currItem.val) sc.params = params } else { - if params, ok := sc.params.([]any); ok { - params = append(params, currItem.ValTyped(source)) + if params, ok := sc.params.([]string); ok { + params = append(params, currItem.val) sc.params = params } else { - return sc, fmt.Errorf("%s: invalid state: invalid param type %T for shortcode %q, expected a slice", errorPrefix, params, sc.name) + return sc, errShortCodeIllegalState } + } } - case currItem.IsDone(): - if !currItem.IsError() { - if !closed && sc.needsInner() { - return sc, fmt.Errorf("%s: shortcode %q must be closed or self-closed", errorPrefix, sc.name) - } - } + + case tError, tEOF: // handled by caller - pt.Backup() + pt.backup() break Loop } @@ -734,16 +556,86 @@ Loop: return sc, nil } -// Replace prefixed shortcode tokens with the real content. +func (s *shortcodeHandler) extractShortcodes(stringToParse string, p *Page) (string, error) { + + startIdx := strings.Index(stringToParse, "{{") + + // short cut for docs with no shortcodes + if startIdx < 0 { + return stringToParse, nil + } + + // the parser takes a string; + // since this is an internal API, it could make sense to use the mutable []byte all the way, but + // it seems that the time isn't really spent in the byte copy operations, and the impl. gets a lot cleaner + pt := &pageTokens{lexer: newShortcodeLexer("parse-page", stringToParse, pos(startIdx))} + + id := 1 // incremented id, will be appended onto temp. shortcode placeholders + + result := bp.GetBuffer() + defer bp.PutBuffer(result) + //var result bytes.Buffer + + // the parser is guaranteed to return items in proper order or fail, so … + // … it's safe to keep some "global" state + var currItem item + var currShortcode shortcode + +Loop: + for { + currItem = pt.next() + + switch currItem.typ { + case tText: + result.WriteString(currItem.val) + case tLeftDelimScWithMarkup, tLeftDelimScNoMarkup: + // let extractShortcode handle left delim (will do so recursively) + pt.backup() + + currShortcode, err := s.extractShortcode(pt, p) + + if currShortcode.name != "" { + s.nameSet[currShortcode.name] = true + } + + if err != nil { + return result.String(), err + } + + if currShortcode.params == nil { + currShortcode.params = make([]string, 0) + } + + placeHolder := createShortcodePlaceholder(id) + result.WriteString(placeHolder) + s.shortcodes[placeHolder] = currShortcode + id++ + case tEOF: + break Loop + case tError: + err := fmt.Errorf("%s:%d: %s", + p.FullFilePath(), (p.lineNumRawContentStart() + pt.lexer.lineNum() - 1), currItem) + currShortcode.err = err + return result.String(), err + } + } + + return result.String(), nil + +} + +// Replace prefixed shortcode tokens (HUGOSHORTCODE-1, HUGOSHORTCODE-2) with the real content. // Note: This function will rewrite the input slice. -func expandShortcodeTokens( - ctx context.Context, - source []byte, - tokenHandler func(ctx context.Context, token string) ([]byte, error), -) ([]byte, error) { +func replaceShortcodeTokens(source []byte, prefix string, replacements map[string]string) ([]byte, error) { + + if len(replacements) == 0 { + return source, nil + } + + sourceLen := len(source) start := 0 - pre := []byte(shortcodePlaceholderPrefix) + pre := []byte("HAHA" + prefix) post := []byte("HBHB") pStart := []byte("<p>") pEnd := []byte("</p>") @@ -759,15 +651,12 @@ func expandShortcodeTokens( } end := j + postIdx + 4 - key := string(source[j:end]) - newVal, err := tokenHandler(ctx, key) - if err != nil { - return nil, err - } + + newVal := []byte(replacements[string(source[j:end])]) // Issue #1148: Check for wrapping p-tags <p> if j >= 3 && bytes.Equal(source[j-3:j], pStart) { - if (k+4) < len(source) && bytes.Equal(source[end:end+4], pEnd) { + if (k+4) < sourceLen && bytes.Equal(source[end:end+4], pEnd) { j -= 3 end += 4 } @@ -783,13 +672,56 @@ func expandShortcodeTokens( return source, nil } -func renderShortcodeWithPage(ctx context.Context, h *tplimpl.TemplateStore, tmpl *tplimpl.TemplInfo, data *ShortcodeWithPage) (string, error) { +func getShortcodeTemplateForTemplateKey(key scKey, shortcodeName string, t tpl.TemplateFinder) *tpl.TemplateAdapter { + isInnerShortcodeCache.RLock() + defer isInnerShortcodeCache.RUnlock() + + var names []string + + suffix := strings.ToLower(key.Suffix) + outFormat := strings.ToLower(key.OutputFormat) + lang := strings.ToLower(key.Lang) + + if outFormat != "" && suffix != "" { + if lang != "" { + names = append(names, fmt.Sprintf("%s.%s.%s.%s", shortcodeName, lang, outFormat, suffix)) + } + names = append(names, fmt.Sprintf("%s.%s.%s", shortcodeName, outFormat, suffix)) + } + + if suffix != "" { + if lang != "" { + names = append(names, fmt.Sprintf("%s.%s.%s", shortcodeName, lang, suffix)) + } + names = append(names, fmt.Sprintf("%s.%s", shortcodeName, suffix)) + } + + names = append(names, shortcodeName) + + for _, name := range names { + + if x := t.Lookup("shortcodes/" + name); x != nil { + return x + } + if x := t.Lookup("theme/shortcodes/" + name); x != nil { + return x + } + if x := t.Lookup("_internal/shortcodes/" + name); x != nil { + return x + } + } + return nil +} + +func renderShortcodeWithPage(tmpl tpl.Template, data *ShortcodeWithPage) string { buffer := bp.GetBuffer() defer bp.PutBuffer(buffer) - err := h.ExecuteWithContext(ctx, tmpl, buffer, data) + isInnerShortcodeCache.RLock() + err := tmpl.Execute(buffer, data) + isInnerShortcodeCache.RUnlock() if err != nil { - return "", fmt.Errorf("failed to process shortcode: %w", err) + data.Page.s.Log.ERROR.Printf("error processing shortcode %q for page %q: %s", tmpl.Name(), data.Page.Path(), err) } - return buffer.String(), nil + return buffer.String() } diff --git a/hugolib/shortcode_page.go b/hugolib/shortcode_page.go deleted file mode 100644 index 3d27cc93c..000000000 --- a/hugolib/shortcode_page.go +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright 2019 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 hugolib - -import ( - "context" - "html/template" - - "github.com/gohugoio/hugo/common/types" - "github.com/gohugoio/hugo/resources/page" -) - -// A placeholder for the TableOfContents markup. This is what we pass to the Goldmark etc. renderers. -var tocShortcodePlaceholder = createShortcodePlaceholder("TOC", 0, 0) - -// shortcodeRenderer is typically used to delay rendering of inner shortcodes -// marked with placeholders in the content. -type shortcodeRenderer interface { - renderShortcode(context.Context) ([]byte, bool, error) - renderShortcodeString(context.Context) (string, bool, error) -} - -type shortcodeRenderFunc func(context.Context) ([]byte, bool, error) - -func (f shortcodeRenderFunc) renderShortcode(ctx context.Context) ([]byte, bool, error) { - return f(ctx) -} - -func (f shortcodeRenderFunc) renderShortcodeString(ctx context.Context) (string, bool, error) { - b, has, err := f(ctx) - return string(b), has, err -} - -type prerenderedShortcode struct { - s string - hasVariants bool -} - -func (p prerenderedShortcode) renderShortcode(context.Context) ([]byte, bool, error) { - return []byte(p.s), p.hasVariants, nil -} - -func (p prerenderedShortcode) renderShortcodeString(context.Context) (string, bool, error) { - return p.s, p.hasVariants, nil -} - -var zeroShortcode = prerenderedShortcode{} - -// This is sent to the shortcodes. They cannot access the content -// they're a part of. It would cause an infinite regress. -// -// Go doesn't support virtual methods, so this careful dance is currently (I think) -// the best we can do. -type pageForShortcode struct { - page.PageWithoutContent - page.TableOfContentsProvider - page.MarkupProvider - page.ContentProvider - - // We need to replace it after we have rendered it, so provide a - // temporary placeholder. - toc template.HTML - - p *pageState -} - -var _ types.Unwrapper = (*pageForShortcode)(nil) - -func newPageForShortcode(p *pageState) page.Page { - return &pageForShortcode{ - PageWithoutContent: p, - TableOfContentsProvider: p, - MarkupProvider: page.NopPage, - ContentProvider: page.NopPage, - toc: template.HTML(tocShortcodePlaceholder), - p: p, - } -} - -// For internal use. -func (p *pageForShortcode) Unwrapv() any { - return p.PageWithoutContent.(page.Page) -} - -func (p *pageForShortcode) String() string { - return p.p.String() -} - -func (p *pageForShortcode) TableOfContents(context.Context) template.HTML { - return p.toc -} - -var _ types.Unwrapper = (*pageForRenderHooks)(nil) - -// This is what is sent into the content render hooks (link, image). -type pageForRenderHooks struct { - page.PageWithoutContent - page.TableOfContentsProvider - page.MarkupProvider - page.ContentProvider - p *pageState -} - -func newPageForRenderHook(p *pageState) page.Page { - return &pageForRenderHooks{ - PageWithoutContent: p, - MarkupProvider: page.NopPage, - ContentProvider: page.NopPage, - TableOfContentsProvider: p, - p: p, - } -} - -func (p *pageForRenderHooks) Unwrapv() any { - return p.p -} - -func (p *pageForRenderHooks) String() string { - return p.p.String() -} diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go index a1f12e77a..3d355f947 100644 --- a/hugolib/shortcode_test.go +++ b/hugolib/shortcode_test.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Hugo Authors. All rights reserved. +// Copyright 2016 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,121 +14,572 @@ package hugolib import ( - "context" "fmt" "path/filepath" "reflect" + "regexp" + "sort" "strings" "testing" - "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/resources/kinds" + jww "github.com/spf13/jwalterweatherman" - "github.com/gohugoio/hugo/parser/pageparser" + "github.com/spf13/afero" - qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/output" + + "github.com/gohugoio/hugo/media" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/source" + "github.com/gohugoio/hugo/tpl" + "github.com/stretchr/testify/require" ) -func TestExtractShortcodes(t *testing.T) { - b := newTestSitesBuilder(t).WithSimpleConfigFile() +// TODO(bep) remove +func pageFromString(in, filename string, withTemplate ...func(templ tpl.TemplateHandler) error) (*Page, error) { + s := newTestSite(nil) + if len(withTemplate) > 0 { + // Have to create a new site + var err error + cfg, fs := newTestCfg() - b.WithTemplates( - "pages/single.html", `EMPTY`, - "shortcodes/tag.html", `tag`, - "shortcodes/legacytag.html", `{{ $_hugo_config := "{ \"version\": 1 }" }}tag`, - "shortcodes/sc1.html", `sc1`, - "shortcodes/sc2.html", `sc2`, - "shortcodes/inner.html", `{{with .Inner }}{{ . }}{{ end }}`, - "shortcodes/inner2.html", `{{.Inner}}`, - "shortcodes/inner3.html", `{{.Inner}}`, - ).WithContent("page.md", `--- -title: "Shortcodes Galore!" + d := deps.DepsCfg{Language: helpers.NewLanguage("en", cfg), Cfg: cfg, Fs: fs, WithTemplate: withTemplate[0]} + + s, err = NewSiteForCfg(d) + if err != nil { + return nil, err + } + } + return s.NewPageFrom(strings.NewReader(in), filename) +} + +func CheckShortCodeMatch(t *testing.T, input, expected string, withTemplate func(templ tpl.TemplateHandler) error) { + CheckShortCodeMatchAndError(t, input, expected, withTemplate, false) +} + +func CheckShortCodeMatchAndError(t *testing.T, input, expected string, withTemplate func(templ tpl.TemplateHandler) error, expectError bool) { + + cfg, fs := newTestCfg() + + // Need some front matter, see https://github.com/gohugoio/hugo/issues/2337 + contentFile := `--- +title: "Title" --- -`) +` + input - b.CreateSites().Build(BuildCfg{}) + writeSource(t, fs, "content/simple.md", contentFile) - s := b.H.Sites[0] + h, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg, WithTemplate: withTemplate}) - // Make it more regexp friendly - strReplacer := strings.NewReplacer("[", "{", "]", "}") + require.NoError(t, err) + require.Len(t, h.Sites, 1) - str := func(s *shortcode) string { - if s == nil { - return "<nil>" - } - var version int - if s.templ != nil { - version = s.templ.ParseInfo.Config.Version - } - return strReplacer.Replace(fmt.Sprintf("%s;inline:%t;closing:%t;inner:%v;params:%v;ordinal:%d;markup:%t;version:%d;pos:%d", - s.name, s.isInline, s.isClosing, s.inner, s.params, s.ordinal, s.doMarkup, version, s.pos)) + err = h.Build(BuildCfg{}) + + if err != nil && !expectError { + t.Fatalf("Shortcode rendered error %s.", err) } - regexpCheck := func(re string) func(c *qt.C, shortcode *shortcode, err error) { - return func(c *qt.C, shortcode *shortcode, err error) { - c.Assert(err, qt.IsNil) - c.Assert(str(shortcode), qt.Matches, ".*"+re+".*", qt.Commentf("%s", shortcode.name)) - } + if err == nil && expectError { + t.Fatalf("No error from shortcode") } - for _, test := range []struct { - name string - input string - check func(c *qt.C, shortcode *shortcode, err error) + require.Len(t, h.Sites[0].RegularPages, 1) + + output := strings.TrimSpace(string(h.Sites[0].RegularPages[0].Content)) + output = strings.TrimPrefix(output, "<p>") + output = strings.TrimSuffix(output, "</p>") + + expected = strings.TrimSpace(expected) + + if output != expected { + t.Fatalf("Shortcode render didn't match. got \n%q but expected \n%q", output, expected) + } +} + +func TestNonSC(t *testing.T) { + t.Parallel() + // notice the syntax diff from 0.12, now comment delims must be added + CheckShortCodeMatch(t, "{{%/* movie 47238zzb */%}}", "{{% movie 47238zzb %}}", nil) +} + +// Issue #929 +func TestHyphenatedSC(t *testing.T) { + t.Parallel() + wt := func(tem tpl.TemplateHandler) error { + + tem.AddTemplate("_internal/shortcodes/hyphenated-video.html", `Playing Video {{ .Get 0 }}`) + return nil + } + + CheckShortCodeMatch(t, "{{< hyphenated-video 47238zzb >}}", "Playing Video 47238zzb", wt) +} + +// Issue #1753 +func TestNoTrailingNewline(t *testing.T) { + t.Parallel() + wt := func(tem tpl.TemplateHandler) error { + tem.AddTemplate("_internal/shortcodes/a.html", `{{ .Get 0 }}`) + return nil + } + + CheckShortCodeMatch(t, "ab{{< a c >}}d", "abcd", wt) +} + +func TestPositionalParamSC(t *testing.T) { + t.Parallel() + wt := func(tem tpl.TemplateHandler) error { + tem.AddTemplate("_internal/shortcodes/video.html", `Playing Video {{ .Get 0 }}`) + return nil + } + + CheckShortCodeMatch(t, "{{< video 47238zzb >}}", "Playing Video 47238zzb", wt) + CheckShortCodeMatch(t, "{{< video 47238zzb 132 >}}", "Playing Video 47238zzb", wt) + CheckShortCodeMatch(t, "{{<video 47238zzb>}}", "Playing Video 47238zzb", wt) + CheckShortCodeMatch(t, "{{<video 47238zzb >}}", "Playing Video 47238zzb", wt) + CheckShortCodeMatch(t, "{{< video 47238zzb >}}", "Playing Video 47238zzb", wt) +} + +func TestPositionalParamIndexOutOfBounds(t *testing.T) { + t.Parallel() + wt := func(tem tpl.TemplateHandler) error { + tem.AddTemplate("_internal/shortcodes/video.html", `Playing Video {{ .Get 1 }}`) + return nil + } + CheckShortCodeMatch(t, "{{< video 47238zzb >}}", "Playing Video error: index out of range for positional param at position 1", wt) +} + +// some repro issues for panics in Go Fuzz testing + +func TestNamedParamSC(t *testing.T) { + t.Parallel() + wt := func(tem tpl.TemplateHandler) error { + tem.AddTemplate("_internal/shortcodes/img.html", `<img{{ with .Get "src" }} src="{{.}}"{{end}}{{with .Get "class"}} class="{{.}}"{{end}}>`) + return nil + } + CheckShortCodeMatch(t, `{{< img src="one" >}}`, `<img src="one">`, wt) + CheckShortCodeMatch(t, `{{< img class="aspen" >}}`, `<img class="aspen">`, wt) + CheckShortCodeMatch(t, `{{< img src= "one" >}}`, `<img src="one">`, wt) + CheckShortCodeMatch(t, `{{< img src ="one" >}}`, `<img src="one">`, wt) + CheckShortCodeMatch(t, `{{< img src = "one" >}}`, `<img src="one">`, wt) + CheckShortCodeMatch(t, `{{< img src = "one" class = "aspen grove" >}}`, `<img src="one" class="aspen grove">`, wt) +} + +// Issue #2294 +func TestNestedNamedMissingParam(t *testing.T) { + t.Parallel() + wt := func(tem tpl.TemplateHandler) error { + tem.AddTemplate("_internal/shortcodes/acc.html", `<div class="acc">{{ .Inner }}</div>`) + tem.AddTemplate("_internal/shortcodes/div.html", `<div {{with .Get "class"}} class="{{ . }}"{{ end }}>{{ .Inner }}</div>`) + tem.AddTemplate("_internal/shortcodes/div2.html", `<div {{with .Get 0}} class="{{ . }}"{{ end }}>{{ .Inner }}</div>`) + return nil + } + CheckShortCodeMatch(t, + `{{% acc %}}{{% div %}}d1{{% /div %}}{{% div2 %}}d2{{% /div2 %}}{{% /acc %}}`, + "<div class=\"acc\"><div >d1</div><div >d2</div>\n</div>", wt) +} + +func TestIsNamedParamsSC(t *testing.T) { + t.Parallel() + wt := func(tem tpl.TemplateHandler) error { + tem.AddTemplate("_internal/shortcodes/byposition.html", `<div id="{{ .Get 0 }}">`) + tem.AddTemplate("_internal/shortcodes/byname.html", `<div id="{{ .Get "id" }}">`) + tem.AddTemplate("_internal/shortcodes/ifnamedparams.html", `<div id="{{ if .IsNamedParams }}{{ .Get "id" }}{{ else }}{{ .Get 0 }}{{end}}">`) + return nil + } + CheckShortCodeMatch(t, `{{< ifnamedparams id="name" >}}`, `<div id="name">`, wt) + CheckShortCodeMatch(t, `{{< ifnamedparams position >}}`, `<div id="position">`, wt) + CheckShortCodeMatch(t, `{{< byname id="name" >}}`, `<div id="name">`, wt) + CheckShortCodeMatch(t, `{{< byname position >}}`, `<div id="error: cannot access positional params by string name">`, wt) + CheckShortCodeMatch(t, `{{< byposition position >}}`, `<div id="position">`, wt) + CheckShortCodeMatch(t, `{{< byposition id="name" >}}`, `<div id="error: cannot access named params by position">`, wt) +} + +func TestInnerSC(t *testing.T) { + t.Parallel() + wt := func(tem tpl.TemplateHandler) error { + tem.AddTemplate("_internal/shortcodes/inside.html", `<div{{with .Get "class"}} class="{{.}}"{{end}}>{{ .Inner }}</div>`) + return nil + } + CheckShortCodeMatch(t, `{{< inside class="aspen" >}}`, `<div class="aspen"></div>`, wt) + CheckShortCodeMatch(t, `{{< inside class="aspen" >}}More Here{{< /inside >}}`, "<div class=\"aspen\">More Here</div>", wt) + CheckShortCodeMatch(t, `{{< inside >}}More Here{{< /inside >}}`, "<div>More Here</div>", wt) +} + +func TestInnerSCWithMarkdown(t *testing.T) { + t.Parallel() + wt := func(tem tpl.TemplateHandler) error { + tem.AddTemplate("_internal/shortcodes/inside.html", `<div{{with .Get "class"}} class="{{.}}"{{end}}>{{ .Inner }}</div>`) + return nil + } + CheckShortCodeMatch(t, `{{% inside %}} +# More Here + +[link](http://spf13.com) and text + +{{% /inside %}}`, "<div><h1 id=\"more-here\">More Here</h1>\n\n<p><a href=\"http://spf13.com\">link</a> and text</p>\n</div>", wt) +} + +func TestInnerSCWithAndWithoutMarkdown(t *testing.T) { + t.Parallel() + wt := func(tem tpl.TemplateHandler) error { + tem.AddTemplate("_internal/shortcodes/inside.html", `<div{{with .Get "class"}} class="{{.}}"{{end}}>{{ .Inner }}</div>`) + return nil + } + CheckShortCodeMatch(t, `{{% inside %}} +# More Here + +[link](http://spf13.com) and text + +{{% /inside %}} + +And then: + +{{< inside >}} +# More Here + +This is **plain** text. + +{{< /inside >}} +`, "<div><h1 id=\"more-here\">More Here</h1>\n\n<p><a href=\"http://spf13.com\">link</a> and text</p>\n</div>\n\n<p>And then:</p>\n\n<p><div>\n# More Here\n\nThis is **plain** text.\n\n</div>", wt) +} + +func TestEmbeddedSC(t *testing.T) { + t.Parallel() + CheckShortCodeMatch(t, "{{% test %}}", "This is a simple Test", nil) + CheckShortCodeMatch(t, `{{% figure src="/found/here" class="bananas orange" %}}`, "\n<figure class=\"bananas orange\">\n \n <img src=\"/found/here\" />\n \n \n</figure>\n", nil) + CheckShortCodeMatch(t, `{{% figure src="/found/here" class="bananas orange" caption="This is a caption" %}}`, "\n<figure class=\"bananas orange\">\n \n <img src=\"/found/here\" alt=\"This is a caption\" />\n \n \n <figcaption>\n <p>\n This is a caption\n \n \n \n </p> \n </figcaption>\n \n</figure>\n", nil) +} + +func TestNestedSC(t *testing.T) { + t.Parallel() + wt := func(tem tpl.TemplateHandler) error { + tem.AddTemplate("_internal/shortcodes/scn1.html", `<div>Outer, inner is {{ .Inner }}</div>`) + tem.AddTemplate("_internal/shortcodes/scn2.html", `<div>SC2</div>`) + return nil + } + CheckShortCodeMatch(t, `{{% scn1 %}}{{% scn2 %}}{{% /scn1 %}}`, "<div>Outer, inner is <div>SC2</div>\n</div>", wt) + + CheckShortCodeMatch(t, `{{< scn1 >}}{{% scn2 %}}{{< /scn1 >}}`, "<div>Outer, inner is <div>SC2</div></div>", wt) +} + +func TestNestedComplexSC(t *testing.T) { + t.Parallel() + wt := func(tem tpl.TemplateHandler) error { + tem.AddTemplate("_internal/shortcodes/row.html", `-row-{{ .Inner}}-rowStop-`) + tem.AddTemplate("_internal/shortcodes/column.html", `-col-{{.Inner }}-colStop-`) + tem.AddTemplate("_internal/shortcodes/aside.html", `-aside-{{ .Inner }}-asideStop-`) + return nil + } + CheckShortCodeMatch(t, `{{< row >}}1-s{{% column %}}2-**s**{{< aside >}}3-**s**{{< /aside >}}4-s{{% /column %}}5-s{{< /row >}}6-s`, + "-row-1-s-col-2-<strong>s</strong>-aside-3-<strong>s</strong>-asideStop-4-s-colStop-5-s-rowStop-6-s", wt) + + // turn around the markup flag + CheckShortCodeMatch(t, `{{% row %}}1-s{{< column >}}2-**s**{{% aside %}}3-**s**{{% /aside %}}4-s{{< /column >}}5-s{{% /row %}}6-s`, + "-row-1-s-col-2-<strong>s</strong>-aside-3-<strong>s</strong>-asideStop-4-s-colStop-5-s-rowStop-6-s", wt) +} + +func TestParentShortcode(t *testing.T) { + t.Parallel() + wt := func(tem tpl.TemplateHandler) error { + tem.AddTemplate("_internal/shortcodes/r1.html", `1: {{ .Get "pr1" }} {{ .Inner }}`) + tem.AddTemplate("_internal/shortcodes/r2.html", `2: {{ .Parent.Get "pr1" }}{{ .Get "pr2" }} {{ .Inner }}`) + tem.AddTemplate("_internal/shortcodes/r3.html", `3: {{ .Parent.Parent.Get "pr1" }}{{ .Parent.Get "pr2" }}{{ .Get "pr3" }} {{ .Inner }}`) + return nil + } + CheckShortCodeMatch(t, `{{< r1 pr1="p1" >}}1: {{< r2 pr2="p2" >}}2: {{< r3 pr3="p3" >}}{{< /r3 >}}{{< /r2 >}}{{< /r1 >}}`, + "1: p1 1: 2: p1p2 2: 3: p1p2p3 ", wt) + +} + +func TestFigureImgWidth(t *testing.T) { + t.Parallel() + CheckShortCodeMatch(t, `{{% figure src="/found/here" class="bananas orange" alt="apple" width="100px" %}}`, "\n<figure class=\"bananas orange\">\n \n <img src=\"/found/here\" alt=\"apple\" width=\"100px\" />\n \n \n</figure>\n", nil) +} + +const testScPlaceholderRegexp = "HAHAHUGOSHORTCODE-\\d+HBHB" + +func TestExtractShortcodes(t *testing.T) { + t.Parallel() + for i, this := range []struct { + name string + input string + expectShortCodes string + expect interface{} + expectErrorMsg string }{ - {"one shortcode, no markup", "{{< tag >}}", regexpCheck("tag.*closing:false.*markup:false")}, - {"one shortcode, markup", "{{% tag %}}", regexpCheck("tag.*closing:false.*markup:true;version:2")}, - {"one shortcode, markup, legacy", "{{% legacytag %}}", regexpCheck("tag.*closing:false.*markup:true;version:1")}, - {"outer shortcode markup", "{{% inner %}}{{< tag >}}{{% /inner %}}", regexpCheck("inner.*closing:true.*markup:true")}, - {"inner shortcode markup", "{{< inner >}}{{% tag %}}{{< /inner >}}", regexpCheck("inner.*closing:true.*;markup:false;version:2")}, - {"one pos param", "{{% tag param1 %}}", regexpCheck("tag.*params:{param1}")}, - {"two pos params", "{{< tag param1 param2>}}", regexpCheck("tag.*params:{param1 param2}")}, - {"one named param", `{{% tag param1="value" %}}`, regexpCheck("tag.*params:map{param1:value}")}, - {"two named params", `{{< tag param1="value1" param2="value2" >}}`, regexpCheck("tag.*params:map{param\\d:value\\d param\\d:value\\d}")}, - {"inner", `{{< inner >}}Inner Content{{< / inner >}}`, regexpCheck("inner;inline:false;closing:true;inner:{Inner Content};")}, + {"text", "Some text.", "map[]", "Some text.", ""}, + {"invalid right delim", "{{< tag }}", "", false, "simple.md:4:.*unrecognized character.*}"}, + {"invalid close", "\n{{< /tag >}}", "", false, "simple.md:5:.*got closing shortcode, but none is open"}, + {"invalid close2", "\n\n{{< tag >}}{{< /anotherTag >}}", "", false, "simple.md:6: closing tag for shortcode 'anotherTag' does not match start tag"}, + {"unterminated quote 1", `{{< figure src="im caption="S" >}}`, "", false, "simple.md:4:.got pos.*"}, + {"unterminated quote 1", `{{< figure src="im" caption="S >}}`, "", false, "simple.md:4:.*unterm.*}"}, + {"one shortcode, no markup", "{{< tag >}}", "", testScPlaceholderRegexp, ""}, + {"one shortcode, markup", "{{% tag %}}", "", testScPlaceholderRegexp, ""}, + {"one pos param", "{{% tag param1 %}}", `tag([\"param1\"], true){[]}"]`, testScPlaceholderRegexp, ""}, + {"two pos params", "{{< tag param1 param2>}}", `tag([\"param1\" \"param2\"], false){[]}"]`, testScPlaceholderRegexp, ""}, + {"one named param", `{{% tag param1="value" %}}`, `tag([\"param1:value\"], true){[]}`, testScPlaceholderRegexp, ""}, + {"two named params", `{{< tag param1="value1" param2="value2" >}}`, `tag([\"param1:value1\" \"param2:value2\"], false){[]}"]`, + testScPlaceholderRegexp, ""}, + {"inner", `Some text. {{< inner >}}Inner Content{{< / inner >}}. Some more text.`, `inner([], false){[Inner Content]}`, + fmt.Sprintf("Some text. %s. Some more text.", testScPlaceholderRegexp), ""}, // issue #934 - {"inner self-closing", `{{< inner />}}`, regexpCheck("inner;.*inner:{}")}, - { - "nested inner", `{{< inner >}}Inner Content->{{% inner2 param1 %}}inner2txt{{% /inner2 %}}Inner close->{{< / inner >}}`, - regexpCheck("inner;.*inner:{Inner Content->.*Inner close->}"), - }, - { - "nested, nested inner", `{{< inner >}}inner2->{{% inner2 param1 %}}inner2txt->inner3{{< inner3>}}inner3txt{{</ inner3 >}}{{% /inner2 %}}final close->{{< / inner >}}`, - regexpCheck("inner:{inner2-> inner2.*{{inner2txt->inner3.*final close->}"), - }, - {"closed without content", `{{< inner param1 >}}{{< / inner >}}`, regexpCheck("inner.*inner:{}")}, - {"inline", `{{< my.inline >}}Hi{{< /my.inline >}}`, regexpCheck("my.inline;inline:true;closing:true;inner:{Hi};")}, + {"inner self-closing", `Some text. {{< inner />}}. Some more text.`, `inner([], false){[]}`, + fmt.Sprintf("Some text. %s. Some more text.", testScPlaceholderRegexp), ""}, + {"close, but not inner", "{{< tag >}}foo{{< /tag >}}", "", false, "Shortcode 'tag' in page 'simple.md' has no .Inner.*"}, + {"nested inner", `Inner->{{< inner >}}Inner Content->{{% inner2 param1 %}}inner2txt{{% /inner2 %}}Inner close->{{< / inner >}}<-done`, + `inner([], false){[Inner Content-> inner2([\"param1\"], true){[inner2txt]} Inner close->]}`, + fmt.Sprintf("Inner->%s<-done", testScPlaceholderRegexp), ""}, + {"nested, nested inner", `Inner->{{< inner >}}inner2->{{% inner2 param1 %}}inner2txt->inner3{{< inner3>}}inner3txt{{</ inner3 >}}{{% /inner2 %}}final close->{{< / inner >}}<-done`, + `inner([], false){[inner2-> inner2([\"param1\"], true){[inner2txt->inner3 inner3(%!q(<nil>), false){[inner3txt]}]} final close->`, + fmt.Sprintf("Inner->%s<-done", testScPlaceholderRegexp), ""}, + {"two inner", `Some text. {{% inner %}}First **Inner** Content{{% / inner %}} {{< inner >}}Inner **Content**{{< / inner >}}. Some more text.`, + `map["HAHAHUGOSHORTCODE-1HBHB:inner([], true){[First **Inner** Content]}" "HAHAHUGOSHORTCODE-2HBHB:inner([], false){[Inner **Content**]}"]`, + fmt.Sprintf("Some text. %s %s. Some more text.", testScPlaceholderRegexp, testScPlaceholderRegexp), ""}, + {"closed without content", `Some text. {{< inner param1 >}}{{< / inner >}}. Some more text.`, `inner([\"param1\"], false){[]}`, + fmt.Sprintf("Some text. %s. Some more text.", testScPlaceholderRegexp), ""}, + {"two shortcodes", "{{< sc1 >}}{{< sc2 >}}", + `map["HAHAHUGOSHORTCODE-1HBHB:sc1([], false){[]}" "HAHAHUGOSHORTCODE-2HBHB:sc2([], false){[]}"]`, + testScPlaceholderRegexp + testScPlaceholderRegexp, ""}, + {"mix of shortcodes", `Hello {{< sc1 >}}world{{% sc2 p2="2"%}}. And that's it.`, + `map["HAHAHUGOSHORTCODE-1HBHB:sc1([], false){[]}" "HAHAHUGOSHORTCODE-2HBHB:sc2([\"p2:2\"]`, + fmt.Sprintf("Hello %sworld%s. And that's it.", testScPlaceholderRegexp, testScPlaceholderRegexp), ""}, + {"mix with inner", `Hello {{< sc1 >}}world{{% inner p2="2"%}}Inner{{%/ inner %}}. And that's it.`, + `map["HAHAHUGOSHORTCODE-1HBHB:sc1([], false){[]}" "HAHAHUGOSHORTCODE-2HBHB:inner([\"p2:2\"], true){[Inner]}"]`, + fmt.Sprintf("Hello %sworld%s. And that's it.", testScPlaceholderRegexp, testScPlaceholderRegexp), ""}, } { - test := test - - t.Run(test.name, func(t *testing.T) { - t.Parallel() - c := qt.New(t) - - p, err := pageparser.ParseMain(strings.NewReader(test.input), pageparser.Config{}) - c.Assert(err, qt.IsNil) - handler := newShortcodeHandler("", s) - iter := p.Iterator() - - short, err := handler.extractShortcode(0, 0, p.Input(), iter) - - test.check(c, short, err) + p, _ := pageFromString(simplePage, "simple.md", func(templ tpl.TemplateHandler) error { + templ.AddTemplate("_internal/shortcodes/tag.html", `tag`) + templ.AddTemplate("_internal/shortcodes/sc1.html", `sc1`) + templ.AddTemplate("_internal/shortcodes/sc2.html", `sc2`) + templ.AddTemplate("_internal/shortcodes/inner.html", `{{with .Inner }}{{ . }}{{ end }}`) + templ.AddTemplate("_internal/shortcodes/inner2.html", `{{.Inner}}`) + templ.AddTemplate("_internal/shortcodes/inner3.html", `{{.Inner}}`) + return nil }) + + s := newShortcodeHandler(p) + content, err := s.extractShortcodes(this.input, p) + + if b, ok := this.expect.(bool); ok && !b { + if err == nil { + t.Fatalf("[%d] %s: ExtractShortcodes didn't return an expected error", i, this.name) + } else { + r, _ := regexp.Compile(this.expectErrorMsg) + if !r.MatchString(err.Error()) { + t.Fatalf("[%d] %s: ExtractShortcodes didn't return an expected error message, got %s but expected %s", + i, this.name, err.Error(), this.expectErrorMsg) + } + } + continue + } else { + if err != nil { + t.Fatalf("[%d] %s: failed: %q", i, this.name, err) + } + } + + shortCodes := s.shortcodes + + var expected string + av := reflect.ValueOf(this.expect) + switch av.Kind() { + case reflect.String: + expected = av.String() + } + + r, err := regexp.Compile(expected) + + if err != nil { + t.Fatalf("[%d] %s: Failed to compile regexp %q: %q", i, this.name, expected, err) + } + + if strings.Count(content, shortcodePlaceholderPrefix) != len(shortCodes) { + t.Fatalf("[%d] %s: Not enough placeholders, found %d", i, this.name, len(shortCodes)) + } + + if !r.MatchString(content) { + t.Fatalf("[%d] %s: Shortcode extract didn't match. got %q but expected %q", i, this.name, content, expected) + } + + for placeHolder, sc := range shortCodes { + if !strings.Contains(content, placeHolder) { + t.Fatalf("[%d] %s: Output does not contain placeholder %q", i, this.name, placeHolder) + } + + if sc.params == nil { + t.Fatalf("[%d] %s: Params is nil for shortcode '%s'", i, this.name, sc.name) + } + } + + if this.expectShortCodes != "" { + shortCodesAsStr := fmt.Sprintf("map%q", collectAndSortShortcodes(shortCodes)) + if !strings.Contains(shortCodesAsStr, this.expectShortCodes) { + t.Fatalf("[%d] %s: Shortcodes not as expected, got %s but expected %s", i, this.name, shortCodesAsStr, this.expectShortCodes) + } + } } } +func TestShortcodesInSite(t *testing.T) { + t.Parallel() + baseURL := "http://foo/bar" + + tests := []struct { + contentPath string + content string + outFile string + expected string + }{ + {"sect/doc1.md", `a{{< b >}}c`, + filepath.FromSlash("public/sect/doc1/index.html"), "<p>abc</p>\n"}, + // Issue #1642: Multiple shortcodes wrapped in P + // Deliberately forced to pass even if they maybe shouldn't. + {"sect/doc2.md", `a + +{{< b >}} +{{< c >}} +{{< d >}} + +e`, + filepath.FromSlash("public/sect/doc2/index.html"), + "<p>a</p>\n\n<p>b<br />\nc\nd</p>\n\n<p>e</p>\n"}, + {"sect/doc3.md", `a + +{{< b >}} +{{< c >}} + +{{< d >}} + +e`, + filepath.FromSlash("public/sect/doc3/index.html"), + "<p>a</p>\n\n<p>b<br />\nc</p>\n\nd\n\n<p>e</p>\n"}, + {"sect/doc4.md", `a +{{< b >}} +{{< b >}} +{{< b >}} +{{< b >}} +{{< b >}} + + + + + + + + + + +`, + filepath.FromSlash("public/sect/doc4/index.html"), + "<p>a\nb\nb\nb\nb\nb</p>\n"}, + // #2192 #2209: Shortcodes in markdown headers + {"sect/doc5.md", `# {{< b >}} +## {{% c %}}`, + filepath.FromSlash("public/sect/doc5/index.html"), "\n\n<h1 id=\"hahahugoshortcode-1hbhb\">b</h1>\n\n<h2 id=\"hahahugoshortcode-2hbhb\">c</h2>\n"}, + // #2223 pygments + {"sect/doc6.md", "\n```bash\nb: {{< b >}} c: {{% c %}}\n```\n", + filepath.FromSlash("public/sect/doc6/index.html"), + "b: b c: c\n</code></pre></div>\n"}, + // #2249 + {"sect/doc7.ad", `_Shortcodes:_ *b: {{< b >}} c: {{% c %}}*`, + filepath.FromSlash("public/sect/doc7/index.html"), + "<div class=\"paragraph\">\n<p><em>Shortcodes:</em> <strong>b: b c: c</strong></p>\n</div>\n"}, + {"sect/doc8.rst", `**Shortcodes:** *b: {{< b >}} c: {{% c %}}*`, + filepath.FromSlash("public/sect/doc8/index.html"), + "<div class=\"document\">\n\n\n<p><strong>Shortcodes:</strong> <em>b: b c: c</em></p>\n</div>"}, + {"sect/doc9.mmark", ` +--- +menu: + main: + parent: 'parent' +--- +**Shortcodes:** *b: {{< b >}} c: {{% c %}}*`, + filepath.FromSlash("public/sect/doc9/index.html"), + "<p><strong>Shortcodes:</strong> <em>b: b c: c</em></p>\n"}, + // Issue #1229: Menus not available in shortcode. + {"sect/doc10.md", `--- +menu: + main: + identifier: 'parent' +tags: +- Menu +--- +**Menus:** {{< menu >}}`, + filepath.FromSlash("public/sect/doc10/index.html"), + "<p><strong>Menus:</strong> 1</p>\n"}, + // Issue #2323: Taxonomies not available in shortcode. + {"sect/doc11.md", `--- +tags: +- Bugs +--- +**Tags:** {{< tags >}}`, + filepath.FromSlash("public/sect/doc11/index.html"), + "<p><strong>Tags:</strong> 2</p>\n"}, + } + + sources := make([]source.ByteSource, len(tests)) + + for i, test := range tests { + sources[i] = source.ByteSource{Name: filepath.FromSlash(test.contentPath), Content: []byte(test.content)} + } + + addTemplates := func(templ tpl.TemplateHandler) error { + templ.AddTemplate("_default/single.html", "{{.Content}}") + + templ.AddTemplate("_internal/shortcodes/b.html", `b`) + templ.AddTemplate("_internal/shortcodes/c.html", `c`) + templ.AddTemplate("_internal/shortcodes/d.html", `d`) + templ.AddTemplate("_internal/shortcodes/menu.html", `{{ len (index .Page.Menus "main").Children }}`) + templ.AddTemplate("_internal/shortcodes/tags.html", `{{ len .Page.Site.Taxonomies.tags }}`) + + return nil + + } + + cfg, fs := newTestCfg() + + cfg.Set("defaultContentLanguage", "en") + cfg.Set("baseURL", baseURL) + cfg.Set("uglyURLs", false) + cfg.Set("verbose", true) + + cfg.Set("pygmentsUseClasses", true) + cfg.Set("pygmentsCodefences", true) + + writeSourcesToSource(t, "content", fs, sources...) + + s := buildSingleSite(t, deps.DepsCfg{WithTemplate: addTemplates, Fs: fs, Cfg: cfg}, BuildCfg{}) + th := testHelper{s.Cfg, s.Fs, t} + + for _, test := range tests { + if strings.HasSuffix(test.contentPath, ".ad") && !helpers.HasAsciidoc() { + fmt.Println("Skip Asciidoc test case as no Asciidoc present.") + continue + } else if strings.HasSuffix(test.contentPath, ".rst") && !helpers.HasRst() { + fmt.Println("Skip Rst test case as no rst2html present.") + continue + } else if strings.Contains(test.expected, "code") && !helpers.HasPygments() { + fmt.Println("Skip Pygments test case as no pygments present.") + continue + } + + th.assertFileContent(test.outFile, test.expected) + } + +} + func TestShortcodeMultipleOutputFormats(t *testing.T) { t.Parallel() siteConfig := ` baseURL = "http://example.com/blog" -disableKinds = ["section", "term", "taxonomy", "RSS", "sitemap", "robotsTXT", "404"] +paginate = 1 -[pagination] -pagerSize = 1 +disableKinds = ["section", "taxonomy", "taxonomyTerm", "RSS", "sitemap", "robotsTXT", "404"] [outputs] home = [ "HTML", "AMP", "Calendar" ] @@ -158,8 +609,18 @@ outputs: ["CSV"] CSV: {{< myShort >}} ` - b := newTestSitesBuilder(t).WithConfigFile("toml", siteConfig) - b.WithTemplates( + pageTemplateShortcodeNotFound := `--- +title: "%s" +outputs: ["CSV"] +--- +# Doc + +NotFound: {{< thisDoesNotExist >}} +` + + mf := afero.NewMemMapFs() + + th, h := newTestSitesFromConfig(t, mf, siteConfig, "layouts/_default/single.html", `Single HTML: {{ .Title }}|{{ .Content }}`, "layouts/_default/single.json", `Single JSON: {{ .Title }}|{{ .Content }}`, "layouts/_default/single.csv", `Single CSV: {{ .Title }}|{{ .Content }}`, @@ -176,21 +637,22 @@ CSV: {{< myShort >}} "layouts/shortcodes/myInner.html", `myInner:--{{- .Inner -}}--`, ) - b.WithContent("_index.md", fmt.Sprintf(pageTemplate, "Home"), - "sect/mypage.md", fmt.Sprintf(pageTemplate, "Single"), - "sect/mycsvpage.md", fmt.Sprintf(pageTemplateCSVOnly, "Single CSV"), - ) + fs := th.Fs - b.Build(BuildCfg{}) - h := b.H - b.Assert(len(h.Sites), qt.Equals, 1) + writeSource(t, fs, "content/_index.md", fmt.Sprintf(pageTemplate, "Home")) + writeSource(t, fs, "content/sect/mypage.md", fmt.Sprintf(pageTemplate, "Single")) + writeSource(t, fs, "content/sect/mycsvpage.md", fmt.Sprintf(pageTemplateCSVOnly, "Single CSV")) + writeSource(t, fs, "content/sect/notfound.md", fmt.Sprintf(pageTemplateShortcodeNotFound, "Single CSV")) + + require.NoError(t, h.Build(BuildCfg{})) + require.Len(t, h.Sites, 1) s := h.Sites[0] - home := s.getPageOldVersion(kinds.KindHome) - b.Assert(home, qt.Not(qt.IsNil)) - b.Assert(len(home.OutputFormats()), qt.Equals, 3) + home := s.getPage(KindHome) + require.NotNil(t, home) + require.Len(t, home.outputFormats, 3) - b.AssertFileContent("public/index.html", + th.assertFileContent("public/index.html", "Home HTML", "ShortHTML", "ShortNoExt", @@ -198,7 +660,7 @@ CSV: {{< myShort >}} "myInner:--ShortHTML--", ) - b.AssertFileContent("public/amp/index.html", + th.assertFileContent("public/amp/index.html", "Home AMP", "ShortAMP", "ShortNoExt", @@ -206,7 +668,7 @@ CSV: {{< myShort >}} "myInner:--ShortAMP--", ) - b.AssertFileContent("public/index.ics", + th.assertFileContent("public/index.ics", "Home Calendar", "ShortCalendar", "ShortNoExt", @@ -214,7 +676,7 @@ CSV: {{< myShort >}} "myInner:--ShortCalendar--", ) - b.AssertFileContent("public/sect/mypage/index.html", + th.assertFileContent("public/sect/mypage/index.html", "Single HTML", "ShortHTML", "ShortNoExt", @@ -222,7 +684,7 @@ CSV: {{< myShort >}} "myInner:--ShortHTML--", ) - b.AssertFileContent("public/sect/mypage/index.json", + th.assertFileContent("public/sect/mypage/index.json", "Single JSON", "ShortJSON", "ShortNoExt", @@ -230,7 +692,7 @@ CSV: {{< myShort >}} "myInner:--ShortJSON--", ) - b.AssertFileContent("public/amp/sect/mypage/index.html", + th.assertFileContent("public/amp/sect/mypage/index.html", // No special AMP template "Single HTML", "ShortAMP", @@ -239,16 +701,37 @@ CSV: {{< myShort >}} "myInner:--ShortAMP--", ) - b.AssertFileContent("public/sect/mycsvpage/index.csv", + th.assertFileContent("public/sect/mycsvpage/index.csv", "Single CSV", "ShortCSV", ) + + th.assertFileContent("public/sect/notfound/index.csv", + "NotFound:", + "thisDoesNotExist", + ) + + require.Equal(t, uint64(1), s.Log.LogCountForLevel(jww.LevelError)) + +} + +func collectAndSortShortcodes(shortcodes map[string]shortcode) []string { + var asArray []string + + for key, sc := range shortcodes { + asArray = append(asArray, fmt.Sprintf("%s:%s", key, sc)) + } + + sort.Strings(asArray) + return asArray + } func BenchmarkReplaceShortcodeTokens(b *testing.B) { + type input struct { in []byte - tokenHandler func(ctx context.Context, token string) ([]byte, error) + replacements map[string]string expect []byte } @@ -264,30 +747,23 @@ func BenchmarkReplaceShortcodeTokens(b *testing.B) { {strings.Repeat("A ", 3000) + " HAHAHUGOSHORTCODE-1HBHB." + strings.Repeat("BC ", 1000) + " HAHAHUGOSHORTCODE-1HBHB.", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "Hello World"}, []byte(strings.Repeat("A ", 3000) + " Hello World." + strings.Repeat("BC ", 1000) + " Hello World.")}, } - cnt := 0 - in := make([]input, b.N*len(data)) + var in = make([]input, b.N*len(data)) + var cnt = 0 for i := 0; i < b.N; i++ { for _, this := range data { - replacements := make(map[string]shortcodeRenderer) - for k, v := range this.replacements { - replacements[k] = prerenderedShortcode{s: v} - } - tokenHandler := func(ctx context.Context, token string) ([]byte, error) { - return []byte(this.replacements[token]), nil - } - in[cnt] = input{[]byte(this.input), tokenHandler, this.expect} + in[cnt] = input{[]byte(this.input), this.replacements, this.expect} cnt++ } } b.ResetTimer() cnt = 0 - ctx := context.Background() for i := 0; i < b.N; i++ { for j := range data { currIn := in[cnt] cnt++ - results, err := expandShortcodeTokens(ctx, currIn.in, currIn.tokenHandler) + results, err := replaceShortcodeTokens(currIn.in, "HUGOSHORTCODE", currIn.replacements) + if err != nil { b.Fatalf("[%d] failed: %s", i, err) continue @@ -297,58 +773,7 @@ func BenchmarkReplaceShortcodeTokens(b *testing.B) { } } - } -} -func BenchmarkShortcodesInSite(b *testing.B) { - files := ` --- config.toml -- --- layouts/shortcodes/mark1.md -- -{{ .Inner }} --- layouts/shortcodes/mark2.md -- -1. Item Mark2 1 -1. Item Mark2 2 - 1. Item Mark2 2-1 -1. Item Mark2 3 --- layouts/_default/single.html -- -{{ .Content }} -` - - content := ` ---- -title: "Markdown Shortcode" ---- - -## List - -1. List 1 - {{§ mark1 §}} - 1. Item Mark1 1 - 1. Item Mark1 2 - {{§ mark2 §}} - {{§ /mark1 §}} - -` - - for i := 1; i < 100; i++ { - files += fmt.Sprintf("\n-- content/posts/p%d.md --\n"+content, i+1) - } - files = strings.ReplaceAll(files, "§", "%") - - cfg := IntegrationTestConfig{ - T: b, - TxtarString: files, - } - builders := make([]*IntegrationTestBuilder, b.N) - - for i := range builders { - builders[i] = NewIntegrationTestBuilder(cfg) - } - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - builders[i].Build() } } @@ -358,50 +783,38 @@ func TestReplaceShortcodeTokens(t *testing.T) { input string prefix string replacements map[string]string - expect any + expect interface{} }{ - {"Hello HAHAHUGOSHORTCODE-1HBHB.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "Hello World."}, - {"Hello HAHAHUGOSHORTCODE-1@}@.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, false}, - {"HAHAHUGOSHORTCODE2-1HBHB", "PREFIX2", map[string]string{"HAHAHUGOSHORTCODE2-1HBHB": "World"}, "World"}, + {"Hello HAHAPREFIX-1HBHB.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World"}, "Hello World."}, + {"Hello HAHAPREFIX-1@}@.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World"}, false}, + {"HAHAPREFIX2-1HBHB", "PREFIX2", map[string]string{"HAHAPREFIX2-1HBHB": "World"}, "World"}, {"Hello World!", "PREFIX2", map[string]string{}, "Hello World!"}, - {"!HAHAHUGOSHORTCODE-1HBHB", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "!World"}, - {"HAHAHUGOSHORTCODE-1HBHB!", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "World!"}, - {"!HAHAHUGOSHORTCODE-1HBHB!", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "!World!"}, - {"_{_PREFIX-1HBHB", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "_{_PREFIX-1HBHB"}, - {"Hello HAHAHUGOSHORTCODE-1HBHB.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "To You My Old Friend Who Told Me This Fantastic Story"}, "Hello To You My Old Friend Who Told Me This Fantastic Story."}, - {"A HAHAHUGOSHORTCODE-1HBHB asdf HAHAHUGOSHORTCODE-2HBHB.", "A", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "v1", "HAHAHUGOSHORTCODE-2HBHB": "v2"}, "A v1 asdf v2."}, - {"Hello HAHAHUGOSHORTCODE2-1HBHB. Go HAHAHUGOSHORTCODE2-2HBHB, Go, Go HAHAHUGOSHORTCODE2-3HBHB Go Go!.", "PREFIX2", map[string]string{"HAHAHUGOSHORTCODE2-1HBHB": "Europe", "HAHAHUGOSHORTCODE2-2HBHB": "Jonny", "HAHAHUGOSHORTCODE2-3HBHB": "Johnny"}, "Hello Europe. Go Jonny, Go, Go Johnny Go Go!."}, - {"A HAHAHUGOSHORTCODE-2HBHB HAHAHUGOSHORTCODE-1HBHB.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "A", "HAHAHUGOSHORTCODE-2HBHB": "B"}, "A B A."}, - {"A HAHAHUGOSHORTCODE-1HBHB HAHAHUGOSHORTCODE-2", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "A"}, false}, - {"A HAHAHUGOSHORTCODE-1HBHB but not the second.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "A", "HAHAHUGOSHORTCODE-2HBHB": "B"}, "A A but not the second."}, - {"An HAHAHUGOSHORTCODE-1HBHB.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "A", "HAHAHUGOSHORTCODE-2HBHB": "B"}, "An A."}, - {"An HAHAHUGOSHORTCODE-1HBHB HAHAHUGOSHORTCODE-2HBHB.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "A", "HAHAHUGOSHORTCODE-2HBHB": "B"}, "An A B."}, - {"A HAHAHUGOSHORTCODE-1HBHB HAHAHUGOSHORTCODE-2HBHB HAHAHUGOSHORTCODE-3HBHB HAHAHUGOSHORTCODE-1HBHB HAHAHUGOSHORTCODE-3HBHB.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "A", "HAHAHUGOSHORTCODE-2HBHB": "B", "HAHAHUGOSHORTCODE-3HBHB": "C"}, "A A B C A C."}, - {"A HAHAHUGOSHORTCODE-1HBHB HAHAHUGOSHORTCODE-2HBHB HAHAHUGOSHORTCODE-3HBHB HAHAHUGOSHORTCODE-1HBHB HAHAHUGOSHORTCODE-3HBHB.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "A", "HAHAHUGOSHORTCODE-2HBHB": "B", "HAHAHUGOSHORTCODE-3HBHB": "C"}, "A A B C A C."}, + {"!HAHAPREFIX-1HBHB", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World"}, "!World"}, + {"HAHAPREFIX-1HBHB!", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World"}, "World!"}, + {"!HAHAPREFIX-1HBHB!", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World"}, "!World!"}, + {"_{_PREFIX-1HBHB", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World"}, "_{_PREFIX-1HBHB"}, + {"Hello HAHAPREFIX-1HBHB.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "To You My Old Friend Who Told Me This Fantastic Story"}, "Hello To You My Old Friend Who Told Me This Fantastic Story."}, + {"A HAHAA-1HBHB asdf HAHAA-2HBHB.", "A", map[string]string{"HAHAA-1HBHB": "v1", "HAHAA-2HBHB": "v2"}, "A v1 asdf v2."}, + {"Hello HAHAPREFIX2-1HBHB. Go HAHAPREFIX2-2HBHB, Go, Go HAHAPREFIX2-3HBHB Go Go!.", "PREFIX2", map[string]string{"HAHAPREFIX2-1HBHB": "Europe", "HAHAPREFIX2-2HBHB": "Jonny", "HAHAPREFIX2-3HBHB": "Johnny"}, "Hello Europe. Go Jonny, Go, Go Johnny Go Go!."}, + {"A HAHAPREFIX-2HBHB HAHAPREFIX-1HBHB.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "A", "HAHAPREFIX-2HBHB": "B"}, "A B A."}, + {"A HAHAPREFIX-1HBHB HAHAPREFIX-2", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "A"}, false}, + {"A HAHAPREFIX-1HBHB but not the second.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "A", "HAHAPREFIX-2HBHB": "B"}, "A A but not the second."}, + {"An HAHAPREFIX-1HBHB.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "A", "HAHAPREFIX-2HBHB": "B"}, "An A."}, + {"An HAHAPREFIX-1HBHB HAHAPREFIX-2HBHB.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "A", "HAHAPREFIX-2HBHB": "B"}, "An A B."}, + {"A HAHAPREFIX-1HBHB HAHAPREFIX-2HBHB HAHAPREFIX-3HBHB HAHAPREFIX-1HBHB HAHAPREFIX-3HBHB.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "A", "HAHAPREFIX-2HBHB": "B", "HAHAPREFIX-3HBHB": "C"}, "A A B C A C."}, + {"A HAHAPREFIX-1HBHB HAHAPREFIX-2HBHB HAHAPREFIX-3HBHB HAHAPREFIX-1HBHB HAHAPREFIX-3HBHB.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "A", "HAHAPREFIX-2HBHB": "B", "HAHAPREFIX-3HBHB": "C"}, "A A B C A C."}, // Issue #1148 remove p-tags 10 => - {"Hello <p>HAHAHUGOSHORTCODE-1HBHB</p>. END.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "Hello World. END."}, - {"Hello <p>HAHAHUGOSHORTCODE-1HBHB</p>. <p>HAHAHUGOSHORTCODE-2HBHB</p> END.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World", "HAHAHUGOSHORTCODE-2HBHB": "THE"}, "Hello World. THE END."}, - {"Hello <p>HAHAHUGOSHORTCODE-1HBHB. END</p>.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "Hello <p>World. END</p>."}, - {"<p>Hello HAHAHUGOSHORTCODE-1HBHB</p>. END.", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "<p>Hello World</p>. END."}, - {"Hello <p>HAHAHUGOSHORTCODE-1HBHB12", "PREFIX", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "World"}, "Hello <p>World12"}, - { - "Hello HAHAHUGOSHORTCODE-1HBHB. HAHAHUGOSHORTCODE-1HBHB-HAHAHUGOSHORTCODE-1HBHB HAHAHUGOSHORTCODE-1HBHB HAHAHUGOSHORTCODE-1HBHB HAHAHUGOSHORTCODE-1HBHB END", "P", - map[string]string{"HAHAHUGOSHORTCODE-1HBHB": strings.Repeat("BC", 100)}, + {"Hello <p>HAHAPREFIX-1HBHB</p>. END.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World"}, "Hello World. END."}, + {"Hello <p>HAHAPREFIX-1HBHB</p>. <p>HAHAPREFIX-2HBHB</p> END.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World", "HAHAPREFIX-2HBHB": "THE"}, "Hello World. THE END."}, + {"Hello <p>HAHAPREFIX-1HBHB. END</p>.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World"}, "Hello <p>World. END</p>."}, + {"<p>Hello HAHAPREFIX-1HBHB</p>. END.", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World"}, "<p>Hello World</p>. END."}, + {"Hello <p>HAHAPREFIX-1HBHB12", "PREFIX", map[string]string{"HAHAPREFIX-1HBHB": "World"}, "Hello <p>World12"}, + {"Hello HAHAP-1HBHB. HAHAP-1HBHB-HAHAP-1HBHB HAHAP-1HBHB HAHAP-1HBHB HAHAP-1HBHB END", "P", map[string]string{"HAHAP-1HBHB": strings.Repeat("BC", 100)}, fmt.Sprintf("Hello %s. %s-%s %s %s %s END", - strings.Repeat("BC", 100), strings.Repeat("BC", 100), strings.Repeat("BC", 100), strings.Repeat("BC", 100), strings.Repeat("BC", 100), strings.Repeat("BC", 100)), - }, + strings.Repeat("BC", 100), strings.Repeat("BC", 100), strings.Repeat("BC", 100), strings.Repeat("BC", 100), strings.Repeat("BC", 100), strings.Repeat("BC", 100))}, } { - replacements := make(map[string]shortcodeRenderer) - for k, v := range this.replacements { - replacements[k] = prerenderedShortcode{s: v} - } - tokenHandler := func(ctx context.Context, token string) ([]byte, error) { - return []byte(this.replacements[token]), nil - } - - ctx := context.Background() - results, err := expandShortcodeTokens(ctx, []byte(this.input), tokenHandler) + results, err := replaceShortcodeTokens([]byte(this.input), this.prefix, this.replacements) if b, ok := this.expect.(bool); ok && !b { if err == nil { @@ -418,850 +831,15 @@ func TestReplaceShortcodeTokens(t *testing.T) { } } + } -func TestShortcodeGetContent(t *testing.T) { - t.Parallel() +func TestScKey(t *testing.T) { + require.Equal(t, scKey{Suffix: "xml", ShortcodePlaceholder: "ABCD"}, + newScKey(media.XMLType, "ABCD")) + require.Equal(t, scKey{Lang: "en", Suffix: "html", OutputFormat: "AMP", ShortcodePlaceholder: "EFGH"}, + newScKeyFromLangAndOutputFormat("en", output.AMPFormat, "EFGH")) + require.Equal(t, scKey{Suffix: "html", ShortcodePlaceholder: "IJKL"}, + newDefaultScKey("IJKL")) - contentShortcode := ` -{{- $t := .Get 0 -}} -{{- $p := .Get 1 -}} -{{- $k := .Get 2 -}} -{{- $page := $.Page.Site.GetPage "page" $p -}} -{{ if $page }} -{{- if eq $t "bundle" -}} -{{- .Scratch.Set "p" ($page.Resources.GetMatch (printf "%s*" $k)) -}} -{{- else -}} -{{- $.Scratch.Set "p" $page -}} -{{- end -}}P1:{{ .Page.Content }}|P2:{{ $p := ($.Scratch.Get "p") }}{{ $p.Title }}/{{ $p.Content }}| -{{- else -}} -{{- errorf "Page %s is nil" $p -}} -{{- end -}} -` - - var templates []string - var content []string - - contentWithShortcodeTemplate := `--- -title: doc%s -weight: %d ---- -Logo:{{< c "bundle" "b1" "logo.png" >}}:P1: {{< c "page" "section1/p1" "" >}}:BP1:{{< c "bundle" "b1" "bp1" >}}` - - simpleContentTemplate := `--- -title: doc%s -weight: %d ---- -C-%s` - - templates = append(templates, []string{"shortcodes/c.html", contentShortcode}...) - templates = append(templates, []string{"_default/single.html", "Single Content: {{ .Content }}"}...) - templates = append(templates, []string{"_default/list.html", "List Content: {{ .Content }}"}...) - - content = append(content, []string{"b1/index.md", fmt.Sprintf(contentWithShortcodeTemplate, "b1", 1)}...) - content = append(content, []string{"b1/logo.png", "PNG logo"}...) - content = append(content, []string{"b1/bp1.md", fmt.Sprintf(simpleContentTemplate, "bp1", 1, "bp1")}...) - - content = append(content, []string{"section1/_index.md", fmt.Sprintf(contentWithShortcodeTemplate, "s1", 2)}...) - content = append(content, []string{"section1/p1.md", fmt.Sprintf(simpleContentTemplate, "s1p1", 2, "s1p1")}...) - - content = append(content, []string{"section2/_index.md", fmt.Sprintf(simpleContentTemplate, "b1", 1, "b1")}...) - content = append(content, []string{"section2/s2p1.md", fmt.Sprintf(contentWithShortcodeTemplate, "bp1", 1)}...) - - builder := newTestSitesBuilder(t).WithDefaultMultiSiteConfig() - - builder.WithContent(content...).WithTemplates(templates...).CreateSites().Build(BuildCfg{}) - s := builder.H.Sites[0] - builder.Assert(len(s.RegularPages()), qt.Equals, 3) - - builder.AssertFileContent("public/en/section1/index.html", - "List Content: <p>Logo:P1:|P2:logo.png/PNG logo|:P1: P1:|P2:docs1p1/<p>C-s1p1</p>\n|", - "BP1:P1:|P2:docbp1/<p>C-bp1</p>", - ) - - builder.AssertFileContent("public/en/b1/index.html", - "Single Content: <p>Logo:P1:|P2:logo.png/PNG logo|:P1: P1:|P2:docs1p1/<p>C-s1p1</p>\n|", - "P2:docbp1/<p>C-bp1</p>", - ) - - builder.AssertFileContent("public/en/section2/s2p1/index.html", - "Single Content: <p>Logo:P1:|P2:logo.png/PNG logo|:P1: P1:|P2:docs1p1/<p>C-s1p1</p>\n|", - "P2:docbp1/<p>C-bp1</p>", - ) -} - -// https://github.com/gohugoio/hugo/issues/5833 -func TestShortcodeParentResourcesOnRebuild(t *testing.T) { - t.Parallel() - - b := newTestSitesBuilder(t).Running().WithSimpleConfigFile() - b.WithTemplatesAdded( - "index.html", ` -{{ $b := .Site.GetPage "b1" }} -b1 Content: {{ $b.Content }} -{{$p := $b.Resources.GetMatch "p1*" }} -Content: {{ $p.Content }} -{{ $article := .Site.GetPage "blog/article" }} -Article Content: {{ $article.Content }} -`, - "shortcodes/c.html", ` -{{ range .Page.Parent.Resources }} -* Parent resource: {{ .Name }}: {{ .RelPermalink }} -{{ end }} -`) - - pageContent := ` ---- -title: MyPage ---- - -SHORTCODE: {{< c >}} - -` - - b.WithContent("b1/index.md", pageContent, - "b1/logo.png", "PNG logo", - "b1/p1.md", pageContent, - "blog/_index.md", pageContent, - "blog/logo-article.png", "PNG logo", - "blog/article.md", pageContent, - ) - - b.Build(BuildCfg{}) - - assert := func(matchers ...string) { - allMatchers := append(matchers, "Parent resource: logo.png: /b1/logo.png", - "Article Content: <p>SHORTCODE: \n\n* Parent resource: logo-article.png: /blog/logo-article.png", - ) - - b.AssertFileContent("public/index.html", - allMatchers..., - ) - } - - assert() - - b.EditFiles("content/b1/index.md", pageContent+" Edit.") - - b.Build(BuildCfg{}) - - assert("Edit.") -} - -func TestShortcodePreserveOrder(t *testing.T) { - t.Parallel() - c := qt.New(t) - - contentTemplate := `--- -title: doc%d -weight: %d ---- -# doc - -{{< s1 >}}{{< s2 >}}{{< s3 >}}{{< s4 >}}{{< s5 >}} - -{{< nested >}} -{{< ordinal >}} {{< scratch >}} -{{< ordinal >}} {{< scratch >}} -{{< ordinal >}} {{< scratch >}} -{{< /nested >}} - -` - - ordinalShortcodeTemplate := `ordinal: {{ .Ordinal }}{{ .Page.Scratch.Set "ordinal" .Ordinal }}` - - nestedShortcode := `outer ordinal: {{ .Ordinal }} inner: {{ .Inner }}` - scratchGetShortcode := `scratch ordinal: {{ .Ordinal }} scratch get ordinal: {{ .Page.Scratch.Get "ordinal" }}` - shortcodeTemplate := `v%d: {{ .Ordinal }} sgo: {{ .Page.Scratch.Get "o2" }}{{ .Page.Scratch.Set "o2" .Ordinal }}|` - - var shortcodes []string - var content []string - - shortcodes = append(shortcodes, []string{"shortcodes/nested.html", nestedShortcode}...) - shortcodes = append(shortcodes, []string{"shortcodes/ordinal.html", ordinalShortcodeTemplate}...) - shortcodes = append(shortcodes, []string{"shortcodes/scratch.html", scratchGetShortcode}...) - - for i := 1; i <= 5; i++ { - sc := fmt.Sprintf(shortcodeTemplate, i) - sc = strings.Replace(sc, "%%", "%", -1) - shortcodes = append(shortcodes, []string{fmt.Sprintf("shortcodes/s%d.html", i), sc}...) - } - - for i := 1; i <= 3; i++ { - content = append(content, []string{fmt.Sprintf("p%d.md", i), fmt.Sprintf(contentTemplate, i, i)}...) - } - - builder := newTestSitesBuilder(t).WithDefaultMultiSiteConfig() - - builder.WithContent(content...).WithTemplatesAdded(shortcodes...).CreateSites().Build(BuildCfg{}) - - s := builder.H.Sites[0] - c.Assert(len(s.RegularPages()), qt.Equals, 3) - - builder.AssertFileContent("public/en/p1/index.html", `v1: 0 sgo: |v2: 1 sgo: 0|v3: 2 sgo: 1|v4: 3 sgo: 2|v5: 4 sgo: 3`) - builder.AssertFileContent("public/en/p1/index.html", `outer ordinal: 5 inner: -ordinal: 0 scratch ordinal: 1 scratch get ordinal: 0 -ordinal: 2 scratch ordinal: 3 scratch get ordinal: 2 -ordinal: 4 scratch ordinal: 5 scratch get ordinal: 4`) -} - -func TestShortcodeVariables(t *testing.T) { - t.Parallel() - c := qt.New(t) - - builder := newTestSitesBuilder(t).WithSimpleConfigFile() - - builder.WithContent("page.md", `--- -title: "Hugo Rocks!" ---- - -# doc - - {{< s1 >}} - -`).WithTemplatesAdded("layouts/shortcodes/s1.html", ` -Name: {{ .Name }} -{{ with .Position }} -File: {{ .Filename }} -Offset: {{ .Offset }} -Line: {{ .LineNumber }} -Column: {{ .ColumnNumber }} -String: {{ . | safeHTML }} -{{ end }} - -`).CreateSites().Build(BuildCfg{}) - - s := builder.H.Sites[0] - c.Assert(len(s.RegularPages()), qt.Equals, 1) - - builder.AssertFileContent("public/page/index.html", - filepath.FromSlash("File: content/page.md"), - "Line: 7", "Column: 4", "Offset: 40", - filepath.FromSlash("String: \"content/page.md:7:4\""), - "Name: s1", - ) -} - -func TestInlineShortcodes(t *testing.T) { - for _, enableInlineShortcodes := range []bool{true, false} { - enableInlineShortcodes := enableInlineShortcodes - t.Run(fmt.Sprintf("enableInlineShortcodes=%t", enableInlineShortcodes), - func(t *testing.T) { - t.Parallel() - conf := fmt.Sprintf(` -baseURL = "https://example.com" -enableInlineShortcodes = %t -`, enableInlineShortcodes) - - b := newTestSitesBuilder(t) - b.WithConfigFile("toml", conf) - - shortcodeContent := `FIRST:{{< myshort.inline "first" >}} -Page: {{ .Page.Title }} -Seq: {{ seq 3 }} -Param: {{ .Get 0 }} -{{< /myshort.inline >}}:END: - -SECOND:{{< myshort.inline "second" />}}:END -NEW INLINE: {{< n1.inline "5" >}}W1: {{ seq (.Get 0) }}{{< /n1.inline >}}:END: -INLINE IN INNER: {{< outer >}}{{< n2.inline >}}W2: {{ seq 4 }}{{< /n2.inline >}}{{< /outer >}}:END: -REUSED INLINE IN INNER: {{< outer >}}{{< n1.inline "3" />}}{{< /outer >}}:END: -## MARKDOWN DELIMITER: {{% mymarkdown.inline %}}**Hugo Rocks!**{{% /mymarkdown.inline %}} -` - - b.WithContent("page-md-shortcode.md", `--- -title: "Hugo" ---- -`+shortcodeContent) - - b.WithContent("_index.md", `--- -title: "Hugo Home" ---- - -`+shortcodeContent) - - b.WithTemplatesAdded("layouts/_default/single.html", ` -CONTENT:{{ .Content }} -TOC: {{ .TableOfContents }} -`) - - b.WithTemplatesAdded("layouts/index.html", ` -CONTENT:{{ .Content }} -TOC: {{ .TableOfContents }} -`) - - b.WithTemplatesAdded("layouts/shortcodes/outer.html", `Inner: {{ .Inner }}`) - - b.CreateSites().Build(BuildCfg{}) - - shouldContain := []string{ - "Seq: [1 2 3]", - "Param: first", - "Param: second", - "NEW INLINE: W1: [1 2 3 4 5]", - "INLINE IN INNER: Inner: W2: [1 2 3 4]", - "REUSED INLINE IN INNER: Inner: W1: [1 2 3]", - `<li><a href="#markdown-delimiter-hugo-rocks">MARKDOWN DELIMITER: <strong>Hugo Rocks!</strong></a></li>`, - } - - if enableInlineShortcodes { - b.AssertFileContent("public/page-md-shortcode/index.html", - shouldContain..., - ) - b.AssertFileContent("public/index.html", - shouldContain..., - ) - } else { - b.AssertFileContent("public/page-md-shortcode/index.html", - "FIRST::END", - "SECOND::END", - "NEW INLINE: :END", - "INLINE IN INNER: Inner: :END:", - "REUSED INLINE IN INNER: Inner: :END:", - ) - } - }) - - } -} - -// https://github.com/gohugoio/hugo/issues/5863 -func TestShortcodeNamespaced(t *testing.T) { - t.Parallel() - c := qt.New(t) - - builder := newTestSitesBuilder(t).WithSimpleConfigFile() - - builder.WithContent("page.md", `--- -title: "Hugo Rocks!" ---- - -# doc - - hello: {{< hello >}} - test/hello: {{< test/hello >}} - -`).WithTemplatesAdded( - "layouts/shortcodes/hello.html", `hello`, - "layouts/shortcodes/test/hello.html", `test/hello`).CreateSites().Build(BuildCfg{}) - - s := builder.H.Sites[0] - c.Assert(len(s.RegularPages()), qt.Equals, 1) - - builder.AssertFileContent("public/page/index.html", - "hello: hello", - "test/hello: test/hello", - ) -} - -func TestShortcodeParams(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.org" --- layouts/shortcodes/hello.html -- -{{ range $i, $v := .Params }}{{ printf "- %v: %v (%T) " $i $v $v -}}{{ end }} --- content/page.md -- -title: "Hugo Rocks!" -summary: "Foo" ---- - -# doc - -types positional: {{< hello true false 33 3.14 >}} -types named: {{< hello b1=true b2=false i1=33 f1=3.14 >}} -types string: {{< hello "true" trues "33" "3.14" >}} -escaped quoute: {{< hello "hello \"world\"." >}} --- layouts/_default/single.html -- -Content: {{ .Content }}| -` - - b := Test(t, files) - - b.AssertFileContent("public/page/index.html", - "types positional: - 0: true (bool) - 1: false (bool) - 2: 33 (int) - 3: 3.14 (float64)", - "types named: - b1: true (bool) - b2: false (bool) - f1: 3.14 (float64) - i1: 33 (int)", - "types string: - 0: true (string) - 1: trues (string) - 2: 33 (string) - 3: 3.14 (string) ", - "hello "world". (string)", - ) -} - -func TestShortcodeRef(t *testing.T) { - t.Parallel() - - v := config.New() - v.Set("baseURL", "https://example.org") - - builder := newTestSitesBuilder(t).WithViper(v) - - for i := 1; i <= 2; i++ { - builder.WithContent(fmt.Sprintf("page%d.md", i), `--- -title: "Hugo Rocks!" ---- - - - -[Page 1]({{< ref "page1.md" >}}) -[Page 1 with anchor]({{< relref "page1.md#doc" >}}) -[Page 2]({{< ref "page2.md" >}}) -[Page 2 with anchor]({{< relref "page2.md#doc" >}}) - - -## Doc - - -`) - } - - builder.Build(BuildCfg{}) - - builder.AssertFileContent("public/page2/index.html", ` -<a href="/page1/#doc">Page 1 with anchor</a> -<a href="https://example.org/page2/">Page 2</a> -<a href="/page2/#doc">Page 2 with anchor</a></p> - -<h2 id="doc">Doc</h2> -`, - ) -} - -// https://github.com/gohugoio/hugo/issues/6857 -func TestShortcodeNoInner(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.org" -disableKinds = ["term", "taxonomy", "home", "section"] --- content/mypage.md -- ---- -title: "No Inner!" ---- - -{{< noinner >}}{{< /noinner >}} - --- layouts/shortcodes/noinner.html -- -No inner here. --- layouts/_default/single.html -- -Content: {{ .Content }}| - -` - - b, err := TestE(t, files) - - assert := func() { - b.Assert(err.Error(), qt.Contains, filepath.FromSlash(`failed to extract shortcode: shortcode "noinner" does not evaluate .Inner or .InnerDeindent, yet a closing tag was provided`)) - } - - assert() - - b, err = TestE(t, strings.Replace(files, `{{< noinner >}}{{< /noinner >}}`, `{{< noinner />}}`, 1)) - - assert() -} - -func TestShortcodeStableOutputFormatTemplates(t *testing.T) { - t.Parallel() - - for range 5 { - - b := newTestSitesBuilder(t) - - const numPages = 10 - - for i := range numPages { - b.WithContent(fmt.Sprintf("page%d.md", i), `--- -title: "Page" -outputs: ["html", "css", "csv", "json"] ---- -{{< myshort >}} - -`) - } - - b.WithTemplates( - "_default/single.html", "{{ .Content }}", - "_default/single.css", "{{ .Content }}", - "_default/single.csv", "{{ .Content }}", - "_default/single.json", "{{ .Content }}", - "shortcodes/myshort.html", `Short-HTML`, - "shortcodes/myshort.csv", `Short-CSV`, - "shortcodes/myshort.txt", `Short-TXT`, - ) - - b.Build(BuildCfg{}) - - // helpers.PrintFs(b.Fs.Destination, "public", os.Stdout) - - for i := range numPages { - b.AssertFileContent(fmt.Sprintf("public/page%d/index.html", i), "Short-HTML") - b.AssertFileContent(fmt.Sprintf("public/page%d/index.csv", i), "Short-CSV") - b.AssertFileContent(fmt.Sprintf("public/page%d/index.json", i), "Short-CSV") - - } - - for i := range numPages { - b.AssertFileContent(fmt.Sprintf("public/page%d/styles.css", i), "Short-CSV") - } - - } -} - -// #9821 -func TestShortcodeMarkdownOutputFormat(t *testing.T) { - t.Parallel() - - files := ` --- config.toml -- --- content/p1.md -- ---- -title: "p1" ---- -{{% foo %}} -# The below would have failed using the HTML template parser. --- layouts/shortcodes/foo.md -- -§§§ -<x -§§§ --- layouts/_default/single.html -- -{{ .Content }} -` - - b := Test(t, files) - - b.AssertFileContent("public/p1/index.html", "<code><x") -} - -func TestShortcodePreserveIndentation(t *testing.T) { - t.Parallel() - - files := ` --- config.toml -- --- content/p1.md -- ---- -title: "p1" ---- - -## List With Indented Shortcodes - -1. List 1 - {{% mark1 %}} - 1. Item Mark1 1 - 1. Item Mark1 2 - {{% mark2 %}} - {{% /mark1 %}} --- layouts/shortcodes/mark1.md -- -{{ .Inner }} --- layouts/shortcodes/mark2.md -- -1. Item Mark2 1 -1. Item Mark2 2 - 1. Item Mark2 2-1 -1. Item Mark2 3 --- layouts/_default/single.html -- -{{ .Content }} -` - - b := Test(t, files) - - b.AssertFileContent("public/p1/index.html", "<ol>\n<li>\n<p>List 1</p>\n<ol>\n<li>Item Mark1 1</li>\n<li>Item Mark1 2</li>\n<li>Item Mark2 1</li>\n<li>Item Mark2 2\n<ol>\n<li>Item Mark2 2-1</li>\n</ol>\n</li>\n<li>Item Mark2 3</li>\n</ol>\n</li>\n</ol>") -} - -func TestShortcodeCodeblockIndent(t *testing.T) { - t.Parallel() - - files := ` --- config.toml -- --- content/p1.md -- ---- -title: "p1" ---- - -## Code block - - {{% code %}} - --- layouts/shortcodes/code.md -- -echo "foo"; --- layouts/_default/single.html -- -{{ .Content }} -` - - b := Test(t, files) - - b.AssertFileContent("public/p1/index.html", "<pre><code>echo "foo";\n</code></pre>") -} - -func TestShortcodeHighlightDeindent(t *testing.T) { - t.Parallel() - - files := ` --- config.toml -- -[markup] -[markup.highlight] -codeFences = true -noClasses = false --- content/p1.md -- ---- -title: "p1" ---- - -## Indent 5 Spaces - - {{< highlight bash >}} - line 1; - line 2; - line 3; - {{< /highlight >}} - --- layouts/_default/single.html -- -{{ .Content }} -` - - b := Test(t, files) - - b.AssertFileContent("public/p1/index.html", ` -<pre><code> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">line 1<span class="p">;</span> -</span></span><span class="line"><span class="cl">line 2<span class="p">;</span> -</span></span><span class="line"><span class="cl">line 3<span class="p">;</span></span></span></code></pre></div> -</code></pre> - - `) -} - -// Issue 10236. -func TestShortcodeParamEscapedQuote(t *testing.T) { - t.Parallel() - - files := ` --- config.toml -- --- content/p1.md -- ---- -title: "p1" ---- - -{{< figure src="/media/spf13.jpg" title="Steve \"Francia\"." >}} - --- layouts/shortcodes/figure.html -- -Title: {{ .Get "title" | safeHTML }} --- layouts/_default/single.html -- -{{ .Content }} -` - - b := NewIntegrationTestBuilder( - IntegrationTestConfig{ - T: t, - TxtarString: files, - - Verbose: true, - }, - ).Build() - - b.AssertFileContent("public/p1/index.html", `Title: Steve "Francia".`) -} - -// Issue 10391. -func TestNestedShortcodeCustomOutputFormat(t *testing.T) { - t.Parallel() - - files := ` --- config.toml -- - -[outputFormats.Foobar] -baseName = "foobar" -isPlainText = true -mediaType = "application/json" -notAlternative = true - -[languages.en] -languageName = "English" - -[languages.en.outputs] -home = [ "HTML", "RSS", "Foobar" ] - -[languages.fr] -languageName = "Français" - -[[module.mounts]] -source = "content/en" -target = "content" -lang = "en" - -[[module.mounts]] -source = "content/fr" -target = "content" -lang = "fr" - --- layouts/_default/list.foobar.json -- -{{- $.Scratch.Add "data" slice -}} -{{- range (where .Site.AllPages "Kind" "!=" "home") -}} - {{- $.Scratch.Add "data" (dict "content" (.Plain | truncate 10000) "type" .Type "full_url" .Permalink) -}} -{{- end -}} -{{- $.Scratch.Get "data" | jsonify -}} --- content/en/p1.md -- ---- -title: "p1" ---- - -### More information - -{{< tabs >}} -{{% tab "Test" %}} - -It's a test - -{{% /tab %}} -{{< /tabs >}} - --- content/fr/p2.md -- ---- -title: Test ---- - -### Plus d'informations - -{{< tabs >}} -{{% tab "Test" %}} - -C'est un test - -{{% /tab %}} -{{< /tabs >}} - --- layouts/shortcodes/tabs.html -- -<div> - <div class="tab-content">{{ .Inner }}</div> -</div> - --- layouts/shortcodes/tab.html -- -<div>{{ .Inner }}</div> - --- layouts/_default/single.html -- -{{ .Content }} -` - - b := NewIntegrationTestBuilder( - IntegrationTestConfig{ - T: t, - TxtarString: files, - - Verbose: true, - }, - ).Build() - - b.AssertFileContent("public/fr/p2/index.html", `plus-dinformations`) -} - -// Issue 10671. -func TestShortcodeInnerShouldBeEmptyWhenNotClosed(t *testing.T) { - t.Parallel() - - files := ` --- config.toml -- -disableKinds = ["home", "taxonomy", "term"] --- content/p1.md -- ---- -title: "p1" ---- - -{{< sc "self-closing" />}} - -Text. - -{{< sc "closing-no-newline" >}}{{< /sc >}} - --- layouts/shortcodes/sc.html -- -Inner: {{ .Get 0 }}: {{ len .Inner }} -InnerDeindent: {{ .Get 0 }}: {{ len .InnerDeindent }} --- layouts/_default/single.html -- -{{ .Content }} -` - - b := NewIntegrationTestBuilder( - IntegrationTestConfig{ - T: t, - TxtarString: files, - - Verbose: true, - }, - ).Build() - - b.AssertFileContent("public/p1/index.html", ` -Inner: self-closing: 0 -InnerDeindent: self-closing: 0 -Inner: closing-no-newline: 0 -InnerDeindent: closing-no-newline: 0 - -`) -} - -// Issue 10675. -func TestShortcodeErrorWhenItShouldBeClosed(t *testing.T) { - t.Parallel() - - files := ` --- config.toml -- -disableKinds = ["home", "taxonomy", "term"] --- content/p1.md -- ---- -title: "p1" ---- - -{{< sc >}} - -Text. - --- layouts/shortcodes/sc.html -- -Inner: {{ .Get 0 }}: {{ len .Inner }} --- layouts/_default/single.html -- -{{ .Content }} -` - - b, err := NewIntegrationTestBuilder( - IntegrationTestConfig{ - T: t, - TxtarString: files, - - Verbose: true, - }, - ).BuildE() - - b.Assert(err, qt.Not(qt.IsNil)) - b.Assert(err.Error(), qt.Contains, `p1.md:5:1": failed to extract shortcode: shortcode "sc" must be closed or self-closed`) -} - -// Issue 10819. -func TestShortcodeInCodeFenceHyphen(t *testing.T) { - t.Parallel() - - files := ` --- config.toml -- -disableKinds = ["home", "taxonomy", "term"] --- content/p1.md -- ---- -title: "p1" ---- - -§§§go -{{< sc >}} -§§§ - -Text. - --- layouts/shortcodes/sc.html -- -Hello. --- layouts/_default/single.html -- -{{ .Content }} -` - - b := NewIntegrationTestBuilder( - IntegrationTestConfig{ - T: t, - TxtarString: files, - - Verbose: true, - }, - ).Build() - - b.AssertFileContent("public/p1/index.html", "<span style=\"color:#a6e22e\">Hello.</span>") } diff --git a/hugolib/shortcodeparser.go b/hugolib/shortcodeparser.go new file mode 100644 index 000000000..bdbd3ae50 --- /dev/null +++ b/hugolib/shortcodeparser.go @@ -0,0 +1,588 @@ +// Copyright 2015 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 hugolib + +import ( + "fmt" + "strings" + "unicode" + "unicode/utf8" +) + +// The lexical scanning below is highly inspired by the great talk given by +// Rob Pike called "Lexical Scanning in Go" (it's on YouTube, Google it!). +// See slides here: http://cuddle.googlecode.com/hg/talk/lex.html + +// parsing + +type pageTokens struct { + lexer *pagelexer + token [3]item // 3-item look-ahead is what we currently need + peekCount int +} + +func (t *pageTokens) next() item { + if t.peekCount > 0 { + t.peekCount-- + } else { + t.token[0] = t.lexer.nextItem() + } + return t.token[t.peekCount] +} + +// backs up one token. +func (t *pageTokens) backup() { + t.peekCount++ +} + +// backs up two tokens. +func (t *pageTokens) backup2(t1 item) { + t.token[1] = t1 + t.peekCount = 2 +} + +// backs up three tokens. +func (t *pageTokens) backup3(t2, t1 item) { + t.token[1] = t1 + t.token[2] = t2 + t.peekCount = 3 +} + +// check for non-error and non-EOF types coming next +func (t *pageTokens) isValueNext() bool { + i := t.peek() + return i.typ != tError && i.typ != tEOF +} + +// look at, but do not consume, the next item +// repeated, sequential calls will return the same item +func (t *pageTokens) peek() item { + if t.peekCount > 0 { + return t.token[t.peekCount-1] + } + t.peekCount = 1 + t.token[0] = t.lexer.nextItem() + return t.token[0] +} + +// convencience method to consume the next n tokens, but back off Errors and EOF +func (t *pageTokens) consume(cnt int) { + for i := 0; i < cnt; i++ { + token := t.next() + if token.typ == tError || token.typ == tEOF { + t.backup() + break + } + } +} + +// lexical scanning + +// position (in bytes) +type pos int + +type item struct { + typ itemType + pos pos + val string +} + +func (i item) String() string { + switch { + case i.typ == tEOF: + return "EOF" + case i.typ == tError: + return i.val + case i.typ > tKeywordMarker: + return fmt.Sprintf("<%s>", i.val) + case len(i.val) > 20: + return fmt.Sprintf("%.20q...", i.val) + } + return fmt.Sprintf("[%s]", i.val) +} + +type itemType int + +const ( + tError itemType = iota + tEOF + + // shortcode items + tLeftDelimScNoMarkup + tRightDelimScNoMarkup + tLeftDelimScWithMarkup + tRightDelimScWithMarkup + tScClose + tScName + tScParam + tScParamVal + + //itemIdentifier + tText // plain text, used for everything outside the shortcodes + + // preserved for later - keywords come after this + tKeywordMarker +) + +const eof = -1 + +// returns the next state in scanner. +type stateFunc func(*pagelexer) stateFunc + +type pagelexer struct { + name string + input string + state stateFunc + pos pos // input position + start pos // item start position + width pos // width of last element + lastPos pos // position of the last item returned by nextItem + + // shortcode state + currLeftDelimItem itemType + currRightDelimItem itemType + currShortcodeName string // is only set when a shortcode is in opened state + closingState int // > 0 = on its way to be closed + elementStepNum int // step number in element + paramElements int // number of elements (name + value = 2) found first + openShortcodes map[string]bool // set of shortcodes in open state + + // items delivered to client + items chan item +} + +// note: the input position here is normally 0 (start), but +// can be set if position of first shortcode is known +func newShortcodeLexer(name, input string, inputPosition pos) *pagelexer { + lexer := &pagelexer{ + name: name, + input: input, + currLeftDelimItem: tLeftDelimScNoMarkup, + currRightDelimItem: tRightDelimScNoMarkup, + pos: inputPosition, + openShortcodes: make(map[string]bool), + items: make(chan item), + } + go lexer.runShortcodeLexer() + return lexer +} + +// main loop +// this looks kind of funky, but it works +func (l *pagelexer) runShortcodeLexer() { + for l.state = lexTextOutsideShortcodes; l.state != nil; { + l.state = l.state(l) + } + + close(l.items) +} + +// state functions + +const ( + leftDelimScNoMarkup = "{{<" + rightDelimScNoMarkup = ">}}" + leftDelimScWithMarkup = "{{%" + rightDelimScWithMarkup = "%}}" + leftComment = "/*" // comments in this context us used to to mark shortcodes as "not really a shortcode" + rightComment = "*/" +) + +func (l *pagelexer) next() rune { + if int(l.pos) >= len(l.input) { + l.width = 0 + return eof + } + + // looks expensive, but should produce the same iteration sequence as the string range loop + // see: http://blog.golang.org/strings + runeValue, runeWidth := utf8.DecodeRuneInString(l.input[l.pos:]) + l.width = pos(runeWidth) + l.pos += l.width + return runeValue +} + +// peek, but no consume +func (l *pagelexer) peek() rune { + r := l.next() + l.backup() + return r +} + +// steps back one +func (l *pagelexer) backup() { + l.pos -= l.width +} + +// sends an item back to the client. +func (l *pagelexer) emit(t itemType) { + l.items <- item{t, l.start, l.input[l.start:l.pos]} + l.start = l.pos +} + +// special case, do not send '\\' back to client +func (l *pagelexer) ignoreEscapesAndEmit(t itemType) { + val := strings.Map(func(r rune) rune { + if r == '\\' { + return -1 + } + return r + }, l.input[l.start:l.pos]) + l.items <- item{t, l.start, val} + l.start = l.pos +} + +// gets the current value (for debugging and error handling) +func (l *pagelexer) current() string { + return l.input[l.start:l.pos] +} + +// ignore current element +func (l *pagelexer) ignore() { + l.start = l.pos +} + +// nice to have in error logs +func (l *pagelexer) lineNum() int { + return strings.Count(l.input[:l.lastPos], "\n") + 1 +} + +// nil terminates the parser +func (l *pagelexer) errorf(format string, args ...interface{}) stateFunc { + l.items <- item{tError, l.start, fmt.Sprintf(format, args...)} + return nil +} + +// consumes and returns the next item +func (l *pagelexer) nextItem() item { + item := <-l.items + l.lastPos = item.pos + return item +} + +// scans until an opening shortcode opening bracket. +// if no shortcodes, it will keep on scanning until EOF +func lexTextOutsideShortcodes(l *pagelexer) stateFunc { + for { + if strings.HasPrefix(l.input[l.pos:], leftDelimScWithMarkup) || strings.HasPrefix(l.input[l.pos:], leftDelimScNoMarkup) { + if l.pos > l.start { + l.emit(tText) + } + if strings.HasPrefix(l.input[l.pos:], leftDelimScWithMarkup) { + l.currLeftDelimItem = tLeftDelimScWithMarkup + l.currRightDelimItem = tRightDelimScWithMarkup + } else { + l.currLeftDelimItem = tLeftDelimScNoMarkup + l.currRightDelimItem = tRightDelimScNoMarkup + } + return lexShortcodeLeftDelim + + } + if l.next() == eof { + break + } + } + // Done! + if l.pos > l.start { + l.emit(tText) + } + l.emit(tEOF) + return nil +} + +func lexShortcodeLeftDelim(l *pagelexer) stateFunc { + l.pos += pos(len(l.currentLeftShortcodeDelim())) + if strings.HasPrefix(l.input[l.pos:], leftComment) { + return lexShortcodeComment + } + l.emit(l.currentLeftShortcodeDelimItem()) + l.elementStepNum = 0 + l.paramElements = 0 + return lexInsideShortcode +} + +func lexShortcodeComment(l *pagelexer) stateFunc { + posRightComment := strings.Index(l.input[l.pos:], rightComment) + if posRightComment <= 1 { + return l.errorf("comment must be closed") + } + // we emit all as text, except the comment markers + l.emit(tText) + l.pos += pos(len(leftComment)) + l.ignore() + l.pos += pos(posRightComment - len(leftComment)) + l.emit(tText) + l.pos += pos(len(rightComment)) + l.ignore() + if !strings.HasPrefix(l.input[l.pos:], l.currentRightShortcodeDelim()) { + return l.errorf("comment ends before the right shortcode delimiter") + } + l.pos += pos(len(l.currentRightShortcodeDelim())) + l.emit(tText) + return lexTextOutsideShortcodes +} + +func lexShortcodeRightDelim(l *pagelexer) stateFunc { + l.closingState = 0 + l.pos += pos(len(l.currentRightShortcodeDelim())) + l.emit(l.currentRightShortcodeDelimItem()) + return lexTextOutsideShortcodes +} + +// either: +// 1. param +// 2. "param" or "param\" +// 3. param="123" or param="123\" +// 4. param="Some \"escaped\" text" +func lexShortcodeParam(l *pagelexer, escapedQuoteStart bool) stateFunc { + + first := true + nextEq := false + + var r rune + + for { + r = l.next() + if first { + if r == '"' { + // a positional param with quotes + if l.paramElements == 2 { + return l.errorf("got quoted positional parameter. Cannot mix named and positional parameters") + } + l.paramElements = 1 + l.backup() + return lexShortcodeQuotedParamVal(l, !escapedQuoteStart, tScParam) + } + first = false + } else if r == '=' { + // a named param + l.backup() + nextEq = true + break + } + + if !isAlphaNumericOrHyphen(r) { + l.backup() + break + } + } + + if l.paramElements == 0 { + l.paramElements++ + + if nextEq { + l.paramElements++ + } + } else { + if nextEq && l.paramElements == 1 { + return l.errorf("got named parameter '%s'. Cannot mix named and positional parameters", l.current()) + } else if !nextEq && l.paramElements == 2 { + return l.errorf("got positional parameter '%s'. Cannot mix named and positional parameters", l.current()) + } + } + + l.emit(tScParam) + return lexInsideShortcode + +} + +func lexShortcodeQuotedParamVal(l *pagelexer, escapedQuotedValuesAllowed bool, typ itemType) stateFunc { + openQuoteFound := false + escapedInnerQuoteFound := false + escapedQuoteState := 0 + +Loop: + for { + switch r := l.next(); { + case r == '\\': + if l.peek() == '"' { + if openQuoteFound && !escapedQuotedValuesAllowed { + l.backup() + break Loop + } else if openQuoteFound { + // the coming quoute is inside + escapedInnerQuoteFound = true + escapedQuoteState = 1 + } + } + case r == eof, r == '\n': + return l.errorf("unterminated quoted string in shortcode parameter-argument: '%s'", l.current()) + case r == '"': + if escapedQuoteState == 0 { + if openQuoteFound { + l.backup() + break Loop + + } else { + openQuoteFound = true + l.ignore() + } + } else { + escapedQuoteState = 0 + } + + } + } + + if escapedInnerQuoteFound { + l.ignoreEscapesAndEmit(typ) + } else { + l.emit(typ) + } + + r := l.next() + + if r == '\\' { + if l.peek() == '"' { + // ignore the escaped closing quote + l.ignore() + l.next() + l.ignore() + } + } else if r == '"' { + // ignore closing quote + l.ignore() + } else { + // handled by next state + l.backup() + } + + return lexInsideShortcode +} + +// scans an alphanumeric inside shortcode +func lexIdentifierInShortcode(l *pagelexer) stateFunc { + lookForEnd := false +Loop: + for { + switch r := l.next(); { + case isAlphaNumericOrHyphen(r): + default: + l.backup() + word := l.input[l.start:l.pos] + if l.closingState > 0 && !l.openShortcodes[word] { + return l.errorf("closing tag for shortcode '%s' does not match start tag", word) + } else if l.closingState > 0 { + l.openShortcodes[word] = false + lookForEnd = true + } + + l.closingState = 0 + l.currShortcodeName = word + l.openShortcodes[word] = true + l.elementStepNum++ + l.emit(tScName) + break Loop + } + } + + if lookForEnd { + return lexEndOfShortcode + } + return lexInsideShortcode +} + +func lexEndOfShortcode(l *pagelexer) stateFunc { + if strings.HasPrefix(l.input[l.pos:], l.currentRightShortcodeDelim()) { + return lexShortcodeRightDelim + } + switch r := l.next(); { + case isSpace(r): + l.ignore() + default: + return l.errorf("unclosed shortcode") + } + return lexEndOfShortcode +} + +// scans the elements inside shortcode tags +func lexInsideShortcode(l *pagelexer) stateFunc { + if strings.HasPrefix(l.input[l.pos:], l.currentRightShortcodeDelim()) { + return lexShortcodeRightDelim + } + switch r := l.next(); { + case r == eof: + // eol is allowed inside shortcodes; this may go to end of document before it fails + return l.errorf("unclosed shortcode action") + case isSpace(r), isEndOfLine(r): + l.ignore() + case r == '=': + l.ignore() + return lexShortcodeQuotedParamVal(l, l.peek() != '\\', tScParamVal) + case r == '/': + if l.currShortcodeName == "" { + return l.errorf("got closing shortcode, but none is open") + } + l.closingState++ + l.emit(tScClose) + case r == '\\': + l.ignore() + if l.peek() == '"' { + return lexShortcodeParam(l, true) + } + case l.elementStepNum > 0 && (isAlphaNumericOrHyphen(r) || r == '"'): // positional params can have quotes + l.backup() + return lexShortcodeParam(l, false) + case isAlphaNumeric(r): + l.backup() + return lexIdentifierInShortcode + default: + return l.errorf("unrecognized character in shortcode action: %#U. Note: Parameters with non-alphanumeric args must be quoted", r) + } + return lexInsideShortcode +} + +// state helpers + +func (l *pagelexer) currentLeftShortcodeDelimItem() itemType { + return l.currLeftDelimItem +} + +func (l *pagelexer) currentRightShortcodeDelimItem() itemType { + return l.currRightDelimItem +} + +func (l *pagelexer) currentLeftShortcodeDelim() string { + if l.currLeftDelimItem == tLeftDelimScWithMarkup { + return leftDelimScWithMarkup + } + return leftDelimScNoMarkup + +} + +func (l *pagelexer) currentRightShortcodeDelim() string { + if l.currRightDelimItem == tRightDelimScWithMarkup { + return rightDelimScWithMarkup + } + return rightDelimScNoMarkup +} + +// helper functions + +func isSpace(r rune) bool { + return r == ' ' || r == '\t' +} + +func isAlphaNumericOrHyphen(r rune) bool { + // let unquoted YouTube ids as positional params slip through (they contain hyphens) + return isAlphaNumeric(r) || r == '-' +} + +func isEndOfLine(r rune) bool { + return r == '\r' || r == '\n' +} + +func isAlphaNumeric(r rune) bool { + return r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r) +} diff --git a/hugolib/shortcodeparser_test.go b/hugolib/shortcodeparser_test.go new file mode 100644 index 000000000..3103fd4de --- /dev/null +++ b/hugolib/shortcodeparser_test.go @@ -0,0 +1,202 @@ +// Copyright 2015 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 hugolib + +import ( + "testing" +) + +type shortCodeLexerTest struct { + name string + input string + items []item +} + +var ( + tstEOF = item{tEOF, 0, ""} + tstLeftNoMD = item{tLeftDelimScNoMarkup, 0, "{{<"} + tstRightNoMD = item{tRightDelimScNoMarkup, 0, ">}}"} + tstLeftMD = item{tLeftDelimScWithMarkup, 0, "{{%"} + tstRightMD = item{tRightDelimScWithMarkup, 0, "%}}"} + tstSCClose = item{tScClose, 0, "/"} + tstSC1 = item{tScName, 0, "sc1"} + tstSC2 = item{tScName, 0, "sc2"} + tstSC3 = item{tScName, 0, "sc3"} + tstParam1 = item{tScParam, 0, "param1"} + tstParam2 = item{tScParam, 0, "param2"} + tstVal = item{tScParamVal, 0, "Hello World"} +) + +var shortCodeLexerTests = []shortCodeLexerTest{ + {"empty", "", []item{tstEOF}}, + {"spaces", " \t\n", []item{{tText, 0, " \t\n"}, tstEOF}}, + {"text", `to be or not`, []item{{tText, 0, "to be or not"}, tstEOF}}, + {"no markup", `{{< sc1 >}}`, []item{tstLeftNoMD, tstSC1, tstRightNoMD, tstEOF}}, + {"with EOL", "{{< sc1 \n >}}", []item{tstLeftNoMD, tstSC1, tstRightNoMD, tstEOF}}, + + {"simple with markup", `{{% sc1 %}}`, []item{tstLeftMD, tstSC1, tstRightMD, tstEOF}}, + {"with spaces", `{{< sc1 >}}`, []item{tstLeftNoMD, tstSC1, tstRightNoMD, tstEOF}}, + {"mismatched rightDelim", `{{< sc1 %}}`, []item{tstLeftNoMD, tstSC1, + {tError, 0, "unrecognized character in shortcode action: U+0025 '%'. Note: Parameters with non-alphanumeric args must be quoted"}}}, + {"inner, markup", `{{% sc1 %}} inner {{% /sc1 %}}`, []item{ + tstLeftMD, + tstSC1, + tstRightMD, + {tText, 0, " inner "}, + tstLeftMD, + tstSCClose, + tstSC1, + tstRightMD, + tstEOF, + }}, + {"close, but no open", `{{< /sc1 >}}`, []item{ + tstLeftNoMD, {tError, 0, "got closing shortcode, but none is open"}}}, + {"close wrong", `{{< sc1 >}}{{< /another >}}`, []item{ + tstLeftNoMD, tstSC1, tstRightNoMD, tstLeftNoMD, tstSCClose, + {tError, 0, "closing tag for shortcode 'another' does not match start tag"}}}, + {"close, but no open, more", `{{< sc1 >}}{{< /sc1 >}}{{< /another >}}`, []item{ + tstLeftNoMD, tstSC1, tstRightNoMD, tstLeftNoMD, tstSCClose, tstSC1, tstRightNoMD, tstLeftNoMD, tstSCClose, + {tError, 0, "closing tag for shortcode 'another' does not match start tag"}}}, + {"close with extra keyword", `{{< sc1 >}}{{< /sc1 keyword>}}`, []item{ + tstLeftNoMD, tstSC1, tstRightNoMD, tstLeftNoMD, tstSCClose, tstSC1, + {tError, 0, "unclosed shortcode"}}}, + {"Youtube id", `{{< sc1 -ziL-Q_456igdO-4 >}}`, []item{ + tstLeftNoMD, tstSC1, {tScParam, 0, "-ziL-Q_456igdO-4"}, tstRightNoMD, tstEOF}}, + {"non-alphanumerics param quoted", `{{< sc1 "-ziL-.%QigdO-4" >}}`, []item{ + tstLeftNoMD, tstSC1, {tScParam, 0, "-ziL-.%QigdO-4"}, tstRightNoMD, tstEOF}}, + + {"two params", `{{< sc1 param1 param2 >}}`, []item{ + tstLeftNoMD, tstSC1, tstParam1, tstParam2, tstRightNoMD, tstEOF}}, + // issue #934 + {"self-closing", `{{< sc1 />}}`, []item{ + tstLeftNoMD, tstSC1, tstSCClose, tstRightNoMD, tstEOF}}, + // Issue 2498 + {"multiple self-closing", `{{< sc1 />}}{{< sc1 />}}`, []item{ + tstLeftNoMD, tstSC1, tstSCClose, tstRightNoMD, + tstLeftNoMD, tstSC1, tstSCClose, tstRightNoMD, tstEOF}}, + {"self-closing with param", `{{< sc1 param1 />}}`, []item{ + tstLeftNoMD, tstSC1, tstParam1, tstSCClose, tstRightNoMD, tstEOF}}, + {"multiple self-closing with param", `{{< sc1 param1 />}}{{< sc1 param1 />}}`, []item{ + tstLeftNoMD, tstSC1, tstParam1, tstSCClose, tstRightNoMD, + tstLeftNoMD, tstSC1, tstParam1, tstSCClose, tstRightNoMD, tstEOF}}, + {"multiple different self-closing with param", `{{< sc1 param1 />}}{{< sc2 param1 />}}`, []item{ + tstLeftNoMD, tstSC1, tstParam1, tstSCClose, tstRightNoMD, + tstLeftNoMD, tstSC2, tstParam1, tstSCClose, tstRightNoMD, tstEOF}}, + {"nested simple", `{{< sc1 >}}{{< sc2 >}}{{< /sc1 >}}`, []item{ + tstLeftNoMD, tstSC1, tstRightNoMD, + tstLeftNoMD, tstSC2, tstRightNoMD, + tstLeftNoMD, tstSCClose, tstSC1, tstRightNoMD, tstEOF}}, + {"nested complex", `{{< sc1 >}}ab{{% sc2 param1 %}}cd{{< sc3 >}}ef{{< /sc3 >}}gh{{% /sc2 %}}ij{{< /sc1 >}}kl`, []item{ + tstLeftNoMD, tstSC1, tstRightNoMD, + {tText, 0, "ab"}, + tstLeftMD, tstSC2, tstParam1, tstRightMD, + {tText, 0, "cd"}, + tstLeftNoMD, tstSC3, tstRightNoMD, + {tText, 0, "ef"}, + tstLeftNoMD, tstSCClose, tstSC3, tstRightNoMD, + {tText, 0, "gh"}, + tstLeftMD, tstSCClose, tstSC2, tstRightMD, + {tText, 0, "ij"}, + tstLeftNoMD, tstSCClose, tstSC1, tstRightNoMD, + {tText, 0, "kl"}, tstEOF, + }}, + + {"two quoted params", `{{< sc1 "param nr. 1" "param nr. 2" >}}`, []item{ + tstLeftNoMD, tstSC1, {tScParam, 0, "param nr. 1"}, {tScParam, 0, "param nr. 2"}, tstRightNoMD, tstEOF}}, + {"two named params", `{{< sc1 param1="Hello World" param2="p2Val">}}`, []item{ + tstLeftNoMD, tstSC1, tstParam1, tstVal, tstParam2, {tScParamVal, 0, "p2Val"}, tstRightNoMD, tstEOF}}, + {"escaped quotes", `{{< sc1 param1=\"Hello World\" >}}`, []item{ + tstLeftNoMD, tstSC1, tstParam1, tstVal, tstRightNoMD, tstEOF}}, + {"escaped quotes, positional param", `{{< sc1 \"param1\" >}}`, []item{ + tstLeftNoMD, tstSC1, tstParam1, tstRightNoMD, tstEOF}}, + {"escaped quotes inside escaped quotes", `{{< sc1 param1=\"Hello \"escaped\" World\" >}}`, []item{ + tstLeftNoMD, tstSC1, tstParam1, + {tScParamVal, 0, `Hello `}, {tError, 0, `got positional parameter 'escaped'. Cannot mix named and positional parameters`}}}, + {"escaped quotes inside nonescaped quotes", + `{{< sc1 param1="Hello \"escaped\" World" >}}`, []item{ + tstLeftNoMD, tstSC1, tstParam1, {tScParamVal, 0, `Hello "escaped" World`}, tstRightNoMD, tstEOF}}, + {"escaped quotes inside nonescaped quotes in positional param", + `{{< sc1 "Hello \"escaped\" World" >}}`, []item{ + tstLeftNoMD, tstSC1, {tScParam, 0, `Hello "escaped" World`}, tstRightNoMD, tstEOF}}, + {"unterminated quote", `{{< sc1 param2="Hello World>}}`, []item{ + tstLeftNoMD, tstSC1, tstParam2, {tError, 0, "unterminated quoted string in shortcode parameter-argument: 'Hello World>}}'"}}}, + {"one named param, one not", `{{< sc1 param1="Hello World" p2 >}}`, []item{ + tstLeftNoMD, tstSC1, tstParam1, tstVal, + {tError, 0, "got positional parameter 'p2'. Cannot mix named and positional parameters"}}}, + {"one named param, one quoted positional param", `{{< sc1 param1="Hello World" "And Universe" >}}`, []item{ + tstLeftNoMD, tstSC1, tstParam1, tstVal, + {tError, 0, "got quoted positional parameter. Cannot mix named and positional parameters"}}}, + {"one quoted positional param, one named param", `{{< sc1 "param1" param2="And Universe" >}}`, []item{ + tstLeftNoMD, tstSC1, tstParam1, + {tError, 0, "got named parameter 'param2'. Cannot mix named and positional parameters"}}}, + {"ono positional param, one not", `{{< sc1 param1 param2="Hello World">}}`, []item{ + tstLeftNoMD, tstSC1, tstParam1, + {tError, 0, "got named parameter 'param2'. Cannot mix named and positional parameters"}}}, + {"commented out", `{{</* sc1 */>}}`, []item{ + {tText, 0, "{{<"}, {tText, 0, " sc1 "}, {tText, 0, ">}}"}, tstEOF}}, + {"commented out, missing close", `{{</* sc1 >}}`, []item{ + {tError, 0, "comment must be closed"}}}, + {"commented out, misplaced close", `{{</* sc1 >}}*/`, []item{ + {tText, 0, "{{<"}, {tText, 0, " sc1 >}}"}, {tError, 0, "comment ends before the right shortcode delimiter"}}}, +} + +func TestShortcodeLexer(t *testing.T) { + t.Parallel() + for i, test := range shortCodeLexerTests { + items := collect(&test) + if !equal(items, test.items) { + t.Errorf("[%d] %s: got\n\t%v\nexpected\n\t%v", i, test.name, items, test.items) + } + } +} + +func BenchmarkShortcodeLexer(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, test := range shortCodeLexerTests { + items := collect(&test) + if !equal(items, test.items) { + b.Errorf("%s: got\n\t%v\nexpected\n\t%v", test.name, items, test.items) + } + } + } +} + +func collect(t *shortCodeLexerTest) (items []item) { + l := newShortcodeLexer(t.name, t.input, 0) + for { + item := l.nextItem() + items = append(items, item) + if item.typ == tEOF || item.typ == tError { + break + } + } + return +} + +// no positional checking, for now ... +func equal(i1, i2 []item) bool { + if len(i1) != len(i2) { + return false + } + for k := range i1 { + if i1[k].typ != i2[k].typ { + return false + } + if i1[k].val != i2[k].val { + return false + } + } + return true +} diff --git a/hugolib/site.go b/hugolib/site.go index acd3b5410..8aa1e087f 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -1,4 +1,4 @@ -// Copyright 2024 The Hugo Authors. All rights reserved. +// Copyright 2017 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,116 +14,114 @@ package hugolib import ( - "context" "errors" "fmt" + "html/template" "io" "mime" "net/url" "os" "path/filepath" - "runtime" "sort" + "strconv" "strings" "sync" "time" - "github.com/bep/logg" - "github.com/gohugoio/hugo/cache/dynacache" - "github.com/gohugoio/hugo/common/htime" - "github.com/gohugoio/hugo/common/hugio" - "github.com/gohugoio/hugo/common/hugo" - "github.com/gohugoio/hugo/common/loggers" - "github.com/gohugoio/hugo/common/maps" - "github.com/gohugoio/hugo/common/para" - "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/hugolib/doctree" - "github.com/gohugoio/hugo/hugolib/pagesfromdata" - "github.com/gohugoio/hugo/internal/js/esbuild" - "github.com/gohugoio/hugo/internal/warpc" - "github.com/gohugoio/hugo/langs/i18n" - "github.com/gohugoio/hugo/modules" - "github.com/gohugoio/hugo/resources" - "github.com/gohugoio/hugo/tpl/tplimpl" - "github.com/gohugoio/hugo/tpl/tplimplinit" - xmaps "golang.org/x/exp/maps" + "github.com/gohugoio/hugo/media" - // Loads the template funcs namespaces. + "github.com/bep/inflect" - "golang.org/x/text/unicode/norm" - - "github.com/gohugoio/hugo/common/paths" - - "github.com/gohugoio/hugo/identity" - - "github.com/gohugoio/hugo/markup/converter/hooks" - - "github.com/gohugoio/hugo/markup/converter" - - "github.com/gohugoio/hugo/common/text" - - "github.com/gohugoio/hugo/publisher" - - "github.com/gohugoio/hugo/langs" - - "github.com/gohugoio/hugo/resources/kinds" - "github.com/gohugoio/hugo/resources/page" - "github.com/gohugoio/hugo/resources/page/pagemeta" - "github.com/gohugoio/hugo/resources/page/siteidentities" - "github.com/gohugoio/hugo/resources/resource" - - "github.com/gohugoio/hugo/lazy" + "sync/atomic" "github.com/fsnotify/fsnotify" bp "github.com/gohugoio/hugo/bufferpool" + "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/navigation" "github.com/gohugoio/hugo/output" + "github.com/gohugoio/hugo/parser" + "github.com/gohugoio/hugo/source" "github.com/gohugoio/hugo/tpl" + "github.com/gohugoio/hugo/transform" + "github.com/spf13/afero" + "github.com/spf13/cast" + "github.com/spf13/nitro" + "github.com/spf13/viper" ) -var _ page.Site = (*Site)(nil) +var _ = transform.AbsURL -type siteState int +// used to indicate if run as a test. +var testMode bool -const ( - siteStateInit siteState = iota - siteStateReady -) +var defaultTimer *nitro.B +// Site contains all the information relevant for constructing a static +// site. The basic flow of information is as follows: +// +// 1. A list of Files is parsed and then converted into Pages. +// +// 2. Pages contain sections (based on the file they were generated from), +// aliases and slugs (included in a pages frontmatter) which are the +// various targets that will get generated. There will be canonical +// listing. The canonical path can be overruled based on a pattern. +// +// 3. Taxonomies are created via configuration and will present some aspect of +// the final page and typically a perm url. +// +// 4. All Pages are passed through a template based on their desired +// layout based on numerous different elements. +// +// 5. The entire collection of files is written to disk. type Site struct { - state siteState - conf *allconfig.Config - language *langs.Language - languagei int - pageMap *pageMap - store *maps.Scratch + owner *HugoSites - // The owning container. - h *HugoSites + *PageCollections - *deps.Deps + Files []*source.File + Taxonomies TaxonomyList - // Page navigation. - *pageFinder - taxonomies page.TaxonomyList - menus navigation.Menus + // Plural is what we get in the folder, so keep track of this mapping + // to get the singular form from that value. + taxonomiesPluralSingular map[string]string - // Shortcut to the home page. Note that this may be nil if - // home page, for some odd reason, is disabled. - home *pageState + // This is temporary, see https://github.com/gohugoio/hugo/issues/2835 + // Maps "actors-gerard-depardieu" to "Gérard Depardieu" when preserveTaxonomyNames + // is set. + taxonomiesOrigKey map[string]string - // The last modification date of this site. - lastmod time.Time + Source source.Input + Sections Taxonomy + Info SiteInfo + Menus Menus + timer *nitro.B - relatedDocsHandler *page.RelatedDocsHandler - siteRefLinker - publisher publisher.Publisher - frontmatterHandler pagemeta.FrontMatterHandler + layoutHandler *output.LayoutHandler + + draftCount int + futureCount int + expiredCount int + Data map[string]interface{} + Language *helpers.Language + + disabledKinds map[string]bool + + // Output formats defined in site config per Page Kind, or some defaults + // if not set. + // Output formats defined in Page front matter will override these. + outputFormats map[string]output.Formats + + // All the output formats and media types available for this site. + // These values will be merged from the Hugo defaults, the site config and, + // finally, the language settings. + outputFormatsConfig output.Formats + mediaTypesConfig media.Types + + // We render each site for all the relevant output formats in serial with + // this rendering context pointing to the current one. + rc *siteRenderingContext // The output formats that we need to render this site in. This slice // will be fixed once set. @@ -131,703 +129,24 @@ type Site struct { // This slice will be sorted. renderFormats output.Formats - // Lazily loaded site dependencies - init *siteInit + // Logger etc. + *deps.Deps `json:"-"` + + siteStats *siteStats } -func (s *Site) Debug() { - fmt.Println("Debugging site", s.Lang(), "=>") - // fmt.Println(s.pageMap.testDump()) -} - -// NewHugoSites creates HugoSites from the given config. -func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) { - conf := cfg.Configs.GetFirstLanguageConfig() - - var logger loggers.Logger - if cfg.TestLogger != nil { - logger = cfg.TestLogger - } else { - var logHookLast func(e *logg.Entry) error - if cfg.Configs.Base.PanicOnWarning { - logHookLast = loggers.PanicOnWarningHook - } - if cfg.StdOut == nil { - cfg.StdOut = os.Stdout - } - if cfg.StdErr == nil { - cfg.StdErr = os.Stderr - } - if cfg.LogLevel == 0 { - cfg.LogLevel = logg.LevelWarn - } - - logOpts := loggers.Options{ - Level: cfg.LogLevel, - DistinctLevel: logg.LevelWarn, // This will drop duplicate log warning and errors. - HandlerPost: logHookLast, - StdOut: cfg.StdOut, - StdErr: cfg.StdErr, - StoreErrors: conf.Watching(), - SuppressStatements: conf.IgnoredLogs(), - } - logger = loggers.New(logOpts) - - } - - memCache := dynacache.New(dynacache.Options{Watching: conf.Watching(), Log: logger}) - - var h *HugoSites - onSignalRebuild := func(ids ...identity.Identity) { - // This channel is buffered, but make sure we do this in a non-blocking way. - if cfg.ChangesFromBuild != nil { - go func() { - cfg.ChangesFromBuild <- ids - }() - } - } - - firstSiteDeps := &deps.Deps{ - Fs: cfg.Fs, - Log: logger, - Conf: conf, - BuildState: &deps.BuildState{ - OnSignalRebuild: onSignalRebuild, - }, - Counters: &deps.Counters{}, - MemCache: memCache, - TranslationProvider: i18n.NewTranslationProvider(), - WasmDispatchers: warpc.AllDispatchers( - warpc.Options{ - CompilationCacheDir: filepath.Join(conf.Dirs().CacheDir, "_warpc"), - - // Katex is relatively slow. - PoolSize: 8, - Infof: logger.InfoCommand("wasm").Logf, - Warnf: logger.WarnCommand("wasm").Logf, - }, - ), - } - - if err := firstSiteDeps.Init(); err != nil { - return nil, err - } - - batcherClient, err := esbuild.NewBatcherClient(firstSiteDeps) - if err != nil { - return nil, err - } - firstSiteDeps.JSBatcherClient = batcherClient - - confm := cfg.Configs - if err := confm.Validate(logger); err != nil { - return nil, err - } - var sites []*Site - - ns := &contentNodeShifter{ - numLanguages: len(confm.Languages), - } - - treeConfig := doctree.Config[contentNodeI]{ - Shifter: ns, - } - - pageTrees := &pageTrees{ - treePages: doctree.New( - treeConfig, - ), - treeResources: doctree.New( - treeConfig, - ), - treeTaxonomyEntries: doctree.NewTreeShiftTree[*weightedContentNode](doctree.DimensionLanguage.Index(), len(confm.Languages)), - treePagesFromTemplateAdapters: doctree.NewTreeShiftTree[*pagesfromdata.PagesFromTemplate](doctree.DimensionLanguage.Index(), len(confm.Languages)), - } - - pageTrees.createMutableTrees() - - for i, confp := range confm.ConfigLangs() { - language := confp.Language() - if language.Disabled { - continue - } - k := language.Lang - conf := confm.LanguageConfigMap[k] - frontmatterHandler, err := pagemeta.NewFrontmatterHandler(firstSiteDeps.Log, conf.Frontmatter) - if err != nil { - return nil, err - } - - langs.SetParams(language, conf.Params) - - s := &Site{ - conf: conf, - language: language, - languagei: i, - frontmatterHandler: frontmatterHandler, - store: maps.NewScratch(), - } - - if i == 0 { - firstSiteDeps.Site = s - s.Deps = firstSiteDeps - } else { - d, err := firstSiteDeps.Clone(s, confp) - if err != nil { - return nil, err - } - s.Deps = d - } - - s.pageMap = newPageMap(i, s, memCache, pageTrees) - - s.pageFinder = newPageFinder(s.pageMap) - s.siteRefLinker, err = newSiteRefLinker(s) - if err != nil { - return nil, err - } - // Set up the main publishing chain. - pub, err := publisher.NewDestinationPublisher( - firstSiteDeps.ResourceSpec, - s.conf.OutputFormats.Config, - s.conf.MediaTypes.Config, - ) - if err != nil { - return nil, err - } - - s.publisher = pub - s.relatedDocsHandler = page.NewRelatedDocsHandler(s.conf.Related) - // Site deps end. - - s.prepareInits() - sites = append(sites, s) - } - - if len(sites) == 0 { - return nil, errors.New("no sites to build") - } - - // Pull the default content language to the top, then sort the sites by language weight (if set) or lang. - defaultContentLanguage := confm.Base.DefaultContentLanguage - sort.Slice(sites, func(i, j int) bool { - li := sites[i].language - lj := sites[j].language - if li.Lang == defaultContentLanguage { - return true - } - - if lj.Lang == defaultContentLanguage { - return false - } - - if li.Weight != lj.Weight { - return li.Weight < lj.Weight - } - return li.Lang < lj.Lang - }) - - h, err = newHugoSites(cfg, firstSiteDeps, pageTrees, sites) - if err == nil && h == nil { - panic("hugo: newHugoSitesNew returned nil error and nil HugoSites") - } - - return h, err -} - -func newHugoSites(cfg deps.DepsCfg, d *deps.Deps, pageTrees *pageTrees, sites []*Site) (*HugoSites, error) { - numWorkers := config.GetNumWorkerMultiplier() - numWorkersSite := min(numWorkers, len(sites)) - workersSite := para.New(numWorkersSite) - - h := &HugoSites{ - Sites: sites, - Deps: sites[0].Deps, - Configs: cfg.Configs, - workersSite: workersSite, - numWorkersSites: numWorkers, - numWorkers: numWorkers, - pageTrees: pageTrees, - cachePages: dynacache.GetOrCreatePartition[string, - page.Pages](d.MemCache, "/pags/all", - dynacache.OptionsPartition{Weight: 10, ClearWhen: dynacache.ClearOnRebuild}, - ), - cacheContentSource: dynacache.GetOrCreatePartition[string, *resources.StaleValue[[]byte]](d.MemCache, "/cont/src", dynacache.OptionsPartition{Weight: 70, ClearWhen: dynacache.ClearOnChange}), - translationKeyPages: maps.NewSliceCache[page.Page](), - currentSite: sites[0], - skipRebuildForFilenames: make(map[string]bool), - init: &hugoSitesInit{ - data: lazy.New(), - gitInfo: lazy.New(), - }, - } - - // Assemble dependencies to be used in hugo.Deps. - var dependencies []*hugo.Dependency - var depFromMod func(m modules.Module) *hugo.Dependency - depFromMod = func(m modules.Module) *hugo.Dependency { - dep := &hugo.Dependency{ - Path: m.Path(), - Version: m.Version(), - Time: m.Time(), - Vendor: m.Vendor(), - } - - // These are pointers, but this all came from JSON so there's no recursive navigation, - // so just create new values. - if m.Replace() != nil { - dep.Replace = depFromMod(m.Replace()) - } - if m.Owner() != nil { - dep.Owner = depFromMod(m.Owner()) - } - return dep - } - for _, m := range d.Paths.AllModules() { - dependencies = append(dependencies, depFromMod(m)) - } - - h.hugoInfo = hugo.NewInfo(h.Configs.GetFirstLanguageConfig(), dependencies) - - var prototype *deps.Deps - for i, s := range sites { - s.h = h - // The template store needs to be initialized after the h container is set on s. - if i == 0 { - templateStore, err := tplimpl.NewStore( - tplimpl.StoreOptions{ - Fs: s.BaseFs.Layouts.Fs, - Log: s.Log, - DefaultContentLanguage: s.Conf.DefaultContentLanguage(), - Watching: s.Conf.Watching(), - PathParser: s.Conf.PathParser(), - Metrics: d.Metrics, - OutputFormats: s.conf.OutputFormats.Config, - MediaTypes: s.conf.MediaTypes.Config, - DefaultOutputFormat: s.conf.DefaultOutputFormat, - TaxonomySingularPlural: s.conf.Taxonomies, - }, tplimpl.SiteOptions{ - Site: s, - TemplateFuncs: tplimplinit.CreateFuncMap(s.Deps), - }) - if err != nil { - return nil, err - } - s.Deps.TemplateStore = templateStore - } else { - s.Deps.TemplateStore = prototype.TemplateStore.WithSiteOpts( - tplimpl.SiteOptions{ - Site: s, - TemplateFuncs: tplimplinit.CreateFuncMap(s.Deps), - }) - } - if err := s.Deps.Compile(prototype); err != nil { - return nil, err - } - if i == 0 { - prototype = s.Deps - } - } - - h.fatalErrorHandler = &fatalErrorHandler{ - h: h, - donec: make(chan bool), - } - - h.init.data.Add(func(context.Context) (any, error) { - err := h.loadData() - if err != nil { - return nil, fmt.Errorf("failed to load data: %w", err) - } - return nil, nil - }) - - h.init.gitInfo.Add(func(context.Context) (any, error) { - err := h.loadGitInfo() - if err != nil { - return nil, fmt.Errorf("failed to load Git info: %w", err) - } - return nil, nil - }) - - return h, nil -} - -// Returns the server port. -func (s *Site) ServerPort() int { - return s.conf.C.BaseURL.Port() -} - -// Returns the configured title for this Site. -func (s *Site) Title() string { - return s.conf.Title -} - -func (s *Site) Copyright() string { - return s.conf.Copyright -} - -func (s *Site) Config() page.SiteConfig { - return page.SiteConfig{ - Privacy: s.conf.Privacy, - Services: s.conf.Services, - } -} - -func (s *Site) LanguageCode() string { - return s.Language().LanguageCode() -} - -// Returns all Sites for all languages. -func (s *Site) Sites() page.Sites { - sites := make(page.Sites, len(s.h.Sites)) - for i, s := range s.h.Sites { - sites[i] = s.Site() - } - return sites -} - -// Returns Site currently rendering. -func (s *Site) Current() page.Site { - return s.h.currentSite -} - -// MainSections returns the list of main sections. -func (s *Site) MainSections() []string { - s.CheckReady() - return s.conf.C.MainSections -} - -// Returns a struct with some information about the build. -func (s *Site) Hugo() hugo.HugoInfo { - if s.h == nil { - panic("site: hugo: h not initialized") - } - if s.h.hugoInfo.Environment == "" { - panic("site: hugo: hugoInfo not initialized") - } - return s.h.hugoInfo -} - -// Returns the BaseURL for this Site. -func (s *Site) BaseURL() string { - return s.conf.C.BaseURL.WithPath -} - -// Deprecated: Use .Site.Lastmod instead. -func (s *Site) LastChange() time.Time { - s.CheckReady() - hugo.Deprecate(".Site.LastChange", "Use .Site.Lastmod instead.", "v0.123.0") - return s.lastmod -} - -// Returns the last modification date of the content. -func (s *Site) Lastmod() time.Time { - return s.lastmod -} - -// Returns the Params configured for this site. -func (s *Site) Params() maps.Params { - return s.conf.Params -} - -// Deprecated: Use taxonomies instead. -func (s *Site) Author() map[string]any { - if len(s.conf.Author) != 0 { - hugo.Deprecate(".Site.Author", "Implement taxonomy 'author' or use .Site.Params.Author instead.", "v0.124.0") - } - return s.conf.Author -} - -// Deprecated: Use taxonomies instead. -func (s *Site) Authors() page.AuthorList { - hugo.Deprecate(".Site.Authors", "Implement taxonomy 'authors' or use .Site.Params.Author instead.", "v0.124.0") - return page.AuthorList{} -} - -// Deprecated: Use .Site.Params instead. -func (s *Site) Social() map[string]string { - hugo.Deprecate(".Site.Social", "Implement taxonomy 'social' or use .Site.Params.Social instead.", "v0.124.0") - return s.conf.Social -} - -func (s *Site) Param(key any) (any, error) { - return resource.Param(s, nil, key) -} - -// Returns a map of all the data inside /data. -func (s *Site) Data() map[string]any { - return s.s.h.Data() -} - -func (s *Site) BuildDrafts() bool { - return s.conf.BuildDrafts -} - -// Deprecated: Use hugo.IsMultilingual instead. -func (s *Site) IsMultiLingual() bool { - hugo.Deprecate(".Site.IsMultiLingual", "Use hugo.IsMultilingual instead.", "v0.124.0") - return s.h.isMultilingual() -} - -func (s *Site) LanguagePrefix() string { - prefix := s.GetLanguagePrefix() - if prefix == "" { - return "" - } - return "/" + prefix -} - -func (s *Site) Site() page.Site { - return page.WrapSite(s) -} - -func (s *Site) ForEeachIdentityByName(name string, f func(identity.Identity) bool) { - if id, found := siteidentities.FromString(name); found { - if f(id) { - return - } - } -} - -// Pages returns all pages. -// This is for the current language only. -func (s *Site) Pages() page.Pages { - s.CheckReady() - return s.pageMap.getPagesInSection( - pageMapQueryPagesInSection{ - pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{ - Path: "", - KeyPart: "global", - Include: pagePredicates.ShouldListGlobal, - }, - Recursive: true, - IncludeSelf: true, - }, - ) -} - -// RegularPages returns all the regular pages. -// This is for the current language only. -func (s *Site) RegularPages() page.Pages { - s.CheckReady() - return s.pageMap.getPagesInSection( - pageMapQueryPagesInSection{ - pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{ - Path: "", - KeyPart: "global", - Include: pagePredicates.ShouldListGlobal.And(pagePredicates.KindPage), - }, - Recursive: true, - }, - ) -} - -// AllPages returns all pages for all sites. -func (s *Site) AllPages() page.Pages { - s.CheckReady() - return s.h.Pages() -} - -// AllRegularPages returns all regular pages for all sites. -func (s *Site) AllRegularPages() page.Pages { - s.CheckReady() - return s.h.RegularPages() -} - -func (s *Site) Store() *maps.Scratch { - return s.store -} - -func (s *Site) CheckReady() { - if s.state != siteStateReady { - panic("this method cannot be called before the site is fully initialized") - } -} - -func (s *Site) Taxonomies() page.TaxonomyList { - s.CheckReady() - s.init.taxonomies.Do(context.Background()) - return s.taxonomies -} - -type ( - taxonomiesConfig map[string]string - taxonomiesConfigValues struct { - views []viewName - viewsByTreeKey map[string]viewName - } -) - -func (t taxonomiesConfig) Values() taxonomiesConfigValues { - var views []viewName - for k, v := range t { - views = append(views, viewName{singular: k, plural: v, pluralTreeKey: cleanTreeKey(v)}) - } - sort.Slice(views, func(i, j int) bool { - return views[i].plural < views[j].plural - }) - - viewsByTreeKey := make(map[string]viewName) - for _, v := range views { - viewsByTreeKey[v.pluralTreeKey] = v - } - - return taxonomiesConfigValues{ - views: views, - viewsByTreeKey: viewsByTreeKey, - } -} - -// Lazily loaded site dependencies. -type siteInit struct { - prevNext *lazy.Init - prevNextInSection *lazy.Init - menus *lazy.Init - taxonomies *lazy.Init -} - -func (init *siteInit) Reset() { - init.prevNext.Reset() - init.prevNextInSection.Reset() - init.menus.Reset() - init.taxonomies.Reset() -} - -func (s *Site) prepareInits() { - s.init = &siteInit{} - - var init lazy.Init - - s.init.prevNext = init.Branch(func(context.Context) (any, error) { - regularPages := s.RegularPages() - if s.conf.Page.NextPrevSortOrder == "asc" { - regularPages = regularPages.Reverse() - } - for i, p := range regularPages { - np, ok := p.(nextPrevProvider) - if !ok { - continue - } - - pos := np.getNextPrev() - if pos == nil { - continue - } - - pos.nextPage = nil - pos.prevPage = nil - - if i > 0 { - pos.nextPage = regularPages[i-1] - } - - if i < len(regularPages)-1 { - pos.prevPage = regularPages[i+1] - } - } - return nil, nil - }) - - s.init.prevNextInSection = init.Branch(func(context.Context) (any, error) { - setNextPrev := func(pas page.Pages) { - for i, p := range pas { - np, ok := p.(nextPrevInSectionProvider) - if !ok { - continue - } - - pos := np.getNextPrevInSection() - if pos == nil { - continue - } - - pos.nextPage = nil - pos.prevPage = nil - - if i > 0 { - pos.nextPage = pas[i-1] - } - - if i < len(pas)-1 { - pos.prevPage = pas[i+1] - } - } - } - - sections := s.pageMap.getPagesInSection( - pageMapQueryPagesInSection{ - pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{ - Path: "", - KeyPart: "sectionorhome", - Include: pagePredicates.KindSection.Or(pagePredicates.KindHome), - }, - IncludeSelf: true, - Recursive: true, - }, - ) - - for _, section := range sections { - ps := section.RegularPages() - if s.conf.Page.NextPrevInSectionSortOrder == "asc" { - ps = ps.Reverse() - } - setNextPrev(ps) - } - - return nil, nil - }) - - s.init.menus = init.Branch(func(context.Context) (any, error) { - err := s.assembleMenus() - return nil, err - }) - - s.init.taxonomies = init.Branch(func(ctx context.Context) (any, error) { - if err := s.pageMap.CreateSiteTaxonomies(ctx); err != nil { - return nil, err - } - return s.taxonomies, nil - }) -} - -func (s *Site) Menus() navigation.Menus { - s.CheckReady() - s.init.menus.Do(context.Background()) - return s.menus +type siteRenderingContext struct { + output.Format } func (s *Site) initRenderFormats() { formatSet := make(map[string]bool) formats := output.Formats{} - - w := &doctree.NodeShiftTreeWalker[contentNodeI]{ - Tree: s.pageMap.treePages, - Handle: func(key string, n contentNodeI, match doctree.DimensionFlag) (bool, error) { - if p, ok := n.(*pageState); ok { - for _, f := range p.m.pageConfig.ConfiguredOutputFormats { - if !formatSet[f.Name] { - formats = append(formats, f) - formatSet[f.Name] = true - } - } - } - return false, nil - }, - } - - if err := w.Walk(context.TODO()); err != nil { - panic(err) - } - - // Add the per kind configured output formats - for _, kind := range kinds.AllKindsInPages { - if siteFormats, found := s.conf.C.KindOutputFormats[kind]; found { - for _, f := range siteFormats { - if !formatSet[f.Name] { - formats = append(formats, f) - formatSet[f.Name] = true - } + for _, p := range s.Pages { + for _, f := range p.outputFormats { + if !formatSet[f.Name] { + formats = append(formats, f) + formatSet[f.Name] = true } } } @@ -836,82 +155,281 @@ func (s *Site) initRenderFormats() { s.renderFormats = formats } -func (s *Site) GetInternalRelatedDocsHandler() *page.RelatedDocsHandler { - return s.relatedDocsHandler +type siteStats struct { + pageCount int + pageCountRegular int } -func (s *Site) Language() *langs.Language { - return s.language -} - -func (s *Site) Languages() langs.Languages { - return s.h.Configs.Languages -} - -type siteRefLinker struct { - s *Site - - errorLogger logg.LevelLogger - notFoundURL string -} - -func newSiteRefLinker(s *Site) (siteRefLinker, error) { - logger := s.Log.Error() - - notFoundURL := s.conf.RefLinksNotFoundURL - errLevel := s.conf.RefLinksErrorLevel - if strings.EqualFold(errLevel, "warning") { - logger = s.Log.Warn() +func (s *Site) isEnabled(kind string) bool { + if kind == kindUnknown { + panic("Unknown kind") } - return siteRefLinker{s: s, errorLogger: logger, notFoundURL: notFoundURL}, nil + return !s.disabledKinds[kind] } -func (s siteRefLinker) logNotFound(ref, what string, p page.Page, position text.Position) { - if position.IsValid() { - s.errorLogger.Logf("[%s] REF_NOT_FOUND: Ref %q: %s: %s", s.s.Lang(), ref, position.String(), what) - } else if p == nil { - s.errorLogger.Logf("[%s] REF_NOT_FOUND: Ref %q: %s", s.s.Lang(), ref, what) - } else { - s.errorLogger.Logf("[%s] REF_NOT_FOUND: Ref %q from page %q: %s", s.s.Lang(), ref, p.Path(), what) +// reset returns a new Site prepared for rebuild. +func (s *Site) reset() *Site { + return &Site{Deps: s.Deps, + layoutHandler: output.NewLayoutHandler(s.PathSpec.ThemeSet()), + disabledKinds: s.disabledKinds, + outputFormats: s.outputFormats, + outputFormatsConfig: s.outputFormatsConfig, + mediaTypesConfig: s.mediaTypesConfig, + Language: s.Language, + owner: s.owner, + PageCollections: newPageCollections()} +} + +// newSite creates a new site with the given configuration. +func newSite(cfg deps.DepsCfg) (*Site, error) { + c := newPageCollections() + + if cfg.Language == nil { + cfg.Language = helpers.NewDefaultLanguage(cfg.Cfg) + } + + disabledKinds := make(map[string]bool) + for _, disabled := range cast.ToStringSlice(cfg.Language.Get("disableKinds")) { + disabledKinds[disabled] = true + } + + var ( + mediaTypesConfig []map[string]interface{} + outputFormatsConfig []map[string]interface{} + + siteOutputFormatsConfig output.Formats + siteMediaTypesConfig media.Types + err error + ) + + // Add language last, if set, so it gets precedence. + for _, cfg := range []config.Provider{cfg.Cfg, cfg.Language} { + if cfg.IsSet("mediaTypes") { + mediaTypesConfig = append(mediaTypesConfig, cfg.GetStringMap("mediaTypes")) + } + if cfg.IsSet("outputFormats") { + outputFormatsConfig = append(outputFormatsConfig, cfg.GetStringMap("outputFormats")) + } + } + + siteMediaTypesConfig, err = media.DecodeTypes(mediaTypesConfig...) + if err != nil { + return nil, err + } + + siteOutputFormatsConfig, err = output.DecodeFormats(siteMediaTypesConfig, outputFormatsConfig...) + if err != nil { + return nil, err + } + + outputFormats, err := createSiteOutputFormats(siteOutputFormatsConfig, cfg.Language) + if err != nil { + return nil, err + } + + s := &Site{ + PageCollections: c, + layoutHandler: output.NewLayoutHandler(cfg.Cfg.GetString("themesDir") != ""), + Language: cfg.Language, + disabledKinds: disabledKinds, + outputFormats: outputFormats, + outputFormatsConfig: siteOutputFormatsConfig, + mediaTypesConfig: siteMediaTypesConfig, + } + + s.Info = newSiteInfo(siteBuilderCfg{s: s, pageCollections: c, language: s.Language}) + + return s, nil + +} + +// NewSite creates a new site with the given dependency configuration. +// The site will have a template system loaded and ready to use. +// Note: This is mainly used in single site tests. +func NewSite(cfg deps.DepsCfg) (*Site, error) { + s, err := newSite(cfg) + if err != nil { + return nil, err + } + + if err = applyDepsIfNeeded(cfg, s); err != nil { + return nil, err + } + + return s, nil +} + +// NewSiteDefaultLang creates a new site in the default language. +// The site will have a template system loaded and ready to use. +// Note: This is mainly used in single site tests. +func NewSiteDefaultLang(withTemplate ...func(templ tpl.TemplateHandler) error) (*Site, error) { + v := viper.New() + loadDefaultSettingsFor(v) + return newSiteForLang(helpers.NewDefaultLanguage(v), withTemplate...) +} + +// NewEnglishSite creates a new site in English language. +// The site will have a template system loaded and ready to use. +// Note: This is mainly used in single site tests. +func NewEnglishSite(withTemplate ...func(templ tpl.TemplateHandler) error) (*Site, error) { + v := viper.New() + loadDefaultSettingsFor(v) + return newSiteForLang(helpers.NewLanguage("en", v), withTemplate...) +} + +// newSiteForLang creates a new site in the given language. +func newSiteForLang(lang *helpers.Language, withTemplate ...func(templ tpl.TemplateHandler) error) (*Site, error) { + withTemplates := func(templ tpl.TemplateHandler) error { + for _, wt := range withTemplate { + if err := wt(templ); err != nil { + return err + } + } + return nil + } + + cfg := deps.DepsCfg{WithTemplate: withTemplates, Language: lang, Cfg: lang} + + return NewSiteForCfg(cfg) + +} + +// NewSiteForCfg creates a new site for the given configuration. +// The site will have a template system loaded and ready to use. +// Note: This is mainly used in single site tests. +func NewSiteForCfg(cfg deps.DepsCfg) (*Site, error) { + s, err := newSite(cfg) + + if err != nil { + return nil, err + } + + if err := applyDepsIfNeeded(cfg, s); err != nil { + return nil, err + } + return s, nil +} + +type SiteInfo struct { + // atomic requires 64-bit alignment for struct field access + // According to the docs, " The first word in a global variable or in an + // allocated struct or slice can be relied upon to be 64-bit aligned." + // Moving paginationPageCount to the top of this struct didn't do the + // magic, maybe due to the way SiteInfo is embedded. + // Adding the 4 byte padding below does the trick. + _ [4]byte + paginationPageCount uint64 + + Taxonomies TaxonomyList + Authors AuthorList + Social SiteSocial + *PageCollections + Files *[]*source.File + Menus *Menus + Hugo *HugoInfo + Title string + RSSLink string + Author map[string]interface{} + LanguageCode string + DisqusShortname string + GoogleAnalytics string + Copyright string + LastChange time.Time + Permalinks PermalinkOverrides + Params map[string]interface{} + BuildDrafts bool + canonifyURLs bool + relativeURLs bool + uglyURLs bool + preserveTaxonomyNames bool + Data *map[string]interface{} + + owner *HugoSites + s *Site + multilingual *Multilingual + Language *helpers.Language + LanguagePrefix string + Languages helpers.Languages + defaultContentLanguageInSubdir bool + sectionPagesMenu string +} + +func (s *SiteInfo) String() string { + return fmt.Sprintf("Site(%q)", s.Title) +} + +func (s *SiteInfo) BaseURL() template.URL { + return template.URL(s.s.PathSpec.BaseURL.String()) +} + +// Used in tests. + +type siteBuilderCfg struct { + language *helpers.Language + s *Site + pageCollections *PageCollections + baseURL string +} + +// TODO(bep) get rid of this +func newSiteInfo(cfg siteBuilderCfg) SiteInfo { + return SiteInfo{ + s: cfg.s, + multilingual: newMultiLingualForLanguage(cfg.language), + PageCollections: cfg.pageCollections, + Params: make(map[string]interface{}), } } -func (s *siteRefLinker) refLink(ref string, source any, relative bool, outputFormat string) (string, error) { - p, err := unwrapPage(source) +// SiteSocial is a place to put social details on a site level. These are the +// standard keys that themes will expect to have available, but can be +// expanded to any others on a per site basis +// github +// facebook +// facebook_admin +// twitter +// twitter_domain +// googleplus +// pinterest +// instagram +// youtube +// linkedin +type SiteSocial map[string]string + +// Param is a convenience method to do lookups in SiteInfo's Params map. +// +// This method is also implemented on Page and Node. +func (s *SiteInfo) Param(key interface{}) (interface{}, error) { + keyStr, err := cast.ToStringE(key) + if err != nil { + return nil, err + } + keyStr = strings.ToLower(keyStr) + return s.Params[keyStr], nil +} + +func (s *SiteInfo) IsMultiLingual() bool { + return len(s.Languages) > 1 +} + +func (s *SiteInfo) refLink(ref string, page *Page, relative bool, outputFormat string) (string, error) { + var refURL *url.URL + var err error + + refURL, err = url.Parse(ref) + if err != nil { return "", err } - var refURL *url.URL - - ref = filepath.ToSlash(ref) - - refURL, err = url.Parse(ref) - if err != nil { - return s.notFoundURL, err - } - - var target page.Page + var target *Page var link string if refURL.Path != "" { - var err error - target, err = s.s.getPageRef(p, refURL.Path) - var pos text.Position - if err != nil || target == nil { - if p, ok := source.(text.Positioner); ok { - pos = p.Position() - } - } - - if err != nil { - s.logNotFound(refURL.Path, err.Error(), p, pos) - return s.notFoundURL, nil - } + target := s.getPage(KindPage, refURL.Path) if target == nil { - s.logNotFound(refURL.Path, "page not found", p, pos) - return s.notFoundURL, nil + return "", fmt.Errorf("No page found with path or logical name \"%s\".\n", refURL.Path) } var permalinker Permalinker = target @@ -920,8 +438,7 @@ func (s *siteRefLinker) refLink(ref string, source any, relative bool, outputFor o := target.OutputFormats().Get(outputFormat) if o == nil { - s.logNotFound(refURL.Path, fmt.Sprintf("output format %q", outputFormat), p, pos) - return s.notFoundURL, nil + return "", fmt.Errorf("Output format %q not found for page %q", outputFormat, refURL.Path) } permalinker = o } @@ -934,94 +451,214 @@ func (s *siteRefLinker) refLink(ref string, source any, relative bool, outputFor } if refURL.Fragment != "" { - _ = target link = link + "#" + refURL.Fragment - if pctx, ok := target.(pageContext); ok { - if refURL.Path != "" { - if di, ok := pctx.getContentConverter().(converter.DocumentInfo); ok { - link = link + di.AnchorSuffix() - } - } - } else if pctx, ok := p.(pageContext); ok { - if di, ok := pctx.getContentConverter().(converter.DocumentInfo); ok { - link = link + di.AnchorSuffix() - } + if refURL.Path != "" && target != nil && !target.getRenderingConfig().PlainIDAnchors { + link = link + ":" + target.UniqueID() + } else if page != nil && !page.getRenderingConfig().PlainIDAnchors { + link = link + ":" + page.UniqueID() } - } return link, nil } -func (s *Site) watching() bool { - return s.h != nil && s.h.Configs.Base.Internal.Watch -} - -type WhatChanged struct { - mu sync.Mutex - - needsPagesAssembly bool - - ids map[identity.Identity]bool -} - -func (w *WhatChanged) init() { - if w.ids == nil { - w.ids = make(map[identity.Identity]bool) +// Ref will give an absolute URL to ref in the given Page. +func (s *SiteInfo) Ref(ref string, page *Page, options ...string) (string, error) { + outputFormat := "" + if len(options) > 0 { + outputFormat = options[0] } + + return s.refLink(ref, page, false, outputFormat) } -func (w *WhatChanged) Add(ids ...identity.Identity) { - w.mu.Lock() - defer w.mu.Unlock() - - w.init() - - for _, id := range ids { - w.ids[id] = true +// RelRef will give an relative URL to ref in the given Page. +func (s *SiteInfo) RelRef(ref string, page *Page, options ...string) (string, error) { + outputFormat := "" + if len(options) > 0 { + outputFormat = options[0] } + + return s.refLink(ref, page, true, outputFormat) } -func (w *WhatChanged) Clear() { - w.mu.Lock() - defer w.mu.Unlock() - w.clear() -} +// SourceRelativeLink attempts to convert any source page relative links (like [../another.md]) into absolute links +func (s *SiteInfo) SourceRelativeLink(ref string, currentPage *Page) (string, error) { + var refURL *url.URL + var err error -func (w *WhatChanged) clear() { - w.ids = nil -} - -func (w *WhatChanged) Changes() []identity.Identity { - if w == nil || w.ids == nil { - return nil + refURL, err = url.Parse(strings.TrimPrefix(ref, currentPage.getRenderingConfig().SourceRelativeLinksProjectFolder)) + if err != nil { + return "", err } - return xmaps.Keys(w.ids) + + if refURL.Scheme != "" { + // Not a relative source level path + return ref, nil + } + + var target *Page + var link string + + if refURL.Path != "" { + refPath := filepath.Clean(filepath.FromSlash(refURL.Path)) + + if strings.IndexRune(refPath, os.PathSeparator) == 0 { // filepath.IsAbs fails to me. + refPath = refPath[1:] + } else { + if currentPage != nil { + refPath = filepath.Join(currentPage.Source.Dir(), refURL.Path) + } + } + + for _, page := range s.AllRegularPages { + if page.Source.Path() == refPath { + target = page + break + } + } + // need to exhaust the test, then try with the others :/ + // if the refPath doesn't end in a filename with extension `.md`, then try with `.md` , and then `/index.md` + mdPath := strings.TrimSuffix(refPath, string(os.PathSeparator)) + ".md" + for _, page := range s.AllRegularPages { + if page.Source.Path() == mdPath { + target = page + break + } + } + indexPath := filepath.Join(refPath, "index.md") + for _, page := range s.AllRegularPages { + if page.Source.Path() == indexPath { + target = page + break + } + } + + if target == nil { + return "", fmt.Errorf("No page found for \"%s\" on page \"%s\".\n", ref, currentPage.Source.Path()) + } + + link = target.RelPermalink() + + } + + if refURL.Fragment != "" { + link = link + "#" + refURL.Fragment + + if refURL.Path != "" && target != nil && !target.getRenderingConfig().PlainIDAnchors { + link = link + ":" + target.UniqueID() + } else if currentPage != nil && !currentPage.getRenderingConfig().PlainIDAnchors { + link = link + ":" + currentPage.UniqueID() + } + } + + return link, nil } -func (w *WhatChanged) Drain() []identity.Identity { - w.mu.Lock() - defer w.mu.Unlock() - ids := w.Changes() - w.clear() - return ids +// SourceRelativeLinkFile attempts to convert any non-md source relative links (like [../another.gif]) into absolute links +func (s *SiteInfo) SourceRelativeLinkFile(ref string, currentPage *Page) (string, error) { + var refURL *url.URL + var err error + + refURL, err = url.Parse(strings.TrimPrefix(ref, currentPage.getRenderingConfig().SourceRelativeLinksProjectFolder)) + if err != nil { + return "", err + } + + if refURL.Scheme != "" { + // Not a relative source level path + return ref, nil + } + + var target *source.File + var link string + + if refURL.Path != "" { + refPath := filepath.Clean(filepath.FromSlash(refURL.Path)) + + if strings.IndexRune(refPath, os.PathSeparator) == 0 { // filepath.IsAbs fails to me. + refPath = refPath[1:] + } else { + if currentPage != nil { + refPath = filepath.Join(currentPage.Source.Dir(), refURL.Path) + } + } + + for _, file := range *s.Files { + if file.Path() == refPath { + target = file + break + } + } + + if target == nil { + return "", fmt.Errorf("No file found for \"%s\" on page \"%s\".\n", ref, currentPage.Source.Path()) + } + + link = target.Path() + return "/" + filepath.ToSlash(link), nil + } + + return "", fmt.Errorf("failed to find a file to match \"%s\" on page \"%s\"", ref, currentPage.Source.Path()) +} + +func (s *SiteInfo) addToPaginationPageCount(cnt uint64) { + atomic.AddUint64(&s.paginationPageCount, cnt) +} + +type runmode struct { + Watching bool +} + +func (s *Site) running() bool { + return s.owner.runMode.Watching +} + +func init() { + defaultTimer = nitro.Initalize() +} + +func (s *Site) timerStep(step string) { + if s.timer == nil { + s.timer = defaultTimer + } + s.timer.Step(step) +} + +type whatChanged struct { + source bool + other bool } // RegisterMediaTypes will register the Site's media types in the mime // package, so it will behave correctly with Hugo's built-in server. func (s *Site) RegisterMediaTypes() { - for _, mt := range s.conf.MediaTypes.Config { - for _, suffix := range mt.Suffixes() { - _ = mime.AddExtensionType(mt.Delimiter+suffix, mt.Type) - } + for _, mt := range s.mediaTypesConfig { + // The last one will win if there are any duplicates. + _ = mime.AddExtensionType("."+mt.Suffix, mt.Type()+"; charset=utf-8") } } -func (h *HugoSites) fileEventsFilter(events []fsnotify.Event) []fsnotify.Event { +// reBuild partially rebuilds a site given the filesystem events. +// It returns whetever the content source was changed. +func (s *Site) reProcess(events []fsnotify.Event) (whatChanged, error) { + s.Log.DEBUG.Printf("Rebuild for events %q", events) + + s.timerStep("initialize rebuild") + + // First we need to determine what changed + + sourceChanged := []fsnotify.Event{} + sourceReallyChanged := []fsnotify.Event{} + tmplChanged := []fsnotify.Event{} + dataChanged := []fsnotify.Event{} + i18nChanged := []fsnotify.Event{} + shortcodesChanged := make(map[string]bool) + // prevent spamming the log on changes + logger := helpers.NewDistinctFeedbackLogger() seen := make(map[fsnotify.Event]bool) - n := 0 for _, ev := range events { // Avoid processing the same event twice. if seen[ev] { @@ -1029,303 +666,964 @@ func (h *HugoSites) fileEventsFilter(events []fsnotify.Event) []fsnotify.Event { } seen[ev] = true - if h.SourceSpec.IgnoreFile(ev.Name) { + if s.isContentDirEvent(ev) { + logger.Println("Source changed", ev) + sourceChanged = append(sourceChanged, ev) + } + if s.isLayoutDirEvent(ev) { + logger.Println("Template changed", ev) + tmplChanged = append(tmplChanged, ev) + + if strings.Contains(ev.Name, "shortcodes") { + clearIsInnerShortcodeCache() + shortcode := filepath.Base(ev.Name) + shortcode = strings.TrimSuffix(shortcode, filepath.Ext(shortcode)) + shortcodesChanged[shortcode] = true + } + } + if s.isDataDirEvent(ev) { + logger.Println("Data changed", ev) + dataChanged = append(dataChanged, ev) + } + if s.isI18nEvent(ev) { + logger.Println("i18n changed", ev) + i18nChanged = append(dataChanged, ev) + } + } + + if len(tmplChanged) > 0 || len(i18nChanged) > 0 { + sites := s.owner.Sites + first := sites[0] + + // TOD(bep) globals clean + if err := first.Deps.LoadResources(); err != nil { + s.Log.ERROR.Println(err) + } + + s.TemplateHandler().PrintErrors() + + for i := 1; i < len(sites); i++ { + site := sites[i] + var err error + site.Deps, err = first.Deps.ForLanguage(site.Language) + if err != nil { + return whatChanged{}, err + } + } + + s.timerStep("template prep") + } + + if len(dataChanged) > 0 { + if err := s.readDataFromSourceFS(); err != nil { + s.Log.ERROR.Println(err) + } + } + + // If a content file changes, we need to reload only it and re-render the entire site. + + // First step is to read the changed files and (re)place them in site.AllPages + // This includes processing any meta-data for that content + + // The second step is to convert the content into HTML + // This includes processing any shortcodes that may be present. + + // We do this in parallel... even though it's likely only one file at a time. + // We need to process the reading prior to the conversion for each file, but + // we can convert one file while another one is still reading. + errs := make(chan error, 2) + readResults := make(chan HandledResult) + filechan := make(chan *source.File) + convertResults := make(chan HandledResult) + pageChan := make(chan *Page) + fileConvChan := make(chan *source.File) + coordinator := make(chan bool) + + wg := &sync.WaitGroup{} + wg.Add(2) + for i := 0; i < 2; i++ { + go sourceReader(s, filechan, readResults, wg) + } + + wg2 := &sync.WaitGroup{} + wg2.Add(4) + for i := 0; i < 2; i++ { + go fileConverter(s, fileConvChan, convertResults, wg2) + go pageConverter(pageChan, convertResults, wg2) + } + + sp := source.NewSourceSpec(s.Cfg, s.Fs) + fs := sp.NewFilesystem("") + + for _, ev := range sourceChanged { + // The incrementalReadCollator below will also make changes to the site's pages, + // so we do this first to prevent races. + if ev.Op&fsnotify.Remove == fsnotify.Remove { + //remove the file & a create will follow + path, _ := helpers.GetRelativePath(ev.Name, s.getContentDir(ev.Name)) + s.removePageByPathPrefix(path) continue } - if runtime.GOOS == "darwin" { // When a file system is HFS+, its filepath is in NFD form. - ev.Name = norm.NFC.String(ev.Name) - } - - events[n] = ev - n++ - } - events = events[:n] - - eventOrdinal := func(e fsnotify.Event) int { - // Pull the structural changes to the top. - if e.Op.Has(fsnotify.Create) { - return 1 - } - if e.Op.Has(fsnotify.Remove) { - return 2 - } - if e.Op.Has(fsnotify.Rename) { - return 3 - } - if e.Op.Has(fsnotify.Write) { - return 4 - } - return 5 - } - - sort.Slice(events, func(i, j int) bool { - // First sort by event type. - if eventOrdinal(events[i]) != eventOrdinal(events[j]) { - return eventOrdinal(events[i]) < eventOrdinal(events[j]) - } - // Then sort by name. - return events[i].Name < events[j].Name - }) - - return events -} - -type fileEventInfo struct { - fsnotify.Event - fi os.FileInfo - added bool - removed bool - isChangedDir bool -} - -func (h *HugoSites) fileEventsApplyInfo(events []fsnotify.Event) []fileEventInfo { - var infos []fileEventInfo - for _, ev := range events { - removed := false - added := false - - if ev.Op&fsnotify.Remove == fsnotify.Remove { - removed = true - } - - fi, statErr := h.Fs.Source.Stat(ev.Name) - // Some editors (Vim) sometimes issue only a Rename operation when writing an existing file // Sometimes a rename operation means that file has been renamed other times it means - // it's been updated. - if ev.Op.Has(fsnotify.Rename) { + // it's been updated + if ev.Op&fsnotify.Rename == fsnotify.Rename { // If the file is still on disk, it's only been updated, if it's not, it's been moved - if statErr != nil { - removed = true + if ex, err := afero.Exists(s.Fs.Source, ev.Name); !ex || err != nil { + path, _ := helpers.GetRelativePath(ev.Name, s.getContentDir(ev.Name)) + s.removePageByPath(path) + continue } } - if ev.Op.Has(fsnotify.Create) { - added = true - } - isChangedDir := statErr == nil && fi.IsDir() - - infos = append(infos, fileEventInfo{ - Event: ev, - fi: fi, - added: added, - removed: removed, - isChangedDir: isChangedDir, - }) - } - - n := 0 - - for _, ev := range infos { - // Remove any directories that's also represented by a file. - keep := true - if ev.isChangedDir { - for _, ev2 := range infos { - if ev2.fi != nil && !ev2.fi.IsDir() && filepath.Dir(ev2.Name) == ev.Name { - keep = false - break - } - } - } - if keep { - infos[n] = ev - n++ - } - } - infos = infos[:n] - - return infos -} - -func (h *HugoSites) fileEventsTrim(events []fsnotify.Event) []fsnotify.Event { - seen := make(map[string]bool) - n := 0 - for _, ev := range events { - if seen[ev.Name] { + // ignore files shouldn't be proceed + if fi, err := s.Fs.Source.Stat(ev.Name); err != nil { continue + } else { + if ok, err := fs.ShouldRead(ev.Name, fi); err != nil || !ok { + continue + } } - seen[ev.Name] = true - events[n] = ev - n++ + + sourceReallyChanged = append(sourceReallyChanged, ev) } - return events + + go incrementalReadCollator(s, readResults, pageChan, fileConvChan, coordinator, errs) + go converterCollator(convertResults, errs) + + for _, ev := range sourceReallyChanged { + + file, err := s.reReadFile(ev.Name) + + if err != nil { + s.Log.ERROR.Println("Error reading file", ev.Name, ";", err) + } + + if file != nil { + filechan <- file + } + + } + + for shortcode := range shortcodesChanged { + // There are certain scenarios that, when a shortcode changes, + // it isn't sufficient to just rerender the already parsed shortcode. + // One example is if the user adds a new shortcode to the content file first, + // and then creates the shortcode on the file system. + // To handle these scenarios, we must do a full reprocessing of the + // pages that keeps a reference to the changed shortcode. + pagesWithShortcode := s.findPagesByShortcode(shortcode) + for _, p := range pagesWithShortcode { + p.rendered = false + pageChan <- p + } + } + + // we close the filechan as we have sent everything we want to send to it. + // this will tell the sourceReaders to stop iterating on that channel + close(filechan) + + // waiting for the sourceReaders to all finish + wg.Wait() + // Now closing readResults as this will tell the incrementalReadCollator to + // stop iterating over that. + close(readResults) + + // once readResults is finished it will close coordinator and move along + <-coordinator + // allow that routine to finish, then close page & fileconvchan as we've sent + // everything to them we need to. + close(pageChan) + close(fileConvChan) + + wg2.Wait() + close(convertResults) + + s.timerStep("read & convert pages from source") + + for i := 0; i < 2; i++ { + err := <-errs + if err != nil { + s.Log.ERROR.Println(err) + } + } + + changed := whatChanged{ + source: len(sourceChanged) > 0, + other: len(tmplChanged) > 0 || len(i18nChanged) > 0 || len(dataChanged) > 0, + } + + return changed, nil + } -func (h *HugoSites) fileEventsContentPaths(p []pathChange) []pathChange { - var bundles []pathChange - var dirs []pathChange - var regular []pathChange - - var others []pathChange - for _, p := range p { - if p.isDir { - dirs = append(dirs, p) - } else { - others = append(others, p) - } - } - - // Remove all files below dir. - if len(dirs) > 0 { - n := 0 - for _, d := range dirs { - dir := d.p.Path() + "/" - for _, o := range others { - if !strings.HasPrefix(o.p.Path(), dir) { - others[n] = o - n++ +func (s *Site) loadData(sources []source.Input) (err error) { + s.Log.DEBUG.Printf("Load Data from %d source(s)", len(sources)) + s.Data = make(map[string]interface{}) + var current map[string]interface{} + for _, currentSource := range sources { + for _, r := range currentSource.Files() { + // Crawl in data tree to insert data + current = s.Data + for _, key := range strings.Split(r.Dir(), helpers.FilePathSeparator) { + if key != "" { + if _, ok := current[key]; !ok { + current[key] = make(map[string]interface{}) + } + current = current[key].(map[string]interface{}) } } - } - others = others[:n] - } - - for _, p := range others { - if p.p.IsBundle() { - bundles = append(bundles, p) - } else { - regular = append(regular, p) - } - } - - // Remove any files below leaf bundles. - // Remove any files in the same folder as branch bundles. - var keepers []pathChange - - for _, o := range regular { - keep := true - for _, b := range bundles { - prefix := b.p.Base() + "/" - if b.p.IsLeafBundle() && strings.HasPrefix(o.p.Path(), prefix) { - keep = false - break - } else if b.p.IsBranchBundle() && o.p.Dir() == b.p.Dir() { - keep = false - break + data, err := s.readData(r) + if err != nil { + s.Log.WARN.Printf("Failed to read data from %s: %s", filepath.Join(r.Path(), r.LogicalName()), err) + continue } - } - if keep { - keepers = append(keepers, o) + if data == nil { + continue + } + + // Copy content from current to data when needed + if _, ok := current[r.BaseFileName()]; ok { + data := data.(map[string]interface{}) + + for key, value := range current[r.BaseFileName()].(map[string]interface{}) { + if _, override := data[key]; override { + // filepath.Walk walks the files in lexical order, '/' comes before '.' + // this warning could happen if + // 1. A theme uses the same key; the main data folder wins + // 2. A sub folder uses the same key: the sub folder wins + s.Log.WARN.Printf("Data for key '%s' in path '%s' is overridden in subfolder", key, r.Path()) + } + data[key] = value + } + } + + // Insert data + current[r.BaseFileName()] = data } } - keepers = append(dirs, keepers...) - keepers = append(bundles, keepers...) + return +} - return keepers +func (s *Site) readData(f *source.File) (interface{}, error) { + switch f.Extension() { + case "yaml", "yml": + return parser.HandleYAMLMetaData(f.Bytes()) + case "json": + return parser.HandleJSONMetaData(f.Bytes()) + case "toml": + return parser.HandleTOMLMetaData(f.Bytes()) + default: + return nil, fmt.Errorf("Data not supported for extension '%s'", f.Extension()) + } +} + +func (s *Site) readDataFromSourceFS() error { + sp := source.NewSourceSpec(s.Cfg, s.Fs) + dataSources := make([]source.Input, 0, 2) + dataSources = append(dataSources, sp.NewFilesystem(s.absDataDir())) + + // have to be last - duplicate keys in earlier entries will win + themeDataDir, err := s.PathSpec.GetThemeDataDirPath() + if err == nil { + dataSources = append(dataSources, sp.NewFilesystem(themeDataDir)) + } + + err = s.loadData(dataSources) + s.timerStep("load data") + return err +} + +func (s *Site) process(config BuildCfg) (err error) { + s.timerStep("Go initialization") + if err = s.initialize(); err != nil { + return + } + s.timerStep("initialize") + + if err = s.readDataFromSourceFS(); err != nil { + return + } + + s.timerStep("load i18n") + return s.createPages() + +} + +func (s *Site) setupSitePages() { + var siteLastChange time.Time + + for i, page := range s.RegularPages { + if i < len(s.RegularPages)-1 { + page.Next = s.RegularPages[i+1] + } + + if i > 0 { + page.Prev = s.RegularPages[i-1] + } + + // Determine Site.Info.LastChange + // Note that the logic to determine which date to use for Lastmod + // is already applied, so this is *the* date to use. + // We cannot just pick the last page in the default sort, because + // that may not be ordered by date. + if page.Lastmod.After(siteLastChange) { + siteLastChange = page.Lastmod + } + } + + s.Info.LastChange = siteLastChange +} + +func (s *Site) render(outFormatIdx int) (err error) { + + if outFormatIdx == 0 { + if err = s.preparePages(); err != nil { + return + } + s.timerStep("prepare pages") + + // Note that even if disableAliases is set, the aliases themselves are + // preserved on page. The motivation with this is to be able to generate + // 301 redirects in a .htacess file and similar using a custom output format. + if !s.Cfg.GetBool("disableAliases") { + // Aliases must be rendered before pages. + // Some sites, Hugo docs included, have faulty alias definitions that point + // to itself or another real page. These will be overwritten in the next + // step. + if err = s.renderAliases(); err != nil { + return + } + s.timerStep("render and write aliases") + } + + } + + if err = s.renderPages(); err != nil { + return + } + + s.timerStep("render and write pages") + + // TODO(bep) render consider this, ref. render404 etc. + if outFormatIdx > 0 { + return + } + + if err = s.renderSitemap(); err != nil { + return + } + s.timerStep("render and write Sitemap") + + if err = s.renderRobotsTXT(); err != nil { + return + } + s.timerStep("render and write robots.txt") + + if err = s.render404(); err != nil { + return + } + s.timerStep("render and write 404") + + return +} + +func (s *Site) Initialise() (err error) { + return s.initialize() +} + +func (s *Site) initialize() (err error) { + defer s.initializeSiteInfo() + s.Menus = Menus{} + + // May be supplied in tests. + if s.Source != nil && len(s.Source.Files()) > 0 { + s.Log.DEBUG.Println("initialize: Source is already set") + return + } + + if err = s.checkDirectories(); err != nil { + return err + } + + staticDir := s.PathSpec.GetStaticDirPath() + "/" + + sp := source.NewSourceSpec(s.Cfg, s.Fs) + s.Source = sp.NewFilesystem(s.absContentDir(), staticDir) + + return +} + +// HomeAbsURL is a convenience method giving the absolute URL to the home page. +func (s *SiteInfo) HomeAbsURL() string { + base := "" + if s.IsMultiLingual() { + base = s.Language.Lang + } + return s.owner.AbsURL(base, false) } // SitemapAbsURL is a convenience method giving the absolute URL to the sitemap. -func (s *Site) SitemapAbsURL() string { - base := "" - if len(s.conf.Languages) > 1 || s.Conf.DefaultContentLanguageInSubdir() { - base = s.Language().Lang - } - p := s.AbsURL(base, false) +func (s *SiteInfo) SitemapAbsURL() string { + sitemapDefault := parseSitemap(s.s.Cfg.GetStringMap("sitemap")) + p := s.HomeAbsURL() if !strings.HasSuffix(p, "/") { p += "/" } - p += s.conf.Sitemap.Filename + p += sitemapDefault.Filename return p } -func (s *Site) createNodeMenuEntryURL(in string) string { +func (s *Site) initializeSiteInfo() { + var ( + lang = s.Language + languages helpers.Languages + ) + + if s.owner != nil && s.owner.multilingual != nil { + languages = s.owner.multilingual.Languages + } + + params := lang.Params() + + permalinks := make(PermalinkOverrides) + for k, v := range s.Cfg.GetStringMapString("permalinks") { + permalinks[k] = pathPattern(v) + } + + defaultContentInSubDir := s.Cfg.GetBool("defaultContentLanguageInSubdir") + defaultContentLanguage := s.Cfg.GetString("defaultContentLanguage") + + languagePrefix := "" + if s.multilingualEnabled() && (defaultContentInSubDir || lang.Lang != defaultContentLanguage) { + languagePrefix = "/" + lang.Lang + } + + var multilingual *Multilingual + if s.owner != nil { + multilingual = s.owner.multilingual + } + + s.Info = SiteInfo{ + Title: lang.GetString("title"), + Author: lang.GetStringMap("author"), + Social: lang.GetStringMapString("social"), + LanguageCode: lang.GetString("languageCode"), + Copyright: lang.GetString("copyright"), + DisqusShortname: lang.GetString("disqusShortname"), + multilingual: multilingual, + Language: lang, + LanguagePrefix: languagePrefix, + Languages: languages, + defaultContentLanguageInSubdir: defaultContentInSubDir, + sectionPagesMenu: lang.GetString("sectionPagesMenu"), + GoogleAnalytics: lang.GetString("googleAnalytics"), + BuildDrafts: s.Cfg.GetBool("buildDrafts"), + canonifyURLs: s.Cfg.GetBool("canonifyURLs"), + relativeURLs: s.Cfg.GetBool("relativeURLs"), + uglyURLs: s.Cfg.GetBool("uglyURLs"), + preserveTaxonomyNames: lang.GetBool("preserveTaxonomyNames"), + PageCollections: s.PageCollections, + Files: &s.Files, + Menus: &s.Menus, + Params: params, + Permalinks: permalinks, + Data: &s.Data, + owner: s.owner, + s: s, + } + + rssOutputFormat, found := s.outputFormats[KindHome].GetByName(output.RSSFormat.Name) + + if found { + s.Info.RSSLink = s.permalink(rssOutputFormat.BaseFilename()) + } +} + +func (s *Site) dataDir() string { + return s.Cfg.GetString("dataDir") +} + +func (s *Site) absDataDir() string { + return s.PathSpec.AbsPathify(s.dataDir()) +} + +func (s *Site) i18nDir() string { + return s.Cfg.GetString("i18nDir") +} + +func (s *Site) absI18nDir() string { + return s.PathSpec.AbsPathify(s.i18nDir()) +} + +func (s *Site) isI18nEvent(e fsnotify.Event) bool { + if s.getI18nDir(e.Name) != "" { + return true + } + return s.getThemeI18nDir(e.Name) != "" +} + +func (s *Site) getI18nDir(path string) string { + return s.getRealDir(s.absI18nDir(), path) +} + +func (s *Site) getThemeI18nDir(path string) string { + if !s.PathSpec.ThemeSet() { + return "" + } + return s.getRealDir(filepath.Join(s.PathSpec.GetThemeDir(), s.i18nDir()), path) +} + +func (s *Site) isDataDirEvent(e fsnotify.Event) bool { + if s.getDataDir(e.Name) != "" { + return true + } + return s.getThemeDataDir(e.Name) != "" +} + +func (s *Site) getDataDir(path string) string { + return s.getRealDir(s.absDataDir(), path) +} + +func (s *Site) getThemeDataDir(path string) string { + if !s.PathSpec.ThemeSet() { + return "" + } + return s.getRealDir(filepath.Join(s.PathSpec.GetThemeDir(), s.dataDir()), path) +} + +func (s *Site) layoutDir() string { + return s.Cfg.GetString("layoutDir") +} + +func (s *Site) isLayoutDirEvent(e fsnotify.Event) bool { + if s.getLayoutDir(e.Name) != "" { + return true + } + return s.getThemeLayoutDir(e.Name) != "" +} + +func (s *Site) getLayoutDir(path string) string { + return s.getRealDir(s.PathSpec.GetLayoutDirPath(), path) +} + +func (s *Site) getThemeLayoutDir(path string) string { + if !s.PathSpec.ThemeSet() { + return "" + } + return s.getRealDir(filepath.Join(s.PathSpec.GetThemeDir(), s.layoutDir()), path) +} + +func (s *Site) absContentDir() string { + return s.PathSpec.AbsPathify(s.Cfg.GetString("contentDir")) +} + +func (s *Site) isContentDirEvent(e fsnotify.Event) bool { + return s.getContentDir(e.Name) != "" +} + +func (s *Site) getContentDir(path string) string { + return s.getRealDir(s.absContentDir(), path) +} + +// getRealDir gets the base path of the given path, also handling the case where +// base is a symlinked folder. +func (s *Site) getRealDir(base, path string) string { + + if strings.HasPrefix(path, base) { + return base + } + + realDir, err := helpers.GetRealPath(s.Fs.Source, base) + + if err != nil { + if !os.IsNotExist(err) { + s.Log.ERROR.Printf("Failed to get real path for %s: %s", path, err) + } + return "" + } + + if strings.HasPrefix(path, realDir) { + return realDir + } + + return "" +} + +func (s *Site) absPublishDir() string { + return s.PathSpec.AbsPathify(s.Cfg.GetString("publishDir")) +} + +func (s *Site) checkDirectories() (err error) { + if b, _ := helpers.DirExists(s.absContentDir(), s.Fs.Source); !b { + return errors.New("No source directory found, expecting to find it at " + s.absContentDir()) + } + return +} + +// reReadFile resets file to be read from disk again +func (s *Site) reReadFile(absFilePath string) (*source.File, error) { + s.Log.INFO.Println("rereading", absFilePath) + var file *source.File + + reader, err := source.NewLazyFileReader(s.Fs.Source, absFilePath) + if err != nil { + return nil, err + } + + sp := source.NewSourceSpec(s.Cfg, s.Fs) + file, err = sp.NewFileFromAbs(s.getContentDir(absFilePath), absFilePath, reader) + + if err != nil { + return nil, err + } + + return file, nil +} + +func (s *Site) readPagesFromSource() chan error { + if s.Source == nil { + panic(fmt.Sprintf("s.Source not set %s", s.absContentDir())) + } + + s.Log.DEBUG.Printf("Read %d pages from source", len(s.Source.Files())) + + errs := make(chan error) + if len(s.Source.Files()) < 1 { + close(errs) + return errs + } + + files := s.Source.Files() + results := make(chan HandledResult) + filechan := make(chan *source.File) + wg := &sync.WaitGroup{} + numWorkers := getGoMaxProcs() * 4 + wg.Add(numWorkers) + for i := 0; i < numWorkers; i++ { + go sourceReader(s, filechan, results, wg) + } + + // we can only have exactly one result collator, since it makes changes that + // must be synchronized. + go readCollator(s, results, errs) + + for _, file := range files { + filechan <- file + } + + close(filechan) + wg.Wait() + close(results) + + return errs +} + +func (s *Site) convertSource() chan error { + errs := make(chan error) + results := make(chan HandledResult) + pageChan := make(chan *Page) + fileConvChan := make(chan *source.File) + numWorkers := getGoMaxProcs() * 4 + wg := &sync.WaitGroup{} + + for i := 0; i < numWorkers; i++ { + wg.Add(2) + go fileConverter(s, fileConvChan, results, wg) + go pageConverter(pageChan, results, wg) + } + + go converterCollator(results, errs) + + for _, p := range s.rawAllPages { + if p.shouldBuild() { + pageChan <- p + } + } + + for _, f := range s.Files { + fileConvChan <- f + } + + close(pageChan) + close(fileConvChan) + wg.Wait() + close(results) + + return errs +} + +func (s *Site) createPages() error { + readErrs := <-s.readPagesFromSource() + s.timerStep("read pages from source") + + renderErrs := <-s.convertSource() + s.timerStep("convert source") + + if renderErrs == nil && readErrs == nil { + return nil + } + if renderErrs == nil { + return readErrs + } + if readErrs == nil { + return renderErrs + } + + return fmt.Errorf("%s\n%s", readErrs, renderErrs) +} + +func sourceReader(s *Site, files <-chan *source.File, results chan<- HandledResult, wg *sync.WaitGroup) { + defer wg.Done() + for file := range files { + readSourceFile(s, file, results) + } +} + +func readSourceFile(s *Site, file *source.File, results chan<- HandledResult) { + h := NewMetaHandler(file.Extension()) + if h != nil { + h.Read(file, s, results) + } else { + s.Log.ERROR.Println("Unsupported File Type", file.Path()) + } +} + +func pageConverter(pages <-chan *Page, results HandleResults, wg *sync.WaitGroup) { + defer wg.Done() + for page := range pages { + var h *MetaHandle + if page.Markup != "" { + h = NewMetaHandler(page.Markup) + } else { + h = NewMetaHandler(page.File.Extension()) + } + if h != nil { + // Note that we convert pages from the site's rawAllPages collection + // Which may contain pages from multiple sites, so we use the Page's site + // for the conversion. + h.Convert(page, page.s, results) + } + } +} + +func fileConverter(s *Site, files <-chan *source.File, results HandleResults, wg *sync.WaitGroup) { + defer wg.Done() + for file := range files { + h := NewMetaHandler(file.Extension()) + if h != nil { + h.Convert(file, s, results) + } + } +} + +func converterCollator(results <-chan HandledResult, errs chan<- error) { + errMsgs := []string{} + for r := range results { + if r.err != nil { + errMsgs = append(errMsgs, r.err.Error()) + continue + } + } + if len(errMsgs) == 0 { + errs <- nil + return + } + errs <- fmt.Errorf("Errors rendering pages: %s", strings.Join(errMsgs, "\n")) +} + +func (s *Site) replaceFile(sf *source.File) { + for i, f := range s.Files { + if f.Path() == sf.Path() { + s.Files[i] = sf + return + } + } + + // If a match isn't found, then append it + s.Files = append(s.Files, sf) +} + +func incrementalReadCollator(s *Site, results <-chan HandledResult, pageChan chan *Page, fileConvChan chan *source.File, coordinator chan bool, errs chan<- error) { + errMsgs := []string{} + for r := range results { + if r.err != nil { + errMsgs = append(errMsgs, r.Error()) + continue + } + + if r.page == nil { + s.replaceFile(r.file) + fileConvChan <- r.file + } else { + s.replacePage(r.page) + pageChan <- r.page + } + } + + s.rawAllPages.Sort() + close(coordinator) + + if len(errMsgs) == 0 { + errs <- nil + return + } + errs <- fmt.Errorf("Errors reading pages: %s", strings.Join(errMsgs, "\n")) +} + +func readCollator(s *Site, results <-chan HandledResult, errs chan<- error) { + if s.PageCollections == nil { + panic("No page collections") + } + errMsgs := []string{} + for r := range results { + if r.err != nil { + errMsgs = append(errMsgs, r.Error()) + continue + } + + // !page == file + if r.page == nil { + s.Files = append(s.Files, r.file) + } else { + s.addPage(r.page) + } + } + + s.rawAllPages.Sort() + if len(errMsgs) == 0 { + errs <- nil + return + } + errs <- fmt.Errorf("Errors reading pages: %s", strings.Join(errMsgs, "\n")) +} + +func (s *Site) buildSiteMeta() (err error) { + defer s.timerStep("build Site meta") + + if len(s.Pages) == 0 { + return + } + + s.assembleTaxonomies() + + for _, p := range s.AllPages { + // this depends on taxonomies + p.setValuesForKind(s) + } + + return +} + +func (s *Site) getMenusFromConfig() Menus { + + ret := Menus{} + + if menus := s.Language.GetStringMap("menu"); menus != nil { + for name, menu := range menus { + m, err := cast.ToSliceE(menu) + if err != nil { + s.Log.ERROR.Printf("unable to process menus in site config\n") + s.Log.ERROR.Println(err) + } else { + for _, entry := range m { + s.Log.DEBUG.Printf("found menu: %q, in site config\n", name) + + menuEntry := MenuEntry{Menu: name} + ime, err := cast.ToStringMapE(entry) + if err != nil { + s.Log.ERROR.Printf("unable to process menus in site config\n") + s.Log.ERROR.Println(err) + } + + menuEntry.marshallMap(ime) + menuEntry.URL = s.Info.createNodeMenuEntryURL(menuEntry.URL) + + if ret[name] == nil { + ret[name] = &Menu{} + } + *ret[name] = ret[name].add(&menuEntry) + } + } + } + return ret + } + return ret +} + +func (s *SiteInfo) createNodeMenuEntryURL(in string) string { + if !strings.HasPrefix(in, "/") { return in } // make it match the nodes menuEntryURL := in - menuEntryURL = s.s.PathSpec.URLize(menuEntryURL) - if !s.conf.CanonifyURLs { - menuEntryURL = paths.AddContextRoot(s.s.PathSpec.Cfg.BaseURL().String(), menuEntryURL) + menuEntryURL = helpers.SanitizeURLKeepTrailingSlash(s.s.PathSpec.URLize(menuEntryURL)) + if !s.canonifyURLs { + menuEntryURL = helpers.AddContextRoot(s.s.PathSpec.BaseURL.String(), menuEntryURL) } return menuEntryURL } -func (s *Site) assembleMenus() error { - s.menus = make(navigation.Menus) +func (s *Site) assembleMenus() { + s.Menus = Menus{} type twoD struct { MenuName, EntryName string } - flat := map[twoD]*navigation.MenuEntry{} - children := map[twoD]navigation.Menu{} + flat := map[twoD]*MenuEntry{} + children := map[twoD]Menu{} // add menu entries from config to flat hash - for name, menu := range s.conf.Menus.Config { - for _, me := range menu { - if types.IsNil(me.Page) && me.PageRef != "" { - // Try to resolve the page. - me.Page, _ = s.getPage(nil, me.PageRef) - } - - // If page is still nill, we must make sure that we have a URL that considers baseURL etc. - if types.IsNil(me.Page) { - me.ConfiguredURL = s.createNodeMenuEntryURL(me.MenuConfig.URL) - } else { - navigation.SetPageValues(me, me.Page) - } - + menuConfig := s.getMenusFromConfig() + for name, menu := range menuConfig { + for _, me := range *menu { flat[twoD{name, me.KeyName()}] = me } } - sectionPagesMenu := s.conf.SectionPagesMenu + sectionPagesMenu := s.Info.sectionPagesMenu + pages := s.Pages if sectionPagesMenu != "" { - if err := s.pageMap.forEachPage(pagePredicates.ShouldListGlobal, func(p *pageState) (bool, error) { - if p.Kind() != kinds.KindSection || !p.m.shouldBeCheckedForMenuDefinitions() { - return false, nil - } + for _, p := range pages { + if p.Kind == KindSection { + // From Hugo 0.22 we have nested sections, but until we get a + // feel of how that would work in this setting, let us keep + // this menu for the top level only. + id := p.Section() + if _, ok := flat[twoD{sectionPagesMenu, id}]; ok { + continue + } - // The section pages menus are attached to the top level section. - id := p.Section() - if id == "" { - id = "/" + me := MenuEntry{Identifier: id, + Name: p.LinkTitle(), + Weight: p.Weight, + URL: p.RelPermalink()} + flat[twoD{sectionPagesMenu, me.KeyName()}] = &me } - - if _, ok := flat[twoD{sectionPagesMenu, id}]; ok { - return false, nil - } - me := navigation.MenuEntry{ - MenuConfig: navigation.MenuConfig{ - Identifier: id, - Name: p.LinkTitle(), - Weight: p.Weight(), - }, - Page: p, - } - - navigation.SetPageValues(&me, p) - flat[twoD{sectionPagesMenu, me.KeyName()}] = &me - return false, nil - }); err != nil { - return err } } // Add menu entries provided by pages - if err := s.pageMap.forEachPage(pagePredicates.ShouldListGlobal, func(p *pageState) (bool, error) { - for name, me := range p.pageMenus.menus() { + for _, p := range pages { + for name, me := range p.Menus() { if _, ok := flat[twoD{name, me.KeyName()}]; ok { - err := p.wrapError(fmt.Errorf("duplicate menu entry with identifier %q in menu %q", me.KeyName(), name)) - s.Log.Warnln(err) + s.Log.ERROR.Printf("Two or more menu items have the same name/identifier in Menu %q: %q.\nRename or set an unique identifier.\n", name, me.KeyName()) continue } flat[twoD{name, me.KeyName()}] = me } - return false, nil - }); err != nil { - return err } // Create Children Menus First for _, e := range flat { if e.Parent != "" { - children[twoD{e.Menu, e.Parent}] = children[twoD{e.Menu, e.Parent}].Add(e) + children[twoD{e.Menu, e.Parent}] = children[twoD{e.Menu, e.Parent}].add(e) } } @@ -1334,11 +1632,7 @@ func (s *Site) assembleMenus() error { _, ok := flat[twoD{p.MenuName, p.EntryName}] if !ok { // if parent does not exist, create one without a URL - flat[twoD{p.MenuName, p.EntryName}] = &navigation.MenuEntry{ - MenuConfig: navigation.MenuConfig{ - Name: p.EntryName, - }, - } + flat[twoD{p.MenuName, p.EntryName}] = &MenuEntry{Name: p.EntryName, URL: ""} } flat[twoD{p.MenuName, p.EntryName}].Children = childmenu } @@ -1346,269 +1640,488 @@ func (s *Site) assembleMenus() error { // Assembling Top Level of Tree for menu, e := range flat { if e.Parent == "" { - _, ok := s.menus[menu.MenuName] + _, ok := s.Menus[menu.MenuName] if !ok { - s.menus[menu.MenuName] = navigation.Menu{} + s.Menus[menu.MenuName] = &Menu{} } - s.menus[menu.MenuName] = s.menus[menu.MenuName].Add(e) + *s.Menus[menu.MenuName] = s.Menus[menu.MenuName].add(e) } } +} + +func (s *Site) getTaxonomyKey(key string) string { + if s.Info.preserveTaxonomyNames { + // Keep as is + return key + } + return s.PathSpec.MakePathSanitized(key) +} + +// We need to create the top level taxonomy early in the build process +// to be able to determine the page Kind correctly. +func (s *Site) createTaxonomiesEntries() { + s.Taxonomies = make(TaxonomyList) + taxonomies := s.Language.GetStringMapString("taxonomies") + for _, plural := range taxonomies { + s.Taxonomies[plural] = make(Taxonomy) + } +} + +func (s *Site) assembleTaxonomies() { + s.taxonomiesPluralSingular = make(map[string]string) + s.taxonomiesOrigKey = make(map[string]string) + + taxonomies := s.Language.GetStringMapString("taxonomies") + + s.Log.INFO.Printf("found taxonomies: %#v\n", taxonomies) + + for singular, plural := range taxonomies { + s.taxonomiesPluralSingular[plural] = singular + + for _, p := range s.Pages { + vals := p.getParam(plural, !s.Info.preserveTaxonomyNames) + weight := p.GetParam(plural + "_weight") + if weight == nil { + weight = 0 + } + if vals != nil { + if v, ok := vals.([]string); ok { + for _, idx := range v { + x := WeightedPage{weight.(int), p} + s.Taxonomies[plural].add(s.getTaxonomyKey(idx), x) + if s.Info.preserveTaxonomyNames { + // Need to track the original + s.taxonomiesOrigKey[fmt.Sprintf("%s-%s", plural, s.PathSpec.MakePathSanitized(idx))] = idx + } + } + } else if v, ok := vals.(string); ok { + x := WeightedPage{weight.(int), p} + s.Taxonomies[plural].add(s.getTaxonomyKey(v), x) + if s.Info.preserveTaxonomyNames { + // Need to track the original + s.taxonomiesOrigKey[fmt.Sprintf("%s-%s", plural, s.PathSpec.MakePathSanitized(v))] = v + } + } else { + s.Log.ERROR.Printf("Invalid %s in %s\n", plural, p.File.Path()) + } + } + } + for k := range s.Taxonomies[plural] { + s.Taxonomies[plural][k].Sort() + } + } + + s.Info.Taxonomies = s.Taxonomies +} + +// Prepare site for a new full build. +func (s *Site) resetBuildState() { + + s.PageCollections = newPageCollectionsFromPages(s.rawAllPages) + // TODO(bep) get rid of this double + s.Info.PageCollections = s.PageCollections + + s.Info.paginationPageCount = 0 + s.draftCount = 0 + s.futureCount = 0 + + s.expiredCount = 0 + + for _, p := range s.rawAllPages { + p.scratch = newScratch() + p.subSections = Pages{} + p.parent = nil + } +} + +func (s *Site) kindFromSections(sections []string) string { + if _, isTaxonomy := s.Taxonomies[sections[0]]; isTaxonomy { + if len(sections) == 1 { + return KindTaxonomyTerm + } + return KindTaxonomy + } + return KindSection +} + +func (s *Site) layouts(p *PageOutput) ([]string, error) { + return s.layoutHandler.For(p.layoutDescriptor, "", p.outputFormat) +} + +func (s *Site) preparePages() error { + var errors []error + + for _, p := range s.Pages { + if err := p.prepareLayouts(); err != nil { + errors = append(errors, err) + } + if err := p.prepareData(s); err != nil { + errors = append(errors, err) + } + } + + if len(errors) != 0 { + return fmt.Errorf("Prepare pages failed: %.100q…", errors) + } return nil } -// get any language code to prefix the target file path with. -func (s *Site) getLanguageTargetPathLang(alwaysInSubDir bool) string { - if s.h.Conf.IsMultihost() { - return s.Language().Lang +func errorCollator(results <-chan error, errs chan<- error) { + errMsgs := []string{} + for err := range results { + if err != nil { + errMsgs = append(errMsgs, err.Error()) + } } - - return s.getLanguagePermalinkLang(alwaysInSubDir) -} - -// get any language code to prefix the relative permalink with. -func (s *Site) getLanguagePermalinkLang(alwaysInSubDir bool) string { - if s.h.Conf.IsMultihost() { - return "" + if len(errMsgs) == 0 { + errs <- nil + } else { + errs <- errors.New(strings.Join(errMsgs, "\n")) } - - if s.h.Conf.IsMultilingual() && alwaysInSubDir { - return s.Language().Lang - } - - return s.GetLanguagePrefix() -} - -// Prepare site for a new full build. -func (s *Site) resetBuildState(sourceChanged bool) { - s.relatedDocsHandler = s.relatedDocsHandler.Clone() - s.init.Reset() - s.pageMap.Reset() -} - -func (s *Site) errorCollator(results <-chan error, errs chan<- error) { - var errors []error - for e := range results { - errors = append(errors, e) - } - - errs <- s.h.pickOneAndLogTheRest(errors) - close(errs) } -// GetPage looks up a page of a given type for the given ref. -// In Hugo <= 0.44 you had to add Page Kind (section, home) etc. as the first -// argument and then either a unix styled path (with or without a leading slash)) -// or path elements separated. -// When we now remove the Kind from this API, we need to make the transition as painless -// as possible for existing sites. Most sites will use {{ .Site.GetPage "section" "my/section" }}, -// i.e. 2 arguments, so we test for that. -func (s *Site) GetPage(ref ...string) (page.Page, error) { - s.CheckReady() - p, err := s.s.getPageForRefs(ref...) - - if p == nil { - // The nil struct has meaning in some situations, mostly to avoid breaking - // existing sites doing $nilpage.IsDescendant($p), which will always return - // false. - p = page.NilPage +func (s *Site) appendThemeTemplates(in []string) []string { + if !s.PathSpec.ThemeSet() { + return in } - return p, err -} - -func (s *Site) absURLPath(targetPath string) string { - var path string - if s.conf.RelativeURLs { - path = helpers.GetDottedRelativePath(targetPath) - } else { - url := s.PathSpec.Cfg.BaseURL().String() - if !strings.HasSuffix(url, "/") { - url += "/" + out := []string{} + // First place all non internal templates + for _, t := range in { + if !strings.HasPrefix(t, "_internal/") { + out = append(out, t) } - path = url } - return path + // Then place theme templates with the same names + for _, t := range in { + if !strings.HasPrefix(t, "_internal/") { + out = append(out, "theme/"+t) + } + } + + // Lastly place internal templates + for _, t := range in { + if strings.HasPrefix(t, "_internal/") { + out = append(out, t) + } + } + return out + } -const ( - pageDependencyScopeDefault int = iota - pageDependencyScopeGlobal -) +// Stats prints Hugo builds stats to the console. +// This is what you see after a successful hugo build. +func (s *Site) Stats() { -func (s *Site) renderAndWritePage(statCounter *uint64, name string, targetPath string, p *pageState, d any, templ *tplimpl.TemplInfo) error { - s.h.buildCounters.pageRenderCounter.Add(1) + s.Log.FEEDBACK.Printf("Built site for language %s:\n", s.Language.Lang) + s.Log.FEEDBACK.Println(s.draftStats()) + s.Log.FEEDBACK.Println(s.futureStats()) + s.Log.FEEDBACK.Println(s.expiredStats()) + s.Log.FEEDBACK.Printf("%d regular pages created\n", s.siteStats.pageCountRegular) + s.Log.FEEDBACK.Printf("%d other pages created\n", (s.siteStats.pageCount - s.siteStats.pageCountRegular)) + s.Log.FEEDBACK.Printf("%d non-page files copied\n", len(s.Files)) + s.Log.FEEDBACK.Printf("%d paginator pages created\n", s.Info.paginationPageCount) + + if s.isEnabled(KindTaxonomy) { + taxonomies := s.Language.GetStringMapString("taxonomies") + + for _, pl := range taxonomies { + s.Log.FEEDBACK.Printf("%d %s created\n", len(s.Taxonomies[pl]), pl) + } + } + +} + +// GetPage looks up a page of a given type in the path given. +// {{ with .Site.GetPage "section" "blog" }}{{ .Title }}{{ end }} +// +// This will return nil when no page could be found, and will return the +// first page found if the key is ambigous. +func (s *SiteInfo) GetPage(typ string, path ...string) (*Page, error) { + return s.getPage(typ, path...), nil +} + +func (s *Site) permalinkForOutputFormat(link string, f output.Format) (string, error) { + var ( + baseURL string + err error + ) + + if f.Protocol != "" { + baseURL, err = s.PathSpec.BaseURL.WithProtocol(f.Protocol) + if err != nil { + return "", err + } + } else { + baseURL = s.PathSpec.BaseURL.String() + } + return s.permalinkForBaseURL(link, baseURL), nil +} + +func (s *Site) permalink(link string) string { + return s.permalinkForBaseURL(link, s.PathSpec.BaseURL.String()) + +} + +func (s *Site) permalinkForBaseURL(link, baseURL string) string { + link = strings.TrimPrefix(link, "/") + if !strings.HasSuffix(baseURL, "/") { + baseURL += "/" + } + return baseURL + link +} + +func (s *Site) renderAndWriteXML(name string, dest string, d interface{}, layouts ...string) error { + s.Log.DEBUG.Printf("Render XML for %q to %q", name, dest) + renderBuffer := bp.GetBuffer() + defer bp.PutBuffer(renderBuffer) + renderBuffer.WriteString("<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>\n") + + if err := s.renderForLayouts(name, d, renderBuffer, layouts...); err != nil { + helpers.DistinctWarnLog.Println(err) + return nil + } + + outBuffer := bp.GetBuffer() + defer bp.PutBuffer(outBuffer) + + var path []byte + if s.Info.relativeURLs { + path = []byte(helpers.GetDottedRelativePath(dest)) + } else { + s := s.Cfg.GetString("baseURL") + if !strings.HasSuffix(s, "/") { + s += "/" + } + path = []byte(s) + } + transformer := transform.NewChain(transform.AbsURLInXML) + if err := transformer.Apply(outBuffer, renderBuffer, path); err != nil { + helpers.DistinctErrorLog.Println(err) + return nil + } + + return s.publish(dest, outBuffer) + +} + +func (s *Site) renderAndWritePage(name string, dest string, p *PageOutput, layouts ...string) error { renderBuffer := bp.GetBuffer() defer bp.PutBuffer(renderBuffer) - of := p.outputFormat() - p.incrRenderState() - - ctx := tpl.Context.Page.Set(context.Background(), p) - ctx = tpl.Context.DependencyManagerScopedProvider.Set(ctx, p) - - if err := s.renderForTemplate(ctx, p.Kind(), of.Name, d, renderBuffer, templ); err != nil { - return err + if err := s.renderForLayouts(p.Kind, p, renderBuffer, layouts...); err != nil { + helpers.DistinctWarnLog.Println(err) + return nil } if renderBuffer.Len() == 0 { return nil } - isHTML := of.IsHTML - isRSS := of.Name == "rss" + outBuffer := bp.GetBuffer() + defer bp.PutBuffer(outBuffer) - pd := publisher.Descriptor{ - Src: renderBuffer, - TargetPath: targetPath, - StatCounter: statCounter, - OutputFormat: p.outputFormat(), - } + transformLinks := transform.NewEmptyTransforms() - if isRSS { - // Always canonify URLs in RSS - pd.AbsURLPath = s.absURLPath(targetPath) - } else if isHTML { - if s.conf.RelativeURLs || s.conf.CanonifyURLs { - pd.AbsURLPath = s.absURLPath(targetPath) + isHTML := p.outputFormat.IsHTML + + if isHTML { + if s.Info.relativeURLs || s.Info.canonifyURLs { + transformLinks = append(transformLinks, transform.AbsURL) } - if s.watching() && s.conf.Internal.Running && !s.conf.DisableLiveReload { - pd.LiveReloadBaseURL = s.Conf.BaseURLLiveReload().URL() + if s.running() && s.Cfg.GetBool("watch") && !s.Cfg.GetBool("disableLiveReload") { + transformLinks = append(transformLinks, transform.LiveReloadInject(s.Cfg.GetInt("port"))) } // For performance reasons we only inject the Hugo generator tag on the home page. if p.IsHome() { - pd.AddHugoGeneratorTag = !s.conf.DisableHugoGeneratorInject - } - - } - - return s.publisher.Publish(pd) -} - -var infoOnMissingLayout = map[string]bool{ - // The 404 layout is very much optional in Hugo, but we do look for it. - "404": true, -} - -// hookRendererTemplate is the canonical implementation of all hooks.ITEMRenderer, -// where ITEM is the thing being hooked. -type hookRendererTemplate struct { - templateHandler *tplimpl.TemplateStore - templ *tplimpl.TemplInfo - resolvePosition func(ctx any) text.Position -} - -func (hr hookRendererTemplate) RenderLink(cctx context.Context, w io.Writer, ctx hooks.LinkContext) error { - return hr.templateHandler.ExecuteWithContext(cctx, hr.templ, w, ctx) -} - -func (hr hookRendererTemplate) RenderHeading(cctx context.Context, w io.Writer, ctx hooks.HeadingContext) error { - return hr.templateHandler.ExecuteWithContext(cctx, hr.templ, w, ctx) -} - -func (hr hookRendererTemplate) RenderCodeblock(cctx context.Context, w hugio.FlexiWriter, ctx hooks.CodeblockContext) error { - return hr.templateHandler.ExecuteWithContext(cctx, hr.templ, w, ctx) -} - -func (hr hookRendererTemplate) RenderPassthrough(cctx context.Context, w io.Writer, ctx hooks.PassthroughContext) error { - return hr.templateHandler.ExecuteWithContext(cctx, hr.templ, w, ctx) -} - -func (hr hookRendererTemplate) RenderBlockquote(cctx context.Context, w hugio.FlexiWriter, ctx hooks.BlockquoteContext) error { - return hr.templateHandler.ExecuteWithContext(cctx, hr.templ, w, ctx) -} - -func (hr hookRendererTemplate) RenderTable(cctx context.Context, w hugio.FlexiWriter, ctx hooks.TableContext) error { - return hr.templateHandler.ExecuteWithContext(cctx, hr.templ, w, ctx) -} - -func (hr hookRendererTemplate) ResolvePosition(ctx any) text.Position { - return hr.resolvePosition(ctx) -} - -func (hr hookRendererTemplate) IsDefaultCodeBlockRenderer() bool { - return false -} - -func (s *Site) renderForTemplate(ctx context.Context, name, outputFormat string, d any, w io.Writer, templ *tplimpl.TemplInfo) (err error) { - if templ == nil { - s.logMissingLayout(name, "", "", outputFormat) - return nil - } - - if ctx == nil { - panic("nil context") - } - - if err = s.GetTemplateStore().ExecuteWithContext(ctx, templ, w, d); err != nil { - filename := name - if p, ok := d.(*pageState); ok { - filename = p.String() - } - return fmt.Errorf("render of %q failed: %w", filename, err) - } - return -} - -func (s *Site) shouldBuild(p page.Page) bool { - if !s.conf.IsKindEnabled(p.Kind()) { - return false - } - return shouldBuild(s.Conf.BuildFuture(), s.Conf.BuildExpired(), - s.Conf.BuildDrafts(), p.Draft(), p.PublishDate(), p.ExpiryDate()) -} - -func shouldBuild(buildFuture bool, buildExpired bool, buildDrafts bool, Draft bool, - publishDate time.Time, expiryDate time.Time, -) bool { - if !(buildDrafts || !Draft) { - return false - } - hnow := htime.Now() - if !buildFuture && !publishDate.IsZero() && publishDate.After(hnow) { - return false - } - if !buildExpired && !expiryDate.IsZero() && expiryDate.Before(hnow) { - return false - } - return true -} - -func (s *Site) render(ctx *siteRenderContext) (err error) { - if err := page.Clear(); err != nil { - return err - } - - if ctx.outIdx == 0 && s.h.buildCounter.Load() == 0 { - // Note that even if disableAliases is set, the aliases themselves are - // preserved on page. The motivation with this is to be able to generate - // 301 redirects in a .htaccess file and similar using a custom output format. - if !s.conf.DisableAliases { - // Aliases must be rendered before pages. - // Some sites, Hugo docs included, have faulty alias definitions that point - // to itself or another real page. These will be overwritten in the next - // step. - if err = s.renderAliases(); err != nil { - return + if !s.Cfg.GetBool("disableHugoGeneratorInject") { + transformLinks = append(transformLinks, transform.HugoGeneratorInject) } } } - if err = s.renderPages(ctx); err != nil { - return + var path []byte + + if s.Info.relativeURLs { + path = []byte(helpers.GetDottedRelativePath(dest)) + } else if s.Info.canonifyURLs { + url := s.Cfg.GetString("baseURL") + if !strings.HasSuffix(url, "/") { + url += "/" + } + path = []byte(url) } - if !ctx.shouldRenderStandalonePage("") { - return + transformer := transform.NewChain(transformLinks...) + if err := transformer.Apply(outBuffer, renderBuffer, path); err != nil { + helpers.DistinctErrorLog.Println(err) + return nil } - if err = s.renderMainLanguageRedirect(); err != nil { - return - } - - return + return s.publish(dest, outBuffer) +} + +func (s *Site) renderForLayouts(name string, d interface{}, w io.Writer, layouts ...string) error { + templ := s.findFirstTemplate(layouts...) + if templ == nil { + return fmt.Errorf("[%s] Unable to locate layout for %q: %s\n", s.Language.Lang, name, layouts) + } + + if err := templ.Execute(w, d); err != nil { + // Behavior here should be dependent on if running in server or watch mode. + helpers.DistinctErrorLog.Printf("Error while rendering %q: %s", name, err) + if !s.running() && !testMode { + // TODO(bep) check if this can be propagated + os.Exit(-1) + } else if testMode { + return err + } + } + + return nil +} + +func (s *Site) findFirstTemplate(layouts ...string) tpl.Template { + for _, layout := range layouts { + if templ := s.Tmpl.Lookup(layout); templ != nil { + return templ + } + } + return nil +} + +func (s *Site) publish(path string, r io.Reader) (err error) { + path = filepath.Join(s.absPublishDir(), path) + return helpers.WriteToDisk(path, r, s.Fs.Destination) +} + +func (s *Site) draftStats() string { + var msg string + + switch s.draftCount { + case 0: + return "0 draft content" + case 1: + msg = "1 draft rendered" + default: + msg = fmt.Sprintf("%d drafts rendered", s.draftCount) + } + + if s.Cfg.GetBool("buildDrafts") { + return fmt.Sprintf("%d of ", s.draftCount) + msg + } + + return "0 of " + msg +} + +func (s *Site) futureStats() string { + var msg string + + switch s.futureCount { + case 0: + return "0 future content" + case 1: + msg = "1 future rendered" + default: + msg = fmt.Sprintf("%d futures rendered", s.futureCount) + } + + if s.Cfg.GetBool("buildFuture") { + return fmt.Sprintf("%d of ", s.futureCount) + msg + } + + return "0 of " + msg +} + +func (s *Site) expiredStats() string { + var msg string + + switch s.expiredCount { + case 0: + return "0 expired content" + case 1: + msg = "1 expired rendered" + default: + msg = fmt.Sprintf("%d expired rendered", s.expiredCount) + } + + if s.Cfg.GetBool("buildExpired") { + return fmt.Sprintf("%d of ", s.expiredCount) + msg + } + + return "0 of " + msg +} + +func getGoMaxProcs() int { + if gmp := os.Getenv("GOMAXPROCS"); gmp != "" { + if p, err := strconv.Atoi(gmp); err != nil { + return p + } + } + return 1 +} + +func (s *Site) newNodePage(typ string, sections ...string) *Page { + p := &Page{ + language: s.Language, + pageInit: &pageInit{}, + Kind: typ, + Data: make(map[string]interface{}), + Site: &s.Info, + sections: sections, + s: s} + + p.outputFormats = p.s.outputFormats[p.Kind] + + return p + +} + +func (s *Site) newHomePage() *Page { + p := s.newNodePage(KindHome) + p.Title = s.Info.Title + pages := Pages{} + p.Data["Pages"] = pages + p.Pages = pages + return p +} + +func (s *Site) newTaxonomyPage(plural, key string) *Page { + + p := s.newNodePage(KindTaxonomy, plural, key) + + if s.Info.preserveTaxonomyNames { + // Keep (mostly) as is in the title + // We make the first character upper case, mostly because + // it is easier to reason about in the tests. + p.Title = helpers.FirstUpper(key) + key = s.PathSpec.MakePathSanitized(key) + } else { + p.Title = strings.Replace(strings.Title(key), "-", " ", -1) + } + + return p +} + +func (s *Site) newSectionPage(name string) *Page { + p := s.newNodePage(KindSection, name) + + sectionName := helpers.FirstUpper(name) + if s.Cfg.GetBool("pluralizeListTitles") { + p.Title = inflect.Pluralize(sectionName) + } else { + p.Title = sectionName + } + return p +} + +func (s *Site) newTaxonomyTermsPage(plural string) *Page { + p := s.newNodePage(KindTaxonomyTerm, plural) + p.Title = strings.Title(plural) + return p } diff --git a/hugolib/siteJSONEncode_test.go b/hugolib/siteJSONEncode_test.go index 94bac1873..9c83899fd 100644 --- a/hugolib/siteJSONEncode_test.go +++ b/hugolib/siteJSONEncode_test.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Hugo Authors. All rights reserved. +// Copyright 2015 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,12 @@ package hugolib import ( + "encoding/json" "testing" + + "path/filepath" + + "github.com/gohugoio/hugo/deps" ) // Issue #1123 @@ -22,23 +27,25 @@ import ( // May be smart to run with: -timeout 4000ms func TestEncodePage(t *testing.T) { t.Parallel() + cfg, fs := newTestCfg() - templ := `Page: |{{ index .Site.RegularPages 0 | jsonify }}| -Site: {{ site | jsonify }} -` + // borrowed from menu_test.go + for _, src := range menuPageSources { + writeSource(t, fs, filepath.Join("content", src.Name), string(src.Content)) - b := newTestSitesBuilder(t) - b.WithSimpleConfigFile().WithTemplatesAdded("index.html", templ) - b.WithContent("page.md", `--- -title: "Page" -date: 2019-02-28 ---- + } -Content. + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) -`) + _, err := json.Marshal(s) + check(t, err) - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", `"Date":"2019-02-28T00:00:00Z"`) + _, err = json.Marshal(s.RegularPages[0]) + check(t, err) +} + +func check(t *testing.T, err error) { + if err != nil { + t.Fatalf("Failed %s", err) + } } diff --git a/hugolib/site_benchmark_new_test.go b/hugolib/site_benchmark_new_test.go deleted file mode 100644 index 023d8e4d5..000000000 --- a/hugolib/site_benchmark_new_test.go +++ /dev/null @@ -1,560 +0,0 @@ -// Copyright 2019 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 hugolib - -import ( - "fmt" - "math/rand" - "path" - "path/filepath" - "strconv" - "strings" - "testing" - - "github.com/gohugoio/hugo/resources/page" - - qt "github.com/frankban/quicktest" -) - -type siteBenchmarkTestcase struct { - name string - create func(t testing.TB) *sitesBuilder - check func(s *sitesBuilder) -} - -func getBenchmarkSiteDeepContent(b testing.TB) *sitesBuilder { - pageContent := func(size int) string { - return getBenchmarkTestDataPageContentForMarkdown(size, false, "", benchmarkMarkdownSnippets) - } - - sb := newTestSitesBuilder(b).WithConfigFile("toml", ` -baseURL = "https://example.com" - -[languages] -[languages.en] -weight=1 -contentDir="content/en" -[languages.fr] -weight=2 -contentDir="content/fr" -[languages.no] -weight=3 -contentDir="content/no" -[languages.sv] -weight=4 -contentDir="content/sv" - -`) - - createContent := func(dir, name string) { - sb.WithContent(filepath.Join("content", dir, name), pageContent(1)) - } - - createBundledFiles := func(dir string) { - sb.WithContent(filepath.Join("content", dir, "data.json"), `{ "hello": "world" }`) - for i := 1; i <= 3; i++ { - sb.WithContent(filepath.Join("content", dir, fmt.Sprintf("page%d.md", i)), pageContent(1)) - } - } - - for _, lang := range []string{"en", "fr", "no", "sv"} { - for level := 1; level <= 5; level++ { - sectionDir := path.Join(lang, strings.Repeat("section/", level)) - createContent(sectionDir, "_index.md") - createBundledFiles(sectionDir) - for i := 1; i <= 3; i++ { - leafBundleDir := path.Join(sectionDir, fmt.Sprintf("bundle%d", i)) - createContent(leafBundleDir, "index.md") - createBundledFiles(path.Join(leafBundleDir, "assets1")) - createBundledFiles(path.Join(leafBundleDir, "assets1", "assets2")) - } - } - } - - return sb -} - -func getBenchmarkTestDataPageContentForMarkdown(size int, toml bool, category, markdown string) string { - base := `--- -title: "My Page" -%s ---- - -My page content. -` - if toml { - base = `+++ -title="My Page" -%s -+++ - -My page content. -` - } - - var categoryKey string - if category != "" { - categoryKey = fmt.Sprintf("categories: [%s]", category) - if toml { - categoryKey = fmt.Sprintf("categories=[%s]", category) - } - } - base = fmt.Sprintf(base, categoryKey) - - return base + strings.Repeat(markdown, size) -} - -const benchmarkMarkdownSnippets = ` - -## Links - - -This is [an example](http://example.com/ "Title") inline link. - -[This link](http://example.net/) has no title attribute. - -This is [Relative](/all-is-relative). - -See my [About](/about/) page for details. -` - -func getBenchmarkSiteTestCases() []siteBenchmarkTestcase { - pageContentWithCategory := func(size int, category string) string { - return getBenchmarkTestDataPageContentForMarkdown(size, false, category, benchmarkMarkdownSnippets) - } - - pageContent := func(size int) string { - return getBenchmarkTestDataPageContentForMarkdown(size, false, "", benchmarkMarkdownSnippets) - } - - config := ` -baseURL = "https://example.com" -` - - benchmarks := []siteBenchmarkTestcase{ - { - "Bundle with image", func(b testing.TB) *sitesBuilder { - sb := newTestSitesBuilder(b).WithConfigFile("toml", config) - sb.WithContent("content/blog/mybundle/index.md", pageContent(1)) - sb.WithSunset("content/blog/mybundle/sunset1.jpg") - - return sb - }, - func(s *sitesBuilder) { - s.AssertFileContent("public/blog/mybundle/index.html", "/blog/mybundle/sunset1.jpg") - s.CheckExists("public/blog/mybundle/sunset1.jpg") - }, - }, - { - "Bundle with JSON file", func(b testing.TB) *sitesBuilder { - sb := newTestSitesBuilder(b).WithConfigFile("toml", config) - sb.WithContent("content/blog/mybundle/index.md", pageContent(1)) - sb.WithContent("content/blog/mybundle/mydata.json", `{ "hello": "world" }`) - - return sb - }, - func(s *sitesBuilder) { - s.AssertFileContent("public/blog/mybundle/index.html", "Resources: application/json: /blog/mybundle/mydata.json") - s.CheckExists("public/blog/mybundle/mydata.json") - }, - }, - { - "Tags and categories", func(b testing.TB) *sitesBuilder { - sb := newTestSitesBuilder(b).WithConfigFile("toml", ` -title = "Tags and Cats" -baseURL = "https://example.com" - -`) - - const pageTemplate = ` ---- -title: "Some tags and cats" -categories: ["caGR", "cbGR"] -tags: ["taGR", "tbGR"] ---- - -Some content. - -` - for i := 1; i <= 100; i++ { - content := strings.Replace(pageTemplate, "GR", strconv.Itoa(i/3), -1) - sb.WithContent(fmt.Sprintf("content/page%d.md", i), content) - } - - return sb - }, - func(s *sitesBuilder) { - s.AssertFileContent("public/page3/index.html", "/page3/|Permalink: https://example.com/page3/") - s.AssertFileContent("public/tags/ta3/index.html", "a3") - }, - }, - { - "Canonify URLs", func(b testing.TB) *sitesBuilder { - sb := newTestSitesBuilder(b).WithConfigFile("toml", ` -title = "Canon" -baseURL = "https://example.com" -canonifyURLs = true - -`) - for i := 1; i <= 100; i++ { - sb.WithContent(fmt.Sprintf("content/page%d.md", i), pageContent(i)) - } - - return sb - }, - func(s *sitesBuilder) { - s.AssertFileContent("public/page8/index.html", "https://example.com/about/") - }, - }, - - { - "Deep content tree", func(b testing.TB) *sitesBuilder { - return getBenchmarkSiteDeepContent(b) - }, - func(s *sitesBuilder) { - s.CheckExists("public/blog/mybundle/index.html") - s.Assert(len(s.H.Sites), qt.Equals, 4) - s.Assert(len(s.H.Sites[0].RegularPages()), qt.Equals, len(s.H.Sites[1].RegularPages())) - s.Assert(len(s.H.Sites[0].RegularPages()), qt.Equals, 30) - }, - }, - { - "TOML front matter", func(b testing.TB) *sitesBuilder { - sb := newTestSitesBuilder(b).WithConfigFile("toml", config) - for i := 1; i <= 200; i++ { - content := getBenchmarkTestDataPageContentForMarkdown(1, true, "\"a\", \"b\", \"c\"", benchmarkMarkdownSnippets) - sb.WithContent(fmt.Sprintf("content/p%d.md", i), content) - } - - return sb - }, - func(s *sitesBuilder) { - }, - }, - { - "Many HTML templates", func(b testing.TB) *sitesBuilder { - pageTemplateTemplate := ` -<!DOCTYPE html> -<html> - <head> - <meta charset="utf-8"> - <title>{{ if not .IsPage }}{{ .Title }}{{ else }}{{ printf "Site: %s" site.Title }}{{ end }} - - - -
    {{ .Content }}
    - - - -` - - sb := newTestSitesBuilder(b).WithConfigFile("toml", ` -baseURL = "https://example.com" - -[languages] -[languages.en] -weight=1 -contentDir="content/en" -[languages.fr] -weight=2 -contentDir="content/fr" -[languages.no] -weight=3 -contentDir="content/no" -[languages.sv] -weight=4 -contentDir="content/sv" - -`) - - createContent := func(dir, name string) { - sb.WithContent(filepath.Join("content", dir, name), pageContent(1)) - } - - for _, lang := range []string{"en", "fr", "no", "sv"} { - sb.WithTemplatesAdded(fmt.Sprintf("_default/single.%s.html", lang), pageTemplateTemplate) - sb.WithTemplatesAdded(fmt.Sprintf("_default/list.%s.html", lang), pageTemplateTemplate) - - for level := 1; level <= 5; level++ { - sectionDir := path.Join(lang, strings.Repeat("section/", level)) - createContent(sectionDir, "_index.md") - for i := 1; i <= 3; i++ { - leafBundleDir := path.Join(sectionDir, fmt.Sprintf("bundle%d", i)) - createContent(leafBundleDir, "index.md") - } - } - } - - return sb - }, - func(s *sitesBuilder) { - s.CheckExists("public/blog/mybundle/index.html") - s.Assert(len(s.H.Sites), qt.Equals, 4) - s.Assert(len(s.H.Sites[0].RegularPages()), qt.Equals, len(s.H.Sites[1].RegularPages())) - s.Assert(len(s.H.Sites[0].RegularPages()), qt.Equals, 15) - }, - }, - { - "Page collections", func(b testing.TB) *sitesBuilder { - pageTemplateTemplate := ` -{{ if .IsNode }} -{{ len .Paginator.Pages }} -{{ end }} -{{ len .Sections }} -{{ len .Pages }} -{{ len .RegularPages }} -{{ len .Resources }} -{{ len site.RegularPages }} -{{ len site.Pages }} -{{ with .NextInSection }}Next in section: {{ .RelPermalink }}{{ end }} -{{ with .PrevInSection }}Prev in section: {{ .RelPermalink }}{{ end }} -{{ with .Next }}Next: {{ .RelPermalink }}{{ end }} -{{ with .Prev }}Prev: {{ .RelPermalink }}{{ end }} -` - - sb := newTestSitesBuilder(b).WithConfigFile("toml", ` -baseURL = "https://example.com" - -[languages] -[languages.en] -weight=1 -contentDir="content/en" -[languages.fr] -weight=2 -contentDir="content/fr" -[languages.no] -weight=3 -contentDir="content/no" -[languages.sv] -weight=4 -contentDir="content/sv" - -`) - - sb.WithTemplates("index.html", pageTemplateTemplate) - sb.WithTemplates("_default/single.html", pageTemplateTemplate) - sb.WithTemplates("_default/list.html", pageTemplateTemplate) - - r := rand.New(rand.NewSource(99)) - - createContent := func(dir, name string) { - var content string - if strings.Contains(name, "_index") { - content = pageContent(1) - } else { - content = pageContentWithCategory(1, fmt.Sprintf("category%d", r.Intn(5)+1)) - } - - sb.WithContent(filepath.Join("content", dir, name), content) - } - - createBundledFiles := func(dir string) { - sb.WithContent(filepath.Join("content", dir, "data.json"), `{ "hello": "world" }`) - for i := 1; i <= 3; i++ { - sb.WithContent(filepath.Join("content", dir, fmt.Sprintf("page%d.md", i)), pageContent(1)) - } - } - - for _, lang := range []string{"en", "fr", "no", "sv"} { - for level := 1; level <= r.Intn(5)+1; level++ { - sectionDir := path.Join(lang, strings.Repeat("section/", level)) - createContent(sectionDir, "_index.md") - createBundledFiles(sectionDir) - for i := 1; i <= r.Intn(20)+1; i++ { - leafBundleDir := path.Join(sectionDir, fmt.Sprintf("bundle%d", i)) - createContent(leafBundleDir, "index.md") - createBundledFiles(path.Join(leafBundleDir, "assets1")) - createBundledFiles(path.Join(leafBundleDir, "assets1", "assets2")) - } - } - } - - return sb - }, - func(s *sitesBuilder) { - s.CheckExists("public/blog/mybundle/index.html") - s.Assert(len(s.H.Sites), qt.Equals, 4) - s.Assert(len(s.H.Sites[0].RegularPages()), qt.Equals, 26) - }, - }, - { - "List terms", func(b testing.TB) *sitesBuilder { - pageTemplateTemplate := ` - -` - - sb := newTestSitesBuilder(b).WithConfigFile("toml", ` -baseURL = "https://example.com" -`) - - sb.WithTemplates("_default/single.html", pageTemplateTemplate) - sb.WithTemplates("_default/list.html", "List") - - r := rand.New(rand.NewSource(99)) - - createContent := func(dir, name string) { - var content string - if strings.Contains(name, "_index") { - // Empty - } else { - content = pageContentWithCategory(1, fmt.Sprintf("category%d", r.Intn(5)+1)) - } - sb.WithContent(filepath.Join("content", dir, name), content) - } - - for level := 1; level <= r.Intn(5)+1; level++ { - sectionDir := path.Join(strings.Repeat("section/", level)) - createContent(sectionDir, "_index.md") - for i := 1; i <= r.Intn(33); i++ { - leafBundleDir := path.Join(sectionDir, fmt.Sprintf("bundle%d", i)) - createContent(leafBundleDir, "index.md") - } - } - - return sb - }, - func(s *sitesBuilder) { - s.AssertFileContent("public/section/bundle8/index.html", ``) - s.Assert(len(s.H.Sites), qt.Equals, 1) - s.Assert(len(s.H.Sites[0].RegularPages()), qt.Equals, 35) - }, - }, - } - - return benchmarks -} - -// Run the benchmarks below as tests. Mostly useful when adding new benchmark -// variants. -func TestBenchmarkSite(b *testing.T) { - benchmarks := getBenchmarkSiteTestCases() - for _, bm := range benchmarks { - if bm.name != "Deep content tree" { - continue - } - b.Run(bm.name, func(b *testing.T) { - s := bm.create(b) - - err := s.BuildE(BuildCfg{}) - if err != nil { - b.Fatal(err) - } - bm.check(s) - }) - } -} - -func TestBenchmarkSiteDeepContentEdit(t *testing.T) { - b := getBenchmarkSiteDeepContent(t).Running() - b.Build(BuildCfg{}) - - p := b.H.Sites[0].RegularPages()[12] - - b.EditFiles(p.File().Filename(), fmt.Sprintf(`--- -title: %s ---- - -Edited!!`, p.Title())) - - counters := &buildCounters{} - - b.Build(BuildCfg{testCounters: counters}) - - // We currently rebuild all the language versions of the same content file. - // We could probably optimize that case, but it's not trivial. - b.Assert(int(counters.contentRenderCounter.Load()), qt.Equals, 4) - b.AssertFileContent("public"+p.RelPermalink()+"index.html", "Edited!!") -} - -func BenchmarkSiteNew(b *testing.B) { - rnd := rand.New(rand.NewSource(32)) - benchmarks := getBenchmarkSiteTestCases() - for _, edit := range []bool{true, false} { - for _, bm := range benchmarks { - name := bm.name - if edit { - name = "Edit_" + name - } else { - name = "Regular_" + name - } - b.Run(name, func(b *testing.B) { - sites := make([]*sitesBuilder, b.N) - for i := 0; i < b.N; i++ { - sites[i] = bm.create(b) - if edit { - sites[i].Running() - } - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - if edit { - b.StopTimer() - } - s := sites[i] - err := s.BuildE(BuildCfg{}) - if err != nil { - b.Fatal(err) - } - bm.check(s) - - if edit { - if edit { - b.StartTimer() - } - // Edit a random page in a random language. - pages := s.H.Sites[rnd.Intn(len(s.H.Sites))].Pages() - var p page.Page - count := 0 - for { - count++ - if count > 100 { - panic("infinite loop") - } - p = pages[rnd.Intn(len(pages))] - if p.File() != nil { - break - } - } - - s.EditFiles(p.File().Filename(), fmt.Sprintf(`--- -title: %s ---- - -Edited!!`, p.Title())) - - err := s.BuildE(BuildCfg{}) - if err != nil { - b.Fatal(err) - } - } - } - }) - } - } -} diff --git a/hugolib/site_benchmark_test.go b/hugolib/site_benchmark_test.go new file mode 100644 index 000000000..d23271af7 --- /dev/null +++ b/hugolib/site_benchmark_test.go @@ -0,0 +1,254 @@ +// Copyright 2017-present 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 hugolib + +import ( + "fmt" + "math/rand" + "path/filepath" + "strings" + "testing" + + "github.com/spf13/afero" +) + +type siteBuildingBenchmarkConfig struct { + Frontmatter string + NumPages int + RootSections int + Render bool + Shortcodes bool + NumTags int + TagsPerPage int +} + +func (s siteBuildingBenchmarkConfig) String() string { + // Make it comma separated with no spaces, so it is both Bash and regexp friendly. + // To make it a short as possible, we only shows bools when enabled and ints when >= 0 (RootSections > 1) + sep := "," + id := s.Frontmatter + sep + if s.RootSections > 1 { + id += fmt.Sprintf("num_root_sections=%d%s", s.RootSections, sep) + } + id += fmt.Sprintf("num_pages=%d%s", s.NumPages, sep) + + if s.NumTags > 0 { + id += fmt.Sprintf("num_tags=%d%s", s.NumTags, sep) + } + + if s.TagsPerPage > 0 { + id += fmt.Sprintf("tags_per_page=%d%s", s.TagsPerPage, sep) + } + + if s.Shortcodes { + id += "shortcodes" + sep + } + + if s.Render { + id += "render" + sep + } + + return strings.TrimSuffix(id, sep) + +} + +func BenchmarkSiteBuilding(b *testing.B) { + var conf siteBuildingBenchmarkConfig + for _, frontmatter := range []string{"YAML", "TOML"} { + conf.Frontmatter = frontmatter + for _, rootSections := range []int{1, 5} { + conf.RootSections = rootSections + for _, numTags := range []int{0, 1, 10, 20, 50, 100, 500, 1000, 5000} { + conf.NumTags = numTags + for _, tagsPerPage := range []int{0, 1, 5, 20, 50, 80} { + conf.TagsPerPage = tagsPerPage + for _, numPages := range []int{1, 10, 100, 500, 1000, 5000, 10000} { + conf.NumPages = numPages + for _, render := range []bool{false, true} { + conf.Render = render + for _, shortcodes := range []bool{false, true} { + conf.Shortcodes = shortcodes + doBenchMarkSiteBuilding(conf, b) + } + } + } + } + } + } + } +} + +func doBenchMarkSiteBuilding(conf siteBuildingBenchmarkConfig, b *testing.B) { + b.Run(conf.String(), func(b *testing.B) { + sites := createHugoBenchmarkSites(b, b.N, conf) + b.ResetTimer() + for i := 0; i < b.N; i++ { + h := sites[0] + + err := h.Build(BuildCfg{SkipRender: !conf.Render}) + if err != nil { + b.Fatal(err) + } + + // Try to help the GC + sites[0] = nil + sites = sites[1:len(sites)] + } + }) +} + +func createHugoBenchmarkSites(b *testing.B, count int, cfg siteBuildingBenchmarkConfig) []*HugoSites { + someMarkdown := ` +An h1 header +============ + +Paragraphs are separated by a blank line. + +2nd paragraph. *Italic* and **bold**. Itemized lists +look like: + + * this one + * that one + * the other one + +Note that --- not considering the asterisk --- the actual text +content starts at 4-columns in. + +> Block quotes are +> written like so. +> +> They can span multiple paragraphs, +> if you like. + +Use 3 dashes for an em-dash. Use 2 dashes for ranges (ex., "it's all +in chapters 12--14"). Three dots ... will be converted to an ellipsis. +Unicode is supported. ☺ +` + + someMarkdownWithShortCode := someMarkdown + ` + +{{< myShortcode >}} + +` + + pageTemplateTOML := `+++ +title = "%s" +tags = %s ++++ +%s + +` + + pageTemplateYAML := `--- +title: "%s" +tags: +%s +--- +%s + +` + + siteConfig := ` +baseURL = "http://example.com/blog" + +paginate = 10 +defaultContentLanguage = "en" + +[Taxonomies] +tag = "tags" +category = "categories" +` + + numTags := cfg.NumTags + + if cfg.TagsPerPage > numTags { + numTags = cfg.TagsPerPage + } + + var ( + contentPagesContent [3]string + tags = make([]string, numTags) + pageTemplate string + ) + + for i := 0; i < numTags; i++ { + tags[i] = fmt.Sprintf("Hugo %d", i+1) + } + + var tagsStr string + + if cfg.Shortcodes { + contentPagesContent = [3]string{ + someMarkdownWithShortCode, + strings.Repeat(someMarkdownWithShortCode, 2), + strings.Repeat(someMarkdownWithShortCode, 3), + } + } else { + contentPagesContent = [3]string{ + someMarkdown, + strings.Repeat(someMarkdown, 2), + strings.Repeat(someMarkdown, 3), + } + } + + sites := make([]*HugoSites, count) + for i := 0; i < count; i++ { + // Maybe consider reusing the Source fs + mf := afero.NewMemMapFs() + th, h := newTestSitesFromConfig(b, mf, siteConfig, + "layouts/_default/single.html", `Single HTML|{{ .Title }}|{{ .Content }}`, + "layouts/_default/list.html", `List HTML|{{ .Title }}|{{ .Content }}`, + "layouts/shortcodes/myShortcode.html", `

    MyShortcode

    `) + + fs := th.Fs + + pagesPerSection := cfg.NumPages / cfg.RootSections + + for i := 0; i < cfg.RootSections; i++ { + for j := 0; j < pagesPerSection; j++ { + var tagsSlice []string + + if numTags > 0 { + tagsStart := rand.Intn(numTags) - cfg.TagsPerPage + if tagsStart < 0 { + tagsStart = 0 + } + tagsSlice = tags[tagsStart : tagsStart+cfg.TagsPerPage] + } + + if cfg.Frontmatter == "TOML" { + pageTemplate = pageTemplateTOML + tagsStr = "[]" + if cfg.TagsPerPage > 0 { + tagsStr = strings.Replace(fmt.Sprintf("%q", tagsSlice), " ", ", ", -1) + } + } else { + // YAML + pageTemplate = pageTemplateYAML + for _, tag := range tagsSlice { + tagsStr += "\n- " + tag + } + } + + content := fmt.Sprintf(pageTemplate, fmt.Sprintf("Title%d_%d", i, j), tagsStr, contentPagesContent[rand.Intn(3)]) + + writeSource(b, fs, filepath.Join("content", fmt.Sprintf("sect%d", i), fmt.Sprintf("page%d.md", j)), content) + } + } + + sites[i] = h + } + + return sites +} diff --git a/hugolib/site_output.go b/hugolib/site_output.go index 3438ea9f7..f5eb2ba57 100644 --- a/hugolib/site_output.go +++ b/hugolib/site_output.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Hugo Authors. All rights reserved. +// Copyright 2017-present 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,95 +15,85 @@ package hugolib import ( "fmt" + "path" "strings" + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/output" - "github.com/gohugoio/hugo/resources/kinds" "github.com/spf13/cast" ) -func createDefaultOutputFormats(allFormats output.Formats) map[string]output.Formats { - rssOut, rssFound := allFormats.GetByName(output.RSSFormat.Name) - htmlOut, _ := allFormats.GetByName(output.HTMLFormat.Name) - robotsOut, _ := allFormats.GetByName(output.RobotsTxtFormat.Name) - sitemapOut, _ := allFormats.GetByName(output.SitemapFormat.Name) - httpStatus404Out, _ := allFormats.GetByName(output.HTTPStatus404HTMLFormat.Name) - - defaultListTypes := output.Formats{htmlOut} - if rssFound { - defaultListTypes = append(defaultListTypes, rssOut) - } - - m := map[string]output.Formats{ - kinds.KindPage: {htmlOut}, - kinds.KindHome: defaultListTypes, - kinds.KindSection: defaultListTypes, - kinds.KindTerm: defaultListTypes, - kinds.KindTaxonomy: defaultListTypes, - // Below are for consistency. They are currently not used during rendering. - kinds.KindSitemap: {sitemapOut}, - kinds.KindRobotsTXT: {robotsOut}, - kinds.KindStatus404: {httpStatus404Out}, - } - - // May be disabled - if rssFound { - m[kinds.KindRSS] = output.Formats{rssOut} - } - - return m -} - -func createSiteOutputFormats(allFormats output.Formats, outputs map[string]any, rssDisabled bool) (map[string]output.Formats, error) { - defaultOutputFormats := createDefaultOutputFormats(allFormats) - - if outputs == nil { - return defaultOutputFormats, nil +func createSiteOutputFormats(allFormats output.Formats, cfg config.Provider) (map[string]output.Formats, error) { + if !cfg.IsSet("outputs") { + return createDefaultOutputFormats(allFormats, cfg) } outFormats := make(map[string]output.Formats) - if len(outputs) == 0 { + outputs := cfg.GetStringMap("outputs") + + if outputs == nil || len(outputs) == 0 { return outFormats, nil } - seen := make(map[string]bool) - for k, v := range outputs { - k = kinds.GetKindAny(k) - if k == "" { - // Invalid kind - continue - } var formats output.Formats vals := cast.ToStringSlice(v) for _, format := range vals { f, found := allFormats.GetByName(format) if !found { - if rssDisabled && strings.EqualFold(format, "RSS") { - // This is legacy behavior. We used to have both - // a RSS page kind and output format. - continue - } - return nil, fmt.Errorf("failed to resolve output format %q from site config", format) + return nil, fmt.Errorf("Failed to resolve output format %q from site config", format) } formats = append(formats, f) } - // This effectively prevents empty outputs entries for a given Kind. - // We need at least one. if len(formats) > 0 { - seen[k] = true outFormats[k] = formats } } - // Add defaults for the entries not provided by the user. - for k, v := range defaultOutputFormats { - if !seen[k] { - outFormats[k] = v + // Make sure every kind has at least one output format + for _, kind := range allKinds { + if _, found := outFormats[kind]; !found { + outFormats[kind] = output.Formats{output.HTMLFormat} } } + return outFormats, nil + +} + +func createDefaultOutputFormats(allFormats output.Formats, cfg config.Provider) (map[string]output.Formats, error) { + outFormats := make(map[string]output.Formats) + rssOut, _ := allFormats.GetByName(output.RSSFormat.Name) + htmlOut, _ := allFormats.GetByName(output.HTMLFormat.Name) + + for _, kind := range allKinds { + var formats output.Formats + // All have HTML + formats = append(formats, htmlOut) + + // All but page have RSS + if kind != KindPage { + + rssBase := cfg.GetString("rssURI") + if rssBase == "" || rssBase == "index.xml" { + rssBase = rssOut.BaseName + } else { + // Remove in Hugo 0.22. + helpers.Deprecated("Site config", "rssURI", "Set baseName in outputFormats.RSS", false) + // RSS has now a well defined media type, so strip any suffix provided + rssBase = strings.TrimSuffix(rssBase, path.Ext(rssBase)) + } + + rssOut.BaseName = rssBase + formats = append(formats, rssOut) + + } + + outFormats[kind] = formats + } + return outFormats, nil } diff --git a/hugolib/site_output_test.go b/hugolib/site_output_test.go index caec4c700..8455a13f7 100644 --- a/hugolib/site_output_test.go +++ b/hugolib/site_output_test.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Hugo Authors. All rights reserved. +// Copyright 2017-present 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,42 +14,89 @@ package hugolib import ( - "fmt" + "reflect" "strings" "testing" - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/common/hstrings" - "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/resources/kinds" - "github.com/spf13/afero" + "github.com/stretchr/testify/require" + + "fmt" + + "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/output" + "github.com/spf13/viper" ) +func TestDefaultOutputFormats(t *testing.T) { + t.Parallel() + defs, err := createDefaultOutputFormats(output.DefaultFormats, viper.New()) + + require.NoError(t, err) + + tests := []struct { + name string + kind string + want output.Formats + }{ + {"RSS not for regular pages", KindPage, output.Formats{output.HTMLFormat}}, + {"Home Sweet Home", KindHome, output.Formats{output.HTMLFormat, output.RSSFormat}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := defs[tt.kind]; !reflect.DeepEqual(got, tt.want) { + t.Errorf("createDefaultOutputFormats(%v) = %v, want %v", tt.kind, got, tt.want) + } + }) + } +} + +func TestDefaultOutputFormatsWithOverrides(t *testing.T) { + t.Parallel() + + htmlOut := output.HTMLFormat + htmlOut.BaseName = "htmlindex" + rssOut := output.RSSFormat + rssOut.BaseName = "feed" + + defs, err := createDefaultOutputFormats(output.Formats{htmlOut, rssOut}, viper.New()) + + homeDefs := defs[KindHome] + + rss, found := homeDefs.GetByName("RSS") + require.True(t, found) + require.Equal(t, rss.BaseName, "feed") + + html, found := homeDefs.GetByName("HTML") + require.True(t, found) + require.Equal(t, html.BaseName, "htmlindex") + + require.NoError(t, err) + +} + func TestSiteWithPageOutputs(t *testing.T) { for _, outputs := range [][]string{{"html", "json", "calendar"}, {"json"}} { - outputs := outputs t.Run(fmt.Sprintf("%v", outputs), func(t *testing.T) { - t.Parallel() doTestSiteWithPageOutputs(t, outputs) }) } } func doTestSiteWithPageOutputs(t *testing.T, outputs []string) { + t.Parallel() + outputsStr := strings.Replace(fmt.Sprintf("%q", outputs), " ", ", ", -1) siteConfig := ` baseURL = "http://example.com/blog" +paginate = 1 defaultContentLanguage = "en" -disableKinds = ["section", "term", "taxonomy", "RSS", "sitemap", "robotsTXT", "404"] - -[pagination] -pagerSize = 1 +disableKinds = ["page", "section", "taxonomy", "taxonomyTerm", "RSS", "sitemap", "robotsTXT", "404"] [Taxonomies] tag = "tags" @@ -57,7 +104,6 @@ category = "categories" defaultContentLanguage = "en" - [languages] [languages.en] @@ -79,26 +125,25 @@ outputs: %s # Doc {{< myShort >}} - -{{< myOtherShort >}} - ` - b := newTestSitesBuilder(t).WithConfigFile("toml", siteConfig) - b.WithI18n("en.toml", ` + mf := afero.NewMemMapFs() + + writeToFs(t, mf, "i18n/en.toml", ` [elbow] other = "Elbow" -`, "nn.toml", ` +`) + writeToFs(t, mf, "i18n/nn.toml", ` [elbow] other = "Olboge" `) - b.WithTemplates( + th, h := newTestSitesFromConfig(t, mf, siteConfig, + // Case issue partials #3333 "layouts/partials/GoHugo.html", `Go Hugo Partial`, "layouts/_default/baseof.json", `START JSON:{{block "main" .}}default content{{ end }}:END JSON`, "layouts/_default/baseof.html", `START HTML:{{block "main" .}}default content{{ end }}:END HTML`, - "layouts/shortcodes/myOtherShort.html", `OtherShort: {{ "

    Hi!

    " | safeHTML }}`, "layouts/shortcodes/myShort.html", `ShortHTML`, "layouts/shortcodes/myShort.json", `ShortJSON`, @@ -125,74 +170,66 @@ List HTML|{{.Title }}| Partial Hugo 1: {{ partial "GoHugo.html" . }} Partial Hugo 2: {{ partial "GoHugo" . -}} Content: {{ .Content }} -Len Pages: {{ .Kind }} {{ len .Site.RegularPages }} Page Number: {{ .Paginator.PageNumber }} {{ end }} `, - "layouts/_default/single.html", `{{ define "main" }}{{ .Content }}{{ end }}`, ) + require.Len(t, h.Sites, 2) - b.WithContent("_index.md", fmt.Sprintf(pageTemplate, "JSON Home", outputsStr)) - b.WithContent("_index.nn.md", fmt.Sprintf(pageTemplate, "JSON Nynorsk Heim", outputsStr)) + fs := th.Fs - for i := 1; i <= 10; i++ { - b.WithContent(fmt.Sprintf("p%d.md", i), fmt.Sprintf(pageTemplate, fmt.Sprintf("Page %d", i), outputsStr)) - } + writeSource(t, fs, "content/_index.md", fmt.Sprintf(pageTemplate, "JSON Home", outputsStr)) + writeSource(t, fs, "content/_index.nn.md", fmt.Sprintf(pageTemplate, "JSON Nynorsk Heim", outputsStr)) - b.Build(BuildCfg{}) + err := h.Build(BuildCfg{}) - s := b.H.Sites[0] - b.Assert(s.language.Lang, qt.Equals, "en") + require.NoError(t, err) - home := s.getPageOldVersion(kinds.KindHome) + s := h.Sites[0] + require.Equal(t, "en", s.Language.Lang) - b.Assert(home, qt.Not(qt.IsNil)) + home := s.getPage(KindHome) + + require.NotNil(t, home) lenOut := len(outputs) - b.Assert(len(home.OutputFormats()), qt.Equals, lenOut) + require.Len(t, home.outputFormats, lenOut) // There is currently always a JSON output to make it simpler ... altFormats := lenOut - 1 - hasHTML := hstrings.InSlice(outputs, "html") - b.AssertFileContent("public/index.json", + hasHTML := helpers.InStringArray(outputs, "html") + th.assertFileContent("public/index.json", "List JSON", fmt.Sprintf("Alt formats: %d", altFormats), ) if hasHTML { - b.AssertFileContent("public/index.json", - "Alt Output: html", - "Output/Rel: json/alternate|", - "Output/Rel: html/canonical|", + th.assertFileContent("public/index.json", + "Alt Output: HTML", + "Output/Rel: JSON/alternate|", + "Output/Rel: HTML/canonical|", "en: Elbow", "ShortJSON", - "OtherShort:

    Hi!

    ", ) - b.AssertFileContent("public/index.html", + th.assertFileContent("public/index.html", // The HTML entity is a deliberate part of this test: The HTML templates are // parsed with html/template. - `List HTML|JSON Home|`, + `List HTML|JSON Home|`, "en: Elbow", "ShortHTML", - "OtherShort:

    Hi!

    ", - "Len Pages: home 10", ) - b.AssertFileContent("public/page/2/index.html", "Page Number: 2") - b.Assert(b.CheckExists("public/page/2/index.json"), qt.Equals, false) - - b.AssertFileContent("public/nn/index.html", + th.assertFileContent("public/nn/index.html", "List HTML|JSON Nynorsk Heim|", "nn: Olboge") } else { - b.AssertFileContent("public/index.json", - "Output/Rel: json/canonical|", + th.assertFileContent("public/index.json", + "Output/Rel: JSON/canonical|", // JSON is plain text, so no need to safeHTML this and that - ``, + ``, "ShortJSON", - "OtherShort:

    Hi!

    ", ) - b.AssertFileContent("public/nn/index.json", + th.assertFileContent("public/nn/index.json", "List JSON|JSON Nynorsk Heim|", "nn: Olboge", "ShortJSON", @@ -200,21 +237,23 @@ Len Pages: {{ .Kind }} {{ len .Site.RegularPages }} Page Number: {{ .Paginator.P } of := home.OutputFormats() - + require.Len(t, of, lenOut) + require.Nil(t, of.Get("Hugo")) + require.NotNil(t, of.Get("json")) json := of.Get("JSON") - b.Assert(json, qt.Not(qt.IsNil)) - b.Assert(json.RelPermalink(), qt.Equals, "/blog/index.json") - b.Assert(json.Permalink(), qt.Equals, "http://example.com/blog/index.json") + _, err = home.AlternativeOutputFormats() + require.Error(t, err) + require.NotNil(t, json) + require.Equal(t, "/blog/index.json", json.RelPermalink()) + require.Equal(t, "http://example.com/blog/index.json", json.Permalink()) - if hstrings.InSlice(outputs, "cal") { + if helpers.InStringArray(outputs, "cal") { cal := of.Get("calendar") - b.Assert(cal, qt.Not(qt.IsNil)) - b.Assert(cal.RelPermalink(), qt.Equals, "/blog/index.ics") - b.Assert(cal.Permalink(), qt.Equals, "webcal://example.com/blog/index.ics") + require.NotNil(t, cal) + require.Equal(t, "/blog/index.ics", cal.RelPermalink()) + require.Equal(t, "webcal://example.com/blog/index.ics", cal.Permalink()) } - b.Assert(home.HasShortcode("myShort"), qt.Equals, true) - b.Assert(home.HasShortcode("doesNotExist"), qt.Equals, false) } // Issue #3447 @@ -222,12 +261,10 @@ func TestRedefineRSSOutputFormat(t *testing.T) { siteConfig := ` baseURL = "http://example.com/blog" +paginate = 1 defaultContentLanguage = "en" -disableKinds = ["page", "section", "term", "taxonomy", "sitemap", "robotsTXT", "404"] - -[pagination] -pagerSize = 1 +disableKinds = ["page", "section", "taxonomy", "taxonomyTerm", "sitemap", "robotsTXT", "404"] [outputFormats] [outputFormats.RSS] @@ -236,8 +273,6 @@ baseName = "feed" ` - c := qt.New(t) - mf := afero.NewMemMapFs() writeToFs(t, mf, "content/foo.html", `foo`) @@ -245,14 +280,15 @@ baseName = "feed" err := h.Build(BuildCfg{}) - c.Assert(err, qt.IsNil) + require.NoError(t, err) th.assertFileContent("public/feed.xml", "Recent content on") s := h.Sites[0] - // Issue #3450 - c.Assert(s.Home().OutputFormats().Get("rss").Permalink(), qt.Equals, "http://example.com/blog/feed.xml") + //Issue #3450 + require.Equal(t, "http://example.com/blog/feed.xml", s.Info.RSSLink) + } // Issue #3614 @@ -260,21 +296,21 @@ func TestDotLessOutputFormat(t *testing.T) { siteConfig := ` baseURL = "http://example.com/blog" +paginate = 1 defaultContentLanguage = "en" -disableKinds = ["page", "section", "term", "taxonomy", "sitemap", "robotsTXT", "404"] - -[pagination] -pagerSize = 1 +disableKinds = ["page", "section", "taxonomy", "taxonomyTerm", "sitemap", "robotsTXT", "404"] [mediaTypes] [mediaTypes."text/nodot"] +suffix = "" delimiter = "" [mediaTypes."text/defaultdelim"] -suffixes = ["defd"] +suffix = "defd" [mediaTypes."text/nosuffix"] +suffix = "" [mediaTypes."text/customdelim"] -suffixes = ["del"] +suffix = "del" delimiter = "_" [outputs] @@ -296,8 +332,6 @@ baseName = "customdelimbase" ` - c := qt.New(t) - mf := afero.NewMemMapFs() writeToFs(t, mf, "content/foo.html", `foo`) writeToFs(t, mf, "layouts/_default/list.dotless", `a dotless`) @@ -309,383 +343,23 @@ baseName = "customdelimbase" err := h.Build(BuildCfg{}) - c.Assert(err, qt.IsNil) - - s := h.Sites[0] + require.NoError(t, err) th.assertFileContent("public/_redirects", "a dotless") th.assertFileContent("public/defaultdelimbase.defd", "default delimim") // This looks weird, but the user has chosen this definition. - th.assertFileContent("public/nosuffixbase", "no suffix") + th.assertFileContent("public/nosuffixbase.", "no suffix") th.assertFileContent("public/customdelimbase_del", "custom delim") - home := s.getPageOldVersion(kinds.KindHome) - c.Assert(home, qt.Not(qt.IsNil)) + s := h.Sites[0] + home := s.getPage(KindHome) + require.NotNil(t, home) outputs := home.OutputFormats() - c.Assert(outputs.Get("DOTLESS").RelPermalink(), qt.Equals, "/blog/_redirects") - c.Assert(outputs.Get("DEF").RelPermalink(), qt.Equals, "/blog/defaultdelimbase.defd") - c.Assert(outputs.Get("NOS").RelPermalink(), qt.Equals, "/blog/nosuffixbase") - c.Assert(outputs.Get("CUS").RelPermalink(), qt.Equals, "/blog/customdelimbase_del") -} - -// Issue 8030 -func TestGetOutputFormatRel(t *testing.T) { - b := newTestSitesBuilder(t). - WithSimpleConfigFileAndSettings(map[string]any{ - "outputFormats": map[string]any{ - "HUMANS": map[string]any{ - "mediaType": "text/plain", - "baseName": "humans", - "isPlainText": true, - "rel": "author", - }, - }, - }).WithTemplates("index.html", ` -{{- with ($.Site.GetPage "humans").OutputFormats.Get "humans" -}} - -{{- end -}} -`).WithContent("humans.md", `--- -outputs: -- HUMANS ---- -This is my content. -`) - - b.Build(BuildCfg{}) - b.AssertFileContent("public/index.html", ` - -`) -} - -func TestCreateSiteOutputFormats(t *testing.T) { - t.Run("Basic", func(t *testing.T) { - c := qt.New(t) - - outputsConfig := map[string]any{ - kinds.KindHome: []string{"HTML", "JSON"}, - kinds.KindSection: []string{"JSON"}, - } - - cfg := config.New() - cfg.Set("outputs", outputsConfig) - - outputs, err := createSiteOutputFormats(output.DefaultFormats, cfg.GetStringMap("outputs"), false) - c.Assert(err, qt.IsNil) - c.Assert(outputs[kinds.KindSection], deepEqualsOutputFormats, output.Formats{output.JSONFormat}) - c.Assert(outputs[kinds.KindHome], deepEqualsOutputFormats, output.Formats{output.HTMLFormat, output.JSONFormat}) - - // Defaults - c.Assert(outputs[kinds.KindTerm], deepEqualsOutputFormats, output.Formats{output.HTMLFormat, output.RSSFormat}) - c.Assert(outputs[kinds.KindTaxonomy], deepEqualsOutputFormats, output.Formats{output.HTMLFormat, output.RSSFormat}) - c.Assert(outputs[kinds.KindPage], deepEqualsOutputFormats, output.Formats{output.HTMLFormat}) - - // These aren't (currently) in use when rendering in Hugo, - // but the pages needs to be assigned an output format, - // so these should also be correct/sensible. - c.Assert(outputs[kinds.KindRSS], deepEqualsOutputFormats, output.Formats{output.RSSFormat}) - c.Assert(outputs[kinds.KindSitemap], deepEqualsOutputFormats, output.Formats{output.SitemapFormat}) - c.Assert(outputs[kinds.KindRobotsTXT], deepEqualsOutputFormats, output.Formats{output.RobotsTxtFormat}) - c.Assert(outputs[kinds.KindStatus404], deepEqualsOutputFormats, output.Formats{output.HTTPStatus404HTMLFormat}) - }) - - // Issue #4528 - t.Run("Mixed case", func(t *testing.T) { - c := qt.New(t) - cfg := config.New() - - outputsConfig := map[string]any{ - // Note that we in Hugo 0.53.0 renamed this Kind to "taxonomy", - // but keep this test to test the legacy mapping. - "taxonomyterm": []string{"JSON"}, - } - cfg.Set("outputs", outputsConfig) - - outputs, err := createSiteOutputFormats(output.DefaultFormats, cfg.GetStringMap("outputs"), false) - c.Assert(err, qt.IsNil) - c.Assert(outputs[kinds.KindTaxonomy], deepEqualsOutputFormats, output.Formats{output.JSONFormat}) - }) -} - -func TestCreateSiteOutputFormatsInvalidConfig(t *testing.T) { - c := qt.New(t) - - outputsConfig := map[string]any{ - kinds.KindHome: []string{"FOO", "JSON"}, - } - - cfg := config.New() - cfg.Set("outputs", outputsConfig) - - _, err := createSiteOutputFormats(output.DefaultFormats, cfg.GetStringMap("outputs"), false) - c.Assert(err, qt.Not(qt.IsNil)) -} - -func TestCreateSiteOutputFormatsEmptyConfig(t *testing.T) { - c := qt.New(t) - - outputsConfig := map[string]any{ - kinds.KindHome: []string{}, - } - - cfg := config.New() - cfg.Set("outputs", outputsConfig) - - outputs, err := createSiteOutputFormats(output.DefaultFormats, cfg.GetStringMap("outputs"), false) - c.Assert(err, qt.IsNil) - c.Assert(outputs[kinds.KindHome], deepEqualsOutputFormats, output.Formats{output.HTMLFormat, output.RSSFormat}) -} - -func TestCreateSiteOutputFormatsCustomFormats(t *testing.T) { - c := qt.New(t) - - outputsConfig := map[string]any{ - kinds.KindHome: []string{}, - } - - cfg := config.New() - cfg.Set("outputs", outputsConfig) - - var ( - customRSS = output.Format{Name: "RSS", BaseName: "customRSS"} - customHTML = output.Format{Name: "HTML", BaseName: "customHTML"} - ) - - outputs, err := createSiteOutputFormats(output.Formats{customRSS, customHTML}, cfg.GetStringMap("outputs"), false) - c.Assert(err, qt.IsNil) - c.Assert(outputs[kinds.KindHome], deepEqualsOutputFormats, output.Formats{customHTML, customRSS}) -} - -// https://github.com/gohugoio/hugo/issues/5849 -func TestOutputFormatPermalinkable(t *testing.T) { - config := ` -baseURL = "https://example.com" - - - -# DAMP is similar to AMP, but not permalinkable. -[outputFormats] -[outputFormats.damp] -mediaType = "text/html" -path = "damp" -[outputFormats.ramp] -mediaType = "text/html" -path = "ramp" -permalinkable = true -[outputFormats.base] -mediaType = "text/html" -isHTML = true -baseName = "that" -permalinkable = true -[outputFormats.nobase] -mediaType = "application/json" -permalinkable = true -isPlainText = true - -` - - b := newTestSitesBuilder(t).WithConfigFile("toml", config) - b.WithContent("_index.md", ` ---- -Title: Home Sweet Home -outputs: [ "html", "amp", "damp", "base" ] ---- - -`) - - b.WithContent("blog/html-amp.md", ` ---- -Title: AMP and HTML -outputs: [ "html", "amp" ] ---- - -`) - - b.WithContent("blog/html-damp.md", ` ---- -Title: DAMP and HTML -outputs: [ "html", "damp" ] ---- - -`) - - b.WithContent("blog/html-ramp.md", ` ---- -Title: RAMP and HTML -outputs: [ "html", "ramp" ] ---- - -`) - - b.WithContent("blog/html.md", ` ---- -Title: HTML only -outputs: [ "html" ] ---- - -`) - - b.WithContent("blog/amp.md", ` ---- -Title: AMP only -outputs: [ "amp" ] ---- - -`) - - b.WithContent("blog/html-base-nobase.md", ` ---- -Title: HTML, Base and Nobase -outputs: [ "html", "base", "nobase" ] ---- - -`) - - const commonTemplate = ` -This RelPermalink: {{ .RelPermalink }} -Output Formats: {{ len .OutputFormats }};{{ range .OutputFormats }}{{ .Name }};{{ .RelPermalink }}|{{ end }} - -` - - b.WithTemplatesAdded("index.html", commonTemplate) - b.WithTemplatesAdded("_default/single.html", commonTemplate) - b.WithTemplatesAdded("_default/single.json", commonTemplate) - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", - "This RelPermalink: /", - "Output Formats: 4;html;/|amp;/amp/|damp;/damp/|base;/that.html|", - ) - - b.AssertFileContent("public/amp/index.html", - "This RelPermalink: /amp/", - "Output Formats: 4;html;/|amp;/amp/|damp;/damp/|base;/that.html|", - ) - - b.AssertFileContent("public/blog/html-amp/index.html", - "Output Formats: 2;html;/blog/html-amp/|amp;/amp/blog/html-amp/|", - "This RelPermalink: /blog/html-amp/") - - b.AssertFileContent("public/amp/blog/html-amp/index.html", - "Output Formats: 2;html;/blog/html-amp/|amp;/amp/blog/html-amp/|", - "This RelPermalink: /amp/blog/html-amp/") - - // Damp is not permalinkable - b.AssertFileContent("public/damp/blog/html-damp/index.html", - "This RelPermalink: /blog/html-damp/", - "Output Formats: 2;html;/blog/html-damp/|damp;/damp/blog/html-damp/|") - - b.AssertFileContent("public/blog/html-ramp/index.html", - "This RelPermalink: /blog/html-ramp/", - "Output Formats: 2;html;/blog/html-ramp/|ramp;/ramp/blog/html-ramp/|") - - b.AssertFileContent("public/ramp/blog/html-ramp/index.html", - "This RelPermalink: /ramp/blog/html-ramp/", - "Output Formats: 2;html;/blog/html-ramp/|ramp;/ramp/blog/html-ramp/|") - - // https://github.com/gohugoio/hugo/issues/5877 - outputFormats := "Output Formats: 3;html;/blog/html-base-nobase/|base;/blog/html-base-nobase/that.html|nobase;/blog/html-base-nobase/index.json|" - - b.AssertFileContent("public/blog/html-base-nobase/index.json", - "This RelPermalink: /blog/html-base-nobase/index.json", - outputFormats, - ) - - b.AssertFileContent("public/blog/html-base-nobase/that.html", - "This RelPermalink: /blog/html-base-nobase/that.html", - outputFormats, - ) - - b.AssertFileContent("public/blog/html-base-nobase/index.html", - "This RelPermalink: /blog/html-base-nobase/", - outputFormats, - ) -} - -func TestSiteWithPageNoOutputs(t *testing.T) { - t.Parallel() - - b := newTestSitesBuilder(t) - b.WithConfigFile("toml", ` -baseURL = "https://example.com" - -[outputFormats.o1] -mediaType = "text/html" - - - -`) - b.WithContent("outputs-empty.md", `--- -title: "Empty Outputs" -outputs: [] ---- - -Word1. Word2. - -`, - "outputs-string.md", `--- -title: "Outputs String" -outputs: "o1" ---- - -Word1. Word2. - -`) - - b.WithTemplates("index.html", ` -{{ range .Site.RegularPages }} -WordCount: {{ .WordCount }} -{{ end }} -`) - - b.WithTemplates("_default/single.html", `HTML: {{ .Content }}`) - b.WithTemplates("_default/single.o1.html", `O1: {{ .Content }}`) - - b.Build(BuildCfg{}) - - b.AssertFileContent( - "public/index.html", - " WordCount: 2") - - b.AssertFileContent("public/outputs-empty/index.html", "HTML:", "Word1. Word2.") - b.AssertFileContent("public/outputs-string/index.html", "O1:", "Word1. Word2.") -} - -func TestOuputFormatFrontMatterTermIssue12275(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ['home','page','rss','section','sitemap','taxonomy'] --- content/p1.md -- ---- -title: p1 -tags: - - tag-a - - tag-b ---- --- content/tags/tag-a/_index.md -- ---- -title: tag-a -outputs: - - html - - json ---- --- content/tags/tag-b/_index.md -- ---- -title: tag-b ---- --- layouts/_default/term.html -- -{{ .Title }} --- layouts/_default/term.json -- -{{ jsonify (dict "title" .Title) }} -` - - b := Test(t, files) - - b.AssertFileContent("public/tags/tag-a/index.html", "tag-a") - b.AssertFileContent("public/tags/tag-b/index.html", "tag-b") - b.AssertFileContent("public/tags/tag-a/index.json", `{"title":"tag-a"}`) // failing test + require.Equal(t, "/blog/_redirects", outputs.Get("DOTLESS").RelPermalink()) + require.Equal(t, "/blog/defaultdelimbase.defd", outputs.Get("DEF").RelPermalink()) + require.Equal(t, "/blog/nosuffixbase.", outputs.Get("NOS").RelPermalink()) + require.Equal(t, "/blog/customdelimbase_del", outputs.Get("CUS").RelPermalink()) + } diff --git a/hugolib/site_render.go b/hugolib/site_render.go index 6dbb19827..42433a70a 100644 --- a/hugolib/site_render.go +++ b/hugolib/site_render.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Hugo Authors. All rights reserved. +// Copyright 2016 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,96 +14,38 @@ package hugolib import ( - "context" "fmt" "path" - "strings" "sync" - "github.com/bep/logg" - "github.com/gohugoio/hugo/common/herrors" - "github.com/gohugoio/hugo/hugolib/doctree" - "github.com/gohugoio/hugo/tpl/tplimpl" + "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/output" - "github.com/gohugoio/hugo/resources/kinds" - "github.com/gohugoio/hugo/resources/page" + bp "github.com/gohugoio/hugo/bufferpool" ) -type siteRenderContext struct { - cfg *BuildCfg - - infol logg.LevelLogger - - // languageIdx is the zero based index of the site. - languageIdx int - - // Zero based index for all output formats combined. - sitesOutIdx int - - // Zero based index of the output formats configured within a Site. - // Note that these outputs are sorted. - outIdx int - - multihost bool -} - -// Whether to render 404.html, robotsTXT.txt and similar. -// These are usually rendered once in the root of public. -func (s siteRenderContext) shouldRenderStandalonePage(kind string) bool { - if s.multihost || kind == kinds.KindSitemap { - // 1 per site - return s.outIdx == 0 - } - - if kind == kinds.KindTemporary || kind == kinds.KindStatus404 { - // 1 for all output formats - return s.outIdx == 0 - } - - // 1 for all sites and output formats. - return s.languageIdx == 0 && s.outIdx == 0 -} - -// renderPages renders pages concurrently. -func (s *Site) renderPages(ctx *siteRenderContext) error { - numWorkers := config.GetNumWorkerMultiplier() +// renderPages renders pages each corresponding to a markdown file. +// TODO(bep np doc +func (s *Site) renderPages() error { results := make(chan error) - pages := make(chan *pageState, numWorkers) // buffered for performance + pages := make(chan *Page) errs := make(chan error) - go s.errorCollator(results, errs) + go errorCollator(results, errs) + + numWorkers := getGoMaxProcs() * 4 wg := &sync.WaitGroup{} - for range numWorkers { + for i := 0; i < numWorkers; i++ { wg.Add(1) - go pageRenderer(ctx, s, pages, results, wg) + go pageRenderer(s, pages, results, wg) } - cfg := ctx.cfg - - w := &doctree.NodeShiftTreeWalker[contentNodeI]{ - Tree: s.pageMap.treePages, - Handle: func(key string, n contentNodeI, match doctree.DimensionFlag) (bool, error) { - if p, ok := n.(*pageState); ok { - if cfg.shouldRender(ctx.infol, p) { - select { - case <-s.h.Done(): - return true, nil - default: - pages <- p - } - } - } - return false, nil - }, - } - - if err := w.Walk(context.Background()); err != nil { - return err + for _, page := range s.Pages { + pages <- page } close(pages) @@ -114,250 +56,344 @@ func (s *Site) renderPages(ctx *siteRenderContext) error { err := <-errs if err != nil { - return fmt.Errorf("failed to render pages: %w", herrors.ImproveRenderErr(err)) + return fmt.Errorf("Error(s) rendering pages: %s", err) } return nil } -func pageRenderer( - ctx *siteRenderContext, - s *Site, - pages <-chan *pageState, - results chan<- error, - wg *sync.WaitGroup, -) { +func pageRenderer(s *Site, pages <-chan *Page, results chan<- error, wg *sync.WaitGroup) { defer wg.Done() - for p := range pages { - if p.m.isStandalone() && !ctx.shouldRenderStandalonePage(p.Kind()) { - continue - } + for page := range pages { - if p.m.pageConfig.Build.PublishResources { - if err := p.renderResources(); err != nil { - s.SendError(p.errorf(err, "failed to render page resources")) + for i, outFormat := range page.outputFormats { + + var ( + pageOutput *PageOutput + err error + ) + + if i == 0 { + pageOutput, err = newPageOutput(page, false, outFormat) + page.mainPageOutput = pageOutput + } + + if outFormat != page.s.rc.Format { + // Will be rendered ... later. continue } - } - if !p.render { - // Nothing more to do for this page. - continue - } - - templ, found, err := p.resolveTemplate() - if err != nil { - s.SendError(p.errorf(err, "failed to resolve template")) - continue - } - - if !found { - s.Log.Trace( - func() string { - return fmt.Sprintf("no layout for kind %q found", p.Kind()) - }, - ) - // Don't emit warning for missing 404 etc. pages. - if !p.m.isStandalone() { - s.logMissingLayout("", p.Layout(), p.Kind(), p.f.Name) + if pageOutput == nil { + pageOutput, err = page.mainPageOutput.copyWithFormat(outFormat) } - continue - } - targetPath := p.targetPaths().TargetFilename - - s.Log.Trace( - func() string { - return fmt.Sprintf("rendering outputFormat %q kind %q using layout %q to %q", p.pageOutput.f.Name, p.Kind(), templ.Name(), targetPath) - }, - ) - - var d any = p - switch p.Kind() { - case kinds.KindSitemapIndex: - d = s.h.Sites - } - - if err := s.renderAndWritePage(&s.PathSpec.ProcessingStats.Pages, "page "+p.Title(), targetPath, p, d, templ); err != nil { - results <- err - } - - if p.paginator != nil && p.paginator.current != nil { - if err := s.renderPaginator(p, templ); err != nil { - results <- err + if err != nil { + s.Log.ERROR.Printf("Failed to create output page for type %q for page %q: %s", outFormat.Name, page, err) + continue } + + var layouts []string + + if page.selfLayout != "" { + layouts = []string{page.selfLayout} + } else { + layouts, err = s.layouts(pageOutput) + if err != nil { + s.Log.ERROR.Printf("Failed to resolve layout output %q for page %q: %s", outFormat.Name, page, err) + continue + } + } + + switch pageOutput.outputFormat.Name { + + case "RSS": + if err := s.renderRSS(pageOutput); err != nil { + results <- err + } + default: + targetPath, err := pageOutput.targetPath() + if err != nil { + s.Log.ERROR.Printf("Failed to create target path for output %q for page %q: %s", outFormat.Name, page, err) + continue + } + + s.Log.DEBUG.Printf("Render %s to %q with layouts %q", pageOutput.Kind, targetPath, layouts) + + if err := s.renderAndWritePage("page "+pageOutput.FullFilePath(), targetPath, pageOutput, layouts...); err != nil { + results <- err + } + + if pageOutput.IsNode() { + if err := s.renderPaginator(pageOutput); err != nil { + results <- err + } + } + } + } } } -func (s *Site) logMissingLayout(name, layout, kind, outputFormat string) { - log := s.Log.Warn() - if name != "" && infoOnMissingLayout[name] { - log = s.Log.Info() - } - - errMsg := "You should create a template file which matches Hugo Layouts Lookup Rules for this combination." - var args []any - msg := "found no layout file for" - if outputFormat != "" { - msg += " %q" - args = append(args, outputFormat) - } - - if layout != "" { - msg += " for layout %q" - args = append(args, layout) - } - - if kind != "" { - msg += " for kind %q" - args = append(args, kind) - } - - if name != "" { - msg += " for %q" - args = append(args, name) - } - - msg += ": " + errMsg - - log.Logf(msg, args...) -} - // renderPaginator must be run after the owning Page has been rendered. -func (s *Site) renderPaginator(p *pageState, templ *tplimpl.TemplInfo) error { - paginatePath := s.Conf.Pagination().Path +func (s *Site) renderPaginator(p *PageOutput) error { + if p.paginator != nil { + s.Log.DEBUG.Printf("Render paginator for page %q", p.Path()) + paginatePath := s.Cfg.GetString("paginatePath") - d := p.targetPathDescriptor - f := p.outputFormat() - d.Type = f - - if p.paginator.current == nil || p.paginator.current != p.paginator.current.First() { - panic(fmt.Sprintf("invalid paginator state for %q", p.pathOrTitle())) - } - - if f.IsHTML && !s.Conf.Pagination().DisableAliases { - // Write alias for page 1 - d.Addends = fmt.Sprintf("/%s/%d", paginatePath, 1) - targetPaths := page.CreateTargetPaths(d) - - if err := s.writeDestAlias(targetPaths.TargetFilename, p.Permalink(), f, p); err != nil { + // write alias for page 1 + addend := fmt.Sprintf("/%s/%d", paginatePath, 1) + target, err := p.createTargetPath(p.outputFormat, addend) + if err != nil { return err } - } - - // Render pages for the rest - for current := p.paginator.current.Next(); current != nil; current = current.Next() { - - p.paginator.current = current - d.Addends = fmt.Sprintf("/%s/%d", paginatePath, current.PageNumber()) - targetPaths := page.CreateTargetPaths(d) - - if err := s.renderAndWritePage( - &s.PathSpec.ProcessingStats.PaginatorPages, - p.Title(), - targetPaths.TargetFilename, p, p, templ); err != nil { + // TODO(bep) do better + link := newOutputFormat(p.Page, p.outputFormat).Permalink() + if err := s.writeDestAlias(target, link, nil); err != nil { return err } - } + pagers := p.paginator.Pagers() + for i, pager := range pagers { + if i == 0 { + // already created + continue + } + + pagerNode, err := p.copy() + if err != nil { + return err + } + + pagerNode.origOnCopy = p.Page + + pagerNode.paginator = pager + if pager.TotalPages() > 0 { + first, _ := pager.page(0) + pagerNode.Date = first.Date + pagerNode.Lastmod = first.Lastmod + } + + pageNumber := i + 1 + addend := fmt.Sprintf("/%s/%d", paginatePath, pageNumber) + targetPath, _ := p.targetPath(addend) + layouts, err := p.layouts() + + if err != nil { + return err + } + + if err := s.renderAndWritePage( + pagerNode.Title, + targetPath, pagerNode, layouts...); err != nil { + return err + } + + } + } return nil } +func (s *Site) renderRSS(p *PageOutput) error { + + if !s.isEnabled(kindRSS) { + return nil + } + + if s.Cfg.GetBool("disableRSS") { + return nil + } + + p.Kind = kindRSS + + limit := s.Cfg.GetInt("rssLimit") + if limit >= 0 && len(p.Pages) > limit { + p.Pages = p.Pages[:limit] + p.Data["Pages"] = p.Pages + } + + layouts, err := s.layoutHandler.For( + p.layoutDescriptor, + "", + p.outputFormat) + if err != nil { + return err + } + + targetPath, err := p.targetPath() + if err != nil { + return err + } + + return s.renderAndWriteXML(p.Title, + targetPath, p, layouts...) +} + +func (s *Site) render404() error { + if !s.isEnabled(kind404) { + return nil + } + + if s.Cfg.GetBool("disable404") { + return nil + } + + p := s.newNodePage(kind404) + + p.Title = "404 Page not found" + p.Data["Pages"] = s.Pages + p.Pages = s.Pages + p.URLPath.URL = "404.html" + + if err := p.initTargetPathDescriptor(); err != nil { + return err + } + + nfLayouts := []string{"404.html"} + + htmlOut := output.HTMLFormat + htmlOut.BaseName = "404" + + pageOutput, err := newPageOutput(p, false, htmlOut) + if err != nil { + return err + } + + targetPath, err := pageOutput.targetPath() + if err != nil { + s.Log.ERROR.Printf("Failed to create target path for page %q: %s", p, err) + } + + return s.renderAndWritePage("404 page", targetPath, pageOutput, s.appendThemeTemplates(nfLayouts)...) +} + +func (s *Site) renderSitemap() error { + if !s.isEnabled(kindSitemap) { + return nil + } + + if s.Cfg.GetBool("disableSitemap") { + return nil + } + + sitemapDefault := parseSitemap(s.Cfg.GetStringMap("sitemap")) + + n := s.newNodePage(kindSitemap) + + // Include all pages (regular, home page, taxonomies etc.) + pages := s.Pages + + page := s.newNodePage(kindSitemap) + page.URLPath.URL = "" + if err := page.initTargetPathDescriptor(); err != nil { + return err + } + page.Sitemap.ChangeFreq = sitemapDefault.ChangeFreq + page.Sitemap.Priority = sitemapDefault.Priority + page.Sitemap.Filename = sitemapDefault.Filename + + n.Data["Pages"] = pages + n.Pages = pages + + // TODO(bep) we have several of these + if err := page.initTargetPathDescriptor(); err != nil { + return err + } + + // TODO(bep) this should be done somewhere else + for _, page := range pages { + if page.Sitemap.ChangeFreq == "" { + page.Sitemap.ChangeFreq = sitemapDefault.ChangeFreq + } + + if page.Sitemap.Priority == -1 { + page.Sitemap.Priority = sitemapDefault.Priority + } + + if page.Sitemap.Filename == "" { + page.Sitemap.Filename = sitemapDefault.Filename + } + } + + smLayouts := []string{"sitemap.xml", "_default/sitemap.xml", "_internal/_default/sitemap.xml"} + addLanguagePrefix := n.Site.IsMultiLingual() + + return s.renderAndWriteXML("sitemap", + n.addLangPathPrefixIfFlagSet(page.Sitemap.Filename, addLanguagePrefix), n, s.appendThemeTemplates(smLayouts)...) +} + +func (s *Site) renderRobotsTXT() error { + if !s.isEnabled(kindRobotsTXT) { + return nil + } + + if !s.Cfg.GetBool("enableRobotsTXT") { + return nil + } + + n := s.newNodePage(kindRobotsTXT) + if err := n.initTargetPathDescriptor(); err != nil { + return err + } + n.Data["Pages"] = s.Pages + n.Pages = s.Pages + + rLayouts := []string{"robots.txt", "_default/robots.txt", "_internal/_default/robots.txt"} + outBuffer := bp.GetBuffer() + defer bp.PutBuffer(outBuffer) + if err := s.renderForLayouts("robots", n, outBuffer, s.appendThemeTemplates(rLayouts)...); err != nil { + helpers.DistinctWarnLog.Println(err) + return nil + } + + if outBuffer.Len() == 0 { + return nil + } + + return s.publish("robots.txt", outBuffer) +} + // renderAliases renders shell pages that simply have a redirect in the header. func (s *Site) renderAliases() error { - w := &doctree.NodeShiftTreeWalker[contentNodeI]{ - Tree: s.pageMap.treePages, - Handle: func(key string, n contentNodeI, match doctree.DimensionFlag) (bool, error) { - p := n.(*pageState) + for _, p := range s.Pages { + if len(p.Aliases) == 0 { + continue + } - // We cannot alias a page that's not rendered. - if p.m.noLink() || p.skipRender() { - return false, nil + for _, f := range p.outputFormats { + if !f.IsHTML { + continue } - if len(p.Aliases()) == 0 { - return false, nil - } + o := newOutputFormat(p, f) + plink := o.Permalink() - pathSeen := make(map[string]bool) - for _, of := range p.OutputFormats() { - if !of.Format.IsHTML { - continue + for _, a := range p.Aliases { + if f.Path != "" { + // Make sure AMP and similar doesn't clash with regular aliases. + a = path.Join(a, f.Path) } - f := of.Format - - if pathSeen[f.Path] { - continue - } - pathSeen[f.Path] = true - - plink := of.Permalink() - - for _, a := range p.Aliases() { - isRelative := !strings.HasPrefix(a, "/") - - if isRelative { - // Make alias relative, where "." will be on the - // same directory level as the current page. - basePath := path.Join(p.targetPaths().SubResourceBaseLink, "..") - a = path.Join(basePath, a) - - } else { - // Make sure AMP and similar doesn't clash with regular aliases. - a = path.Join(f.Path, a) - } - - if s.conf.C.IsUglyURLSection(p.Section()) && !strings.HasSuffix(a, ".html") { - a += ".html" - } - - lang := p.Language().Lang - - if s.h.Configs.IsMultihost && !strings.HasPrefix(a, "/"+lang) { - // These need to be in its language root. - a = path.Join(lang, a) - } - - err := s.writeDestAlias(a, plink, f, p) - if err != nil { - return true, err - } + if err := s.writeDestAlias(a, plink, p); err != nil { + return err } } - return false, nil - }, - } - return w.Walk(context.TODO()) -} - -// renderMainLanguageRedirect creates a redirect to the main language home, -// depending on if it lives in sub folder (e.g. /en) or not. -func (s *Site) renderMainLanguageRedirect() error { - if s.conf.DisableDefaultLanguageRedirect { - return nil - } - if s.h.Conf.IsMultihost() || !(s.h.Conf.DefaultContentLanguageInSubdir() || s.h.Conf.IsMultilingual()) { - // No need for a redirect - return nil + } } - html, found := s.conf.OutputFormats.Config.GetByName("html") - if found { - mainLang := s.conf.DefaultContentLanguage - if s.conf.DefaultContentLanguageInSubdir { - mainLangURL := s.PathSpec.AbsURL(mainLang+"/", false) - s.Log.Debugf("Write redirect to main language %s: %s", mainLang, mainLangURL) - if err := s.publishDestAlias(true, "/", mainLangURL, html, nil); err != nil { + if s.owner.multilingual.enabled() { + mainLang := s.owner.multilingual.DefaultLang + if s.Info.defaultContentLanguageInSubdir { + mainLangURL := s.PathSpec.AbsURL(mainLang.Lang, false) + s.Log.DEBUG.Printf("Write redirect to main language %s: %s", mainLang, mainLangURL) + if err := s.publishDestAlias(true, "/", mainLangURL, nil); err != nil { return err } } else { mainLangURL := s.PathSpec.AbsURL("", false) - s.Log.Debugf("Write redirect to main language %s: %s", mainLang, mainLangURL) - if err := s.publishDestAlias(true, mainLang, mainLangURL, html, nil); err != nil { + s.Log.DEBUG.Printf("Write redirect to main language %s: %s", mainLang, mainLangURL) + if err := s.publishDestAlias(true, mainLang.Lang, mainLangURL, nil); err != nil { return err } } diff --git a/hugolib/site_sections.go b/hugolib/site_sections.go index 385f3f291..4f4a217d5 100644 --- a/hugolib/site_sections.go +++ b/hugolib/site_sections.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Hugo Authors. All rights reserved. +// Copyright 2017 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,17 +14,316 @@ package hugolib import ( - "github.com/gohugoio/hugo/resources/page" + "fmt" + "path" + "strconv" + "strings" + + "github.com/gohugoio/hugo/helpers" + + radix "github.com/hashicorp/go-immutable-radix" ) // Sections returns the top level sections. -func (s *Site) Sections() page.Pages { - s.CheckReady() - return s.Home().Sections() +func (s *SiteInfo) Sections() Pages { + home, err := s.Home() + if err == nil { + return home.Sections() + } + return nil } // Home is a shortcut to the home page, equivalent to .Site.GetPage "home". -func (s *Site) Home() page.Page { - s.CheckReady() - return s.s.home +func (s *SiteInfo) Home() (*Page, error) { + return s.GetPage(KindHome) +} + +// Parent returns a section's parent section or a page's section. +// To get a section's subsections, see Page's Sections method. +func (p *Page) Parent() *Page { + return p.parent +} + +// CurrentSection returns the page's current section or the page itself if home or a section. +// Note that this will return nil for pages that is not regular, home or section pages. +func (p *Page) CurrentSection() *Page { + v := p + if v.origOnCopy != nil { + v = v.origOnCopy + } + if v.IsHome() || v.IsSection() { + return v + } + + return v.parent +} + +// InSection returns whether the given page is in the current section. +// Note that this will always return false for pages that are +// not either regular, home or section pages. +func (p *Page) InSection(other interface{}) (bool, error) { + if p == nil || other == nil { + return false, nil + } + + pp, err := unwrapPage(other) + if err != nil { + return false, err + } + + if pp == nil { + return false, nil + } + + return pp.CurrentSection() == p.CurrentSection(), nil +} + +// IsDescendant returns whether the current page is a descendant of the given page. +// Note that this method is not relevant for taxonomy lists and taxonomy terms pages. +func (p *Page) IsDescendant(other interface{}) (bool, error) { + pp, err := unwrapPage(other) + if err != nil { + return false, err + } + + if pp.Kind == KindPage && len(p.sections) == len(pp.sections) { + // A regular page is never its section's descendant. + return false, nil + } + return helpers.HasStringsPrefix(p.sections, pp.sections), nil +} + +// IsAncestor returns whether the current page is an ancestor of the given page. +// Note that this method is not relevant for taxonomy lists and taxonomy terms pages. +func (p *Page) IsAncestor(other interface{}) (bool, error) { + pp, err := unwrapPage(other) + if err != nil { + return false, err + } + + if p.Kind == KindPage && len(p.sections) == len(pp.sections) { + // A regular page is never its section's ancestor. + return false, nil + } + + return helpers.HasStringsPrefix(pp.sections, p.sections), nil +} + +func unwrapPage(in interface{}) (*Page, error) { + if po, ok := in.(*PageOutput); ok { + in = po.Page + } + + pp, ok := in.(*Page) + if !ok { + return nil, fmt.Errorf("%T not supported", in) + } + return pp, nil +} + +// Sections returns this section's subsections, if any. +// Note that for non-sections, this method will always return an empty list. +func (p *Page) Sections() Pages { + return p.subSections +} + +func (s *Site) assembleSections() Pages { + var newPages Pages + + if !s.isEnabled(KindSection) { + return newPages + } + + // Maps section kind pages to their path, i.e. "my/section" + sectionPages := make(map[string]*Page) + + // The sections with content files will already have been created. + for _, sect := range s.findPagesByKind(KindSection) { + sectionPages[path.Join(sect.sections...)] = sect + } + + const ( + sectKey = "__hs" + sectSectKey = "_a" + sectKey + sectPageKey = "_b" + sectKey + ) + + var ( + home *Page + inPages = radix.New().Txn() + inSections = radix.New().Txn() + undecided Pages + ) + + for i, p := range s.Pages { + if p.Kind != KindPage { + if p.Kind == KindHome { + home = p + } + continue + } + + if len(p.sections) == 0 { + // Root level pages. These will have the home page as their Parent. + p.parent = home + continue + } + + sectionKey := path.Join(p.sections...) + sect, found := sectionPages[sectionKey] + + if !found && len(p.sections) == 1 { + // We only create content-file-less sections for the root sections. + sect = s.newSectionPage(p.sections[0]) + sectionPages[sectionKey] = sect + newPages = append(newPages, sect) + found = true + } + + if len(p.sections) > 1 { + // Create the root section if not found. + _, rootFound := sectionPages[p.sections[0]] + if !rootFound { + sect = s.newSectionPage(p.sections[0]) + sectionPages[p.sections[0]] = sect + newPages = append(newPages, sect) + } + } + + if found { + pagePath := path.Join(sectionKey, sectPageKey, strconv.Itoa(i)) + inPages.Insert([]byte(pagePath), p) + } else { + undecided = append(undecided, p) + } + } + + // Create any missing sections in the tree. + // A sub-section needs a content file, but to create a navigational tree, + // given a content file in /content/a/b/c/_index.md, we cannot create just + // the c section. + for _, sect := range sectionPages { + for i := len(sect.sections); i > 0; i-- { + sectionPath := sect.sections[:i] + sectionKey := path.Join(sectionPath...) + sect, found := sectionPages[sectionKey] + if !found { + sect = s.newSectionPage(sectionPath[len(sectionPath)-1]) + sect.sections = sectionPath + sectionPages[sectionKey] = sect + newPages = append(newPages, sect) + } + } + } + + for k, sect := range sectionPages { + inPages.Insert([]byte(path.Join(k, sectSectKey)), sect) + inSections.Insert([]byte(k), sect) + } + + var ( + currentSection *Page + children Pages + rootSections = inSections.Commit().Root() + ) + + for i, p := range undecided { + // Now we can decide where to put this page into the tree. + sectionKey := path.Join(p.sections...) + _, v, _ := rootSections.LongestPrefix([]byte(sectionKey)) + sect := v.(*Page) + pagePath := path.Join(path.Join(sect.sections...), sectSectKey, "u", strconv.Itoa(i)) + inPages.Insert([]byte(pagePath), p) + } + + var rootPages = inPages.Commit().Root() + + rootPages.Walk(func(path []byte, v interface{}) bool { + p := v.(*Page) + + if p.Kind == KindSection { + if currentSection != nil { + // A new section + currentSection.setPagePages(children) + } + + currentSection = p + children = make(Pages, 0) + + return false + + } + + // Regular page + p.parent = currentSection + children = append(children, p) + return false + }) + + if currentSection != nil { + currentSection.setPagePages(children) + } + + // Build the sections hierarchy + for _, sect := range sectionPages { + if len(sect.sections) == 1 { + sect.parent = home + } else { + parentSearchKey := path.Join(sect.sections[:len(sect.sections)-1]...) + _, v, _ := rootSections.LongestPrefix([]byte(parentSearchKey)) + p := v.(*Page) + sect.parent = p + } + + if sect.parent != nil { + sect.parent.subSections = append(sect.parent.subSections, sect) + } + } + + var ( + sectionsParamId = "mainSections" + sectionsParamIdLower = strings.ToLower(sectionsParamId) + mainSections interface{} + mainSectionsFound bool + maxSectionWeight int + ) + + mainSections, mainSectionsFound = s.Info.Params[sectionsParamIdLower] + + for _, sect := range sectionPages { + if sect.parent != nil { + sect.parent.subSections.Sort() + } + + for i, p := range sect.Pages { + if i > 0 { + p.NextInSection = sect.Pages[i-1] + } + if i < len(sect.Pages)-1 { + p.PrevInSection = sect.Pages[i+1] + } + } + + if !mainSectionsFound { + weight := len(sect.Pages) + (len(sect.Sections()) * 5) + if weight >= maxSectionWeight { + mainSections = []string{sect.Section()} + maxSectionWeight = weight + } + } + } + + // Try to make this as backwards compatible as possible. + s.Info.Params[sectionsParamId] = mainSections + s.Info.Params[sectionsParamIdLower] = mainSections + + return newPages + +} + +func (p *Page) setPagePages(pages Pages) { + pages.Sort() + p.Pages = pages + p.Data = make(map[string]interface{}) + p.Data["Pages"] = pages } diff --git a/hugolib/site_sections_test.go b/hugolib/site_sections_test.go index 0bf166092..441391197 100644 --- a/hugolib/site_sections_test.go +++ b/hugolib/site_sections_test.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Hugo Authors. All rights reserved. +// Copyright 2017-present 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,23 +19,21 @@ import ( "strings" "testing" - qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/deps" - "github.com/gohugoio/hugo/htesting" - "github.com/gohugoio/hugo/resources/kinds" - "github.com/gohugoio/hugo/resources/page" + "github.com/stretchr/testify/require" ) func TestNestedSections(t *testing.T) { + t.Parallel() + var ( - c = qt.New(t) + assert = require.New(t) cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} ) - tt := htesting.NewPinnedRunner(c, "") - cfg.Set("permalinks", map[string]string{ - "perm-a": ":sections/:title", + "perm a": ":sections/:title", }) pageTemplate := `--- @@ -106,318 +104,194 @@ Content writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), "Single|{{ .Title }}") writeSource(t, fs, filepath.Join("layouts", "_default", "list.html"), ` -{{ $sect := (.Site.GetPage "l1/l2") }} +{{ $sect := (.Site.GetPage "section" "l1" "l2") }} List|{{ .Title }}|L1/l2-IsActive: {{ .InSection $sect }} {{ range .Paginator.Pages }} PAG|{{ .Title }}|{{ $sect.InSection . }} {{ end }} -{{/* https://github.com/gohugoio/hugo/issues/4989 */}} -{{ $sections := (.Site.GetPage "section" .Section).Sections.ByWeight }} `) - cfg.Set("pagination.pagerSize", 2) + cfg.Set("paginate", 2) - th, configs := newTestHelperFromProvider(cfg, fs, t) - - s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{}) - - c.Assert(len(s.RegularPages()), qt.Equals, 21) + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + require.Len(t, s.RegularPages, 21) tests := []struct { sections string - verify func(c *qt.C, p page.Page) + verify func(p *Page) }{ - {"elsewhere", func(c *qt.C, p page.Page) { - c.Assert(len(p.Pages()), qt.Equals, 1) - for _, p := range p.Pages() { - c.Assert(p.SectionsPath(), qt.Equals, "/elsewhere") + {"elsewhere", func(p *Page) { + assert.Len(p.Pages, 1) + for _, p := range p.Pages { + assert.Equal([]string{"elsewhere"}, p.sections) } }}, - {"post", func(c *qt.C, p page.Page) { - c.Assert(len(p.Pages()), qt.Equals, 2) - for _, p := range p.Pages() { - c.Assert(p.Section(), qt.Equals, "post") + {"post", func(p *Page) { + assert.Len(p.Pages, 2) + for _, p := range p.Pages { + assert.Equal("post", p.Section()) } }}, - {"empty1", func(c *qt.C, p page.Page) { + {"empty1", func(p *Page) { // > b,c - c.Assert(getPage(p, "/empty1/b"), qt.IsNil) // No _index.md page. - c.Assert(getPage(p, "/empty1/b/c"), qt.Not(qt.IsNil)) + assert.NotNil(p.s.getPage(KindSection, "empty1", "b")) + assert.NotNil(p.s.getPage(KindSection, "empty1", "b", "c")) + }}, - {"empty2", func(c *qt.C, p page.Page) { - // > b,c,d where b and d have _index.md files. - b := getPage(p, "/empty2/b") - c.Assert(b, qt.Not(qt.IsNil)) - c.Assert(b.Title(), qt.Equals, "T40_-1") - - cp := getPage(p, "/empty2/b/c") - c.Assert(cp, qt.IsNil) // No _index.md - - d := getPage(p, "/empty2/b/c/d") - c.Assert(d, qt.Not(qt.IsNil)) - c.Assert(d.Title(), qt.Equals, "T41_-1") - - c.Assert(cp.Eq(d), qt.Equals, false) - c.Assert(cp.Eq(cp), qt.Equals, true) - c.Assert(cp.Eq("asdf"), qt.Equals, false) + {"empty2", func(p *Page) { + // > b,c,d where b and d have content files. + b := p.s.getPage(KindSection, "empty2", "b") + assert.NotNil(b) + assert.Equal("T40_-1", b.Title) + c := p.s.getPage(KindSection, "empty2", "b", "c") + assert.NotNil(c) + assert.Equal("Cs", c.Title) + d := p.s.getPage(KindSection, "empty2", "b", "c", "d") + assert.NotNil(d) + assert.Equal("T41_-1", d.Title) }}, - {"empty3", func(c *qt.C, p page.Page) { + {"empty3", func(p *Page) { // b,c,d with regular page in b - b := getPage(p, "/empty3/b") - c.Assert(b, qt.IsNil) // No _index.md - e3 := getPage(p, "/empty3/b/empty3") - c.Assert(e3, qt.Not(qt.IsNil)) - c.Assert(e3.File().LogicalName(), qt.Equals, "empty3.md") + b := p.s.getPage(KindSection, "empty3", "b") + assert.NotNil(b) + assert.Len(b.Pages, 1) + assert.Equal("empty3.md", b.Pages[0].File.LogicalName()) + }}, - {"empty3", func(c *qt.C, p page.Page) { - xxx := getPage(p, "/empty3/nil") - c.Assert(xxx, qt.IsNil) - }}, - {"top", func(c *qt.C, p page.Page) { - c.Assert(p.Title(), qt.Equals, "Tops") - c.Assert(len(p.Pages()), qt.Equals, 2) - c.Assert(p.Pages()[0].File().LogicalName(), qt.Equals, "mypage2.md") - c.Assert(p.Pages()[1].File().LogicalName(), qt.Equals, "mypage3.md") + {"top", func(p *Page) { + assert.Equal("Tops", p.Title) + assert.Len(p.Pages, 2) + assert.Equal("mypage2.md", p.Pages[0].LogicalName()) + assert.Equal("mypage3.md", p.Pages[1].LogicalName()) home := p.Parent() - c.Assert(home.IsHome(), qt.Equals, true) - c.Assert(len(p.Sections()), qt.Equals, 0) - c.Assert(home.CurrentSection(), qt.Equals, home) - active := home.InSection(home) - c.Assert(active, qt.Equals, true) - c.Assert(p.FirstSection(), qt.Equals, p) - c.Assert(len(p.Ancestors()), qt.Equals, 1) + assert.True(home.IsHome()) + assert.Len(p.Sections(), 0) + assert.Equal(home, home.CurrentSection()) + active, err := home.InSection(home) + assert.NoError(err) + assert.True(active) }}, - {"l1", func(c *qt.C, p page.Page) { - c.Assert(p.Title(), qt.Equals, "L1s") - c.Assert(len(p.Pages()), qt.Equals, 4) // 2 pages + 2 sections - c.Assert(p.Parent().IsHome(), qt.Equals, true) - c.Assert(len(p.Sections()), qt.Equals, 2) - c.Assert(len(p.Ancestors()), qt.Equals, 1) + {"l1", func(p *Page) { + assert.Equal("L1s", p.Title) + assert.Len(p.Pages, 2) + assert.True(p.Parent().IsHome()) + assert.Len(p.Sections(), 2) }}, - {"l1,l2", func(c *qt.C, p page.Page) { - c.Assert(p.Title(), qt.Equals, "T2_-1") - c.Assert(len(p.Pages()), qt.Equals, 4) // 3 pages + 1 section - c.Assert(p.Pages()[0].Parent(), qt.Equals, p) - c.Assert(p.Parent().Title(), qt.Equals, "L1s") - c.Assert(p.RelPermalink(), qt.Equals, "/l1/l2/") - c.Assert(len(p.Sections()), qt.Equals, 1) - c.Assert(len(p.Ancestors()), qt.Equals, 2) + {"l1,l2", func(p *Page) { + assert.Equal("T2_-1", p.Title) + assert.Len(p.Pages, 3) + assert.Equal(p, p.Pages[0].Parent()) + assert.Equal("L1s", p.Parent().Title) + assert.Equal("/l1/l2/", p.URLPath.URL) + assert.Equal("/l1/l2/", p.RelPermalink()) + assert.Len(p.Sections(), 1) - for _, child := range p.Pages() { - if child.IsSection() { - c.Assert(child.CurrentSection(), qt.Equals, child) - continue - } + for _, child := range p.Pages { + assert.Equal(p, child.CurrentSection()) + active, err := child.InSection(p) + assert.NoError(err) + assert.True(active) + active, err = p.InSection(child) + assert.NoError(err) + assert.True(active) + active, err = p.InSection(p.s.getPage(KindHome)) + assert.NoError(err) + assert.False(active) - c.Assert(child.CurrentSection(), qt.Equals, p) - active := child.InSection(p) + isAncestor, err := p.IsAncestor(child) + assert.NoError(err) + assert.True(isAncestor) + isAncestor, err = child.IsAncestor(p) + assert.NoError(err) + assert.False(isAncestor) - c.Assert(active, qt.Equals, true) - active = p.InSection(child) - c.Assert(active, qt.Equals, true) - active = p.InSection(getPage(p, "/")) - c.Assert(active, qt.Equals, false) - - isAncestor := p.IsAncestor(child) - c.Assert(isAncestor, qt.Equals, true) - isAncestor = child.IsAncestor(p) - c.Assert(isAncestor, qt.Equals, false) - - isDescendant := p.IsDescendant(child) - c.Assert(isDescendant, qt.Equals, false) - isDescendant = child.IsDescendant(p) - c.Assert(isDescendant, qt.Equals, true) + isDescendant, err := p.IsDescendant(child) + assert.NoError(err) + assert.False(isDescendant) + isDescendant, err = child.IsDescendant(p) + assert.NoError(err) + assert.True(isDescendant) } - c.Assert(p.Eq(p.CurrentSection()), qt.Equals, true) + assert.Equal(p, p.CurrentSection()) + }}, - {"l1,l2_2", func(c *qt.C, p page.Page) { - c.Assert(p.Title(), qt.Equals, "T22_-1") - c.Assert(len(p.Pages()), qt.Equals, 2) - c.Assert(p.Pages()[0].File().Path(), qt.Equals, filepath.FromSlash("l1/l2_2/page_2_2_1.md")) - c.Assert(p.Parent().Title(), qt.Equals, "L1s") - c.Assert(len(p.Sections()), qt.Equals, 0) - c.Assert(len(p.Ancestors()), qt.Equals, 2) + {"l1,l2_2", func(p *Page) { + assert.Equal("T22_-1", p.Title) + assert.Len(p.Pages, 2) + assert.Equal(filepath.FromSlash("l1/l2_2/page_2_2_1.md"), p.Pages[0].Path()) + assert.Equal("L1s", p.Parent().Title) + assert.Len(p.Sections(), 0) }}, - {"l1,l2,l3", func(c *qt.C, p page.Page) { - nilp, _ := p.GetPage("this/does/not/exist") + {"l1,l2,l3", func(p *Page) { + assert.Equal("T3_-1", p.Title) + assert.Len(p.Pages, 2) + assert.Equal("T2_-1", p.Parent().Title) + assert.Len(p.Sections(), 0) - c.Assert(p.Title(), qt.Equals, "T3_-1") - c.Assert(len(p.Pages()), qt.Equals, 2) - c.Assert(p.Parent().Title(), qt.Equals, "T2_-1") - c.Assert(len(p.Sections()), qt.Equals, 0) - c.Assert(len(p.Ancestors()), qt.Equals, 3) + l1 := p.s.getPage(KindSection, "l1") + isDescendant, err := l1.IsDescendant(p) + assert.NoError(err) + assert.False(isDescendant) + isDescendant, err = p.IsDescendant(l1) + assert.NoError(err) + assert.True(isDescendant) - l1 := getPage(p, "/l1") - isDescendant := l1.IsDescendant(p) - c.Assert(isDescendant, qt.Equals, false) - isDescendant = l1.IsDescendant(nil) - c.Assert(isDescendant, qt.Equals, false) - isDescendant = nilp.IsDescendant(p) - c.Assert(isDescendant, qt.Equals, false) - isDescendant = p.IsDescendant(l1) - c.Assert(isDescendant, qt.Equals, true) + isAncestor, err := l1.IsAncestor(p) + assert.NoError(err) + assert.True(isAncestor) + isAncestor, err = p.IsAncestor(l1) + assert.NoError(err) + assert.False(isAncestor) - isAncestor := l1.IsAncestor(p) - c.Assert(isAncestor, qt.Equals, true) - isAncestor = p.IsAncestor(l1) - c.Assert(isAncestor, qt.Equals, false) - c.Assert(p.FirstSection(), qt.Equals, l1) - isAncestor = p.IsAncestor(nil) - c.Assert(isAncestor, qt.Equals, false) - c.Assert(isAncestor, qt.Equals, false) - - l3 := getPage(p, "/l1/l2/l3") - c.Assert(l3.FirstSection(), qt.Equals, l1) }}, - {"perm a,link", func(c *qt.C, p page.Page) { - c.Assert(p.Title(), qt.Equals, "T9_-1") - c.Assert(p.RelPermalink(), qt.Equals, "/perm-a/link/") - c.Assert(len(p.Pages()), qt.Equals, 4) - first := p.Pages()[0] - c.Assert(first.RelPermalink(), qt.Equals, "/perm-a/link/t1_1/") + {"perm a,link", func(p *Page) { + assert.Equal("T9_-1", p.Title) + assert.Equal("/perm-a/link/", p.RelPermalink()) + assert.Len(p.Pages, 4) + first := p.Pages[0] + assert.Equal("/perm-a/link/t1_1/", first.RelPermalink()) th.assertFileContent("public/perm-a/link/t1_1/index.html", "Single|T1_1") - last := p.Pages()[3] - c.Assert(last.RelPermalink(), qt.Equals, "/perm-a/link/t1_5/") + last := p.Pages[3] + assert.Equal("/perm-a/link/t1_5/", last.RelPermalink()) + }}, } - home := s.getPageOldVersion(kinds.KindHome) - for _, test := range tests { - test := test - tt.Run(fmt.Sprintf("sections %s", test.sections), func(c *qt.C) { - c.Parallel() - sections := strings.Split(test.sections, ",") - p := s.getPageOldVersion(kinds.KindSection, sections...) - c.Assert(p, qt.Not(qt.IsNil), qt.Commentf(fmt.Sprint(sections))) + sections := strings.Split(test.sections, ",") + p := s.getPage(KindSection, sections...) + assert.NotNil(p, fmt.Sprint(sections)) - if p.Pages() != nil { - c.Assert(p.Data().(page.Data).Pages(), deepEqualsPages, p.Pages()) - } - c.Assert(p.Parent(), qt.Not(qt.IsNil)) - test.verify(c, p) - }) + if p.Pages != nil { + assert.Equal(p.Pages, p.Data["Pages"]) + } + assert.NotNil(p.Parent(), fmt.Sprintf("Parent nil: %q", test.sections)) + test.verify(p) } - c.Assert(home, qt.Not(qt.IsNil)) - c.Assert(len(home.Ancestors()), qt.Equals, 0) + home := s.getPage(KindHome) - c.Assert(len(home.Sections()), qt.Equals, 9) - c.Assert(s.Sections(), deepEqualsPages, home.Sections()) + assert.NotNil(home) - rootPage := s.getPageOldVersion(kinds.KindPage, "mypage.md") - c.Assert(rootPage, qt.Not(qt.IsNil)) - c.Assert(rootPage.Parent().IsHome(), qt.Equals, true) - // https://github.com/gohugoio/hugo/issues/6365 - c.Assert(rootPage.Sections(), qt.HasLen, 0) + assert.Len(home.Sections(), 9) + assert.Equal(home.Sections(), s.Info.Sections()) - sectionWithSpace := s.getPageOldVersion(kinds.KindSection, "Spaces in Section") - // s.h.pageTrees.debugPrint() - c.Assert(sectionWithSpace, qt.Not(qt.IsNil)) - c.Assert(sectionWithSpace.RelPermalink(), qt.Equals, "/spaces-in-section/") + rootPage := s.getPage(KindPage, "mypage.md") + assert.NotNil(rootPage) + assert.True(rootPage.Parent().IsHome()) + + // Add a odd test for this as this looks a little bit off, but I'm not in the mood + // to think too hard a out this right now. It works, but people will have to spell + // out the directory name as is. + // If we later decide to do something about this, we will have to do some normalization in + // getPage. + // TODO(bep) + sectionWithSpace := s.getPage(KindSection, "Spaces in Section") + require.NotNil(t, sectionWithSpace) + require.Equal(t, "/spaces-in-section/", sectionWithSpace.RelPermalink()) th.assertFileContent("public/l1/l2/page/2/index.html", "L1/l2-IsActive: true", "PAG|T2_3|true") -} - -func TestNextInSectionNested(t *testing.T) { - t.Parallel() - - pageContent := `--- -title: "The Page" -weight: %d ---- -Some content. -` - createPageContent := func(weight int) string { - return fmt.Sprintf(pageContent, weight) - } - - b := newTestSitesBuilder(t) - b.WithSimpleConfigFile() - b.WithTemplates("_default/single.html", ` -Prev: {{ with .PrevInSection }}{{ .RelPermalink }}{{ end }}| -Next: {{ with .NextInSection }}{{ .RelPermalink }}{{ end }}| -`) - - b.WithContent("blog/page1.md", createPageContent(1)) - b.WithContent("blog/page2.md", createPageContent(2)) - b.WithContent("blog/cool/_index.md", createPageContent(1)) - b.WithContent("blog/cool/cool1.md", createPageContent(1)) - b.WithContent("blog/cool/cool2.md", createPageContent(2)) - b.WithContent("root1.md", createPageContent(1)) - b.WithContent("root2.md", createPageContent(2)) - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/root1/index.html", - "Prev: /root2/|", "Next: |") - b.AssertFileContent("public/root2/index.html", - "Prev: |", "Next: /root1/|") - b.AssertFileContent("public/blog/page1/index.html", - "Prev: /blog/page2/|", "Next: |") - b.AssertFileContent("public/blog/page2/index.html", - "Prev: |", "Next: /blog/page1/|") - b.AssertFileContent("public/blog/cool/cool1/index.html", - "Prev: /blog/cool/cool2/|", "Next: |") - b.AssertFileContent("public/blog/cool/cool2/index.html", - "Prev: |", "Next: /blog/cool/cool1/|") -} - -func TestSectionEntries(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.com/" --- content/myfirstsection/p1.md -- ---- -title: "P1" ---- -P1 --- content/a/b/c/_index.md -- ---- -title: "C" ---- -C --- content/a/b/c/mybundle/index.md -- ---- -title: "My Bundle" ---- --- layouts/_default/list.html -- -Kind: {{ .Kind }}|RelPermalink: {{ .RelPermalink }}|SectionsPath: {{ .SectionsPath }}|SectionsEntries: {{ .SectionsEntries }}|Len: {{ len .SectionsEntries }}| --- layouts/_default/single.html -- -Kind: {{ .Kind }}|RelPermalink: {{ .RelPermalink }}|SectionsPath: {{ .SectionsPath }}|SectionsEntries: {{ .SectionsEntries }}|Len: {{ len .SectionsEntries }}| -` - - b := Test(t, files) - - b.AssertFileContent("public/myfirstsection/p1/index.html", "RelPermalink: /myfirstsection/p1/|SectionsPath: /myfirstsection|SectionsEntries: [myfirstsection]|Len: 1") - b.AssertFileContent("public/a/b/c/index.html", "RelPermalink: /a/b/c/|SectionsPath: /a/b/c|SectionsEntries: [a b c]|Len: 3") - b.AssertFileContent("public/a/b/c/mybundle/index.html", "Kind: page|RelPermalink: /a/b/c/mybundle/|SectionsPath: /a/b/c|SectionsEntries: [a b c]|Len: 3") - b.AssertFileContent("public/index.html", "Kind: home|RelPermalink: /|SectionsPath: /|SectionsEntries: []|Len: 0") -} - -func TestParentWithPageOverlap(t *testing.T) { - files := ` --- hugo.toml -- -baseURL = "https://example.com/" --- content/docs/_index.md -- --- content/docs/logs/_index.md -- --- content/docs/logs/sdk.md -- --- content/docs/logs/sdk_exporters/stdout.md -- --- layouts/_default/list.html -- -{{ .RelPermalink }}|{{ with .Parent}}{{ .RelPermalink }}{{ end }}| --- layouts/_default/single.html -- -{{ .RelPermalink }}|{{ with .Parent}}{{ .RelPermalink }}{{ end }}| - -` - b := Test(t, files) - - b.AssertFileContent("public/index.html", "/||") - b.AssertFileContent("public/docs/index.html", "/docs/|/|") - b.AssertFileContent("public/docs/logs/index.html", "/docs/logs/|/docs/|") - b.AssertFileContent("public/docs/logs/sdk/index.html", "/docs/logs/sdk/|/docs/logs/|") - b.AssertFileContent("public/docs/logs/sdk_exporters/stdout/index.html", "/docs/logs/sdk_exporters/stdout/|/docs/logs/|") + } diff --git a/hugolib/site_stats_test.go b/hugolib/site_stats_test.go deleted file mode 100644 index c045963f3..000000000 --- a/hugolib/site_stats_test.go +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright 2017 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 hugolib - -import ( - "bytes" - "fmt" - "testing" - - "github.com/gohugoio/hugo/helpers" - - qt "github.com/frankban/quicktest" -) - -func TestSiteStats(t *testing.T) { - t.Parallel() - - c := qt.New(t) - - siteConfig := ` -baseURL = "http://example.com/blog" - -defaultContentLanguage = "nn" - -[pagination] -pagerSize = 1 - -[languages] -[languages.nn] -languageName = "Nynorsk" -weight = 1 -title = "Hugo på norsk" - -[languages.en] -languageName = "English" -weight = 2 -title = "Hugo in English" - -` - - pageTemplate := `--- -title: "T%d" -tags: -%s -categories: -%s -aliases: [/Ali%d] ---- -# Doc -` - - b := newTestSitesBuilder(t).WithConfigFile("toml", siteConfig) - - b.WithTemplates( - "_default/single.html", "Single|{{ .Title }}|{{ .Content }}", - "_default/list.html", `List|{{ .Title }}|Pages: {{ .Paginator.TotalPages }}|{{ .Content }}`, - "_default/terms.html", "Terms List|{{ .Title }}|{{ .Content }}", - ) - - for i := range 2 { - for j := range 2 { - pageID := i + j + 1 - b.WithContent(fmt.Sprintf("content/sect/p%d.md", pageID), - fmt.Sprintf(pageTemplate, pageID, fmt.Sprintf("- tag%d", j), fmt.Sprintf("- category%d", j), pageID)) - } - } - - for i := range 5 { - b.WithContent(fmt.Sprintf("assets/image%d.png", i+1), "image") - } - - b.Build(BuildCfg{}) - h := b.H - - stats := []*helpers.ProcessingStats{ - h.Sites[0].PathSpec.ProcessingStats, - h.Sites[1].PathSpec.ProcessingStats, - } - - var buff bytes.Buffer - - helpers.ProcessingStatsTable(&buff, stats...) - - c.Assert(buff.String(), qt.Contains, "Pages │ 21 │ 7") -} - -func TestSiteLastmod(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.com/" --- content/_index.md -- ---- -date: 2023-01-01 ---- --- content/posts/_index.md -- ---- -date: 2023-02-01 ---- --- content/posts/post-1.md -- ---- -date: 2023-03-01 ---- --- content/posts/post-2.md -- ---- -date: 2023-04-01 ---- --- layouts/index.html -- -site.Lastmod: {{ .Site.Lastmod.Format "2006-01-02" }} -home.Lastmod: {{ site.Home.Lastmod.Format "2006-01-02" }} - -` - b := Test(t, files) - - b.AssertFileContent("public/index.html", "site.Lastmod: 2023-04-01\nhome.Lastmod: 2023-01-01") -} diff --git a/hugolib/site_test.go b/hugolib/site_test.go index 199c878cd..ff8fdf48b 100644 --- a/hugolib/site_test.go +++ b/hugolib/site_test.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Hugo Authors. All rights reserved. +// Copyright 2016 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,36 +14,74 @@ package hugolib import ( - "context" - "encoding/json" "fmt" - "os" "path/filepath" "strings" "testing" - "github.com/gobuffalo/flect" - "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/publisher" + "github.com/bep/inflect" + jww "github.com/spf13/jwalterweatherman" + + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/source" - qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/deps" - "github.com/gohugoio/hugo/resources/kinds" - "github.com/gohugoio/hugo/resources/page" + "github.com/gohugoio/hugo/hugofs" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) +const ( + pageSimpleTitle = `--- +title: simple template +--- +content` + + templateMissingFunc = "{{ .Title | funcdoesnotexists }}" + templateWithURLAbs = "
    Going" +) + +func init() { + testMode = true +} + +func pageMust(p *Page, err error) *Page { + if err != nil { + panic(err) + } + return p +} + +func TestRenderWithInvalidTemplate(t *testing.T) { + t.Parallel() + cfg, fs := newTestCfg() + + writeSource(t, fs, filepath.Join("content", "foo.md"), "foo") + + withTemplate := createWithTemplateFromNameValues("missing", templateMissingFunc) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg, WithTemplate: withTemplate}, BuildCfg{}) + + errCount := s.Log.LogCountForLevelsGreaterThanorEqualTo(jww.LevelError) + + // TODO(bep) clean up the template error handling + // The template errors are stored in a slice etc. so we get 4 log entries + // When we should get only 1 + if errCount == 0 { + t.Fatalf("Expecting the template to log 1 ERROR, got %d", errCount) + } +} + func TestDraftAndFutureRender(t *testing.T) { t.Parallel() - c := qt.New(t) - - sources := [][2]string{ - {filepath.FromSlash("sect/doc1.md"), "---\ntitle: doc1\ndraft: true\npublishdate: \"2414-05-29\"\n---\n# doc1\n*some content*"}, - {filepath.FromSlash("sect/doc2.md"), "---\ntitle: doc2\ndraft: true\npublishdate: \"2012-05-29\"\n---\n# doc2\n*some content*"}, - {filepath.FromSlash("sect/doc3.md"), "---\ntitle: doc3\ndraft: false\npublishdate: \"2414-05-29\"\n---\n# doc3\n*some content*"}, - {filepath.FromSlash("sect/doc4.md"), "---\ntitle: doc4\ndraft: false\npublishdate: \"2012-05-29\"\n---\n# doc4\n*some content*"}, + sources := []source.ByteSource{ + {Name: filepath.FromSlash("sect/doc1.md"), Content: []byte("---\ntitle: doc1\ndraft: true\npublishdate: \"2414-05-29\"\n---\n# doc1\n*some content*")}, + {Name: filepath.FromSlash("sect/doc2.md"), Content: []byte("---\ntitle: doc2\ndraft: true\npublishdate: \"2012-05-29\"\n---\n# doc2\n*some content*")}, + {Name: filepath.FromSlash("sect/doc3.md"), Content: []byte("---\ntitle: doc3\ndraft: false\npublishdate: \"2414-05-29\"\n---\n# doc3\n*some content*")}, + {Name: filepath.FromSlash("sect/doc4.md"), Content: []byte("---\ntitle: doc4\ndraft: false\npublishdate: \"2012-05-29\"\n---\n# doc4\n*some content*")}, } - siteSetup := func(t *testing.T, configKeyValues ...any) *Site { + siteSetup := func(t *testing.T, configKeyValues ...interface{}) *Site { cfg, fs := newTestCfg() cfg.Set("baseURL", "http://auth/bub") @@ -51,25 +89,24 @@ func TestDraftAndFutureRender(t *testing.T) { for i := 0; i < len(configKeyValues); i += 2 { cfg.Set(configKeyValues[i].(string), configKeyValues[i+1]) } - configs, err := loadTestConfigFromProvider(cfg) - c.Assert(err, qt.IsNil) for _, src := range sources { - writeSource(t, fs, filepath.Join("content", src[0]), src[1]) + writeSource(t, fs, filepath.Join("content", src.Name), string(src.Content)) + } - return buildSingleSite(t, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{}) + return buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) } // Testing Defaults.. Only draft:true and publishDate in the past should be rendered s := siteSetup(t) - if len(s.RegularPages()) != 1 { + if len(s.RegularPages) != 1 { t.Fatal("Draft or Future dated content published unexpectedly") } // only publishDate in the past should be rendered s = siteSetup(t, "buildDrafts", true) - if len(s.RegularPages()) != 2 { + if len(s.RegularPages) != 2 { t.Fatal("Future Dated Posts published unexpectedly") } @@ -78,7 +115,7 @@ func TestDraftAndFutureRender(t *testing.T) { "buildDrafts", false, "buildFuture", true) - if len(s.RegularPages()) != 2 { + if len(s.RegularPages) != 2 { t.Fatal("Draft posts published unexpectedly") } @@ -87,46 +124,44 @@ func TestDraftAndFutureRender(t *testing.T) { "buildDrafts", true, "buildFuture", true) - if len(s.RegularPages()) != 4 { + if len(s.RegularPages) != 4 { t.Fatal("Drafts or Future posts not included as expected") } + } func TestFutureExpirationRender(t *testing.T) { t.Parallel() - c := qt.New(t) - sources := [][2]string{ - {filepath.FromSlash("sect/doc3.md"), "---\ntitle: doc1\nexpirydate: \"2400-05-29\"\n---\n# doc1\n*some content*"}, - {filepath.FromSlash("sect/doc4.md"), "---\ntitle: doc2\nexpirydate: \"2000-05-29\"\n---\n# doc2\n*some content*"}, + sources := []source.ByteSource{ + {Name: filepath.FromSlash("sect/doc3.md"), Content: []byte("---\ntitle: doc1\nexpirydate: \"2400-05-29\"\n---\n# doc1\n*some content*")}, + {Name: filepath.FromSlash("sect/doc4.md"), Content: []byte("---\ntitle: doc2\nexpirydate: \"2000-05-29\"\n---\n# doc2\n*some content*")}, } siteSetup := func(t *testing.T) *Site { cfg, fs := newTestCfg() cfg.Set("baseURL", "http://auth/bub") - configs, err := loadTestConfigFromProvider(cfg) - c.Assert(err, qt.IsNil) - for _, src := range sources { - writeSource(t, fs, filepath.Join("content", src[0]), src[1]) + writeSource(t, fs, filepath.Join("content", src.Name), string(src.Content)) + } - return buildSingleSite(t, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{}) + return buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) } s := siteSetup(t) - if len(s.AllPages()) != 1 { - if len(s.RegularPages()) > 1 { + if len(s.AllPages) != 1 { + if len(s.RegularPages) > 1 { t.Fatal("Expired content published unexpectedly") } - if len(s.RegularPages()) < 1 { + if len(s.RegularPages) < 1 { t.Fatal("Valid content expired unexpectedly") } } - if s.AllPages()[0].Title() == "doc2" { + if s.AllPages[0].Title == "doc2" { t.Fatal("Expired content published unexpectedly") } } @@ -135,9 +170,6 @@ func TestLastChange(t *testing.T) { t.Parallel() cfg, fs := newTestCfg() - c := qt.New(t) - configs, err := loadTestConfigFromProvider(cfg) - c.Assert(err, qt.IsNil) writeSource(t, fs, filepath.Join("content", "sect/doc1.md"), "---\ntitle: doc1\nweight: 1\ndate: 2014-05-29\n---\n# doc1\n*some content*") writeSource(t, fs, filepath.Join("content", "sect/doc2.md"), "---\ntitle: doc2\nweight: 2\ndate: 2015-05-29\n---\n# doc2\n*some content*") @@ -145,26 +177,24 @@ func TestLastChange(t *testing.T) { writeSource(t, fs, filepath.Join("content", "sect/doc4.md"), "---\ntitle: doc4\nweight: 4\ndate: 2016-05-29\n---\n# doc4\n*some content*") writeSource(t, fs, filepath.Join("content", "sect/doc5.md"), "---\ntitle: doc5\nweight: 3\n---\n# doc5\n*some content*") - s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{SkipRender: true}) + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) - c.Assert(s.Lastmod().IsZero(), qt.Equals, false) - c.Assert(s.Lastmod().Year(), qt.Equals, 2017) + require.False(t, s.Info.LastChange.IsZero(), "Site.LastChange is zero") + require.Equal(t, 2017, s.Info.LastChange.Year(), "Site.LastChange should be set to the page with latest Lastmod (year 2017)") } // Issue #_index func TestPageWithUnderScoreIndexInFilename(t *testing.T) { t.Parallel() - c := qt.New(t) cfg, fs := newTestCfg() - configs, err := loadTestConfigFromProvider(cfg) - c.Assert(err, qt.IsNil) writeSource(t, fs, filepath.Join("content", "sect/my_index_file.md"), "---\ntitle: doc1\nweight: 1\ndate: 2014-05-29\n---\n# doc1\n*some content*") - s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{SkipRender: true}) + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) + + require.Len(t, s.RegularPages, 1) - c.Assert(len(s.RegularPages()), qt.Equals, 1) } // Issue #957 @@ -178,7 +208,6 @@ func TestCrossrefs(t *testing.T) { } func doTestCrossrefs(t *testing.T, relative, uglyURLs bool) { - c := qt.New(t) baseURL := "http://foo/bar" @@ -203,31 +232,24 @@ func doTestCrossrefs(t *testing.T, relative, uglyURLs bool) { expectedPathSuffix = "/index.html" } - doc3Slashed := filepath.FromSlash("/sect/doc3.md") - - sources := [][2]string{ + sources := []source.ByteSource{ { - filepath.FromSlash("sect/doc1.md"), - fmt.Sprintf(`Ref 2: {{< %s "sect/doc2.md" >}}`, refShortcode), + Name: filepath.FromSlash("sect/doc1.md"), + Content: []byte(fmt.Sprintf(`Ref 2: {{< %s "sect/doc2.md" >}}`, refShortcode)), }, // Issue #1148: Make sure that no P-tags is added around shortcodes. { - filepath.FromSlash("sect/doc2.md"), - fmt.Sprintf(`**Ref 1:** + Name: filepath.FromSlash("sect/doc2.md"), + Content: []byte(fmt.Sprintf(`**Ref 1:** {{< %s "sect/doc1.md" >}} -THE END.`, refShortcode), +THE END.`, refShortcode)), }, // Issue #1753: Should not add a trailing newline after shortcode. { - filepath.FromSlash("sect/doc3.md"), - fmt.Sprintf(`**Ref 1:** {{< %s "sect/doc3.md" >}}.`, refShortcode), - }, - // Issue #3703 - { - filepath.FromSlash("sect/doc4.md"), - fmt.Sprintf(`**Ref 1:** {{< %s "%s" >}}.`, refShortcode, doc3Slashed), + Name: filepath.FromSlash("sect/doc3.md"), + Content: []byte(fmt.Sprintf(`**Ref 1:**{{< %s "sect/doc3.md" >}}.`, refShortcode)), }, } @@ -236,39 +258,39 @@ THE END.`, refShortcode), cfg.Set("baseURL", baseURL) cfg.Set("uglyURLs", uglyURLs) cfg.Set("verbose", true) - configs, err := loadTestConfigFromProvider(cfg) - c.Assert(err, qt.IsNil) for _, src := range sources { - writeSource(t, fs, filepath.Join("content", src[0]), src[1]) + writeSource(t, fs, filepath.Join("content", src.Name), string(src.Content)) } - writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), "{{.Content}}") s := buildSingleSite( t, deps.DepsCfg{ - Fs: fs, - Configs: configs, - }, + Fs: fs, + Cfg: cfg, + WithTemplate: createWithTemplateFromNameValues("_default/single.html", "{{.Content}}")}, BuildCfg{}) - c.Assert(len(s.RegularPages()), qt.Equals, 4) + if len(s.RegularPages) != 3 { + t.Fatalf("Expected 3 got %d pages", len(s.AllPages)) + } - th := newTestHelper(s.conf, s.Fs, t) + th := testHelper{s.Cfg, s.Fs, t} tests := []struct { doc string expected string }{ {filepath.FromSlash(fmt.Sprintf("public/sect/doc1%s", expectedPathSuffix)), fmt.Sprintf("

    Ref 2: %s/sect/doc2%s

    \n", expectedBase, expectedURLSuffix)}, - {filepath.FromSlash(fmt.Sprintf("public/sect/doc2%s", expectedPathSuffix)), fmt.Sprintf("

    Ref 1:

    \n%s/sect/doc1%s\n

    THE END.

    \n", expectedBase, expectedURLSuffix)}, - {filepath.FromSlash(fmt.Sprintf("public/sect/doc3%s", expectedPathSuffix)), fmt.Sprintf("

    Ref 1: %s/sect/doc3%s.

    \n", expectedBase, expectedURLSuffix)}, - {filepath.FromSlash(fmt.Sprintf("public/sect/doc4%s", expectedPathSuffix)), fmt.Sprintf("

    Ref 1: %s/sect/doc3%s.

    \n", expectedBase, expectedURLSuffix)}, + {filepath.FromSlash(fmt.Sprintf("public/sect/doc2%s", expectedPathSuffix)), fmt.Sprintf("

    Ref 1:

    \n\n%s/sect/doc1%s\n\n

    THE END.

    \n", expectedBase, expectedURLSuffix)}, + {filepath.FromSlash(fmt.Sprintf("public/sect/doc3%s", expectedPathSuffix)), fmt.Sprintf("

    Ref 1:%s/sect/doc3%s.

    \n", expectedBase, expectedURLSuffix)}, } for _, test := range tests { th.assertFileContent(test.doc, test.expected) + } + } // Issue #939 @@ -281,23 +303,27 @@ func TestShouldAlwaysHaveUglyURLs(t *testing.T) { } func doTestShouldAlwaysHaveUglyURLs(t *testing.T, uglyURLs bool) { + cfg, fs := newTestCfg() - c := qt.New(t) cfg.Set("verbose", true) cfg.Set("baseURL", "http://auth/bub") + cfg.Set("disableSitemap", false) + cfg.Set("disableRSS", false) + cfg.Set("rssURI", "index.xml") + cfg.Set("blackfriday", + map[string]interface{}{ + "plainIDAnchors": true}) + cfg.Set("uglyURLs", uglyURLs) - configs, err := loadTestConfigFromProvider(cfg) - c.Assert(err, qt.IsNil) - - sources := [][2]string{ - {filepath.FromSlash("sect/doc1.md"), "---\nmarkup: markdown\n---\n# title\nsome *content*"}, - {filepath.FromSlash("sect/doc2.md"), "---\nurl: /ugly.html\nmarkup: markdown\n---\n# title\ndoc2 *content*"}, + sources := []source.ByteSource{ + {Name: filepath.FromSlash("sect/doc1.md"), Content: []byte("---\nmarkup: markdown\n---\n# title\nsome *content*")}, + {Name: filepath.FromSlash("sect/doc2.md"), Content: []byte("---\nurl: /ugly.html\nmarkup: markdown\n---\n# title\ndoc2 *content*")}, } for _, src := range sources { - writeSource(t, fs, filepath.Join("content", src[0]), src[1]) + writeSource(t, fs, filepath.Join("content", src.Name), string(src.Content)) } writeSource(t, fs, filepath.Join("layouts", "index.html"), "Home Sweet {{ if.IsHome }}Home{{ end }}.") @@ -306,7 +332,7 @@ func doTestShouldAlwaysHaveUglyURLs(t *testing.T, uglyURLs bool) { writeSource(t, fs, filepath.Join("layouts", "rss.xml"), "RSS") writeSource(t, fs, filepath.Join("layouts", "sitemap.xml"), "SITEMAP") - s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{}) + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) var expectedPagePath string if uglyURLs { @@ -320,196 +346,63 @@ func doTestShouldAlwaysHaveUglyURLs(t *testing.T, uglyURLs bool) { expected string }{ {filepath.FromSlash("public/index.html"), "Home Sweet Home."}, - {filepath.FromSlash(expectedPagePath), "

    title

    \n

    some content

    \n"}, + {filepath.FromSlash(expectedPagePath), "\n\n

    title

    \n\n

    some content

    \n"}, {filepath.FromSlash("public/404.html"), "Page Not Found."}, - {filepath.FromSlash("public/index.xml"), "RSS"}, - {filepath.FromSlash("public/sitemap.xml"), "SITEMAP"}, + {filepath.FromSlash("public/index.xml"), "\nRSS"}, + {filepath.FromSlash("public/sitemap.xml"), "\nSITEMAP"}, // Issue #1923 - {filepath.FromSlash("public/ugly.html"), "

    title

    \n

    doc2 content

    \n"}, + {filepath.FromSlash("public/ugly.html"), "\n\n

    title

    \n\n

    doc2 content

    \n"}, } - for _, p := range s.RegularPages() { - c.Assert(p.IsHome(), qt.Equals, false) + for _, p := range s.RegularPages { + assert.False(t, p.IsHome()) } for _, test := range tests { - content := readWorkingDir(t, fs, test.doc) + content := readDestination(t, fs, test.doc) if content != test.expected { t.Errorf("%s content expected:\n%q\ngot:\n%q", test.doc, test.expected, content) } } + +} + +func TestNewSiteDefaultLang(t *testing.T) { + t.Parallel() + s, err := NewSiteDefaultLang() + require.NoError(t, err) + require.Equal(t, hugofs.Os, s.Fs.Source) + require.Equal(t, hugofs.Os, s.Fs.Destination) } // Issue #3355 func TestShouldNotWriteZeroLengthFilesToDestination(t *testing.T) { - c := qt.New(t) - cfg, fs := newTestCfg() - configs, err := loadTestConfigFromProvider(cfg) - c.Assert(err, qt.IsNil) writeSource(t, fs, filepath.Join("content", "simple.html"), "simple") writeSource(t, fs, filepath.Join("layouts", "_default/single.html"), "{{.Content}}") writeSource(t, fs, filepath.Join("layouts", "_default/list.html"), "") - s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{}) - th := newTestHelper(s.conf, s.Fs, t) + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + th := testHelper{s.Cfg, s.Fs, t} th.assertFileNotExist(filepath.Join("public", "index.html")) } -func TestMainSections(t *testing.T) { - c := qt.New(t) - for _, paramSet := range []bool{false, true} { - c.Run(fmt.Sprintf("param-%t", paramSet), func(c *qt.C) { - v := config.New() - if paramSet { - v.Set("params", map[string]any{ - "mainSections": []string{"a1", "a2"}, - }) - } - - b := newTestSitesBuilder(c).WithViper(v) - - for i := range 20 { - b.WithContent(fmt.Sprintf("page%d.md", i), `--- -title: "Page" ---- -`) - } - - for i := range 5 { - b.WithContent(fmt.Sprintf("blog/page%d.md", i), `--- -title: "Page" -tags: ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"] ---- -`) - } - - for i := range 3 { - b.WithContent(fmt.Sprintf("docs/page%d.md", i), `--- -title: "Page" ---- -`) - } - - b.WithTemplates("index.html", ` -mainSections: {{ .Site.Params.mainSections }} - -{{ range (where .Site.RegularPages "Type" "in" .Site.Params.mainSections) }} -Main section page: {{ .RelPermalink }} -{{ end }} -`) - - b.Build(BuildCfg{}) - - if paramSet { - b.AssertFileContent("public/index.html", "mainSections: [a1 a2]") - } else { - b.AssertFileContent("public/index.html", "mainSections: [blog]", "Main section page: /blog/page3/") - } - }) - } -} - -func TestMainSectionsMoveToSite(t *testing.T) { - t.Run("defined in params", func(t *testing.T) { - t.Parallel() - - files := ` --- config.toml -- -disableKinds = ['RSS','sitemap','taxonomy','term'] -[params] -mainSections=["a", "b"] --- content/mysect/page1.md -- --- layouts/index.html -- -{{/* Behaviour before Hugo 0.112.0. */}} -MainSections Params: {{ site.Params.mainSections }}| -MainSections Site method: {{ site.MainSections }}| - - - ` - - b := Test(t, files) - - b.AssertFileContent("public/index.html", ` -MainSections Params: [a b]| -MainSections Site method: [a b]| - `) - }) - - t.Run("defined in top level config", func(t *testing.T) { - t.Parallel() - - files := ` --- config.toml -- -disableKinds = ['RSS','sitemap','taxonomy','term'] -mainSections=["a", "b"] -[params] -[params.sub] -mainSections=["c", "d"] --- content/mysect/page1.md -- --- layouts/index.html -- -{{/* Behaviour before Hugo 0.112.0. */}} -MainSections Params: {{ site.Params.mainSections }}| -MainSections Param sub: {{ site.Params.sub.mainSections }}| -MainSections Site method: {{ site.MainSections }}| - - -` - - b := Test(t, files) - - b.AssertFileContent("public/index.html", ` -MainSections Params: [a b]| -MainSections Param sub: [c d]| -MainSections Site method: [a b]| -`) - }) - - t.Run("guessed from pages", func(t *testing.T) { - t.Parallel() - - files := ` --- config.toml -- -disableKinds = ['RSS','sitemap','taxonomy','term'] --- content/mysect/page1.md -- --- layouts/index.html -- -MainSections Params: {{ site.Params.mainSections }}| -MainSections Site method: {{ site.MainSections }}| - - - ` - - b := Test(t, files) - - b.AssertFileContent("public/index.html", ` -MainSections Params: [mysect]| -MainSections Site method: [mysect]| - `) - }) -} - // Issue #1176 func TestSectionNaming(t *testing.T) { + t.Parallel() for _, canonify := range []bool{true, false} { for _, uglify := range []bool{true, false} { for _, pluralize := range []bool{true, false} { - canonify := canonify - uglify := uglify - pluralize := pluralize - t.Run(fmt.Sprintf("canonify=%t,uglify=%t,pluralize=%t", canonify, uglify, pluralize), func(t *testing.T) { - t.Parallel() - doTestSectionNaming(t, canonify, uglify, pluralize) - }) + doTestSectionNaming(t, canonify, uglify, pluralize) } } } } func doTestSectionNaming(t *testing.T, canonify, uglify, pluralize bool) { - c := qt.New(t) var expectedPathSuffix string @@ -519,12 +412,12 @@ func doTestSectionNaming(t *testing.T, canonify, uglify, pluralize bool) { expectedPathSuffix = "/index.html" } - sources := [][2]string{ - {filepath.FromSlash("sect/doc1.html"), "doc1"}, + sources := []source.ByteSource{ + {Name: filepath.FromSlash("sect/doc1.html"), Content: []byte("doc1")}, // Add one more page to sect to make sure sect is picked in mainSections - {filepath.FromSlash("sect/sect.html"), "sect"}, - {filepath.FromSlash("Fish and Chips/doc2.html"), "doc2"}, - {filepath.FromSlash("ラーメン/doc3.html"), "doc3"}, + {Name: filepath.FromSlash("sect/sect.html"), Content: []byte("sect")}, + {Name: filepath.FromSlash("Fish and Chips/doc2.html"), Content: []byte("doc2")}, + {Name: filepath.FromSlash("ラーメン/doc3.html"), Content: []byte("doc3")}, } cfg, fs := newTestCfg() @@ -534,21 +427,20 @@ func doTestSectionNaming(t *testing.T, canonify, uglify, pluralize bool) { cfg.Set("pluralizeListTitles", pluralize) cfg.Set("canonifyURLs", canonify) - configs, err := loadTestConfigFromProvider(cfg) - c.Assert(err, qt.IsNil) - - for _, src := range sources { - writeSource(t, fs, filepath.Join("content", src[0]), src[1]) + for _, source := range sources { + writeSource(t, fs, filepath.Join("content", source.Name), string(source.Content)) } writeSource(t, fs, filepath.Join("layouts", "_default/single.html"), "{{.Content}}") - writeSource(t, fs, filepath.Join("layouts", "_default/list.html"), "{{ .Kind }}|{{.Title}}") + writeSource(t, fs, filepath.Join("layouts", "_default/list.html"), "{{.Title}}") - s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{}) + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) - c.Assert(s.MainSections(), qt.DeepEquals, []string{"sect"}) + mainSections, err := s.Info.Param("mainSections") + require.NoError(t, err) + require.Equal(t, []string{"sect"}, mainSections) - th := newTestHelper(s.conf, s.Fs, t) + th := testHelper{s.Cfg, s.Fs, t} tests := []struct { doc string pluralAware bool @@ -565,30 +457,144 @@ func doTestSectionNaming(t *testing.T, canonify, uglify, pluralize bool) { for _, test := range tests { if test.pluralAware && pluralize { - test.expected = flect.Pluralize(test.expected) + test.expected = inflect.Pluralize(test.expected) } th.assertFileContent(filepath.Join("public", test.doc), test.expected) } + +} +func TestSkipRender(t *testing.T) { + t.Parallel() + sources := []source.ByteSource{ + {Name: filepath.FromSlash("sect/doc1.html"), Content: []byte("---\nmarkup: markdown\n---\n# title\nsome *content*")}, + {Name: filepath.FromSlash("sect/doc2.html"), Content: []byte("more content")}, + {Name: filepath.FromSlash("sect/doc3.md"), Content: []byte("# doc3\n*some* content")}, + {Name: filepath.FromSlash("sect/doc4.md"), Content: []byte("---\ntitle: doc4\n---\n# doc4\n*some content*")}, + {Name: filepath.FromSlash("sect/doc5.html"), Content: []byte("{{ template \"head\" }}body5")}, + {Name: filepath.FromSlash("sect/doc6.html"), Content: []byte("{{ template \"head_abs\" }}body5")}, + {Name: filepath.FromSlash("doc7.html"), Content: []byte("doc7 content")}, + {Name: filepath.FromSlash("sect/doc8.html"), Content: []byte("---\nmarkup: md\n---\n# title\nsome *content*")}, + // Issue #3021 + {Name: filepath.FromSlash("doc9.html"), Content: []byte("doc9: {{< myshortcode >}}")}, + } + + cfg, fs := newTestCfg() + + cfg.Set("verbose", true) + cfg.Set("canonifyURLs", true) + cfg.Set("uglyURLs", true) + cfg.Set("baseURL", "http://auth/bub") + + for _, src := range sources { + writeSource(t, fs, filepath.Join("content", src.Name), string(src.Content)) + + } + + writeSource(t, fs, filepath.Join("layouts", "_default/single.html"), "{{.Content}}") + writeSource(t, fs, filepath.Join("layouts", "head"), "") + writeSource(t, fs, filepath.Join("layouts", "head_abs"), "") + writeSource(t, fs, filepath.Join("layouts", "shortcodes", "myshortcode.html"), "SHORT") + + buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + + tests := []struct { + doc string + expected string + }{ + {filepath.FromSlash("public/sect/doc1.html"), "\n\n

    title

    \n\n

    some content

    \n"}, + {filepath.FromSlash("public/sect/doc2.html"), "more content"}, + {filepath.FromSlash("public/sect/doc3.html"), "\n\n

    doc3

    \n\n

    some content

    \n"}, + {filepath.FromSlash("public/sect/doc4.html"), "\n\n

    doc4

    \n\n

    some content

    \n"}, + {filepath.FromSlash("public/sect/doc5.html"), "body5"}, + {filepath.FromSlash("public/sect/doc6.html"), "body5"}, + {filepath.FromSlash("public/doc7.html"), "doc7 content"}, + {filepath.FromSlash("public/sect/doc8.html"), "\n\n

    title

    \n\n

    some content

    \n"}, + {filepath.FromSlash("public/doc9.html"), "doc9: SHORT"}, + } + + for _, test := range tests { + file, err := fs.Destination.Open(test.doc) + if err != nil { + t.Fatalf("Did not find %s in target.", test.doc) + } + + content := helpers.ReaderToString(file) + + if content != test.expected { + t.Errorf("%s content expected:\n%q\ngot:\n%q", test.doc, test.expected, content) + } + } } -var weightedPage1 = `+++ +func TestAbsURLify(t *testing.T) { + t.Parallel() + sources := []source.ByteSource{ + {Name: filepath.FromSlash("sect/doc1.html"), Content: []byte("link")}, + {Name: filepath.FromSlash("blue/doc2.html"), Content: []byte("---\nf: t\n---\nmore content")}, + } + for _, baseURL := range []string{"http://auth/bub", "http://base", "//base"} { + for _, canonify := range []bool{true, false} { + + cfg, fs := newTestCfg() + + cfg.Set("uglyURLs", true) + cfg.Set("canonifyURLs", canonify) + cfg.Set("baseURL", baseURL) + + for _, src := range sources { + writeSource(t, fs, filepath.Join("content", src.Name), string(src.Content)) + + } + + writeSource(t, fs, filepath.Join("layouts", "blue/single.html"), templateWithURLAbs) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + th := testHelper{s.Cfg, s.Fs, t} + + tests := []struct { + file, expected string + }{ + {"public/blue/doc2.html", "Going"}, + {"public/sect/doc1.html", "link"}, + } + + for _, test := range tests { + + expected := test.expected + + if strings.Contains(expected, "%s") { + expected = fmt.Sprintf(expected, baseURL) + } + + if !canonify { + expected = strings.Replace(expected, baseURL, "", -1) + } + + th.assertFileContent(test.file, expected) + + } + } + } +} + +var weightedPage1 = []byte(`+++ weight = "2" title = "One" my_param = "foo" my_date = 1979-05-27T07:32:00Z +++ -Front Matter with Ordered Pages` +Front Matter with Ordered Pages`) -var weightedPage2 = `+++ +var weightedPage2 = []byte(`+++ weight = "6" title = "Two" publishdate = "2012-03-05" my_param = "foo" +++ -Front Matter with Ordered Pages 2` +Front Matter with Ordered Pages 2`) -var weightedPage3 = `+++ +var weightedPage3 = []byte(`+++ weight = "4" title = "Three" date = "2012-04-06" @@ -597,108 +603,97 @@ my_param = "bar" only_one = "yes" my_date = 2010-05-27T07:32:00Z +++ -Front Matter with Ordered Pages 3` +Front Matter with Ordered Pages 3`) -var weightedPage4 = `+++ +var weightedPage4 = []byte(`+++ weight = "4" title = "Four" date = "2012-01-01" publishdate = "2012-01-01" my_param = "baz" my_date = 2010-05-27T07:32:00Z -summary = "A _custom_ summary" categories = [ "hugo" ] +++ -Front Matter with Ordered Pages 4. This is longer content` +Front Matter with Ordered Pages 4. This is longer content`) -var weightedPage5 = `+++ -weight = "5" -title = "Five" - -[build] -render = "never" -+++ -Front Matter with Ordered Pages 5` - -var weightedSources = [][2]string{ - {filepath.FromSlash("sect/doc1.md"), weightedPage1}, - {filepath.FromSlash("sect/doc2.md"), weightedPage2}, - {filepath.FromSlash("sect/doc3.md"), weightedPage3}, - {filepath.FromSlash("sect/doc4.md"), weightedPage4}, - {filepath.FromSlash("sect/doc5.md"), weightedPage5}, +var weightedSources = []source.ByteSource{ + {Name: filepath.FromSlash("sect/doc1.md"), Content: weightedPage1}, + {Name: filepath.FromSlash("sect/doc2.md"), Content: weightedPage2}, + {Name: filepath.FromSlash("sect/doc3.md"), Content: weightedPage3}, + {Name: filepath.FromSlash("sect/doc4.md"), Content: weightedPage4}, } func TestOrderedPages(t *testing.T) { t.Parallel() - c := qt.New(t) cfg, fs := newTestCfg() cfg.Set("baseURL", "http://auth/bub") - configs, err := loadTestConfigFromProvider(cfg) - c.Assert(err, qt.IsNil) for _, src := range weightedSources { - writeSource(t, fs, filepath.Join("content", src[0]), src[1]) + writeSource(t, fs, filepath.Join("content", src.Name), string(src.Content)) + } - s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{SkipRender: true}) + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) - if s.getPageOldVersion(kinds.KindSection, "sect").Pages()[1].Title() != "Three" || s.getPageOldVersion(kinds.KindSection, "sect").Pages()[2].Title() != "Four" { + if s.getPage(KindSection, "sect").Pages[1].Title != "Three" || s.getPage(KindSection, "sect").Pages[2].Title != "Four" { t.Error("Pages in unexpected order.") } - bydate := s.RegularPages().ByDate() + bydate := s.RegularPages.ByDate() - if bydate[0].Title() != "One" { - t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", "One", bydate[0].Title()) + if bydate[0].Title != "One" { + t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", "One", bydate[0].Title) } rev := bydate.Reverse() - if rev[0].Title() != "Three" { - t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", "Three", rev[0].Title()) + if rev[0].Title != "Three" { + t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", "Three", rev[0].Title) } - bypubdate := s.RegularPages().ByPublishDate() + bypubdate := s.RegularPages.ByPublishDate() - if bypubdate[0].Title() != "One" { - t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", "One", bypubdate[0].Title()) + if bypubdate[0].Title != "One" { + t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", "One", bypubdate[0].Title) } rbypubdate := bypubdate.Reverse() - if rbypubdate[0].Title() != "Three" { - t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", "Three", rbypubdate[0].Title()) + if rbypubdate[0].Title != "Three" { + t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", "Three", rbypubdate[0].Title) } - bylength := s.RegularPages().ByLength(context.Background()) - if bylength[0].Title() != "One" { - t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", "One", bylength[0].Title()) + bylength := s.RegularPages.ByLength() + if bylength[0].Title != "One" { + t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", "One", bylength[0].Title) } rbylength := bylength.Reverse() - if rbylength[0].Title() != "Four" { - t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", "Four", rbylength[0].Title()) + if rbylength[0].Title != "Four" { + t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", "Four", rbylength[0].Title) } } -var groupedSources = [][2]string{ - {filepath.FromSlash("sect1/doc1.md"), weightedPage1}, - {filepath.FromSlash("sect1/doc2.md"), weightedPage2}, - {filepath.FromSlash("sect2/doc3.md"), weightedPage3}, - {filepath.FromSlash("sect3/doc4.md"), weightedPage4}, +var groupedSources = []source.ByteSource{ + {Name: filepath.FromSlash("sect1/doc1.md"), Content: weightedPage1}, + {Name: filepath.FromSlash("sect1/doc2.md"), Content: weightedPage2}, + {Name: filepath.FromSlash("sect2/doc3.md"), Content: weightedPage3}, + {Name: filepath.FromSlash("sect3/doc4.md"), Content: weightedPage4}, } func TestGroupedPages(t *testing.T) { t.Parallel() - c := qt.New(t) + defer func() { + if r := recover(); r != nil { + fmt.Println("Recovered in f", r) + } + }() cfg, fs := newTestCfg() cfg.Set("baseURL", "http://auth/bub") - configs, err := loadTestConfigFromProvider(cfg) - c.Assert(err, qt.IsNil) writeSourcesToSource(t, "content", fs, groupedSources...) - s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{}) + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) - rbysection, err := s.RegularPages().GroupBy(context.Background(), "Section", "desc") + rbysection, err := s.RegularPages.GroupBy("Section", "desc") if err != nil { t.Fatalf("Unable to make PageGroup array: %s", err) } @@ -712,14 +707,14 @@ func TestGroupedPages(t *testing.T) { if rbysection[2].Key != "sect1" { t.Errorf("PageGroup array in unexpected order. Third group key should be '%s', got '%s'", "sect1", rbysection[2].Key) } - if rbysection[0].Pages[0].Title() != "Four" { - t.Errorf("PageGroup has an unexpected page. First group's pages should have '%s', got '%s'", "Four", rbysection[0].Pages[0].Title()) + if rbysection[0].Pages[0].Title != "Four" { + t.Errorf("PageGroup has an unexpected page. First group's pages should have '%s', got '%s'", "Four", rbysection[0].Pages[0].Title) } if len(rbysection[2].Pages) != 2 { t.Errorf("PageGroup has unexpected number of pages. Third group should have '%d' pages, got '%d' pages", 2, len(rbysection[2].Pages)) } - bytype, err := s.RegularPages().GroupBy(context.Background(), "Type", "asc") + bytype, err := s.RegularPages.GroupBy("Type", "asc") if err != nil { t.Fatalf("Unable to make PageGroup array: %s", err) } @@ -732,14 +727,14 @@ func TestGroupedPages(t *testing.T) { if bytype[2].Key != "sect3" { t.Errorf("PageGroup array in unexpected order. Third group key should be '%s', got '%s'", "sect3", bytype[2].Key) } - if bytype[2].Pages[0].Title() != "Four" { - t.Errorf("PageGroup has an unexpected page. Third group's data should have '%s', got '%s'", "Four", bytype[0].Pages[0].Title()) + if bytype[2].Pages[0].Title != "Four" { + t.Errorf("PageGroup has an unexpected page. Third group's data should have '%s', got '%s'", "Four", bytype[0].Pages[0].Title) } if len(bytype[0].Pages) != 2 { t.Errorf("PageGroup has unexpected number of pages. First group should have '%d' pages, got '%d' pages", 2, len(bytype[2].Pages)) } - bydate, err := s.RegularPages().GroupByDate("2006-01", "asc") + bydate, err := s.RegularPages.GroupByDate("2006-01", "asc") if err != nil { t.Fatalf("Unable to make PageGroup array: %s", err) } @@ -749,8 +744,17 @@ func TestGroupedPages(t *testing.T) { if bydate[1].Key != "2012-01" { t.Errorf("PageGroup array in unexpected order. Second group key should be '%s', got '%s'", "2012-01", bydate[1].Key) } + if bydate[2].Key != "2012-04" { + t.Errorf("PageGroup array in unexpected order. Third group key should be '%s', got '%s'", "2012-04", bydate[2].Key) + } + if bydate[2].Pages[0].Title != "Three" { + t.Errorf("PageGroup has an unexpected page. Third group's pages should have '%s', got '%s'", "Three", bydate[2].Pages[0].Title) + } + if len(bydate[0].Pages) != 2 { + t.Errorf("PageGroup has unexpected number of pages. First group should have '%d' pages, got '%d' pages", 2, len(bydate[2].Pages)) + } - bypubdate, err := s.RegularPages().GroupByPublishDate("2006") + bypubdate, err := s.RegularPages.GroupByPublishDate("2006") if err != nil { t.Fatalf("Unable to make PageGroup array: %s", err) } @@ -760,14 +764,14 @@ func TestGroupedPages(t *testing.T) { if bypubdate[1].Key != "0001" { t.Errorf("PageGroup array in unexpected order. Second group key should be '%s', got '%s'", "0001", bypubdate[1].Key) } - if bypubdate[0].Pages[0].Title() != "Three" { - t.Errorf("PageGroup has an unexpected page. Third group's pages should have '%s', got '%s'", "Three", bypubdate[0].Pages[0].Title()) + if bypubdate[0].Pages[0].Title != "Three" { + t.Errorf("PageGroup has an unexpected page. Third group's pages should have '%s', got '%s'", "Three", bypubdate[0].Pages[0].Title) } if len(bypubdate[0].Pages) != 3 { t.Errorf("PageGroup has unexpected number of pages. First group should have '%d' pages, got '%d' pages", 3, len(bypubdate[0].Pages)) } - byparam, err := s.RegularPages().GroupByParam("my_param", "desc") + byparam, err := s.RegularPages.GroupByParam("my_param", "desc") if err != nil { t.Fatalf("Unable to make PageGroup array: %s", err) } @@ -780,22 +784,19 @@ func TestGroupedPages(t *testing.T) { if byparam[2].Key != "bar" { t.Errorf("PageGroup array in unexpected order. Third group key should be '%s', got '%s'", "bar", byparam[2].Key) } - if byparam[2].Pages[0].Title() != "Three" { - t.Errorf("PageGroup has an unexpected page. Third group's pages should have '%s', got '%s'", "Three", byparam[2].Pages[0].Title()) + if byparam[2].Pages[0].Title != "Three" { + t.Errorf("PageGroup has an unexpected page. Third group's pages should have '%s', got '%s'", "Three", byparam[2].Pages[0].Title) } if len(byparam[0].Pages) != 2 { t.Errorf("PageGroup has unexpected number of pages. First group should have '%d' pages, got '%d' pages", 2, len(byparam[0].Pages)) } - byNonExistentParam, err := s.RegularPages().GroupByParam("not_exist") - if err != nil { - t.Errorf("GroupByParam returned an error when it shouldn't") - } - if len(byNonExistentParam) != 0 { - t.Errorf("PageGroup array has unexpected elements. Group length should be '%d', got '%d'", 0, len(byNonExistentParam)) + _, err = s.RegularPages.GroupByParam("not_exist") + if err == nil { + t.Errorf("GroupByParam didn't return an expected error") } - byOnlyOneParam, err := s.RegularPages().GroupByParam("only_one") + byOnlyOneParam, err := s.RegularPages.GroupByParam("only_one") if err != nil { t.Fatalf("Unable to make PageGroup array: %s", err) } @@ -806,7 +807,7 @@ func TestGroupedPages(t *testing.T) { t.Errorf("PageGroup array in unexpected order. First group key should be '%s', got '%s'", "yes", byOnlyOneParam[0].Key) } - byParamDate, err := s.RegularPages().GroupByParamDate("my_date", "2006-01") + byParamDate, err := s.RegularPages.GroupByParamDate("my_date", "2006-01") if err != nil { t.Fatalf("Unable to make PageGroup array: %s", err) } @@ -816,51 +817,49 @@ func TestGroupedPages(t *testing.T) { if byParamDate[1].Key != "1979-05" { t.Errorf("PageGroup array in unexpected order. Second group key should be '%s', got '%s'", "1979-05", byParamDate[1].Key) } - if byParamDate[1].Pages[0].Title() != "One" { - t.Errorf("PageGroup has an unexpected page. Second group's pages should have '%s', got '%s'", "One", byParamDate[1].Pages[0].Title()) + if byParamDate[1].Pages[0].Title != "One" { + t.Errorf("PageGroup has an unexpected page. Second group's pages should have '%s', got '%s'", "One", byParamDate[1].Pages[0].Title) } if len(byParamDate[0].Pages) != 2 { t.Errorf("PageGroup has unexpected number of pages. First group should have '%d' pages, got '%d' pages", 2, len(byParamDate[2].Pages)) } } -var pageWithWeightedTaxonomies1 = `+++ +var pageWithWeightedTaxonomies1 = []byte(`+++ tags = [ "a", "b", "c" ] tags_weight = 22 categories = ["d"] title = "foo" categories_weight = 44 +++ -Front Matter with weighted tags and categories` +Front Matter with weighted tags and categories`) -var pageWithWeightedTaxonomies2 = `+++ +var pageWithWeightedTaxonomies2 = []byte(`+++ tags = "a" tags_weight = 33 title = "bar" categories = [ "d", "e" ] -categories_weight = 11.0 +categories_weight = 11 alias = "spf13" date = 1979-05-27T07:32:00Z +++ -Front Matter with weighted tags and categories` +Front Matter with weighted tags and categories`) -var pageWithWeightedTaxonomies3 = `+++ +var pageWithWeightedTaxonomies3 = []byte(`+++ title = "bza" categories = [ "e" ] categories_weight = 11 alias = "spf13" date = 2010-05-27T07:32:00Z +++ -Front Matter with weighted tags and categories` +Front Matter with weighted tags and categories`) func TestWeightedTaxonomies(t *testing.T) { t.Parallel() - c := qt.New(t) - - sources := [][2]string{ - {filepath.FromSlash("sect/doc1.md"), pageWithWeightedTaxonomies2}, - {filepath.FromSlash("sect/doc2.md"), pageWithWeightedTaxonomies1}, - {filepath.FromSlash("sect/doc3.md"), pageWithWeightedTaxonomies3}, + sources := []source.ByteSource{ + {Name: filepath.FromSlash("sect/doc1.md"), Content: pageWithWeightedTaxonomies2}, + {Name: filepath.FromSlash("sect/doc2.md"), Content: pageWithWeightedTaxonomies1}, + {Name: filepath.FromSlash("sect/doc3.md"), Content: pageWithWeightedTaxonomies3}, } taxonomies := make(map[string]string) @@ -871,74 +870,80 @@ func TestWeightedTaxonomies(t *testing.T) { cfg.Set("baseURL", "http://auth/bub") cfg.Set("taxonomies", taxonomies) - configs, err := loadTestConfigFromProvider(cfg) - c.Assert(err, qt.IsNil) writeSourcesToSource(t, "content", fs, sources...) - s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{}) + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) - if s.Taxonomies()["tags"]["a"][0].Page.Title() != "foo" { - t.Errorf("Pages in unexpected order, 'foo' expected first, got '%v'", s.Taxonomies()["tags"]["a"][0].Page.Title()) + if s.Taxonomies["tags"]["a"][0].Page.Title != "foo" { + t.Errorf("Pages in unexpected order, 'foo' expected first, got '%v'", s.Taxonomies["tags"]["a"][0].Page.Title) } - if s.Taxonomies()["categories"]["d"][0].Page.Title() != "bar" { - t.Errorf("Pages in unexpected order, 'bar' expected first, got '%v'", s.Taxonomies()["categories"]["d"][0].Page.Title()) + if s.Taxonomies["categories"]["d"][0].Page.Title != "bar" { + t.Errorf("Pages in unexpected order, 'bar' expected first, got '%v'", s.Taxonomies["categories"]["d"][0].Page.Title) } - if s.Taxonomies()["categories"]["e"][0].Page.Title() != "bza" { - t.Errorf("Pages in unexpected order, 'bza' expected first, got '%v'", s.Taxonomies()["categories"]["e"][0].Page.Title()) + if s.Taxonomies["categories"]["e"][0].Page.Title != "bza" { + t.Errorf("Pages in unexpected order, 'bza' expected first, got '%v'", s.Taxonomies["categories"]["e"][0].Page.Title) } } +func findPage(site *Site, f string) *Page { + sp := source.NewSourceSpec(site.Cfg, site.Fs) + currentPath := sp.NewFile(filepath.FromSlash(f)) + //t.Logf("looking for currentPath: %s", currentPath.Path()) + + for _, page := range site.Pages { + //t.Logf("page: %s", page.Source.Path()) + if page.Source.Path() == currentPath.Path() { + return page + } + } + return nil +} + func setupLinkingMockSite(t *testing.T) *Site { - sources := [][2]string{ - {filepath.FromSlash("level2/unique.md"), ""}, - {filepath.FromSlash("_index.md"), ""}, - {filepath.FromSlash("common.md"), ""}, - {filepath.FromSlash("rootfile.md"), ""}, - {filepath.FromSlash("root-image.png"), ""}, + sources := []source.ByteSource{ + {Name: filepath.FromSlash("level2/unique.md"), Content: []byte("")}, + {Name: filepath.FromSlash("index.md"), Content: []byte("")}, + {Name: filepath.FromSlash("rootfile.md"), Content: []byte("")}, + {Name: filepath.FromSlash("root-image.png"), Content: []byte("")}, - {filepath.FromSlash("level2/2-root.md"), ""}, - {filepath.FromSlash("level2/common.md"), ""}, + {Name: filepath.FromSlash("level2/2-root.md"), Content: []byte("")}, + {Name: filepath.FromSlash("level2/index.md"), Content: []byte("")}, + {Name: filepath.FromSlash("level2/common.md"), Content: []byte("")}, - {filepath.FromSlash("level2/2-image.png"), ""}, - {filepath.FromSlash("level2/common.png"), ""}, + {Name: filepath.FromSlash("level2/2-image.png"), Content: []byte("")}, + {Name: filepath.FromSlash("level2/common.png"), Content: []byte("")}, - {filepath.FromSlash("level2/level3/start.md"), ""}, - {filepath.FromSlash("level2/level3/_index.md"), ""}, - {filepath.FromSlash("level2/level3/3-root.md"), ""}, - {filepath.FromSlash("level2/level3/common.md"), ""}, - {filepath.FromSlash("level2/level3/3-image.png"), ""}, - {filepath.FromSlash("level2/level3/common.png"), ""}, - - {filepath.FromSlash("level2/level3/embedded.dot.md"), ""}, - - {filepath.FromSlash("leafbundle/index.md"), ""}, + {Name: filepath.FromSlash("level2/level3/3-root.md"), Content: []byte("")}, + {Name: filepath.FromSlash("level2/level3/index.md"), Content: []byte("")}, + {Name: filepath.FromSlash("level2/level3/common.md"), Content: []byte("")}, + {Name: filepath.FromSlash("level2/level3/3-image.png"), Content: []byte("")}, + {Name: filepath.FromSlash("level2/level3/common.png"), Content: []byte("")}, } cfg, fs := newTestCfg() cfg.Set("baseURL", "http://auth/") cfg.Set("uglyURLs", false) - cfg.Set("outputs", map[string]any{ + cfg.Set("outputs", map[string]interface{}{ "page": []string{"HTML", "AMP"}, }) cfg.Set("pluralizeListTitles", false) cfg.Set("canonifyURLs", false) - configs, err := loadTestConfigFromProvider(cfg) - if err != nil { - t.Fatal(err) - } - + cfg.Set("blackfriday", + map[string]interface{}{ + "sourceRelativeLinksProjectFolder": "/docs"}) writeSourcesToSource(t, "content", fs, sources...) - return buildSingleSite(t, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{}) + return buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + } func TestRefLinking(t *testing.T) { t.Parallel() site := setupLinkingMockSite(t) - currentPage := site.getPageOldVersion(kinds.KindPage, "level2/level3/start.md") + currentPage := findPage(site, "level2/level3/index.md") if currentPage == nil { t.Fatalf("failed to find current page in site") } @@ -949,317 +954,147 @@ func TestRefLinking(t *testing.T) { relative bool expected string }{ - // different refs resolving to the same unique filename: - {"/level2/unique.md", "", true, "/level2/unique/"}, - {"../unique.md", "", true, "/level2/unique/"}, {"unique.md", "", true, "/level2/unique/"}, - {"level2/common.md", "", true, "/level2/common/"}, {"3-root.md", "", true, "/level2/level3/3-root/"}, - {"../..", "", true, "/"}, - - // different refs resolving to the same ambiguous top-level filename: - {"../../common.md", "", true, "/common/"}, - {"/common.md", "", true, "/common/"}, - - // different refs resolving to the same ambiguous level-2 filename: - {"/level2/common.md", "", true, "/level2/common/"}, - {"../common.md", "", true, "/level2/common/"}, - {"common.md", "", true, "/level2/level3/common/"}, - - // different refs resolving to the same section: - {"/level2", "", true, "/level2/"}, - {"..", "", true, "/level2/"}, - {"../", "", true, "/level2/"}, - - // different refs resolving to the same subsection: - {"/level2/level3", "", true, "/level2/level3/"}, - {"/level2/level3/_index.md", "", true, "/level2/level3/"}, - {".", "", true, "/level2/level3/"}, - {"./", "", true, "/level2/level3/"}, - - {"embedded.dot.md", "", true, "/level2/level3/embedded.dot/"}, - - // test empty link, as well as fragment only link - {"", "", true, ""}, + {"level2/level3/index.md", "amp", true, "/amp/level2/level3/"}, + {"level2/index.md", "amp", false, "http://auth/amp/level2/"}, } { - t.Run(fmt.Sprintf("t%dt", i), func(t *testing.T) { - checkLinkCase(site, test.link, currentPage, test.relative, test.outputFormat, test.expected, t, i) - - // make sure fragment links are also handled - checkLinkCase(site, test.link+"#intro", currentPage, test.relative, test.outputFormat, test.expected+"#intro", t, i) - }) + if out, err := site.Info.refLink(test.link, currentPage, test.relative, test.outputFormat); err != nil || out != test.expected { + t.Errorf("[%d] Expected %s to resolve to (%s), got (%s) - error: %s", i, test.link, test.expected, out, err) + } } // TODO: and then the failure cases. } -func TestRelRefWithTrailingSlash(t *testing.T) { - files := ` --- hugo.toml -- --- content/docs/5.3/examples/_index.md -- ---- -title: "Examples" ---- --- content/_index.md -- ---- -title: "Home" ---- +func TestSourceRelativeLinksing(t *testing.T) { + t.Parallel() + site := setupLinkingMockSite(t) -Examples: {{< relref "/docs/5.3/examples/" >}} --- layouts/home.html -- -Content: {{ .Content }}| -` + type resultMap map[string]string - b := Test(t, files) - - b.AssertFileContent("public/index.html", "Examples: /docs/5.3/examples/") -} - -func checkLinkCase(site *Site, link string, currentPage page.Page, relative bool, outputFormat string, expected string, t *testing.T, i int) { - t.Helper() - if out, err := site.refLink(link, currentPage, relative, outputFormat); err != nil || out != expected { - t.Fatalf("[%d] Expected %q from %q to resolve to %q, got %q - error: %s", i, link, currentPage.Path(), expected, out, err) - } -} - -// https://github.com/gohugoio/hugo/issues/6952 -func TestRefIssues(t *testing.T) { - b := newTestSitesBuilder(t) - b.WithContent( - "post/b1/index.md", "---\ntitle: pb1\n---\nRef: {{< ref \"b2\" >}}", - "post/b2/index.md", "---\ntitle: pb2\n---\n", - "post/nested-a/content-a.md", "---\ntitle: ca\n---\n{{< ref \"content-b\" >}}", - "post/nested-b/content-b.md", "---\ntitle: ca\n---\n", - ) - b.WithTemplates("index.html", `Home`) - b.WithTemplates("_default/single.html", `Content: {{ .Content }}`) - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/post/b1/index.html", `Content:

    Ref: http://example.com/post/b2/

    `) - b.AssertFileContent("public/post/nested-a/content-a/index.html", `Content: http://example.com/post/nested-b/content-b/`) -} - -func TestClassCollector(t *testing.T) { - for _, minify := range []bool{false, true} { - t.Run(fmt.Sprintf("minify-%t", minify), func(t *testing.T) { - statsFilename := "hugo_stats.json" - defer os.Remove(statsFilename) - - b := newTestSitesBuilder(t) - b.WithConfigFile("toml", fmt.Sprintf(` - - -minify = %t - -[build] - writeStats = true - -`, minify)) - - b.WithTemplates("index.html", ` - -
    Foo
    - -Some text. - -
    Foo
    - -FOO - - {{ .Title }} - - -`) - - b.WithContent("p1.md", "") - - b.Build(BuildCfg{}) - - b.AssertFileContent("hugo_stats.json", ` - { - "htmlElements": { - "tags": [ - "a", - "div", - "span" - ], - "classes": [ - "a", - "b", - "c", - "d", - "e", - "hover:text-gradient", - "[&>p]:text-red-600", - "inline-block", - "lowercase", - "pb-1", - "px-3", - "rounded", - "text-base", - "z" - ], - "ids": [ - "el1", - "el2" - ] - } - } -`) - }) - } -} - -func TestClassCollectorConfigWriteStats(t *testing.T) { - r := func(writeStatsConfig string) *IntegrationTestBuilder { - files := ` --- hugo.toml -- -WRITE_STATS_CONFIG --- layouts/_default/list.html -- -
    Foo
    - -` - files = strings.Replace(files, "WRITE_STATS_CONFIG", writeStatsConfig, 1) - - b := NewIntegrationTestBuilder( - IntegrationTestConfig{ - T: t, - TxtarString: files, - NeedsOsFS: true, - }, - ).Build() - - return b + okresults := map[string]resultMap{ + "index.md": map[string]string{ + "/docs/rootfile.md": "/rootfile/", + "rootfile.md": "/rootfile/", + // See #3396 -- this may potentially be ambiguous (i.e. name conflict with home page). + // But the user have chosen so. This index.md patterns is more relevant in /sub-folders. + "index.md": "/", + "level2/2-root.md": "/level2/2-root/", + "/docs/level2/2-root.md": "/level2/2-root/", + "level2/level3/3-root.md": "/level2/level3/3-root/", + "/docs/level2/level3/3-root.md": "/level2/level3/3-root/", + "/docs/level2/2-root/": "/level2/2-root/", + "/docs/level2/2-root": "/level2/2-root/", + "/level2/2-root/": "/level2/2-root/", + "/level2/2-root": "/level2/2-root/", + }, "rootfile.md": map[string]string{ + "/docs/rootfile.md": "/rootfile/", + "rootfile.md": "/rootfile/", + "level2/2-root.md": "/level2/2-root/", + "/docs/level2/2-root.md": "/level2/2-root/", + "level2/level3/3-root.md": "/level2/level3/3-root/", + "/docs/level2/level3/3-root.md": "/level2/level3/3-root/", + }, "level2/2-root.md": map[string]string{ + "../rootfile.md": "/rootfile/", + "/docs/rootfile.md": "/rootfile/", + "2-root.md": "/level2/2-root/", + "../level2/2-root.md": "/level2/2-root/", + "./2-root.md": "/level2/2-root/", + "/docs/level2/2-root.md": "/level2/2-root/", + "level3/3-root.md": "/level2/level3/3-root/", + "../level2/level3/3-root.md": "/level2/level3/3-root/", + "/docs/level2/level3/3-root.md": "/level2/level3/3-root/", + }, "level2/index.md": map[string]string{ + "../rootfile.md": "/rootfile/", + "/docs/rootfile.md": "/rootfile/", + "2-root.md": "/level2/2-root/", + "../level2/2-root.md": "/level2/2-root/", + "./2-root.md": "/level2/2-root/", + "/docs/level2/2-root.md": "/level2/2-root/", + "level3/3-root.md": "/level2/level3/3-root/", + "../level2/level3/3-root.md": "/level2/level3/3-root/", + "/docs/level2/level3/3-root.md": "/level2/level3/3-root/", + }, "level2/level3/3-root.md": map[string]string{ + "../../rootfile.md": "/rootfile/", + "/docs/rootfile.md": "/rootfile/", + "../2-root.md": "/level2/2-root/", + "/docs/level2/2-root.md": "/level2/2-root/", + "3-root.md": "/level2/level3/3-root/", + "./3-root.md": "/level2/level3/3-root/", + "/docs/level2/level3/3-root.md": "/level2/level3/3-root/", + }, "level2/level3/index.md": map[string]string{ + "../../rootfile.md": "/rootfile/", + "/docs/rootfile.md": "/rootfile/", + "../2-root.md": "/level2/2-root/", + "/docs/level2/2-root.md": "/level2/2-root/", + "3-root.md": "/level2/level3/3-root/", + "./3-root.md": "/level2/level3/3-root/", + "/docs/level2/level3/3-root.md": "/level2/level3/3-root/", + }, } - // Legacy config. - b := r(` -[build] -writeStats = true -`) - - b.AssertFileContent("hugo_stats.json", "myclass", "div", "myid") - - b = r(` -[build] -writeStats = false - `) - - b.AssertFileExists("public/hugo_stats.json", false) - - b = r(` -[build.buildStats] -enable = true -`) - - b.AssertFileContent("hugo_stats.json", "myclass", "div", "myid") - - b = r(` -[build.buildStats] -enable = true -disableids = true -`) - - b.AssertFileContent("hugo_stats.json", "myclass", "div", "! myid") - - b = r(` -[build.buildStats] -enable = true -disableclasses = true -`) - - b.AssertFileContent("hugo_stats.json", "! myclass", "div", "myid") - - b = r(` -[build.buildStats] -enable = true -disabletags = true - `) - - b.AssertFileContent("hugo_stats.json", "myclass", "! div", "myid") - - b = r(` -[build.buildStats] -enable = true -disabletags = true -disableclasses = true - `) - - b.AssertFileContent("hugo_stats.json", "! myclass", "! div", "myid") - - b = r(` -[build.buildStats] -enable = false - `) - b.AssertFileExists("public/hugo_stats.json", false) -} - -func TestClassCollectorStress(t *testing.T) { - statsFilename := "hugo_stats.json" - defer os.Remove(statsFilename) - - b := newTestSitesBuilder(t) - b.WithConfigFile("toml", ` - -disableKinds = ["home", "section", "term", "taxonomy" ] - -[languages] -[languages.en] -[languages.nb] -[languages.no] -[languages.sv] - - -[build] - writeStats = true - -`) - - b.WithTemplates("_default/single.html", ` -
    Foo
    - -Some text. - -{{ $n := index (shuffle (seq 1 20)) 0 }} - -{{ "Foo" | strings.Repeat $n | safeHTML }} - -
    -ABC. -
    - -
    - -{{ $n := index (shuffle (seq 1 5)) 0 }} - -{{ "
    " | safeHTML }} - -`) - - for _, lang := range []string{"en", "nb", "no", "sv"} { - for i := 100; i <= 999; i++ { - b.WithContent(fmt.Sprintf("p%d.%s.md", i, lang), fmt.Sprintf("---\ntitle: p%s%d\n---", lang, i)) + for currentFile, results := range okresults { + currentPage := findPage(site, currentFile) + if currentPage == nil { + t.Fatalf("failed to find current page in site") + } + for link, url := range results { + if out, err := site.Info.SourceRelativeLink(link, currentPage); err != nil || out != url { + t.Errorf("Expected %s to resolve to (%s), got (%s) - error: %s", link, url, out, err) + } else { + //t.Logf("tested ok %s maps to %s", link, out) + } } } + // TODO: and then the failure cases. + // "https://docker.com": "", + // site_test.go:1094: Expected https://docker.com to resolve to (), got () - error: Not a plain filepath link (https://docker.com) - b.Build(BuildCfg{}) +} - contentMem := b.FileContent(statsFilename) - cb, err := os.ReadFile(statsFilename) - b.Assert(err, qt.IsNil) - contentFile := string(cb) +func TestSourceRelativeLinkFileing(t *testing.T) { + t.Parallel() + site := setupLinkingMockSite(t) - for _, content := range []string{contentMem, contentFile} { + type resultMap map[string]string - stats := &publisher.PublishStats{} - b.Assert(json.Unmarshal([]byte(content), stats), qt.IsNil) + okresults := map[string]resultMap{ + "index.md": map[string]string{ + "/root-image.png": "/root-image.png", + "root-image.png": "/root-image.png", + }, "rootfile.md": map[string]string{ + "/root-image.png": "/root-image.png", + }, "level2/2-root.md": map[string]string{ + "/root-image.png": "/root-image.png", + "common.png": "/level2/common.png", + }, "level2/index.md": map[string]string{ + "/root-image.png": "/root-image.png", + "common.png": "/level2/common.png", + "./common.png": "/level2/common.png", + }, "level2/level3/3-root.md": map[string]string{ + "/root-image.png": "/root-image.png", + "common.png": "/level2/level3/common.png", + "../common.png": "/level2/common.png", + }, "level2/level3/index.md": map[string]string{ + "/root-image.png": "/root-image.png", + "common.png": "/level2/level3/common.png", + "../common.png": "/level2/common.png", + }, + } - els := stats.HTMLElements - - b.Assert(els.Classes, qt.HasLen, 3606) // (4 * 900) + 4 +2 - b.Assert(els.Tags, qt.HasLen, 8) - b.Assert(els.IDs, qt.HasLen, 1) + for currentFile, results := range okresults { + currentPage := findPage(site, currentFile) + if currentPage == nil { + t.Fatalf("failed to find current page in site") + } + for link, url := range results { + if out, err := site.Info.SourceRelativeLinkFile(link, currentPage); err != nil || out != url { + t.Errorf("Expected %s to resolve to (%s), got (%s) - error: %s", link, url, out, err) + } else { + //t.Logf("tested ok %s maps to %s", link, out) + } + } } } diff --git a/hugolib/site_url_test.go b/hugolib/site_url_test.go index 091251f80..272c78c7e 100644 --- a/hugolib/site_url_test.go +++ b/hugolib/site_url_test.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Hugo Authors. All rights reserved. +// Copyright 2016 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,122 +14,77 @@ package hugolib import ( - "fmt" "path/filepath" "testing" - qt "github.com/frankban/quicktest" + "html/template" + "github.com/gohugoio/hugo/deps" - "github.com/gohugoio/hugo/resources/kinds" + "github.com/gohugoio/hugo/source" + "github.com/stretchr/testify/require" ) -func TestUglyURLsPerSection(t *testing.T) { - t.Parallel() +const slugDoc1 = "---\ntitle: slug doc 1\nslug: slug-doc-1\naliases:\n - sd1/foo/\n - sd2\n - sd3/\n - sd4.html\n---\nslug doc 1 content\n" - c := qt.New(t) - - const dt = `--- -title: Do not go gentle into that good night +const slugDoc2 = `--- +title: slug doc 2 +slug: slug-doc-2 --- - -Wild men who caught and sang the sun in flight, -And learn, too late, they grieved it on its way, -Do not go gentle into that good night. - +slug doc 2 content ` - cfg, fs := newTestCfg() - - cfg.Set("uglyURLs", map[string]bool{ - "sect2": true, - }) - configs, err := loadTestConfigFromProvider(cfg) - c.Assert(err, qt.IsNil) - - writeSource(t, fs, filepath.Join("content", "sect1", "p1.md"), dt) - writeSource(t, fs, filepath.Join("content", "sect2", "p2.md"), dt) - - s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{SkipRender: true}) - - c.Assert(len(s.RegularPages()), qt.Equals, 2) - - notUgly := s.getPageOldVersion(kinds.KindPage, "sect1/p1.md") - c.Assert(notUgly, qt.Not(qt.IsNil)) - c.Assert(notUgly.Section(), qt.Equals, "sect1") - c.Assert(notUgly.RelPermalink(), qt.Equals, "/sect1/p1/") - - ugly := s.getPageOldVersion(kinds.KindPage, "sect2/p2.md") - c.Assert(ugly, qt.Not(qt.IsNil)) - c.Assert(ugly.Section(), qt.Equals, "sect2") - c.Assert(ugly.RelPermalink(), qt.Equals, "/sect2/p2.html") +var urlFakeSource = []source.ByteSource{ + {Name: filepath.FromSlash("content/blue/doc1.md"), Content: []byte(slugDoc1)}, + {Name: filepath.FromSlash("content/blue/doc2.md"), Content: []byte(slugDoc2)}, } -func TestSectionWithURLInFrontMatter(t *testing.T) { +// Issue #1105 +func TestShouldNotAddTrailingSlashToBaseURL(t *testing.T) { t.Parallel() + for i, this := range []struct { + in string + expected string + }{ + {"http://base.com/", "http://base.com/"}, + {"http://base.com/sub/", "http://base.com/sub/"}, + {"http://base.com/sub", "http://base.com/sub"}, + {"http://base.com", "http://base.com"}} { - c := qt.New(t) + cfg, fs := newTestCfg() + cfg.Set("baseURL", this.in) + d := deps.DepsCfg{Cfg: cfg, Fs: fs} + s, err := NewSiteForCfg(d) + require.NoError(t, err) + s.initializeSiteInfo() - const st = `--- -title: Do not go gentle into that good night -url: %s ---- - -Wild men who caught and sang the sun in flight, -And learn, too late, they grieved it on its way, -Do not go gentle into that good night. - -` - - const pt = `--- -title: Wild men who caught and sang the sun in flight ---- - -Wild men who caught and sang the sun in flight, -And learn, too late, they grieved it on its way, -Do not go gentle into that good night. - -` + if s.Info.BaseURL() != template.URL(this.expected) { + t.Errorf("[%d] got %s expected %s", i, s.Info.BaseURL(), this.expected) + } + } +} +func TestPageCount(t *testing.T) { + t.Parallel() cfg, fs := newTestCfg() - cfg.Set("pagination.pagerSize", 1) - th, configs := newTestHelperFromProvider(cfg, fs, t) + cfg.Set("uglyURLs", false) + cfg.Set("paginate", 10) - writeSource(t, fs, filepath.Join("content", "sect1", "_index.md"), fmt.Sprintf(st, "/ss1/")) - writeSource(t, fs, filepath.Join("content", "sect2", "_index.md"), fmt.Sprintf(st, "/ss2/")) + writeSourcesToSource(t, "", fs, urlFakeSource...) + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) - for i := range 5 { - writeSource(t, fs, filepath.Join("content", "sect1", fmt.Sprintf("p%d.md", i+1)), pt) - writeSource(t, fs, filepath.Join("content", "sect2", fmt.Sprintf("p%d.md", i+1)), pt) + _, err := s.Fs.Destination.Open("public/blue") + if err != nil { + t.Errorf("No indexed rendered.") } - writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), "{{.Content}}") - writeSource(t, fs, filepath.Join("layouts", "_default", "list.html"), - "P{{.Paginator.PageNumber}}|URL: {{.Paginator.URL}}|{{ if .Paginator.HasNext }}Next: {{.Paginator.Next.URL }}{{ end }}") - - s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{}) - - c.Assert(len(s.RegularPages()), qt.Equals, 10) - - sect1 := s.getPageOldVersion(kinds.KindSection, "sect1") - c.Assert(sect1, qt.Not(qt.IsNil)) - c.Assert(sect1.RelPermalink(), qt.Equals, "/ss1/") - th.assertFileContent(filepath.Join("public", "ss1", "index.html"), "P1|URL: /ss1/|Next: /ss1/page/2/") - th.assertFileContent(filepath.Join("public", "ss1", "page", "2", "index.html"), "P2|URL: /ss1/page/2/|Next: /ss1/page/3/") -} - -func TestSectionsEntries(t *testing.T) { - files := ` --- hugo.toml -- --- content/withfile/_index.md -- --- content/withoutfile/p1.md -- --- layouts/_default/list.html -- -SectionsEntries: {{ .SectionsEntries }} - - -` - - b := Test(t, files) - - b.AssertFileContent("public/withfile/index.html", "SectionsEntries: [withfile]") - b.AssertFileContent("public/withoutfile/index.html", "SectionsEntries: [withoutfile]") + for _, pth := range []string{ + "public/sd1/foo/index.html", + "public/sd2/index.html", + "public/sd3/index.html", + "public/sd4.html", + } { + if _, err := s.Fs.Destination.Open(filepath.FromSlash(pth)); err != nil { + t.Errorf("No alias rendered: %s", pth) + } + } } diff --git a/hugolib/sitemap.go b/hugolib/sitemap.go new file mode 100644 index 000000000..64d6f5b7a --- /dev/null +++ b/hugolib/sitemap.go @@ -0,0 +1,45 @@ +// Copyright 2015 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 hugolib + +import ( + "github.com/spf13/cast" + jww "github.com/spf13/jwalterweatherman" +) + +// Sitemap configures the sitemap to be generated. +type Sitemap struct { + ChangeFreq string + Priority float64 + Filename string +} + +func parseSitemap(input map[string]interface{}) Sitemap { + sitemap := Sitemap{Priority: -1, Filename: "sitemap.xml"} + + for key, value := range input { + switch key { + case "changefreq": + sitemap.ChangeFreq = cast.ToString(value) + case "priority": + sitemap.Priority = cast.ToFloat64(value) + case "filename": + sitemap.Filename = cast.ToString(value) + default: + jww.WARN.Printf("Unknown Sitemap field: %s\n", key) + } + } + + return sitemap +} diff --git a/hugolib/sitemap_test.go b/hugolib/sitemap_test.go index 922ecbc12..002f772d8 100644 --- a/hugolib/sitemap_test.go +++ b/hugolib/sitemap_test.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Hugo Authors. All rights reserved. +// Copyright 2016 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,215 +14,89 @@ package hugolib import ( - "reflect" - "strings" "testing" - "github.com/gohugoio/hugo/config" + "reflect" + + "github.com/stretchr/testify/require" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/tpl" ) -func TestSitemapBasic(t *testing.T) { +const sitemapTemplate = ` + {{ range .Data.Pages }} + + {{ .Permalink }}{{ if not .Lastmod.IsZero }} + {{ safeHTML ( .Lastmod.Format "2006-01-02T15:04:05-07:00" ) }}{{ end }}{{ with .Sitemap.ChangeFreq }} + {{ . }}{{ end }}{{ if ge .Sitemap.Priority 0.0 }} + {{ .Sitemap.Priority }}{{ end }} + + {{ end }} +` + +func TestSitemapOutput(t *testing.T) { t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableKinds = ["term", "taxonomy"] --- content/sect/doc1.md -- ---- -title: doc1 ---- -Doc1 --- content/sect/doc2.md -- ---- -title: doc2 ---- -Doc2 -` - - b := Test(t, files) - - b.AssertFileContent("public/sitemap.xml", " https://example.com/sect/doc1/", "doc2") + for _, internal := range []bool{false, true} { + doTestSitemapOutput(t, internal) + } } -func TestSitemapMultilingual(t *testing.T) { - t.Parallel() +func doTestSitemapOutput(t *testing.T, internal bool) { - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableKinds = ["term", "taxonomy"] -defaultContentLanguage = "en" -[languages] -[languages.en] -weight = 1 -languageName = "English" -[languages.nn] -weight = 2 -languageName = "Nynorsk" --- content/sect/doc1.md -- ---- -title: doc1 ---- -Doc1 --- content/sect/doc2.md -- ---- -title: doc2 ---- -Doc2 --- content/sect/doc2.nn.md -- ---- -title: doc2 ---- -Doc2 -` + cfg, fs := newTestCfg() + cfg.Set("baseURL", "http://auth/bub/") - b := Test(t, files) + depsCfg := deps.DepsCfg{Fs: fs, Cfg: cfg} - b.AssertFileContent("public/sitemap.xml", "https://example.com/en/sitemap.xml", "https://example.com/nn/sitemap.xml") - b.AssertFileContent("public/en/sitemap.xml", " https://example.com/sect/doc1/", "doc2") - b.AssertFileContent("public/nn/sitemap.xml", " https://example.com/nn/sect/doc2/") -} + depsCfg.WithTemplate = func(templ tpl.TemplateHandler) error { + if !internal { + templ.AddTemplate("sitemap.xml", sitemapTemplate) + } -// https://github.com/gohugoio/hugo/issues/5910 -func TestSitemapOutputFormats(t *testing.T) { - t.Parallel() + // We want to check that the 404 page is not included in the sitemap + // output. This template should have no effect either way, but include + // it for the clarity. + templ.AddTemplate("404.html", "Not found") + return nil + } - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableKinds = ["term", "taxonomy"] --- content/blog/html-amp.md -- ---- -Title: AMP and HTML -outputs: [ "html", "amp" ] ---- + writeSourcesToSource(t, "content", fs, weightedSources...) + s := buildSingleSite(t, depsCfg, BuildCfg{}) + th := testHelper{s.Cfg, s.Fs, t} + outputSitemap := "public/sitemap.xml" -` + th.assertFileContent(outputSitemap, + // Regular page + " http://auth/bub/sect/doc1/", + // Home page + "http://auth/bub/", + // Section + "http://auth/bub/sect/", + // Tax terms + "http://auth/bub/categories/", + // Tax list + "http://auth/bub/categories/hugo/", + ) - b := Test(t, files) + content := readDestination(th.T, th.Fs, outputSitemap) + require.NotContains(t, content, "404") - // Should link to the HTML version. - b.AssertFileContent("public/sitemap.xml", " https://example.com/blog/html-amp/") } func TestParseSitemap(t *testing.T) { t.Parallel() - expected := config.SitemapConfig{ChangeFreq: "3", Disable: true, Filename: "doo.xml", Priority: 3.0} - input := map[string]any{ + expected := Sitemap{Priority: 3.0, Filename: "doo.xml", ChangeFreq: "3"} + input := map[string]interface{}{ "changefreq": "3", - "disable": true, - "filename": "doo.xml", "priority": 3.0, + "filename": "doo.xml", "unknown": "ignore", } - result, err := config.DecodeSitemap(config.SitemapConfig{}, input) - if err != nil { - t.Fatalf("Failed to parse sitemap: %s", err) - } + result := parseSitemap(input) if !reflect.DeepEqual(expected, result) { t.Errorf("Got \n%v expected \n%v", result, expected) } -} - -func TestSitemapShouldNotUseListXML(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableKinds = ["term", "taxonomy"] -[languages] -[languages.en] -weight = 1 -languageName = "English" -[languages.nn] -weight = 2 --- layouts/list.xml -- -Site: {{ .Site.Title }}| --- layouts/home -- -Home. - -` - - b := Test(t, files) - - b.AssertFileContent("public/sitemap.xml", "https://example.com/en/sitemap.xml") -} - -func TestSitemapAndContentBundleNamedSitemap(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ['home','rss','section','taxonomy','term'] --- layouts/_default/single.html -- -layouts/_default/single.html --- layouts/sitemap/single.html -- -layouts/sitemap/single.html --- content/sitemap/index.md -- ---- -title: My sitemap -type: sitemap ---- -` - - b := Test(t, files) - - b.AssertFileExists("public/sitemap.xml", true) -} - -// Issue 12266 -func TestSitemapIssue12266(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = 'https://example.org/' -disableKinds = ['rss','taxonomy','term'] -defaultContentLanguage = 'en' -defaultContentLanguageInSubdir = true -[languages.de] -[languages.en] - ` - - // Test A: multilingual with defaultContentLanguageInSubdir = true - b := Test(t, files) - - b.AssertFileContent("public/sitemap.xml", - "https://example.org/de/sitemap.xml", - "https://example.org/en/sitemap.xml", - ) - b.AssertFileContent("public/de/sitemap.xml", "https://example.org/de/") - b.AssertFileContent("public/en/sitemap.xml", "https://example.org/en/") - - // Test B: multilingual with defaultContentLanguageInSubdir = false - files = strings.ReplaceAll(files, "defaultContentLanguageInSubdir = true", "defaultContentLanguageInSubdir = false") - - b = Test(t, files) - - b.AssertFileContent("public/sitemap.xml", - "https://example.org/de/sitemap.xml", - "https://example.org/en/sitemap.xml", - ) - b.AssertFileContent("public/de/sitemap.xml", "https://example.org/de/") - b.AssertFileContent("public/en/sitemap.xml", "https://example.org/") - - // Test C: monolingual with defaultContentLanguageInSubdir = false - files = strings.ReplaceAll(files, "[languages.de]", "") - files = strings.ReplaceAll(files, "[languages.en]", "") - - b = Test(t, files) - - b.AssertFileExists("public/en/sitemap.xml", false) - b.AssertFileContent("public/sitemap.xml", "https://example.org/") - - // Test D: monolingual with defaultContentLanguageInSubdir = true - files = strings.ReplaceAll(files, "defaultContentLanguageInSubdir = false", "defaultContentLanguageInSubdir = true") - - b = Test(t, files) - - b.AssertFileContent("public/sitemap.xml", "https://example.org/en/sitemap.xml") - b.AssertFileContent("public/en/sitemap.xml", "https://example.org/en/") + } diff --git a/hugolib/taxonomy.go b/hugolib/taxonomy.go new file mode 100644 index 000000000..35e0795e5 --- /dev/null +++ b/hugolib/taxonomy.go @@ -0,0 +1,224 @@ +// Copyright 2015 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 hugolib + +import ( + "fmt" + "sort" +) + +// The TaxonomyList is a list of all taxonomies and their values +// e.g. List['tags'] => TagTaxonomy (from above) +type TaxonomyList map[string]Taxonomy + +func (tl TaxonomyList) String() string { + return fmt.Sprintf("TaxonomyList(%d)", len(tl)) +} + +// A Taxonomy is a map of keywords to a list of pages. +// For example +// TagTaxonomy['technology'] = WeightedPages +// TagTaxonomy['go'] = WeightedPages2 +type Taxonomy map[string]WeightedPages + +// WeightedPages is a list of Pages with their corresponding (and relative) weight +// [{Weight: 30, Page: *1}, {Weight: 40, Page: *2}] +type WeightedPages []WeightedPage + +// A WeightedPage is a Page with a weight. +type WeightedPage struct { + Weight int + *Page +} + +func (w WeightedPage) String() string { + return fmt.Sprintf("WeightedPage(%d,%q)", w.Weight, w.Page.Title) +} + +// OrderedTaxonomy is another representation of an Taxonomy using an array rather than a map. +// Important because you can't order a map. +type OrderedTaxonomy []OrderedTaxonomyEntry + +// OrderedTaxonomyEntry is similar to an element of a Taxonomy, but with the key embedded (as name) +// e.g: {Name: Technology, WeightedPages: Taxonomyedpages} +type OrderedTaxonomyEntry struct { + Name string + WeightedPages WeightedPages +} + +// Get the weighted pages for the given key. +func (i Taxonomy) Get(key string) WeightedPages { + return i[key] +} + +// Count the weighted pages for the given key. +func (i Taxonomy) Count(key string) int { return len(i[key]) } + +func (i Taxonomy) add(key string, w WeightedPage) { + i[key] = append(i[key], w) +} + +// TaxonomyArray returns an ordered taxonomy with a non defined order. +func (i Taxonomy) TaxonomyArray() OrderedTaxonomy { + ies := make([]OrderedTaxonomyEntry, len(i)) + count := 0 + for k, v := range i { + ies[count] = OrderedTaxonomyEntry{Name: k, WeightedPages: v} + count++ + } + return ies +} + +// Alphabetical returns an ordered taxonomy sorted by key name. +func (i Taxonomy) Alphabetical() OrderedTaxonomy { + name := func(i1, i2 *OrderedTaxonomyEntry) bool { + return i1.Name < i2.Name + } + + ia := i.TaxonomyArray() + oiBy(name).Sort(ia) + return ia +} + +// ByCount returns an ordered taxonomy sorted by # of pages per key. +// If taxonomies have the same # of pages, sort them alphabetical +func (i Taxonomy) ByCount() OrderedTaxonomy { + count := func(i1, i2 *OrderedTaxonomyEntry) bool { + li1 := len(i1.WeightedPages) + li2 := len(i2.WeightedPages) + + if li1 == li2 { + return i1.Name < i2.Name + } + return li1 > li2 + } + + ia := i.TaxonomyArray() + oiBy(count).Sort(ia) + return ia +} + +// Pages returns the Pages for this taxonomy. +func (ie OrderedTaxonomyEntry) Pages() Pages { + return ie.WeightedPages.Pages() +} + +// Count returns the count the pages in this taxonomy. +func (ie OrderedTaxonomyEntry) Count() int { + return len(ie.WeightedPages) +} + +// Term returns the name given to this taxonomy. +func (ie OrderedTaxonomyEntry) Term() string { + return ie.Name +} + +// Reverse reverses the order of the entries in this taxonomy. +func (t OrderedTaxonomy) Reverse() OrderedTaxonomy { + for i, j := 0, len(t)-1; i < j; i, j = i+1, j-1 { + t[i], t[j] = t[j], t[i] + } + + return t +} + +// A type to implement the sort interface for TaxonomyEntries. +type orderedTaxonomySorter struct { + taxonomy OrderedTaxonomy + by oiBy +} + +// Closure used in the Sort.Less method. +type oiBy func(i1, i2 *OrderedTaxonomyEntry) bool + +func (by oiBy) Sort(taxonomy OrderedTaxonomy) { + ps := &orderedTaxonomySorter{ + taxonomy: taxonomy, + by: by, // The Sort method's receiver is the function (closure) that defines the sort order. + } + sort.Stable(ps) +} + +// Len is part of sort.Interface. +func (s *orderedTaxonomySorter) Len() int { + return len(s.taxonomy) +} + +// Swap is part of sort.Interface. +func (s *orderedTaxonomySorter) Swap(i, j int) { + s.taxonomy[i], s.taxonomy[j] = s.taxonomy[j], s.taxonomy[i] +} + +// Less is part of sort.Interface. It is implemented by calling the "by" closure in the sorter. +func (s *orderedTaxonomySorter) Less(i, j int) bool { + return s.by(&s.taxonomy[i], &s.taxonomy[j]) +} + +// Pages returns the Pages in this weighted page set. +func (wp WeightedPages) Pages() Pages { + pages := make(Pages, len(wp)) + for i := range wp { + pages[i] = wp[i].Page + } + return pages +} + +// Prev returns the previous Page relative to the given Page in +// this weighted page set. +func (wp WeightedPages) Prev(cur *Page) *Page { + for x, c := range wp { + if c.Page.UniqueID() == cur.UniqueID() { + if x == 0 { + return wp[len(wp)-1].Page + } + return wp[x-1].Page + } + } + return nil +} + +// Next returns the next Page relative to the given Page in +// this weighted page set. +func (wp WeightedPages) Next(cur *Page) *Page { + for x, c := range wp { + if c.Page.UniqueID() == cur.UniqueID() { + if x < len(wp)-1 { + return wp[x+1].Page + } + return wp[0].Page + } + } + return nil +} + +func (wp WeightedPages) Len() int { return len(wp) } +func (wp WeightedPages) Swap(i, j int) { wp[i], wp[j] = wp[j], wp[i] } + +// Sort stable sorts this weighted page set. +func (wp WeightedPages) Sort() { sort.Stable(wp) } + +// Count returns the number of pages in this weighted page set. +func (wp WeightedPages) Count() int { return len(wp) } + +func (wp WeightedPages) Less(i, j int) bool { + if wp[i].Weight == wp[j].Weight { + if wp[i].Page.Date.Equal(wp[j].Page.Date) { + return wp[i].Page.Title < wp[j].Page.Title + } + return wp[i].Page.Date.After(wp[i].Page.Date) + } + return wp[i].Weight < wp[j].Weight +} + +// TODO mimic PagesSorter for WeightedPages diff --git a/hugolib/taxonomy_test.go b/hugolib/taxonomy_test.go index 7aeaa780c..4f8717d72 100644 --- a/hugolib/taxonomy_test.go +++ b/hugolib/taxonomy_test.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Hugo Authors. All rights reserved. +// Copyright 2015 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. @@ -20,79 +20,63 @@ import ( "strings" "testing" - "github.com/gohugoio/hugo/resources/kinds" - "github.com/gohugoio/hugo/resources/page" - - qt "github.com/frankban/quicktest" + "github.com/stretchr/testify/require" "github.com/gohugoio/hugo/deps" ) -func TestTaxonomiesCountOrder(t *testing.T) { +func TestByCountOrderOfTaxonomies(t *testing.T) { t.Parallel() - c := qt.New(t) - taxonomies := make(map[string]string) + taxonomies["tag"] = "tags" taxonomies["category"] = "categories" cfg, fs := newTestCfg() - cfg.Set("titleCaseStyle", "none") cfg.Set("taxonomies", taxonomies) - configs, err := loadTestConfigFromProvider(cfg) - c.Assert(err, qt.IsNil) - const pageContent = `--- -tags: ['a', 'B', 'c'] -categories: 'd' ---- -YAML frontmatter with tags and categories taxonomy.` + writeSource(t, fs, filepath.Join("content", "page.md"), pageYamlWithTaxonomiesA) - writeSource(t, fs, filepath.Join("content", "page.md"), pageContent) - - s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{}) + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) st := make([]string, 0) - for _, t := range s.Taxonomies()["tags"].ByCount() { - st = append(st, t.Page().Title()+":"+t.Name) + for _, t := range s.Taxonomies["tags"].ByCount() { + st = append(st, t.Name) } - expect := []string{"a:a", "B:b", "c:c"} - - if !reflect.DeepEqual(st, expect) { - t.Fatalf("ordered taxonomies mismatch, expected\n%v\ngot\n%q", expect, st) + if !reflect.DeepEqual(st, []string{"a", "b", "c"}) { + t.Fatalf("ordered taxonomies do not match [a, b, c]. Got: %s", st) } } +// func TestTaxonomiesWithAndWithoutContentFile(t *testing.T) { for _, uglyURLs := range []bool{false, true} { - uglyURLs := uglyURLs - t.Run(fmt.Sprintf("uglyURLs=%t", uglyURLs), func(t *testing.T) { - t.Parallel() - doTestTaxonomiesWithAndWithoutContentFile(t, uglyURLs) - }) + for _, preserveTaxonomyNames := range []bool{false, true} { + t.Run(fmt.Sprintf("uglyURLs=%t,preserveTaxonomyNames=%t", uglyURLs, preserveTaxonomyNames), func(t *testing.T) { + doTestTaxonomiesWithAndWithoutContentFile(t, preserveTaxonomyNames, uglyURLs) + }) + } } } -func doTestTaxonomiesWithAndWithoutContentFile(t *testing.T, uglyURLs bool) { - t.Helper() +func doTestTaxonomiesWithAndWithoutContentFile(t *testing.T, preserveTaxonomyNames, uglyURLs bool) { + t.Parallel() siteConfig := ` baseURL = "http://example.com/blog" -titleCaseStyle = "firstupper" +preserveTaxonomyNames = %t uglyURLs = %t + +paginate = 1 defaultContentLanguage = "en" -[pagination] -pagerSize = 1 + [Taxonomies] tag = "tags" category = "categories" other = "others" empty = "empties" -permalinked = "permalinkeds" -[permalinks] -permalinkeds = "/perma/:slug/" ` pageTemplate := `--- @@ -103,34 +87,39 @@ categories: %s others: %s -permalinkeds: -%s --- # Doc ` - siteConfig = fmt.Sprintf(siteConfig, uglyURLs) + siteConfig = fmt.Sprintf(siteConfig, preserveTaxonomyNames, uglyURLs) - b := newTestSitesBuilder(t).WithConfigFile("toml", siteConfig) + th, h := newTestSitesFromConfigWithDefaultTemplates(t, siteConfig) + require.Len(t, h.Sites, 1) - b.WithContent( - "p1.md", fmt.Sprintf(pageTemplate, "t1/c1", "- Tag1", "- cAt1", "- o1", "- Pl1"), - "p2.md", fmt.Sprintf(pageTemplate, "t2/c1", "- tag2", "- cAt1", "- o1", "- Pl1"), - "p3.md", fmt.Sprintf(pageTemplate, "t2/c12", "- tag2", "- cat2", "- o1", "- Pl1"), - "p4.md", fmt.Sprintf(pageTemplate, "Hello World", "", "", "- \"Hello Hugo world\"", "- Pl1"), - "categories/_index.md", newTestPage("Category Terms", "2017-01-01", 10), - "tags/Tag1/_index.md", newTestPage("Tag1 List", "2017-01-01", 10), - // https://github.com/gohugoio/hugo/issues/5847 - "/tags/not-used/_index.md", newTestPage("Unused Tag List", "2018-01-01", 10), - ) + fs := th.Fs - b.Build(BuildCfg{}) + if preserveTaxonomyNames { + writeSource(t, fs, "content/p1.md", fmt.Sprintf(pageTemplate, "t1/c1", "- tag1", "- cat1", "- o1")) + } else { + // Check lower-casing of tags + writeSource(t, fs, "content/p1.md", fmt.Sprintf(pageTemplate, "t1/c1", "- Tag1", "- cAt1", "- o1")) + + } + writeSource(t, fs, "content/p2.md", fmt.Sprintf(pageTemplate, "t2/c1", "- tag2", "- cat1", "- o1")) + writeSource(t, fs, "content/p3.md", fmt.Sprintf(pageTemplate, "t2/c12", "- tag2", "- cat2", "- o1")) + writeSource(t, fs, "content/p4.md", fmt.Sprintf(pageTemplate, "Hello World", "", "", "- \"Hello Hugo world\"")) + + writeNewContentFile(t, fs, "Category Terms", "2017-01-01", "content/categories/_index.md", 10) + writeNewContentFile(t, fs, "Tag1 List", "2017-01-01", "content/tags/tag1/_index.md", 10) + + err := h.Build(BuildCfg{}) + + require.NoError(t, err) // So what we have now is: // 1. categories with terms content page, but no content page for the only c1 category // 2. tags with no terms content page, but content page for one of 2 tags (tag1) // 3. the "others" taxonomy with no content pages. - // 4. the "permalinkeds" taxonomy with permalinks configuration. pathFunc := func(s string) string { if uglyURLs { @@ -140,891 +129,59 @@ permalinkeds: } // 1. - b.AssertFileContent(pathFunc("public/categories/cat1/index.html"), "List", "CAt1") - b.AssertFileContent(pathFunc("public/categories/index.html"), "Taxonomy Term Page", "Category Terms") + th.assertFileContent(pathFunc("public/categories/cat1/index.html"), "List", "Cat1") + th.assertFileContent(pathFunc("public/categories/index.html"), "Terms List", "Category Terms") // 2. - b.AssertFileContent(pathFunc("public/tags/tag2/index.html"), "List", "tag2") - b.AssertFileContent(pathFunc("public/tags/tag1/index.html"), "List", "Tag1") - b.AssertFileContent(pathFunc("public/tags/index.html"), "Taxonomy Term Page", "Tags") + th.assertFileContent(pathFunc("public/tags/tag2/index.html"), "List", "Tag2") + th.assertFileContent(pathFunc("public/tags/tag1/index.html"), "List", "Tag1") + th.assertFileContent(pathFunc("public/tags/index.html"), "Terms List", "Tags") // 3. - b.AssertFileContent(pathFunc("public/others/o1/index.html"), "List", "o1") - b.AssertFileContent(pathFunc("public/others/index.html"), "Taxonomy Term Page", "Others") + th.assertFileContent(pathFunc("public/others/o1/index.html"), "List", "O1") + th.assertFileContent(pathFunc("public/others/index.html"), "Terms List", "Others") - // 4. - b.AssertFileContent(pathFunc("public/perma/pl1/index.html"), "List", "Pl1") + s := h.Sites[0] - // This looks kind of funky, but the taxonomy terms do not have a permalinks definition, - // for good reasons. - b.AssertFileContent(pathFunc("public/permalinkeds/index.html"), "Taxonomy Term Page", "Permalinkeds") - - s := b.H.Sites[0] - - // Make sure that each kinds.KindTaxonomyTerm page has an appropriate number - // of kinds.KindTaxonomy pages in its Pages slice. + // Make sure that each KindTaxonomyTerm page has an appropriate number + // of KindTaxonomy pages in its Pages slice. taxonomyTermPageCounts := map[string]int{ - "tags": 3, - "categories": 2, - "others": 2, - "empties": 0, - "permalinkeds": 1, + "tags": 2, + "categories": 2, + "others": 2, + "empties": 0, } for taxonomy, count := range taxonomyTermPageCounts { - msg := qt.Commentf(taxonomy) - term := s.getPageOldVersion(kinds.KindTaxonomy, taxonomy) - b.Assert(term, qt.Not(qt.IsNil), msg) - b.Assert(len(term.Pages()), qt.Equals, count, msg) + term := s.getPage(KindTaxonomyTerm, taxonomy) + require.NotNil(t, term) + require.Len(t, term.Pages, count) - for _, p := range term.Pages() { - b.Assert(p.Kind(), qt.Equals, kinds.KindTerm) + for _, page := range term.Pages { + require.Equal(t, KindTaxonomy, page.Kind) } } - cat1 := s.getPageOldVersion(kinds.KindTerm, "categories", "cat1") - b.Assert(cat1, qt.Not(qt.IsNil)) + cat1 := s.getPage(KindTaxonomy, "categories", "cat1") + require.NotNil(t, cat1) if uglyURLs { - b.Assert(cat1.RelPermalink(), qt.Equals, "/blog/categories/cat1.html") + require.Equal(t, "/blog/categories/cat1.html", cat1.RelPermalink()) } else { - b.Assert(cat1.RelPermalink(), qt.Equals, "/blog/categories/cat1/") + require.Equal(t, "/blog/categories/cat1/", cat1.RelPermalink()) } - pl1 := s.getPageOldVersion(kinds.KindTerm, "permalinkeds", "pl1") - permalinkeds := s.getPageOldVersion(kinds.KindTaxonomy, "permalinkeds") - b.Assert(pl1, qt.Not(qt.IsNil)) - b.Assert(permalinkeds, qt.Not(qt.IsNil)) - if uglyURLs { - b.Assert(pl1.RelPermalink(), qt.Equals, "/blog/perma/pl1.html") - b.Assert(permalinkeds.RelPermalink(), qt.Equals, "/blog/permalinkeds.html") + // Issue #3070 preserveTaxonomyNames + if preserveTaxonomyNames { + helloWorld := s.getPage(KindTaxonomy, "others", "Hello Hugo world") + require.NotNil(t, helloWorld) + require.Equal(t, "Hello Hugo world", helloWorld.Title) } else { - b.Assert(pl1.RelPermalink(), qt.Equals, "/blog/perma/pl1/") - b.Assert(permalinkeds.RelPermalink(), qt.Equals, "/blog/permalinkeds/") + helloWorld := s.getPage(KindTaxonomy, "others", "hello-hugo-world") + require.NotNil(t, helloWorld) + require.Equal(t, "Hello Hugo World", helloWorld.Title) } - helloWorld := s.getPageOldVersion(kinds.KindTerm, "others", "hello-hugo-world") - b.Assert(helloWorld, qt.Not(qt.IsNil)) - b.Assert(helloWorld.Title(), qt.Equals, "Hello Hugo world") - // Issue #2977 - b.AssertFileContent(pathFunc("public/empties/index.html"), "Taxonomy Term Page", "Empties") -} - -// https://github.com/gohugoio/hugo/issues/5513 -// https://github.com/gohugoio/hugo/issues/5571 -func TestTaxonomiesPathSeparation(t *testing.T) { - t.Parallel() - - config := ` -baseURL = "https://example.com" -titleCaseStyle = "none" -[taxonomies] -"news/tag" = "news/tags" -"news/category" = "news/categories" -"t1/t2/t3" = "t1/t2/t3s" -"s1/s2/s3" = "s1/s2/s3s" -` - - pageContent := ` -+++ -title = "foo" -"news/categories" = ["a", "b", "c", "d/e", "f/g/h"] -"t1/t2/t3s" = ["t4/t5", "t4/t5/t6"] -+++ -Content. -` - - b := newTestSitesBuilder(t) - b.WithConfigFile("toml", config) - b.WithContent("page.md", pageContent) - b.WithContent("news/categories/b/_index.md", ` ---- -title: "This is B" ---- -`) - - b.WithContent("news/categories/f/g/h/_index.md", ` ---- -title: "This is H" ---- -`) - - b.WithContent("t1/t2/t3s/t4/t5/_index.md", ` ---- -title: "This is T5" ---- -`) - - b.WithContent("s1/s2/s3s/_index.md", ` ---- -title: "This is S3s" ---- -`) - - b.CreateSites().Build(BuildCfg{}) - - s := b.H.Sites[0] - - filterbyKind := func(kind string) page.Pages { - var pages page.Pages - for _, p := range s.Pages() { - if p.Kind() == kind { - pages = append(pages, p) - } - } - return pages - } - - ta := filterbyKind(kinds.KindTerm) - te := filterbyKind(kinds.KindTaxonomy) - - b.Assert(len(te), qt.Equals, 4) - b.Assert(len(ta), qt.Equals, 7) - - b.AssertFileContent("public/news/categories/a/index.html", "Taxonomy List Page 1|a|Hello|https://example.com/news/categories/a/|") - b.AssertFileContent("public/news/categories/b/index.html", "Taxonomy List Page 1|This is B|Hello|https://example.com/news/categories/b/|") - b.AssertFileContent("public/news/categories/d/e/index.html", "Taxonomy List Page 1|d/e|Hello|https://example.com/news/categories/d/e/|") - b.AssertFileContent("public/news/categories/f/g/h/index.html", "Taxonomy List Page 1|This is H|Hello|https://example.com/news/categories/f/g/h/|") - b.AssertFileContent("public/t1/t2/t3s/t4/t5/index.html", "Taxonomy List Page 1|This is T5|Hello|https://example.com/t1/t2/t3s/t4/t5/|") - b.AssertFileContent("public/t1/t2/t3s/t4/t5/t6/index.html", "Taxonomy List Page 1|t4/t5/t6|Hello|https://example.com/t1/t2/t3s/t4/t5/t6/|") - - b.AssertFileContent("public/news/categories/index.html", "Taxonomy Term Page 1|categories|Hello|https://example.com/news/categories/|") - b.AssertFileContent("public/t1/t2/t3s/index.html", "Taxonomy Term Page 1|t3s|Hello|https://example.com/t1/t2/t3s/|") - b.AssertFileContent("public/s1/s2/s3s/index.html", "Taxonomy Term Page 1|This is S3s|Hello|https://example.com/s1/s2/s3s/|") -} - -// https://github.com/gohugoio/hugo/issues/5719 -func TestTaxonomiesNextGenLoops(t *testing.T) { - b := newTestSitesBuilder(t).WithSimpleConfigFile() - - b.WithTemplatesAdded("index.html", ` -

    Tags

    - - -`) - - b.WithTemplatesAdded("_default/terms.html", ` -

    Terms

    -
      - {{ range .Data.Terms.Alphabetical }} -
    • {{ .Page.Title }} {{ .Count }}
    • - {{ end }} -
    -`) - - for i := range 10 { - b.WithContent(fmt.Sprintf("page%d.md", i+1), ` ---- -Title: "Taxonomy!" -tags: ["Hugo Rocks!", "Rocks I say!" ] -categories: ["This is Cool", "And new" ] ---- - -Content. - - `) - } - - b.CreateSites().Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", `
  • Hugo Rocks! 10
  • `) - b.AssertFileContent("public/categories/index.html", `
  • This Is Cool 10
  • `) - b.AssertFileContent("public/tags/index.html", `
  • Rocks I Say! 10
  • `) -} - -// Issue 6213 -func TestTaxonomiesNotForDrafts(t *testing.T) { - t.Parallel() - - b := newTestSitesBuilder(t) - b.WithContent("draft.md", `--- -title: "Draft" -draft: true -categories: ["drafts"] ---- - -`, - "regular.md", `--- -title: "Not Draft" -categories: ["regular"] ---- - -`) - - b.Build(BuildCfg{}) - s := b.H.Sites[0] - - b.Assert(b.CheckExists("public/categories/regular/index.html"), qt.Equals, true) - b.Assert(b.CheckExists("public/categories/drafts/index.html"), qt.Equals, false) - - reg, _ := s.getPage(nil, "categories/regular") - dra, _ := s.getPage(nil, "categories/draft") - b.Assert(reg, qt.Not(qt.IsNil)) - b.Assert(dra, qt.IsNil) -} - -func TestTaxonomiesIndexDraft(t *testing.T) { - t.Parallel() - b := newTestSitesBuilder(t) - b.WithContent( - "categories/_index.md", `--- -title: "The Categories" -draft: true ---- - -Content. - -`, - "page.md", `--- -title: "The Page" -categories: ["cool"] ---- - -Content. - -`, - ) - - b.WithTemplates("index.html", ` -{{ range .Site.Pages }} -{{ .RelPermalink }}|{{ .Title }}|{{ .WordCount }}|{{ .Content }}| -{{ end }} -`) - - b.Build(BuildCfg{}) - - b.AssertFileContentFn("public/index.html", func(s string) bool { - return !strings.Contains(s, "/categories/|") - }) -} - -// https://github.com/gohugoio/hugo/issues/6927 -func TestTaxonomiesHomeDraft(t *testing.T) { - t.Parallel() - - b := newTestSitesBuilder(t) - b.WithContent( - "_index.md", `--- -title: "Home" -draft: true ---- - -Content. - -`, - "posts/_index.md", `--- -title: "Posts" -draft: true ---- - -Content. - -`, - "posts/page.md", `--- -title: "The Page" -categories: ["cool"] ---- - -Content. - -`, - ) - - b.WithTemplates("index.html", ` -NO HOME FOR YOU -`) - - b.Build(BuildCfg{}) - - b.Assert(b.CheckExists("public/index.html"), qt.Equals, false) - b.Assert(b.CheckExists("public/categories/index.html"), qt.Equals, false) - b.Assert(b.CheckExists("public/posts/index.html"), qt.Equals, false) -} - -// https://github.com/gohugoio/hugo/issues/6173 -func TestTaxonomiesWithBundledResources(t *testing.T) { - b := newTestSitesBuilder(t) - b.WithTemplates("_default/list.html", ` -List {{ .Title }}: -{{ range .Resources }} -Resource: {{ .RelPermalink }}|{{ .MediaType }} -{{ end }} - `) - - b.WithContent("p1.md", `--- -title: Page -categories: ["funny"] ---- - `, - "categories/_index.md", "---\ntitle: Categories Page\n---", - "categories/data.json", "Category data", - "categories/funny/_index.md", "---\ntitle: Funny Category\n---", - "categories/funny/funnydata.json", "Category funny data", - ) - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/categories/index.html", `Resource: /categories/data.json|application/json`) - b.AssertFileContent("public/categories/funny/index.html", `Resource: /categories/funny/funnydata.json|application/json`) -} - -func TestTaxonomiesRemoveOne(t *testing.T) { - files := ` --- hugo.toml -- -disableLiveReload = true --- layouts/index.html -- -{{ $cats := .Site.Taxonomies.categories.cats }} -{{ if $cats }} -Len cats: {{ len $cats }} -{{ range $cats }} - Cats:|{{ .Page.RelPermalink }}| -{{ end }} -{{ end }} -{{ $funny := .Site.Taxonomies.categories.funny }} -{{ if $funny }} -Len funny: {{ len $funny }} -{{ range $funny }} - Funny:|{{ .Page.RelPermalink }}| -{{ end }} -{{ end }} --- content/p1.md -- ---- -title: Page -categories: ["funny", "cats"] ---- --- content/p2.md -- ---- -title: Page2 -categories: ["funny", "cats"] ---- - -` - b := TestRunning(t, files) - - b.AssertFileContent("public/index.html", ` -Len cats: 2 -Len funny: 2 -Cats:|/p1/| -Cats:|/p2/| -Funny:|/p1/| -Funny:|/p2/|`) - - // Remove one category from one of the pages. - b.EditFiles("content/p1.md", `--- -title: Page -categories: ["funny"] ---- - `) - - b.Build() - - b.AssertFileContent("public/index.html", ` -Len cats: 1 -Len funny: 2 -Cats:|/p2/| -Funny:|/p1/| -Funny:|/p2/|`) -} - -// https://github.com/gohugoio/hugo/issues/6590 -func TestTaxonomiesListPages(t *testing.T) { - b := newTestSitesBuilder(t) - b.WithTemplates("_default/list.html", ` - -{{ template "print-taxo" "categories.cats" }} -{{ template "print-taxo" "categories.funny" }} - -{{ define "print-taxo" }} -{{ $node := index site.Taxonomies (split $ ".") }} -{{ if $node }} -Len {{ $ }}: {{ len $node }} -{{ range $node }} - {{ $ }}:|{{ .Page.RelPermalink }}| -{{ end }} -{{ else }} -{{ $ }} not found. -{{ end }} -{{ end }} - `) - - b.WithContent("_index.md", `--- -title: Home -categories: ["funny", "cats"] ---- - `, "blog/p1.md", `--- -title: Page1 -categories: ["funny"] ---- - `, "blog/_index.md", `--- -title: Blog Section -categories: ["cats"] ---- - `, - ) - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", ` - -Len categories.cats: 2 -categories.cats:|/blog/| -categories.cats:|/| - -Len categories.funny: 2 -categories.funny:|/| -categories.funny:|/blog/p1/| -`) -} - -func TestTaxonomiesPageCollections(t *testing.T) { - t.Parallel() - - b := newTestSitesBuilder(t) - b.WithContent( - "_index.md", `--- -title: "Home Sweet Home" -categories: [ "dogs", "gorillas"] ---- -`, - "section/_index.md", `--- -title: "Section" -categories: [ "cats", "dogs", "birds"] ---- -`, - "section/p1.md", `--- -title: "Page1" -categories: ["funny", "cats"] ---- -`, "section/p2.md", `--- -title: "Page2" -categories: ["funny"] ---- -`) - - b.WithTemplatesAdded("index.html", ` -{{ $home := site.Home }} -{{ $section := site.GetPage "section" }} -{{ $categories := site.GetPage "categories" }} -{{ $funny := site.GetPage "categories/funny" }} -{{ $cats := site.GetPage "categories/cats" }} -{{ $p1 := site.GetPage "section/p1" }} - -Categories Pages: {{ range $categories.Pages}}{{.RelPermalink }}|{{ end }}:END -Funny Pages: {{ range $funny.Pages}}{{.RelPermalink }}|{{ end }}:END -Cats Pages: {{ range $cats.Pages}}{{.RelPermalink }}|{{ end }}:END -P1 Terms: {{ range $p1.GetTerms "categories" }}{{.RelPermalink }}|{{ end }}:END -Section Terms: {{ range $section.GetTerms "categories" }}{{.RelPermalink }}|{{ end }}:END -Home Terms: {{ range $home.GetTerms "categories" }}{{.RelPermalink }}|{{ end }}:END -Category Paginator {{ range $categories.Paginator.Pages }}{{ .RelPermalink }}|{{ end }}:END -Cats Paginator {{ range $cats.Paginator.Pages }}{{ .RelPermalink }}|{{ end }}:END - -`) - b.WithTemplatesAdded("404.html", ` -404 Terms: {{ range .GetTerms "categories" }}{{.RelPermalink }}|{{ end }}:END - `) - b.Build(BuildCfg{}) - - cat := b.GetPage("categories") - funny := b.GetPage("categories/funny") - - b.Assert(cat, qt.Not(qt.IsNil)) - b.Assert(funny, qt.Not(qt.IsNil)) - - b.Assert(cat.Parent().IsHome(), qt.Equals, true) - b.Assert(funny.Kind(), qt.Equals, "term") - b.Assert(funny.Parent(), qt.Equals, cat) - - b.AssertFileContent("public/index.html", ` -Categories Pages: /categories/birds/|/categories/cats/|/categories/dogs/|/categories/funny/|/categories/gorillas/|:END -Funny Pages: /section/p1/|/section/p2/|:END -Cats Pages: /section/p1/|/section/|:END -P1 Terms: /categories/funny/|/categories/cats/|:END -Section Terms: /categories/cats/|/categories/dogs/|/categories/birds/|:END -Home Terms: /categories/dogs/|/categories/gorillas/|:END -Cats Paginator /section/p1/|/section/|:END -Category Paginator /categories/birds/|/categories/cats/|/categories/dogs/|/categories/funny/|/categories/gorillas/|:END`, - ) - b.AssertFileContent("public/404.html", "\n404 Terms: :END\n\t") - b.AssertFileContent("public/categories/funny/index.xml", `http://example.com/section/p1/`) - b.AssertFileContent("public/categories/index.xml", `http://example.com/categories/funny/`) -} - -func TestTaxonomiesDirectoryOverlaps(t *testing.T) { - t.Parallel() - - b := newTestSitesBuilder(t).WithContent( - "abc/_index.md", "---\ntitle: \"abc\"\nabcdefgs: [abc]\n---", - "abc/p1.md", "---\ntitle: \"abc-p\"\n---", - "abcdefgh/_index.md", "---\ntitle: \"abcdefgh\"\n---", - "abcdefgh/p1.md", "---\ntitle: \"abcdefgh-p\"\n---", - "abcdefghijk/index.md", "---\ntitle: \"abcdefghijk\"\n---", - ) - - b.WithConfigFile("toml", ` -baseURL = "https://example.org" -titleCaseStyle = "none" - -[taxonomies] - abcdef = "abcdefs" - abcdefg = "abcdefgs" - abcdefghi = "abcdefghis" -`) - - b.WithTemplatesAdded("index.html", ` -{{ range site.Pages }}Page: {{ template "print-page" . }} -{{ end }} -{{ $abc := site.GetPage "abcdefgs/abc" }} -{{ $abcdefgs := site.GetPage "abcdefgs" }} -abc: {{ template "print-page" $abc }}|IsAncestor: {{ $abc.IsAncestor $abcdefgs }}|IsDescendant: {{ $abc.IsDescendant $abcdefgs }} -abcdefgs: {{ template "print-page" $abcdefgs }}|IsAncestor: {{ $abcdefgs.IsAncestor $abc }}|IsDescendant: {{ $abcdefgs.IsDescendant $abc }} - -{{ define "print-page" }}{{ .RelPermalink }}|{{ .Title }}|{{.Kind }}|Parent: {{ with .Parent }}{{ .RelPermalink }}{{ end }}|CurrentSection: {{ .CurrentSection.RelPermalink}}|FirstSection: {{ .FirstSection.RelPermalink }}{{ end }} - -`) - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", ` - Page: /||home|Parent: |CurrentSection: /| - Page: /abc/|abc|section|Parent: /|CurrentSection: /abc/| - Page: /abc/p1/|abc-p|page|Parent: /abc/|CurrentSection: /abc/| - Page: /abcdefgh/|abcdefgh|section|Parent: /|CurrentSection: /abcdefgh/| - Page: /abcdefgh/p1/|abcdefgh-p|page|Parent: /abcdefgh/|CurrentSection: /abcdefgh/| - Page: /abcdefghijk/|abcdefghijk|page|Parent: /|CurrentSection: /| - Page: /abcdefghis/|abcdefghis|taxonomy|Parent: /|CurrentSection: /abcdefghis/| - Page: /abcdefgs/|abcdefgs|taxonomy|Parent: /|CurrentSection: /abcdefgs/| - Page: /abcdefs/|abcdefs|taxonomy|Parent: /|CurrentSection: /abcdefs/| - abc: /abcdefgs/abc/|abc|term|Parent: /abcdefgs/|CurrentSection: /abcdefgs/abc/| - abcdefgs: /abcdefgs/|abcdefgs|taxonomy|Parent: /|CurrentSection: /abcdefgs/| - abc: /abcdefgs/abc/|abc|term|Parent: /abcdefgs/|CurrentSection: /abcdefgs/abc/|FirstSection: /abcdefgs/|IsAncestor: false|IsDescendant: true - abcdefgs: /abcdefgs/|abcdefgs|taxonomy|Parent: /|CurrentSection: /abcdefgs/|FirstSection: /abcdefgs/|IsAncestor: true|IsDescendant: false -`) -} - -func TestTaxonomiesWeightSort(t *testing.T) { - files := ` --- layouts/index.html -- -{{ $a := site.GetPage "tags/a"}} -:{{ range $a.Pages }}{{ .RelPermalink }}|{{ end }}: --- content/p1.md -- ---- -title: P1 -weight: 100 -tags: ['a'] -tags_weight: 20 ---- --- content/p3.md -- ---- -title: P2 -weight: 200 -tags: ['a'] -tags_weight: 30 ---- --- content/p2.md -- ---- -title: P3 -weight: 50 -tags: ['a'] -tags_weight: 40 ---- - ` - - b := Test(t, files) - - b.AssertFileContent("public/index.html", `:/p1/|/p3/|/p2/|:`) -} - -func TestTaxonomiesEmptyTagsString(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -[taxonomies] -tag = 'tags' --- content/p1.md -- -+++ -title = "P1" -tags = '' -+++ --- layouts/_default/single.html -- -Single. - -` - Test(t, files) -} - -func TestTaxonomiesSpaceInName(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -[taxonomies] -authors = 'book authors' --- content/p1.md -- ---- -title: Good Omens -book authors: - - Neil Gaiman - - Terry Pratchett ---- --- layouts/index.html -- -{{- $taxonomy := "book authors" }} -Len Book Authors: {{ len (index .Site.Taxonomies $taxonomy) }} -` - b := Test(t, files) - - b.AssertFileContent("public/index.html", "Len Book Authors: 2") -} - -func TestTaxonomiesListTermsHome(t *testing.T) { - files := ` --- hugo.toml -- -baseURL = "https://example.com" -[taxonomies] -tag = "tags" --- content/_index.md -- ---- -title: "Home" -tags: ["a", "b", "c", "hello world"] ---- --- content/tags/a/_index.md -- ---- -title: "A" ---- --- content/tags/b/_index.md -- ---- -title: "B" ---- --- content/tags/c/_index.md -- ---- -title: "C" ---- --- content/tags/d/_index.md -- ---- -title: "D" ---- --- content/tags/hello-world/_index.md -- ---- -title: "Hello World!" ---- --- layouts/home.html -- -Terms: {{ range site.Taxonomies.tags }}{{ .Page.Title }}: {{ .Count }}|{{ end }}$ -` - b := Test(t, files) - - b.AssertFileContent("public/index.html", "Terms: A: 1|B: 1|C: 1|Hello World!: 1|$") -} - -func TestTaxonomiesTermTitleAndTerm(t *testing.T) { - files := ` --- hugo.toml -- -baseURL = "https://example.com" -[taxonomies] -tag = "tags" --- content/_index.md -- ---- -title: "Home" -tags: ["hellO world"] ---- --- layouts/_default/term.html -- -{{ .Title }}|{{ .Kind }}|{{ .Data.Singular }}|{{ .Data.Plural }}|{{ .Page.Data.Term }}| -` - - b := Test(t, files) - - b.AssertFileContent("public/tags/hello-world/index.html", "HellO World|term|tag|tags|hellO world|") -} - -func TestTermDraft(t *testing.T) { - t.Parallel() - - files := ` --- layouts/_default/list.html -- -|{{ .Title }}| --- content/p1.md -- ---- -title: p1 -tags: [a] ---- --- content/tags/a/_index.md -- ---- -title: tag-a-title-override -draft: true ---- - ` - - b := Test(t, files) - - b.AssertFileExists("public/tags/a/index.html", false) -} - -func TestTermBuildNeverRenderNorList(t *testing.T) { - t.Parallel() - - files := ` --- layouts/index.html -- -|{{ len site.Taxonomies.tags }}| --- content/p1.md -- ---- -title: p1 -tags: [a] ---- --- content/tags/a/_index.md -- ---- -title: tag-a-title-override -build: - render: never - list: never ---- - - ` - - b := Test(t, files) - - b.AssertFileExists("public/tags/a/index.html", false) - b.AssertFileContent("public/index.html", "|0|") -} - -func TestTaxonomiesTermLookup(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -baseURL = "https://example.com" -[taxonomies] -tag = "tags" --- content/_index.md -- ---- -title: "Home" -tags: ["a", "b"] ---- --- layouts/taxonomy/tag.html -- -Tag: {{ .Title }}| --- content/tags/a/_index.md -- ---- -title: tag-a-title-override ---- -` - - b := Test(t, files) - - b.AssertFileContent("public/tags/a/index.html", "Tag: tag-a-title-override|") -} - -func TestTaxonomyLookupIssue12193(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ['page','rss','section','sitemap'] -[taxonomies] -author = 'authors' --- layouts/_default/list.html -- -{{ .Title }}| --- layouts/_default/author.terms.html -- -layouts/_default/author.terms.html --- content/authors/_index.md -- ---- -title: Authors Page ---- -` - - b := Test(t, files) - - b.AssertFileExists("public/index.html", true) - b.AssertFileExists("public/authors/index.html", true) - b.AssertFileContent("public/authors/index.html", "layouts/_default/author.terms.html") // failing test -} - -func TestTaxonomyNestedEmptySectionsIssue12188(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -disableKinds = ['rss','sitemap'] -defaultContentLanguage = 'en' -defaultContentLanguageInSubdir = true -[languages.en] -weight = 1 -[languages.ja] -weight = 2 -[taxonomies] -'s1/category' = 's1/category' --- layouts/_default/single.html -- -{{ .Title }}| --- layouts/_default/list.html -- -{{ .Title }}| --- content/s1/p1.en.md -- ---- -title: p1 ---- -` - - b := Test(t, files) - - b.AssertFileExists("public/en/s1/index.html", true) - b.AssertFileExists("public/en/s1/p1/index.html", true) - b.AssertFileExists("public/en/s1/category/index.html", true) - - b.AssertFileExists("public/ja/s1/index.html", false) // failing test - b.AssertFileExists("public/ja/s1/category/index.html", true) -} - -func BenchmarkTaxonomiesGetTerms(b *testing.B) { - createBuilders := func(b *testing.B, numPages int) []*IntegrationTestBuilder { - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableKinds = ["RSS", "sitemap", "section"] -[taxononomies] -tag = "tags" --- layouts/_default/list.html -- -List. --- layouts/_default/single.html -- -GetTerms.tags: {{ range .GetTerms "tags" }}{{ .Title }}|{{ end }} --- content/_index.md -- -` - - tagsVariants := []string{ - "tags: ['a']", - "tags: ['a', 'b']", - "tags: ['a', 'b', 'c']", - "tags: ['a', 'b', 'c', 'd']", - "tags: ['a', 'b', 'd', 'e']", - "tags: ['a', 'b', 'c', 'd', 'e']", - "tags: ['a', 'd']", - "tags: ['a', 'f']", - } - - for i := 1; i < numPages; i++ { - tags := tagsVariants[i%len(tagsVariants)] - files += fmt.Sprintf("\n-- content/posts/p%d.md --\n---\n%s\n---", i+1, tags) - } - cfg := IntegrationTestConfig{ - T: b, - TxtarString: files, - } - builders := make([]*IntegrationTestBuilder, b.N) - - for i := range builders { - builders[i] = NewIntegrationTestBuilder(cfg) - } - - b.ResetTimer() - - return builders - } - - for _, numPages := range []int{100, 1000, 10000, 20000} { - b.Run(fmt.Sprintf("pages_%d", numPages), func(b *testing.B) { - builders := createBuilders(b, numPages) - for i := 0; i < b.N; i++ { - builders[i].Build() - } - }) - } + th.assertFileContent(pathFunc("public/empties/index.html"), "Terms List", "Empties") + } diff --git a/hugolib/template_engines_test.go b/hugolib/template_engines_test.go new file mode 100644 index 000000000..e2e4ee986 --- /dev/null +++ b/hugolib/template_engines_test.go @@ -0,0 +1,95 @@ +// Copyright 2017 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 hugolib + +import ( + "fmt" + "path/filepath" + "testing" + + "strings" + + "github.com/gohugoio/hugo/deps" +) + +func TestAllTemplateEngines(t *testing.T) { + t.Parallel() + noOp := func(s string) string { + return s + } + + amberFixer := func(s string) string { + fixed := strings.Replace(s, "{{ .Title", "{{ Title", -1) + fixed = strings.Replace(fixed, ".Content", "Content", -1) + fixed = strings.Replace(fixed, "{{", "#{", -1) + fixed = strings.Replace(fixed, "}}", "}", -1) + fixed = strings.Replace(fixed, `title "hello world"`, `title("hello world")`, -1) + + return fixed + } + + for _, config := range []struct { + suffix string + templateFixer func(s string) string + }{ + {"amber", amberFixer}, + {"html", noOp}, + {"ace", noOp}, + } { + doTestTemplateEngine(t, config.suffix, config.templateFixer) + + } + +} + +func doTestTemplateEngine(t *testing.T, suffix string, templateFixer func(s string) string) { + + cfg, fs := newTestCfg() + + writeSource(t, fs, filepath.Join("content", "p.md"), ` +--- +title: My Title +--- +My Content +`) + + t.Log("Testing", suffix) + + templTemplate := ` +p + | + | Page Title: {{ .Title }} + br + | Page Content: {{ .Content }} + br + | {{ title "hello world" }} + +` + + templ := templateFixer(templTemplate) + + t.Log(templ) + + writeSource(t, fs, filepath.Join("layouts", "_default", fmt.Sprintf("single.%s", suffix)), templ) + + s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + th := testHelper{s.Cfg, s.Fs, t} + + th.assertFileContent(filepath.Join("public", "p", "index.html"), + "Page Title: My Title", + "My Content", + "Hello World", + ) + +} diff --git a/hugolib/template_test.go b/hugolib/template_test.go index a08f83cb8..a5bec103a 100644 --- a/hugolib/template_test.go +++ b/hugolib/template_test.go @@ -18,22 +18,18 @@ import ( "path/filepath" "testing" - "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/config/allconfig" - - qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/hugofs" + + "github.com/spf13/viper" ) -// TODO(bep) keep this until we release v0.146.0 as a security against breaking changes, but it's rather messy and mostly duplicate of -// tests in the tplimpl package, so eventually just remove it. func TestTemplateLookupOrder(t *testing.T) { + t.Parallel() var ( - fs *hugofs.Fs - cfg config.Provider - th testHelper - configs *allconfig.Configs + fs *hugofs.Fs + cfg *viper.Viper + th testHelper ) // Variants base templates: @@ -41,102 +37,107 @@ func TestTemplateLookupOrder(t *testing.T) { // 2. /baseof. // 3. _default/-baseof., e.g. list-baseof.. // 4. _default/baseof. - for _, this := range []struct { - name string + for i, this := range []struct { setup func(t *testing.T) assert func(t *testing.T) }{ { - "Variant 1", + // Variant 1 func(t *testing.T) { writeSource(t, fs, filepath.Join("layouts", "section", "sect1-baseof.html"), `Base: {{block "main" .}}block{{end}}`) writeSource(t, fs, filepath.Join("layouts", "section", "sect1.html"), `{{define "main"}}sect{{ end }}`) + }, func(t *testing.T) { th.assertFileContent(filepath.Join("public", "sect1", "index.html"), "Base: sect") }, }, { - "Variant 2", + // Variant 2 func(t *testing.T) { writeSource(t, fs, filepath.Join("layouts", "baseof.html"), `Base: {{block "main" .}}block{{end}}`) writeSource(t, fs, filepath.Join("layouts", "index.html"), `{{define "main"}}index{{ end }}`) + }, func(t *testing.T) { th.assertFileContent(filepath.Join("public", "index.html"), "Base: index") }, }, { - "Variant 3", + // Variant 3 func(t *testing.T) { writeSource(t, fs, filepath.Join("layouts", "_default", "list-baseof.html"), `Base: {{block "main" .}}block{{end}}`) writeSource(t, fs, filepath.Join("layouts", "_default", "list.html"), `{{define "main"}}list{{ end }}`) + }, func(t *testing.T) { th.assertFileContent(filepath.Join("public", "sect1", "index.html"), "Base: list") }, }, { - "Variant 4", + // Variant 4 func(t *testing.T) { writeSource(t, fs, filepath.Join("layouts", "_default", "baseof.html"), `Base: {{block "main" .}}block{{end}}`) writeSource(t, fs, filepath.Join("layouts", "_default", "list.html"), `{{define "main"}}list{{ end }}`) + }, func(t *testing.T) { th.assertFileContent(filepath.Join("public", "sect1", "index.html"), "Base: list") }, }, { - "Variant 1, theme, use site base", + // Variant 1, theme, use project's base func(t *testing.T) { cfg.Set("theme", "mytheme") writeSource(t, fs, filepath.Join("layouts", "section", "sect1-baseof.html"), `Base: {{block "main" .}}block{{end}}`) writeSource(t, fs, filepath.Join("themes", "mytheme", "layouts", "section", "sect-baseof.html"), `Base Theme: {{block "main" .}}block{{end}}`) writeSource(t, fs, filepath.Join("layouts", "section", "sect1.html"), `{{define "main"}}sect{{ end }}`) + }, func(t *testing.T) { th.assertFileContent(filepath.Join("public", "sect1", "index.html"), "Base: sect") }, }, { - "Variant 1, theme, use theme base", + // Variant 1, theme, use theme's base func(t *testing.T) { cfg.Set("theme", "mytheme") writeSource(t, fs, filepath.Join("themes", "mytheme", "layouts", "section", "sect1-baseof.html"), `Base Theme: {{block "main" .}}block{{end}}`) writeSource(t, fs, filepath.Join("layouts", "section", "sect1.html"), `{{define "main"}}sect{{ end }}`) + }, func(t *testing.T) { th.assertFileContent(filepath.Join("public", "sect1", "index.html"), "Base Theme: sect") }, }, { - "Variant 4, theme, use site base", + // Variant 4, theme, use project's base func(t *testing.T) { cfg.Set("theme", "mytheme") writeSource(t, fs, filepath.Join("layouts", "_default", "baseof.html"), `Base: {{block "main" .}}block{{end}}`) writeSource(t, fs, filepath.Join("themes", "mytheme", "layouts", "_default", "baseof.html"), `Base Theme: {{block "main" .}}block{{end}}`) writeSource(t, fs, filepath.Join("themes", "mytheme", "layouts", "_default", "list.html"), `{{define "main"}}list{{ end }}`) - writeSource(t, fs, filepath.Join("themes", "mytheme", "layouts", "index.html"), `{{define "main"}}index{{ end }}`) + }, func(t *testing.T) { th.assertFileContent(filepath.Join("public", "sect1", "index.html"), "Base: list") - th.assertFileContent(filepath.Join("public", "index.html"), "Base: index") // Issue #3505 }, }, { - "Variant 4, theme, use themes base", + // Variant 4, theme, use themes's base func(t *testing.T) { cfg.Set("theme", "mytheme") writeSource(t, fs, filepath.Join("themes", "mytheme", "layouts", "_default", "baseof.html"), `Base Theme: {{block "main" .}}block{{end}}`) writeSource(t, fs, filepath.Join("themes", "mytheme", "layouts", "_default", "list.html"), `{{define "main"}}list{{ end }}`) + }, func(t *testing.T) { th.assertFileContent(filepath.Join("public", "sect1", "index.html"), "Base Theme: list") }, }, { + // Test section list and single template selection. // Issue #3116 - "Test section list and single template selection", func(t *testing.T) { cfg.Set("theme", "mytheme") @@ -150,6 +151,7 @@ func TestTemplateLookupOrder(t *testing.T) { // sect2 with list template in /section writeSource(t, fs, filepath.Join("themes", "mytheme", "layouts", "section", "sect2.html"), `sect2 list`) + }, func(t *testing.T) { th.assertFileContent(filepath.Join("public", "sect1", "index.html"), "sect list") @@ -158,9 +160,10 @@ func TestTemplateLookupOrder(t *testing.T) { }, }, { + // Test section list and single template selection with base template. // Issue #2995 - "Test section list and single template selection with base template", func(t *testing.T) { + writeSource(t, fs, filepath.Join("layouts", "_default", "baseof.html"), `Base Default: {{block "main" .}}block{{end}}`) writeSource(t, fs, filepath.Join("layouts", "sect1", "baseof.html"), `Base Sect1: {{block "main" .}}block{{end}}`) writeSource(t, fs, filepath.Join("layouts", "section", "sect2-baseof.html"), `Base Sect2: {{block "main" .}}block{{end}}`) @@ -173,6 +176,7 @@ func TestTemplateLookupOrder(t *testing.T) { // sect2 with list template in /section writeSource(t, fs, filepath.Join("layouts", "section", "sect2.html"), `{{define "main"}}sect2 list{{ end }}`) + }, func(t *testing.T) { th.assertFileContent(filepath.Join("public", "sect1", "index.html"), "Base Sect1", "sect1 list") @@ -186,546 +190,26 @@ func TestTemplateLookupOrder(t *testing.T) { }, } { - this := this - if this.name != "Variant 1" { + if i != 9 { continue } - t.Run(this.name, func(t *testing.T) { - // TODO(bep) there are some function vars need to pull down here to enable => t.Parallel() - cfg, fs = newTestCfg() - this.setup(t) - th, configs = newTestHelperFromProvider(cfg, fs, t) - for i := 1; i <= 3; i++ { - writeSource(t, fs, filepath.Join("content", fmt.Sprintf("sect%d", i), fmt.Sprintf("page%d.md", i)), `--- + cfg, fs = newTestCfg() + th = testHelper{cfg, fs, t} + + for i := 1; i <= 3; i++ { + writeSource(t, fs, filepath.Join("content", fmt.Sprintf("sect%d", i), fmt.Sprintf("page%d.md", i)), `--- title: Template test --- Some content `) - } - - buildSingleSite(t, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{}) - // s.TemplateStore.PrintDebug("", 0, os.Stdout) - this.assert(t) - }) - - } -} - -// https://github.com/gohugoio/hugo/issues/4895 -func TestTemplateBOM(t *testing.T) { - b := newTestSitesBuilder(t).WithSimpleConfigFile() - bom := "\ufeff" - - b.WithTemplatesAdded( - "_default/baseof.html", bom+` - Base: {{ block "main" . }}base main{{ end }}`, - "_default/single.html", bom+`{{ define "main" }}Hi!?{{ end }}`) - - b.WithContent("page.md", `--- -title: "Page" ---- - -Page Content -`) - - b.CreateSites().Build(BuildCfg{}) - - b.AssertFileContent("public/page/index.html", "Base: Hi!?") -} - -func TestTemplateManyBaseTemplates(t *testing.T) { - t.Parallel() - b := newTestSitesBuilder(t).WithSimpleConfigFile() - - numPages := 100 // To get some parallelism - - pageTemplate := `--- -title: "Page %d" -layout: "layout%d" ---- - -Content. -` - - singleTemplate := ` -{{ define "main" }}%d{{ end }} -` - baseTemplate := ` -Base %d: {{ block "main" . }}FOO{{ end }} -` - - for i := range numPages { - id := i + 1 - b.WithContent(fmt.Sprintf("page%d.md", id), fmt.Sprintf(pageTemplate, id, id)) - b.WithTemplates(fmt.Sprintf("_default/layout%d.html", id), fmt.Sprintf(singleTemplate, id)) - b.WithTemplates(fmt.Sprintf("_default/layout%d-baseof.html", id), fmt.Sprintf(baseTemplate, id)) - } - - b.Build(BuildCfg{}) - for i := range numPages { - id := i + 1 - b.AssertFileContent(fmt.Sprintf("public/page%d/index.html", id), fmt.Sprintf(`Base %d: %d`, id, id)) - } -} - -// https://github.com/gohugoio/hugo/issues/6790 -func TestTemplateNoBasePlease(t *testing.T) { - t.Parallel() - b := newTestSitesBuilder(t).WithSimpleConfigFile() - - b.WithTemplates("_default/list.html", ` -{{ define "main" }} - Bonjour -{{ end }} - -{{ printf "list" }} - - - `) - - b.WithTemplates( - "_default/single.html", ` -{{ printf "single" }} -{{ define "main" }} - Bonjour -{{ end }} - - -`) - - b.WithContent("blog/p1.md", `--- -title: The Page ---- -`) - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/blog/p1/index.html", `single`) - b.AssertFileContent("public/blog/index.html", `list`) -} - -// https://github.com/gohugoio/hugo/issues/6816 -func TestTemplateBaseWithComment(t *testing.T) { - t.Parallel() - b := newTestSitesBuilder(t).WithSimpleConfigFile() - b.WithTemplatesAdded( - "baseof.html", `Base: {{ block "main" . }}{{ end }}`, - "index.html", ` - {{/* A comment */}} - {{ define "main" }} - Bonjour - {{ end }} - - - `) - - b.Build(BuildCfg{}) - b.AssertFileContent("public/index.html", `Base: -Bonjour`) -} - -func TestTemplateLookupSite(t *testing.T) { - t.Run("basic", func(t *testing.T) { - t.Parallel() - b := newTestSitesBuilder(t).WithSimpleConfigFile() - b.WithTemplates( - "_default/single.html", `Single: {{ .Title }}`, - "_default/list.html", `List: {{ .Title }}`, - ) - - createContent := func(title string) string { - return fmt.Sprintf(`--- -title: %s ----`, title) } - b.WithContent( - "_index.md", createContent("Home Sweet Home"), - "p1.md", createContent("P1")) + this.setup(t) - b.CreateSites().Build(BuildCfg{}) - b.AssertFileContent("public/index.html", `List: Home Sweet Home`) - b.AssertFileContent("public/p1/index.html", `Single: P1`) - }) + buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) + t.Log("Template Lookup test", i) + this.assert(t) - { } } - -func TestTemplateLookupSitBaseOf(t *testing.T) { - t.Parallel() - b := newTestSitesBuilder(t).WithDefaultMultiSiteConfig() - - b.WithTemplatesAdded( - "index.html", `{{ define "main" }}Main Home En{{ end }}`, - "index.fr.html", `{{ define "main" }}Main Home Fr{{ end }}`, - "baseof.html", `Baseof en: {{ block "main" . }}main block{{ end }}`, - "baseof.fr.html", `Baseof fr: {{ block "main" . }}main block{{ end }}`, - "mysection/baseof.html", `Baseof mysection: {{ block "main" . }}mysection block{{ end }}`, - "_default/single.html", `{{ define "main" }}Main Default Single{{ end }}`, - "_default/list.html", `{{ define "main" }}Main Default List{{ end }}`, - ) - - b.WithContent("mysection/p1.md", `--- -title: My Page ---- - -`) - - b.CreateSites().Build(BuildCfg{}) - - b.AssertFileContent("public/en/index.html", `Baseof en: Main Home En`) - b.AssertFileContent("public/fr/index.html", `Baseof fr: Main Home Fr`) - b.AssertFileContent("public/en/mysection/index.html", `Baseof mysection: Main Default List`) - b.AssertFileContent("public/en/mysection/p1/index.html", `Baseof mysection: Main Default Single`) -} - -func TestTemplateFuncs(t *testing.T) { - b := newTestSitesBuilder(t).WithDefaultMultiSiteConfig() - - homeTpl := `Site: {{ site.Language.Lang }} / {{ .Site.Language.Lang }} / {{ site.BaseURL }} -Sites: {{ site.Sites.Default.Home.Language.Lang }} -Hugo: {{ hugo.Generator }} -` - - b.WithTemplatesAdded( - "index.html", homeTpl, - "index.fr.html", homeTpl, - ) - - b.CreateSites().Build(BuildCfg{}) - - b.AssertFileContent("public/en/index.html", - "Site: en / en / http://example.com/blog", - "Sites: en", - "Hugo: {"@type":"WebPage","headline":"{{$title}}"} - -{{/* Action/commands newlines, from Go 1.16, see https://github.com/golang/go/issues/29770 */}} -{{ $norway := dict - "country" "Norway" - "population" "5 millions" - "language" "Norwegian" - "language_code" "nb" - "weather" "freezing cold" - "capitol" "Oslo" - "largest_city" "Oslo" - "currency" "Norwegian krone" - "dialing_code" "+47" -}} - -Population in Norway is {{ - $norway.population - | lower - | upper -}} - -`, - ) - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", ` - -Population in Norway is 5 MILLIONS - -`) -} - -func TestPartialInline(t *testing.T) { - b := newTestSitesBuilder(t) - - b.WithContent("p1.md", "") - - b.WithTemplates( - "index.html", ` - -{{ $p1 := partial "p1" . }} -{{ $p2 := partial "p2" . }} - -P1: {{ $p1 }} -P2: {{ $p2 }} - -{{ define "partials/p1" }}Inline: p1{{ end }} - -{{ define "partials/p2" }} -{{ $value := 32 }} -{{ return $value }} -{{ end }} - - -`, - ) - - b.CreateSites().Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", - ` -P1: Inline: p1 -P2: 32`, - ) -} - -func TestPartialInlineBase(t *testing.T) { - b := newTestSitesBuilder(t) - - b.WithContent("p1.md", "") - - b.WithTemplates( - "baseof.html", `{{ $p3 := partial "p3" . }}P3: {{ $p3 }} -{{ block "main" . }}{{ end }}{{ define "partials/p3" }}Inline: p3{{ end }}`, - "index.html", ` -{{ define "main" }} - -{{ $p1 := partial "p1" . }} -{{ $p2 := partial "p2" . }} - -P1: {{ $p1 }} -P2: {{ $p2 }} - -{{ end }} - - -{{ define "partials/p1" }}Inline: p1{{ end }} - -{{ define "partials/p2" }} -{{ $value := 32 }} -{{ return $value }} -{{ end }} - - -`, - ) - - b.CreateSites().Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", - ` -P1: Inline: p1 -P2: 32 -P3: Inline: p3 -`, - ) -} - -// https://github.com/gohugoio/hugo/issues/7478 -func TestBaseWithAndWithoutDefine(t *testing.T) { - b := newTestSitesBuilder(t) - - b.WithContent("p1.md", "---\ntitle: P\n---\nContent") - - b.WithTemplates( - "_default/baseof.html", ` -::Header Start:{{ block "header" . }}{{ end }}:Header End: -::{{ block "main" . }}Main{{ end }}:: -`, "index.html", ` -{{ define "header" }} -Home Header -{{ end }} -{{ define "main" }} -This is home main -{{ end }} -`, - - "_default/single.html", ` -{{ define "main" }} -This is single main -{{ end }} -`, - ) - - b.CreateSites().Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", ` -Home Header -This is home main -`, - ) - - b.AssertFileContent("public/p1/index.html", ` - ::Header Start::Header End: -This is single main -`, - ) -} - -// Issue 9393. -func TestApplyWithNamespace(t *testing.T) { - b := newTestSitesBuilder(t) - - b.WithTemplates( - "index.html", ` -{{ $b := slice " a " " b " " c" }} -{{ $a := apply $b "strings.Trim" "." " " }} -a: {{ $a }} -`, - ).WithContent("p1.md", "") - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", `a: [a b c]`) -} - -// Legacy behavior for internal templates. -func TestOverrideInternalTemplate(t *testing.T) { - files := ` --- hugo.toml -- -baseURL = "https://example.org" --- layouts/index.html -- -{{ template "_internal/google_analytics_async.html" . }} --- layouts/_internal/google_analytics_async.html -- -Overridden. -` - b := Test(t, files) - - b.AssertFileContent("public/index.html", "Overridden.") -} diff --git a/hugolib/testdata/cities.csv b/hugolib/testdata/cities.csv deleted file mode 100644 index ee6b058b6..000000000 --- a/hugolib/testdata/cities.csv +++ /dev/null @@ -1,130 +0,0 @@ -"LatD", "LatM", "LatS", "NS", "LonD", "LonM", "LonS", "EW", "City", "State" - 41, 5, 59, "N", 80, 39, 0, "W", "Youngstown", OH - 42, 52, 48, "N", 97, 23, 23, "W", "Yankton", SD - 46, 35, 59, "N", 120, 30, 36, "W", "Yakima", WA - 42, 16, 12, "N", 71, 48, 0, "W", "Worcester", MA - 43, 37, 48, "N", 89, 46, 11, "W", "Wisconsin Dells", WI - 36, 5, 59, "N", 80, 15, 0, "W", "Winston-Salem", NC - 49, 52, 48, "N", 97, 9, 0, "W", "Winnipeg", MB - 39, 11, 23, "N", 78, 9, 36, "W", "Winchester", VA - 34, 14, 24, "N", 77, 55, 11, "W", "Wilmington", NC - 39, 45, 0, "N", 75, 33, 0, "W", "Wilmington", DE - 48, 9, 0, "N", 103, 37, 12, "W", "Williston", ND - 41, 15, 0, "N", 77, 0, 0, "W", "Williamsport", PA - 37, 40, 48, "N", 82, 16, 47, "W", "Williamson", WV - 33, 54, 0, "N", 98, 29, 23, "W", "Wichita Falls", TX - 37, 41, 23, "N", 97, 20, 23, "W", "Wichita", KS - 40, 4, 11, "N", 80, 43, 12, "W", "Wheeling", WV - 26, 43, 11, "N", 80, 3, 0, "W", "West Palm Beach", FL - 47, 25, 11, "N", 120, 19, 11, "W", "Wenatchee", WA - 41, 25, 11, "N", 122, 23, 23, "W", "Weed", CA - 31, 13, 11, "N", 82, 20, 59, "W", "Waycross", GA - 44, 57, 35, "N", 89, 38, 23, "W", "Wausau", WI - 42, 21, 36, "N", 87, 49, 48, "W", "Waukegan", IL - 44, 54, 0, "N", 97, 6, 36, "W", "Watertown", SD - 43, 58, 47, "N", 75, 55, 11, "W", "Watertown", NY - 42, 30, 0, "N", 92, 20, 23, "W", "Waterloo", IA - 41, 32, 59, "N", 73, 3, 0, "W", "Waterbury", CT - 38, 53, 23, "N", 77, 1, 47, "W", "Washington", DC - 41, 50, 59, "N", 79, 8, 23, "W", "Warren", PA - 46, 4, 11, "N", 118, 19, 48, "W", "Walla Walla", WA - 31, 32, 59, "N", 97, 8, 23, "W", "Waco", TX - 38, 40, 48, "N", 87, 31, 47, "W", "Vincennes", IN - 28, 48, 35, "N", 97, 0, 36, "W", "Victoria", TX - 32, 20, 59, "N", 90, 52, 47, "W", "Vicksburg", MS - 49, 16, 12, "N", 123, 7, 12, "W", "Vancouver", BC - 46, 55, 11, "N", 98, 0, 36, "W", "Valley City", ND - 30, 49, 47, "N", 83, 16, 47, "W", "Valdosta", GA - 43, 6, 36, "N", 75, 13, 48, "W", "Utica", NY - 39, 54, 0, "N", 79, 43, 48, "W", "Uniontown", PA - 32, 20, 59, "N", 95, 18, 0, "W", "Tyler", TX - 42, 33, 36, "N", 114, 28, 12, "W", "Twin Falls", ID - 33, 12, 35, "N", 87, 34, 11, "W", "Tuscaloosa", AL - 34, 15, 35, "N", 88, 42, 35, "W", "Tupelo", MS - 36, 9, 35, "N", 95, 54, 36, "W", "Tulsa", OK - 32, 13, 12, "N", 110, 58, 12, "W", "Tucson", AZ - 37, 10, 11, "N", 104, 30, 36, "W", "Trinidad", CO - 40, 13, 47, "N", 74, 46, 11, "W", "Trenton", NJ - 44, 45, 35, "N", 85, 37, 47, "W", "Traverse City", MI - 43, 39, 0, "N", 79, 22, 47, "W", "Toronto", ON - 39, 2, 59, "N", 95, 40, 11, "W", "Topeka", KS - 41, 39, 0, "N", 83, 32, 24, "W", "Toledo", OH - 33, 25, 48, "N", 94, 3, 0, "W", "Texarkana", TX - 39, 28, 12, "N", 87, 24, 36, "W", "Terre Haute", IN - 27, 57, 0, "N", 82, 26, 59, "W", "Tampa", FL - 30, 27, 0, "N", 84, 16, 47, "W", "Tallahassee", FL - 47, 14, 24, "N", 122, 25, 48, "W", "Tacoma", WA - 43, 2, 59, "N", 76, 9, 0, "W", "Syracuse", NY - 32, 35, 59, "N", 82, 20, 23, "W", "Swainsboro", GA - 33, 55, 11, "N", 80, 20, 59, "W", "Sumter", SC - 40, 59, 24, "N", 75, 11, 24, "W", "Stroudsburg", PA - 37, 57, 35, "N", 121, 17, 24, "W", "Stockton", CA - 44, 31, 12, "N", 89, 34, 11, "W", "Stevens Point", WI - 40, 21, 36, "N", 80, 37, 12, "W", "Steubenville", OH - 40, 37, 11, "N", 103, 13, 12, "W", "Sterling", CO - 38, 9, 0, "N", 79, 4, 11, "W", "Staunton", VA - 39, 55, 11, "N", 83, 48, 35, "W", "Springfield", OH - 37, 13, 12, "N", 93, 17, 24, "W", "Springfield", MO - 42, 5, 59, "N", 72, 35, 23, "W", "Springfield", MA - 39, 47, 59, "N", 89, 39, 0, "W", "Springfield", IL - 47, 40, 11, "N", 117, 24, 36, "W", "Spokane", WA - 41, 40, 48, "N", 86, 15, 0, "W", "South Bend", IN - 43, 32, 24, "N", 96, 43, 48, "W", "Sioux Falls", SD - 42, 29, 24, "N", 96, 23, 23, "W", "Sioux City", IA - 32, 30, 35, "N", 93, 45, 0, "W", "Shreveport", LA - 33, 38, 23, "N", 96, 36, 36, "W", "Sherman", TX - 44, 47, 59, "N", 106, 57, 35, "W", "Sheridan", WY - 35, 13, 47, "N", 96, 40, 48, "W", "Seminole", OK - 32, 25, 11, "N", 87, 1, 11, "W", "Selma", AL - 38, 42, 35, "N", 93, 13, 48, "W", "Sedalia", MO - 47, 35, 59, "N", 122, 19, 48, "W", "Seattle", WA - 41, 24, 35, "N", 75, 40, 11, "W", "Scranton", PA - 41, 52, 11, "N", 103, 39, 36, "W", "Scottsbluff", NB - 42, 49, 11, "N", 73, 56, 59, "W", "Schenectady", NY - 32, 4, 48, "N", 81, 5, 23, "W", "Savannah", GA - 46, 29, 24, "N", 84, 20, 59, "W", "Sault Sainte Marie", MI - 27, 20, 24, "N", 82, 31, 47, "W", "Sarasota", FL - 38, 26, 23, "N", 122, 43, 12, "W", "Santa Rosa", CA - 35, 40, 48, "N", 105, 56, 59, "W", "Santa Fe", NM - 34, 25, 11, "N", 119, 41, 59, "W", "Santa Barbara", CA - 33, 45, 35, "N", 117, 52, 12, "W", "Santa Ana", CA - 37, 20, 24, "N", 121, 52, 47, "W", "San Jose", CA - 37, 46, 47, "N", 122, 25, 11, "W", "San Francisco", CA - 41, 27, 0, "N", 82, 42, 35, "W", "Sandusky", OH - 32, 42, 35, "N", 117, 9, 0, "W", "San Diego", CA - 34, 6, 36, "N", 117, 18, 35, "W", "San Bernardino", CA - 29, 25, 12, "N", 98, 30, 0, "W", "San Antonio", TX - 31, 27, 35, "N", 100, 26, 24, "W", "San Angelo", TX - 40, 45, 35, "N", 111, 52, 47, "W", "Salt Lake City", UT - 38, 22, 11, "N", 75, 35, 59, "W", "Salisbury", MD - 36, 40, 11, "N", 121, 39, 0, "W", "Salinas", CA - 38, 50, 24, "N", 97, 36, 36, "W", "Salina", KS - 38, 31, 47, "N", 106, 0, 0, "W", "Salida", CO - 44, 56, 23, "N", 123, 1, 47, "W", "Salem", OR - 44, 57, 0, "N", 93, 5, 59, "W", "Saint Paul", MN - 38, 37, 11, "N", 90, 11, 24, "W", "Saint Louis", MO - 39, 46, 12, "N", 94, 50, 23, "W", "Saint Joseph", MO - 42, 5, 59, "N", 86, 28, 48, "W", "Saint Joseph", MI - 44, 25, 11, "N", 72, 1, 11, "W", "Saint Johnsbury", VT - 45, 34, 11, "N", 94, 10, 11, "W", "Saint Cloud", MN - 29, 53, 23, "N", 81, 19, 11, "W", "Saint Augustine", FL - 43, 25, 48, "N", 83, 56, 24, "W", "Saginaw", MI - 38, 35, 24, "N", 121, 29, 23, "W", "Sacramento", CA - 43, 36, 36, "N", 72, 58, 12, "W", "Rutland", VT - 33, 24, 0, "N", 104, 31, 47, "W", "Roswell", NM - 35, 56, 23, "N", 77, 48, 0, "W", "Rocky Mount", NC - 41, 35, 24, "N", 109, 13, 48, "W", "Rock Springs", WY - 42, 16, 12, "N", 89, 5, 59, "W", "Rockford", IL - 43, 9, 35, "N", 77, 36, 36, "W", "Rochester", NY - 44, 1, 12, "N", 92, 27, 35, "W", "Rochester", MN - 37, 16, 12, "N", 79, 56, 24, "W", "Roanoke", VA - 37, 32, 24, "N", 77, 26, 59, "W", "Richmond", VA - 39, 49, 48, "N", 84, 53, 23, "W", "Richmond", IN - 38, 46, 12, "N", 112, 5, 23, "W", "Richfield", UT - 45, 38, 23, "N", 89, 25, 11, "W", "Rhinelander", WI - 39, 31, 12, "N", 119, 48, 35, "W", "Reno", NV - 50, 25, 11, "N", 104, 39, 0, "W", "Regina", SA - 40, 10, 48, "N", 122, 14, 23, "W", "Red Bluff", CA - 40, 19, 48, "N", 75, 55, 48, "W", "Reading", PA - 41, 9, 35, "N", 81, 14, 23, "W", "Ravenna", OH - diff --git a/hugolib/testdata/fakejson.json b/hugolib/testdata/fakejson.json deleted file mode 100644 index f191b280c..000000000 Binary files a/hugolib/testdata/fakejson.json and /dev/null differ diff --git a/hugolib/testdata/fruits.json b/hugolib/testdata/fruits.json deleted file mode 100644 index 3bb802a16..000000000 --- a/hugolib/testdata/fruits.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "fruit": "Apple", - "size": "Large", - "color": "Red" -} diff --git a/hugolib/testdata/sunset.jpg b/hugolib/testdata/sunset.jpg deleted file mode 100644 index 7d7307bed..000000000 Binary files a/hugolib/testdata/sunset.jpg and /dev/null differ diff --git a/hugolib/testdata/what-is-markdown.md b/hugolib/testdata/what-is-markdown.md deleted file mode 100644 index 87db650b7..000000000 --- a/hugolib/testdata/what-is-markdown.md +++ /dev/null @@ -1,9702 +0,0 @@ -# Introduction - -## What is Markdown? - -Markdown is a plain text format for writing structured documents, -based on conventions for indicating formatting in email -and usenet posts. It was developed by John Gruber (with -help from Aaron Swartz) and released in 2004 in the form of a -[syntax description](http://daringfireball.net/projects/markdown/syntax) -and a Perl script (`Markdown.pl`) for converting Markdown to -HTML. In the next decade, dozens of implementations were -developed in many languages. Some extended the original -Markdown syntax with conventions for footnotes, tables, and -other document elements. Some allowed Markdown documents to be -rendered in formats other than HTML. Websites like Reddit, -StackOverflow, and GitHub had millions of people using Markdown. -And Markdown started to be used beyond the web, to author books, -articles, slide shows, letters, and lecture notes. - -What distinguishes Markdown from many other lightweight markup -syntaxes, which are often easier to write, is its readability. -As Gruber writes: - -> The overriding design goal for Markdown's formatting syntax is -> to make it as readable as possible. The idea is that a -> Markdown-formatted document should be publishable as-is, as -> plain text, without looking like it's been marked up with tags -> or formatting instructions. -> () - -The point can be illustrated by comparing a sample of -[AsciiDoc](http://www.methods.co.nz/asciidoc/) with -an equivalent sample of Markdown. Here is a sample of -AsciiDoc from the AsciiDoc manual: - -``` -1. List item one. -+ -List item one continued with a second paragraph followed by an -Indented block. -+ -................. -$ ls *.sh -$ mv *.sh ~/tmp -................. -+ -List item continued with a third paragraph. - -2. List item two continued with an open block. -+ --- -This paragraph is part of the preceding list item. - -a. This list is nested and does not require explicit item -continuation. -+ -This paragraph is part of the preceding list item. - -b. List item b. - -This paragraph belongs to item two of the outer list. --- -``` - -And here is the equivalent in Markdown: -``` -1. List item one. - - List item one continued with a second paragraph followed by an - Indented block. - - $ ls *.sh - $ mv *.sh ~/tmp - - List item continued with a third paragraph. - -2. List item two continued with an open block. - - This paragraph is part of the preceding list item. - - 1. This list is nested and does not require explicit item continuation. - - This paragraph is part of the preceding list item. - - 2. List item b. - - This paragraph belongs to item two of the outer list. -``` - -The AsciiDoc version is, arguably, easier to write. You don't need -to worry about indentation. But the Markdown version is much easier -to read. The nesting of list items is apparent to the eye in the -source, not just in the processed document. - -## Why is a spec needed? - -John Gruber's [canonical description of Markdown's -syntax](http://daringfireball.net/projects/markdown/syntax) -does not specify the syntax unambiguously. Here are some examples of -questions it does not answer: - -1. How much indentation is needed for a sublist? The spec says that - continuation paragraphs need to be indented four spaces, but is - not fully explicit about sublists. It is natural to think that - they, too, must be indented four spaces, but `Markdown.pl` does - not require that. This is hardly a "corner case," and divergences - between implementations on this issue often lead to surprises for - users in real documents. (See [this comment by John - Gruber](http://article.gmane.org/gmane.text.markdown.general/1997).) - -2. Is a blank line needed before a block quote or heading? - Most implementations do not require the blank line. However, - this can lead to unexpected results in hard-wrapped text, and - also to ambiguities in parsing (note that some implementations - put the heading inside the blockquote, while others do not). - (John Gruber has also spoken [in favor of requiring the blank - lines](http://article.gmane.org/gmane.text.markdown.general/2146).) - -3. Is a blank line needed before an indented code block? - (`Markdown.pl` requires it, but this is not mentioned in the - documentation, and some implementations do not require it.) - - ``` markdown - paragraph - code? - ``` - -4. What is the exact rule for determining when list items get - wrapped in `

    ` tags? Can a list be partially "loose" and partially - "tight"? What should we do with a list like this? - - ``` markdown - 1. one - - 2. two - 3. three - ``` - - Or this? - - ``` markdown - 1. one - - a - - - b - 2. two - ``` - - (There are some relevant comments by John Gruber - [here](http://article.gmane.org/gmane.text.markdown.general/2554).) - -5. Can list markers be indented? Can ordered list markers be right-aligned? - - ``` markdown - 8. item 1 - 9. item 2 - 10. item 2a - ``` - -6. Is this one list with a thematic break in its second item, - or two lists separated by a thematic break? - - ``` markdown - * a - * * * * * - * b - ``` - -7. When list markers change from numbers to bullets, do we have - two lists or one? (The Markdown syntax description suggests two, - but the perl scripts and many other implementations produce one.) - - ``` markdown - 1. fee - 2. fie - - foe - - fum - ``` - -8. What are the precedence rules for the markers of inline structure? - For example, is the following a valid link, or does the code span - take precedence ? - - ``` markdown - [a backtick (`)](/url) and [another backtick (`)](/url). - ``` - -9. What are the precedence rules for markers of emphasis and strong - emphasis? For example, how should the following be parsed? - - ``` markdown - *foo *bar* baz* - ``` - -10. What are the precedence rules between block-level and inline-level - structure? For example, how should the following be parsed? - - ``` markdown - - `a long code span can contain a hyphen like this - - and it can screw things up` - ``` - -11. Can list items include section headings? (`Markdown.pl` does not - allow this, but does allow blockquotes to include headings.) - - ``` markdown - - # Heading - ``` - -12. Can list items be empty? - - ``` markdown - * a - * - * b - ``` - -13. Can link references be defined inside block quotes or list items? - - ``` markdown - > Blockquote [foo]. - > - > [foo]: /url - ``` - -14. If there are multiple definitions for the same reference, which takes - precedence? - - ``` markdown - [foo]: /url1 - [foo]: /url2 - - [foo][] - ``` - -In the absence of a spec, early implementers consulted `Markdown.pl` -to resolve these ambiguities. But `Markdown.pl` was quite buggy, and -gave manifestly bad results in many cases, so it was not a -satisfactory replacement for a spec. - -Because there is no unambiguous spec, implementations have diverged -considerably. As a result, users are often surprised to find that -a document that renders one way on one system (say, a GitHub wiki) -renders differently on another (say, converting to docbook using -pandoc). To make matters worse, because nothing in Markdown counts -as a "syntax error," the divergence often isn't discovered right away. - -## About this document - -This document attempts to specify Markdown syntax unambiguously. -It contains many examples with side-by-side Markdown and -HTML. These are intended to double as conformance tests. An -accompanying script `spec_tests.py` can be used to run the tests -against any Markdown program: - - python test/spec_tests.py --spec spec.txt --program PROGRAM - -Since this document describes how Markdown is to be parsed into -an abstract syntax tree, it would have made sense to use an abstract -representation of the syntax tree instead of HTML. But HTML is capable -of representing the structural distinctions we need to make, and the -choice of HTML for the tests makes it possible to run the tests against -an implementation without writing an abstract syntax tree renderer. - -This document is generated from a text file, `spec.txt`, written -in Markdown with a small extension for the side-by-side tests. -The script `tools/makespec.py` can be used to convert `spec.txt` into -HTML or CommonMark (which can then be converted into other formats). - -In the examples, the `→` character is used to represent tabs. - -# Preliminaries - -## Characters and lines - -Any sequence of [characters] is a valid CommonMark -document. - -A [character](@) is a Unicode code point. Although some -code points (for example, combining accents) do not correspond to -characters in an intuitive sense, all code points count as characters -for purposes of this spec. - -This spec does not specify an encoding; it thinks of lines as composed -of [characters] rather than bytes. A conforming parser may be limited -to a certain encoding. - -A [line](@) is a sequence of zero or more [characters] -other than newline (`U+000A`) or carriage return (`U+000D`), -followed by a [line ending] or by the end of file. - -A [line ending](@) is a newline (`U+000A`), a carriage return -(`U+000D`) not followed by a newline, or a carriage return and a -following newline. - -A line containing no characters, or a line containing only spaces -(`U+0020`) or tabs (`U+0009`), is called a [blank line](@). - -The following definitions of character classes will be used in this spec: - -A [whitespace character](@) is a space -(`U+0020`), tab (`U+0009`), newline (`U+000A`), line tabulation (`U+000B`), -form feed (`U+000C`), or carriage return (`U+000D`). - -[Whitespace](@) is a sequence of one or more [whitespace -characters]. - -A [Unicode whitespace character](@) is -any code point in the Unicode `Zs` general category, or a tab (`U+0009`), -carriage return (`U+000D`), newline (`U+000A`), or form feed -(`U+000C`). - -[Unicode whitespace](@) is a sequence of one -or more [Unicode whitespace characters]. - -A [space](@) is `U+0020`. - -A [non-whitespace character](@) is any character -that is not a [whitespace character]. - -An [ASCII punctuation character](@) -is `!`, `"`, `#`, `$`, `%`, `&`, `'`, `(`, `)`, -`*`, `+`, `,`, `-`, `.`, `/` (U+0021–2F), -`:`, `;`, `<`, `=`, `>`, `?`, `@` (U+003A–0040), -`[`, `\`, `]`, `^`, `_`, `` ` `` (U+005B–0060), -`{`, `|`, `}`, or `~` (U+007B–007E). - -A [punctuation character](@) is an [ASCII -punctuation character] or anything in -the general Unicode categories `Pc`, `Pd`, `Pe`, `Pf`, `Pi`, `Po`, or `Ps`. - -## Tabs - -Tabs in lines are not expanded to [spaces]. However, -in contexts where whitespace helps to define block structure, -tabs behave as if they were replaced by spaces with a tab stop -of 4 characters. - -Thus, for example, a tab can be used instead of four spaces -in an indented code block. (Note, however, that internal -tabs are passed through as literal tabs, not expanded to -spaces.) - -```````````````````````````````` example -→foo→baz→→bim -. -

    foo→baz→→bim
    -
    -```````````````````````````````` - -```````````````````````````````` example - →foo→baz→→bim -. -
    foo→baz→→bim
    -
    -```````````````````````````````` - -```````````````````````````````` example - a→a - ὐ→a -. -
    a→a
    -ὐ→a
    -
    -```````````````````````````````` - -In the following example, a continuation paragraph of a list -item is indented with a tab; this has exactly the same effect -as indentation with four spaces would: - -```````````````````````````````` example - - foo - -→bar -. -
      -
    • -

      foo

      -

      bar

      -
    • -
    -```````````````````````````````` - -```````````````````````````````` example -- foo - -→→bar -. -
      -
    • -

      foo

      -
        bar
      -
      -
    • -
    -```````````````````````````````` - -Normally the `>` that begins a block quote may be followed -optionally by a space, which is not considered part of the -content. In the following case `>` is followed by a tab, -which is treated as if it were expanded into three spaces. -Since one of these spaces is considered part of the -delimiter, `foo` is considered to be indented six spaces -inside the block quote context, so we get an indented -code block starting with two spaces. - -```````````````````````````````` example ->→→foo -. -
    -
      foo
    -
    -
    -```````````````````````````````` - -```````````````````````````````` example --→→foo -. -
      -
    • -
        foo
      -
      -
    • -
    -```````````````````````````````` - - -```````````````````````````````` example - foo -→bar -. -
    foo
    -bar
    -
    -```````````````````````````````` - -```````````````````````````````` example - - foo - - bar -→ - baz -. -
      -
    • foo -
        -
      • bar -
          -
        • baz
        • -
        -
      • -
      -
    • -
    -```````````````````````````````` - -```````````````````````````````` example -#→Foo -. -

    Foo

    -```````````````````````````````` - -```````````````````````````````` example -*→*→*→ -. -
    -```````````````````````````````` - - -## Insecure characters - -For security reasons, the Unicode character `U+0000` must be replaced -with the REPLACEMENT CHARACTER (`U+FFFD`). - -# Blocks and inlines - -We can think of a document as a sequence of -[blocks](@)---structural elements like paragraphs, block -quotations, lists, headings, rules, and code blocks. Some blocks (like -block quotes and list items) contain other blocks; others (like -headings and paragraphs) contain [inline](@) content---text, -links, emphasized text, images, code spans, and so on. - -## Precedence - -Indicators of block structure always take precedence over indicators -of inline structure. So, for example, the following is a list with -two items, not a list with one item containing a code span: - -```````````````````````````````` example -- `one -- two` -. -
      -
    • `one
    • -
    • two`
    • -
    -```````````````````````````````` - - -This means that parsing can proceed in two steps: first, the block -structure of the document can be discerned; second, text lines inside -paragraphs, headings, and other block constructs can be parsed for inline -structure. The second step requires information about link reference -definitions that will be available only at the end of the first -step. Note that the first step requires processing lines in sequence, -but the second can be parallelized, since the inline parsing of -one block element does not affect the inline parsing of any other. - -## Container blocks and leaf blocks - -We can divide blocks into two types: -[container blocks](@), -which can contain other blocks, and [leaf blocks](@), -which cannot. - -# Leaf blocks - -This section describes the different kinds of leaf block that make up a -Markdown document. - -## Thematic breaks - -A line consisting of 0-3 spaces of indentation, followed by a sequence -of three or more matching `-`, `_`, or `*` characters, each followed -optionally by any number of spaces or tabs, forms a -[thematic break](@). - -```````````````````````````````` example -*** ---- -___ -. -
    -
    -
    -```````````````````````````````` - - -Wrong characters: - -```````````````````````````````` example -+++ -. -

    +++

    -```````````````````````````````` - - -```````````````````````````````` example -=== -. -

    ===

    -```````````````````````````````` - - -Not enough characters: - -```````````````````````````````` example --- -** -__ -. -

    -- -** -__

    -```````````````````````````````` - - -One to three spaces indent are allowed: - -```````````````````````````````` example - *** - *** - *** -. -
    -
    -
    -```````````````````````````````` - - -Four spaces is too many: - -```````````````````````````````` example - *** -. -
    ***
    -
    -```````````````````````````````` - - -```````````````````````````````` example -Foo - *** -. -

    Foo -***

    -```````````````````````````````` - - -More than three characters may be used: - -```````````````````````````````` example -_____________________________________ -. -
    -```````````````````````````````` - - -Spaces are allowed between the characters: - -```````````````````````````````` example - - - - -. -
    -```````````````````````````````` - - -```````````````````````````````` example - ** * ** * ** * ** -. -
    -```````````````````````````````` - - -```````````````````````````````` example -- - - - -. -
    -```````````````````````````````` - - -Spaces are allowed at the end: - -```````````````````````````````` example -- - - - -. -
    -```````````````````````````````` - - -However, no other characters may occur in the line: - -```````````````````````````````` example -_ _ _ _ a - -a------ - ----a--- -. -

    _ _ _ _ a

    -

    a------

    -

    ---a---

    -```````````````````````````````` - - -It is required that all of the [non-whitespace characters] be the same. -So, this is not a thematic break: - -```````````````````````````````` example - *-* -. -

    -

    -```````````````````````````````` - - -Thematic breaks do not need blank lines before or after: - -```````````````````````````````` example -- foo -*** -- bar -. -
      -
    • foo
    • -
    -
    -
      -
    • bar
    • -
    -```````````````````````````````` - - -Thematic breaks can interrupt a paragraph: - -```````````````````````````````` example -Foo -*** -bar -. -

    Foo

    -
    -

    bar

    -```````````````````````````````` - - -If a line of dashes that meets the above conditions for being a -thematic break could also be interpreted as the underline of a [setext -heading], the interpretation as a -[setext heading] takes precedence. Thus, for example, -this is a setext heading, not a paragraph followed by a thematic break: - -```````````````````````````````` example -Foo ---- -bar -. -

    Foo

    -

    bar

    -```````````````````````````````` - - -When both a thematic break and a list item are possible -interpretations of a line, the thematic break takes precedence: - -```````````````````````````````` example -* Foo -* * * -* Bar -. -
      -
    • Foo
    • -
    -
    -
      -
    • Bar
    • -
    -```````````````````````````````` - - -If you want a thematic break in a list item, use a different bullet: - -```````````````````````````````` example -- Foo -- * * * -. -
      -
    • Foo
    • -
    • -
      -
    • -
    -```````````````````````````````` - - -## ATX headings - -An [ATX heading](@) -consists of a string of characters, parsed as inline content, between an -opening sequence of 1--6 unescaped `#` characters and an optional -closing sequence of any number of unescaped `#` characters. -The opening sequence of `#` characters must be followed by a -[space] or by the end of line. The optional closing sequence of `#`s must be -preceded by a [space] and may be followed by spaces only. The opening -`#` character may be indented 0-3 spaces. The raw contents of the -heading are stripped of leading and trailing spaces before being parsed -as inline content. The heading level is equal to the number of `#` -characters in the opening sequence. - -Simple headings: - -```````````````````````````````` example -# foo -## foo -### foo -#### foo -##### foo -###### foo -. -

    foo

    -

    foo

    -

    foo

    -

    foo

    -
    foo
    -
    foo
    -```````````````````````````````` - - -More than six `#` characters is not a heading: - -```````````````````````````````` example -####### foo -. -

    ####### foo

    -```````````````````````````````` - - -At least one space is required between the `#` characters and the -heading's contents, unless the heading is empty. Note that many -implementations currently do not require the space. However, the -space was required by the -[original ATX implementation](http://www.aaronsw.com/2002/atx/atx.py), -and it helps prevent things like the following from being parsed as -headings: - -```````````````````````````````` example -#5 bolt - -#hashtag -. -

    #5 bolt

    -

    #hashtag

    -```````````````````````````````` - - -This is not a heading, because the first `#` is escaped: - -```````````````````````````````` example -\## foo -. -

    ## foo

    -```````````````````````````````` - - -Contents are parsed as inlines: - -```````````````````````````````` example -# foo *bar* \*baz\* -. -

    foo bar *baz*

    -```````````````````````````````` - - -Leading and trailing [whitespace] is ignored in parsing inline content: - -```````````````````````````````` example -# foo -. -

    foo

    -```````````````````````````````` - - -One to three spaces indentation are allowed: - -```````````````````````````````` example - ### foo - ## foo - # foo -. -

    foo

    -

    foo

    -

    foo

    -```````````````````````````````` - - -Four spaces are too much: - -```````````````````````````````` example - # foo -. -
    # foo
    -
    -```````````````````````````````` - - -```````````````````````````````` example -foo - # bar -. -

    foo -# bar

    -```````````````````````````````` - - -A closing sequence of `#` characters is optional: - -```````````````````````````````` example -## foo ## - ### bar ### -. -

    foo

    -

    bar

    -```````````````````````````````` - - -It need not be the same length as the opening sequence: - -```````````````````````````````` example -# foo ################################## -##### foo ## -. -

    foo

    -
    foo
    -```````````````````````````````` - - -Spaces are allowed after the closing sequence: - -```````````````````````````````` example -### foo ### -. -

    foo

    -```````````````````````````````` - - -A sequence of `#` characters with anything but [spaces] following it -is not a closing sequence, but counts as part of the contents of the -heading: - -```````````````````````````````` example -### foo ### b -. -

    foo ### b

    -```````````````````````````````` - - -The closing sequence must be preceded by a space: - -```````````````````````````````` example -# foo# -. -

    foo#

    -```````````````````````````````` - - -Backslash-escaped `#` characters do not count as part -of the closing sequence: - -```````````````````````````````` example -### foo \### -## foo #\## -# foo \# -. -

    foo ###

    -

    foo ###

    -

    foo #

    -```````````````````````````````` - - -ATX headings need not be separated from surrounding content by blank -lines, and they can interrupt paragraphs: - -```````````````````````````````` example -**** -## foo -**** -. -
    -

    foo

    -
    -```````````````````````````````` - - -```````````````````````````````` example -Foo bar -# baz -Bar foo -. -

    Foo bar

    -

    baz

    -

    Bar foo

    -```````````````````````````````` - - -ATX headings can be empty: - -```````````````````````````````` example -## -# -### ### -. -

    -

    -

    -```````````````````````````````` - - -## Setext headings - -A [setext heading](@) consists of one or more -lines of text, each containing at least one [non-whitespace -character], with no more than 3 spaces indentation, followed by -a [setext heading underline]. The lines of text must be such -that, were they not followed by the setext heading underline, -they would be interpreted as a paragraph: they cannot be -interpretable as a [code fence], [ATX heading][ATX headings], -[block quote][block quotes], [thematic break][thematic breaks], -[list item][list items], or [HTML block][HTML blocks]. - -A [setext heading underline](@) is a sequence of -`=` characters or a sequence of `-` characters, with no more than 3 -spaces indentation and any number of trailing spaces. If a line -containing a single `-` can be interpreted as an -empty [list items], it should be interpreted this way -and not as a [setext heading underline]. - -The heading is a level 1 heading if `=` characters are used in -the [setext heading underline], and a level 2 heading if `-` -characters are used. The contents of the heading are the result -of parsing the preceding lines of text as CommonMark inline -content. - -In general, a setext heading need not be preceded or followed by a -blank line. However, it cannot interrupt a paragraph, so when a -setext heading comes after a paragraph, a blank line is needed between -them. - -Simple examples: - -```````````````````````````````` example -Foo *bar* -========= - -Foo *bar* ---------- -. -

    Foo bar

    -

    Foo bar

    -```````````````````````````````` - - -The content of the header may span more than one line: - -```````````````````````````````` example -Foo *bar -baz* -==== -. -

    Foo bar -baz

    -```````````````````````````````` - -The contents are the result of parsing the headings's raw -content as inlines. The heading's raw content is formed by -concatenating the lines and removing initial and final -[whitespace]. - -```````````````````````````````` example - Foo *bar -baz*→ -==== -. -

    Foo bar -baz

    -```````````````````````````````` - - -The underlining can be any length: - -```````````````````````````````` example -Foo -------------------------- - -Foo -= -. -

    Foo

    -

    Foo

    -```````````````````````````````` - - -The heading content can be indented up to three spaces, and need -not line up with the underlining: - -```````````````````````````````` example - Foo ---- - - Foo ------ - - Foo - === -. -

    Foo

    -

    Foo

    -

    Foo

    -```````````````````````````````` - - -Four spaces indent is too much: - -```````````````````````````````` example - Foo - --- - - Foo ---- -. -
    Foo
    ----
    -
    -Foo
    -
    -
    -```````````````````````````````` - - -The setext heading underline can be indented up to three spaces, and -may have trailing spaces: - -```````````````````````````````` example -Foo - ---- -. -

    Foo

    -```````````````````````````````` - - -Four spaces is too much: - -```````````````````````````````` example -Foo - --- -. -

    Foo ----

    -```````````````````````````````` - - -The setext heading underline cannot contain internal spaces: - -```````````````````````````````` example -Foo -= = - -Foo ---- - -. -

    Foo -= =

    -

    Foo

    -
    -```````````````````````````````` - - -Trailing spaces in the content line do not cause a line break: - -```````````````````````````````` example -Foo ------ -. -

    Foo

    -```````````````````````````````` - - -Nor does a backslash at the end: - -```````````````````````````````` example -Foo\ ----- -. -

    Foo\

    -```````````````````````````````` - - -Since indicators of block structure take precedence over -indicators of inline structure, the following are setext headings: - -```````````````````````````````` example -`Foo ----- -` - - -. -

    `Foo

    -

    `

    -

    <a title="a lot

    -

    of dashes"/>

    -```````````````````````````````` - - -The setext heading underline cannot be a [lazy continuation -line] in a list item or block quote: - -```````````````````````````````` example -> Foo ---- -. -
    -

    Foo

    -
    -
    -```````````````````````````````` - - -```````````````````````````````` example -> foo -bar -=== -. -
    -

    foo -bar -===

    -
    -```````````````````````````````` - - -```````````````````````````````` example -- Foo ---- -. -
      -
    • Foo
    • -
    -
    -```````````````````````````````` - - -A blank line is needed between a paragraph and a following -setext heading, since otherwise the paragraph becomes part -of the heading's content: - -```````````````````````````````` example -Foo -Bar ---- -. -

    Foo -Bar

    -```````````````````````````````` - - -But in general a blank line is not required before or after -setext headings: - -```````````````````````````````` example ---- -Foo ---- -Bar ---- -Baz -. -
    -

    Foo

    -

    Bar

    -

    Baz

    -```````````````````````````````` - - -Setext headings cannot be empty: - -```````````````````````````````` example - -==== -. -

    ====

    -```````````````````````````````` - - -Setext heading text lines must not be interpretable as block -constructs other than paragraphs. So, the line of dashes -in these examples gets interpreted as a thematic break: - -```````````````````````````````` example ---- ---- -. -
    -
    -```````````````````````````````` - - -```````````````````````````````` example -- foo ------ -. -
      -
    • foo
    • -
    -
    -```````````````````````````````` - - -```````````````````````````````` example - foo ---- -. -
    foo
    -
    -
    -```````````````````````````````` - - -```````````````````````````````` example -> foo ------ -. -
    -

    foo

    -
    -
    -```````````````````````````````` - - -If you want a heading with `> foo` as its literal text, you can -use backslash escapes: - -```````````````````````````````` example -\> foo ------- -. -

    > foo

    -```````````````````````````````` - - -**Compatibility note:** Most existing Markdown implementations -do not allow the text of setext headings to span multiple lines. -But there is no consensus about how to interpret - -``` markdown -Foo -bar ---- -baz -``` - -One can find four different interpretations: - -1. paragraph "Foo", heading "bar", paragraph "baz" -2. paragraph "Foo bar", thematic break, paragraph "baz" -3. paragraph "Foo bar --- baz" -4. heading "Foo bar", paragraph "baz" - -We find interpretation 4 most natural, and interpretation 4 -increases the expressive power of CommonMark, by allowing -multiline headings. Authors who want interpretation 1 can -put a blank line after the first paragraph: - -```````````````````````````````` example -Foo - -bar ---- -baz -. -

    Foo

    -

    bar

    -

    baz

    -```````````````````````````````` - - -Authors who want interpretation 2 can put blank lines around -the thematic break, - -```````````````````````````````` example -Foo -bar - ---- - -baz -. -

    Foo -bar

    -
    -

    baz

    -```````````````````````````````` - - -or use a thematic break that cannot count as a [setext heading -underline], such as - -```````````````````````````````` example -Foo -bar -* * * -baz -. -

    Foo -bar

    -
    -

    baz

    -```````````````````````````````` - - -Authors who want interpretation 3 can use backslash escapes: - -```````````````````````````````` example -Foo -bar -\--- -baz -. -

    Foo -bar ---- -baz

    -```````````````````````````````` - - -## Indented code blocks - -An [indented code block](@) is composed of one or more -[indented chunks] separated by blank lines. -An [indented chunk](@) is a sequence of non-blank lines, -each indented four or more spaces. The contents of the code block are -the literal contents of the lines, including trailing -[line endings], minus four spaces of indentation. -An indented code block has no [info string]. - -An indented code block cannot interrupt a paragraph, so there must be -a blank line between a paragraph and a following indented code block. -(A blank line is not needed, however, between a code block and a following -paragraph.) - -```````````````````````````````` example - a simple - indented code block -. -
    a simple
    -  indented code block
    -
    -```````````````````````````````` - - -If there is any ambiguity between an interpretation of indentation -as a code block and as indicating that material belongs to a [list -item][list items], the list item interpretation takes precedence: - -```````````````````````````````` example - - foo - - bar -. -
      -
    • -

      foo

      -

      bar

      -
    • -
    -```````````````````````````````` - - -```````````````````````````````` example -1. foo - - - bar -. -
      -
    1. -

      foo

      -
        -
      • bar
      • -
      -
    2. -
    -```````````````````````````````` - - - -The contents of a code block are literal text, and do not get parsed -as Markdown: - -```````````````````````````````` example -
    - *hi* - - - one -. -
    <a/>
    -*hi*
    -
    -- one
    -
    -```````````````````````````````` - - -Here we have three chunks separated by blank lines: - -```````````````````````````````` example - chunk1 - - chunk2 - - - - chunk3 -. -
    chunk1
    -
    -chunk2
    -
    -
    -
    -chunk3
    -
    -```````````````````````````````` - - -Any initial spaces beyond four will be included in the content, even -in interior blank lines: - -```````````````````````````````` example - chunk1 - - chunk2 -. -
    chunk1
    -  
    -  chunk2
    -
    -```````````````````````````````` - - -An indented code block cannot interrupt a paragraph. (This -allows hanging indents and the like.) - -```````````````````````````````` example -Foo - bar - -. -

    Foo -bar

    -```````````````````````````````` - - -However, any non-blank line with fewer than four leading spaces ends -the code block immediately. So a paragraph may occur immediately -after indented code: - -```````````````````````````````` example - foo -bar -. -
    foo
    -
    -

    bar

    -```````````````````````````````` - - -And indented code can occur immediately before and after other kinds of -blocks: - -```````````````````````````````` example -# Heading - foo -Heading ------- - foo ----- -. -

    Heading

    -
    foo
    -
    -

    Heading

    -
    foo
    -
    -
    -```````````````````````````````` - - -The first line can be indented more than four spaces: - -```````````````````````````````` example - foo - bar -. -
        foo
    -bar
    -
    -```````````````````````````````` - - -Blank lines preceding or following an indented code block -are not included in it: - -```````````````````````````````` example - - - foo - - -. -
    foo
    -
    -```````````````````````````````` - - -Trailing spaces are included in the code block's content: - -```````````````````````````````` example - foo -. -
    foo  
    -
    -```````````````````````````````` - - - -## Fenced code blocks - -A [code fence](@) is a sequence -of at least three consecutive backtick characters (`` ` ``) or -tildes (`~`). (Tildes and backticks cannot be mixed.) -A [fenced code block](@) -begins with a code fence, indented no more than three spaces. - -The line with the opening code fence may optionally contain some text -following the code fence; this is trimmed of leading and trailing -whitespace and called the [info string](@). If the [info string] comes -after a backtick fence, it may not contain any backtick -characters. (The reason for this restriction is that otherwise -some inline code would be incorrectly interpreted as the -beginning of a fenced code block.) - -The content of the code block consists of all subsequent lines, until -a closing [code fence] of the same type as the code block -began with (backticks or tildes), and with at least as many backticks -or tildes as the opening code fence. If the leading code fence is -indented N spaces, then up to N spaces of indentation are removed from -each line of the content (if present). (If a content line is not -indented, it is preserved unchanged. If it is indented less than N -spaces, all of the indentation is removed.) - -The closing code fence may be indented up to three spaces, and may be -followed only by spaces, which are ignored. If the end of the -containing block (or document) is reached and no closing code fence -has been found, the code block contains all of the lines after the -opening code fence until the end of the containing block (or -document). (An alternative spec would require backtracking in the -event that a closing code fence is not found. But this makes parsing -much less efficient, and there seems to be no real down side to the -behavior described here.) - -A fenced code block may interrupt a paragraph, and does not require -a blank line either before or after. - -The content of a code fence is treated as literal text, not parsed -as inlines. The first word of the [info string] is typically used to -specify the language of the code sample, and rendered in the `class` -attribute of the `code` tag. However, this spec does not mandate any -particular treatment of the [info string]. - -Here is a simple example with backticks: - -```````````````````````````````` example -``` -< - > -``` -. -
    <
    - >
    -
    -```````````````````````````````` - - -With tildes: - -```````````````````````````````` example -~~~ -< - > -~~~ -. -
    <
    - >
    -
    -```````````````````````````````` - -Fewer than three backticks is not enough: - -```````````````````````````````` example -`` -foo -`` -. -

    foo

    -```````````````````````````````` - -The closing code fence must use the same character as the opening -fence: - -```````````````````````````````` example -``` -aaa -~~~ -``` -. -
    aaa
    -~~~
    -
    -```````````````````````````````` - - -```````````````````````````````` example -~~~ -aaa -``` -~~~ -. -
    aaa
    -```
    -
    -```````````````````````````````` - - -The closing code fence must be at least as long as the opening fence: - -```````````````````````````````` example -```` -aaa -``` -`````` -. -
    aaa
    -```
    -
    -```````````````````````````````` - - -```````````````````````````````` example -~~~~ -aaa -~~~ -~~~~ -. -
    aaa
    -~~~
    -
    -```````````````````````````````` - - -Unclosed code blocks are closed by the end of the document -(or the enclosing [block quote][block quotes] or [list item][list items]): - -```````````````````````````````` example -``` -. -
    -```````````````````````````````` - - -```````````````````````````````` example -````` - -``` -aaa -. -
    
    -```
    -aaa
    -
    -```````````````````````````````` - - -```````````````````````````````` example -> ``` -> aaa - -bbb -. -
    -
    aaa
    -
    -
    -

    bbb

    -```````````````````````````````` - - -A code block can have all empty lines as its content: - -```````````````````````````````` example -``` - - -``` -. -
    
    -  
    -
    -```````````````````````````````` - - -A code block can be empty: - -```````````````````````````````` example -``` -``` -. -
    -```````````````````````````````` - - -Fences can be indented. If the opening fence is indented, -content lines will have equivalent opening indentation removed, -if present: - -```````````````````````````````` example - ``` - aaa -aaa -``` -. -
    aaa
    -aaa
    -
    -```````````````````````````````` - - -```````````````````````````````` example - ``` -aaa - aaa -aaa - ``` -. -
    aaa
    -aaa
    -aaa
    -
    -```````````````````````````````` - - -```````````````````````````````` example - ``` - aaa - aaa - aaa - ``` -. -
    aaa
    - aaa
    -aaa
    -
    -```````````````````````````````` - - -Four spaces indentation produces an indented code block: - -```````````````````````````````` example - ``` - aaa - ``` -. -
    ```
    -aaa
    -```
    -
    -```````````````````````````````` - - -Closing fences may be indented by 0-3 spaces, and their indentation -need not match that of the opening fence: - -```````````````````````````````` example -``` -aaa - ``` -. -
    aaa
    -
    -```````````````````````````````` - - -```````````````````````````````` example - ``` -aaa - ``` -. -
    aaa
    -
    -```````````````````````````````` - - -This is not a closing fence, because it is indented 4 spaces: - -```````````````````````````````` example -``` -aaa - ``` -. -
    aaa
    -    ```
    -
    -```````````````````````````````` - - - -Code fences (opening and closing) cannot contain internal spaces: - -```````````````````````````````` example -``` ``` -aaa -. -

    -aaa

    -```````````````````````````````` - - -```````````````````````````````` example -~~~~~~ -aaa -~~~ ~~ -. -
    aaa
    -~~~ ~~
    -
    -```````````````````````````````` - - -Fenced code blocks can interrupt paragraphs, and can be followed -directly by paragraphs, without a blank line between: - -```````````````````````````````` example -foo -``` -bar -``` -baz -. -

    foo

    -
    bar
    -
    -

    baz

    -```````````````````````````````` - - -Other blocks can also occur before and after fenced code blocks -without an intervening blank line: - -```````````````````````````````` example -foo ---- -~~~ -bar -~~~ -# baz -. -

    foo

    -
    bar
    -
    -

    baz

    -```````````````````````````````` - - -An [info string] can be provided after the opening code fence. -Although this spec doesn't mandate any particular treatment of -the info string, the first word is typically used to specify -the language of the code block. In HTML output, the language is -normally indicated by adding a class to the `code` element consisting -of `language-` followed by the language name. - -```````````````````````````````` example -```ruby -def foo(x) - return 3 -end -``` -. -
    def foo(x)
    -  return 3
    -end
    -
    -```````````````````````````````` - - -```````````````````````````````` example -~~~~ ruby startline=3 $%@#$ -def foo(x) - return 3 -end -~~~~~~~ -. -
    def foo(x)
    -  return 3
    -end
    -
    -```````````````````````````````` - - -```````````````````````````````` example -````; -```` -. -
    -```````````````````````````````` - - -[Info strings] for backtick code blocks cannot contain backticks: - -```````````````````````````````` example -``` aa ``` -foo -. -

    aa -foo

    -```````````````````````````````` - - -[Info strings] for tilde code blocks can contain backticks and tildes: - -```````````````````````````````` example -~~~ aa ``` ~~~ -foo -~~~ -. -
    foo
    -
    -```````````````````````````````` - - -Closing code fences cannot have [info strings]: - -```````````````````````````````` example -``` -``` aaa -``` -. -
    ``` aaa
    -
    -```````````````````````````````` - - - -## HTML blocks - -An [HTML block](@) is a group of lines that is treated -as raw HTML (and will not be escaped in HTML output). - -There are seven kinds of [HTML block], which can be defined by their -start and end conditions. The block begins with a line that meets a -[start condition](@) (after up to three spaces optional indentation). -It ends with the first subsequent line that meets a matching [end -condition](@), or the last line of the document, or the last line of -the [container block](#container-blocks) containing the current HTML -block, if no line is encountered that meets the [end condition]. If -the first line meets both the [start condition] and the [end -condition], the block will contain just that line. - -1. **Start condition:** line begins with the string ``, or the end of the line.\ -**End condition:** line contains an end tag -``, ``, or `` (case-insensitive; it -need not match the start tag). - -2. **Start condition:** line begins with the string ``. - -3. **Start condition:** line begins with the string ``. - -4. **Start condition:** line begins with the string ``. - -5. **Start condition:** line begins with the string -``. - -6. **Start condition:** line begins the string `<` or ``, or -the string `/>`.\ -**End condition:** line is followed by a [blank line]. - -7. **Start condition:** line begins with a complete [open tag] -(with any [tag name] other than `script`, -`style`, or `pre`) or a complete [closing tag], -followed only by [whitespace] or the end of the line.\ -**End condition:** line is followed by a [blank line]. - -HTML blocks continue until they are closed by their appropriate -[end condition], or the last line of the document or other [container -block](#container-blocks). This means any HTML **within an HTML -block** that might otherwise be recognised as a start condition will -be ignored by the parser and passed through as-is, without changing -the parser's state. - -For instance, `
    ` within a HTML block started by `` will not affect
    -the parser state; as the HTML block was started in by start condition 6, it
    -will end at any blank line. This can be surprising:
    -
    -```````````````````````````````` example
    -
    -
    -**Hello**,
    -
    -_world_.
    -
    -
    -. -
    -
    -**Hello**,
    -

    world. -

    -
    -```````````````````````````````` - -In this case, the HTML block is terminated by the newline — the `**Hello**` -text remains verbatim — and regular parsing resumes, with a paragraph, -emphasised `world` and inline and block HTML following. - -All types of [HTML blocks] except type 7 may interrupt -a paragraph. Blocks of type 7 may not interrupt a paragraph. -(This restriction is intended to prevent unwanted interpretation -of long tags inside a wrapped paragraph as starting HTML blocks.) - -Some simple examples follow. Here are some basic HTML blocks -of type 6: - -```````````````````````````````` example - - - - -
    - hi -
    - -okay. -. - - - - -
    - hi -
    -

    okay.

    -```````````````````````````````` - - -```````````````````````````````` example -
    -*foo* -```````````````````````````````` - - -Here we have two HTML blocks with a Markdown paragraph between them: - -```````````````````````````````` example -
    - -*Markdown* - -
    -. -
    -

    Markdown

    -
    -```````````````````````````````` - - -The tag on the first line can be partial, as long -as it is split where there would be whitespace: - -```````````````````````````````` example -
    -
    -. -
    -
    -```````````````````````````````` - - -```````````````````````````````` example -
    -
    -. -
    -
    -```````````````````````````````` - - -An open tag need not be closed: -```````````````````````````````` example -
    -*foo* - -*bar* -. -
    -*foo* -

    bar

    -```````````````````````````````` - - - -A partial tag need not even be completed (garbage -in, garbage out): - -```````````````````````````````` example -
    -. - -```````````````````````````````` - - -```````````````````````````````` example -
    -foo -
    -. -
    -foo -
    -```````````````````````````````` - - -Everything until the next blank line or end of document -gets included in the HTML block. So, in the following -example, what looks like a Markdown code block -is actually part of the HTML block, which continues until a blank -line or the end of the document is reached: - -```````````````````````````````` example -
    -``` c -int x = 33; -``` -. -
    -``` c -int x = 33; -``` -```````````````````````````````` - - -To start an [HTML block] with a tag that is *not* in the -list of block-level tags in (6), you must put the tag by -itself on the first line (and it must be complete): - -```````````````````````````````` example - -*bar* - -. - -*bar* - -```````````````````````````````` - - -In type 7 blocks, the [tag name] can be anything: - -```````````````````````````````` example - -*bar* - -. - -*bar* - -```````````````````````````````` - - -```````````````````````````````` example - -*bar* - -. - -*bar* - -```````````````````````````````` - - -```````````````````````````````` example - -*bar* -. - -*bar* -```````````````````````````````` - - -These rules are designed to allow us to work with tags that -can function as either block-level or inline-level tags. -The `` tag is a nice example. We can surround content with -`` tags in three different ways. In this case, we get a raw -HTML block, because the `` tag is on a line by itself: - -```````````````````````````````` example - -*foo* - -. - -*foo* - -```````````````````````````````` - - -In this case, we get a raw HTML block that just includes -the `` tag (because it ends with the following blank -line). So the contents get interpreted as CommonMark: - -```````````````````````````````` example - - -*foo* - - -. - -

    foo

    -
    -```````````````````````````````` - - -Finally, in this case, the `` tags are interpreted -as [raw HTML] *inside* the CommonMark paragraph. (Because -the tag is not on a line by itself, we get inline HTML -rather than an [HTML block].) - -```````````````````````````````` example -*foo* -. -

    foo

    -```````````````````````````````` - - -HTML tags designed to contain literal content -(`script`, `style`, `pre`), comments, processing instructions, -and declarations are treated somewhat differently. -Instead of ending at the first blank line, these blocks -end at the first line containing a corresponding end tag. -As a result, these blocks can contain blank lines: - -A pre tag (type 1): - -```````````````````````````````` example -
    
    -import Text.HTML.TagSoup
    -
    -main :: IO ()
    -main = print $ parseTags tags
    -
    -okay -. -
    
    -import Text.HTML.TagSoup
    -
    -main :: IO ()
    -main = print $ parseTags tags
    -
    -

    okay

    -```````````````````````````````` - - -A script tag (type 1): - -```````````````````````````````` example - -okay -. - -

    okay

    -```````````````````````````````` - - -A style tag (type 1): - -```````````````````````````````` example - -okay -. - -

    okay

    -```````````````````````````````` - - -If there is no matching end tag, the block will end at the -end of the document (or the enclosing [block quote][block quotes] -or [list item][list items]): - -```````````````````````````````` example - -*foo* -. - -

    foo

    -```````````````````````````````` - - -```````````````````````````````` example -*bar* -*baz* -. -*bar* -

    baz

    -```````````````````````````````` - - -Note that anything on the last line after the -end tag will be included in the [HTML block]: - -```````````````````````````````` example -1. *bar* -. -1. *bar* -```````````````````````````````` - - -A comment (type 2): - -```````````````````````````````` example - -okay -. - -

    okay

    -```````````````````````````````` - - - -A processing instruction (type 3): - -```````````````````````````````` example -'; - -?> -okay -. -'; - -?> -

    okay

    -```````````````````````````````` - - -A declaration (type 4): - -```````````````````````````````` example - -. - -```````````````````````````````` - - -CDATA (type 5): - -```````````````````````````````` example - -okay -. - -

    okay

    -```````````````````````````````` - - -The opening tag can be indented 1-3 spaces, but not 4: - -```````````````````````````````` example - - - -. - -
    <!-- foo -->
    -
    -```````````````````````````````` - - -```````````````````````````````` example -
    - -
    -. -
    -
    <div>
    -
    -```````````````````````````````` - - -An HTML block of types 1--6 can interrupt a paragraph, and need not be -preceded by a blank line. - -```````````````````````````````` example -Foo -
    -bar -
    -. -

    Foo

    -
    -bar -
    -```````````````````````````````` - - -However, a following blank line is needed, except at the end of -a document, and except for blocks of types 1--5, [above][HTML -block]: - -```````````````````````````````` example -
    -bar -
    -*foo* -. -
    -bar -
    -*foo* -```````````````````````````````` - - -HTML blocks of type 7 cannot interrupt a paragraph: - -```````````````````````````````` example -Foo - -baz -. -

    Foo - -baz

    -```````````````````````````````` - - -This rule differs from John Gruber's original Markdown syntax -specification, which says: - -> The only restrictions are that block-level HTML elements — -> e.g. `
    `, ``, `
    `, `

    `, etc. — must be separated from -> surrounding content by blank lines, and the start and end tags of the -> block should not be indented with tabs or spaces. - -In some ways Gruber's rule is more restrictive than the one given -here: - -- It requires that an HTML block be preceded by a blank line. -- It does not allow the start tag to be indented. -- It requires a matching end tag, which it also does not allow to - be indented. - -Most Markdown implementations (including some of Gruber's own) do not -respect all of these restrictions. - -There is one respect, however, in which Gruber's rule is more liberal -than the one given here, since it allows blank lines to occur inside -an HTML block. There are two reasons for disallowing them here. -First, it removes the need to parse balanced tags, which is -expensive and can require backtracking from the end of the document -if no matching end tag is found. Second, it provides a very simple -and flexible way of including Markdown content inside HTML tags: -simply separate the Markdown from the HTML using blank lines: - -Compare: - -```````````````````````````````` example -

    - -*Emphasized* text. - -
    -. -
    -

    Emphasized text.

    -
    -```````````````````````````````` - - -```````````````````````````````` example -
    -*Emphasized* text. -
    -. -
    -*Emphasized* text. -
    -```````````````````````````````` - - -Some Markdown implementations have adopted a convention of -interpreting content inside tags as text if the open tag has -the attribute `markdown=1`. The rule given above seems a simpler and -more elegant way of achieving the same expressive power, which is also -much simpler to parse. - -The main potential drawback is that one can no longer paste HTML -blocks into Markdown documents with 100% reliability. However, -*in most cases* this will work fine, because the blank lines in -HTML are usually followed by HTML block tags. For example: - -```````````````````````````````` example -
    - - - - - - - -
    -Hi -
    -. - - - - -
    -Hi -
    -```````````````````````````````` - - -There are problems, however, if the inner tags are indented -*and* separated by spaces, as then they will be interpreted as -an indented code block: - -```````````````````````````````` example - - - - - - - - -
    - Hi -
    -. - - -
    <td>
    -  Hi
    -</td>
    -
    - -
    -```````````````````````````````` - - -Fortunately, blank lines are usually not necessary and can be -deleted. The exception is inside `
    ` tags, but as described
    -[above][HTML blocks], raw HTML blocks starting with `
    `
    -*can* contain blank lines.
    -
    -## Link reference definitions
    -
    -A [link reference definition](@)
    -consists of a [link label], indented up to three spaces, followed
    -by a colon (`:`), optional [whitespace] (including up to one
    -[line ending]), a [link destination],
    -optional [whitespace] (including up to one
    -[line ending]), and an optional [link
    -title], which if it is present must be separated
    -from the [link destination] by [whitespace].
    -No further [non-whitespace characters] may occur on the line.
    -
    -A [link reference definition]
    -does not correspond to a structural element of a document.  Instead, it
    -defines a label which can be used in [reference links]
    -and reference-style [images] elsewhere in the document.  [Link
    -reference definitions] can come either before or after the links that use
    -them.
    -
    -```````````````````````````````` example
    -[foo]: /url "title"
    -
    -[foo]
    -.
    -

    foo

    -```````````````````````````````` - - -```````````````````````````````` example - [foo]: - /url - 'the title' - -[foo] -. -

    foo

    -```````````````````````````````` - - -```````````````````````````````` example -[Foo*bar\]]:my_(url) 'title (with parens)' - -[Foo*bar\]] -. -

    Foo*bar]

    -```````````````````````````````` - - -```````````````````````````````` example -[Foo bar]: - -'title' - -[Foo bar] -. -

    Foo bar

    -```````````````````````````````` - - -The title may extend over multiple lines: - -```````````````````````````````` example -[foo]: /url ' -title -line1 -line2 -' - -[foo] -. -

    foo

    -```````````````````````````````` - - -However, it may not contain a [blank line]: - -```````````````````````````````` example -[foo]: /url 'title - -with blank line' - -[foo] -. -

    [foo]: /url 'title

    -

    with blank line'

    -

    [foo]

    -```````````````````````````````` - - -The title may be omitted: - -```````````````````````````````` example -[foo]: -/url - -[foo] -. -

    foo

    -```````````````````````````````` - - -The link destination may not be omitted: - -```````````````````````````````` example -[foo]: - -[foo] -. -

    [foo]:

    -

    [foo]

    -```````````````````````````````` - - However, an empty link destination may be specified using - angle brackets: - -```````````````````````````````` example -[foo]: <> - -[foo] -. -

    foo

    -```````````````````````````````` - -The title must be separated from the link destination by -whitespace: - -```````````````````````````````` example -[foo]: (baz) - -[foo] -. -

    [foo]: (baz)

    -

    [foo]

    -```````````````````````````````` - - -Both title and destination can contain backslash escapes -and literal backslashes: - -```````````````````````````````` example -[foo]: /url\bar\*baz "foo\"bar\baz" - -[foo] -. -

    foo

    -```````````````````````````````` - - -A link can come before its corresponding definition: - -```````````````````````````````` example -[foo] - -[foo]: url -. -

    foo

    -```````````````````````````````` - - -If there are several matching definitions, the first one takes -precedence: - -```````````````````````````````` example -[foo] - -[foo]: first -[foo]: second -. -

    foo

    -```````````````````````````````` - - -As noted in the section on [Links], matching of labels is -case-insensitive (see [matches]). - -```````````````````````````````` example -[FOO]: /url - -[Foo] -. -

    Foo

    -```````````````````````````````` - - -```````````````````````````````` example -[ΑΓΩ]: /φου - -[αγω] -. -

    αγω

    -```````````````````````````````` - - -Here is a link reference definition with no corresponding link. -It contributes nothing to the document. - -```````````````````````````````` example -[foo]: /url -. -```````````````````````````````` - - -Here is another one: - -```````````````````````````````` example -[ -foo -]: /url -bar -. -

    bar

    -```````````````````````````````` - - -This is not a link reference definition, because there are -[non-whitespace characters] after the title: - -```````````````````````````````` example -[foo]: /url "title" ok -. -

    [foo]: /url "title" ok

    -```````````````````````````````` - - -This is a link reference definition, but it has no title: - -```````````````````````````````` example -[foo]: /url -"title" ok -. -

    "title" ok

    -```````````````````````````````` - - -This is not a link reference definition, because it is indented -four spaces: - -```````````````````````````````` example - [foo]: /url "title" - -[foo] -. -
    [foo]: /url "title"
    -
    -

    [foo]

    -```````````````````````````````` - - -This is not a link reference definition, because it occurs inside -a code block: - -```````````````````````````````` example -``` -[foo]: /url -``` - -[foo] -. -
    [foo]: /url
    -
    -

    [foo]

    -```````````````````````````````` - - -A [link reference definition] cannot interrupt a paragraph. - -```````````````````````````````` example -Foo -[bar]: /baz - -[bar] -. -

    Foo -[bar]: /baz

    -

    [bar]

    -```````````````````````````````` - - -However, it can directly follow other block elements, such as headings -and thematic breaks, and it need not be followed by a blank line. - -```````````````````````````````` example -# [Foo] -[foo]: /url -> bar -. -

    Foo

    -
    -

    bar

    -
    -```````````````````````````````` - -```````````````````````````````` example -[foo]: /url -bar -=== -[foo] -. -

    bar

    -

    foo

    -```````````````````````````````` - -```````````````````````````````` example -[foo]: /url -=== -[foo] -. -

    === -foo

    -```````````````````````````````` - - -Several [link reference definitions] -can occur one after another, without intervening blank lines. - -```````````````````````````````` example -[foo]: /foo-url "foo" -[bar]: /bar-url - "bar" -[baz]: /baz-url - -[foo], -[bar], -[baz] -. -

    foo, -bar, -baz

    -```````````````````````````````` - - -[Link reference definitions] can occur -inside block containers, like lists and block quotations. They -affect the entire document, not just the container in which they -are defined: - -```````````````````````````````` example -[foo] - -> [foo]: /url -. -

    foo

    -
    -
    -```````````````````````````````` - - -Whether something is a [link reference definition] is -independent of whether the link reference it defines is -used in the document. Thus, for example, the following -document contains just a link reference definition, and -no visible content: - -```````````````````````````````` example -[foo]: /url -. -```````````````````````````````` - - -## Paragraphs - -A sequence of non-blank lines that cannot be interpreted as other -kinds of blocks forms a [paragraph](@). -The contents of the paragraph are the result of parsing the -paragraph's raw content as inlines. The paragraph's raw content -is formed by concatenating the lines and removing initial and final -[whitespace]. - -A simple example with two paragraphs: - -```````````````````````````````` example -aaa - -bbb -. -

    aaa

    -

    bbb

    -```````````````````````````````` - - -Paragraphs can contain multiple lines, but no blank lines: - -```````````````````````````````` example -aaa -bbb - -ccc -ddd -. -

    aaa -bbb

    -

    ccc -ddd

    -```````````````````````````````` - - -Multiple blank lines between paragraph have no effect: - -```````````````````````````````` example -aaa - - -bbb -. -

    aaa

    -

    bbb

    -```````````````````````````````` - - -Leading spaces are skipped: - -```````````````````````````````` example - aaa - bbb -. -

    aaa -bbb

    -```````````````````````````````` - - -Lines after the first may be indented any amount, since indented -code blocks cannot interrupt paragraphs. - -```````````````````````````````` example -aaa - bbb - ccc -. -

    aaa -bbb -ccc

    -```````````````````````````````` - - -However, the first line may be indented at most three spaces, -or an indented code block will be triggered: - -```````````````````````````````` example - aaa -bbb -. -

    aaa -bbb

    -```````````````````````````````` - - -```````````````````````````````` example - aaa -bbb -. -
    aaa
    -
    -

    bbb

    -```````````````````````````````` - - -Final spaces are stripped before inline parsing, so a paragraph -that ends with two or more spaces will not end with a [hard line -break]: - -```````````````````````````````` example -aaa -bbb -. -

    aaa
    -bbb

    -```````````````````````````````` - - -## Blank lines - -[Blank lines] between block-level elements are ignored, -except for the role they play in determining whether a [list] -is [tight] or [loose]. - -Blank lines at the beginning and end of the document are also ignored. - -```````````````````````````````` example - - -aaa - - -# aaa - - -. -

    aaa

    -

    aaa

    -```````````````````````````````` - - - -# Container blocks - -A [container block](#container-blocks) is a block that has other -blocks as its contents. There are two basic kinds of container blocks: -[block quotes] and [list items]. -[Lists] are meta-containers for [list items]. - -We define the syntax for container blocks recursively. The general -form of the definition is: - -> If X is a sequence of blocks, then the result of -> transforming X in such-and-such a way is a container of type Y -> with these blocks as its content. - -So, we explain what counts as a block quote or list item by explaining -how these can be *generated* from their contents. This should suffice -to define the syntax, although it does not give a recipe for *parsing* -these constructions. (A recipe is provided below in the section entitled -[A parsing strategy](#appendix-a-parsing-strategy).) - -## Block quotes - -A [block quote marker](@) -consists of 0-3 spaces of initial indent, plus (a) the character `>` together -with a following space, or (b) a single character `>` not followed by a space. - -The following rules define [block quotes]: - -1. **Basic case.** If a string of lines *Ls* constitute a sequence - of blocks *Bs*, then the result of prepending a [block quote - marker] to the beginning of each line in *Ls* - is a [block quote](#block-quotes) containing *Bs*. - -2. **Laziness.** If a string of lines *Ls* constitute a [block - quote](#block-quotes) with contents *Bs*, then the result of deleting - the initial [block quote marker] from one or - more lines in which the next [non-whitespace character] after the [block - quote marker] is [paragraph continuation - text] is a block quote with *Bs* as its content. - [Paragraph continuation text](@) is text - that will be parsed as part of the content of a paragraph, but does - not occur at the beginning of the paragraph. - -3. **Consecutiveness.** A document cannot contain two [block - quotes] in a row unless there is a [blank line] between them. - -Nothing else counts as a [block quote](#block-quotes). - -Here is a simple example: - -```````````````````````````````` example -> # Foo -> bar -> baz -. -
    -

    Foo

    -

    bar -baz

    -
    -```````````````````````````````` - - -The spaces after the `>` characters can be omitted: - -```````````````````````````````` example -># Foo ->bar -> baz -. -
    -

    Foo

    -

    bar -baz

    -
    -```````````````````````````````` - - -The `>` characters can be indented 1-3 spaces: - -```````````````````````````````` example - > # Foo - > bar - > baz -. -
    -

    Foo

    -

    bar -baz

    -
    -```````````````````````````````` - - -Four spaces gives us a code block: - -```````````````````````````````` example - > # Foo - > bar - > baz -. -
    > # Foo
    -> bar
    -> baz
    -
    -```````````````````````````````` - - -The Laziness clause allows us to omit the `>` before -[paragraph continuation text]: - -```````````````````````````````` example -> # Foo -> bar -baz -. -
    -

    Foo

    -

    bar -baz

    -
    -```````````````````````````````` - - -A block quote can contain some lazy and some non-lazy -continuation lines: - -```````````````````````````````` example -> bar -baz -> foo -. -
    -

    bar -baz -foo

    -
    -```````````````````````````````` - - -Laziness only applies to lines that would have been continuations of -paragraphs had they been prepended with [block quote markers]. -For example, the `> ` cannot be omitted in the second line of - -``` markdown -> foo -> --- -``` - -without changing the meaning: - -```````````````````````````````` example -> foo ---- -. -
    -

    foo

    -
    -
    -```````````````````````````````` - - -Similarly, if we omit the `> ` in the second line of - -``` markdown -> - foo -> - bar -``` - -then the block quote ends after the first line: - -```````````````````````````````` example -> - foo -- bar -. -
    -
      -
    • foo
    • -
    -
    -
      -
    • bar
    • -
    -```````````````````````````````` - - -For the same reason, we can't omit the `> ` in front of -subsequent lines of an indented or fenced code block: - -```````````````````````````````` example -> foo - bar -. -
    -
    foo
    -
    -
    -
    bar
    -
    -```````````````````````````````` - - -```````````````````````````````` example -> ``` -foo -``` -. -
    -
    -
    -

    foo

    -
    -```````````````````````````````` - - -Note that in the following case, we have a [lazy -continuation line]: - -```````````````````````````````` example -> foo - - bar -. -
    -

    foo -- bar

    -
    -```````````````````````````````` - - -To see why, note that in - -```markdown -> foo -> - bar -``` - -the `- bar` is indented too far to start a list, and can't -be an indented code block because indented code blocks cannot -interrupt paragraphs, so it is [paragraph continuation text]. - -A block quote can be empty: - -```````````````````````````````` example -> -. -
    -
    -```````````````````````````````` - - -```````````````````````````````` example -> -> -> -. -
    -
    -```````````````````````````````` - - -A block quote can have initial or final blank lines: - -```````````````````````````````` example -> -> foo -> -. -
    -

    foo

    -
    -```````````````````````````````` - - -A blank line always separates block quotes: - -```````````````````````````````` example -> foo - -> bar -. -
    -

    foo

    -
    -
    -

    bar

    -
    -```````````````````````````````` - - -(Most current Markdown implementations, including John Gruber's -original `Markdown.pl`, will parse this example as a single block quote -with two paragraphs. But it seems better to allow the author to decide -whether two block quotes or one are wanted.) - -Consecutiveness means that if we put these block quotes together, -we get a single block quote: - -```````````````````````````````` example -> foo -> bar -. -
    -

    foo -bar

    -
    -```````````````````````````````` - - -To get a block quote with two paragraphs, use: - -```````````````````````````````` example -> foo -> -> bar -. -
    -

    foo

    -

    bar

    -
    -```````````````````````````````` - - -Block quotes can interrupt paragraphs: - -```````````````````````````````` example -foo -> bar -. -

    foo

    -
    -

    bar

    -
    -```````````````````````````````` - - -In general, blank lines are not needed before or after block -quotes: - -```````````````````````````````` example -> aaa -*** -> bbb -. -
    -

    aaa

    -
    -
    -
    -

    bbb

    -
    -```````````````````````````````` - - -However, because of laziness, a blank line is needed between -a block quote and a following paragraph: - -```````````````````````````````` example -> bar -baz -. -
    -

    bar -baz

    -
    -```````````````````````````````` - - -```````````````````````````````` example -> bar - -baz -. -
    -

    bar

    -
    -

    baz

    -```````````````````````````````` - - -```````````````````````````````` example -> bar -> -baz -. -
    -

    bar

    -
    -

    baz

    -```````````````````````````````` - - -It is a consequence of the Laziness rule that any number -of initial `>`s may be omitted on a continuation line of a -nested block quote: - -```````````````````````````````` example -> > > foo -bar -. -
    -
    -
    -

    foo -bar

    -
    -
    -
    -```````````````````````````````` - - -```````````````````````````````` example ->>> foo -> bar ->>baz -. -
    -
    -
    -

    foo -bar -baz

    -
    -
    -
    -```````````````````````````````` - - -When including an indented code block in a block quote, -remember that the [block quote marker] includes -both the `>` and a following space. So *five spaces* are needed after -the `>`: - -```````````````````````````````` example -> code - -> not code -. -
    -
    code
    -
    -
    -
    -

    not code

    -
    -```````````````````````````````` - - - -## List items - -A [list marker](@) is a -[bullet list marker] or an [ordered list marker]. - -A [bullet list marker](@) -is a `-`, `+`, or `*` character. - -An [ordered list marker](@) -is a sequence of 1--9 arabic digits (`0-9`), followed by either a -`.` character or a `)` character. (The reason for the length -limit is that with 10 digits we start seeing integer overflows -in some browsers.) - -The following rules define [list items]: - -1. **Basic case.** If a sequence of lines *Ls* constitute a sequence of - blocks *Bs* starting with a [non-whitespace character], and *M* is a - list marker of width *W* followed by 1 ≤ *N* ≤ 4 spaces, then the result - of prepending *M* and the following spaces to the first line of - *Ls*, and indenting subsequent lines of *Ls* by *W + N* spaces, is a - list item with *Bs* as its contents. The type of the list item - (bullet or ordered) is determined by the type of its list marker. - If the list item is ordered, then it is also assigned a start - number, based on the ordered list marker. - - Exceptions: - - 1. When the first list item in a [list] interrupts - a paragraph---that is, when it starts on a line that would - otherwise count as [paragraph continuation text]---then (a) - the lines *Ls* must not begin with a blank line, and (b) if - the list item is ordered, the start number must be 1. - 2. If any line is a [thematic break][thematic breaks] then - that line is not a list item. - -For example, let *Ls* be the lines - -```````````````````````````````` example -A paragraph -with two lines. - - indented code - -> A block quote. -. -

    A paragraph -with two lines.

    -
    indented code
    -
    -
    -

    A block quote.

    -
    -```````````````````````````````` - - -And let *M* be the marker `1.`, and *N* = 2. Then rule #1 says -that the following is an ordered list item with start number 1, -and the same contents as *Ls*: - -```````````````````````````````` example -1. A paragraph - with two lines. - - indented code - - > A block quote. -. -
      -
    1. -

      A paragraph -with two lines.

      -
      indented code
      -
      -
      -

      A block quote.

      -
      -
    2. -
    -```````````````````````````````` - - -The most important thing to notice is that the position of -the text after the list marker determines how much indentation -is needed in subsequent blocks in the list item. If the list -marker takes up two spaces, and there are three spaces between -the list marker and the next [non-whitespace character], then blocks -must be indented five spaces in order to fall under the list -item. - -Here are some examples showing how far content must be indented to be -put under the list item: - -```````````````````````````````` example -- one - - two -. -
      -
    • one
    • -
    -

    two

    -```````````````````````````````` - - -```````````````````````````````` example -- one - - two -. -
      -
    • -

      one

      -

      two

      -
    • -
    -```````````````````````````````` - - -```````````````````````````````` example - - one - - two -. -
      -
    • one
    • -
    -
     two
    -
    -```````````````````````````````` - - -```````````````````````````````` example - - one - - two -. -
      -
    • -

      one

      -

      two

      -
    • -
    -```````````````````````````````` - - -It is tempting to think of this in terms of columns: the continuation -blocks must be indented at least to the column of the first -[non-whitespace character] after the list marker. However, that is not quite right. -The spaces after the list marker determine how much relative indentation -is needed. Which column this indentation reaches will depend on -how the list item is embedded in other constructions, as shown by -this example: - -```````````````````````````````` example - > > 1. one ->> ->> two -. -
    -
    -
      -
    1. -

      one

      -

      two

      -
    2. -
    -
    -
    -```````````````````````````````` - - -Here `two` occurs in the same column as the list marker `1.`, -but is actually contained in the list item, because there is -sufficient indentation after the last containing blockquote marker. - -The converse is also possible. In the following example, the word `two` -occurs far to the right of the initial text of the list item, `one`, but -it is not considered part of the list item, because it is not indented -far enough past the blockquote marker: - -```````````````````````````````` example ->>- one ->> - > > two -. -
    -
    -
      -
    • one
    • -
    -

    two

    -
    -
    -```````````````````````````````` - - -Note that at least one space is needed between the list marker and -any following content, so these are not list items: - -```````````````````````````````` example --one - -2.two -. -

    -one

    -

    2.two

    -```````````````````````````````` - - -A list item may contain blocks that are separated by more than -one blank line. - -```````````````````````````````` example -- foo - - - bar -. -
      -
    • -

      foo

      -

      bar

      -
    • -
    -```````````````````````````````` - - -A list item may contain any kind of block: - -```````````````````````````````` example -1. foo - - ``` - bar - ``` - - baz - - > bam -. -
      -
    1. -

      foo

      -
      bar
      -
      -

      baz

      -
      -

      bam

      -
      -
    2. -
    -```````````````````````````````` - - -A list item that contains an indented code block will preserve -empty lines within the code block verbatim. - -```````````````````````````````` example -- Foo - - bar - - - baz -. -
      -
    • -

      Foo

      -
      bar
      -
      -
      -baz
      -
      -
    • -
    -```````````````````````````````` - -Note that ordered list start numbers must be nine digits or less: - -```````````````````````````````` example -123456789. ok -. -
      -
    1. ok
    2. -
    -```````````````````````````````` - - -```````````````````````````````` example -1234567890. not ok -. -

    1234567890. not ok

    -```````````````````````````````` - - -A start number may begin with 0s: - -```````````````````````````````` example -0. ok -. -
      -
    1. ok
    2. -
    -```````````````````````````````` - - -```````````````````````````````` example -003. ok -. -
      -
    1. ok
    2. -
    -```````````````````````````````` - - -A start number may not be negative: - -```````````````````````````````` example --1. not ok -. -

    -1. not ok

    -```````````````````````````````` - - - -2. **Item starting with indented code.** If a sequence of lines *Ls* - constitute a sequence of blocks *Bs* starting with an indented code - block, and *M* is a list marker of width *W* followed by - one space, then the result of prepending *M* and the following - space to the first line of *Ls*, and indenting subsequent lines of - *Ls* by *W + 1* spaces, is a list item with *Bs* as its contents. - If a line is empty, then it need not be indented. The type of the - list item (bullet or ordered) is determined by the type of its list - marker. If the list item is ordered, then it is also assigned a - start number, based on the ordered list marker. - -An indented code block will have to be indented four spaces beyond -the edge of the region where text will be included in the list item. -In the following case that is 6 spaces: - -```````````````````````````````` example -- foo - - bar -. -
      -
    • -

      foo

      -
      bar
      -
      -
    • -
    -```````````````````````````````` - - -And in this case it is 11 spaces: - -```````````````````````````````` example - 10. foo - - bar -. -
      -
    1. -

      foo

      -
      bar
      -
      -
    2. -
    -```````````````````````````````` - - -If the *first* block in the list item is an indented code block, -then by rule #2, the contents must be indented *one* space after the -list marker: - -```````````````````````````````` example - indented code - -paragraph - - more code -. -
    indented code
    -
    -

    paragraph

    -
    more code
    -
    -```````````````````````````````` - - -```````````````````````````````` example -1. indented code - - paragraph - - more code -. -
      -
    1. -
      indented code
      -
      -

      paragraph

      -
      more code
      -
      -
    2. -
    -```````````````````````````````` - - -Note that an additional space indent is interpreted as space -inside the code block: - -```````````````````````````````` example -1. indented code - - paragraph - - more code -. -
      -
    1. -
       indented code
      -
      -

      paragraph

      -
      more code
      -
      -
    2. -
    -```````````````````````````````` - - -Note that rules #1 and #2 only apply to two cases: (a) cases -in which the lines to be included in a list item begin with a -[non-whitespace character], and (b) cases in which -they begin with an indented code -block. In a case like the following, where the first block begins with -a three-space indent, the rules do not allow us to form a list item by -indenting the whole thing and prepending a list marker: - -```````````````````````````````` example - foo - -bar -. -

    foo

    -

    bar

    -```````````````````````````````` - - -```````````````````````````````` example -- foo - - bar -. -
      -
    • foo
    • -
    -

    bar

    -```````````````````````````````` - - -This is not a significant restriction, because when a block begins -with 1-3 spaces indent, the indentation can always be removed without -a change in interpretation, allowing rule #1 to be applied. So, in -the above case: - -```````````````````````````````` example -- foo - - bar -. -
      -
    • -

      foo

      -

      bar

      -
    • -
    -```````````````````````````````` - - -3. **Item starting with a blank line.** If a sequence of lines *Ls* - starting with a single [blank line] constitute a (possibly empty) - sequence of blocks *Bs*, not separated from each other by more than - one blank line, and *M* is a list marker of width *W*, - then the result of prepending *M* to the first line of *Ls*, and - indenting subsequent lines of *Ls* by *W + 1* spaces, is a list - item with *Bs* as its contents. - If a line is empty, then it need not be indented. The type of the - list item (bullet or ordered) is determined by the type of its list - marker. If the list item is ordered, then it is also assigned a - start number, based on the ordered list marker. - -Here are some list items that start with a blank line but are not empty: - -```````````````````````````````` example -- - foo -- - ``` - bar - ``` -- - baz -. -
      -
    • foo
    • -
    • -
      bar
      -
      -
    • -
    • -
      baz
      -
      -
    • -
    -```````````````````````````````` - -When the list item starts with a blank line, the number of spaces -following the list marker doesn't change the required indentation: - -```````````````````````````````` example -- - foo -. -
      -
    • foo
    • -
    -```````````````````````````````` - - -A list item can begin with at most one blank line. -In the following example, `foo` is not part of the list -item: - -```````````````````````````````` example -- - - foo -. -
      -
    • -
    -

    foo

    -```````````````````````````````` - - -Here is an empty bullet list item: - -```````````````````````````````` example -- foo -- -- bar -. -
      -
    • foo
    • -
    • -
    • bar
    • -
    -```````````````````````````````` - - -It does not matter whether there are spaces following the [list marker]: - -```````````````````````````````` example -- foo -- -- bar -. -
      -
    • foo
    • -
    • -
    • bar
    • -
    -```````````````````````````````` - - -Here is an empty ordered list item: - -```````````````````````````````` example -1. foo -2. -3. bar -. -
      -
    1. foo
    2. -
    3. -
    4. bar
    5. -
    -```````````````````````````````` - - -A list may start or end with an empty list item: - -```````````````````````````````` example -* -. -
      -
    • -
    -```````````````````````````````` - -However, an empty list item cannot interrupt a paragraph: - -```````````````````````````````` example -foo -* - -foo -1. -. -

    foo -*

    -

    foo -1.

    -```````````````````````````````` - - -4. **Indentation.** If a sequence of lines *Ls* constitutes a list item - according to rule #1, #2, or #3, then the result of indenting each line - of *Ls* by 1-3 spaces (the same for each line) also constitutes a - list item with the same contents and attributes. If a line is - empty, then it need not be indented. - -Indented one space: - -```````````````````````````````` example - 1. A paragraph - with two lines. - - indented code - - > A block quote. -. -
      -
    1. -

      A paragraph -with two lines.

      -
      indented code
      -
      -
      -

      A block quote.

      -
      -
    2. -
    -```````````````````````````````` - - -Indented two spaces: - -```````````````````````````````` example - 1. A paragraph - with two lines. - - indented code - - > A block quote. -. -
      -
    1. -

      A paragraph -with two lines.

      -
      indented code
      -
      -
      -

      A block quote.

      -
      -
    2. -
    -```````````````````````````````` - - -Indented three spaces: - -```````````````````````````````` example - 1. A paragraph - with two lines. - - indented code - - > A block quote. -. -
      -
    1. -

      A paragraph -with two lines.

      -
      indented code
      -
      -
      -

      A block quote.

      -
      -
    2. -
    -```````````````````````````````` - - -Four spaces indent gives a code block: - -```````````````````````````````` example - 1. A paragraph - with two lines. - - indented code - - > A block quote. -. -
    1.  A paragraph
    -    with two lines.
    -
    -        indented code
    -
    -    > A block quote.
    -
    -```````````````````````````````` - - - -5. **Laziness.** If a string of lines *Ls* constitute a [list - item](#list-items) with contents *Bs*, then the result of deleting - some or all of the indentation from one or more lines in which the - next [non-whitespace character] after the indentation is - [paragraph continuation text] is a - list item with the same contents and attributes. The unindented - lines are called - [lazy continuation line](@)s. - -Here is an example with [lazy continuation lines]: - -```````````````````````````````` example - 1. A paragraph -with two lines. - - indented code - - > A block quote. -. -
      -
    1. -

      A paragraph -with two lines.

      -
      indented code
      -
      -
      -

      A block quote.

      -
      -
    2. -
    -```````````````````````````````` - - -Indentation can be partially deleted: - -```````````````````````````````` example - 1. A paragraph - with two lines. -. -
      -
    1. A paragraph -with two lines.
    2. -
    -```````````````````````````````` - - -These examples show how laziness can work in nested structures: - -```````````````````````````````` example -> 1. > Blockquote -continued here. -. -
    -
      -
    1. -
      -

      Blockquote -continued here.

      -
      -
    2. -
    -
    -```````````````````````````````` - - -```````````````````````````````` example -> 1. > Blockquote -> continued here. -. -
    -
      -
    1. -
      -

      Blockquote -continued here.

      -
      -
    2. -
    -
    -```````````````````````````````` - - - -6. **That's all.** Nothing that is not counted as a list item by rules - #1--5 counts as a [list item](#list-items). - -The rules for sublists follow from the general rules -[above][List items]. A sublist must be indented the same number -of spaces a paragraph would need to be in order to be included -in the list item. - -So, in this case we need two spaces indent: - -```````````````````````````````` example -- foo - - bar - - baz - - boo -. -
      -
    • foo -
        -
      • bar -
          -
        • baz -
            -
          • boo
          • -
          -
        • -
        -
      • -
      -
    • -
    -```````````````````````````````` - - -One is not enough: - -```````````````````````````````` example -- foo - - bar - - baz - - boo -. -
      -
    • foo
    • -
    • bar
    • -
    • baz
    • -
    • boo
    • -
    -```````````````````````````````` - - -Here we need four, because the list marker is wider: - -```````````````````````````````` example -10) foo - - bar -. -
      -
    1. foo -
        -
      • bar
      • -
      -
    2. -
    -```````````````````````````````` - - -Three is not enough: - -```````````````````````````````` example -10) foo - - bar -. -
      -
    1. foo
    2. -
    -
      -
    • bar
    • -
    -```````````````````````````````` - - -A list may be the first block in a list item: - -```````````````````````````````` example -- - foo -. -
      -
    • -
        -
      • foo
      • -
      -
    • -
    -```````````````````````````````` - - -```````````````````````````````` example -1. - 2. foo -. -
      -
    1. -
        -
      • -
          -
        1. foo
        2. -
        -
      • -
      -
    2. -
    -```````````````````````````````` - - -A list item can contain a heading: - -```````````````````````````````` example -- # Foo -- Bar - --- - baz -. -
      -
    • -

      Foo

      -
    • -
    • -

      Bar

      -baz
    • -
    -```````````````````````````````` - - -### Motivation - -John Gruber's Markdown spec says the following about list items: - -1. "List markers typically start at the left margin, but may be indented - by up to three spaces. List markers must be followed by one or more - spaces or a tab." - -2. "To make lists look nice, you can wrap items with hanging indents.... - But if you don't want to, you don't have to." - -3. "List items may consist of multiple paragraphs. Each subsequent - paragraph in a list item must be indented by either 4 spaces or one - tab." - -4. "It looks nice if you indent every line of the subsequent paragraphs, - but here again, Markdown will allow you to be lazy." - -5. "To put a blockquote within a list item, the blockquote's `>` - delimiters need to be indented." - -6. "To put a code block within a list item, the code block needs to be - indented twice — 8 spaces or two tabs." - -These rules specify that a paragraph under a list item must be indented -four spaces (presumably, from the left margin, rather than the start of -the list marker, but this is not said), and that code under a list item -must be indented eight spaces instead of the usual four. They also say -that a block quote must be indented, but not by how much; however, the -example given has four spaces indentation. Although nothing is said -about other kinds of block-level content, it is certainly reasonable to -infer that *all* block elements under a list item, including other -lists, must be indented four spaces. This principle has been called the -*four-space rule*. - -The four-space rule is clear and principled, and if the reference -implementation `Markdown.pl` had followed it, it probably would have -become the standard. However, `Markdown.pl` allowed paragraphs and -sublists to start with only two spaces indentation, at least on the -outer level. Worse, its behavior was inconsistent: a sublist of an -outer-level list needed two spaces indentation, but a sublist of this -sublist needed three spaces. It is not surprising, then, that different -implementations of Markdown have developed very different rules for -determining what comes under a list item. (Pandoc and python-Markdown, -for example, stuck with Gruber's syntax description and the four-space -rule, while discount, redcarpet, marked, PHP Markdown, and others -followed `Markdown.pl`'s behavior more closely.) - -Unfortunately, given the divergences between implementations, there -is no way to give a spec for list items that will be guaranteed not -to break any existing documents. However, the spec given here should -correctly handle lists formatted with either the four-space rule or -the more forgiving `Markdown.pl` behavior, provided they are laid out -in a way that is natural for a human to read. - -The strategy here is to let the width and indentation of the list marker -determine the indentation necessary for blocks to fall under the list -item, rather than having a fixed and arbitrary number. The writer can -think of the body of the list item as a unit which gets indented to the -right enough to fit the list marker (and any indentation on the list -marker). (The laziness rule, #5, then allows continuation lines to be -unindented if needed.) - -This rule is superior, we claim, to any rule requiring a fixed level of -indentation from the margin. The four-space rule is clear but -unnatural. It is quite unintuitive that - -``` markdown -- foo - - bar - - - baz -``` - -should be parsed as two lists with an intervening paragraph, - -``` html -
      -
    • foo
    • -
    -

    bar

    -
      -
    • baz
    • -
    -``` - -as the four-space rule demands, rather than a single list, - -``` html -
      -
    • -

      foo

      -

      bar

      -
        -
      • baz
      • -
      -
    • -
    -``` - -The choice of four spaces is arbitrary. It can be learned, but it is -not likely to be guessed, and it trips up beginners regularly. - -Would it help to adopt a two-space rule? The problem is that such -a rule, together with the rule allowing 1--3 spaces indentation of the -initial list marker, allows text that is indented *less than* the -original list marker to be included in the list item. For example, -`Markdown.pl` parses - -``` markdown - - one - - two -``` - -as a single list item, with `two` a continuation paragraph: - -``` html -
      -
    • -

      one

      -

      two

      -
    • -
    -``` - -and similarly - -``` markdown -> - one -> -> two -``` - -as - -``` html -
    -
      -
    • -

      one

      -

      two

      -
    • -
    -
    -``` - -This is extremely unintuitive. - -Rather than requiring a fixed indent from the margin, we could require -a fixed indent (say, two spaces, or even one space) from the list marker (which -may itself be indented). This proposal would remove the last anomaly -discussed. Unlike the spec presented above, it would count the following -as a list item with a subparagraph, even though the paragraph `bar` -is not indented as far as the first paragraph `foo`: - -``` markdown - 10. foo - - bar -``` - -Arguably this text does read like a list item with `bar` as a subparagraph, -which may count in favor of the proposal. However, on this proposal indented -code would have to be indented six spaces after the list marker. And this -would break a lot of existing Markdown, which has the pattern: - -``` markdown -1. foo - - indented code -``` - -where the code is indented eight spaces. The spec above, by contrast, will -parse this text as expected, since the code block's indentation is measured -from the beginning of `foo`. - -The one case that needs special treatment is a list item that *starts* -with indented code. How much indentation is required in that case, since -we don't have a "first paragraph" to measure from? Rule #2 simply stipulates -that in such cases, we require one space indentation from the list marker -(and then the normal four spaces for the indented code). This will match the -four-space rule in cases where the list marker plus its initial indentation -takes four spaces (a common case), but diverge in other cases. - -## Lists - -A [list](@) is a sequence of one or more -list items [of the same type]. The list items -may be separated by any number of blank lines. - -Two list items are [of the same type](@) -if they begin with a [list marker] of the same type. -Two list markers are of the -same type if (a) they are bullet list markers using the same character -(`-`, `+`, or `*`) or (b) they are ordered list numbers with the same -delimiter (either `.` or `)`). - -A list is an [ordered list](@) -if its constituent list items begin with -[ordered list markers], and a -[bullet list](@) if its constituent list -items begin with [bullet list markers]. - -The [start number](@) -of an [ordered list] is determined by the list number of -its initial list item. The numbers of subsequent list items are -disregarded. - -A list is [loose](@) if any of its constituent -list items are separated by blank lines, or if any of its constituent -list items directly contain two block-level elements with a blank line -between them. Otherwise a list is [tight](@). -(The difference in HTML output is that paragraphs in a loose list are -wrapped in `

    ` tags, while paragraphs in a tight list are not.) - -Changing the bullet or ordered list delimiter starts a new list: - -```````````````````````````````` example -- foo -- bar -+ baz -. -

      -
    • foo
    • -
    • bar
    • -
    -
      -
    • baz
    • -
    -```````````````````````````````` - - -```````````````````````````````` example -1. foo -2. bar -3) baz -. -
      -
    1. foo
    2. -
    3. bar
    4. -
    -
      -
    1. baz
    2. -
    -```````````````````````````````` - - -In CommonMark, a list can interrupt a paragraph. That is, -no blank line is needed to separate a paragraph from a following -list: - -```````````````````````````````` example -Foo -- bar -- baz -. -

    Foo

    -
      -
    • bar
    • -
    • baz
    • -
    -```````````````````````````````` - -`Markdown.pl` does not allow this, through fear of triggering a list -via a numeral in a hard-wrapped line: - -``` markdown -The number of windows in my house is -14. The number of doors is 6. -``` - -Oddly, though, `Markdown.pl` *does* allow a blockquote to -interrupt a paragraph, even though the same considerations might -apply. - -In CommonMark, we do allow lists to interrupt paragraphs, for -two reasons. First, it is natural and not uncommon for people -to start lists without blank lines: - -``` markdown -I need to buy -- new shoes -- a coat -- a plane ticket -``` - -Second, we are attracted to a - -> [principle of uniformity](@): -> if a chunk of text has a certain -> meaning, it will continue to have the same meaning when put into a -> container block (such as a list item or blockquote). - -(Indeed, the spec for [list items] and [block quotes] presupposes -this principle.) This principle implies that if - -``` markdown - * I need to buy - - new shoes - - a coat - - a plane ticket -``` - -is a list item containing a paragraph followed by a nested sublist, -as all Markdown implementations agree it is (though the paragraph -may be rendered without `

    ` tags, since the list is "tight"), -then - -``` markdown -I need to buy -- new shoes -- a coat -- a plane ticket -``` - -by itself should be a paragraph followed by a nested sublist. - -Since it is well established Markdown practice to allow lists to -interrupt paragraphs inside list items, the [principle of -uniformity] requires us to allow this outside list items as -well. ([reStructuredText](http://docutils.sourceforge.net/rst.html) -takes a different approach, requiring blank lines before lists -even inside other list items.) - -In order to solve of unwanted lists in paragraphs with -hard-wrapped numerals, we allow only lists starting with `1` to -interrupt paragraphs. Thus, - -```````````````````````````````` example -The number of windows in my house is -14. The number of doors is 6. -. -

    The number of windows in my house is -14. The number of doors is 6.

    -```````````````````````````````` - -We may still get an unintended result in cases like - -```````````````````````````````` example -The number of windows in my house is -1. The number of doors is 6. -. -

    The number of windows in my house is

    -
      -
    1. The number of doors is 6.
    2. -
    -```````````````````````````````` - -but this rule should prevent most spurious list captures. - -There can be any number of blank lines between items: - -```````````````````````````````` example -- foo - -- bar - - -- baz -. -
      -
    • -

      foo

      -
    • -
    • -

      bar

      -
    • -
    • -

      baz

      -
    • -
    -```````````````````````````````` - -```````````````````````````````` example -- foo - - bar - - baz - - - bim -. -
      -
    • foo -
        -
      • bar -
          -
        • -

          baz

          -

          bim

          -
        • -
        -
      • -
      -
    • -
    -```````````````````````````````` - - -To separate consecutive lists of the same type, or to separate a -list from an indented code block that would otherwise be parsed -as a subparagraph of the final list item, you can insert a blank HTML -comment: - -```````````````````````````````` example -- foo -- bar - - - -- baz -- bim -. -
      -
    • foo
    • -
    • bar
    • -
    - -
      -
    • baz
    • -
    • bim
    • -
    -```````````````````````````````` - - -```````````````````````````````` example -- foo - - notcode - -- foo - - - - code -. -
      -
    • -

      foo

      -

      notcode

      -
    • -
    • -

      foo

      -
    • -
    - -
    code
    -
    -```````````````````````````````` - - -List items need not be indented to the same level. The following -list items will be treated as items at the same list level, -since none is indented enough to belong to the previous list -item: - -```````````````````````````````` example -- a - - b - - c - - d - - e - - f -- g -. -
      -
    • a
    • -
    • b
    • -
    • c
    • -
    • d
    • -
    • e
    • -
    • f
    • -
    • g
    • -
    -```````````````````````````````` - - -```````````````````````````````` example -1. a - - 2. b - - 3. c -. -
      -
    1. -

      a

      -
    2. -
    3. -

      b

      -
    4. -
    5. -

      c

      -
    6. -
    -```````````````````````````````` - -Note, however, that list items may not be indented more than -three spaces. Here `- e` is treated as a paragraph continuation -line, because it is indented more than three spaces: - -```````````````````````````````` example -- a - - b - - c - - d - - e -. -
      -
    • a
    • -
    • b
    • -
    • c
    • -
    • d -- e
    • -
    -```````````````````````````````` - -And here, `3. c` is treated as in indented code block, -because it is indented four spaces and preceded by a -blank line. - -```````````````````````````````` example -1. a - - 2. b - - 3. c -. -
      -
    1. -

      a

      -
    2. -
    3. -

      b

      -
    4. -
    -
    3. c
    -
    -```````````````````````````````` - - -This is a loose list, because there is a blank line between -two of the list items: - -```````````````````````````````` example -- a -- b - -- c -. -
      -
    • -

      a

      -
    • -
    • -

      b

      -
    • -
    • -

      c

      -
    • -
    -```````````````````````````````` - - -So is this, with a empty second item: - -```````````````````````````````` example -* a -* - -* c -. -
      -
    • -

      a

      -
    • -
    • -
    • -

      c

      -
    • -
    -```````````````````````````````` - - -These are loose lists, even though there is no space between the items, -because one of the items directly contains two block-level elements -with a blank line between them: - -```````````````````````````````` example -- a -- b - - c -- d -. -
      -
    • -

      a

      -
    • -
    • -

      b

      -

      c

      -
    • -
    • -

      d

      -
    • -
    -```````````````````````````````` - - -```````````````````````````````` example -- a -- b - - [ref]: /url -- d -. -
      -
    • -

      a

      -
    • -
    • -

      b

      -
    • -
    • -

      d

      -
    • -
    -```````````````````````````````` - - -This is a tight list, because the blank lines are in a code block: - -```````````````````````````````` example -- a -- ``` - b - - - ``` -- c -. -
      -
    • a
    • -
    • -
      b
      -
      -
      -
      -
    • -
    • c
    • -
    -```````````````````````````````` - - -This is a tight list, because the blank line is between two -paragraphs of a sublist. So the sublist is loose while -the outer list is tight: - -```````````````````````````````` example -- a - - b - - c -- d -. -
      -
    • a -
        -
      • -

        b

        -

        c

        -
      • -
      -
    • -
    • d
    • -
    -```````````````````````````````` - - -This is a tight list, because the blank line is inside the -block quote: - -```````````````````````````````` example -* a - > b - > -* c -. -
      -
    • a -
      -

      b

      -
      -
    • -
    • c
    • -
    -```````````````````````````````` - - -This list is tight, because the consecutive block elements -are not separated by blank lines: - -```````````````````````````````` example -- a - > b - ``` - c - ``` -- d -. -
      -
    • a -
      -

      b

      -
      -
      c
      -
      -
    • -
    • d
    • -
    -```````````````````````````````` - - -A single-paragraph list is tight: - -```````````````````````````````` example -- a -. -
      -
    • a
    • -
    -```````````````````````````````` - - -```````````````````````````````` example -- a - - b -. -
      -
    • a -
        -
      • b
      • -
      -
    • -
    -```````````````````````````````` - - -This list is loose, because of the blank line between the -two block elements in the list item: - -```````````````````````````````` example -1. ``` - foo - ``` - - bar -. -
      -
    1. -
      foo
      -
      -

      bar

      -
    2. -
    -```````````````````````````````` - - -Here the outer list is loose, the inner list tight: - -```````````````````````````````` example -* foo - * bar - - baz -. -
      -
    • -

      foo

      -
        -
      • bar
      • -
      -

      baz

      -
    • -
    -```````````````````````````````` - - -```````````````````````````````` example -- a - - b - - c - -- d - - e - - f -. -
      -
    • -

      a

      -
        -
      • b
      • -
      • c
      • -
      -
    • -
    • -

      d

      -
        -
      • e
      • -
      • f
      • -
      -
    • -
    -```````````````````````````````` - - -# Inlines - -Inlines are parsed sequentially from the beginning of the character -stream to the end (left to right, in left-to-right languages). -Thus, for example, in - -```````````````````````````````` example -`hi`lo` -. -

    hilo`

    -```````````````````````````````` - -`hi` is parsed as code, leaving the backtick at the end as a literal -backtick. - - -## Backslash escapes - -Any ASCII punctuation character may be backslash-escaped: - -```````````````````````````````` example -\!\"\#\$\%\&\'\(\)\*\+\,\-\.\/\:\;\<\=\>\?\@\[\\\]\^\_\`\{\|\}\~ -. -

    !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~

    -```````````````````````````````` - - -Backslashes before other characters are treated as literal -backslashes: - -```````````````````````````````` example -\→\A\a\ \3\φ\« -. -

    \→\A\a\ \3\φ\«

    -```````````````````````````````` - - -Escaped characters are treated as regular characters and do -not have their usual Markdown meanings: - -```````````````````````````````` example -\*not emphasized* -\
    not a tag -\[not a link](/foo) -\`not code` -1\. not a list -\* not a list -\# not a heading -\[foo]: /url "not a reference" -\ö not a character entity -. -

    *not emphasized* -<br/> not a tag -[not a link](/foo) -`not code` -1. not a list -* not a list -# not a heading -[foo]: /url "not a reference" -&ouml; not a character entity

    -```````````````````````````````` - - -If a backslash is itself escaped, the following character is not: - -```````````````````````````````` example -\\*emphasis* -. -

    \emphasis

    -```````````````````````````````` - - -A backslash at the end of the line is a [hard line break]: - -```````````````````````````````` example -foo\ -bar -. -

    foo
    -bar

    -```````````````````````````````` - - -Backslash escapes do not work in code blocks, code spans, autolinks, or -raw HTML: - -```````````````````````````````` example -`` \[\` `` -. -

    \[\`

    -```````````````````````````````` - - -```````````````````````````````` example - \[\] -. -
    \[\]
    -
    -```````````````````````````````` - - -```````````````````````````````` example -~~~ -\[\] -~~~ -. -
    \[\]
    -
    -```````````````````````````````` - - -```````````````````````````````` example - -. -

    http://example.com?find=\*

    -```````````````````````````````` - - -```````````````````````````````` example - -. - -```````````````````````````````` - - -But they work in all other contexts, including URLs and link titles, -link references, and [info strings] in [fenced code blocks]: - -```````````````````````````````` example -[foo](/bar\* "ti\*tle") -. -

    foo

    -```````````````````````````````` - - -```````````````````````````````` example -[foo] - -[foo]: /bar\* "ti\*tle" -. -

    foo

    -```````````````````````````````` - - -```````````````````````````````` example -``` foo\+bar -foo -``` -. -
    foo
    -
    -```````````````````````````````` - - - -## Entity and numeric character references - -Valid HTML entity references and numeric character references -can be used in place of the corresponding Unicode character, -with the following exceptions: - -- Entity and character references are not recognized in code - blocks and code spans. - -- Entity and character references cannot stand in place of - special characters that define structural elements in - CommonMark. For example, although `*` can be used - in place of a literal `*` character, `*` cannot replace - `*` in emphasis delimiters, bullet list markers, or thematic - breaks. - -Conforming CommonMark parsers need not store information about -whether a particular character was represented in the source -using a Unicode character or an entity reference. - -[Entity references](@) consist of `&` + any of the valid -HTML5 entity names + `;`. The -document -is used as an authoritative source for the valid entity -references and their corresponding code points. - -```````````````````````````````` example -  & © Æ Ď -¾ ℋ ⅆ -∲ ≧̸ -. -

      & © Æ Ď -¾ ℋ ⅆ -∲ ≧̸

    -```````````````````````````````` - - -[Decimal numeric character -references](@) -consist of `&#` + a string of 1--7 arabic digits + `;`. A -numeric character reference is parsed as the corresponding -Unicode character. Invalid Unicode code points will be replaced by -the REPLACEMENT CHARACTER (`U+FFFD`). For security reasons, -the code point `U+0000` will also be replaced by `U+FFFD`. - -```````````````````````````````` example -# Ӓ Ϡ � -. -

    # Ӓ Ϡ �

    -```````````````````````````````` - - -[Hexadecimal numeric character -references](@) consist of `&#` + -either `X` or `x` + a string of 1-6 hexadecimal digits + `;`. -They too are parsed as the corresponding Unicode character (this -time specified with a hexadecimal numeral instead of decimal). - -```````````````````````````````` example -" ആ ಫ -. -

    " ആ ಫ

    -```````````````````````````````` - - -Here are some nonentities: - -```````````````````````````````` example -  &x; &#; &#x; -� -&#abcdef0; -&ThisIsNotDefined; &hi?; -. -

    &nbsp &x; &#; &#x; -&#87654321; -&#abcdef0; -&ThisIsNotDefined; &hi?;

    -```````````````````````````````` - - -Although HTML5 does accept some entity references -without a trailing semicolon (such as `©`), these are not -recognized here, because it makes the grammar too ambiguous: - -```````````````````````````````` example -© -. -

    &copy

    -```````````````````````````````` - - -Strings that are not on the list of HTML5 named entities are not -recognized as entity references either: - -```````````````````````````````` example -&MadeUpEntity; -. -

    &MadeUpEntity;

    -```````````````````````````````` - - -Entity and numeric character references are recognized in any -context besides code spans or code blocks, including -URLs, [link titles], and [fenced code block][] [info strings]: - -```````````````````````````````` example - -. - -```````````````````````````````` - - -```````````````````````````````` example -[foo](/föö "föö") -. -

    foo

    -```````````````````````````````` - - -```````````````````````````````` example -[foo] - -[foo]: /föö "föö" -. -

    foo

    -```````````````````````````````` - - -```````````````````````````````` example -``` föö -foo -``` -. -
    foo
    -
    -```````````````````````````````` - - -Entity and numeric character references are treated as literal -text in code spans and code blocks: - -```````````````````````````````` example -`föö` -. -

    f&ouml;&ouml;

    -```````````````````````````````` - - -```````````````````````````````` example - föfö -. -
    f&ouml;f&ouml;
    -
    -```````````````````````````````` - - -Entity and numeric character references cannot be used -in place of symbols indicating structure in CommonMark -documents. - -```````````````````````````````` example -*foo* -*foo* -. -

    *foo* -foo

    -```````````````````````````````` - -```````````````````````````````` example -* foo - -* foo -. -

    * foo

    -
      -
    • foo
    • -
    -```````````````````````````````` - -```````````````````````````````` example -foo bar -. -

    foo - -bar

    -```````````````````````````````` - -```````````````````````````````` example - foo -. -

    →foo

    -```````````````````````````````` - - -```````````````````````````````` example -[a](url "tit") -. -

    [a](url "tit")

    -```````````````````````````````` - - -## Code spans - -A [backtick string](@) -is a string of one or more backtick characters (`` ` ``) that is neither -preceded nor followed by a backtick. - -A [code span](@) begins with a backtick string and ends with -a backtick string of equal length. The contents of the code span are -the characters between the two backtick strings, normalized in the -following ways: - -- First, [line endings] are converted to [spaces]. -- If the resulting string both begins *and* ends with a [space] - character, but does not consist entirely of [space] - characters, a single [space] character is removed from the - front and back. This allows you to include code that begins - or ends with backtick characters, which must be separated by - whitespace from the opening or closing backtick strings. - -This is a simple code span: - -```````````````````````````````` example -`foo` -. -

    foo

    -```````````````````````````````` - - -Here two backticks are used, because the code contains a backtick. -This example also illustrates stripping of a single leading and -trailing space: - -```````````````````````````````` example -`` foo ` bar `` -. -

    foo ` bar

    -```````````````````````````````` - - -This example shows the motivation for stripping leading and trailing -spaces: - -```````````````````````````````` example -` `` ` -. -

    ``

    -```````````````````````````````` - -Note that only *one* space is stripped: - -```````````````````````````````` example -` `` ` -. -

    ``

    -```````````````````````````````` - -The stripping only happens if the space is on both -sides of the string: - -```````````````````````````````` example -` a` -. -

    a

    -```````````````````````````````` - -Only [spaces], and not [unicode whitespace] in general, are -stripped in this way: - -```````````````````````````````` example -` b ` -. -

     b 

    -```````````````````````````````` - -No stripping occurs if the code span contains only spaces: - -```````````````````````````````` example -` ` -` ` -. -

      -

    -```````````````````````````````` - - -[Line endings] are treated like spaces: - -```````````````````````````````` example -`` -foo -bar -baz -`` -. -

    foo bar baz

    -```````````````````````````````` - -```````````````````````````````` example -`` -foo -`` -. -

    foo

    -```````````````````````````````` - - -Interior spaces are not collapsed: - -```````````````````````````````` example -`foo bar -baz` -. -

    foo bar baz

    -```````````````````````````````` - -Note that browsers will typically collapse consecutive spaces -when rendering `` elements, so it is recommended that -the following CSS be used: - - code{white-space: pre-wrap;} - - -Note that backslash escapes do not work in code spans. All backslashes -are treated literally: - -```````````````````````````````` example -`foo\`bar` -. -

    foo\bar`

    -```````````````````````````````` - - -Backslash escapes are never needed, because one can always choose a -string of *n* backtick characters as delimiters, where the code does -not contain any strings of exactly *n* backtick characters. - -```````````````````````````````` example -``foo`bar`` -. -

    foo`bar

    -```````````````````````````````` - -```````````````````````````````` example -` foo `` bar ` -. -

    foo `` bar

    -```````````````````````````````` - - -Code span backticks have higher precedence than any other inline -constructs except HTML tags and autolinks. Thus, for example, this is -not parsed as emphasized text, since the second `*` is part of a code -span: - -```````````````````````````````` example -*foo`*` -. -

    *foo*

    -```````````````````````````````` - - -And this is not parsed as a link: - -```````````````````````````````` example -[not a `link](/foo`) -. -

    [not a link](/foo)

    -```````````````````````````````` - - -Code spans, HTML tags, and autolinks have the same precedence. -Thus, this is code: - -```````````````````````````````` example -`` -. -

    <a href="">`

    -```````````````````````````````` - - -But this is an HTML tag: - -```````````````````````````````` example -
    ` -. -

    `

    -```````````````````````````````` - - -And this is code: - -```````````````````````````````` example -`` -. -

    <http://foo.bar.baz>`

    -```````````````````````````````` - - -But this is an autolink: - -```````````````````````````````` example -` -. -

    http://foo.bar.`baz`

    -```````````````````````````````` - - -When a backtick string is not closed by a matching backtick string, -we just have literal backticks: - -```````````````````````````````` example -```foo`` -. -

    ```foo``

    -```````````````````````````````` - - -```````````````````````````````` example -`foo -. -

    `foo

    -```````````````````````````````` - -The following case also illustrates the need for opening and -closing backtick strings to be equal in length: - -```````````````````````````````` example -`foo``bar`` -. -

    `foobar

    -```````````````````````````````` - - -## Emphasis and strong emphasis - -John Gruber's original [Markdown syntax -description](http://daringfireball.net/projects/markdown/syntax#em) says: - -> Markdown treats asterisks (`*`) and underscores (`_`) as indicators of -> emphasis. Text wrapped with one `*` or `_` will be wrapped with an HTML -> `` tag; double `*`'s or `_`'s will be wrapped with an HTML `` -> tag. - -This is enough for most users, but these rules leave much undecided, -especially when it comes to nested emphasis. The original -`Markdown.pl` test suite makes it clear that triple `***` and -`___` delimiters can be used for strong emphasis, and most -implementations have also allowed the following patterns: - -``` markdown -***strong emph*** -***strong** in emph* -***emph* in strong** -**in strong *emph*** -*in emph **strong*** -``` - -The following patterns are less widely supported, but the intent -is clear and they are useful (especially in contexts like bibliography -entries): - -``` markdown -*emph *with emph* in it* -**strong **with strong** in it** -``` - -Many implementations have also restricted intraword emphasis to -the `*` forms, to avoid unwanted emphasis in words containing -internal underscores. (It is best practice to put these in code -spans, but users often do not.) - -``` markdown -internal emphasis: foo*bar*baz -no emphasis: foo_bar_baz -``` - -The rules given below capture all of these patterns, while allowing -for efficient parsing strategies that do not backtrack. - -First, some definitions. A [delimiter run](@) is either -a sequence of one or more `*` characters that is not preceded or -followed by a non-backslash-escaped `*` character, or a sequence -of one or more `_` characters that is not preceded or followed by -a non-backslash-escaped `_` character. - -A [left-flanking delimiter run](@) is -a [delimiter run] that is (1) not followed by [Unicode whitespace], -and either (2a) not followed by a [punctuation character], or -(2b) followed by a [punctuation character] and -preceded by [Unicode whitespace] or a [punctuation character]. -For purposes of this definition, the beginning and the end of -the line count as Unicode whitespace. - -A [right-flanking delimiter run](@) is -a [delimiter run] that is (1) not preceded by [Unicode whitespace], -and either (2a) not preceded by a [punctuation character], or -(2b) preceded by a [punctuation character] and -followed by [Unicode whitespace] or a [punctuation character]. -For purposes of this definition, the beginning and the end of -the line count as Unicode whitespace. - -Here are some examples of delimiter runs. - - - left-flanking but not right-flanking: - - ``` - ***abc - _abc - **"abc" - _"abc" - ``` - - - right-flanking but not left-flanking: - - ``` - abc*** - abc_ - "abc"** - "abc"_ - ``` - - - Both left and right-flanking: - - ``` - abc***def - "abc"_"def" - ``` - - - Neither left nor right-flanking: - - ``` - abc *** def - a _ b - ``` - -(The idea of distinguishing left-flanking and right-flanking -delimiter runs based on the character before and the character -after comes from Roopesh Chander's -[vfmd](http://www.vfmd.org/vfmd-spec/specification/#procedure-for-identifying-emphasis-tags). -vfmd uses the terminology "emphasis indicator string" instead of "delimiter -run," and its rules for distinguishing left- and right-flanking runs -are a bit more complex than the ones given here.) - -The following rules define emphasis and strong emphasis: - -1. A single `*` character [can open emphasis](@) - iff (if and only if) it is part of a [left-flanking delimiter run]. - -2. A single `_` character [can open emphasis] iff - it is part of a [left-flanking delimiter run] - and either (a) not part of a [right-flanking delimiter run] - or (b) part of a [right-flanking delimiter run] - preceded by punctuation. - -3. A single `*` character [can close emphasis](@) - iff it is part of a [right-flanking delimiter run]. - -4. A single `_` character [can close emphasis] iff - it is part of a [right-flanking delimiter run] - and either (a) not part of a [left-flanking delimiter run] - or (b) part of a [left-flanking delimiter run] - followed by punctuation. - -5. A double `**` [can open strong emphasis](@) - iff it is part of a [left-flanking delimiter run]. - -6. A double `__` [can open strong emphasis] iff - it is part of a [left-flanking delimiter run] - and either (a) not part of a [right-flanking delimiter run] - or (b) part of a [right-flanking delimiter run] - preceded by punctuation. - -7. A double `**` [can close strong emphasis](@) - iff it is part of a [right-flanking delimiter run]. - -8. A double `__` [can close strong emphasis] iff - it is part of a [right-flanking delimiter run] - and either (a) not part of a [left-flanking delimiter run] - or (b) part of a [left-flanking delimiter run] - followed by punctuation. - -9. Emphasis begins with a delimiter that [can open emphasis] and ends - with a delimiter that [can close emphasis], and that uses the same - character (`_` or `*`) as the opening delimiter. The - opening and closing delimiters must belong to separate - [delimiter runs]. If one of the delimiters can both - open and close emphasis, then the sum of the lengths of the - delimiter runs containing the opening and closing delimiters - must not be a multiple of 3 unless both lengths are - multiples of 3. - -10. Strong emphasis begins with a delimiter that - [can open strong emphasis] and ends with a delimiter that - [can close strong emphasis], and that uses the same character - (`_` or `*`) as the opening delimiter. The - opening and closing delimiters must belong to separate - [delimiter runs]. If one of the delimiters can both open - and close strong emphasis, then the sum of the lengths of - the delimiter runs containing the opening and closing - delimiters must not be a multiple of 3 unless both lengths - are multiples of 3. - -11. A literal `*` character cannot occur at the beginning or end of - `*`-delimited emphasis or `**`-delimited strong emphasis, unless it - is backslash-escaped. - -12. A literal `_` character cannot occur at the beginning or end of - `_`-delimited emphasis or `__`-delimited strong emphasis, unless it - is backslash-escaped. - -Where rules 1--12 above are compatible with multiple parsings, -the following principles resolve ambiguity: - -13. The number of nestings should be minimized. Thus, for example, - an interpretation `...` is always preferred to - `...`. - -14. An interpretation `...` is always - preferred to `...`. - -15. When two potential emphasis or strong emphasis spans overlap, - so that the second begins before the first ends and ends after - the first ends, the first takes precedence. Thus, for example, - `*foo _bar* baz_` is parsed as `foo _bar baz_` rather - than `*foo bar* baz`. - -16. When there are two potential emphasis or strong emphasis spans - with the same closing delimiter, the shorter one (the one that - opens later) takes precedence. Thus, for example, - `**foo **bar baz**` is parsed as `**foo bar baz` - rather than `foo **bar baz`. - -17. Inline code spans, links, images, and HTML tags group more tightly - than emphasis. So, when there is a choice between an interpretation - that contains one of these elements and one that does not, the - former always wins. Thus, for example, `*[foo*](bar)` is - parsed as `*foo*` rather than as - `[foo](bar)`. - -These rules can be illustrated through a series of examples. - -Rule 1: - -```````````````````````````````` example -*foo bar* -. -

    foo bar

    -```````````````````````````````` - - -This is not emphasis, because the opening `*` is followed by -whitespace, and hence not part of a [left-flanking delimiter run]: - -```````````````````````````````` example -a * foo bar* -. -

    a * foo bar*

    -```````````````````````````````` - - -This is not emphasis, because the opening `*` is preceded -by an alphanumeric and followed by punctuation, and hence -not part of a [left-flanking delimiter run]: - -```````````````````````````````` example -a*"foo"* -. -

    a*"foo"*

    -```````````````````````````````` - - -Unicode nonbreaking spaces count as whitespace, too: - -```````````````````````````````` example -* a * -. -

    * a *

    -```````````````````````````````` - - -Intraword emphasis with `*` is permitted: - -```````````````````````````````` example -foo*bar* -. -

    foobar

    -```````````````````````````````` - - -```````````````````````````````` example -5*6*78 -. -

    5678

    -```````````````````````````````` - - -Rule 2: - -```````````````````````````````` example -_foo bar_ -. -

    foo bar

    -```````````````````````````````` - - -This is not emphasis, because the opening `_` is followed by -whitespace: - -```````````````````````````````` example -_ foo bar_ -. -

    _ foo bar_

    -```````````````````````````````` - - -This is not emphasis, because the opening `_` is preceded -by an alphanumeric and followed by punctuation: - -```````````````````````````````` example -a_"foo"_ -. -

    a_"foo"_

    -```````````````````````````````` - - -Emphasis with `_` is not allowed inside words: - -```````````````````````````````` example -foo_bar_ -. -

    foo_bar_

    -```````````````````````````````` - - -```````````````````````````````` example -5_6_78 -. -

    5_6_78

    -```````````````````````````````` - - -```````````````````````````````` example -пристаням_стремятся_ -. -

    пристаням_стремятся_

    -```````````````````````````````` - - -Here `_` does not generate emphasis, because the first delimiter run -is right-flanking and the second left-flanking: - -```````````````````````````````` example -aa_"bb"_cc -. -

    aa_"bb"_cc

    -```````````````````````````````` - - -This is emphasis, even though the opening delimiter is -both left- and right-flanking, because it is preceded by -punctuation: - -```````````````````````````````` example -foo-_(bar)_ -. -

    foo-(bar)

    -```````````````````````````````` - - -Rule 3: - -This is not emphasis, because the closing delimiter does -not match the opening delimiter: - -```````````````````````````````` example -_foo* -. -

    _foo*

    -```````````````````````````````` - - -This is not emphasis, because the closing `*` is preceded by -whitespace: - -```````````````````````````````` example -*foo bar * -. -

    *foo bar *

    -```````````````````````````````` - - -A newline also counts as whitespace: - -```````````````````````````````` example -*foo bar -* -. -

    *foo bar -*

    -```````````````````````````````` - - -This is not emphasis, because the second `*` is -preceded by punctuation and followed by an alphanumeric -(hence it is not part of a [right-flanking delimiter run]: - -```````````````````````````````` example -*(*foo) -. -

    *(*foo)

    -```````````````````````````````` - - -The point of this restriction is more easily appreciated -with this example: - -```````````````````````````````` example -*(*foo*)* -. -

    (foo)

    -```````````````````````````````` - - -Intraword emphasis with `*` is allowed: - -```````````````````````````````` example -*foo*bar -. -

    foobar

    -```````````````````````````````` - - - -Rule 4: - -This is not emphasis, because the closing `_` is preceded by -whitespace: - -```````````````````````````````` example -_foo bar _ -. -

    _foo bar _

    -```````````````````````````````` - - -This is not emphasis, because the second `_` is -preceded by punctuation and followed by an alphanumeric: - -```````````````````````````````` example -_(_foo) -. -

    _(_foo)

    -```````````````````````````````` - - -This is emphasis within emphasis: - -```````````````````````````````` example -_(_foo_)_ -. -

    (foo)

    -```````````````````````````````` - - -Intraword emphasis is disallowed for `_`: - -```````````````````````````````` example -_foo_bar -. -

    _foo_bar

    -```````````````````````````````` - - -```````````````````````````````` example -_пристаням_стремятся -. -

    _пристаням_стремятся

    -```````````````````````````````` - - -```````````````````````````````` example -_foo_bar_baz_ -. -

    foo_bar_baz

    -```````````````````````````````` - - -This is emphasis, even though the closing delimiter is -both left- and right-flanking, because it is followed by -punctuation: - -```````````````````````````````` example -_(bar)_. -. -

    (bar).

    -```````````````````````````````` - - -Rule 5: - -```````````````````````````````` example -**foo bar** -. -

    foo bar

    -```````````````````````````````` - - -This is not strong emphasis, because the opening delimiter is -followed by whitespace: - -```````````````````````````````` example -** foo bar** -. -

    ** foo bar**

    -```````````````````````````````` - - -This is not strong emphasis, because the opening `**` is preceded -by an alphanumeric and followed by punctuation, and hence -not part of a [left-flanking delimiter run]: - -```````````````````````````````` example -a**"foo"** -. -

    a**"foo"**

    -```````````````````````````````` - - -Intraword strong emphasis with `**` is permitted: - -```````````````````````````````` example -foo**bar** -. -

    foobar

    -```````````````````````````````` - - -Rule 6: - -```````````````````````````````` example -__foo bar__ -. -

    foo bar

    -```````````````````````````````` - - -This is not strong emphasis, because the opening delimiter is -followed by whitespace: - -```````````````````````````````` example -__ foo bar__ -. -

    __ foo bar__

    -```````````````````````````````` - - -A newline counts as whitespace: -```````````````````````````````` example -__ -foo bar__ -. -

    __ -foo bar__

    -```````````````````````````````` - - -This is not strong emphasis, because the opening `__` is preceded -by an alphanumeric and followed by punctuation: - -```````````````````````````````` example -a__"foo"__ -. -

    a__"foo"__

    -```````````````````````````````` - - -Intraword strong emphasis is forbidden with `__`: - -```````````````````````````````` example -foo__bar__ -. -

    foo__bar__

    -```````````````````````````````` - - -```````````````````````````````` example -5__6__78 -. -

    5__6__78

    -```````````````````````````````` - - -```````````````````````````````` example -пристаням__стремятся__ -. -

    пристаням__стремятся__

    -```````````````````````````````` - - -```````````````````````````````` example -__foo, __bar__, baz__ -. -

    foo, bar, baz

    -```````````````````````````````` - - -This is strong emphasis, even though the opening delimiter is -both left- and right-flanking, because it is preceded by -punctuation: - -```````````````````````````````` example -foo-__(bar)__ -. -

    foo-(bar)

    -```````````````````````````````` - - - -Rule 7: - -This is not strong emphasis, because the closing delimiter is preceded -by whitespace: - -```````````````````````````````` example -**foo bar ** -. -

    **foo bar **

    -```````````````````````````````` - - -(Nor can it be interpreted as an emphasized `*foo bar *`, because of -Rule 11.) - -This is not strong emphasis, because the second `**` is -preceded by punctuation and followed by an alphanumeric: - -```````````````````````````````` example -**(**foo) -. -

    **(**foo)

    -```````````````````````````````` - - -The point of this restriction is more easily appreciated -with these examples: - -```````````````````````````````` example -*(**foo**)* -. -

    (foo)

    -```````````````````````````````` - - -```````````````````````````````` example -**Gomphocarpus (*Gomphocarpus physocarpus*, syn. -*Asclepias physocarpa*)** -. -

    Gomphocarpus (Gomphocarpus physocarpus, syn. -Asclepias physocarpa)

    -```````````````````````````````` - - -```````````````````````````````` example -**foo "*bar*" foo** -. -

    foo "bar" foo

    -```````````````````````````````` - - -Intraword emphasis: - -```````````````````````````````` example -**foo**bar -. -

    foobar

    -```````````````````````````````` - - -Rule 8: - -This is not strong emphasis, because the closing delimiter is -preceded by whitespace: - -```````````````````````````````` example -__foo bar __ -. -

    __foo bar __

    -```````````````````````````````` - - -This is not strong emphasis, because the second `__` is -preceded by punctuation and followed by an alphanumeric: - -```````````````````````````````` example -__(__foo) -. -

    __(__foo)

    -```````````````````````````````` - - -The point of this restriction is more easily appreciated -with this example: - -```````````````````````````````` example -_(__foo__)_ -. -

    (foo)

    -```````````````````````````````` - - -Intraword strong emphasis is forbidden with `__`: - -```````````````````````````````` example -__foo__bar -. -

    __foo__bar

    -```````````````````````````````` - - -```````````````````````````````` example -__пристаням__стремятся -. -

    __пристаням__стремятся

    -```````````````````````````````` - - -```````````````````````````````` example -__foo__bar__baz__ -. -

    foo__bar__baz

    -```````````````````````````````` - - -This is strong emphasis, even though the closing delimiter is -both left- and right-flanking, because it is followed by -punctuation: - -```````````````````````````````` example -__(bar)__. -. -

    (bar).

    -```````````````````````````````` - - -Rule 9: - -Any nonempty sequence of inline elements can be the contents of an -emphasized span. - -```````````````````````````````` example -*foo [bar](/url)* -. -

    foo bar

    -```````````````````````````````` - - -```````````````````````````````` example -*foo -bar* -. -

    foo -bar

    -```````````````````````````````` - - -In particular, emphasis and strong emphasis can be nested -inside emphasis: - -```````````````````````````````` example -_foo __bar__ baz_ -. -

    foo bar baz

    -```````````````````````````````` - - -```````````````````````````````` example -_foo _bar_ baz_ -. -

    foo bar baz

    -```````````````````````````````` - - -```````````````````````````````` example -__foo_ bar_ -. -

    foo bar

    -```````````````````````````````` - - -```````````````````````````````` example -*foo *bar** -. -

    foo bar

    -```````````````````````````````` - - -```````````````````````````````` example -*foo **bar** baz* -. -

    foo bar baz

    -```````````````````````````````` - -```````````````````````````````` example -*foo**bar**baz* -. -

    foobarbaz

    -```````````````````````````````` - -Note that in the preceding case, the interpretation - -``` markdown -

    foobarbaz

    -``` - - -is precluded by the condition that a delimiter that -can both open and close (like the `*` after `foo`) -cannot form emphasis if the sum of the lengths of -the delimiter runs containing the opening and -closing delimiters is a multiple of 3 unless -both lengths are multiples of 3. - - -For the same reason, we don't get two consecutive -emphasis sections in this example: - -```````````````````````````````` example -*foo**bar* -. -

    foo**bar

    -```````````````````````````````` - - -The same condition ensures that the following -cases are all strong emphasis nested inside -emphasis, even when the interior spaces are -omitted: - - -```````````````````````````````` example -***foo** bar* -. -

    foo bar

    -```````````````````````````````` - - -```````````````````````````````` example -*foo **bar*** -. -

    foo bar

    -```````````````````````````````` - - -```````````````````````````````` example -*foo**bar*** -. -

    foobar

    -```````````````````````````````` - - -When the lengths of the interior closing and opening -delimiter runs are *both* multiples of 3, though, -they can match to create emphasis: - -```````````````````````````````` example -foo***bar***baz -. -

    foobarbaz

    -```````````````````````````````` - -```````````````````````````````` example -foo******bar*********baz -. -

    foobar***baz

    -```````````````````````````````` - - -Indefinite levels of nesting are possible: - -```````````````````````````````` example -*foo **bar *baz* bim** bop* -. -

    foo bar baz bim bop

    -```````````````````````````````` - - -```````````````````````````````` example -*foo [*bar*](/url)* -. -

    foo bar

    -```````````````````````````````` - - -There can be no empty emphasis or strong emphasis: - -```````````````````````````````` example -** is not an empty emphasis -. -

    ** is not an empty emphasis

    -```````````````````````````````` - - -```````````````````````````````` example -**** is not an empty strong emphasis -. -

    **** is not an empty strong emphasis

    -```````````````````````````````` - - - -Rule 10: - -Any nonempty sequence of inline elements can be the contents of an -strongly emphasized span. - -```````````````````````````````` example -**foo [bar](/url)** -. -

    foo bar

    -```````````````````````````````` - - -```````````````````````````````` example -**foo -bar** -. -

    foo -bar

    -```````````````````````````````` - - -In particular, emphasis and strong emphasis can be nested -inside strong emphasis: - -```````````````````````````````` example -__foo _bar_ baz__ -. -

    foo bar baz

    -```````````````````````````````` - - -```````````````````````````````` example -__foo __bar__ baz__ -. -

    foo bar baz

    -```````````````````````````````` - - -```````````````````````````````` example -____foo__ bar__ -. -

    foo bar

    -```````````````````````````````` - - -```````````````````````````````` example -**foo **bar**** -. -

    foo bar

    -```````````````````````````````` - - -```````````````````````````````` example -**foo *bar* baz** -. -

    foo bar baz

    -```````````````````````````````` - - -```````````````````````````````` example -**foo*bar*baz** -. -

    foobarbaz

    -```````````````````````````````` - - -```````````````````````````````` example -***foo* bar** -. -

    foo bar

    -```````````````````````````````` - - -```````````````````````````````` example -**foo *bar*** -. -

    foo bar

    -```````````````````````````````` - - -Indefinite levels of nesting are possible: - -```````````````````````````````` example -**foo *bar **baz** -bim* bop** -. -

    foo bar baz -bim bop

    -```````````````````````````````` - - -```````````````````````````````` example -**foo [*bar*](/url)** -. -

    foo bar

    -```````````````````````````````` - - -There can be no empty emphasis or strong emphasis: - -```````````````````````````````` example -__ is not an empty emphasis -. -

    __ is not an empty emphasis

    -```````````````````````````````` - - -```````````````````````````````` example -____ is not an empty strong emphasis -. -

    ____ is not an empty strong emphasis

    -```````````````````````````````` - - - -Rule 11: - -```````````````````````````````` example -foo *** -. -

    foo ***

    -```````````````````````````````` - - -```````````````````````````````` example -foo *\** -. -

    foo *

    -```````````````````````````````` - - -```````````````````````````````` example -foo *_* -. -

    foo _

    -```````````````````````````````` - - -```````````````````````````````` example -foo ***** -. -

    foo *****

    -```````````````````````````````` - - -```````````````````````````````` example -foo **\*** -. -

    foo *

    -```````````````````````````````` - - -```````````````````````````````` example -foo **_** -. -

    foo _

    -```````````````````````````````` - - -Note that when delimiters do not match evenly, Rule 11 determines -that the excess literal `*` characters will appear outside of the -emphasis, rather than inside it: - -```````````````````````````````` example -**foo* -. -

    *foo

    -```````````````````````````````` - - -```````````````````````````````` example -*foo** -. -

    foo*

    -```````````````````````````````` - - -```````````````````````````````` example -***foo** -. -

    *foo

    -```````````````````````````````` - - -```````````````````````````````` example -****foo* -. -

    ***foo

    -```````````````````````````````` - - -```````````````````````````````` example -**foo*** -. -

    foo*

    -```````````````````````````````` - - -```````````````````````````````` example -*foo**** -. -

    foo***

    -```````````````````````````````` - - - -Rule 12: - -```````````````````````````````` example -foo ___ -. -

    foo ___

    -```````````````````````````````` - - -```````````````````````````````` example -foo _\__ -. -

    foo _

    -```````````````````````````````` - - -```````````````````````````````` example -foo _*_ -. -

    foo *

    -```````````````````````````````` - - -```````````````````````````````` example -foo _____ -. -

    foo _____

    -```````````````````````````````` - - -```````````````````````````````` example -foo __\___ -. -

    foo _

    -```````````````````````````````` - - -```````````````````````````````` example -foo __*__ -. -

    foo *

    -```````````````````````````````` - - -```````````````````````````````` example -__foo_ -. -

    _foo

    -```````````````````````````````` - - -Note that when delimiters do not match evenly, Rule 12 determines -that the excess literal `_` characters will appear outside of the -emphasis, rather than inside it: - -```````````````````````````````` example -_foo__ -. -

    foo_

    -```````````````````````````````` - - -```````````````````````````````` example -___foo__ -. -

    _foo

    -```````````````````````````````` - - -```````````````````````````````` example -____foo_ -. -

    ___foo

    -```````````````````````````````` - - -```````````````````````````````` example -__foo___ -. -

    foo_

    -```````````````````````````````` - - -```````````````````````````````` example -_foo____ -. -

    foo___

    -```````````````````````````````` - - -Rule 13 implies that if you want emphasis nested directly inside -emphasis, you must use different delimiters: - -```````````````````````````````` example -**foo** -. -

    foo

    -```````````````````````````````` - - -```````````````````````````````` example -*_foo_* -. -

    foo

    -```````````````````````````````` - - -```````````````````````````````` example -__foo__ -. -

    foo

    -```````````````````````````````` - - -```````````````````````````````` example -_*foo*_ -. -

    foo

    -```````````````````````````````` - - -However, strong emphasis within strong emphasis is possible without -switching delimiters: - -```````````````````````````````` example -****foo**** -. -

    foo

    -```````````````````````````````` - - -```````````````````````````````` example -____foo____ -. -

    foo

    -```````````````````````````````` - - - -Rule 13 can be applied to arbitrarily long sequences of -delimiters: - -```````````````````````````````` example -******foo****** -. -

    foo

    -```````````````````````````````` - - -Rule 14: - -```````````````````````````````` example -***foo*** -. -

    foo

    -```````````````````````````````` - - -```````````````````````````````` example -_____foo_____ -. -

    foo

    -```````````````````````````````` - - -Rule 15: - -```````````````````````````````` example -*foo _bar* baz_ -. -

    foo _bar baz_

    -```````````````````````````````` - - -```````````````````````````````` example -*foo __bar *baz bim__ bam* -. -

    foo bar *baz bim bam

    -```````````````````````````````` - - -Rule 16: - -```````````````````````````````` example -**foo **bar baz** -. -

    **foo bar baz

    -```````````````````````````````` - - -```````````````````````````````` example -*foo *bar baz* -. -

    *foo bar baz

    -```````````````````````````````` - - -Rule 17: - -```````````````````````````````` example -*[bar*](/url) -. -

    *bar*

    -```````````````````````````````` - - -```````````````````````````````` example -_foo [bar_](/url) -. -

    _foo bar_

    -```````````````````````````````` - - -```````````````````````````````` example -* -. -

    *

    -```````````````````````````````` - - -```````````````````````````````` example -** -. -

    **

    -```````````````````````````````` - - -```````````````````````````````` example -__ -. -

    __

    -```````````````````````````````` - - -```````````````````````````````` example -*a `*`* -. -

    a *

    -```````````````````````````````` - - -```````````````````````````````` example -_a `_`_ -. -

    a _

    -```````````````````````````````` - - -```````````````````````````````` example -**a -. -

    **ahttp://foo.bar/?q=**

    -```````````````````````````````` - - -```````````````````````````````` example -__a -. -

    __ahttp://foo.bar/?q=__

    -```````````````````````````````` - - - -## Links - -A link contains [link text] (the visible text), a [link destination] -(the URI that is the link destination), and optionally a [link title]. -There are two basic kinds of links in Markdown. In [inline links] the -destination and title are given immediately after the link text. In -[reference links] the destination and title are defined elsewhere in -the document. - -A [link text](@) consists of a sequence of zero or more -inline elements enclosed by square brackets (`[` and `]`). The -following rules apply: - -- Links may not contain other links, at any level of nesting. If - multiple otherwise valid link definitions appear nested inside each - other, the inner-most definition is used. - -- Brackets are allowed in the [link text] only if (a) they - are backslash-escaped or (b) they appear as a matched pair of brackets, - with an open bracket `[`, a sequence of zero or more inlines, and - a close bracket `]`. - -- Backtick [code spans], [autolinks], and raw [HTML tags] bind more tightly - than the brackets in link text. Thus, for example, - `` [foo`]` `` could not be a link text, since the second `]` - is part of a code span. - -- The brackets in link text bind more tightly than markers for - [emphasis and strong emphasis]. Thus, for example, `*[foo*](url)` is a link. - -A [link destination](@) consists of either - -- a sequence of zero or more characters between an opening `<` and a - closing `>` that contains no line breaks or unescaped - `<` or `>` characters, or - -- a nonempty sequence of characters that does not start with - `<`, does not include ASCII space or control characters, and - includes parentheses only if (a) they are backslash-escaped or - (b) they are part of a balanced pair of unescaped parentheses. - (Implementations may impose limits on parentheses nesting to - avoid performance issues, but at least three levels of nesting - should be supported.) - -A [link title](@) consists of either - -- a sequence of zero or more characters between straight double-quote - characters (`"`), including a `"` character only if it is - backslash-escaped, or - -- a sequence of zero or more characters between straight single-quote - characters (`'`), including a `'` character only if it is - backslash-escaped, or - -- a sequence of zero or more characters between matching parentheses - (`(...)`), including a `(` or `)` character only if it is - backslash-escaped. - -Although [link titles] may span multiple lines, they may not contain -a [blank line]. - -An [inline link](@) consists of a [link text] followed immediately -by a left parenthesis `(`, optional [whitespace], an optional -[link destination], an optional [link title] separated from the link -destination by [whitespace], optional [whitespace], and a right -parenthesis `)`. The link's text consists of the inlines contained -in the [link text] (excluding the enclosing square brackets). -The link's URI consists of the link destination, excluding enclosing -`<...>` if present, with backslash-escapes in effect as described -above. The link's title consists of the link title, excluding its -enclosing delimiters, with backslash-escapes in effect as described -above. - -Here is a simple inline link: - -```````````````````````````````` example -[link](/uri "title") -. -

    link

    -```````````````````````````````` - - -The title may be omitted: - -```````````````````````````````` example -[link](/uri) -. -

    link

    -```````````````````````````````` - - -Both the title and the destination may be omitted: - -```````````````````````````````` example -[link]() -. -

    link

    -```````````````````````````````` - - -```````````````````````````````` example -[link](<>) -. -

    link

    -```````````````````````````````` - -The destination can only contain spaces if it is -enclosed in pointy brackets: - -```````````````````````````````` example -[link](/my uri) -. -

    [link](/my uri)

    -```````````````````````````````` - -```````````````````````````````` example -[link](
    ) -. -

    link

    -```````````````````````````````` - -The destination cannot contain line breaks, -even if enclosed in pointy brackets: - -```````````````````````````````` example -[link](foo -bar) -. -

    [link](foo -bar)

    -```````````````````````````````` - -```````````````````````````````` example -[link]() -. -

    [link]()

    -```````````````````````````````` - -The destination can contain `)` if it is enclosed -in pointy brackets: - -```````````````````````````````` example -[a]() -. -

    a

    -```````````````````````````````` - -Pointy brackets that enclose links must be unescaped: - -```````````````````````````````` example -[link]() -. -

    [link](<foo>)

    -```````````````````````````````` - -These are not links, because the opening pointy bracket -is not matched properly: - -```````````````````````````````` example -[a]( -[a](c) -. -

    [a](<b)c -[a](<b)c> -[a](c)

    -```````````````````````````````` - -Parentheses inside the link destination may be escaped: - -```````````````````````````````` example -[link](\(foo\)) -. -

    link

    -```````````````````````````````` - -Any number of parentheses are allowed without escaping, as long as they are -balanced: - -```````````````````````````````` example -[link](foo(and(bar))) -. -

    link

    -```````````````````````````````` - -However, if you have unbalanced parentheses, you need to escape or use the -`<...>` form: - -```````````````````````````````` example -[link](foo\(and\(bar\)) -. -

    link

    -```````````````````````````````` - - -```````````````````````````````` example -[link]() -. -

    link

    -```````````````````````````````` - - -Parentheses and other symbols can also be escaped, as usual -in Markdown: - -```````````````````````````````` example -[link](foo\)\:) -. -

    link

    -```````````````````````````````` - - -A link can contain fragment identifiers and queries: - -```````````````````````````````` example -[link](#fragment) - -[link](http://example.com#fragment) - -[link](http://example.com?foo=3#frag) -. -

    link

    -

    link

    -

    link

    -```````````````````````````````` - - -Note that a backslash before a non-escapable character is -just a backslash: - -```````````````````````````````` example -[link](foo\bar) -. -

    link

    -```````````````````````````````` - - -URL-escaping should be left alone inside the destination, as all -URL-escaped characters are also valid URL characters. Entity and -numerical character references in the destination will be parsed -into the corresponding Unicode code points, as usual. These may -be optionally URL-escaped when written as HTML, but this spec -does not enforce any particular policy for rendering URLs in -HTML or other formats. Renderers may make different decisions -about how to escape or normalize URLs in the output. - -```````````````````````````````` example -[link](foo%20bä) -. -

    link

    -```````````````````````````````` - - -Note that, because titles can often be parsed as destinations, -if you try to omit the destination and keep the title, you'll -get unexpected results: - -```````````````````````````````` example -[link]("title") -. -

    link

    -```````````````````````````````` - - -Titles may be in single quotes, double quotes, or parentheses: - -```````````````````````````````` example -[link](/url "title") -[link](/url 'title') -[link](/url (title)) -. -

    link -link -link

    -```````````````````````````````` - - -Backslash escapes and entity and numeric character references -may be used in titles: - -```````````````````````````````` example -[link](/url "title \""") -. -

    link

    -```````````````````````````````` - - -Titles must be separated from the link using a [whitespace]. -Other [Unicode whitespace] like non-breaking space doesn't work. - -```````````````````````````````` example -[link](/url "title") -. -

    link

    -```````````````````````````````` - - -Nested balanced quotes are not allowed without escaping: - -```````````````````````````````` example -[link](/url "title "and" title") -. -

    [link](/url "title "and" title")

    -```````````````````````````````` - - -But it is easy to work around this by using a different quote type: - -```````````````````````````````` example -[link](/url 'title "and" title') -. -

    link

    -```````````````````````````````` - - -(Note: `Markdown.pl` did allow double quotes inside a double-quoted -title, and its test suite included a test demonstrating this. -But it is hard to see a good rationale for the extra complexity this -brings, since there are already many ways---backslash escaping, -entity and numeric character references, or using a different -quote type for the enclosing title---to write titles containing -double quotes. `Markdown.pl`'s handling of titles has a number -of other strange features. For example, it allows single-quoted -titles in inline links, but not reference links. And, in -reference links but not inline links, it allows a title to begin -with `"` and end with `)`. `Markdown.pl` 1.0.1 even allows -titles with no closing quotation mark, though 1.0.2b8 does not. -It seems preferable to adopt a simple, rational rule that works -the same way in inline links and link reference definitions.) - -[Whitespace] is allowed around the destination and title: - -```````````````````````````````` example -[link]( /uri - "title" ) -. -

    link

    -```````````````````````````````` - - -But it is not allowed between the link text and the -following parenthesis: - -```````````````````````````````` example -[link] (/uri) -. -

    [link] (/uri)

    -```````````````````````````````` - - -The link text may contain balanced brackets, but not unbalanced ones, -unless they are escaped: - -```````````````````````````````` example -[link [foo [bar]]](/uri) -. -

    link [foo [bar]]

    -```````````````````````````````` - - -```````````````````````````````` example -[link] bar](/uri) -. -

    [link] bar](/uri)

    -```````````````````````````````` - - -```````````````````````````````` example -[link [bar](/uri) -. -

    [link bar

    -```````````````````````````````` - - -```````````````````````````````` example -[link \[bar](/uri) -. -

    link [bar

    -```````````````````````````````` - - -The link text may contain inline content: - -```````````````````````````````` example -[link *foo **bar** `#`*](/uri) -. -

    link foo bar #

    -```````````````````````````````` - - -```````````````````````````````` example -[![moon](moon.jpg)](/uri) -. -

    moon

    -```````````````````````````````` - - -However, links may not contain other links, at any level of nesting. - -```````````````````````````````` example -[foo [bar](/uri)](/uri) -. -

    [foo bar](/uri)

    -```````````````````````````````` - - -```````````````````````````````` example -[foo *[bar [baz](/uri)](/uri)*](/uri) -. -

    [foo [bar baz](/uri)](/uri)

    -```````````````````````````````` - - -```````````````````````````````` example -![[[foo](uri1)](uri2)](uri3) -. -

    [foo](uri2)

    -```````````````````````````````` - - -These cases illustrate the precedence of link text grouping over -emphasis grouping: - -```````````````````````````````` example -*[foo*](/uri) -. -

    *foo*

    -```````````````````````````````` - - -```````````````````````````````` example -[foo *bar](baz*) -. -

    foo *bar

    -```````````````````````````````` - - -Note that brackets that *aren't* part of links do not take -precedence: - -```````````````````````````````` example -*foo [bar* baz] -. -

    foo [bar baz]

    -```````````````````````````````` - - -These cases illustrate the precedence of HTML tags, code spans, -and autolinks over link grouping: - -```````````````````````````````` example -[foo -. -

    [foo

    -```````````````````````````````` - - -```````````````````````````````` example -[foo`](/uri)` -. -

    [foo](/uri)

    -```````````````````````````````` - - -```````````````````````````````` example -[foo -. -

    [foohttp://example.com/?search=](uri)

    -```````````````````````````````` - - -There are three kinds of [reference link](@)s: -[full](#full-reference-link), [collapsed](#collapsed-reference-link), -and [shortcut](#shortcut-reference-link). - -A [full reference link](@) -consists of a [link text] immediately followed by a [link label] -that [matches] a [link reference definition] elsewhere in the document. - -A [link label](@) begins with a left bracket (`[`) and ends -with the first right bracket (`]`) that is not backslash-escaped. -Between these brackets there must be at least one [non-whitespace character]. -Unescaped square bracket characters are not allowed inside the -opening and closing square brackets of [link labels]. A link -label can have at most 999 characters inside the square -brackets. - -One label [matches](@) -another just in case their normalized forms are equal. To normalize a -label, strip off the opening and closing brackets, -perform the *Unicode case fold*, strip leading and trailing -[whitespace] and collapse consecutive internal -[whitespace] to a single space. If there are multiple -matching reference link definitions, the one that comes first in the -document is used. (It is desirable in such cases to emit a warning.) - -The contents of the first link label are parsed as inlines, which are -used as the link's text. The link's URI and title are provided by the -matching [link reference definition]. - -Here is a simple example: - -```````````````````````````````` example -[foo][bar] - -[bar]: /url "title" -. -

    foo

    -```````````````````````````````` - - -The rules for the [link text] are the same as with -[inline links]. Thus: - -The link text may contain balanced brackets, but not unbalanced ones, -unless they are escaped: - -```````````````````````````````` example -[link [foo [bar]]][ref] - -[ref]: /uri -. -

    link [foo [bar]]

    -```````````````````````````````` - - -```````````````````````````````` example -[link \[bar][ref] - -[ref]: /uri -. -

    link [bar

    -```````````````````````````````` - - -The link text may contain inline content: - -```````````````````````````````` example -[link *foo **bar** `#`*][ref] - -[ref]: /uri -. -

    link foo bar #

    -```````````````````````````````` - - -```````````````````````````````` example -[![moon](moon.jpg)][ref] - -[ref]: /uri -. -

    moon

    -```````````````````````````````` - - -However, links may not contain other links, at any level of nesting. - -```````````````````````````````` example -[foo [bar](/uri)][ref] - -[ref]: /uri -. -

    [foo bar]ref

    -```````````````````````````````` - - -```````````````````````````````` example -[foo *bar [baz][ref]*][ref] - -[ref]: /uri -. -

    [foo bar baz]ref

    -```````````````````````````````` - - -(In the examples above, we have two [shortcut reference links] -instead of one [full reference link].) - -The following cases illustrate the precedence of link text grouping over -emphasis grouping: - -```````````````````````````````` example -*[foo*][ref] - -[ref]: /uri -. -

    *foo*

    -```````````````````````````````` - - -```````````````````````````````` example -[foo *bar][ref] - -[ref]: /uri -. -

    foo *bar

    -```````````````````````````````` - - -These cases illustrate the precedence of HTML tags, code spans, -and autolinks over link grouping: - -```````````````````````````````` example -[foo - -[ref]: /uri -. -

    [foo

    -```````````````````````````````` - - -```````````````````````````````` example -[foo`][ref]` - -[ref]: /uri -. -

    [foo][ref]

    -```````````````````````````````` - - -```````````````````````````````` example -[foo - -[ref]: /uri -. -

    [foohttp://example.com/?search=][ref]

    -```````````````````````````````` - - -Matching is case-insensitive: - -```````````````````````````````` example -[foo][BaR] - -[bar]: /url "title" -. -

    foo

    -```````````````````````````````` - - -Unicode case fold is used: - -```````````````````````````````` example -[Толпой][Толпой] is a Russian word. - -[ТОЛПОЙ]: /url -. -

    Толпой is a Russian word.

    -```````````````````````````````` - - -Consecutive internal [whitespace] is treated as one space for -purposes of determining matching: - -```````````````````````````````` example -[Foo - bar]: /url - -[Baz][Foo bar] -. -

    Baz

    -```````````````````````````````` - - -No [whitespace] is allowed between the [link text] and the -[link label]: - -```````````````````````````````` example -[foo] [bar] - -[bar]: /url "title" -. -

    [foo] bar

    -```````````````````````````````` - - -```````````````````````````````` example -[foo] -[bar] - -[bar]: /url "title" -. -

    [foo] -bar

    -```````````````````````````````` - - -This is a departure from John Gruber's original Markdown syntax -description, which explicitly allows whitespace between the link -text and the link label. It brings reference links in line with -[inline links], which (according to both original Markdown and -this spec) cannot have whitespace after the link text. More -importantly, it prevents inadvertent capture of consecutive -[shortcut reference links]. If whitespace is allowed between the -link text and the link label, then in the following we will have -a single reference link, not two shortcut reference links, as -intended: - -``` markdown -[foo] -[bar] - -[foo]: /url1 -[bar]: /url2 -``` - -(Note that [shortcut reference links] were introduced by Gruber -himself in a beta version of `Markdown.pl`, but never included -in the official syntax description. Without shortcut reference -links, it is harmless to allow space between the link text and -link label; but once shortcut references are introduced, it is -too dangerous to allow this, as it frequently leads to -unintended results.) - -When there are multiple matching [link reference definitions], -the first is used: - -```````````````````````````````` example -[foo]: /url1 - -[foo]: /url2 - -[bar][foo] -. -

    bar

    -```````````````````````````````` - - -Note that matching is performed on normalized strings, not parsed -inline content. So the following does not match, even though the -labels define equivalent inline content: - -```````````````````````````````` example -[bar][foo\!] - -[foo!]: /url -. -

    [bar][foo!]

    -```````````````````````````````` - - -[Link labels] cannot contain brackets, unless they are -backslash-escaped: - -```````````````````````````````` example -[foo][ref[] - -[ref[]: /uri -. -

    [foo][ref[]

    -

    [ref[]: /uri

    -```````````````````````````````` - - -```````````````````````````````` example -[foo][ref[bar]] - -[ref[bar]]: /uri -. -

    [foo][ref[bar]]

    -

    [ref[bar]]: /uri

    -```````````````````````````````` - - -```````````````````````````````` example -[[[foo]]] - -[[[foo]]]: /url -. -

    [[[foo]]]

    -

    [[[foo]]]: /url

    -```````````````````````````````` - - -```````````````````````````````` example -[foo][ref\[] - -[ref\[]: /uri -. -

    foo

    -```````````````````````````````` - - -Note that in this example `]` is not backslash-escaped: - -```````````````````````````````` example -[bar\\]: /uri - -[bar\\] -. -

    bar\

    -```````````````````````````````` - - -A [link label] must contain at least one [non-whitespace character]: - -```````````````````````````````` example -[] - -[]: /uri -. -

    []

    -

    []: /uri

    -```````````````````````````````` - - -```````````````````````````````` example -[ - ] - -[ - ]: /uri -. -

    [ -]

    -

    [ -]: /uri

    -```````````````````````````````` - - -A [collapsed reference link](@) -consists of a [link label] that [matches] a -[link reference definition] elsewhere in the -document, followed by the string `[]`. -The contents of the first link label are parsed as inlines, -which are used as the link's text. The link's URI and title are -provided by the matching reference link definition. Thus, -`[foo][]` is equivalent to `[foo][foo]`. - -```````````````````````````````` example -[foo][] - -[foo]: /url "title" -. -

    foo

    -```````````````````````````````` - - -```````````````````````````````` example -[*foo* bar][] - -[*foo* bar]: /url "title" -. -

    foo bar

    -```````````````````````````````` - - -The link labels are case-insensitive: - -```````````````````````````````` example -[Foo][] - -[foo]: /url "title" -. -

    Foo

    -```````````````````````````````` - - - -As with full reference links, [whitespace] is not -allowed between the two sets of brackets: - -```````````````````````````````` example -[foo] -[] - -[foo]: /url "title" -. -

    foo -[]

    -```````````````````````````````` - - -A [shortcut reference link](@) -consists of a [link label] that [matches] a -[link reference definition] elsewhere in the -document and is not followed by `[]` or a link label. -The contents of the first link label are parsed as inlines, -which are used as the link's text. The link's URI and title -are provided by the matching link reference definition. -Thus, `[foo]` is equivalent to `[foo][]`. - -```````````````````````````````` example -[foo] - -[foo]: /url "title" -. -

    foo

    -```````````````````````````````` - - -```````````````````````````````` example -[*foo* bar] - -[*foo* bar]: /url "title" -. -

    foo bar

    -```````````````````````````````` - - -```````````````````````````````` example -[[*foo* bar]] - -[*foo* bar]: /url "title" -. -

    [foo bar]

    -```````````````````````````````` - - -```````````````````````````````` example -[[bar [foo] - -[foo]: /url -. -

    [[bar foo

    -```````````````````````````````` - - -The link labels are case-insensitive: - -```````````````````````````````` example -[Foo] - -[foo]: /url "title" -. -

    Foo

    -```````````````````````````````` - - -A space after the link text should be preserved: - -```````````````````````````````` example -[foo] bar - -[foo]: /url -. -

    foo bar

    -```````````````````````````````` - - -If you just want bracketed text, you can backslash-escape the -opening bracket to avoid links: - -```````````````````````````````` example -\[foo] - -[foo]: /url "title" -. -

    [foo]

    -```````````````````````````````` - - -Note that this is a link, because a link label ends with the first -following closing bracket: - -```````````````````````````````` example -[foo*]: /url - -*[foo*] -. -

    *foo*

    -```````````````````````````````` - - -Full and compact references take precedence over shortcut -references: - -```````````````````````````````` example -[foo][bar] - -[foo]: /url1 -[bar]: /url2 -. -

    foo

    -```````````````````````````````` - -```````````````````````````````` example -[foo][] - -[foo]: /url1 -. -

    foo

    -```````````````````````````````` - -Inline links also take precedence: - -```````````````````````````````` example -[foo]() - -[foo]: /url1 -. -

    foo

    -```````````````````````````````` - -```````````````````````````````` example -[foo](not a link) - -[foo]: /url1 -. -

    foo(not a link)

    -```````````````````````````````` - -In the following case `[bar][baz]` is parsed as a reference, -`[foo]` as normal text: - -```````````````````````````````` example -[foo][bar][baz] - -[baz]: /url -. -

    [foo]bar

    -```````````````````````````````` - - -Here, though, `[foo][bar]` is parsed as a reference, since -`[bar]` is defined: - -```````````````````````````````` example -[foo][bar][baz] - -[baz]: /url1 -[bar]: /url2 -. -

    foobaz

    -```````````````````````````````` - - -Here `[foo]` is not parsed as a shortcut reference, because it -is followed by a link label (even though `[bar]` is not defined): - -```````````````````````````````` example -[foo][bar][baz] - -[baz]: /url1 -[foo]: /url2 -. -

    [foo]bar

    -```````````````````````````````` - - - -## Images - -Syntax for images is like the syntax for links, with one -difference. Instead of [link text], we have an -[image description](@). The rules for this are the -same as for [link text], except that (a) an -image description starts with `![` rather than `[`, and -(b) an image description may contain links. -An image description has inline elements -as its contents. When an image is rendered to HTML, -this is standardly used as the image's `alt` attribute. - -```````````````````````````````` example -![foo](/url "title") -. -

    foo

    -```````````````````````````````` - - -```````````````````````````````` example -![foo *bar*] - -[foo *bar*]: train.jpg "train & tracks" -. -

    foo bar

    -```````````````````````````````` - - -```````````````````````````````` example -![foo ![bar](/url)](/url2) -. -

    foo bar

    -```````````````````````````````` - - -```````````````````````````````` example -![foo [bar](/url)](/url2) -. -

    foo bar

    -```````````````````````````````` - - -Though this spec is concerned with parsing, not rendering, it is -recommended that in rendering to HTML, only the plain string content -of the [image description] be used. Note that in -the above example, the alt attribute's value is `foo bar`, not `foo -[bar](/url)` or `foo bar`. Only the plain string -content is rendered, without formatting. - -```````````````````````````````` example -![foo *bar*][] - -[foo *bar*]: train.jpg "train & tracks" -. -

    foo bar

    -```````````````````````````````` - - -```````````````````````````````` example -![foo *bar*][foobar] - -[FOOBAR]: train.jpg "train & tracks" -. -

    foo bar

    -```````````````````````````````` - - -```````````````````````````````` example -![foo](train.jpg) -. -

    foo

    -```````````````````````````````` - - -```````````````````````````````` example -My ![foo bar](/path/to/train.jpg "title" ) -. -

    My foo bar

    -```````````````````````````````` - - -```````````````````````````````` example -![foo]() -. -

    foo

    -```````````````````````````````` - - -```````````````````````````````` example -![](/url) -. -

    -```````````````````````````````` - - -Reference-style: - -```````````````````````````````` example -![foo][bar] - -[bar]: /url -. -

    foo

    -```````````````````````````````` - - -```````````````````````````````` example -![foo][bar] - -[BAR]: /url -. -

    foo

    -```````````````````````````````` - - -Collapsed: - -```````````````````````````````` example -![foo][] - -[foo]: /url "title" -. -

    foo

    -```````````````````````````````` - - -```````````````````````````````` example -![*foo* bar][] - -[*foo* bar]: /url "title" -. -

    foo bar

    -```````````````````````````````` - - -The labels are case-insensitive: - -```````````````````````````````` example -![Foo][] - -[foo]: /url "title" -. -

    Foo

    -```````````````````````````````` - - -As with reference links, [whitespace] is not allowed -between the two sets of brackets: - -```````````````````````````````` example -![foo] -[] - -[foo]: /url "title" -. -

    foo -[]

    -```````````````````````````````` - - -Shortcut: - -```````````````````````````````` example -![foo] - -[foo]: /url "title" -. -

    foo

    -```````````````````````````````` - - -```````````````````````````````` example -![*foo* bar] - -[*foo* bar]: /url "title" -. -

    foo bar

    -```````````````````````````````` - - -Note that link labels cannot contain unescaped brackets: - -```````````````````````````````` example -![[foo]] - -[[foo]]: /url "title" -. -

    ![[foo]]

    -

    [[foo]]: /url "title"

    -```````````````````````````````` - - -The link labels are case-insensitive: - -```````````````````````````````` example -![Foo] - -[foo]: /url "title" -. -

    Foo

    -```````````````````````````````` - - -If you just want a literal `!` followed by bracketed text, you can -backslash-escape the opening `[`: - -```````````````````````````````` example -!\[foo] - -[foo]: /url "title" -. -

    ![foo]

    -```````````````````````````````` - - -If you want a link after a literal `!`, backslash-escape the -`!`: - -```````````````````````````````` example -\![foo] - -[foo]: /url "title" -. -

    !foo

    -```````````````````````````````` - - -## Autolinks - -[Autolink](@)s are absolute URIs and email addresses inside -`<` and `>`. They are parsed as links, with the URL or email address -as the link label. - -A [URI autolink](@) consists of `<`, followed by an -[absolute URI] followed by `>`. It is parsed as -a link to the URI, with the URI as the link's label. - -An [absolute URI](@), -for these purposes, consists of a [scheme] followed by a colon (`:`) -followed by zero or more characters other than ASCII -[whitespace] and control characters, `<`, and `>`. If -the URI includes these characters, they must be percent-encoded -(e.g. `%20` for a space). - -For purposes of this spec, a [scheme](@) is any sequence -of 2--32 characters beginning with an ASCII letter and followed -by any combination of ASCII letters, digits, or the symbols plus -("+"), period ("."), or hyphen ("-"). - -Here are some valid autolinks: - -```````````````````````````````` example - -. -

    http://foo.bar.baz

    -```````````````````````````````` - - -```````````````````````````````` example - -. -

    http://foo.bar.baz/test?q=hello&id=22&boolean

    -```````````````````````````````` - - -```````````````````````````````` example - -. -

    irc://foo.bar:2233/baz

    -```````````````````````````````` - - -Uppercase is also fine: - -```````````````````````````````` example - -. -

    MAILTO:FOO@BAR.BAZ

    -```````````````````````````````` - - -Note that many strings that count as [absolute URIs] for -purposes of this spec are not valid URIs, because their -schemes are not registered or because of other problems -with their syntax: - -```````````````````````````````` example - -. -

    a+b+c:d

    -```````````````````````````````` - - -```````````````````````````````` example - -. -

    made-up-scheme://foo,bar

    -```````````````````````````````` - - -```````````````````````````````` example - -. -

    http://../

    -```````````````````````````````` - - -```````````````````````````````` example - -. -

    localhost:5001/foo

    -```````````````````````````````` - - -Spaces are not allowed in autolinks: - -```````````````````````````````` example - -. -

    <http://foo.bar/baz bim>

    -```````````````````````````````` - - -Backslash-escapes do not work inside autolinks: - -```````````````````````````````` example - -. -

    http://example.com/\[\

    -```````````````````````````````` - - -An [email autolink](@) -consists of `<`, followed by an [email address], -followed by `>`. The link's label is the email address, -and the URL is `mailto:` followed by the email address. - -An [email address](@), -for these purposes, is anything that matches -the [non-normative regex from the HTML5 -spec](https://html.spec.whatwg.org/multipage/forms.html#e-mail-state-(type=email)): - - /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])? - (?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ - -Examples of email autolinks: - -```````````````````````````````` example - -. -

    foo@bar.example.com

    -```````````````````````````````` - - -```````````````````````````````` example - -. -

    foo+special@Bar.baz-bar0.com

    -```````````````````````````````` - - -Backslash-escapes do not work inside email autolinks: - -```````````````````````````````` example - -. -

    <foo+@bar.example.com>

    -```````````````````````````````` - - -These are not autolinks: - -```````````````````````````````` example -<> -. -

    <>

    -```````````````````````````````` - - -```````````````````````````````` example -< http://foo.bar > -. -

    < http://foo.bar >

    -```````````````````````````````` - - -```````````````````````````````` example - -. -

    <m:abc>

    -```````````````````````````````` - - -```````````````````````````````` example - -. -

    <foo.bar.baz>

    -```````````````````````````````` - - -```````````````````````````````` example -http://example.com -. -

    http://example.com

    -```````````````````````````````` - - -```````````````````````````````` example -foo@bar.example.com -. -

    foo@bar.example.com

    -```````````````````````````````` - - -## Raw HTML - -Text between `<` and `>` that looks like an HTML tag is parsed as a -raw HTML tag and will be rendered in HTML without escaping. -Tag and attribute names are not limited to current HTML tags, -so custom tags (and even, say, DocBook tags) may be used. - -Here is the grammar for tags: - -A [tag name](@) consists of an ASCII letter -followed by zero or more ASCII letters, digits, or -hyphens (`-`). - -An [attribute](@) consists of [whitespace], -an [attribute name], and an optional -[attribute value specification]. - -An [attribute name](@) -consists of an ASCII letter, `_`, or `:`, followed by zero or more ASCII -letters, digits, `_`, `.`, `:`, or `-`. (Note: This is the XML -specification restricted to ASCII. HTML5 is laxer.) - -An [attribute value specification](@) -consists of optional [whitespace], -a `=` character, optional [whitespace], and an [attribute -value]. - -An [attribute value](@) -consists of an [unquoted attribute value], -a [single-quoted attribute value], or a [double-quoted attribute value]. - -An [unquoted attribute value](@) -is a nonempty string of characters not -including [whitespace], `"`, `'`, `=`, `<`, `>`, or `` ` ``. - -A [single-quoted attribute value](@) -consists of `'`, zero or more -characters not including `'`, and a final `'`. - -A [double-quoted attribute value](@) -consists of `"`, zero or more -characters not including `"`, and a final `"`. - -An [open tag](@) consists of a `<` character, a [tag name], -zero or more [attributes], optional [whitespace], an optional `/` -character, and a `>` character. - -A [closing tag](@) consists of the string ``. - -An [HTML comment](@) consists of ``, -where *text* does not start with `>` or `->`, does not end with `-`, -and does not contain `--`. (See the -[HTML5 spec](http://www.w3.org/TR/html5/syntax.html#comments).) - -A [processing instruction](@) -consists of the string ``, and the string -`?>`. - -A [declaration](@) consists of the -string ``, and the character `>`. - -A [CDATA section](@) consists of -the string ``, and the string `]]>`. - -An [HTML tag](@) consists of an [open tag], a [closing tag], -an [HTML comment], a [processing instruction], a [declaration], -or a [CDATA section]. - -Here are some simple open tags: - -```````````````````````````````` example - -. -

    -```````````````````````````````` - - -Empty elements: - -```````````````````````````````` example - -. -

    -```````````````````````````````` - - -[Whitespace] is allowed: - -```````````````````````````````` example - -. -

    -```````````````````````````````` - - -With attributes: - -```````````````````````````````` example - -. -

    -```````````````````````````````` - - -Custom tag names can be used: - -```````````````````````````````` example -Foo -. -

    Foo

    -```````````````````````````````` - - -Illegal tag names, not parsed as HTML: - -```````````````````````````````` example -<33> <__> -. -

    <33> <__>

    -```````````````````````````````` - - -Illegal attribute names: - -```````````````````````````````` example -
    -. -

    <a h*#ref="hi">

    -```````````````````````````````` - - -Illegal attribute values: - -```````````````````````````````` example -
    -. -

    </a href="foo">

    -```````````````````````````````` - - -Comments: - -```````````````````````````````` example -foo -. -

    foo

    -```````````````````````````````` - - -```````````````````````````````` example -foo -. -

    foo <!-- not a comment -- two hyphens -->

    -```````````````````````````````` - - -Not comments: - -```````````````````````````````` example -foo foo --> - -foo -. -

    foo <!--> foo -->

    -

    foo <!-- foo--->

    -```````````````````````````````` - - -Processing instructions: - -```````````````````````````````` example -foo -. -

    foo

    -```````````````````````````````` - - -Declarations: - -```````````````````````````````` example -foo -. -

    foo

    -```````````````````````````````` - - -CDATA sections: - -```````````````````````````````` example -foo &<]]> -. -

    foo &<]]>

    -```````````````````````````````` - - -Entity and numeric character references are preserved in HTML -attributes: - -```````````````````````````````` example -foo
    -. -

    foo

    -```````````````````````````````` - - -Backslash escapes do not work in HTML attributes: - -```````````````````````````````` example -foo -. -

    foo

    -```````````````````````````````` - - -```````````````````````````````` example - -. -

    <a href=""">

    -```````````````````````````````` - - -## Hard line breaks - -A line break (not in a code span or HTML tag) that is preceded -by two or more spaces and does not occur at the end of a block -is parsed as a [hard line break](@) (rendered -in HTML as a `
    ` tag): - -```````````````````````````````` example -foo -baz -. -

    foo
    -baz

    -```````````````````````````````` - - -For a more visible alternative, a backslash before the -[line ending] may be used instead of two spaces: - -```````````````````````````````` example -foo\ -baz -. -

    foo
    -baz

    -```````````````````````````````` - - -More than two spaces can be used: - -```````````````````````````````` example -foo -baz -. -

    foo
    -baz

    -```````````````````````````````` - - -Leading spaces at the beginning of the next line are ignored: - -```````````````````````````````` example -foo - bar -. -

    foo
    -bar

    -```````````````````````````````` - - -```````````````````````````````` example -foo\ - bar -. -

    foo
    -bar

    -```````````````````````````````` - - -Line breaks can occur inside emphasis, links, and other constructs -that allow inline content: - -```````````````````````````````` example -*foo -bar* -. -

    foo
    -bar

    -```````````````````````````````` - - -```````````````````````````````` example -*foo\ -bar* -. -

    foo
    -bar

    -```````````````````````````````` - - -Line breaks do not occur inside code spans - -```````````````````````````````` example -`code -span` -. -

    code span

    -```````````````````````````````` - - -```````````````````````````````` example -`code\ -span` -. -

    code\ span

    -```````````````````````````````` - - -or HTML tags: - -```````````````````````````````` example -
    -. -

    -```````````````````````````````` - - -```````````````````````````````` example - -. -

    -```````````````````````````````` - - -Hard line breaks are for separating inline content within a block. -Neither syntax for hard line breaks works at the end of a paragraph or -other block element: - -```````````````````````````````` example -foo\ -. -

    foo\

    -```````````````````````````````` - - -```````````````````````````````` example -foo -. -

    foo

    -```````````````````````````````` - - -```````````````````````````````` example -### foo\ -. -

    foo\

    -```````````````````````````````` - - -```````````````````````````````` example -### foo -. -

    foo

    -```````````````````````````````` - - -## Soft line breaks - -A regular line break (not in a code span or HTML tag) that is not -preceded by two or more spaces or a backslash is parsed as a -[softbreak](@). (A softbreak may be rendered in HTML either as a -[line ending] or as a space. The result will be the same in -browsers. In the examples here, a [line ending] will be used.) - -```````````````````````````````` example -foo -baz -. -

    foo -baz

    -```````````````````````````````` - - -Spaces at the end of the line and beginning of the next line are -removed: - -```````````````````````````````` example -foo - baz -. -

    foo -baz

    -```````````````````````````````` - - -A conforming parser may render a soft line break in HTML either as a -line break or as a space. - -A renderer may also provide an option to render soft line breaks -as hard line breaks. - -## Textual content - -Any characters not given an interpretation by the above rules will -be parsed as plain textual content. - -```````````````````````````````` example -hello $.;'there -. -

    hello $.;'there

    -```````````````````````````````` - - -```````````````````````````````` example -Foo χρῆν -. -

    Foo χρῆν

    -```````````````````````````````` - - -Internal spaces are preserved verbatim: - -```````````````````````````````` example -Multiple spaces -. -

    Multiple spaces

    -```````````````````````````````` - - - - -# Appendix: A parsing strategy - -In this appendix we describe some features of the parsing strategy -used in the CommonMark reference implementations. - -## Overview - -Parsing has two phases: - -1. In the first phase, lines of input are consumed and the block -structure of the document---its division into paragraphs, block quotes, -list items, and so on---is constructed. Text is assigned to these -blocks but not parsed. Link reference definitions are parsed and a -map of links is constructed. - -2. In the second phase, the raw text contents of paragraphs and headings -are parsed into sequences of Markdown inline elements (strings, -code spans, links, emphasis, and so on), using the map of link -references constructed in phase 1. - -At each point in processing, the document is represented as a tree of -**blocks**. The root of the tree is a `document` block. The `document` -may have any number of other blocks as **children**. These children -may, in turn, have other blocks as children. The last child of a block -is normally considered **open**, meaning that subsequent lines of input -can alter its contents. (Blocks that are not open are **closed**.) -Here, for example, is a possible document tree, with the open blocks -marked by arrows: - -``` tree --> document - -> block_quote - paragraph - "Lorem ipsum dolor\nsit amet." - -> list (type=bullet tight=true bullet_char=-) - list_item - paragraph - "Qui *quodsi iracundia*" - -> list_item - -> paragraph - "aliquando id" -``` - -## Phase 1: block structure - -Each line that is processed has an effect on this tree. The line is -analyzed and, depending on its contents, the document may be altered -in one or more of the following ways: - -1. One or more open blocks may be closed. -2. One or more new blocks may be created as children of the - last open block. -3. Text may be added to the last (deepest) open block remaining - on the tree. - -Once a line has been incorporated into the tree in this way, -it can be discarded, so input can be read in a stream. - -For each line, we follow this procedure: - -1. First we iterate through the open blocks, starting with the -root document, and descending through last children down to the last -open block. Each block imposes a condition that the line must satisfy -if the block is to remain open. For example, a block quote requires a -`>` character. A paragraph requires a non-blank line. -In this phase we may match all or just some of the open -blocks. But we cannot close unmatched blocks yet, because we may have a -[lazy continuation line]. - -2. Next, after consuming the continuation markers for existing -blocks, we look for new block starts (e.g. `>` for a block quote). -If we encounter a new block start, we close any blocks unmatched -in step 1 before creating the new block as a child of the last -matched block. - -3. Finally, we look at the remainder of the line (after block -markers like `>`, list markers, and indentation have been consumed). -This is text that can be incorporated into the last open -block (a paragraph, code block, heading, or raw HTML). - -Setext headings are formed when we see a line of a paragraph -that is a [setext heading underline]. - -Reference link definitions are detected when a paragraph is closed; -the accumulated text lines are parsed to see if they begin with -one or more reference link definitions. Any remainder becomes a -normal paragraph. - -We can see how this works by considering how the tree above is -generated by four lines of Markdown: - -``` markdown -> Lorem ipsum dolor -sit amet. -> - Qui *quodsi iracundia* -> - aliquando id -``` - -At the outset, our document model is just - -``` tree --> document -``` - -The first line of our text, - -``` markdown -> Lorem ipsum dolor -``` - -causes a `block_quote` block to be created as a child of our -open `document` block, and a `paragraph` block as a child of -the `block_quote`. Then the text is added to the last open -block, the `paragraph`: - -``` tree --> document - -> block_quote - -> paragraph - "Lorem ipsum dolor" -``` - -The next line, - -``` markdown -sit amet. -``` - -is a "lazy continuation" of the open `paragraph`, so it gets added -to the paragraph's text: - -``` tree --> document - -> block_quote - -> paragraph - "Lorem ipsum dolor\nsit amet." -``` - -The third line, - -``` markdown -> - Qui *quodsi iracundia* -``` - -causes the `paragraph` block to be closed, and a new `list` block -opened as a child of the `block_quote`. A `list_item` is also -added as a child of the `list`, and a `paragraph` as a child of -the `list_item`. The text is then added to the new `paragraph`: - -``` tree --> document - -> block_quote - paragraph - "Lorem ipsum dolor\nsit amet." - -> list (type=bullet tight=true bullet_char=-) - -> list_item - -> paragraph - "Qui *quodsi iracundia*" -``` - -The fourth line, - -``` markdown -> - aliquando id -``` - -causes the `list_item` (and its child the `paragraph`) to be closed, -and a new `list_item` opened up as child of the `list`. A `paragraph` -is added as a child of the new `list_item`, to contain the text. -We thus obtain the final tree: - -``` tree --> document - -> block_quote - paragraph - "Lorem ipsum dolor\nsit amet." - -> list (type=bullet tight=true bullet_char=-) - list_item - paragraph - "Qui *quodsi iracundia*" - -> list_item - -> paragraph - "aliquando id" -``` - -## Phase 2: inline structure - -Once all of the input has been parsed, all open blocks are closed. - -We then "walk the tree," visiting every node, and parse raw -string contents of paragraphs and headings as inlines. At this -point we have seen all the link reference definitions, so we can -resolve reference links as we go. - -``` tree -document - block_quote - paragraph - str "Lorem ipsum dolor" - softbreak - str "sit amet." - list (type=bullet tight=true bullet_char=-) - list_item - paragraph - str "Qui " - emph - str "quodsi iracundia" - list_item - paragraph - str "aliquando id" -``` - -Notice how the [line ending] in the first paragraph has -been parsed as a `softbreak`, and the asterisks in the first list item -have become an `emph`. - -### An algorithm for parsing nested emphasis and links - -By far the trickiest part of inline parsing is handling emphasis, -strong emphasis, links, and images. This is done using the following -algorithm. - -When we're parsing inlines and we hit either - -- a run of `*` or `_` characters, or -- a `[` or `![` - -we insert a text node with these symbols as its literal content, and we -add a pointer to this text node to the [delimiter stack](@). - -The [delimiter stack] is a doubly linked list. Each -element contains a pointer to a text node, plus information about - -- the type of delimiter (`[`, `![`, `*`, `_`) -- the number of delimiters, -- whether the delimiter is "active" (all are active to start), and -- whether the delimiter is a potential opener, a potential closer, - or both (which depends on what sort of characters precede - and follow the delimiters). - -When we hit a `]` character, we call the *look for link or image* -procedure (see below). - -When we hit the end of the input, we call the *process emphasis* -procedure (see below), with `stack_bottom` = NULL. - -#### *look for link or image* - -Starting at the top of the delimiter stack, we look backwards -through the stack for an opening `[` or `![` delimiter. - -- If we don't find one, we return a literal text node `]`. - -- If we do find one, but it's not *active*, we remove the inactive - delimiter from the stack, and return a literal text node `]`. - -- If we find one and it's active, then we parse ahead to see if - we have an inline link/image, reference link/image, compact reference - link/image, or shortcut reference link/image. - - + If we don't, then we remove the opening delimiter from the - delimiter stack and return a literal text node `]`. - - + If we do, then - - * We return a link or image node whose children are the inlines - after the text node pointed to by the opening delimiter. - - * We run *process emphasis* on these inlines, with the `[` opener - as `stack_bottom`. - - * We remove the opening delimiter. - - * If we have a link (and not an image), we also set all - `[` delimiters before the opening delimiter to *inactive*. (This - will prevent us from getting links within links.) - -#### *process emphasis* - -Parameter `stack_bottom` sets a lower bound to how far we -descend in the [delimiter stack]. If it is NULL, we can -go all the way to the bottom. Otherwise, we stop before -visiting `stack_bottom`. - -Let `current_position` point to the element on the [delimiter stack] -just above `stack_bottom` (or the first element if `stack_bottom` -is NULL). - -We keep track of the `openers_bottom` for each delimiter -type (`*`, `_`) and each length of the closing delimiter run -(modulo 3). Initialize this to `stack_bottom`. - -Then we repeat the following until we run out of potential -closers: - -- Move `current_position` forward in the delimiter stack (if needed) - until we find the first potential closer with delimiter `*` or `_`. - (This will be the potential closer closest - to the beginning of the input -- the first one in parse order.) - -- Now, look back in the stack (staying above `stack_bottom` and - the `openers_bottom` for this delimiter type) for the - first matching potential opener ("matching" means same delimiter). - -- If one is found: - - + Figure out whether we have emphasis or strong emphasis: - if both closer and opener spans have length >= 2, we have - strong, otherwise regular. - - + Insert an emph or strong emph node accordingly, after - the text node corresponding to the opener. - - + Remove any delimiters between the opener and closer from - the delimiter stack. - - + Remove 1 (for regular emph) or 2 (for strong emph) delimiters - from the opening and closing text nodes. If they become empty - as a result, remove them and remove the corresponding element - of the delimiter stack. If the closing node is removed, reset - `current_position` to the next element in the stack. - -- If none is found: - - + Set `openers_bottom` to the element before `current_position`. - (We know that there are no openers for this kind of closer up to and - including this point, so this puts a lower bound on future searches.) - - + If the closer at `current_position` is not a potential opener, - remove it from the delimiter stack (since we know it can't - be a closer either). - - + Advance `current_position` to the next element in the stack. - -After we're done, we remove all delimiters above `stack_bottom` from the -delimiter stack. - diff --git a/hugolib/testhelpers_test.go b/hugolib/testhelpers_test.go index 2007b658d..3db2d9d51 100644 --- a/hugolib/testhelpers_test.go +++ b/hugolib/testhelpers_test.go @@ -1,919 +1,133 @@ package hugolib import ( - "bytes" - "context" - "fmt" - "image/jpeg" - "io" - "io/fs" - "math/rand" - "os" "path/filepath" - "regexp" - "strings" "testing" - "text/template" - "time" - "github.com/gohugoio/hugo/config/allconfig" - "github.com/gohugoio/hugo/config/security" - "github.com/gohugoio/hugo/htesting" + "regexp" - "github.com/gohugoio/hugo/output" + "fmt" + "strings" - "github.com/gohugoio/hugo/parser/metadecoders" - "github.com/google/go-cmp/cmp" - - "github.com/gohugoio/hugo/parser" - - "github.com/fsnotify/fsnotify" - "github.com/gohugoio/hugo/common/hexec" - "github.com/gohugoio/hugo/common/loggers" - "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/deps" - "github.com/gohugoio/hugo/resources/page" - "github.com/sanity-io/litter" "github.com/spf13/afero" - "github.com/spf13/cast" "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/source" + "github.com/gohugoio/hugo/tpl" + "github.com/spf13/viper" - "github.com/gohugoio/hugo/resources/resource" + "io/ioutil" + "os" + + "log" - qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/hugofs" + jww "github.com/spf13/jwalterweatherman" + "github.com/stretchr/testify/require" ) -var ( - deepEqualsPages = qt.CmpEquals(cmp.Comparer(func(p1, p2 *pageState) bool { return p1 == p2 })) - deepEqualsOutputFormats = qt.CmpEquals(cmp.Comparer(func(o1, o2 output.Format) bool { - return o1.Name == o2.Name && o1.MediaType.Type == o2.MediaType.Type - })) -) - -type sitesBuilder struct { - Cfg config.Provider - Configs *allconfig.Configs - - environ []string - - Fs *hugofs.Fs - T testing.TB - depsCfg deps.DepsCfg - - *qt.C - - logger loggers.Logger - rnd *rand.Rand - dumper litter.Options - - // Used to test partial rebuilds. - changedFiles []string - removedFiles []string - - // Aka the Hugo server mode. - running bool - - H *HugoSites - - theme string - - // Default toml - configFormat string - configFileSet bool - configSet bool - - // Default is empty. - // TODO(bep) revisit this and consider always setting it to something. - // Consider this in relation to using the BaseFs.PublishFs to all publishing. - workingDir string - - addNothing bool - // Base data/content - contentFilePairs []filenameContent - templateFilePairs []filenameContent - i18nFilePairs []filenameContent - dataFilePairs []filenameContent - - // Additional data/content. - // As in "use the base, but add these on top". - contentFilePairsAdded []filenameContent - templateFilePairsAdded []filenameContent - i18nFilePairsAdded []filenameContent - dataFilePairsAdded []filenameContent -} - -type filenameContent struct { - filename string - content string -} - -func newTestSitesBuilder(t testing.TB) *sitesBuilder { - v := config.New() - v.Set("publishDir", "public") - v.Set("disableLiveReload", true) - fs := hugofs.NewFromOld(afero.NewMemMapFs(), v) - - litterOptions := litter.Options{ - HidePrivateFields: true, - StripPackageNames: true, - Separator: " ", - } - - return &sitesBuilder{ - T: t, C: qt.New(t), Fs: fs, configFormat: "toml", - dumper: litterOptions, rnd: rand.New(rand.NewSource(time.Now().Unix())), - } -} - -func newTestSitesBuilderFromDepsCfg(t testing.TB, d deps.DepsCfg) *sitesBuilder { - c := qt.New(t) - - litterOptions := litter.Options{ - HidePrivateFields: true, - StripPackageNames: true, - Separator: " ", - } - - b := &sitesBuilder{T: t, C: c, depsCfg: d, Fs: d.Fs, dumper: litterOptions, rnd: rand.New(rand.NewSource(time.Now().Unix()))} - workingDir := d.Configs.LoadingInfo.BaseConfig.WorkingDir - - b.WithWorkingDir(workingDir) - - return b -} - -func (s *sitesBuilder) Running() *sitesBuilder { - s.running = true - return s -} - -func (s *sitesBuilder) WithNothingAdded() *sitesBuilder { - s.addNothing = true - return s -} - -func (s *sitesBuilder) WithLogger(logger loggers.Logger) *sitesBuilder { - s.logger = logger - return s -} - -func (s *sitesBuilder) WithWorkingDir(dir string) *sitesBuilder { - s.workingDir = filepath.FromSlash(dir) - return s -} - -func (s *sitesBuilder) WithEnviron(env ...string) *sitesBuilder { - for i := 0; i < len(env); i += 2 { - s.environ = append(s.environ, fmt.Sprintf("%s=%s", env[i], env[i+1])) - } - return s -} - -func (s *sitesBuilder) WithConfigTemplate(data any, format, configTemplate string) *sitesBuilder { - s.T.Helper() - - if format == "" { - format = "toml" - } - - templ, err := template.New("test").Parse(configTemplate) - if err != nil { - s.Fatalf("Template parse failed: %s", err) - } - var b bytes.Buffer - templ.Execute(&b, data) - return s.WithConfigFile(format, b.String()) -} - -func (s *sitesBuilder) WithViper(v config.Provider) *sitesBuilder { - s.T.Helper() - if s.configFileSet { - s.T.Fatal("WithViper: use Viper or config.toml, not both") - } - defer func() { - s.configSet = true - }() - - // Write to a config file to make sure the tests follow the same code path. - var buff bytes.Buffer - m := v.Get("").(maps.Params) - s.Assert(parser.InterfaceToConfig(m, metadecoders.TOML, &buff), qt.IsNil) - return s.WithConfigFile("toml", buff.String()) -} - -func (s *sitesBuilder) WithConfigFile(format, conf string) *sitesBuilder { - s.T.Helper() - if s.configSet { - s.T.Fatal("WithConfigFile: use config.Config or config.toml, not both") - } - s.configFileSet = true - filename := s.absFilename("config." + format) - writeSource(s.T, s.Fs, filename, conf) - s.configFormat = format - return s -} - -func (s *sitesBuilder) WithThemeConfigFile(format, conf string) *sitesBuilder { - s.T.Helper() - if s.theme == "" { - s.theme = "test-theme" - } - filename := filepath.Join("themes", s.theme, "config."+format) - writeSource(s.T, s.Fs, s.absFilename(filename), conf) - return s -} - -func (s *sitesBuilder) WithSourceFile(filenameContent ...string) *sitesBuilder { - s.T.Helper() - for i := 0; i < len(filenameContent); i += 2 { - writeSource(s.T, s.Fs, s.absFilename(filenameContent[i]), filenameContent[i+1]) - } - return s -} - -func (s *sitesBuilder) absFilename(filename string) string { - filename = filepath.FromSlash(filename) - if filepath.IsAbs(filename) { - return filename - } - if s.workingDir != "" && !strings.HasPrefix(filename, s.workingDir) { - filename = filepath.Join(s.workingDir, filename) - } - return filename -} - -const commonConfigSections = ` - -[services] -[services.disqus] -shortname = "disqus_shortname" -[services.googleAnalytics] -id = "UA-ga_id" - -[privacy] -[privacy.disqus] -disable = false -[privacy.googleAnalytics] -respectDoNotTrack = true -[privacy.instagram] -simple = true -[privacy.x] -enableDNT = true -[privacy.vimeo] -disable = false -[privacy.youtube] -disable = false -privacyEnhanced = true - -` - -func (s *sitesBuilder) WithSimpleConfigFile() *sitesBuilder { - s.T.Helper() - return s.WithSimpleConfigFileAndBaseURL("http://example.com/") -} - -func (s *sitesBuilder) WithSimpleConfigFileAndBaseURL(baseURL string) *sitesBuilder { - s.T.Helper() - return s.WithSimpleConfigFileAndSettings(map[string]any{"baseURL": baseURL}) -} - -func (s *sitesBuilder) WithSimpleConfigFileAndSettings(settings any) *sitesBuilder { - s.T.Helper() - var buf bytes.Buffer - parser.InterfaceToConfig(settings, metadecoders.TOML, &buf) - config := buf.String() + commonConfigSections - return s.WithConfigFile("toml", config) -} - -func (s *sitesBuilder) WithDefaultMultiSiteConfig() *sitesBuilder { - defaultMultiSiteConfig := ` -baseURL = "http://example.com/blog" - -disablePathToLower = true -defaultContentLanguage = "en" -defaultContentLanguageInSubdir = true - -[pagination] -pagerSize = 1 - -[permalinks] -other = "/somewhere/else/:filename" - -[Taxonomies] -tag = "tags" - -[Languages] -[Languages.en] -weight = 10 -title = "In English" -languageName = "English" -[[Languages.en.menu.main]] -url = "/" -name = "Home" -weight = 0 - -[Languages.fr] -weight = 20 -title = "Le Français" -languageName = "Français" -[Languages.fr.Taxonomies] -plaque = "plaques" - -[Languages.nn] -weight = 30 -title = "På nynorsk" -languageName = "Nynorsk" -[Languages.nn.pagination] -path = "side" -[Languages.nn.Taxonomies] -lag = "lag" -[[Languages.nn.menu.main]] -url = "/" -name = "Heim" -weight = 1 - -[Languages.nb] -weight = 40 -title = "På bokmål" -languageName = "Bokmål" -[Languages.nb.pagination] -path = "side" -[Languages.nb.Taxonomies] -lag = "lag" -` + commonConfigSections - - return s.WithConfigFile("toml", defaultMultiSiteConfig) -} - -func (s *sitesBuilder) WithSunset(in string) { - // Write a real image into one of the bundle above. - src, err := os.Open(filepath.FromSlash("testdata/sunset.jpg")) - s.Assert(err, qt.IsNil) - - out, err := s.Fs.Source.Create(filepath.FromSlash(filepath.Join(s.workingDir, in))) - s.Assert(err, qt.IsNil) - - _, err = io.Copy(out, src) - s.Assert(err, qt.IsNil) - - out.Close() - src.Close() -} - -func (s *sitesBuilder) createFilenameContent(pairs []string) []filenameContent { - var slice []filenameContent - s.appendFilenameContent(&slice, pairs...) - return slice -} - -func (s *sitesBuilder) appendFilenameContent(slice *[]filenameContent, pairs ...string) { - if len(pairs)%2 != 0 { - panic("file content mismatch") - } - for i := 0; i < len(pairs); i += 2 { - c := filenameContent{ - filename: pairs[i], - content: pairs[i+1], - } - *slice = append(*slice, c) - } -} - -func (s *sitesBuilder) WithContent(filenameContent ...string) *sitesBuilder { - s.appendFilenameContent(&s.contentFilePairs, filenameContent...) - return s -} - -func (s *sitesBuilder) WithContentAdded(filenameContent ...string) *sitesBuilder { - s.appendFilenameContent(&s.contentFilePairsAdded, filenameContent...) - return s -} - -func (s *sitesBuilder) WithTemplates(filenameContent ...string) *sitesBuilder { - s.appendFilenameContent(&s.templateFilePairs, filenameContent...) - return s -} - -func (s *sitesBuilder) WithTemplatesAdded(filenameContent ...string) *sitesBuilder { - s.appendFilenameContent(&s.templateFilePairsAdded, filenameContent...) - return s -} - -func (s *sitesBuilder) WithData(filenameContent ...string) *sitesBuilder { - s.appendFilenameContent(&s.dataFilePairs, filenameContent...) - return s -} - -func (s *sitesBuilder) WithDataAdded(filenameContent ...string) *sitesBuilder { - s.appendFilenameContent(&s.dataFilePairsAdded, filenameContent...) - return s -} - -func (s *sitesBuilder) WithI18n(filenameContent ...string) *sitesBuilder { - s.appendFilenameContent(&s.i18nFilePairs, filenameContent...) - return s -} - -func (s *sitesBuilder) WithI18nAdded(filenameContent ...string) *sitesBuilder { - s.appendFilenameContent(&s.i18nFilePairsAdded, filenameContent...) - return s -} - -func (s *sitesBuilder) EditFiles(filenameContent ...string) *sitesBuilder { - for i := 0; i < len(filenameContent); i += 2 { - filename, content := filepath.FromSlash(filenameContent[i]), filenameContent[i+1] - absFilename := s.absFilename(filename) - s.changedFiles = append(s.changedFiles, absFilename) - writeSource(s.T, s.Fs, absFilename, content) - - } - return s -} - -func (s *sitesBuilder) RemoveFiles(filenames ...string) *sitesBuilder { - for _, filename := range filenames { - absFilename := s.absFilename(filename) - s.removedFiles = append(s.removedFiles, absFilename) - s.Assert(s.Fs.Source.Remove(absFilename), qt.IsNil) - } - return s -} - -func (s *sitesBuilder) writeFilePairs(folder string, files []filenameContent) *sitesBuilder { - // We have had some "filesystem ordering" bugs that we have not discovered in - // our tests running with the in memory filesystem. - // That file system is backed by a map so not sure how this helps, but some - // randomness in tests doesn't hurt. - // TODO(bep) this turns out to be more confusing than helpful. - // s.rnd.Shuffle(len(files), func(i, j int) { files[i], files[j] = files[j], files[i] }) - - for _, fc := range files { - target := folder - // TODO(bep) clean up this magic. - if strings.HasPrefix(fc.filename, folder) { - target = "" - } - - if s.workingDir != "" { - target = filepath.Join(s.workingDir, target) - } - - writeSource(s.T, s.Fs, filepath.Join(target, fc.filename), fc.content) - } - return s -} - -func (s *sitesBuilder) CreateSites() *sitesBuilder { - if err := s.CreateSitesE(); err != nil { - s.Fatalf("Failed to create sites: %s", err) - } - - s.Assert(s.Fs.PublishDir, qt.IsNotNil) - s.Assert(s.Fs.WorkingDirReadOnly, qt.IsNotNil) - - return s -} - -func (s *sitesBuilder) LoadConfig() error { - if !s.configFileSet { - s.WithSimpleConfigFile() - } - - flags := config.New() - flags.Set("internal", map[string]any{ - "running": s.running, - "watch": s.running, - }) - - if s.workingDir != "" { - flags.Set("workingDir", s.workingDir) - } - - res, err := allconfig.LoadConfig(allconfig.ConfigSourceDescriptor{ - Fs: s.Fs.Source, - Logger: s.logger, - Flags: flags, - Environ: s.environ, - Filename: "config." + s.configFormat, - }) - if err != nil { - return err - } - - s.Cfg = res.LoadingInfo.Cfg - s.Configs = res - - return nil -} - -func (s *sitesBuilder) CreateSitesE() error { - if !s.addNothing { - if _, ok := s.Fs.Source.(*afero.OsFs); ok { - for _, dir := range []string{ - "content/sect", - "layouts/_default", - "layouts/_default/_markup", - "layouts/partials", - "layouts/shortcodes", - "data", - "i18n", - } { - if err := os.MkdirAll(filepath.Join(s.workingDir, dir), 0o777); err != nil { - return fmt.Errorf("failed to create %q: %w", dir, err) - } - } - } - - s.addDefaults() - s.writeFilePairs("content", s.contentFilePairsAdded) - s.writeFilePairs("layouts", s.templateFilePairsAdded) - s.writeFilePairs("data", s.dataFilePairsAdded) - s.writeFilePairs("i18n", s.i18nFilePairsAdded) - - s.writeFilePairs("i18n", s.i18nFilePairs) - s.writeFilePairs("data", s.dataFilePairs) - s.writeFilePairs("content", s.contentFilePairs) - s.writeFilePairs("layouts", s.templateFilePairs) - - } - - if err := s.LoadConfig(); err != nil { - return fmt.Errorf("failed to load config: %w", err) - } - - s.Fs.PublishDir = hugofs.NewCreateCountingFs(s.Fs.PublishDir) - - depsCfg := s.depsCfg - depsCfg.Fs = s.Fs - if depsCfg.Configs.IsZero() { - depsCfg.Configs = s.Configs - } - depsCfg.TestLogger = s.logger - - sites, err := NewHugoSites(depsCfg) - if err != nil { - return fmt.Errorf("failed to create sites: %w", err) - } - s.H = sites - - return nil -} - -func (s *sitesBuilder) BuildE(cfg BuildCfg) error { - if s.H == nil { - s.CreateSites() - } - - return s.H.Build(cfg) -} - -func (s *sitesBuilder) Build(cfg BuildCfg) *sitesBuilder { - s.T.Helper() - return s.build(cfg, false) -} - -func (s *sitesBuilder) BuildFail(cfg BuildCfg) *sitesBuilder { - s.T.Helper() - return s.build(cfg, true) -} - -func (s *sitesBuilder) changeEvents() []fsnotify.Event { - var events []fsnotify.Event - - for _, v := range s.changedFiles { - events = append(events, fsnotify.Event{ - Name: v, - Op: fsnotify.Write, - }) - } - for _, v := range s.removedFiles { - events = append(events, fsnotify.Event{ - Name: v, - Op: fsnotify.Remove, - }) - } - - return events -} - -func (s *sitesBuilder) build(cfg BuildCfg, shouldFail bool) *sitesBuilder { - s.Helper() - defer func() { - s.changedFiles = nil - }() - - if s.H == nil { - s.CreateSites() - } - - err := s.H.Build(cfg, s.changeEvents()...) - - if err == nil { - logErrorCount := s.H.NumLogErrors() - if logErrorCount > 0 { - err = fmt.Errorf("logged %d errors", logErrorCount) - } - } - if err != nil && !shouldFail { - s.Fatalf("Build failed: %s", err) - } else if err == nil && shouldFail { - s.Fatalf("Expected error") - } - - return s -} - -func (s *sitesBuilder) addDefaults() { - var ( - contentTemplate = `--- -title: doc1 -weight: 1 -tags: - - tag1 -date: "2018-02-28" ---- -# doc1 -*some "content"* -{{< shortcode >}} -{{< lingo >}} -` - - defaultContent = []string{ - "content/sect/doc1.en.md", contentTemplate, - "content/sect/doc1.fr.md", contentTemplate, - "content/sect/doc1.nb.md", contentTemplate, - "content/sect/doc1.nn.md", contentTemplate, - } - - listTemplateCommon = "{{ $p := .Paginator }}{{ $p.PageNumber }}|{{ .Title }}|{{ i18n \"hello\" }}|{{ .Permalink }}|Pager: {{ template \"_internal/pagination.html\" . }}|Kind: {{ .Kind }}|Content: {{ .Content }}|Len Pages: {{ len .Pages }}|Len RegularPages: {{ len .RegularPages }}| HasParent: {{ if .Parent }}YES{{ else }}NO{{ end }}" - - defaultTemplates = []string{ - "_default/single.html", "Single: {{ .Title }}|{{ i18n \"hello\" }}|{{.Language.Lang}}|RelPermalink: {{ .RelPermalink }}|Permalink: {{ .Permalink }}|{{ .Content }}|Resources: {{ range .Resources }}{{ .MediaType }}: {{ .RelPermalink}} -- {{ end }}|Summary: {{ .Summary }}|Truncated: {{ .Truncated }}|Parent: {{ .Parent.Title }}", - "_default/list.html", "List Page " + listTemplateCommon, - "index.html", "{{ $p := .Paginator }}Default Home Page {{ $p.PageNumber }}: {{ .Title }}|{{ .IsHome }}|{{ i18n \"hello\" }}|{{ .Permalink }}|{{ .Site.Data.hugo.slogan }}|String Resource: {{ ( \"Hugo Pipes\" | resources.FromString \"text/pipes.txt\").RelPermalink }}|String Resource Permalink: {{ ( \"Hugo Pipes\" | resources.FromString \"text/pipes.txt\").Permalink }}", - "index.fr.html", "{{ $p := .Paginator }}French Home Page {{ $p.PageNumber }}: {{ .Title }}|{{ .IsHome }}|{{ i18n \"hello\" }}|{{ .Permalink }}|{{ .Site.Data.hugo.slogan }}|String Resource: {{ ( \"Hugo Pipes\" | resources.FromString \"text/pipes.txt\").RelPermalink }}|String Resource Permalink: {{ ( \"Hugo Pipes\" | resources.FromString \"text/pipes.txt\").Permalink }}", - "_default/terms.html", "Taxonomy Term Page " + listTemplateCommon, - "_default/taxonomy.html", "Taxonomy List Page " + listTemplateCommon, - // Shortcodes - "shortcodes/shortcode.html", "Shortcode: {{ i18n \"hello\" }}", - // A shortcode in multiple languages - "shortcodes/lingo.html", "LingoDefault", - "shortcodes/lingo.fr.html", "LingoFrench", - // Special templates - "404.html", "404|{{ .Lang }}|{{ .Title }}", - "robots.txt", "robots|{{ .Lang }}|{{ .Title }}", - } - - defaultI18n = []string{ - "en.yaml", ` -hello: - other: "Hello" -`, - "fr.yaml", ` -hello: - other: "Bonjour" -`, - } - - defaultData = []string{ - "hugo.toml", "slogan = \"Hugo Rocks!\"", - } - ) - - if len(s.contentFilePairs) == 0 { - s.writeFilePairs("content", s.createFilenameContent(defaultContent)) - } - - if len(s.templateFilePairs) == 0 { - s.writeFilePairs("layouts", s.createFilenameContent(defaultTemplates)) - } - if len(s.dataFilePairs) == 0 { - s.writeFilePairs("data", s.createFilenameContent(defaultData)) - } - if len(s.i18nFilePairs) == 0 { - s.writeFilePairs("i18n", s.createFilenameContent(defaultI18n)) - } -} - -func (s *sitesBuilder) Fatalf(format string, args ...any) { - s.T.Helper() - s.T.Fatalf(format, args...) -} - -func (s *sitesBuilder) AssertFileContentFn(filename string, f func(s string) bool) { - s.T.Helper() - content := s.FileContent(filename) - if !f(content) { - s.Fatalf("Assert failed for %q in content\n%s", filename, content) - } -} - -// Helper to migrate tests to new format. -func (s *sitesBuilder) DumpTxtar() string { - var sb strings.Builder - - skipRe := regexp.MustCompile(`^(public|resources|package-lock.json|go.sum)`) - - afero.Walk(s.Fs.Source, s.workingDir, func(path string, info fs.FileInfo, err error) error { - if err != nil { - return err - } - rel := strings.TrimPrefix(path, s.workingDir+"/") - if skipRe.MatchString(rel) { - if info.IsDir() { - return filepath.SkipDir - } - return nil - } - if info == nil || info.IsDir() { - return nil - } - sb.WriteString(fmt.Sprintf("-- %s --\n", rel)) - b, err := afero.ReadFile(s.Fs.Source, path) - s.Assert(err, qt.IsNil) - sb.WriteString(strings.TrimSpace(string(b))) - sb.WriteString("\n") - return nil - }) - - return sb.String() -} - -func (s *sitesBuilder) AssertHome(matches ...string) { - s.AssertFileContent("public/index.html", matches...) -} - -func (s *sitesBuilder) AssertFileContent(filename string, matches ...string) { - s.T.Helper() - content := s.FileContent(filename) - for _, m := range matches { - lines := strings.Split(m, "\n") - for _, match := range lines { - match = strings.TrimSpace(match) - if match == "" { - continue - } - if !strings.Contains(content, match) { - s.Assert(content, qt.Contains, match, qt.Commentf(match+" not in: \n"+content)) - } - } - } -} - -func (s *sitesBuilder) AssertFileDoesNotExist(filename string) { - if s.CheckExists(filename) { - s.Fatalf("File %q exists but must not exist.", filename) - } -} - -func (s *sitesBuilder) AssertImage(width, height int, filename string) { - f, err := s.Fs.WorkingDirReadOnly.Open(filename) - s.Assert(err, qt.IsNil) - defer f.Close() - cfg, err := jpeg.DecodeConfig(f) - s.Assert(err, qt.IsNil) - s.Assert(cfg.Width, qt.Equals, width) - s.Assert(cfg.Height, qt.Equals, height) -} - -func (s *sitesBuilder) AssertNoDuplicateWrites() { - s.Helper() - hugofs.WalkFilesystems(s.Fs.PublishDir, func(fs afero.Fs) bool { - if dfs, ok := fs.(hugofs.DuplicatesReporter); ok { - s.Assert(dfs.ReportDuplicates(), qt.Equals, "") - } - return false - }) -} - -func (s *sitesBuilder) FileContent(filename string) string { - s.Helper() - filename = filepath.FromSlash(filename) - return readWorkingDir(s.T, s.Fs, filename) -} - -func (s *sitesBuilder) AssertObject(expected string, object any) { - s.T.Helper() - got := s.dumper.Sdump(object) - expected = strings.TrimSpace(expected) - - if expected != got { - fmt.Println(got) - diff := htesting.DiffStrings(expected, got) - s.Fatalf("diff:\n%s\nexpected\n%s\ngot\n%s", diff, expected, got) - } -} - -func (s *sitesBuilder) AssertFileContentRe(filename string, matches ...string) { - content := readWorkingDir(s.T, s.Fs, filename) - for _, match := range matches { - r := regexp.MustCompile("(?s)" + match) - if !r.MatchString(content) { - s.Fatalf("No match for %q in content for %s\n%q", match, filename, content) - } - } -} - -func (s *sitesBuilder) CheckExists(filename string) bool { - return workingDirExists(s.Fs, filepath.Clean(filename)) -} - -func (s *sitesBuilder) GetPage(ref string) page.Page { - p, err := s.H.Sites[0].getPage(nil, ref) - s.Assert(err, qt.IsNil) - return p -} - -func (s *sitesBuilder) GetPageRel(p page.Page, ref string) page.Page { - p, err := s.H.Sites[0].getPage(p, ref) - s.Assert(err, qt.IsNil) - return p -} - -func (s *sitesBuilder) NpmInstall() hexec.Runner { - sc := security.DefaultConfig - var err error - sc.Exec.Allow, err = security.NewWhitelist("npm") - s.Assert(err, qt.IsNil) - ex := hexec.New(sc, s.workingDir, loggers.NewDefault()) - command, err := ex.New("npm", "install") - s.Assert(err, qt.IsNil) - return command -} - -func newTestHelperFromProvider(cfg config.Provider, fs *hugofs.Fs, t testing.TB) (testHelper, *allconfig.Configs) { - res, err := allconfig.LoadConfig(allconfig.ConfigSourceDescriptor{ - Flags: cfg, - Fs: fs.Source, - }) - if err != nil { - t.Fatal(err) - } - return newTestHelper(res.Base, fs, t), res -} - -func newTestHelper(cfg *allconfig.Config, fs *hugofs.Fs, t testing.TB) testHelper { - return testHelper{ - Cfg: cfg, - Fs: fs, - C: qt.New(t), - } -} - type testHelper struct { - Cfg *allconfig.Config + Cfg config.Provider Fs *hugofs.Fs - *qt.C + T testing.TB } func (th testHelper) assertFileContent(filename string, matches ...string) { - th.Helper() filename = th.replaceDefaultContentLanguageValue(filename) - content := readWorkingDir(th, th.Fs, filename) + content := readDestination(th.T, th.Fs, filename) for _, match := range matches { match = th.replaceDefaultContentLanguageValue(match) - th.Assert(strings.Contains(content, match), qt.Equals, true, qt.Commentf(match+" not in: \n"+content)) + require.True(th.T, strings.Contains(content, match), fmt.Sprintf("File no match for\n%q in\n%q:\n%s", strings.Replace(match, "%", "%%", -1), filename, strings.Replace(content, "%", "%%", -1))) + } +} + +// TODO(bep) better name for this. It does no magic replacements depending on defaultontentLanguageInSubDir. +func (th testHelper) assertFileContentStraight(filename string, matches ...string) { + content := readDestination(th.T, th.Fs, filename) + for _, match := range matches { + require.True(th.T, strings.Contains(content, match), fmt.Sprintf("File no match for\n%q in\n%q:\n%s", strings.Replace(match, "%", "%%", -1), filename, strings.Replace(content, "%", "%%", -1))) + } +} + +func (th testHelper) assertFileContentRegexp(filename string, matches ...string) { + filename = th.replaceDefaultContentLanguageValue(filename) + content := readDestination(th.T, th.Fs, filename) + for _, match := range matches { + match = th.replaceDefaultContentLanguageValue(match) + r := regexp.MustCompile(match) + require.True(th.T, r.MatchString(content), fmt.Sprintf("File no match for\n%q in\n%q:\n%s", strings.Replace(match, "%", "%%", -1), filename, strings.Replace(content, "%", "%%", -1))) } } func (th testHelper) assertFileNotExist(filename string) { - exists, err := helpers.Exists(filename, th.Fs.PublishDir) - th.Assert(err, qt.IsNil) - th.Assert(exists, qt.Equals, false) + exists, err := helpers.Exists(filename, th.Fs.Destination) + require.NoError(th.T, err) + require.False(th.T, exists) } func (th testHelper) replaceDefaultContentLanguageValue(value string) string { - defaultInSubDir := th.Cfg.DefaultContentLanguageInSubdir - replace := th.Cfg.DefaultContentLanguage + "/" + defaultInSubDir := th.Cfg.GetBool("defaultContentLanguageInSubDir") + replace := th.Cfg.GetString("defaultContentLanguage") + "/" if !defaultInSubDir { value = strings.Replace(value, replace, "", 1) + } return value } -func loadTestConfigFromProvider(cfg config.Provider) (*allconfig.Configs, error) { - workingDir := cfg.GetString("workingDir") - fs := afero.NewMemMapFs() - if workingDir != "" { - fs.MkdirAll(workingDir, 0o755) - } - res, err := allconfig.LoadConfig(allconfig.ConfigSourceDescriptor{Flags: cfg, Fs: fs}) - return res, err +func newTestPathSpec(fs *hugofs.Fs, v *viper.Viper) *helpers.PathSpec { + l := helpers.NewDefaultLanguage(v) + ps, _ := helpers.NewPathSpec(fs, l) + return ps } -func newTestCfg(withConfig ...func(cfg config.Provider) error) (config.Provider, *hugofs.Fs) { - mm := afero.NewMemMapFs() - cfg := config.New() - cfg.Set("defaultContentLanguageInSubdir", false) - cfg.Set("publishDir", "public") +func newTestDefaultPathSpec() *helpers.PathSpec { + v := viper.New() + // Easier to reason about in tests. + v.Set("disablePathToLower", true) + fs := hugofs.NewDefault(v) + ps, _ := helpers.NewPathSpec(fs, v) + return ps +} - fs := hugofs.NewFromOld(hugofs.NewBaseFileDecorator(mm), cfg) +func newTestCfg() (*viper.Viper, *hugofs.Fs) { - return cfg, fs + v := viper.New() + fs := hugofs.NewMem(v) + + v.SetFs(fs.Source) + + loadDefaultSettingsFor(v) + + // Default is false, but true is easier to use as default in tests + v.Set("defaultContentLanguageInSubdir", true) + + return v, fs + +} + +// newTestSite creates a new site in the English language with in-memory Fs. +// The site will have a template system loaded and ready to use. +// Note: This is only used in single site tests. +func newTestSite(t testing.TB, configKeyValues ...interface{}) *Site { + + cfg, fs := newTestCfg() + + for i := 0; i < len(configKeyValues); i += 2 { + cfg.Set(configKeyValues[i].(string), configKeyValues[i+1]) + } + + d := deps.DepsCfg{Language: helpers.NewLanguage("en", cfg), Fs: fs, Cfg: cfg} + + s, err := NewSiteForCfg(d) + + if err != nil { + t.Fatalf("Failed to create Site: %s", err) + } + return s } func newTestSitesFromConfig(t testing.TB, afs afero.Fs, tomlConfig string, layoutPathContentPairs ...string) (testHelper, *HugoSites) { @@ -921,85 +135,80 @@ func newTestSitesFromConfig(t testing.TB, afs afero.Fs, tomlConfig string, layou t.Fatalf("Layouts must be provided in pairs") } - c := qt.New(t) - - writeToFs(t, afs, filepath.Join("content", ".gitkeep"), "") writeToFs(t, afs, "config.toml", tomlConfig) - cfg, err := allconfig.LoadConfig(allconfig.ConfigSourceDescriptor{Fs: afs}) - c.Assert(err, qt.IsNil) + cfg, err := LoadConfig(afs, "", "config.toml") + require.NoError(t, err) - fs := hugofs.NewFrom(afs, cfg.LoadingInfo.BaseConfig) - th := newTestHelper(cfg.Base, fs, t) + fs := hugofs.NewFrom(afs, cfg) + th := testHelper{cfg, fs, t} for i := 0; i < len(layoutPathContentPairs); i += 2 { writeSource(t, fs, layoutPathContentPairs[i], layoutPathContentPairs[i+1]) } - h, err := NewHugoSites(deps.DepsCfg{Fs: fs, Configs: cfg}) + h, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg}) - c.Assert(err, qt.IsNil) + require.NoError(t, err) return th, h } -// TODO(bep) replace these with the builder -func buildSingleSite(t testing.TB, depsCfg deps.DepsCfg, buildCfg BuildCfg) *Site { - t.Helper() - return buildSingleSiteExpected(t, false, false, depsCfg, buildCfg) +func newTestSitesFromConfigWithDefaultTemplates(t testing.TB, tomlConfig string) (testHelper, *HugoSites) { + return newTestSitesFromConfig(t, afero.NewMemMapFs(), tomlConfig, + "layouts/_default/single.html", "Single|{{ .Title }}|{{ .Content }}", + "layouts/_default/list.html", "List|{{ .Title }}|{{ .Content }}", + "layouts/_default/terms.html", "Terms List|{{ .Title }}|{{ .Content }}", + ) } -func buildSingleSiteExpected(t testing.TB, expectSiteInitError, expectBuildError bool, depsCfg deps.DepsCfg, buildCfg BuildCfg) *Site { - t.Helper() - b := newTestSitesBuilderFromDepsCfg(t, depsCfg).WithNothingAdded() +func newDebugLogger() *jww.Notepad { + return jww.NewNotepad(jww.LevelDebug, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime) +} - err := b.CreateSitesE() +func newErrorLogger() *jww.Notepad { + return jww.NewNotepad(jww.LevelError, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime) +} +func createWithTemplateFromNameValues(additionalTemplates ...string) func(templ tpl.TemplateHandler) error { - if expectSiteInitError { - b.Assert(err, qt.Not(qt.IsNil)) + return func(templ tpl.TemplateHandler) error { + for i := 0; i < len(additionalTemplates); i += 2 { + err := templ.AddTemplate(additionalTemplates[i], additionalTemplates[i+1]) + if err != nil { + return err + } + } return nil - } else { - b.Assert(err, qt.IsNil) } +} - h := b.H +func buildSingleSite(t testing.TB, depsCfg deps.DepsCfg, buildCfg BuildCfg) *Site { + return buildSingleSiteExpected(t, false, depsCfg, buildCfg) +} - b.Assert(len(h.Sites), qt.Equals, 1) +func buildSingleSiteExpected(t testing.TB, expectBuildError bool, depsCfg deps.DepsCfg, buildCfg BuildCfg) *Site { + h, err := NewHugoSites(depsCfg) + + require.NoError(t, err) + require.Len(t, h.Sites, 1) if expectBuildError { - b.Assert(h.Build(buildCfg), qt.Not(qt.IsNil)) + require.Error(t, h.Build(buildCfg)) return nil } - b.Assert(h.Build(buildCfg), qt.IsNil) + require.NoError(t, h.Build(buildCfg)) return h.Sites[0] } -func writeSourcesToSource(t *testing.T, base string, fs *hugofs.Fs, sources ...[2]string) { +func writeSourcesToSource(t *testing.T, base string, fs *hugofs.Fs, sources ...source.ByteSource) { for _, src := range sources { - writeSource(t, fs, filepath.Join(base, src[0]), src[1]) + writeSource(t, fs, filepath.Join(base, src.Name), string(src.Content)) } } -func getPage(in page.Page, ref string) page.Page { - p, err := in.GetPage(ref) - if err != nil { - panic(err) - } - return p -} - -func content(c resource.ContentProvider) string { - cc, err := c.Content(context.Background()) - if err != nil { - panic(err) - } - - ccs, err := cast.ToStringE(cc) - if err != nil { - panic(err) - } - return ccs +func isCI() bool { + return os.Getenv("CI") != "" } diff --git a/hugolib/testsite/.gitignore b/hugolib/testsite/.gitignore deleted file mode 100644 index ab8b69cbc..000000000 --- a/hugolib/testsite/.gitignore +++ /dev/null @@ -1 +0,0 @@ -config.toml \ No newline at end of file diff --git a/hugolib/testsite/CODEOWNERS b/hugolib/testsite/CODEOWNERS deleted file mode 100644 index 41f196327..000000000 --- a/hugolib/testsite/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @bep \ No newline at end of file diff --git a/hugolib/testsite/content/first-post.md b/hugolib/testsite/content/first-post.md deleted file mode 100644 index 4a8007946..000000000 --- a/hugolib/testsite/content/first-post.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "My First Post" -lastmod: 2018-02-28 ---- \ No newline at end of file diff --git a/hugolib/testsite/content_nn/first-post.md b/hugolib/testsite/content_nn/first-post.md deleted file mode 100644 index 1c3b4e831..000000000 --- a/hugolib/testsite/content_nn/first-post.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "Min første dag" -lastmod: 1972-02-28 ---- \ No newline at end of file diff --git a/hugolib/translations.go b/hugolib/translations.go new file mode 100644 index 000000000..53272ee14 --- /dev/null +++ b/hugolib/translations.go @@ -0,0 +1,75 @@ +// Copyright 2016 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 hugolib + +import ( + "fmt" +) + +// Translations represent the other translations for a given page. The +// string here is the language code, as affected by the `post.LANG.md` +// filename. +type Translations map[string]*Page + +func pagesToTranslationsMap(pages []*Page) map[string]Translations { + out := make(map[string]Translations) + + for _, page := range pages { + base := createTranslationKey(page) + + pageTranslation, present := out[base] + if !present { + pageTranslation = make(Translations) + } + + pageLang := page.Lang() + if pageLang == "" { + continue + } + + pageTranslation[pageLang] = page + out[base] = pageTranslation + } + + return out +} + +func createTranslationKey(p *Page) string { + base := p.TranslationBaseName() + + if p.IsNode() { + // TODO(bep) see https://github.com/gohugoio/hugo/issues/2699 + // Must prepend the section and kind to the key to make it unique + base = fmt.Sprintf("%s/%s/%s", p.Kind, p.sections, base) + } + + return base +} + +func assignTranslationsToPages(allTranslations map[string]Translations, pages []*Page) { + for _, page := range pages { + page.translations = page.translations[:0] + base := createTranslationKey(page) + trans, exist := allTranslations[base] + if !exist { + continue + } + + for _, translatedPage := range trans { + page.translations = append(page.translations, translatedPage) + } + + pageBy(languagePageSort).Sort(page.translations) + } +} diff --git a/hugoreleaser.env b/hugoreleaser.env deleted file mode 100644 index 6da749524..000000000 --- a/hugoreleaser.env +++ /dev/null @@ -1,123 +0,0 @@ -# Release env. -# These will be replaced by script before release. -HUGORELEASER_TAG=v0.147.9 -HUGORELEASER_COMMITISH=29bdbde19c288d190e889294a862103c6efb70bf - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/hugoreleaser.yaml b/hugoreleaser.yaml deleted file mode 100644 index 368bc898f..000000000 --- a/hugoreleaser.yaml +++ /dev/null @@ -1,272 +0,0 @@ -project: hugo - -# Common definitions. -definitions: - archive_type_zip: &archive_type_zip - type: - format: zip - extension: .zip - env_extended_linux: &env_extended_linux - - CGO_ENABLED=1 - - CC=aarch64-linux-gnu-gcc - - CXX=aarch64-linux-gnu-g++ - env_extended_windows: &env_extended_windows - - CGO_ENABLED=1 - - CC=x86_64-w64-mingw32-gcc - - CXX=x86_64-w64-mingw32-g++ - env_extended_darwin: &env_extended_darwin - - CGO_ENABLED=1 - - CC=o64-clang - - CXX=o64-clang++ - name_template_extended_withdeploy: &name_template_extended_withdeploy "{{ .Project }}_extended_withdeploy_{{ .Tag | trimPrefix `v` }}_{{ .Goos }}-{{ .Goarch }}" - name_template_extended: &name_template_extended "{{ .Project }}_extended_{{ .Tag | trimPrefix `v` }}_{{ .Goos }}-{{ .Goarch }}" - archive_deb: &archive_deb - binary_dir: /usr/local/bin - extra_files: [] - type: - format: _plugin - extension: .deb - plugin: - id: deb - type: gorun - command: github.com/gohugoio/hugoreleaser-archive-plugins/deb@latest - custom_settings: - vendor: gohugo.io - homepage: https://github.com/gohugoio/hugo - maintainer: Bjørn Erik Pedersen - description: A fast and flexible Static Site Generator written in Go. - license: Apache-2.0 -archive_alias_replacements: - linux-amd64.tar.gz: Linux-64bit.tar.gz -go_settings: - go_proxy: https://proxy.golang.org - go_exe: go -build_settings: - binary: hugo - flags: - - -buildmode - - exe - env: - - CGO_ENABLED=0 - ldflags: -s -w -X github.com/gohugoio/hugo/common/hugo.vendorInfo=gohugoio -archive_settings: - name_template: "{{ .Project }}_{{ .Tag | trimPrefix `v` }}_{{ .Goos }}-{{ .Goarch }}" - extra_files: - - source_path: README.md - target_path: README.md - - source_path: LICENSE - target_path: LICENSE - type: - format: tar.gz - extension: .tar.gz -release_settings: - name: ${HUGORELEASER_TAG} - type: github - repository: hugo - repository_owner: gohugoio - draft: true - prerelease: false - release_notes_settings: - generate: true - short_threshold: 10 - short_title: What's Changed - groups: - - regexp: "Merge commit|Squashed|releaser:" - ignore: true - - title: Note - regexp: (note|deprecated) - ordinal: 10 - - title: Bug fixes - regexp: fix - ordinal: 15 - - title: Dependency Updates - regexp: deps - ordinal: 30 - - title: Build Setup - regexp: (snap|release|update to) - ordinal: 40 - - title: Documentation - regexp: (doc|readme) - ordinal: 40 - - title: Improvements - regexp: .* - ordinal: 20 -builds: - - path: container1/unix/regular - os: - - goos: darwin - archs: - - goarch: universal - - goos: linux - archs: - - goarch: amd64 - - goarch: arm64 - - goarch: arm - build_settings: - env: - - CGO_ENABLED=0 - - GOARM=7 - - goos: dragonfly - archs: - - goarch: amd64 - - goos: freebsd - archs: - - goarch: amd64 - - goos: netbsd - archs: - - goarch: amd64 - - goos: openbsd - archs: - - goarch: amd64 - - goos: solaris - archs: - - goarch: amd64 - - path: container1/unix/extended - build_settings: - flags: - - -buildmode - - exe - - -tags - - extended - env: - - CGO_ENABLED=1 - os: - - goos: darwin - build_settings: - env: *env_extended_darwin - archs: - - goarch: universal - - goos: linux - archs: - - goarch: amd64 - - path: container1/unix/extended-withdeploy - build_settings: - flags: - - -buildmode - - exe - - -tags - - extended,withdeploy - env: - - CGO_ENABLED=1 - os: - - goos: darwin - build_settings: - env: *env_extended_darwin - archs: - - goarch: universal - - goos: linux - archs: - - goarch: amd64 - - path: container2/linux/extended - build_settings: - flags: - - -buildmode - - exe - - -tags - - extended - os: - - goos: linux - build_settings: - env: *env_extended_linux - archs: - - goarch: arm64 - - path: container2/linux/extended-withdeploy - build_settings: - flags: - - -buildmode - - exe - - -tags - - extended,withdeploy - os: - - goos: linux - build_settings: - env: *env_extended_linux - archs: - - goarch: arm64 - - path: container1/windows/regular - os: - - goos: windows - build_settings: - binary: hugo.exe - archs: - - goarch: amd64 - - goarch: arm64 - - path: container1/windows/extended - build_settings: - flags: - - -buildmode - - exe - - -tags - - extended - env: *env_extended_windows - ldflags: -s -w -X github.com/gohugoio/hugo/common/hugo.vendorInfo=gohugoio -extldflags '-static' - os: - - goos: windows - build_settings: - binary: hugo.exe - archs: - - goarch: amd64 - - path: container1/windows/extended-withdeploy - build_settings: - flags: - - -buildmode - - exe - - -tags - - extended,withdeploy - env: *env_extended_windows - ldflags: -s -w -X github.com/gohugoio/hugo/common/hugo.vendorInfo=gohugoio -extldflags '-static' - os: - - goos: windows - build_settings: - binary: hugo.exe - archs: - - goarch: amd64 -archives: - - paths: - - builds/container1/unix/regular/** - - paths: - - builds/container1/unix/extended/** - archive_settings: - name_template: *name_template_extended - - paths: - - builds/container1/unix/extended-withdeploy/** - archive_settings: - name_template: *name_template_extended_withdeploy - - paths: - - builds/container2/*/extended/** - archive_settings: - name_template: *name_template_extended - - paths: - - builds/container2/*/extended-withdeploy/** - archive_settings: - name_template: *name_template_extended_withdeploy - - paths: - - builds/**/windows/regular/** - archive_settings: *archive_type_zip - - paths: - - builds/**/windows/extended/** - archive_settings: - name_template: *name_template_extended - <<: *archive_type_zip - - paths: - - builds/**/windows/extended-withdeploy/** - archive_settings: - name_template: *name_template_extended_withdeploy - <<: *archive_type_zip - - paths: - - builds/**/regular/linux/{arm64,amd64} - archive_settings: *archive_deb - - paths: - - builds/**/extended/linux/{arm64,amd64} - archive_settings: - name_template: *name_template_extended - <<: *archive_deb - - paths: - - builds/**/extended-withdeploy/linux/{arm64,amd64} - archive_settings: - name_template: *name_template_extended_withdeploy - <<: *archive_deb -releases: - - paths: - - archives/** - path: r1 diff --git a/i18n/i18n.go b/i18n/i18n.go new file mode 100644 index 000000000..73417fb32 --- /dev/null +++ b/i18n/i18n.go @@ -0,0 +1,113 @@ +// Copyright 2017 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 i18n + +import ( + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/helpers" + "github.com/nicksnyder/go-i18n/i18n/bundle" + jww "github.com/spf13/jwalterweatherman" +) + +var ( + i18nWarningLogger = helpers.NewDistinctFeedbackLogger() +) + +// Translator handles i18n translations. +type Translator struct { + translateFuncs map[string]bundle.TranslateFunc + cfg config.Provider + logger *jww.Notepad +} + +// NewTranslator creates a new Translator for the given language bundle and configuration. +func NewTranslator(b *bundle.Bundle, cfg config.Provider, logger *jww.Notepad) Translator { + t := Translator{cfg: cfg, logger: logger, translateFuncs: make(map[string]bundle.TranslateFunc)} + t.initFuncs(b) + return t +} + +// Func gets the translate func for the given language, or for the default +// configured language if not found. +func (t Translator) Func(lang string) bundle.TranslateFunc { + if f, ok := t.translateFuncs[lang]; ok { + return f + } + t.logger.WARN.Printf("Translation func for language %v not found, use default.", lang) + if f, ok := t.translateFuncs[t.cfg.GetString("defaultContentLanguage")]; ok { + return f + } + t.logger.WARN.Println("i18n not initialized, check that you have language file (in i18n) that matches the site language or the default language.") + return func(translationID string, args ...interface{}) string { + return "" + } + +} + +func (t Translator) initFuncs(bndl *bundle.Bundle) { + defaultContentLanguage := t.cfg.GetString("defaultContentLanguage") + + defaultT, err := bndl.Tfunc(defaultContentLanguage) + if err != nil { + jww.WARN.Printf("No translation bundle found for default language %q", defaultContentLanguage) + } + + enableMissingTranslationPlaceholders := t.cfg.GetBool("enableMissingTranslationPlaceholders") + for _, lang := range bndl.LanguageTags() { + currentLang := lang + + t.translateFuncs[currentLang] = func(translationID string, args ...interface{}) string { + tFunc, err := bndl.Tfunc(currentLang) + if err != nil { + jww.WARN.Printf("could not load translations for language %q (%s), will use default content language.\n", lang, err) + } + + translated := tFunc(translationID, args...) + if translated != translationID { + return translated + } + // If there is no translation for translationID, + // then Tfunc returns translationID itself. + // But if user set same translationID and translation, we should check + // if it really untranslated: + if isIDTranslated(currentLang, translationID, bndl) { + return translated + } + + if t.cfg.GetBool("logI18nWarnings") { + i18nWarningLogger.Printf("i18n|MISSING_TRANSLATION|%s|%s", currentLang, translationID) + } + if enableMissingTranslationPlaceholders { + return "[i18n] " + translationID + } + if defaultT != nil { + translated := defaultT(translationID, args...) + if translated != translationID { + return translated + } + if isIDTranslated(defaultContentLanguage, translationID, bndl) { + return translated + } + } + return "" + } + } +} + +// If bndl contains the translationID for specified currentLang, +// then the translationID is actually translated. +func isIDTranslated(lang, id string, b *bundle.Bundle) bool { + _, contains := b.Translations()[lang][id] + return contains +} diff --git a/i18n/i18n_test.go b/i18n/i18n_test.go new file mode 100644 index 000000000..6a9d362b0 --- /dev/null +++ b/i18n/i18n_test.go @@ -0,0 +1,177 @@ +// Copyright 2017 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 i18n + +import ( + "testing" + + "io/ioutil" + "os" + + "log" + + "github.com/gohugoio/hugo/config" + "github.com/nicksnyder/go-i18n/i18n/bundle" + jww "github.com/spf13/jwalterweatherman" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" +) + +var logger = jww.NewNotepad(jww.LevelError, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime) + +type i18nTest struct { + data map[string][]byte + args interface{} + lang, id, expected, expectedFlag string +} + +var i18nTests = []i18nTest{ + // All translations present + { + data: map[string][]byte{ + "en.toml": []byte("[hello]\nother = \"Hello, World!\""), + "es.toml": []byte("[hello]\nother = \"¡Hola, Mundo!\""), + }, + args: nil, + lang: "es", + id: "hello", + expected: "¡Hola, Mundo!", + expectedFlag: "¡Hola, Mundo!", + }, + // Translation missing in current language but present in default + { + data: map[string][]byte{ + "en.toml": []byte("[hello]\nother = \"Hello, World!\""), + "es.toml": []byte("[goodbye]\nother = \"¡Adiós, Mundo!\""), + }, + args: nil, + lang: "es", + id: "hello", + expected: "Hello, World!", + expectedFlag: "[i18n] hello", + }, + // Translation missing in default language but present in current + { + data: map[string][]byte{ + "en.toml": []byte("[goodbye]\nother = \"Goodbye, World!\""), + "es.toml": []byte("[hello]\nother = \"¡Hola, Mundo!\""), + }, + args: nil, + lang: "es", + id: "hello", + expected: "¡Hola, Mundo!", + expectedFlag: "¡Hola, Mundo!", + }, + // Translation missing in both default and current language + { + data: map[string][]byte{ + "en.toml": []byte("[goodbye]\nother = \"Goodbye, World!\""), + "es.toml": []byte("[goodbye]\nother = \"¡Adiós, Mundo!\""), + }, + args: nil, + lang: "es", + id: "hello", + expected: "", + expectedFlag: "[i18n] hello", + }, + // Default translation file missing or empty + { + data: map[string][]byte{ + "en.toml": []byte(""), + }, + args: nil, + lang: "es", + id: "hello", + expected: "", + expectedFlag: "[i18n] hello", + }, + // Context provided + { + data: map[string][]byte{ + "en.toml": []byte("[wordCount]\nother = \"Hello, {{.WordCount}} people!\""), + "es.toml": []byte("[wordCount]\nother = \"¡Hola, {{.WordCount}} gente!\""), + }, + args: struct { + WordCount int + }{ + 50, + }, + lang: "es", + id: "wordCount", + expected: "¡Hola, 50 gente!", + expectedFlag: "¡Hola, 50 gente!", + }, + // Same id and translation in current language + // https://github.com/gohugoio/hugo/issues/2607 + { + data: map[string][]byte{ + "es.toml": []byte("[hello]\nother = \"hello\""), + "en.toml": []byte("[hello]\nother = \"hi\""), + }, + args: nil, + lang: "es", + id: "hello", + expected: "hello", + expectedFlag: "hello", + }, + // Translation missing in current language, but same id and translation in default + { + data: map[string][]byte{ + "es.toml": []byte("[bye]\nother = \"bye\""), + "en.toml": []byte("[hello]\nother = \"hello\""), + }, + args: nil, + lang: "es", + id: "hello", + expected: "hello", + expectedFlag: "[i18n] hello", + }, +} + +func doTestI18nTranslate(t *testing.T, test i18nTest, cfg config.Provider) string { + i18nBundle := bundle.New() + + for file, content := range test.data { + err := i18nBundle.ParseTranslationFileBytes(file, content) + if err != nil { + t.Errorf("Error parsing translation file: %s", err) + } + } + + translator := NewTranslator(i18nBundle, cfg, logger) + f := translator.Func(test.lang) + translated := f(test.id, test.args) + return translated +} + +func TestI18nTranslate(t *testing.T) { + var actual, expected string + v := viper.New() + v.SetDefault("defaultContentLanguage", "en") + + // Test without and with placeholders + for _, enablePlaceholders := range []bool{false, true} { + v.Set("enableMissingTranslationPlaceholders", enablePlaceholders) + + for _, test := range i18nTests { + if enablePlaceholders { + expected = test.expectedFlag + } else { + expected = test.expected + } + actual = doTestI18nTranslate(t, test, v) + require.Equal(t, expected, actual) + } + } +} diff --git a/i18n/translationProvider.go b/i18n/translationProvider.go new file mode 100644 index 000000000..9947d3ce5 --- /dev/null +++ b/i18n/translationProvider.go @@ -0,0 +1,73 @@ +// Copyright 2017 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 i18n + +import ( + "fmt" + + "github.com/gohugoio/hugo/deps" + "github.com/gohugoio/hugo/source" + "github.com/nicksnyder/go-i18n/i18n/bundle" +) + +// TranslationProvider provides translation handling, i.e. loading +// of bundles etc. +type TranslationProvider struct { + t Translator +} + +// NewTranslationProvider creates a new translation provider. +func NewTranslationProvider() *TranslationProvider { + return &TranslationProvider{} +} + +// Update updates the i18n func in the provided Deps. +func (tp *TranslationProvider) Update(d *deps.Deps) error { + dir := d.PathSpec.AbsPathify(d.Cfg.GetString("i18nDir")) + sp := source.NewSourceSpec(d.Cfg, d.Fs) + sources := []source.Input{sp.NewFilesystem(dir)} + + themeI18nDir, err := d.PathSpec.GetThemeI18nDirPath() + + if err == nil { + sources = []source.Input{sp.NewFilesystem(themeI18nDir), sources[0]} + } + + d.Log.DEBUG.Printf("Load I18n from %q", sources) + + i18nBundle := bundle.New() + + for _, currentSource := range sources { + for _, r := range currentSource.Files() { + err := i18nBundle.ParseTranslationFileBytes(r.LogicalName(), r.Bytes()) + if err != nil { + return fmt.Errorf("Failed to load translations in file %q: %s", r.LogicalName(), err) + } + } + } + + tp.t = NewTranslator(i18nBundle, d.Cfg, d.Log) + + d.Translate = tp.t.Func(d.Language.Lang) + + return nil + +} + +// Clone sets the language func for the new language. +func (tp *TranslationProvider) Clone(d *deps.Deps) error { + d.Translate = tp.t.Func(d.Language.Lang) + + return nil +} diff --git a/identity/finder.go b/identity/finder.go deleted file mode 100644 index 9d9f9d138..000000000 --- a/identity/finder.go +++ /dev/null @@ -1,337 +0,0 @@ -// 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 identity - -import ( - "fmt" - "sync" - - "github.com/gohugoio/hugo/compare" -) - -// NewFinder creates a new Finder. -// This is a thread safe implementation with a cache. -func NewFinder(cfg FinderConfig) *Finder { - return &Finder{cfg: cfg, answers: make(map[ManagerIdentity]FinderResult), seenFindOnce: make(map[Identity]bool)} -} - -var searchIDPool = sync.Pool{ - New: func() any { - return &searchID{seen: make(map[Manager]bool)} - }, -} - -func getSearchID() *searchID { - return searchIDPool.Get().(*searchID) -} - -func putSearchID(sid *searchID) { - sid.id = nil - sid.isDp = false - sid.isPeq = false - sid.hasEqer = false - sid.maxDepth = 0 - sid.dp = nil - sid.peq = nil - sid.eqer = nil - clear(sid.seen) - searchIDPool.Put(sid) -} - -// GetSearchID returns a searchID from the pool. - -// Finder finds identities inside another. -type Finder struct { - cfg FinderConfig - - answers map[ManagerIdentity]FinderResult - muAnswers sync.RWMutex - - seenFindOnce map[Identity]bool - muSeenFindOnce sync.RWMutex -} - -type FinderResult int - -const ( - FinderNotFound FinderResult = iota - FinderFoundOneOfManyRepetition - FinderFoundOneOfMany - FinderFound -) - -// Contains returns whether in contains id. -func (f *Finder) Contains(id, in Identity, maxDepth int) FinderResult { - if id == Anonymous || in == Anonymous { - return FinderNotFound - } - - if id == GenghisKhan && in == GenghisKhan { - return FinderNotFound - } - - if id == GenghisKhan { - return FinderFound - } - - if id == in { - return FinderFound - } - - if id == nil || in == nil { - return FinderNotFound - } - - var ( - isDp bool - isPeq bool - - dp IsProbablyDependentProvider - peq compare.ProbablyEqer - ) - - if !f.cfg.Exact { - dp, isDp = id.(IsProbablyDependentProvider) - peq, isPeq = id.(compare.ProbablyEqer) - } - - eqer, hasEqer := id.(compare.Eqer) - - sid := getSearchID() - sid.id = id - sid.isDp = isDp - sid.isPeq = isPeq - sid.hasEqer = hasEqer - sid.dp = dp - sid.peq = peq - sid.eqer = eqer - sid.maxDepth = maxDepth - - defer putSearchID(sid) - - r := FinderNotFound - if i := f.checkOne(sid, in, 0); i > r { - r = i - } - if r == FinderFound { - return r - } - - m := GetDependencyManager(in) - if m != nil { - if i := f.checkManager(sid, m, 0); i > r { - r = i - } - } - return r -} - -func (f *Finder) checkMaxDepth(sid *searchID, level int) FinderResult { - if sid.maxDepth >= 0 && level > sid.maxDepth { - return FinderNotFound - } - if level > 100 { - // This should never happen, but some false positives are probably better than a panic. - if !f.cfg.Exact { - return FinderFound - } - panic("too many levels") - } - return -1 -} - -func (f *Finder) checkManager(sid *searchID, m Manager, level int) FinderResult { - if r := f.checkMaxDepth(sid, level); r >= 0 { - return r - } - - if m == nil { - return FinderNotFound - } - if sid.seen[m] { - return FinderNotFound - } - sid.seen[m] = true - - f.muAnswers.RLock() - r, ok := f.answers[ManagerIdentity{Manager: m, Identity: sid.id}] - f.muAnswers.RUnlock() - if ok { - return r - } - - r = f.search(sid, m, level) - - if r == FinderFoundOneOfMany { - // Don't cache this one. - return r - } - - f.muAnswers.Lock() - f.answers[ManagerIdentity{Manager: m, Identity: sid.id}] = r - f.muAnswers.Unlock() - - return r -} - -func (f *Finder) checkOne(sid *searchID, v Identity, depth int) (r FinderResult) { - if ff, ok := v.(FindFirstManagerIdentityProvider); ok { - f.muSeenFindOnce.RLock() - mi := ff.FindFirstManagerIdentity() - seen := f.seenFindOnce[mi.Identity] - f.muSeenFindOnce.RUnlock() - if seen { - return FinderFoundOneOfManyRepetition - } - - r = f.doCheckOne(sid, mi.Identity, depth) - if r == 0 { - r = f.checkManager(sid, mi.Manager, depth) - } - - if r > FinderFoundOneOfManyRepetition { - f.muSeenFindOnce.Lock() - // Double check. - if f.seenFindOnce[mi.Identity] { - f.muSeenFindOnce.Unlock() - return FinderFoundOneOfManyRepetition - } - f.seenFindOnce[mi.Identity] = true - f.muSeenFindOnce.Unlock() - r = FinderFoundOneOfMany - } - return r - } else { - return f.doCheckOne(sid, v, depth) - } -} - -func (f *Finder) doCheckOne(sid *searchID, v Identity, depth int) FinderResult { - id2 := Unwrap(v) - if id2 == Anonymous { - return FinderNotFound - } - id := sid.id - if sid.hasEqer { - if sid.eqer.Eq(id2) { - return FinderFound - } - } else if id == id2 { - return FinderFound - } - - if f.cfg.Exact { - return FinderNotFound - } - - if id2 == nil { - return FinderNotFound - } - - if id2 == GenghisKhan { - return FinderFound - } - - if id.IdentifierBase() == id2.IdentifierBase() { - return FinderFound - } - - if sid.isDp && sid.dp.IsProbablyDependent(id2) { - return FinderFound - } - - if sid.isPeq && sid.peq.ProbablyEq(id2) { - return FinderFound - } - - if pdep, ok := id2.(IsProbablyDependencyProvider); ok && pdep.IsProbablyDependency(id) { - return FinderFound - } - - if peq, ok := id2.(compare.ProbablyEqer); ok && peq.ProbablyEq(id) { - return FinderFound - } - - return FinderNotFound -} - -// search searches for id in ids. -func (f *Finder) search(sid *searchID, m Manager, depth int) FinderResult { - id := sid.id - - if id == Anonymous { - return FinderNotFound - } - - if !f.cfg.Exact && id == GenghisKhan { - return FinderNotFound - } - - var r FinderResult - m.forEeachIdentity( - func(v Identity) bool { - i := f.checkOne(sid, v, depth) - if i > r { - r = i - } - if r == FinderFound { - return true - } - m := GetDependencyManager(v) - if i := f.checkManager(sid, m, depth+1); i > r { - r = i - } - if r == FinderFound { - return true - } - return false - }, - ) - return r -} - -// FinderConfig provides configuration for the Finder. -// Note that we by default will use a strategy where probable matches are -// good enough. The primary use case for this is to identity the change set -// for a given changed identity (e.g. a template), and we don't want to -// have any false negatives there, but some false positives are OK. Also, speed is important. -type FinderConfig struct { - // Match exact matches only. - Exact bool -} - -// ManagerIdentity wraps a pair of Identity and Manager. -type ManagerIdentity struct { - Identity - Manager -} - -func (p ManagerIdentity) String() string { - return fmt.Sprintf("%s:%s", p.Identity.IdentifierBase(), p.Manager.IdentifierBase()) -} - -type searchID struct { - id Identity - isDp bool - isPeq bool - hasEqer bool - - maxDepth int - - seen map[Manager]bool - - dp IsProbablyDependentProvider - peq compare.ProbablyEqer - eqer compare.Eqer -} diff --git a/identity/finder_test.go b/identity/finder_test.go deleted file mode 100644 index abfab9d75..000000000 --- a/identity/finder_test.go +++ /dev/null @@ -1,58 +0,0 @@ -// 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 provides ways to identify values in Hugo. Used for dependency tracking etc. -package identity_test - -import ( - "testing" - - "github.com/gohugoio/hugo/identity" -) - -func BenchmarkFinder(b *testing.B) { - m1 := identity.NewManager("") - m2 := identity.NewManager("") - m3 := identity.NewManager("") - m1.AddIdentity( - testIdentity{"base", "id1", "", "pe1"}, - testIdentity{"base2", "id2", "eq1", ""}, - m2, - m3, - ) - - b4 := testIdentity{"base4", "id4", "", ""} - b5 := testIdentity{"base5", "id5", "", ""} - - m2.AddIdentity(b4) - - f := identity.NewFinder(identity.FinderConfig{}) - - b.Run("Find one", func(b *testing.B) { - for i := 0; i < b.N; i++ { - r := f.Contains(b4, m1, -1) - if r == 0 { - b.Fatal("not found") - } - } - }) - - b.Run("Find none", func(b *testing.B) { - for i := 0; i < b.N; i++ { - r := f.Contains(b5, m1, -1) - if r > 0 { - b.Fatal("found") - } - } - }) -} diff --git a/identity/identity.go b/identity/identity.go deleted file mode 100644 index c78ed0fdd..000000000 --- a/identity/identity.go +++ /dev/null @@ -1,521 +0,0 @@ -// 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 provides ways to identify values in Hugo. Used for dependency tracking etc. -package identity - -import ( - "fmt" - "path" - "path/filepath" - "sort" - "strings" - "sync" - "sync/atomic" - - "github.com/gohugoio/hugo/common/types" - "github.com/gohugoio/hugo/compare" -) - -const ( - // Anonymous is an Identity that can be used when identity doesn't matter. - Anonymous = StringIdentity("__anonymous") - - // GenghisKhan is an Identity everyone relates to. - GenghisKhan = StringIdentity("__genghiskhan") - - StructuralChangeAdd = StringIdentity("__structural_change_add") - StructuralChangeRemove = StringIdentity("__structural_change_remove") -) - -var NopManager = new(nopManager) - -// NewIdentityManager creates a new Manager. -func NewManager(name string, opts ...ManagerOption) Manager { - idm := &identityManager{ - Identity: Anonymous, - name: name, - ids: Identities{}, - } - - for _, o := range opts { - o(idm) - } - - return idm -} - -// CleanString cleans s to be suitable as an identifier. -func CleanString(s string) string { - s = strings.ToLower(s) - s = strings.Trim(filepath.ToSlash(s), "/") - return "/" + path.Clean(s) -} - -// CleanStringIdentity cleans s to be suitable as an identifier and wraps it in a StringIdentity. -func CleanStringIdentity(s string) StringIdentity { - return StringIdentity(CleanString(s)) -} - -// GetDependencyManager returns the DependencyManager from v or nil if none found. -func GetDependencyManager(v any) Manager { - switch vv := v.(type) { - case Manager: - return vv - case types.Unwrapper: - return GetDependencyManager(vv.Unwrapv()) - case DependencyManagerProvider: - return vv.GetDependencyManager() - } - return nil -} - -// FirstIdentity returns the first Identity in v, Anonymous if none found -func FirstIdentity(v any) Identity { - var result Identity = Anonymous - WalkIdentitiesShallow(v, func(level int, id Identity) bool { - result = id - return result != Anonymous - }) - return result -} - -// PrintIdentityInfo is used for debugging/tests only. -func PrintIdentityInfo(v any) { - WalkIdentitiesDeep(v, func(level int, id Identity) bool { - var s string - if idm, ok := id.(*identityManager); ok { - s = " " + idm.name - } - fmt.Printf("%s%s (%T)%s\n", strings.Repeat(" ", level), id.IdentifierBase(), id, s) - return false - }) -} - -func Unwrap(id Identity) Identity { - switch t := id.(type) { - case IdentityProvider: - return t.GetIdentity() - default: - return id - } -} - -// WalkIdentitiesDeep walks identities in v and applies cb to every identity found. -// Return true from cb to terminate. -// If deep is true, it will also walk nested Identities in any Manager found. -func WalkIdentitiesDeep(v any, cb func(level int, id Identity) bool) { - seen := make(map[Identity]bool) - walkIdentities(v, 0, true, seen, cb) -} - -// WalkIdentitiesShallow will not walk into a Manager's Identities. -// See WalkIdentitiesDeep. -// cb is called for every Identity found and returns whether to terminate the walk. -func WalkIdentitiesShallow(v any, cb func(level int, id Identity) bool) { - walkIdentitiesShallow(v, 0, cb) -} - -// WithOnAddIdentity sets a callback that will be invoked when an identity is added to the manager. -func WithOnAddIdentity(f func(id Identity)) ManagerOption { - return func(m *identityManager) { - m.onAddIdentity = f - } -} - -// DependencyManagerProvider provides a manager for dependencies. -type DependencyManagerProvider interface { - GetDependencyManager() Manager -} - -// DependencyManagerProviderFunc is a function that implements the DependencyManagerProvider interface. -type DependencyManagerProviderFunc func() Manager - -func (d DependencyManagerProviderFunc) GetDependencyManager() Manager { - return d() -} - -// DependencyManagerScopedProvider provides a manager for dependencies with a given scope. -type DependencyManagerScopedProvider interface { - GetDependencyManagerForScope(scope int) Manager - GetDependencyManagerForScopesAll() []Manager -} - -// ForEeachIdentityProvider provides a way iterate over identities. -type ForEeachIdentityProvider interface { - // ForEeachIdentityProvider calls cb for each Identity. - // If cb returns true, the iteration is terminated. - // The return value is whether the iteration was terminated. - ForEeachIdentity(cb func(id Identity) bool) bool -} - -// ForEeachIdentityProviderFunc is a function that implements the ForEeachIdentityProvider interface. -type ForEeachIdentityProviderFunc func(func(id Identity) bool) bool - -func (f ForEeachIdentityProviderFunc) ForEeachIdentity(cb func(id Identity) bool) bool { - return f(cb) -} - -// ForEeachIdentityByNameProvider provides a way to look up identities by name. -type ForEeachIdentityByNameProvider interface { - // ForEeachIdentityByName calls cb for each Identity that relates to name. - // If cb returns true, the iteration is terminated. - ForEeachIdentityByName(name string, cb func(id Identity) bool) -} - -type FindFirstManagerIdentityProvider interface { - Identity - FindFirstManagerIdentity() ManagerIdentity -} - -func NewFindFirstManagerIdentityProvider(m Manager, id Identity) FindFirstManagerIdentityProvider { - return findFirstManagerIdentity{ - Identity: Anonymous, - ManagerIdentity: ManagerIdentity{ - Manager: m, Identity: id, - }, - } -} - -type findFirstManagerIdentity struct { - Identity - ManagerIdentity -} - -func (f findFirstManagerIdentity) FindFirstManagerIdentity() ManagerIdentity { - return f.ManagerIdentity -} - -// Identities stores identity providers. -type Identities map[Identity]bool - -func (ids Identities) AsSlice() []Identity { - s := make([]Identity, len(ids)) - i := 0 - for v := range ids { - s[i] = v - i++ - } - sort.Slice(s, func(i, j int) bool { - return s[i].IdentifierBase() < s[j].IdentifierBase() - }) - - return s -} - -func (ids Identities) String() string { - var sb strings.Builder - i := 0 - for id := range ids { - sb.WriteString(fmt.Sprintf("[%s]", id.IdentifierBase())) - if i < len(ids)-1 { - sb.WriteString(", ") - } - i++ - } - return sb.String() -} - -// Identity represents a thing in Hugo (a Page, a template etc.) -// Any implementation must be comparable/hashable. -type Identity interface { - IdentifierBase() string -} - -// IdentityGroupProvider can be implemented by tightly connected types. -// Current use case is Resource transformation via Hugo Pipes. -type IdentityGroupProvider interface { - GetIdentityGroup() Identity -} - -// IdentityProvider can be implemented by types that isn't itself and Identity, -// usually because they're not comparable/hashable. -type IdentityProvider interface { - GetIdentity() Identity -} - -// SignalRebuilder is an optional interface for types that can signal a rebuild. -type SignalRebuilder interface { - SignalRebuild(ids ...Identity) -} - -// IncrementByOne implements Incrementer adding 1 every time Incr is called. -type IncrementByOne struct { - counter uint64 -} - -func (c *IncrementByOne) Incr() int { - return int(atomic.AddUint64(&c.counter, uint64(1))) -} - -// Incrementer increments and returns the value. -// Typically used for IDs. -type Incrementer interface { - Incr() int -} - -// IsProbablyDependentProvider is an optional interface for Identity. -type IsProbablyDependentProvider interface { - IsProbablyDependent(other Identity) bool -} - -// IsProbablyDependencyProvider is an optional interface for Identity. -type IsProbablyDependencyProvider interface { - IsProbablyDependency(other Identity) bool -} - -// Manager is an Identity that also manages identities, typically dependencies. -type Manager interface { - Identity - AddIdentity(ids ...Identity) - AddIdentityForEach(ids ...ForEeachIdentityProvider) - GetIdentity() Identity - Reset() - forEeachIdentity(func(id Identity) bool) bool -} - -type ManagerOption func(m *identityManager) - -// StringIdentity is an Identity that wraps a string. -type StringIdentity string - -func (s StringIdentity) IdentifierBase() string { - return string(s) -} - -type identityManager struct { - Identity - - // Only used for debugging. - name string - - // mu protects _changes_ to this manager, - // reads currently assumes no concurrent writes. - mu sync.RWMutex - ids Identities - forEachIds []ForEeachIdentityProvider - - // Hooks used in debugging. - onAddIdentity func(id Identity) -} - -func (im *identityManager) AddIdentity(ids ...Identity) { - im.mu.Lock() - defer im.mu.Unlock() - - for _, id := range ids { - if id == nil || id == Anonymous { - continue - } - - if _, found := im.ids[id]; !found { - if im.onAddIdentity != nil { - im.onAddIdentity(id) - } - im.ids[id] = true - } - } -} - -func (im *identityManager) AddIdentityForEach(ids ...ForEeachIdentityProvider) { - im.mu.Lock() - im.forEachIds = append(im.forEachIds, ids...) - im.mu.Unlock() -} - -func (im *identityManager) ContainsIdentity(id Identity) FinderResult { - if im.Identity != Anonymous && id == im.Identity { - return FinderFound - } - - f := NewFinder(FinderConfig{Exact: true}) - r := f.Contains(id, im, -1) - - return r -} - -// Managers are always anonymous. -func (im *identityManager) GetIdentity() Identity { - return im.Identity -} - -func (im *identityManager) Reset() { - im.mu.Lock() - im.ids = Identities{} - im.mu.Unlock() -} - -func (im *identityManager) GetDependencyManagerForScope(int) Manager { - return im -} - -func (im *identityManager) GetDependencyManagerForScopesAll() []Manager { - return []Manager{im} -} - -func (im *identityManager) String() string { - return fmt.Sprintf("IdentityManager(%s)", im.name) -} - -func (im *identityManager) forEeachIdentity(fn func(id Identity) bool) bool { - // The absence of a lock here is deliberate. This is currently only used on server reloads - // in a single-threaded context. - for id := range im.ids { - if fn(id) { - return true - } - } - for _, fe := range im.forEachIds { - if fe.ForEeachIdentity(fn) { - return true - } - } - return false -} - -type nopManager int - -func (m *nopManager) AddIdentity(ids ...Identity) { -} - -func (m *nopManager) AddIdentityForEach(ids ...ForEeachIdentityProvider) { -} - -func (m *nopManager) IdentifierBase() string { - return "" -} - -func (m *nopManager) GetIdentity() Identity { - return Anonymous -} - -func (m *nopManager) Reset() { -} - -func (m *nopManager) forEeachIdentity(func(id Identity) bool) bool { - return false -} - -// returns whether further walking should be terminated. -func walkIdentities(v any, level int, deep bool, seen map[Identity]bool, cb func(level int, id Identity) bool) { - if level > 20 { - panic("too deep") - } - var cbRecursive func(level int, id Identity) bool - cbRecursive = func(level int, id Identity) bool { - if id == nil { - return false - } - if deep && seen[id] { - return false - } - seen[id] = true - if cb(level, id) { - return true - } - - if deep { - if m := GetDependencyManager(id); m != nil { - m.forEeachIdentity(func(id2 Identity) bool { - return walkIdentitiesShallow(id2, level+1, cbRecursive) - }) - } - } - return false - } - walkIdentitiesShallow(v, level, cbRecursive) -} - -// returns whether further walking should be terminated. -// Anonymous identities are skipped. -func walkIdentitiesShallow(v any, level int, cb func(level int, id Identity) bool) bool { - cb2 := func(level int, id Identity) bool { - if id == Anonymous { - return false - } - if id == nil { - return false - } - return cb(level, id) - } - - if id, ok := v.(Identity); ok { - if cb2(level, id) { - return true - } - } - - if ipd, ok := v.(IdentityProvider); ok { - if cb2(level, ipd.GetIdentity()) { - return true - } - } - - if ipdgp, ok := v.(IdentityGroupProvider); ok { - if cb2(level, ipdgp.GetIdentityGroup()) { - return true - } - } - - return false -} - -var ( - _ Identity = (*orIdentity)(nil) - _ compare.ProbablyEqer = (*orIdentity)(nil) -) - -func Or(a, b Identity) Identity { - return orIdentity{a: a, b: b} -} - -type orIdentity struct { - a, b Identity -} - -func (o orIdentity) IdentifierBase() string { - return o.a.IdentifierBase() -} - -func (o orIdentity) ProbablyEq(other any) bool { - otherID, ok := other.(Identity) - if !ok { - return false - } - - return probablyEq(o.a, otherID) || probablyEq(o.b, otherID) -} - -func probablyEq(a, b Identity) bool { - if a == b { - return true - } - - if a == Anonymous || b == Anonymous { - return false - } - - if a.IdentifierBase() == b.IdentifierBase() { - return true - } - - if a2, ok := a.(compare.ProbablyEqer); ok && a2.ProbablyEq(b) { - return true - } - - if a2, ok := a.(IsProbablyDependentProvider); ok { - return a2.IsProbablyDependent(b) - } - - return false -} diff --git a/identity/identity_test.go b/identity/identity_test.go deleted file mode 100644 index f9b04aa14..000000000 --- a/identity/identity_test.go +++ /dev/null @@ -1,211 +0,0 @@ -// 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 identity_test - -import ( - "fmt" - "testing" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/identity" - "github.com/gohugoio/hugo/identity/identitytesting" -) - -func BenchmarkIdentityManager(b *testing.B) { - createIds := func(num int) []identity.Identity { - ids := make([]identity.Identity, num) - for i := range num { - name := fmt.Sprintf("id%d", i) - ids[i] = &testIdentity{base: name, name: name} - } - return ids - } - - b.Run("identity.NewManager", func(b *testing.B) { - for i := 0; i < b.N; i++ { - m := identity.NewManager("") - if m == nil { - b.Fatal("manager is nil") - } - } - }) - - b.Run("Add unique", func(b *testing.B) { - ids := createIds(b.N) - im := identity.NewManager("") - - b.ResetTimer() - for i := 0; i < b.N; i++ { - im.AddIdentity(ids[i]) - } - - b.StopTimer() - }) - - b.Run("Add duplicates", func(b *testing.B) { - id := &testIdentity{base: "a", name: "b"} - im := identity.NewManager("") - - b.ResetTimer() - for i := 0; i < b.N; i++ { - im.AddIdentity(id) - } - - b.StopTimer() - }) - - b.Run("Nop StringIdentity const", func(b *testing.B) { - const id = identity.StringIdentity("test") - for i := 0; i < b.N; i++ { - identity.NopManager.AddIdentity(id) - } - }) - - b.Run("Nop StringIdentity const other package", func(b *testing.B) { - for i := 0; i < b.N; i++ { - identity.NopManager.AddIdentity(identitytesting.TestIdentity) - } - }) - - b.Run("Nop StringIdentity var", func(b *testing.B) { - id := identity.StringIdentity("test") - for i := 0; i < b.N; i++ { - identity.NopManager.AddIdentity(id) - } - }) - - b.Run("Nop pointer identity", func(b *testing.B) { - id := &testIdentity{base: "a", name: "b"} - for i := 0; i < b.N; i++ { - identity.NopManager.AddIdentity(id) - } - }) - - b.Run("Nop Anonymous", func(b *testing.B) { - for i := 0; i < b.N; i++ { - identity.NopManager.AddIdentity(identity.Anonymous) - } - }) -} - -func BenchmarkIsNotDependent(b *testing.B) { - runBench := func(b *testing.B, id1, id2 identity.Identity) { - for i := 0; i < b.N; i++ { - isNotDependent(id1, id2) - } - } - - newNestedManager := func(depth, count int) identity.Manager { - m1 := identity.NewManager("") - for range depth { - m2 := identity.NewManager("") - m1.AddIdentity(m2) - for j := range count { - id := fmt.Sprintf("id%d", j) - m2.AddIdentity(&testIdentity{id, id, "", ""}) - } - m1 = m2 - } - return m1 - } - - type depthCount struct { - depth int - count int - } - - for _, dc := range []depthCount{{10, 5}} { - b.Run(fmt.Sprintf("Nested not found %d %d", dc.depth, dc.count), func(b *testing.B) { - im := newNestedManager(dc.depth, dc.count) - id1 := identity.StringIdentity("idnotfound") - b.ResetTimer() - runBench(b, im, id1) - }) - } -} - -func TestIdentityManager(t *testing.T) { - c := qt.New(t) - - newNestedManager := func() identity.Manager { - m1 := identity.NewManager("") - m2 := identity.NewManager("") - m3 := identity.NewManager("") - m1.AddIdentity( - testIdentity{"base", "id1", "", "pe1"}, - testIdentity{"base2", "id2", "eq1", ""}, - m2, - m3, - ) - - m2.AddIdentity(testIdentity{"base4", "id4", "", ""}) - - return m1 - } - - c.Run("Anonymous", func(c *qt.C) { - im := newNestedManager() - c.Assert(im.GetIdentity(), qt.Equals, identity.Anonymous) - im.AddIdentity(identity.Anonymous) - c.Assert(isNotDependent(identity.Anonymous, identity.Anonymous), qt.IsTrue) - }) - - c.Run("GenghisKhan", func(c *qt.C) { - c.Assert(isNotDependent(identity.GenghisKhan, identity.GenghisKhan), qt.IsTrue) - }) -} - -type testIdentity struct { - base string - name string - - idEq string - idProbablyEq string -} - -func (id testIdentity) Eq(other any) bool { - ot, ok := other.(testIdentity) - if !ok { - return false - } - if ot.idEq == "" || id.idEq == "" { - return false - } - return ot.idEq == id.idEq -} - -func (id testIdentity) IdentifierBase() string { - return id.base -} - -func (id testIdentity) Name() string { - return id.name -} - -func (id testIdentity) ProbablyEq(other any) bool { - ot, ok := other.(testIdentity) - if !ok { - return false - } - if ot.idProbablyEq == "" || id.idProbablyEq == "" { - return false - } - return ot.idProbablyEq == id.idProbablyEq -} - -func isNotDependent(a, b identity.Identity) bool { - f := identity.NewFinder(identity.FinderConfig{}) - r := f.Contains(b, a, -1) - return r == 0 -} diff --git a/identity/identitytesting/identitytesting.go b/identity/identitytesting/identitytesting.go deleted file mode 100644 index 74f3ec540..000000000 --- a/identity/identitytesting/identitytesting.go +++ /dev/null @@ -1,5 +0,0 @@ -package identitytesting - -import "github.com/gohugoio/hugo/identity" - -const TestIdentity = identity.StringIdentity("__testIdentity") diff --git a/identity/predicate_identity.go b/identity/predicate_identity.go deleted file mode 100644 index bad247867..000000000 --- a/identity/predicate_identity.go +++ /dev/null @@ -1,78 +0,0 @@ -// 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 provides ways to identify values in Hugo. Used for dependency tracking etc. -package identity - -import ( - "fmt" - "sync/atomic" - - hglob "github.com/gohugoio/hugo/hugofs/glob" -) - -// NewGlobIdentity creates a new Identity that -// is probably dependent on any other Identity -// that matches the given pattern. -func NewGlobIdentity(pattern string) Identity { - glob, err := hglob.GetGlob(pattern) - if err != nil { - panic(err) - } - - predicate := func(other Identity) bool { - return glob.Match(other.IdentifierBase()) - } - - return NewPredicateIdentity(predicate, nil) -} - -var predicateIdentityCounter = &atomic.Uint32{} - -type predicateIdentity struct { - id string - probablyDependent func(Identity) bool - probablyDependency func(Identity) bool -} - -var ( - _ IsProbablyDependencyProvider = &predicateIdentity{} - _ IsProbablyDependentProvider = &predicateIdentity{} -) - -// NewPredicateIdentity creates a new Identity that implements both IsProbablyDependencyProvider and IsProbablyDependentProvider -// using the provided functions, both of which are optional. -func NewPredicateIdentity( - probablyDependent func(Identity) bool, - probablyDependency func(Identity) bool, -) *predicateIdentity { - if probablyDependent == nil { - probablyDependent = func(Identity) bool { return false } - } - if probablyDependency == nil { - probablyDependency = func(Identity) bool { return false } - } - return &predicateIdentity{probablyDependent: probablyDependent, probablyDependency: probablyDependency, id: fmt.Sprintf("predicate%d", predicateIdentityCounter.Add(1))} -} - -func (id *predicateIdentity) IdentifierBase() string { - return id.id -} - -func (id *predicateIdentity) IsProbablyDependent(other Identity) bool { - return id.probablyDependent(other) -} - -func (id *predicateIdentity) IsProbablyDependency(other Identity) bool { - return id.probablyDependency(other) -} diff --git a/identity/predicate_identity_test.go b/identity/predicate_identity_test.go deleted file mode 100644 index 3a54dee75..000000000 --- a/identity/predicate_identity_test.go +++ /dev/null @@ -1,58 +0,0 @@ -// 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 provides ways to identify values in Hugo. Used for dependency tracking etc. -package identity - -import ( - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestGlobIdentity(t *testing.T) { - c := qt.New(t) - - gid := NewGlobIdentity("/a/b/*") - - c.Assert(isNotDependent(gid, StringIdentity("/a/b/c")), qt.IsFalse) - c.Assert(isNotDependent(gid, StringIdentity("/a/c/d")), qt.IsTrue) - c.Assert(isNotDependent(StringIdentity("/a/b/c"), gid), qt.IsTrue) - c.Assert(isNotDependent(StringIdentity("/a/c/d"), gid), qt.IsTrue) -} - -func isNotDependent(a, b Identity) bool { - f := NewFinder(FinderConfig{}) - r := f.Contains(a, b, -1) - return r == 0 -} - -func TestPredicateIdentity(t *testing.T) { - c := qt.New(t) - - isDependent := func(id Identity) bool { - return id.IdentifierBase() == "foo" - } - isDependency := func(id Identity) bool { - return id.IdentifierBase() == "baz" - } - - id := NewPredicateIdentity(isDependent, isDependency) - - c.Assert(id.IsProbablyDependent(StringIdentity("foo")), qt.IsTrue) - c.Assert(id.IsProbablyDependent(StringIdentity("bar")), qt.IsFalse) - c.Assert(id.IsProbablyDependent(id), qt.IsFalse) - c.Assert(id.IsProbablyDependent(NewPredicateIdentity(isDependent, nil)), qt.IsFalse) - c.Assert(id.IsProbablyDependency(StringIdentity("baz")), qt.IsTrue) - c.Assert(id.IsProbablyDependency(StringIdentity("foo")), qt.IsFalse) -} diff --git a/identity/question.go b/identity/question.go deleted file mode 100644 index 78fcb8234..000000000 --- a/identity/question.go +++ /dev/null @@ -1,57 +0,0 @@ -// 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 identity - -import "sync" - -// NewQuestion creates a new question with the given identity. -func NewQuestion[T any](id Identity) *Question[T] { - return &Question[T]{ - Identity: id, - } -} - -// Answer takes a func that knows the answer. -// Note that this is a one-time operation, -// fn will not be invoked again it the question is already answered. -// Use Result to check if the question is answered. -func (q *Question[T]) Answer(fn func() T) { - q.mu.Lock() - defer q.mu.Unlock() - - if q.answered { - return - } - - q.fasit = fn() - q.answered = true -} - -// Result returns the fasit of the question (if answered), -// and a bool indicating if the question has been answered. -func (q *Question[T]) Result() (any, bool) { - q.mu.RLock() - defer q.mu.RUnlock() - - return q.fasit, q.answered -} - -// A Question is defined by its Identity and can be answered once. -type Question[T any] struct { - Identity - fasit T - - mu sync.RWMutex - answered bool -} diff --git a/identity/question_test.go b/identity/question_test.go deleted file mode 100644 index bf1e1d06d..000000000 --- a/identity/question_test.go +++ /dev/null @@ -1,38 +0,0 @@ -// 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 identity - -import ( - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestQuestion(t *testing.T) { - c := qt.New(t) - - q := NewQuestion[int](StringIdentity("2+2?")) - - v, ok := q.Result() - c.Assert(ok, qt.Equals, false) - c.Assert(v, qt.Equals, 0) - - q.Answer(func() int { - return 4 - }) - - v, ok = q.Result() - c.Assert(ok, qt.Equals, true) - c.Assert(v, qt.Equals, 4) -} diff --git a/internal/js/api.go b/internal/js/api.go deleted file mode 100644 index 30180dece..000000000 --- a/internal/js/api.go +++ /dev/null @@ -1,51 +0,0 @@ -// 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 js - -import ( - "context" - - "github.com/gohugoio/hugo/common/maps" - "github.com/gohugoio/hugo/resources/resource" -) - -// BatcherClient is used to do JS batch operations. -type BatcherClient interface { - New(id string) (Batcher, error) - Store() *maps.Cache[string, Batcher] -} - -// BatchPackage holds a group of JavaScript resources. -type BatchPackage interface { - Groups() map[string]resource.Resources -} - -// Batcher is used to build JavaScript packages. -type Batcher interface { - Build(context.Context) (BatchPackage, error) - Config(ctx context.Context) OptionsSetter - Group(ctx context.Context, id string) BatcherGroup -} - -// BatcherGroup is a group of scripts and instances. -type BatcherGroup interface { - Instance(sid, iid string) OptionsSetter - Runner(id string) OptionsSetter - Script(id string) OptionsSetter -} - -// OptionsSetter is used to set options for a batch, script or instance. -type OptionsSetter interface { - SetOptions(map[string]any) string -} diff --git a/internal/js/esbuild/batch-esm-runner.gotmpl b/internal/js/esbuild/batch-esm-runner.gotmpl deleted file mode 100644 index 3193b4c30..000000000 --- a/internal/js/esbuild/batch-esm-runner.gotmpl +++ /dev/null @@ -1,20 +0,0 @@ -{{ range $i, $e := .Scripts -}} - {{ if eq .Export "*" }} - {{- printf "import %s as Script%d from %q;" .Export $i .Import -}} - {{ else -}} - {{- printf "import { %s as Script%d } from %q;" .Export $i .Import -}} - {{ end -}} -{{ end -}} -{{ range $i, $e := .Runners }} - {{- printf "import { %s as Run%d } from %q;" .Export $i .Import -}} -{{ end -}} -{{ if .Runners -}} - let group = { id: "{{ $.ID }}", scripts: [] } - {{ range $i, $e := .Scripts -}} - group.scripts.push({{ .RunnerJSON $i }}); - {{ end -}} - {{ range $i, $e := .Runners -}} - {{ $id := printf "Run%d" $i }} - {{ $id }}(group); - {{ end -}} -{{ end -}} diff --git a/internal/js/esbuild/batch.go b/internal/js/esbuild/batch.go deleted file mode 100644 index aa50cf2c1..000000000 --- a/internal/js/esbuild/batch.go +++ /dev/null @@ -1,1444 +0,0 @@ -// 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 esbuild provides functions for building JavaScript resources. -package esbuild - -import ( - "bytes" - "context" - _ "embed" - "encoding/json" - "fmt" - "io" - "path" - "path/filepath" - "reflect" - "sort" - "strings" - "sync" - "sync/atomic" - - "github.com/evanw/esbuild/pkg/api" - "github.com/gohugoio/hugo/cache/dynacache" - "github.com/gohugoio/hugo/common/hugio" - "github.com/gohugoio/hugo/common/maps" - "github.com/gohugoio/hugo/common/paths" - "github.com/gohugoio/hugo/deps" - "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/identity" - "github.com/gohugoio/hugo/internal/js" - "github.com/gohugoio/hugo/lazy" - "github.com/gohugoio/hugo/media" - "github.com/gohugoio/hugo/resources" - "github.com/gohugoio/hugo/resources/resource" - "github.com/gohugoio/hugo/resources/resource_factories/create" - "github.com/gohugoio/hugo/tpl/tplimpl" - "github.com/mitchellh/mapstructure" - "github.com/spf13/cast" -) - -var _ js.Batcher = (*batcher)(nil) - -const ( - NsBatch = "_hugo-js-batch" - - propsKeyImportContext = "importContext" - propsResoure = "resource" -) - -//go:embed batch-esm-runner.gotmpl -var runnerTemplateStr string - -var _ js.BatchPackage = (*Package)(nil) - -var _ buildToucher = (*optsHolder[scriptOptions])(nil) - -var ( - _ buildToucher = (*scriptGroup)(nil) - _ isBuiltOrTouchedProvider = (*scriptGroup)(nil) -) - -func NewBatcherClient(deps *deps.Deps) (js.BatcherClient, error) { - c := &BatcherClient{ - d: deps, - buildClient: NewBuildClient(deps.BaseFs.Assets, deps.ResourceSpec), - createClient: create.New(deps.ResourceSpec), - batcherStore: maps.NewCache[string, js.Batcher](), - bundlesStore: maps.NewCache[string, js.BatchPackage](), - } - - deps.BuildEndListeners.Add(func(...any) bool { - c.bundlesStore.Reset() - return false - }) - - return c, nil -} - -func (o optionsMap[K, C]) ByKey() optionsGetSetters[K, C] { - var values []optionsGetSetter[K, C] - for _, v := range o { - values = append(values, v) - } - - sort.Slice(values, func(i, j int) bool { - return values[i].Key().String() < values[j].Key().String() - }) - - return values -} - -func (o *opts[K, C]) Compiled() C { - o.h.checkCompileErr() - return o.h.compiled -} - -func (os optionsGetSetters[K, C]) Filter(predicate func(K) bool) optionsGetSetters[K, C] { - var a optionsGetSetters[K, C] - for _, v := range os { - if predicate(v.Key()) { - a = append(a, v) - } - } - return a -} - -func (o *optsHolder[C]) IdentifierBase() string { - return o.optionsID -} - -func (o *opts[K, C]) Key() K { - return o.key -} - -func (o *opts[K, C]) Reset() { - mu := o.once.ResetWithLock() - defer mu.Unlock() - o.h.resetCounter++ -} - -func (o *opts[K, C]) Get(id uint32) js.OptionsSetter { - var b *optsHolder[C] - o.once.Do(func() { - b = o.h - b.setBuilt(id) - }) - return b -} - -func (o *opts[K, C]) GetIdentity() identity.Identity { - return o.h -} - -func (o *optsHolder[C]) SetOptions(m map[string]any) string { - o.optsSetCounter++ - o.optsPrev = o.optsCurr - o.optsCurr = m - o.compiledPrev = o.compiled - o.compiled, o.compileErr = o.compiled.compileOptions(m, o.defaults) - o.checkCompileErr() - return "" -} - -// ValidateBatchID validates the given ID according to some very -func ValidateBatchID(id string, isTopLevel bool) error { - if id == "" { - return fmt.Errorf("id must be set") - } - // No Windows slashes. - if strings.Contains(id, "\\") { - return fmt.Errorf("id must not contain backslashes") - } - - // Allow forward slashes in top level IDs only. - if !isTopLevel && strings.Contains(id, "/") { - return fmt.Errorf("id must not contain forward slashes") - } - - return nil -} - -func newIsBuiltOrTouched() isBuiltOrTouched { - return isBuiltOrTouched{ - built: make(buildIDs), - touched: make(buildIDs), - } -} - -func newOpts[K any, C optionsCompiler[C]](key K, optionsID string, defaults defaultOptionValues) *opts[K, C] { - return &opts[K, C]{ - key: key, - h: &optsHolder[C]{ - optionsID: optionsID, - defaults: defaults, - isBuiltOrTouched: newIsBuiltOrTouched(), - }, - } -} - -// BatcherClient is a client for building JavaScript packages. -type BatcherClient struct { - d *deps.Deps - - once sync.Once - runnerTemplate *tplimpl.TemplInfo - - createClient *create.Client - buildClient *BuildClient - - batcherStore *maps.Cache[string, js.Batcher] - bundlesStore *maps.Cache[string, js.BatchPackage] -} - -// New creates a new Batcher with the given ID. -// This will be typically created once and reused across rebuilds. -func (c *BatcherClient) New(id string) (js.Batcher, error) { - var initErr error - c.once.Do(func() { - // We should fix the initialization order here (or use the Go template package directly), but we need to wait - // for the Hugo templates to be ready. - tmpl, err := c.d.TemplateStore.TextParse("batch-esm-runner", runnerTemplateStr) - if err != nil { - initErr = err - return - } - c.runnerTemplate = tmpl - }) - - if initErr != nil { - return nil, initErr - } - - dependencyManager := c.d.Conf.NewIdentityManager("jsbatch_" + id) - configID := "config_" + id - - b := &batcher{ - id: id, - scriptGroups: make(map[string]*scriptGroup), - dependencyManager: dependencyManager, - client: c, - configOptions: newOpts[scriptID, configOptions]( - scriptID(configID), - configID, - defaultOptionValues{}, - ), - } - - c.d.BuildEndListeners.Add(func(...any) bool { - b.reset() - return false - }) - - idFinder := identity.NewFinder(identity.FinderConfig{}) - - c.d.OnChangeListeners.Add(func(ids ...identity.Identity) bool { - for _, id := range ids { - if r := idFinder.Contains(id, b.dependencyManager, 50); r > 0 { - b.staleVersion.Add(1) - return false - } - - sp, ok := id.(identity.DependencyManagerScopedProvider) - if !ok { - continue - } - idms := sp.GetDependencyManagerForScopesAll() - - for _, g := range b.scriptGroups { - g.forEachIdentity(func(id2 identity.Identity) bool { - bt, ok := id2.(buildToucher) - if !ok { - return false - } - for _, id3 := range idms { - // This handles the removal of the only source for a script group (e.g. all shortcodes in a contnt page). - // Note the very shallow search. - if r := idFinder.Contains(id2, id3, 0); r > 0 { - bt.setTouched(b.buildCount) - return false - } - } - return false - }) - } - } - - return false - }) - - return b, nil -} - -func (c *BatcherClient) Store() *maps.Cache[string, js.Batcher] { - return c.batcherStore -} - -func (c *BatcherClient) buildBatchGroup(ctx context.Context, t *batchGroupTemplateContext) (resource.Resource, string, error) { - var buf bytes.Buffer - - if err := c.d.GetTemplateStore().ExecuteWithContext(ctx, c.runnerTemplate, &buf, t); err != nil { - return nil, "", err - } - - s := paths.AddLeadingSlash(t.keyPath + ".js") - r, err := c.createClient.FromString(s, buf.String()) - if err != nil { - return nil, "", err - } - - return r, s, nil -} - -// Package holds a group of JavaScript resources. -type Package struct { - id string - b *batcher - - groups map[string]resource.Resources -} - -func (p *Package) Groups() map[string]resource.Resources { - return p.groups -} - -type batchGroupTemplateContext struct { - keyPath string - ID string - Runners []scriptRunnerTemplateContext - Scripts []scriptBatchTemplateContext -} - -type batcher struct { - mu sync.Mutex - id string - buildCount uint32 - staleVersion atomic.Uint32 - scriptGroups scriptGroups - - client *BatcherClient - dependencyManager identity.Manager - - configOptions optionsGetSetter[scriptID, configOptions] - - // The last successfully built package. - // If this is non-nil and not stale, we can reuse it (e.g. on server rebuilds) - prevBuild *Package -} - -// Build builds the batch if not already built or if it's stale. -func (b *batcher) Build(ctx context.Context) (js.BatchPackage, error) { - key := dynacache.CleanKey(b.id + ".js") - p, err := b.client.bundlesStore.GetOrCreate(key, func() (js.BatchPackage, error) { - return b.build(ctx) - }) - if err != nil { - return nil, fmt.Errorf("failed to build JS batch %q: %w", b.id, err) - } - return p, nil -} - -func (b *batcher) Config(ctx context.Context) js.OptionsSetter { - return b.configOptions.Get(b.buildCount) -} - -func (b *batcher) Group(ctx context.Context, id string) js.BatcherGroup { - if err := ValidateBatchID(id, false); err != nil { - panic(err) - } - - b.mu.Lock() - defer b.mu.Unlock() - - group, found := b.scriptGroups[id] - if !found { - idm := b.client.d.Conf.NewIdentityManager("jsbatch_" + id) - b.dependencyManager.AddIdentity(idm) - - group = &scriptGroup{ - id: id, b: b, - isBuiltOrTouched: newIsBuiltOrTouched(), - dependencyManager: idm, - scriptsOptions: make(optionsMap[scriptID, scriptOptions]), - instancesOptions: make(optionsMap[instanceID, paramsOptions]), - runnersOptions: make(optionsMap[scriptID, scriptOptions]), - } - b.scriptGroups[id] = group - } - - group.setBuilt(b.buildCount) - - return group -} - -func (b *batcher) isStale() bool { - if b.staleVersion.Load() > 0 { - return true - } - - if b.removeNotSet() { - return true - } - - if b.configOptions.isStale() { - return true - } - - for _, v := range b.scriptGroups { - if v.isStale() { - return true - } - } - - return false -} - -func (b *batcher) build(ctx context.Context) (js.BatchPackage, error) { - b.mu.Lock() - defer b.mu.Unlock() - defer func() { - b.staleVersion.Store(0) - b.buildCount++ - }() - - if b.prevBuild != nil { - if !b.isStale() { - return b.prevBuild, nil - } - } - - p, err := b.doBuild(ctx) - if err != nil { - return nil, err - } - - b.prevBuild = p - - return p, nil -} - -func (b *batcher) doBuild(ctx context.Context) (*Package, error) { - type importContext struct { - name string - resourceGetter resource.ResourceGetter - scriptOptions scriptOptions - dm identity.Manager - } - - state := struct { - importResource *maps.Cache[string, resource.Resource] - resultResource *maps.Cache[string, resource.Resource] - importerImportContext *maps.Cache[string, importContext] - pathGroup *maps.Cache[string, string] - }{ - importResource: maps.NewCache[string, resource.Resource](), - resultResource: maps.NewCache[string, resource.Resource](), - importerImportContext: maps.NewCache[string, importContext](), - pathGroup: maps.NewCache[string, string](), - } - - multihostBasePaths := b.client.d.ResourceSpec.MultihostTargetBasePaths - - // Entry points passed to ESBuid. - var entryPoints []string - addResource := func(group, pth string, r resource.Resource, isResult bool) { - state.pathGroup.Set(paths.TrimExt(pth), group) - state.importResource.Set(pth, r) - if isResult { - state.resultResource.Set(pth, r) - } - entryPoints = append(entryPoints, pth) - } - - for _, g := range b.scriptGroups.Sorted() { - keyPath := g.id - - t := &batchGroupTemplateContext{ - keyPath: keyPath, - ID: g.id, - } - - instances := g.instancesOptions.ByKey() - - for _, vv := range g.scriptsOptions.ByKey() { - keyPath := keyPath + "_" + vv.Key().String() - opts := vv.Compiled() - impPath := path.Join(PrefixHugoVirtual, opts.Dir(), keyPath+opts.Resource.MediaType().FirstSuffix.FullSuffix) - impCtx := opts.ImportContext - - state.importerImportContext.Set(impPath, importContext{ - name: keyPath, - resourceGetter: impCtx, - scriptOptions: opts, - dm: g.dependencyManager, - }) - - bt := scriptBatchTemplateContext{ - opts: vv, - Import: impPath, - Instances: []scriptInstanceBatchTemplateContext{}, - } - state.importResource.Set(bt.Import, vv.Compiled().Resource) - predicate := func(k instanceID) bool { - return k.scriptID == vv.Key() - } - for _, vvv := range instances.Filter(predicate) { - bt.Instances = append(bt.Instances, scriptInstanceBatchTemplateContext{opts: vvv}) - } - - t.Scripts = append(t.Scripts, bt) - } - - for _, vv := range g.runnersOptions.ByKey() { - runnerKeyPath := keyPath + "_" + vv.Key().String() - runnerImpPath := paths.AddLeadingSlash(runnerKeyPath + "_runner" + vv.Compiled().Resource.MediaType().FirstSuffix.FullSuffix) - t.Runners = append(t.Runners, scriptRunnerTemplateContext{opts: vv, Import: runnerImpPath}) - addResource(g.id, runnerImpPath, vv.Compiled().Resource, false) - } - - r, s, err := b.client.buildBatchGroup(ctx, t) - if err != nil { - return nil, fmt.Errorf("failed to build JS batch: %w", err) - } - - state.importerImportContext.Set(s, importContext{ - name: s, - resourceGetter: nil, - dm: g.dependencyManager, - }) - - addResource(g.id, s, r, true) - } - - mediaTypes := b.client.d.ResourceSpec.MediaTypes() - - externalOptions := b.configOptions.Compiled().Options - if externalOptions.Format == "" { - externalOptions.Format = "esm" - } - if externalOptions.Format != "esm" { - return nil, fmt.Errorf("only esm format is currently supported") - } - - jsOpts := Options{ - ExternalOptions: externalOptions, - InternalOptions: InternalOptions{ - DependencyManager: b.dependencyManager, - Splitting: true, - ImportOnResolveFunc: func(imp string, args api.OnResolveArgs) string { - var importContextPath string - if args.Kind == api.ResolveEntryPoint { - importContextPath = args.Path - } else { - importContextPath = args.Importer - } - importContext, importContextFound := state.importerImportContext.Get(importContextPath) - - // We want to track the dependencies closest to where they're used. - dm := b.dependencyManager - if importContextFound { - dm = importContext.dm - } - - if r, found := state.importResource.Get(imp); found { - dm.AddIdentity(identity.FirstIdentity(r)) - return imp - } - - if importContext.resourceGetter != nil { - resolved := ResolveResource(imp, importContext.resourceGetter) - if resolved != nil { - resolvePath := resources.InternalResourceTargetPath(resolved) - dm.AddIdentity(identity.FirstIdentity(resolved)) - imp := PrefixHugoVirtual + resolvePath - state.importResource.Set(imp, resolved) - state.importerImportContext.Set(imp, importContext) - return imp - - } - } - return "" - }, - ImportOnLoadFunc: func(args api.OnLoadArgs) string { - imp := args.Path - - if r, found := state.importResource.Get(imp); found { - content, err := r.(resource.ContentProvider).Content(ctx) - if err != nil { - panic(err) - } - return cast.ToString(content) - } - return "" - }, - ImportParamsOnLoadFunc: func(args api.OnLoadArgs) json.RawMessage { - if importContext, found := state.importerImportContext.Get(args.Path); found { - if !importContext.scriptOptions.IsZero() { - return importContext.scriptOptions.Params - } - } - return nil - }, - ErrorMessageResolveFunc: func(args api.Message) *ErrorMessageResolved { - if loc := args.Location; loc != nil { - path := strings.TrimPrefix(loc.File, NsHugoImportResolveFunc+":") - if r, found := state.importResource.Get(path); found { - sourcePath := resources.InternalResourceSourcePathBestEffort(r) - - var contentr hugio.ReadSeekCloser - if cp, ok := r.(hugio.ReadSeekCloserProvider); ok { - contentr, _ = cp.ReadSeekCloser() - } - return &ErrorMessageResolved{ - Content: contentr, - Path: sourcePath, - Message: args.Text, - } - - } - - } - return nil - }, - ResolveSourceMapSource: func(s string) string { - if r, found := state.importResource.Get(s); found { - if ss := resources.InternalResourceSourcePath(r); ss != "" { - return ss - } - return PrefixHugoMemory + s - } - return "" - }, - EntryPoints: entryPoints, - }, - } - - result, err := b.client.buildClient.Build(jsOpts) - if err != nil { - return nil, fmt.Errorf("failed to build JS bundle: %w", err) - } - - groups := make(map[string]resource.Resources) - - createAndAddResource := func(targetPath, group string, o api.OutputFile, mt media.Type) error { - var sourceFilename string - if r, found := state.importResource.Get(targetPath); found { - sourceFilename = resources.InternalResourceSourcePathBestEffort(r) - } - targetPath = path.Join(b.id, targetPath) - - rd := resources.ResourceSourceDescriptor{ - LazyPublish: true, - OpenReadSeekCloser: func() (hugio.ReadSeekCloser, error) { - return hugio.NewReadSeekerNoOpCloserFromBytes(o.Contents), nil - }, - MediaType: mt, - TargetPath: targetPath, - SourceFilenameOrPath: sourceFilename, - } - r, err := b.client.d.ResourceSpec.NewResource(rd) - if err != nil { - return err - } - - groups[group] = append(groups[group], r) - - return nil - } - - outDir := b.client.d.AbsPublishDir - - createAndAddResources := func(o api.OutputFile) (bool, error) { - p := paths.ToSlashPreserveLeading(strings.TrimPrefix(o.Path, outDir)) - ext := path.Ext(p) - mt, _, found := mediaTypes.GetBySuffix(ext) - if !found { - return false, nil - } - - group, found := state.pathGroup.Get(paths.TrimExt(p)) - - if !found { - return false, nil - } - - if err := createAndAddResource(p, group, o, mt); err != nil { - return false, err - } - - return true, nil - } - - for _, o := range result.OutputFiles { - handled, err := createAndAddResources(o) - if err != nil { - return nil, err - } - - if !handled { - // Copy to destination. - // In a multihost setup, we will have multiple targets. - var targetFilenames []string - if len(multihostBasePaths) > 0 { - for _, base := range multihostBasePaths { - p := strings.TrimPrefix(o.Path, outDir) - targetFilename := filepath.Join(base, b.id, p) - targetFilenames = append(targetFilenames, targetFilename) - } - } else { - p := strings.TrimPrefix(o.Path, outDir) - targetFilename := filepath.Join(b.id, p) - targetFilenames = append(targetFilenames, targetFilename) - } - - fs := b.client.d.BaseFs.PublishFs - - if err := func() error { - fw, err := helpers.OpenFilesForWriting(fs, targetFilenames...) - if err != nil { - return err - } - defer fw.Close() - - fr := bytes.NewReader(o.Contents) - - _, err = io.Copy(fw, fr) - - return err - }(); err != nil { - return nil, fmt.Errorf("failed to copy to %q: %w", targetFilenames, err) - } - } - } - - p := &Package{ - id: path.Join(NsBatch, b.id), - b: b, - groups: groups, - } - - return p, nil -} - -func (b *batcher) removeNotSet() bool { - // We already have the lock. - var removed bool - currentBuildID := b.buildCount - for k, v := range b.scriptGroups { - if !v.isBuilt(currentBuildID) && v.isTouched(currentBuildID) { - // Remove entire group. - removed = true - delete(b.scriptGroups, k) - continue - } - if v.removeTouchedButNotSet() { - removed = true - } - if v.removeNotSet() { - removed = true - } - } - - return removed -} - -func (b *batcher) reset() { - b.mu.Lock() - defer b.mu.Unlock() - b.configOptions.Reset() - for _, v := range b.scriptGroups { - v.Reset() - } -} - -type buildIDs map[uint32]bool - -func (b buildIDs) Has(buildID uint32) bool { - return b[buildID] -} - -func (b buildIDs) Set(buildID uint32) { - b[buildID] = true -} - -type buildToucher interface { - setTouched(buildID uint32) -} - -type configOptions struct { - Options ExternalOptions -} - -func (s configOptions) isStaleCompiled(prev configOptions) bool { - return false -} - -func (s configOptions) compileOptions(m map[string]any, defaults defaultOptionValues) (configOptions, error) { - config, err := DecodeExternalOptions(m) - if err != nil { - return configOptions{}, err - } - - return configOptions{ - Options: config, - }, nil -} - -type defaultOptionValues struct { - defaultExport string -} - -type instanceID struct { - scriptID scriptID - instanceID string -} - -func (i instanceID) String() string { - return i.scriptID.String() + "_" + i.instanceID -} - -type isBuiltOrTouched struct { - built buildIDs - touched buildIDs -} - -func (i isBuiltOrTouched) setBuilt(id uint32) { - i.built.Set(id) -} - -func (i isBuiltOrTouched) isBuilt(id uint32) bool { - return i.built.Has(id) -} - -func (i isBuiltOrTouched) setTouched(id uint32) { - i.touched.Set(id) -} - -func (i isBuiltOrTouched) isTouched(id uint32) bool { - return i.touched.Has(id) -} - -type isBuiltOrTouchedProvider interface { - isBuilt(uint32) bool - isTouched(uint32) bool -} - -type key interface { - comparable - fmt.Stringer -} - -type optionsCompiler[C any] interface { - isStaleCompiled(C) bool - compileOptions(map[string]any, defaultOptionValues) (C, error) -} - -type optionsGetSetter[K, C any] interface { - isBuiltOrTouchedProvider - identity.IdentityProvider - // resource.StaleInfo - - Compiled() C - Key() K - Reset() - - Get(uint32) js.OptionsSetter - isStale() bool - currPrev() (map[string]any, map[string]any) -} - -type optionsGetSetters[K key, C any] []optionsGetSetter[K, C] - -type optionsMap[K key, C any] map[K]optionsGetSetter[K, C] - -type opts[K any, C optionsCompiler[C]] struct { - key K - h *optsHolder[C] - once lazy.OnceMore -} - -type optsHolder[C optionsCompiler[C]] struct { - optionsID string - - defaults defaultOptionValues - - // Keep track of one generation so we can detect changes. - // Note that most of this tracking is performed on the options/map level. - compiled C - compiledPrev C - compileErr error - - resetCounter uint32 - optsSetCounter uint32 - optsCurr map[string]any - optsPrev map[string]any - - isBuiltOrTouched -} - -type paramsOptions struct { - Params json.RawMessage -} - -func (s paramsOptions) isStaleCompiled(prev paramsOptions) bool { - return false -} - -func (s paramsOptions) compileOptions(m map[string]any, defaults defaultOptionValues) (paramsOptions, error) { - v := struct { - Params map[string]any - }{} - - if err := mapstructure.WeakDecode(m, &v); err != nil { - return paramsOptions{}, err - } - - paramsJSON, err := json.Marshal(v.Params) - if err != nil { - return paramsOptions{}, err - } - - return paramsOptions{ - Params: paramsJSON, - }, nil -} - -type scriptBatchTemplateContext struct { - opts optionsGetSetter[scriptID, scriptOptions] - Import string - Instances []scriptInstanceBatchTemplateContext -} - -func (s *scriptBatchTemplateContext) Export() string { - return s.opts.Compiled().Export -} - -func (c scriptBatchTemplateContext) MarshalJSON() (b []byte, err error) { - return json.Marshal(&struct { - ID string `json:"id"` - Instances []scriptInstanceBatchTemplateContext `json:"instances"` - }{ - ID: c.opts.Key().String(), - Instances: c.Instances, - }) -} - -func (b scriptBatchTemplateContext) RunnerJSON(i int) string { - script := fmt.Sprintf("Script%d", i) - - v := struct { - ID string `json:"id"` - - // Read-only live JavaScript binding. - Binding string `json:"binding"` - Instances []scriptInstanceBatchTemplateContext `json:"instances"` - }{ - b.opts.Key().String(), - script, - b.Instances, - } - - bb, err := json.Marshal(v) - if err != nil { - panic(err) - } - s := string(bb) - - // Remove the quotes to make it a valid JS object. - s = strings.ReplaceAll(s, fmt.Sprintf("%q", script), script) - - return s -} - -type scriptGroup struct { - mu sync.Mutex - id string - b *batcher - isBuiltOrTouched - dependencyManager identity.Manager - - scriptsOptions optionsMap[scriptID, scriptOptions] - instancesOptions optionsMap[instanceID, paramsOptions] - runnersOptions optionsMap[scriptID, scriptOptions] -} - -// For internal use only. -func (b *scriptGroup) GetDependencyManager() identity.Manager { - return b.dependencyManager -} - -// For internal use only. -func (b *scriptGroup) IdentifierBase() string { - return b.id -} - -func (s *scriptGroup) Instance(sid, id string) js.OptionsSetter { - if err := ValidateBatchID(sid, false); err != nil { - panic(err) - } - if err := ValidateBatchID(id, false); err != nil { - panic(err) - } - - s.mu.Lock() - defer s.mu.Unlock() - - iid := instanceID{scriptID: scriptID(sid), instanceID: id} - if v, found := s.instancesOptions[iid]; found { - return v.Get(s.b.buildCount) - } - - fullID := "instance_" + s.key() + "_" + iid.String() - - s.instancesOptions[iid] = newOpts[instanceID, paramsOptions]( - iid, - fullID, - defaultOptionValues{}, - ) - - return s.instancesOptions[iid].Get(s.b.buildCount) -} - -func (g *scriptGroup) Reset() { - for _, v := range g.scriptsOptions { - v.Reset() - } - for _, v := range g.instancesOptions { - v.Reset() - } - for _, v := range g.runnersOptions { - v.Reset() - } -} - -func (s *scriptGroup) Runner(id string) js.OptionsSetter { - if err := ValidateBatchID(id, false); err != nil { - panic(err) - } - - s.mu.Lock() - defer s.mu.Unlock() - sid := scriptID(id) - if v, found := s.runnersOptions[sid]; found { - return v.Get(s.b.buildCount) - } - - runnerIdentity := "runner_" + s.key() + "_" + id - - // A typical signature for a runner would be: - // export default function Run(scripts) {} - // The user can override the default export in the templates. - - s.runnersOptions[sid] = newOpts[scriptID, scriptOptions]( - sid, - runnerIdentity, - defaultOptionValues{ - defaultExport: "default", - }, - ) - - return s.runnersOptions[sid].Get(s.b.buildCount) -} - -func (s *scriptGroup) Script(id string) js.OptionsSetter { - if err := ValidateBatchID(id, false); err != nil { - panic(err) - } - - s.mu.Lock() - defer s.mu.Unlock() - sid := scriptID(id) - if v, found := s.scriptsOptions[sid]; found { - return v.Get(s.b.buildCount) - } - - scriptIdentity := "script_" + s.key() + "_" + id - - s.scriptsOptions[sid] = newOpts[scriptID, scriptOptions]( - sid, - scriptIdentity, - defaultOptionValues{ - defaultExport: "*", - }, - ) - - return s.scriptsOptions[sid].Get(s.b.buildCount) -} - -func (s *scriptGroup) isStale() bool { - for _, v := range s.scriptsOptions { - if v.isStale() { - return true - } - } - - for _, v := range s.instancesOptions { - if v.isStale() { - return true - } - } - - for _, v := range s.runnersOptions { - if v.isStale() { - return true - } - } - - return false -} - -func (v *scriptGroup) forEachIdentity( - f func(id identity.Identity) bool, -) bool { - if f(v) { - return true - } - for _, vv := range v.instancesOptions { - if f(vv.GetIdentity()) { - return true - } - } - - for _, vv := range v.scriptsOptions { - if f(vv.GetIdentity()) { - return true - } - } - - for _, vv := range v.runnersOptions { - if f(vv.GetIdentity()) { - return true - } - } - - return false -} - -func (s *scriptGroup) key() string { - return s.b.id + "_" + s.id -} - -func (g *scriptGroup) removeNotSet() bool { - currentBuildID := g.b.buildCount - if !g.isBuilt(currentBuildID) { - // This group was never accessed in this build. - return false - } - var removed bool - - if g.instancesOptions.isBuilt(currentBuildID) { - // A new instance has been set in this group for this build. - // Remove any instance that has not been set in this build. - for k, v := range g.instancesOptions { - if v.isBuilt(currentBuildID) { - continue - } - delete(g.instancesOptions, k) - removed = true - } - } - - if g.runnersOptions.isBuilt(currentBuildID) { - // A new runner has been set in this group for this build. - // Remove any runner that has not been set in this build. - for k, v := range g.runnersOptions { - if v.isBuilt(currentBuildID) { - continue - } - delete(g.runnersOptions, k) - removed = true - } - } - - if g.scriptsOptions.isBuilt(currentBuildID) { - // A new script has been set in this group for this build. - // Remove any script that has not been set in this build. - for k, v := range g.scriptsOptions { - if v.isBuilt(currentBuildID) { - continue - } - delete(g.scriptsOptions, k) - - // Also remove any instance with this ID. - for kk := range g.instancesOptions { - if kk.scriptID == k { - delete(g.instancesOptions, kk) - } - } - removed = true - } - } - - return removed -} - -func (g *scriptGroup) removeTouchedButNotSet() bool { - currentBuildID := g.b.buildCount - var removed bool - for k, v := range g.instancesOptions { - if v.isBuilt(currentBuildID) { - continue - } - if v.isTouched(currentBuildID) { - delete(g.instancesOptions, k) - removed = true - } - } - for k, v := range g.runnersOptions { - if v.isBuilt(currentBuildID) { - continue - } - if v.isTouched(currentBuildID) { - delete(g.runnersOptions, k) - removed = true - } - } - for k, v := range g.scriptsOptions { - if v.isBuilt(currentBuildID) { - continue - } - if v.isTouched(currentBuildID) { - delete(g.scriptsOptions, k) - removed = true - - // Also remove any instance with this ID. - for kk := range g.instancesOptions { - if kk.scriptID == k { - delete(g.instancesOptions, kk) - } - } - } - - } - return removed -} - -type scriptGroups map[string]*scriptGroup - -func (s scriptGroups) Sorted() []*scriptGroup { - var a []*scriptGroup - for _, v := range s { - a = append(a, v) - } - sort.Slice(a, func(i, j int) bool { - return a[i].id < a[j].id - }) - return a -} - -type scriptID string - -func (s scriptID) String() string { - return string(s) -} - -type scriptInstanceBatchTemplateContext struct { - opts optionsGetSetter[instanceID, paramsOptions] -} - -func (c scriptInstanceBatchTemplateContext) ID() string { - return c.opts.Key().instanceID -} - -func (c scriptInstanceBatchTemplateContext) MarshalJSON() (b []byte, err error) { - return json.Marshal(&struct { - ID string `json:"id"` - Params json.RawMessage `json:"params"` - }{ - ID: c.opts.Key().instanceID, - Params: c.opts.Compiled().Params, - }) -} - -type scriptOptions struct { - // The script to build. - Resource resource.Resource - - // The import context to use. - // Note that we will always fall back to the resource's own import context. - ImportContext resource.ResourceGetter - - // The export name to use for this script's group's runners (if any). - // If not set, the default export will be used. - Export string - - // Params marshaled to JSON. - Params json.RawMessage -} - -func (o *scriptOptions) Dir() string { - return path.Dir(resources.InternalResourceTargetPath(o.Resource)) -} - -func (s scriptOptions) IsZero() bool { - return s.Resource == nil -} - -func (s scriptOptions) isStaleCompiled(prev scriptOptions) bool { - if prev.IsZero() { - return false - } - - // All but the ImportContext are checked at the options/map level. - i1nil, i2nil := prev.ImportContext == nil, s.ImportContext == nil - if i1nil && i2nil { - return false - } - if i1nil || i2nil { - return true - } - // On its own this check would have too many false positives, but combined with the other checks it should be fine. - // We cannot do equality checking here. - if !prev.ImportContext.(resource.IsProbablySameResourceGetter).IsProbablySameResourceGetter(s.ImportContext) { - return true - } - - return false -} - -func (s scriptOptions) compileOptions(m map[string]any, defaults defaultOptionValues) (scriptOptions, error) { - v := struct { - Resource resource.Resource - ImportContext any - Export string - Params map[string]any - }{} - - if err := mapstructure.WeakDecode(m, &v); err != nil { - panic(err) - } - - var paramsJSON []byte - if v.Params != nil { - var err error - paramsJSON, err = json.Marshal(v.Params) - if err != nil { - panic(err) - } - } - - if v.Export == "" { - v.Export = defaults.defaultExport - } - - compiled := scriptOptions{ - Resource: v.Resource, - Export: v.Export, - ImportContext: resource.NewCachedResourceGetter(v.ImportContext), - Params: paramsJSON, - } - - if compiled.Resource == nil { - return scriptOptions{}, fmt.Errorf("resource not set") - } - - return compiled, nil -} - -type scriptRunnerTemplateContext struct { - opts optionsGetSetter[scriptID, scriptOptions] - Import string -} - -func (s *scriptRunnerTemplateContext) Export() string { - return s.opts.Compiled().Export -} - -func (c scriptRunnerTemplateContext) MarshalJSON() (b []byte, err error) { - return json.Marshal(&struct { - ID string `json:"id"` - }{ - ID: c.opts.Key().String(), - }) -} - -func (o optionsMap[K, C]) isBuilt(id uint32) bool { - for _, v := range o { - if v.isBuilt(id) { - return true - } - } - - return false -} - -func (o *opts[K, C]) isBuilt(id uint32) bool { - return o.h.isBuilt(id) -} - -func (o *opts[K, C]) isStale() bool { - if o.h.isStaleOpts() { - return true - } - if o.h.compiled.isStaleCompiled(o.h.compiledPrev) { - return true - } - return false -} - -func (o *optsHolder[C]) isStaleOpts() bool { - if o.optsSetCounter == 1 && o.resetCounter > 0 { - return false - } - isStale := func() bool { - if len(o.optsCurr) != len(o.optsPrev) { - return true - } - for k, v := range o.optsPrev { - vv, found := o.optsCurr[k] - if !found { - return true - } - if strings.EqualFold(k, propsKeyImportContext) { - // This is checked later. - } else if si, ok := vv.(resource.StaleInfo); ok { - if si.StaleVersion() > 0 { - return true - } - } else { - if !reflect.DeepEqual(v, vv) { - return true - } - } - } - return false - }() - - return isStale -} - -func (o *opts[K, C]) isTouched(id uint32) bool { - return o.h.isTouched(id) -} - -func (o *optsHolder[C]) checkCompileErr() { - if o.compileErr != nil { - panic(o.compileErr) - } -} - -func (o *opts[K, C]) currPrev() (map[string]any, map[string]any) { - return o.h.optsCurr, o.h.optsPrev -} - -func init() { - // We don't want any dependencies/change tracking on the top level Package, - // we want finer grained control via Package.Group. - var p any = &Package{} - if _, ok := p.(identity.Identity); ok { - panic("esbuid.Package should not implement identity.Identity") - } - if _, ok := p.(identity.DependencyManagerProvider); ok { - panic("esbuid.Package should not implement identity.DependencyManagerProvider") - } -} diff --git a/internal/js/esbuild/batch_integration_test.go b/internal/js/esbuild/batch_integration_test.go deleted file mode 100644 index b4a2454ac..000000000 --- a/internal/js/esbuild/batch_integration_test.go +++ /dev/null @@ -1,723 +0,0 @@ -// 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 js provides functions for building JavaScript resources -package esbuild_test - -import ( - "os" - "path/filepath" - "strings" - "testing" - - qt "github.com/frankban/quicktest" - - "github.com/bep/logg" - "github.com/gohugoio/hugo/common/paths" - "github.com/gohugoio/hugo/hugolib" - "github.com/gohugoio/hugo/internal/js/esbuild" -) - -// Used to test misc. error situations etc. -const jsBatchFilesTemplate = ` --- hugo.toml -- -disableKinds = ["taxonomy", "term", "section"] -disableLiveReload = true --- assets/js/styles.css -- -body { - background-color: red; -} --- assets/js/main.js -- -import './styles.css'; -import * as params from '@params'; -import * as foo from 'mylib'; -console.log("Hello, Main!"); -console.log("params.p1", params.p1); -export default function Main() {}; --- assets/js/runner.js -- -console.log("Hello, Runner!"); --- node_modules/mylib/index.js -- -console.log("Hello, My Lib!"); --- layouts/shortcodes/hdx.html -- -{{ $path := .Get "r" }} -{{ $r := or (.Page.Resources.Get $path) (resources.Get $path) }} -{{ $batch := (js.Batch "mybatch") }} -{{ $scriptID := $path | anchorize }} -{{ $instanceID := .Ordinal | string }} -{{ $group := .Page.RelPermalink | anchorize }} -{{ $params := .Params | default dict }} -{{ $export := .Get "export" | default "default" }} -{{ with $batch.Group $group }} - {{ with .Runner "create-elements" }} - {{ .SetOptions (dict "resource" (resources.Get "js/runner.js")) }} - {{ end }} - {{ with .Script $scriptID }} - {{ .SetOptions (dict - "resource" $r - "export" $export - "importContext" (slice $.Page) - ) - }} - {{ end }} - {{ with .Instance $scriptID $instanceID }} - {{ .SetOptions (dict "params" $params) }} - {{ end }} -{{ end }} -hdx-instance: {{ $scriptID }}: {{ $instanceID }}| --- layouts/_default/baseof.html -- -Base. -{{ $batch := (js.Batch "mybatch") }} - {{ with $batch.Config }} - {{ .SetOptions (dict - "params" (dict "id" "config") - "sourceMap" "" - ) - }} -{{ end }} -{{ with (templates.Defer (dict "key" "global")) }} -Defer: -{{ $batch := (js.Batch "mybatch") }} -{{ range $k, $v := $batch.Build.Groups }} - {{ range $kk, $vv := . -}} - {{ $k }}: {{ .RelPermalink }} - {{ end }} -{{ end -}} -{{ end }} -{{ block "main" . }}Main{{ end }} -End. --- layouts/_default/single.html -- -{{ define "main" }} -==> Single Template Content: {{ .Content }}$ -{{ $batch := (js.Batch "mybatch") }} -{{ with $batch.Group "mygroup" }} - {{ with .Runner "run" }} - {{ .SetOptions (dict "resource" (resources.Get "js/runner.js")) }} - {{ end }} - {{ with .Script "main" }} - {{ .SetOptions (dict "resource" (resources.Get "js/main.js") "params" (dict "p1" "param-p1-main" )) }} - {{ end }} - {{ with .Instance "main" "i1" }} - {{ .SetOptions (dict "params" (dict "title" "Instance 1")) }} - {{ end }} -{{ end }} -{{ end }} --- layouts/index.html -- -{{ define "main" }} -Home. -{{ end }} --- content/p1/index.md -- ---- -title: "P1" ---- - -Some content. - -{{< hdx r="p1script.js" myparam="p1-param-1" >}} -{{< hdx r="p1script.js" myparam="p1-param-2" >}} - --- content/p1/p1script.js -- -console.log("P1 Script"); - - -` - -// Just to verify that the above file setup works. -func TestBatchTemplateOKBuild(t *testing.T) { - b := hugolib.Test(t, jsBatchFilesTemplate, hugolib.TestOptWithOSFs()) - b.AssertPublishDir("mybatch/mygroup.js", "mybatch/mygroup.css") -} - -func TestBatchRemoveAllInGroup(t *testing.T) { - files := jsBatchFilesTemplate - b := hugolib.TestRunning(t, files, hugolib.TestOptWithOSFs()) - - b.AssertFileContent("public/p1/index.html", "p1: /mybatch/p1.js") - - b.EditFiles("content/p1/index.md", ` ---- -title: "P1" ---- -Empty. -`) - b.Build() - - b.AssertFileContent("public/p1/index.html", "! p1: /mybatch/p1.js") - - // Add one script back. - b.EditFiles("content/p1/index.md", ` ---- -title: "P1" ---- - -{{< hdx r="p1script.js" myparam="p1-param-1-new" >}} -`) - b.Build() - - b.AssertFileContent("public/mybatch/p1.js", - "p1-param-1-new", - "p1script.js") -} - -func TestBatchEditInstance(t *testing.T) { - files := jsBatchFilesTemplate - b := hugolib.TestRunning(t, files, hugolib.TestOptWithOSFs()) - b.AssertFileContent("public/mybatch/mygroup.js", "Instance 1") - b.EditFileReplaceAll("layouts/_default/single.html", "Instance 1", "Instance 1 Edit").Build() - b.AssertFileContent("public/mybatch/mygroup.js", "Instance 1 Edit") -} - -func TestBatchEditScriptParam(t *testing.T) { - files := jsBatchFilesTemplate - b := hugolib.TestRunning(t, files, hugolib.TestOptWithOSFs()) - b.AssertFileContent("public/mybatch/mygroup.js", "param-p1-main") - b.EditFileReplaceAll("layouts/_default/single.html", "param-p1-main", "param-p1-main-edited").Build() - b.AssertFileContent("public/mybatch/mygroup.js", "param-p1-main-edited") -} - -func TestBatchMultiHost(t *testing.T) { - files := ` --- hugo.toml -- -disableKinds = ["taxonomy", "term", "section"] -[languages] -[languages.en] -weight = 1 -baseURL = "https://example.com/en" -[languages.fr] -weight = 2 -baseURL = "https://example.com/fr" -disableLiveReload = true --- assets/js/styles.css -- -body { - background-color: red; -} --- assets/js/main.js -- -import * as foo from 'mylib'; -console.log("Hello, Main!"); --- assets/js/runner.js -- -console.log("Hello, Runner!"); --- node_modules/mylib/index.js -- -console.log("Hello, My Lib!"); --- layouts/index.html -- -Home. -{{ $batch := (js.Batch "mybatch") }} - {{ with $batch.Config }} - {{ .SetOptions (dict - "params" (dict "id" "config") - "sourceMap" "" - ) - }} -{{ end }} -{{ with (templates.Defer (dict "key" "global")) }} -Defer: -{{ $batch := (js.Batch "mybatch") }} -{{ range $k, $v := $batch.Build.Groups }} - {{ range $kk, $vv := . -}} - {{ $k }}: {{ .RelPermalink }} - {{ end }} -{{ end -}} -{{ end }} -{{ $batch := (js.Batch "mybatch") }} -{{ with $batch.Group "mygroup" }} - {{ with .Runner "run" }} - {{ .SetOptions (dict "resource" (resources.Get "js/runner.js")) }} - {{ end }} - {{ with .Script "main" }} - {{ .SetOptions (dict "resource" (resources.Get "js/main.js") "params" (dict "p1" "param-p1-main" )) }} - {{ end }} - {{ with .Instance "main" "i1" }} - {{ .SetOptions (dict "params" (dict "title" "Instance 1")) }} - {{ end }} -{{ end }} - - -` - b := hugolib.Test(t, files, hugolib.TestOptWithOSFs()) - b.AssertPublishDir( - "en/mybatch/chunk-TOZKWCDE.js", "en/mybatch/mygroup.js ", - "fr/mybatch/mygroup.js", "fr/mybatch/chunk-TOZKWCDE.js") -} - -func TestBatchRenameBundledScript(t *testing.T) { - files := jsBatchFilesTemplate - b := hugolib.TestRunning(t, files, hugolib.TestOptWithOSFs()) - b.AssertFileContent("public/mybatch/p1.js", "P1 Script") - b.RenameFile("content/p1/p1script.js", "content/p1/p1script2.js") - _, err := b.BuildE() - b.Assert(err, qt.IsNotNil) - b.Assert(err.Error(), qt.Contains, "resource not set") - - // Rename it back. - b.RenameFile("content/p1/p1script2.js", "content/p1/p1script.js") - b.Build() -} - -func TestBatchErrorScriptResourceNotSet(t *testing.T) { - files := strings.Replace(jsBatchFilesTemplate, `(resources.Get "js/main.js")`, `(resources.Get "js/doesnotexist.js")`, 1) - b, err := hugolib.TestE(t, files, hugolib.TestOptWithOSFs()) - b.Assert(err, qt.IsNotNil) - b.Assert(err.Error(), qt.Contains, `error calling SetOptions: resource not set`) -} - -func TestBatchSlashInBatchID(t *testing.T) { - files := strings.ReplaceAll(jsBatchFilesTemplate, `"mybatch"`, `"my/batch"`) - b, err := hugolib.TestE(t, files, hugolib.TestOptWithOSFs()) - b.Assert(err, qt.IsNil) - b.AssertPublishDir("my/batch/mygroup.js") -} - -func TestBatchSourceMaps(t *testing.T) { - filesTemplate := ` --- hugo.toml -- -disableKinds = ["taxonomy", "term", "section"] -disableLiveReload = true --- assets/js/styles.css -- -body { - background-color: red; -} --- assets/js/main.js -- -import * as foo from 'mylib'; -console.log("Hello, Main!"); --- assets/js/runner.js -- -console.log("Hello, Runner!"); --- node_modules/mylib/index.js -- -console.log("Hello, My Lib!"); --- layouts/shortcodes/hdx.html -- -{{ $path := .Get "r" }} -{{ $r := or (.Page.Resources.Get $path) (resources.Get $path) }} -{{ $batch := (js.Batch "mybatch") }} -{{ $scriptID := $path | anchorize }} -{{ $instanceID := .Ordinal | string }} -{{ $group := .Page.RelPermalink | anchorize }} -{{ $params := .Params | default dict }} -{{ $export := .Get "export" | default "default" }} -{{ with $batch.Group $group }} - {{ with .Runner "create-elements" }} - {{ .SetOptions (dict "resource" (resources.Get "js/runner.js")) }} - {{ end }} - {{ with .Script $scriptID }} - {{ .SetOptions (dict - "resource" $r - "export" $export - "importContext" (slice $.Page) - ) - }} - {{ end }} - {{ with .Instance $scriptID $instanceID }} - {{ .SetOptions (dict "params" $params) }} - {{ end }} -{{ end }} -hdx-instance: {{ $scriptID }}: {{ $instanceID }}| --- layouts/_default/baseof.html -- -Base. -{{ $batch := (js.Batch "mybatch") }} - {{ with $batch.Config }} - {{ .SetOptions (dict - "params" (dict "id" "config") - "sourceMap" "" - ) - }} -{{ end }} -{{ with (templates.Defer (dict "key" "global")) }} -Defer: -{{ $batch := (js.Batch "mybatch") }} -{{ range $k, $v := $batch.Build.Groups }} - {{ range $kk, $vv := . -}} - {{ $k }}: {{ .RelPermalink }} - {{ end }} -{{ end -}} -{{ end }} -{{ block "main" . }}Main{{ end }} -End. --- layouts/_default/single.html -- -{{ define "main" }} -==> Single Template Content: {{ .Content }}$ -{{ $batch := (js.Batch "mybatch") }} -{{ with $batch.Group "mygroup" }} - {{ with .Runner "run" }} - {{ .SetOptions (dict "resource" (resources.Get "js/runner.js")) }} - {{ end }} - {{ with .Script "main" }} - {{ .SetOptions (dict "resource" (resources.Get "js/main.js") "params" (dict "p1" "param-p1-main" )) }} - {{ end }} - {{ with .Instance "main" "i1" }} - {{ .SetOptions (dict "params" (dict "title" "Instance 1")) }} - {{ end }} -{{ end }} -{{ end }} --- layouts/index.html -- -{{ define "main" }} -Home. -{{ end }} --- content/p1/index.md -- ---- -title: "P1" ---- - -Some content. - -{{< hdx r="p1script.js" myparam="p1-param-1" >}} -{{< hdx r="p1script.js" myparam="p1-param-2" >}} - --- content/p1/p1script.js -- -import * as foo from 'mylib'; -console.lg("Foo", foo); -console.log("P1 Script"); -export default function P1Script() {}; - - -` - files := strings.Replace(filesTemplate, `"sourceMap" ""`, `"sourceMap" "linked"`, 1) - b := hugolib.TestRunning(t, files, hugolib.TestOptWithOSFs()) - b.AssertFileContent("public/mybatch/mygroup.js.map", "main.js", "! ns-hugo") - b.AssertFileContent("public/mybatch/mygroup.js", "sourceMappingURL=mygroup.js.map") - b.AssertFileContent("public/mybatch/p1.js", "sourceMappingURL=p1.js.map") - b.AssertFileContent("public/mybatch/mygroup_run_runner.js", "sourceMappingURL=mygroup_run_runner.js.map") - b.AssertFileContent("public/mybatch/chunk-UQKPPNA6.js", "sourceMappingURL=chunk-UQKPPNA6.js.map") - - checkMap := func(p string, expectLen int) { - s := b.FileContent(p) - sources := esbuild.SourcesFromSourceMap(s) - b.Assert(sources, qt.HasLen, expectLen) - - // Check that all source files exist. - for _, src := range sources { - filename, ok := paths.UrlStringToFilename(src) - b.Assert(ok, qt.IsTrue) - _, err := os.Stat(filename) - b.Assert(err, qt.IsNil) - } - } - - checkMap("public/mybatch/mygroup.js.map", 1) - checkMap("public/mybatch/p1.js.map", 1) - checkMap("public/mybatch/mygroup_run_runner.js.map", 0) - checkMap("public/mybatch/chunk-UQKPPNA6.js.map", 1) -} - -func TestBatchErrorRunnerResourceNotSet(t *testing.T) { - files := strings.Replace(jsBatchFilesTemplate, `(resources.Get "js/runner.js")`, `(resources.Get "js/doesnotexist.js")`, 1) - b, err := hugolib.TestE(t, files, hugolib.TestOptWithOSFs()) - b.Assert(err, qt.IsNotNil) - b.Assert(err.Error(), qt.Contains, `resource not set`) -} - -func TestBatchErrorScriptResourceInAssetsSyntaxError(t *testing.T) { - // Introduce JS syntax error in assets/js/main.js - files := strings.Replace(jsBatchFilesTemplate, `console.log("Hello, Main!");`, `console.log("Hello, Main!"`, 1) - b, err := hugolib.TestE(t, files, hugolib.TestOptWithOSFs()) - b.Assert(err, qt.IsNotNil) - b.Assert(err.Error(), qt.Contains, filepath.FromSlash(`assets/js/main.js:5:0": Expected ")" but found "console"`)) -} - -func TestBatchErrorScriptResourceInBundleSyntaxError(t *testing.T) { - // Introduce JS syntax error in content/p1/p1script.js - files := strings.Replace(jsBatchFilesTemplate, `console.log("P1 Script");`, `console.log("P1 Script"`, 1) - b, err := hugolib.TestE(t, files, hugolib.TestOptWithOSFs()) - b.Assert(err, qt.IsNotNil) - b.Assert(err.Error(), qt.Contains, filepath.FromSlash(`/content/p1/p1script.js:3:0": Expected ")" but found end of file`)) -} - -func TestBatch(t *testing.T) { - files := ` --- hugo.toml -- -disableKinds = ["taxonomy", "term"] -disableLiveReload = true -baseURL = "https://example.com" --- package.json -- -{ - "devDependencies": { - "react": "^18.3.1", - "react-dom": "^18.3.1" - } -} --- assets/js/shims/react.js -- --- assets/js/shims/react-dom.js -- -module.exports = window.ReactDOM; -module.exports = window.React; --- content/mybundle/index.md -- ---- -title: "My Bundle" ---- --- content/mybundle/mybundlestyles.css -- -@import './foo.css'; -@import './bar.css'; -@import './otherbundlestyles.css'; - -.mybundlestyles { - background-color: blue; -} --- content/mybundle/bundlereact.jsx -- -import * as React from "react"; -import './foo.css'; -import './mybundlestyles.css'; -window.React1 = React; - -let text = 'Click me, too!' - -export default function MyBundleButton() { - return ( - - ) -} - --- assets/js/reactrunner.js -- -import * as ReactDOM from 'react-dom/client'; -import * as React from 'react'; - -export default function Run(group) { - for (const module of group.scripts) { - for (const instance of module.instances) { - /* This is a convention in this project. */ - let elId = §§${module.id}-${instance.id}§§; - let el = document.getElementById(elId); - if (!el) { - console.warn(§§Element with id ${elId} not found§§); - continue; - } - const root = ReactDOM.createRoot(el); - const reactEl = React.createElement(module.mod, instance.params); - root.render(reactEl); - } - } -} --- assets/other/otherbundlestyles.css -- -.otherbundlestyles { - background-color: red; -} --- assets/other/foo.css -- -@import './bar.css'; - -.foo { - background-color: blue; -} --- assets/other/bar.css -- -.bar { - background-color: red; -} --- assets/js/button.css -- -button { - background-color: red; -} --- assets/js/bar.css -- -.bar-assets { - background-color: red; -} --- assets/js/helper.js -- -import './bar.css' - -export function helper() { - console.log('helper'); -} - --- assets/js/react1styles_nested.css -- -.react1styles_nested { - background-color: red; -} --- assets/js/react1styles.css -- -@import './react1styles_nested.css'; -.react1styles { - background-color: red; -} --- assets/js/react1.jsx -- -import * as React from "react"; -import './button.css' -import './foo.css' -import './react1styles.css' - -window.React1 = React; - -let text = 'Click me' - -export default function MyButton() { - return ( - - ) -} - --- assets/js/react2.jsx -- -import * as React from "react"; -import { helper } from './helper.js' -import './foo.css' - -window.React2 = React; - -let text = 'Click me, too!' - -export function MyOtherButton() { - return ( - - ) -} --- assets/js/main1.js -- -import * as React from "react"; -import * as params from '@params'; - -console.log('main1.React', React) -console.log('main1.params.id', params.id) - --- assets/js/main2.js -- -import * as React from "react"; -import * as params from '@params'; - -console.log('main2.React', React) -console.log('main2.params.id', params.id) - -export default function Main2() {}; - --- assets/js/main3.js -- -import * as React from "react"; -import * as params from '@params'; -import * as config from '@params/config'; - -console.log('main3.params.id', params.id) -console.log('config.params.id', config.id) - -export default function Main3() {}; - --- layouts/_default/single.html -- -Single. - -{{ $r := .Resources.GetMatch "*.jsx" }} -{{ $batch := (js.Batch "mybundle") }} -{{ $otherCSS := (resources.Match "/other/*.css").Mount "/other" "." }} - {{ with $batch.Config }} - {{ $shims := dict "react" "js/shims/react.js" "react-dom/client" "js/shims/react-dom.js" }} - {{ .SetOptions (dict - "target" "es2018" - "params" (dict "id" "config") - "shims" $shims - ) - }} -{{ end }} -{{ with $batch.Group "reactbatch" }} - {{ with .Script "r3" }} - {{ .SetOptions (dict - "resource" $r - "importContext" (slice $ $otherCSS) - "params" (dict "id" "r3") - ) - }} - {{ end }} - {{ with .Instance "r3" "r2i1" }} - {{ .SetOptions (dict "title" "r2 instance 1")}} - {{ end }} -{{ end }} --- layouts/index.html -- -Home. -{{ with (templates.Defer (dict "key" "global")) }} -{{ $batch := (js.Batch "mybundle") }} -{{ range $k, $v := $batch.Build.Groups }} - {{ range $kk, $vv := . }} - {{ $k }}: {{ $kk }}: {{ .RelPermalink }} - {{ end }} - {{ end }} -{{ end }} -{{ $myContentBundle := site.GetPage "mybundle" }} -{{ $batch := (js.Batch "mybundle") }} -{{ $otherCSS := (resources.Match "/other/*.css").Mount "/other" "." }} -{{ with $batch.Group "mains" }} - {{ with .Script "main1" }} - {{ .SetOptions (dict - "resource" (resources.Get "js/main1.js") - "params" (dict "id" "main1") - ) - }} - {{ end }} - {{ with .Script "main2" }} - {{ .SetOptions (dict - "resource" (resources.Get "js/main2.js") - "params" (dict "id" "main2") - ) - }} - {{ end }} - {{ with .Script "main3" }} - {{ .SetOptions (dict - "resource" (resources.Get "js/main3.js") - ) - }} - {{ end }} -{{ with .Instance "main1" "m1i1" }}{{ .SetOptions (dict "params" (dict "title" "Main1 Instance 1"))}}{{ end }} -{{ with .Instance "main1" "m1i2" }}{{ .SetOptions (dict "params" (dict "title" "Main1 Instance 2"))}}{{ end }} -{{ end }} -{{ with $batch.Group "reactbatch" }} - {{ with .Runner "reactrunner" }} - {{ .SetOptions ( dict "resource" (resources.Get "js/reactrunner.js") )}} - {{ end }} - {{ with .Script "r1" }} - {{ .SetOptions (dict - "resource" (resources.Get "js/react1.jsx") - "importContext" (slice $myContentBundle $otherCSS) - "params" (dict "id" "r1") - ) - }} - {{ end }} - {{ with .Instance "r1" "i1" }}{{ .SetOptions (dict "params" (dict "title" "Instance 1"))}}{{ end }} - {{ with .Instance "r1" "i2" }}{{ .SetOptions (dict "params" (dict "title" "Instance 2"))}}{{ end }} - {{ with .Script "r2" }} - {{ .SetOptions (dict - "resource" (resources.Get "js/react2.jsx") - "export" "MyOtherButton" - "importContext" $otherCSS - "params" (dict "id" "r2") - ) - }} - {{ end }} - {{ with .Instance "r2" "i1" }}{{ .SetOptions (dict "params" (dict "title" "Instance 2-1"))}}{{ end }} -{{ end }} - -` - - b := hugolib.NewIntegrationTestBuilder( - hugolib.IntegrationTestConfig{ - T: t, - NeedsOsFS: true, - NeedsNpmInstall: true, - TxtarString: files, - Running: true, - LogLevel: logg.LevelWarn, - // PrintAndKeepTempDir: true, - }).Build() - - b.AssertFileContent("public/index.html", - "mains: 0: /mybundle/mains.js", - "reactbatch: 2: /mybundle/reactbatch.css", - ) - - b.AssertFileContent("public/mybundle/reactbatch.css", - ".bar {", - ) - - // Verify params resolution. - b.AssertFileContent("public/mybundle/mains.js", - ` -var id = "main1"; -console.log("main1.params.id", id); -var id2 = "main2"; -console.log("main2.params.id", id2); - - -# Params from top level config. -var id3 = "config"; -console.log("main3.params.id", void 0); -console.log("config.params.id", id3); -`) - - b.EditFileReplaceAll("content/mybundle/mybundlestyles.css", ".mybundlestyles", ".mybundlestyles-edit").Build() - b.AssertFileContent("public/mybundle/reactbatch.css", ".mybundlestyles-edit {") - - b.EditFileReplaceAll("assets/other/bar.css", ".bar {", ".bar-edit {").Build() - b.AssertFileContent("public/mybundle/reactbatch.css", ".bar-edit {") - - b.EditFileReplaceAll("assets/other/bar.css", ".bar-edit {", ".bar-edit2 {").Build() - b.AssertFileContent("public/mybundle/reactbatch.css", ".bar-edit2 {") -} diff --git a/internal/js/esbuild/build.go b/internal/js/esbuild/build.go deleted file mode 100644 index 33b91eafc..000000000 --- a/internal/js/esbuild/build.go +++ /dev/null @@ -1,236 +0,0 @@ -// 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 esbuild provides functions for building JavaScript resources. -package esbuild - -import ( - "errors" - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/evanw/esbuild/pkg/api" - "github.com/gohugoio/hugo/common/herrors" - "github.com/gohugoio/hugo/common/hugio" - "github.com/gohugoio/hugo/common/text" - "github.com/gohugoio/hugo/hugofs" - "github.com/gohugoio/hugo/hugolib/filesystems" - "github.com/gohugoio/hugo/identity" - "github.com/gohugoio/hugo/resources" -) - -// NewBuildClient creates a new BuildClient. -func NewBuildClient(fs *filesystems.SourceFilesystem, rs *resources.Spec) *BuildClient { - return &BuildClient{ - rs: rs, - sfs: fs, - } -} - -// BuildClient is a client for building JavaScript resources using esbuild. -type BuildClient struct { - rs *resources.Spec - sfs *filesystems.SourceFilesystem -} - -// Build builds the given JavaScript resources using esbuild with the given options. -func (c *BuildClient) Build(opts Options) (api.BuildResult, error) { - dependencyManager := opts.DependencyManager - if dependencyManager == nil { - dependencyManager = identity.NopManager - } - - opts.OutDir = c.rs.AbsPublishDir - opts.ResolveDir = c.rs.Cfg.BaseConfig().WorkingDir // where node_modules gets resolved - opts.AbsWorkingDir = opts.ResolveDir - opts.TsConfig = c.rs.ResolveJSConfigFile("tsconfig.json") - assetsResolver := newFSResolver(c.rs.Assets.Fs) - - if err := opts.validate(); err != nil { - return api.BuildResult{}, err - } - - if err := opts.compile(); err != nil { - return api.BuildResult{}, err - } - - var err error - opts.compiled.Plugins, err = createBuildPlugins(c.rs, assetsResolver, dependencyManager, opts) - if err != nil { - return api.BuildResult{}, err - } - - if opts.Inject != nil { - // Resolve the absolute filenames. - for i, ext := range opts.Inject { - impPath := filepath.FromSlash(ext) - if filepath.IsAbs(impPath) { - return api.BuildResult{}, fmt.Errorf("inject: absolute paths not supported, must be relative to /assets") - } - - m := assetsResolver.resolveComponent(impPath) - - if m == nil { - return api.BuildResult{}, fmt.Errorf("inject: file %q not found", ext) - } - - opts.Inject[i] = m.Filename - - } - - opts.compiled.Inject = opts.Inject - - } - - result := api.Build(opts.compiled) - - if len(result.Errors) > 0 { - createErr := func(msg api.Message) error { - if msg.Location == nil { - return errors.New(msg.Text) - } - var ( - contentr hugio.ReadSeekCloser - errorMessage string - loc = msg.Location - errorPath = loc.File - err error - ) - - var resolvedError *ErrorMessageResolved - - if opts.ErrorMessageResolveFunc != nil { - resolvedError = opts.ErrorMessageResolveFunc(msg) - } - - if resolvedError == nil { - if errorPath == stdinImporter { - errorPath = opts.StdinSourcePath - } - - errorMessage = msg.Text - - var namespace string - for _, ns := range hugoNamespaces { - if strings.HasPrefix(errorPath, ns) { - namespace = ns - break - } - } - - if namespace != "" { - namespace += ":" - errorMessage = strings.ReplaceAll(errorMessage, namespace, "") - errorPath = strings.TrimPrefix(errorPath, namespace) - contentr, err = hugofs.Os.Open(errorPath) - } else { - var fi os.FileInfo - fi, err = c.sfs.Fs.Stat(errorPath) - if err == nil { - m := fi.(hugofs.FileMetaInfo).Meta() - errorPath = m.Filename - contentr, err = m.Open() - } - } - } else { - contentr = resolvedError.Content - errorPath = resolvedError.Path - errorMessage = resolvedError.Message - } - - if contentr != nil { - defer contentr.Close() - } - - if err == nil { - fe := herrors. - NewFileErrorFromName(errors.New(errorMessage), errorPath). - UpdatePosition(text.Position{Offset: -1, LineNumber: loc.Line, ColumnNumber: loc.Column}). - UpdateContent(contentr, nil) - - return fe - } - - return fmt.Errorf("%s", errorMessage) - } - - var errors []error - - for _, msg := range result.Errors { - errors = append(errors, createErr(msg)) - } - - // Return 1, log the rest. - for i, err := range errors { - if i > 0 { - c.rs.Logger.Errorf("js.Build failed: %s", err) - } - } - - return result, errors[0] - } - - inOutputPathToAbsFilename := opts.ResolveSourceMapSource - opts.ResolveSourceMapSource = func(s string) string { - if inOutputPathToAbsFilename != nil { - if filename := inOutputPathToAbsFilename(s); filename != "" { - return filename - } - } - - if m := assetsResolver.resolveComponent(s); m != nil { - return m.Filename - } - - return "" - } - - for i, o := range result.OutputFiles { - if err := fixOutputFile(&o, func(s string) string { - if s == "" { - return opts.ResolveSourceMapSource(opts.StdinSourcePath) - } - var isNsHugo bool - if strings.HasPrefix(s, "ns-hugo") { - isNsHugo = true - idxColon := strings.Index(s, ":") - s = s[idxColon+1:] - } - - if !strings.HasPrefix(s, PrefixHugoVirtual) { - if !filepath.IsAbs(s) { - s = filepath.Join(opts.OutDir, s) - } - } - - if isNsHugo { - if ss := opts.ResolveSourceMapSource(s); ss != "" { - if strings.HasPrefix(ss, PrefixHugoMemory) { - // File not on disk, mark it for removal from the sources slice. - return "" - } - return ss - } - return "" - } - return s - }); err != nil { - return result, err - } - result.OutputFiles[i] = o - } - - return result, nil -} diff --git a/internal/js/esbuild/helpers.go b/internal/js/esbuild/helpers.go deleted file mode 100644 index b4cb565b8..000000000 --- a/internal/js/esbuild/helpers.go +++ /dev/null @@ -1,15 +0,0 @@ -// 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 esbuild provides functions for building JavaScript resources. -package esbuild diff --git a/internal/js/esbuild/options.go b/internal/js/esbuild/options.go deleted file mode 100644 index 21f9e31cd..000000000 --- a/internal/js/esbuild/options.go +++ /dev/null @@ -1,411 +0,0 @@ -// 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 esbuild - -import ( - "encoding/json" - "fmt" - "path/filepath" - "strings" - - "github.com/gohugoio/hugo/common/hugio" - "github.com/gohugoio/hugo/common/maps" - "github.com/gohugoio/hugo/common/paths" - "github.com/gohugoio/hugo/identity" - - "github.com/evanw/esbuild/pkg/api" - - "github.com/gohugoio/hugo/media" - "github.com/mitchellh/mapstructure" -) - -var ( - nameTarget = map[string]api.Target{ - "": api.ESNext, - "esnext": api.ESNext, - "es5": api.ES5, - "es6": api.ES2015, - "es2015": api.ES2015, - "es2016": api.ES2016, - "es2017": api.ES2017, - "es2018": api.ES2018, - "es2019": api.ES2019, - "es2020": api.ES2020, - "es2021": api.ES2021, - "es2022": api.ES2022, - "es2023": api.ES2023, - "es2024": api.ES2024, - } - - // source names: https://github.com/evanw/esbuild/blob/9eca46464ed5615cb36a3beb3f7a7b9a8ffbe7cf/internal/config/config.go#L208 - nameLoader = map[string]api.Loader{ - "none": api.LoaderNone, - "base64": api.LoaderBase64, - "binary": api.LoaderBinary, - "copy": api.LoaderFile, - "css": api.LoaderCSS, - "dataurl": api.LoaderDataURL, - "default": api.LoaderDefault, - "empty": api.LoaderEmpty, - "file": api.LoaderFile, - "global-css": api.LoaderGlobalCSS, - "js": api.LoaderJS, - "json": api.LoaderJSON, - "jsx": api.LoaderJSX, - "local-css": api.LoaderLocalCSS, - "text": api.LoaderText, - "ts": api.LoaderTS, - "tsx": api.LoaderTSX, - } -) - -// DecodeExternalOptions decodes the given map into ExternalOptions. -func DecodeExternalOptions(m map[string]any) (ExternalOptions, error) { - opts := ExternalOptions{ - SourcesContent: true, - } - - if err := mapstructure.WeakDecode(m, &opts); err != nil { - return opts, err - } - - if opts.TargetPath != "" { - opts.TargetPath = paths.ToSlashTrimLeading(opts.TargetPath) - } - - opts.Target = strings.ToLower(opts.Target) - opts.Format = strings.ToLower(opts.Format) - - return opts, nil -} - -// ErrorMessageResolved holds a resolved error message. -type ErrorMessageResolved struct { - Path string - Message string - Content hugio.ReadSeekCloser -} - -// ExternalOptions holds user facing options for the js.Build template function. -type ExternalOptions struct { - // If not set, the source path will be used as the base target path. - // Note that the target path's extension may change if the target MIME type - // is different, e.g. when the source is TypeScript. - TargetPath string - - // Whether to minify to output. - Minify bool - - // One of "inline", "external", "linked" or "none". - SourceMap string - - SourcesContent bool - - // The language target. - // One of: es2015, es2016, es2017, es2018, es2019, es2020 or esnext. - // Default is esnext. - Target string - - // The output format. - // One of: iife, cjs, esm - // Default is to esm. - Format string - - // One of browser, node, neutral. - // Default is browser. - // See https://esbuild.github.io/api/#platform - Platform string - - // External dependencies, e.g. "react". - Externals []string - - // This option allows you to automatically replace a global variable with an import from another file. - // The filenames must be relative to /assets. - // See https://esbuild.github.io/api/#inject - Inject []string - - // User defined symbols. - Defines map[string]any - - // This tells esbuild to edit your source code before building to drop certain constructs. - // See https://esbuild.github.io/api/#drop - Drop string - - // Maps a component import to another. - Shims map[string]string - - // Configuring a loader for a given file type lets you load that file type with an - // import statement or a require call. For example, configuring the .png file extension - // to use the data URL loader means importing a .png file gives you a data URL - // containing the contents of that image - // - // See https://esbuild.github.io/api/#loader - Loaders map[string]string - - // User defined params. Will be marshaled to JSON and available as "@params", e.g. - // import * as params from '@params'; - Params any - - // What to use instead of React.createElement. - JSXFactory string - - // What to use instead of React.Fragment. - JSXFragment string - - // What to do about JSX syntax. - // See https://esbuild.github.io/api/#jsx - JSX string - - // Which library to use to automatically import JSX helper functions from. Only works if JSX is set to automatic. - // See https://esbuild.github.io/api/#jsx-import-source - JSXImportSource string - - // There is/was a bug in WebKit with severe performance issue with the tracking - // of TDZ checks in JavaScriptCore. - // - // Enabling this flag removes the TDZ and `const` assignment checks and - // may improve performance of larger JS codebases until the WebKit fix - // is in widespread use. - // - // See https://bugs.webkit.org/show_bug.cgi?id=199866 - // Deprecated: This no longer have any effect and will be removed. - // TODO(bep) remove. See https://github.com/evanw/esbuild/commit/869e8117b499ca1dbfc5b3021938a53ffe934dba - AvoidTDZ bool -} - -// InternalOptions holds internal options for the js.Build template function. -type InternalOptions struct { - MediaType media.Type - OutDir string - Contents string - SourceDir string - ResolveDir string - AbsWorkingDir string - Metafile bool - - StdinSourcePath string - - DependencyManager identity.Manager - - Stdin bool // Set to true to pass in the entry point as a byte slice. - Splitting bool - TsConfig string - EntryPoints []string - ImportOnResolveFunc func(string, api.OnResolveArgs) string - ImportOnLoadFunc func(api.OnLoadArgs) string - ImportParamsOnLoadFunc func(args api.OnLoadArgs) json.RawMessage - ErrorMessageResolveFunc func(api.Message) *ErrorMessageResolved - ResolveSourceMapSource func(string) string // Used to resolve paths in error source maps. -} - -// Options holds the options passed to Build. -type Options struct { - ExternalOptions - InternalOptions - - compiled api.BuildOptions -} - -func (opts *Options) compile() (err error) { - target, found := nameTarget[opts.Target] - if !found { - err = fmt.Errorf("invalid target: %q", opts.Target) - return - } - - var loaders map[string]api.Loader - if opts.Loaders != nil { - loaders = make(map[string]api.Loader) - for k, v := range opts.Loaders { - loader, found := nameLoader[v] - if !found { - err = fmt.Errorf("invalid loader: %q", v) - return - } - loaders[k] = loader - } - } - - mediaType := opts.MediaType - if mediaType.IsZero() { - mediaType = media.Builtin.JavascriptType - } - - var loader api.Loader - switch mediaType.SubType { - case media.Builtin.JavascriptType.SubType: - loader = api.LoaderJS - case media.Builtin.TypeScriptType.SubType: - loader = api.LoaderTS - case media.Builtin.TSXType.SubType: - loader = api.LoaderTSX - case media.Builtin.JSXType.SubType: - loader = api.LoaderJSX - default: - err = fmt.Errorf("unsupported Media Type: %q", opts.MediaType) - return - } - - var format api.Format - // One of: iife, cjs, esm - switch opts.Format { - case "", "iife": - format = api.FormatIIFE - case "esm": - format = api.FormatESModule - case "cjs": - format = api.FormatCommonJS - default: - err = fmt.Errorf("unsupported script output format: %q", opts.Format) - return - } - - var jsx api.JSX - switch opts.JSX { - case "", "transform": - jsx = api.JSXTransform - case "preserve": - jsx = api.JSXPreserve - case "automatic": - jsx = api.JSXAutomatic - default: - err = fmt.Errorf("unsupported jsx type: %q", opts.JSX) - return - } - - var platform api.Platform - switch opts.Platform { - case "", "browser": - platform = api.PlatformBrowser - case "node": - platform = api.PlatformNode - case "neutral": - platform = api.PlatformNeutral - default: - err = fmt.Errorf("unsupported platform type: %q", opts.Platform) - return - } - - var defines map[string]string - if opts.Defines != nil { - defines = maps.ToStringMapString(opts.Defines) - } - - var drop api.Drop - switch opts.Drop { - case "": - case "console": - drop = api.DropConsole - case "debugger": - drop = api.DropDebugger - default: - err = fmt.Errorf("unsupported drop type: %q", opts.Drop) - } - - // By default we only need to specify outDir and no outFile - outDir := opts.OutDir - outFile := "" - var sourceMap api.SourceMap - switch opts.SourceMap { - case "inline": - sourceMap = api.SourceMapInline - case "external": - sourceMap = api.SourceMapExternal - case "linked": - sourceMap = api.SourceMapLinked - case "", "none": - sourceMap = api.SourceMapNone - default: - err = fmt.Errorf("unsupported sourcemap type: %q", opts.SourceMap) - return - } - - sourcesContent := api.SourcesContentInclude - if !opts.SourcesContent { - sourcesContent = api.SourcesContentExclude - } - - opts.compiled = api.BuildOptions{ - Outfile: outFile, - Bundle: true, - Metafile: opts.Metafile, - AbsWorkingDir: opts.AbsWorkingDir, - - Target: target, - Format: format, - Platform: platform, - Sourcemap: sourceMap, - SourcesContent: sourcesContent, - - Loader: loaders, - - MinifyWhitespace: opts.Minify, - MinifyIdentifiers: opts.Minify, - MinifySyntax: opts.Minify, - - Outdir: outDir, - Splitting: opts.Splitting, - - Define: defines, - External: opts.Externals, - Drop: drop, - - JSXFactory: opts.JSXFactory, - JSXFragment: opts.JSXFragment, - - JSX: jsx, - JSXImportSource: opts.JSXImportSource, - - Tsconfig: opts.TsConfig, - - EntryPoints: opts.EntryPoints, - } - - if opts.Stdin { - // This makes ESBuild pass `stdin` as the Importer to the import. - opts.compiled.Stdin = &api.StdinOptions{ - Contents: opts.Contents, - ResolveDir: opts.ResolveDir, - Loader: loader, - } - } - return -} - -func (o Options) loaderFromFilename(filename string) api.Loader { - ext := filepath.Ext(filename) - if optsLoaders := o.compiled.Loader; optsLoaders != nil { - if l, found := optsLoaders[ext]; found { - return l - } - } - l, found := extensionToLoaderMap[ext] - if found { - return l - } - return api.LoaderJS -} - -func (opts *Options) validate() error { - if opts.ImportOnResolveFunc != nil && opts.ImportOnLoadFunc == nil { - return fmt.Errorf("ImportOnLoadFunc must be set if ImportOnResolveFunc is set") - } - if opts.ImportOnResolveFunc == nil && opts.ImportOnLoadFunc != nil { - return fmt.Errorf("ImportOnResolveFunc must be set if ImportOnLoadFunc is set") - } - if opts.AbsWorkingDir == "" { - return fmt.Errorf("AbsWorkingDir must be set") - } - return nil -} diff --git a/internal/js/esbuild/options_test.go b/internal/js/esbuild/options_test.go deleted file mode 100644 index e92c3bea6..000000000 --- a/internal/js/esbuild/options_test.go +++ /dev/null @@ -1,262 +0,0 @@ -// 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 esbuild - -import ( - "testing" - - "github.com/gohugoio/hugo/media" - - "github.com/evanw/esbuild/pkg/api" - - qt "github.com/frankban/quicktest" -) - -func TestToBuildOptions(t *testing.T) { - c := qt.New(t) - - opts := Options{ - InternalOptions: InternalOptions{ - MediaType: media.Builtin.JavascriptType, - Stdin: true, - }, - } - - c.Assert(opts.compile(), qt.IsNil) - c.Assert(opts.compiled, qt.DeepEquals, api.BuildOptions{ - Bundle: true, - Target: api.ESNext, - Format: api.FormatIIFE, - Platform: api.PlatformBrowser, - SourcesContent: 1, - Stdin: &api.StdinOptions{ - Loader: api.LoaderJS, - }, - }) - - opts = Options{ - ExternalOptions: ExternalOptions{ - Target: "es2018", - Format: "cjs", - Minify: true, - AvoidTDZ: true, - }, - InternalOptions: InternalOptions{ - MediaType: media.Builtin.JavascriptType, - Stdin: true, - }, - } - - c.Assert(opts.compile(), qt.IsNil) - c.Assert(opts.compiled, qt.DeepEquals, api.BuildOptions{ - Bundle: true, - Target: api.ES2018, - Format: api.FormatCommonJS, - Platform: api.PlatformBrowser, - SourcesContent: 1, - MinifyIdentifiers: true, - MinifySyntax: true, - MinifyWhitespace: true, - Stdin: &api.StdinOptions{ - Loader: api.LoaderJS, - }, - }) - - opts = Options{ - ExternalOptions: ExternalOptions{ - Target: "es2018", Format: "cjs", Minify: true, - SourceMap: "inline", - }, - InternalOptions: InternalOptions{ - MediaType: media.Builtin.JavascriptType, - Stdin: true, - }, - } - - c.Assert(opts.compile(), qt.IsNil) - c.Assert(opts.compiled, qt.DeepEquals, api.BuildOptions{ - Bundle: true, - Target: api.ES2018, - Format: api.FormatCommonJS, - Platform: api.PlatformBrowser, - MinifyIdentifiers: true, - MinifySyntax: true, - MinifyWhitespace: true, - SourcesContent: 1, - Sourcemap: api.SourceMapInline, - Stdin: &api.StdinOptions{ - Loader: api.LoaderJS, - }, - }) - - opts = Options{ - ExternalOptions: ExternalOptions{ - Target: "es2018", Format: "cjs", Minify: true, - SourceMap: "inline", - }, - InternalOptions: InternalOptions{ - MediaType: media.Builtin.JavascriptType, - Stdin: true, - }, - } - - c.Assert(opts.compile(), qt.IsNil) - c.Assert(opts.compiled, qt.DeepEquals, api.BuildOptions{ - Bundle: true, - Target: api.ES2018, - Format: api.FormatCommonJS, - Platform: api.PlatformBrowser, - MinifyIdentifiers: true, - MinifySyntax: true, - MinifyWhitespace: true, - Sourcemap: api.SourceMapInline, - SourcesContent: 1, - Stdin: &api.StdinOptions{ - Loader: api.LoaderJS, - }, - }) - - opts = Options{ - ExternalOptions: ExternalOptions{ - Target: "es2018", Format: "cjs", Minify: true, - SourceMap: "external", - }, - InternalOptions: InternalOptions{ - MediaType: media.Builtin.JavascriptType, - Stdin: true, - }, - } - - c.Assert(opts.compile(), qt.IsNil) - c.Assert(opts.compiled, qt.DeepEquals, api.BuildOptions{ - Bundle: true, - Target: api.ES2018, - Format: api.FormatCommonJS, - Platform: api.PlatformBrowser, - MinifyIdentifiers: true, - MinifySyntax: true, - MinifyWhitespace: true, - Sourcemap: api.SourceMapExternal, - SourcesContent: 1, - Stdin: &api.StdinOptions{ - Loader: api.LoaderJS, - }, - }) - - opts = Options{ - ExternalOptions: ExternalOptions{ - JSX: "automatic", JSXImportSource: "preact", - }, - InternalOptions: InternalOptions{ - MediaType: media.Builtin.JavascriptType, - Stdin: true, - }, - } - - c.Assert(opts.compile(), qt.IsNil) - c.Assert(opts.compiled, qt.DeepEquals, api.BuildOptions{ - Bundle: true, - Target: api.ESNext, - Format: api.FormatIIFE, - Platform: api.PlatformBrowser, - SourcesContent: 1, - Stdin: &api.StdinOptions{ - Loader: api.LoaderJS, - }, - JSX: api.JSXAutomatic, - JSXImportSource: "preact", - }) - - opts = Options{ - ExternalOptions: ExternalOptions{ - Drop: "console", - }, - } - c.Assert(opts.compile(), qt.IsNil) - c.Assert(opts.compiled.Drop, qt.Equals, api.DropConsole) - opts = Options{ - ExternalOptions: ExternalOptions{ - Drop: "debugger", - }, - } - c.Assert(opts.compile(), qt.IsNil) - c.Assert(opts.compiled.Drop, qt.Equals, api.DropDebugger) - - opts = Options{ - ExternalOptions: ExternalOptions{ - Drop: "adsfadsf", - }, - } - c.Assert(opts.compile(), qt.ErrorMatches, `unsupported drop type: "adsfadsf"`) -} - -func TestToBuildOptionsTarget(t *testing.T) { - c := qt.New(t) - - for _, test := range []struct { - target string - expect api.Target - }{ - {"es2015", api.ES2015}, - {"es2016", api.ES2016}, - {"es2017", api.ES2017}, - {"es2018", api.ES2018}, - {"es2019", api.ES2019}, - {"es2020", api.ES2020}, - {"es2021", api.ES2021}, - {"es2022", api.ES2022}, - {"es2023", api.ES2023}, - {"", api.ESNext}, - {"esnext", api.ESNext}, - } { - c.Run(test.target, func(c *qt.C) { - opts := Options{ - ExternalOptions: ExternalOptions{ - Target: test.target, - }, - InternalOptions: InternalOptions{ - MediaType: media.Builtin.JavascriptType, - }, - } - - c.Assert(opts.compile(), qt.IsNil) - c.Assert(opts.compiled.Target, qt.Equals, test.expect) - }) - } -} - -func TestDecodeExternalOptions(t *testing.T) { - c := qt.New(t) - m := map[string]any{ - "platform": "node", - } - ext, err := DecodeExternalOptions(m) - c.Assert(err, qt.IsNil) - c.Assert(ext, qt.DeepEquals, ExternalOptions{ - SourcesContent: true, - Platform: "node", - }) - - opts := Options{ - ExternalOptions: ext, - } - c.Assert(opts.compile(), qt.IsNil) - c.Assert(opts.compiled, qt.DeepEquals, api.BuildOptions{ - Bundle: true, - Target: api.ESNext, - Format: api.FormatIIFE, - Platform: api.PlatformNode, - SourcesContent: api.SourcesContentInclude, - }) -} diff --git a/internal/js/esbuild/resolve.go b/internal/js/esbuild/resolve.go deleted file mode 100644 index a2516dbd2..000000000 --- a/internal/js/esbuild/resolve.go +++ /dev/null @@ -1,323 +0,0 @@ -// 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 esbuild - -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/evanw/esbuild/pkg/api" - "github.com/gohugoio/hugo/common/maps" - "github.com/gohugoio/hugo/hugofs" - "github.com/gohugoio/hugo/identity" - "github.com/gohugoio/hugo/resources" - "github.com/gohugoio/hugo/resources/resource" - "github.com/spf13/afero" - "slices" -) - -const ( - NsHugoImport = "ns-hugo-imp" - NsHugoImportResolveFunc = "ns-hugo-imp-func" - nsHugoParams = "ns-hugo-params" - pathHugoConfigParams = "@params/config" - - stdinImporter = "" -) - -var hugoNamespaces = []string{NsHugoImport, NsHugoImportResolveFunc, nsHugoParams} - -const ( - PrefixHugoVirtual = "__hu_v" - PrefixHugoMemory = "__hu_m" -) - -var extensionToLoaderMap = map[string]api.Loader{ - ".js": api.LoaderJS, - ".mjs": api.LoaderJS, - ".cjs": api.LoaderJS, - ".jsx": api.LoaderJSX, - ".ts": api.LoaderTS, - ".tsx": api.LoaderTSX, - ".css": api.LoaderCSS, - ".json": api.LoaderJSON, - ".txt": api.LoaderText, -} - -// This is a common sub-set of ESBuild's default extensions. -// We assume that imports of JSON, CSS etc. will be using their full -// name with extension. -var commonExtensions = []string{".js", ".ts", ".tsx", ".jsx"} - -// ResolveComponent resolves a component using the given resolver. -func ResolveComponent[T any](impPath string, resolve func(string) (v T, found, isDir bool)) (v T, found bool) { - findFirst := func(base string) (v T, found, isDir bool) { - for _, ext := range commonExtensions { - if strings.HasSuffix(impPath, ext) { - // Import of foo.js.js need the full name. - continue - } - if v, found, isDir = resolve(base + ext); found { - return - } - } - - // Not found. - return - } - - // We need to check if this is a regular file imported without an extension. - // There may be ambiguous situations where both foo.js and foo/index.js exists. - // This import order is in line with both how Node and ESBuild's native - // import resolver works. - - // It may be a regular file imported without an extension, e.g. - // foo or foo/index. - v, found, _ = findFirst(impPath) - if found { - return v, found - } - - base := filepath.Base(impPath) - if base == "index" { - // try index.esm.js etc. - v, found, _ = findFirst(impPath + ".esm") - if found { - return v, found - } - } - - // Check the path as is. - var isDir bool - v, found, isDir = resolve(impPath) - if found && isDir { - v, found, _ = findFirst(filepath.Join(impPath, "index")) - if !found { - v, found, _ = findFirst(filepath.Join(impPath, "index.esm")) - } - } - - if !found && strings.HasSuffix(base, ".js") { - v, found, _ = findFirst(strings.TrimSuffix(impPath, ".js")) - } - - return -} - -// ResolveResource resolves a resource using the given resourceGetter. -func ResolveResource(impPath string, resourceGetter resource.ResourceGetter) (r resource.Resource) { - resolve := func(name string) (v resource.Resource, found, isDir bool) { - r := resourceGetter.Get(name) - return r, r != nil, false - } - r, found := ResolveComponent(impPath, resolve) - if !found { - return nil - } - return r -} - -func newFSResolver(fs afero.Fs) *fsResolver { - return &fsResolver{fs: fs, resolved: maps.NewCache[string, *hugofs.FileMeta]()} -} - -type fsResolver struct { - fs afero.Fs - resolved *maps.Cache[string, *hugofs.FileMeta] -} - -func (r *fsResolver) resolveComponent(impPath string) *hugofs.FileMeta { - v, _ := r.resolved.GetOrCreate(impPath, func() (*hugofs.FileMeta, error) { - resolve := func(name string) (*hugofs.FileMeta, bool, bool) { - if fi, err := r.fs.Stat(name); err == nil { - return fi.(hugofs.FileMetaInfo).Meta(), true, fi.IsDir() - } - return nil, false, false - } - v, _ := ResolveComponent(impPath, resolve) - return v, nil - }) - return v -} - -func createBuildPlugins(rs *resources.Spec, assetsResolver *fsResolver, depsManager identity.Manager, opts Options) ([]api.Plugin, error) { - fs := rs.Assets - - resolveImport := func(args api.OnResolveArgs) (api.OnResolveResult, error) { - impPath := args.Path - shimmed := false - if opts.Shims != nil { - override, found := opts.Shims[impPath] - if found { - impPath = override - shimmed = true - } - } - - if slices.Contains(opts.Externals, impPath) { - return api.OnResolveResult{ - Path: impPath, - External: true, - }, nil - } - - if opts.ImportOnResolveFunc != nil { - if s := opts.ImportOnResolveFunc(impPath, args); s != "" { - return api.OnResolveResult{Path: s, Namespace: NsHugoImportResolveFunc}, nil - } - } - - importer := args.Importer - - isStdin := importer == stdinImporter - var relDir string - if !isStdin { - if strings.HasPrefix(importer, PrefixHugoVirtual) { - relDir = filepath.Dir(strings.TrimPrefix(importer, PrefixHugoVirtual)) - } else { - rel, found := fs.MakePathRelative(importer, true) - - if !found { - if shimmed { - relDir = opts.SourceDir - } else { - // Not in any of the /assets folders. - // This is an import from a node_modules, let - // ESBuild resolve this. - return api.OnResolveResult{}, nil - } - } else { - relDir = filepath.Dir(rel) - } - } - } else { - relDir = opts.SourceDir - } - - // Imports not starting with a "." is assumed to live relative to /assets. - // Hugo makes no assumptions about the directory structure below /assets. - if relDir != "" && strings.HasPrefix(impPath, ".") { - impPath = filepath.Join(relDir, impPath) - } - - m := assetsResolver.resolveComponent(impPath) - - if m != nil { - depsManager.AddIdentity(m.PathInfo) - - // Store the source root so we can create a jsconfig.json - // to help IntelliSense when the build is done. - // This should be a small number of elements, and when - // in server mode, we may get stale entries on renames etc., - // but that shouldn't matter too much. - rs.JSConfigBuilder.AddSourceRoot(m.SourceRoot) - return api.OnResolveResult{Path: m.Filename, Namespace: NsHugoImport}, nil - } - - // Fall back to ESBuild's resolve. - return api.OnResolveResult{}, nil - } - - importResolver := api.Plugin{ - Name: "hugo-import-resolver", - Setup: func(build api.PluginBuild) { - build.OnResolve(api.OnResolveOptions{Filter: `.*`}, - func(args api.OnResolveArgs) (api.OnResolveResult, error) { - return resolveImport(args) - }) - build.OnLoad(api.OnLoadOptions{Filter: `.*`, Namespace: NsHugoImport}, - func(args api.OnLoadArgs) (api.OnLoadResult, error) { - b, err := os.ReadFile(args.Path) - if err != nil { - return api.OnLoadResult{}, fmt.Errorf("failed to read %q: %w", args.Path, err) - } - c := string(b) - - return api.OnLoadResult{ - // See https://github.com/evanw/esbuild/issues/502 - // This allows all modules to resolve dependencies - // in the main project's node_modules. - ResolveDir: opts.ResolveDir, - Contents: &c, - Loader: opts.loaderFromFilename(args.Path), - }, nil - }) - build.OnLoad(api.OnLoadOptions{Filter: `.*`, Namespace: NsHugoImportResolveFunc}, - func(args api.OnLoadArgs) (api.OnLoadResult, error) { - c := opts.ImportOnLoadFunc(args) - if c == "" { - return api.OnLoadResult{}, fmt.Errorf("ImportOnLoadFunc failed to resolve %q", args.Path) - } - - return api.OnLoadResult{ - ResolveDir: opts.ResolveDir, - Contents: &c, - Loader: opts.loaderFromFilename(args.Path), - }, nil - }) - }, - } - - params := opts.Params - if params == nil { - // This way @params will always resolve to something. - params = make(map[string]any) - } - - b, err := json.Marshal(params) - if err != nil { - return nil, fmt.Errorf("failed to marshal params: %w", err) - } - - paramsPlugin := api.Plugin{ - Name: "hugo-params-plugin", - Setup: func(build api.PluginBuild) { - build.OnResolve(api.OnResolveOptions{Filter: `^@params(/config)?$`}, - func(args api.OnResolveArgs) (api.OnResolveResult, error) { - resolvedPath := args.Importer - - if args.Path == pathHugoConfigParams { - resolvedPath = pathHugoConfigParams - } - - return api.OnResolveResult{ - Path: resolvedPath, - Namespace: nsHugoParams, - }, nil - }) - build.OnLoad(api.OnLoadOptions{Filter: `.*`, Namespace: nsHugoParams}, - func(args api.OnLoadArgs) (api.OnLoadResult, error) { - bb := b - if args.Path != pathHugoConfigParams && opts.ImportParamsOnLoadFunc != nil { - bb = opts.ImportParamsOnLoadFunc(args) - } - s := string(bb) - - if s == "" { - s = "{}" - } - - return api.OnLoadResult{ - Contents: &s, - Loader: api.LoaderJSON, - }, nil - }) - }, - } - - return []api.Plugin{importResolver, paramsPlugin}, nil -} diff --git a/internal/js/esbuild/resolve_test.go b/internal/js/esbuild/resolve_test.go deleted file mode 100644 index 86e3138f2..000000000 --- a/internal/js/esbuild/resolve_test.go +++ /dev/null @@ -1,86 +0,0 @@ -// 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 esbuild - -import ( - "path" - "path/filepath" - "testing" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/config/testconfig" - "github.com/gohugoio/hugo/hugofs" - "github.com/gohugoio/hugo/hugolib/filesystems" - "github.com/gohugoio/hugo/hugolib/paths" - "github.com/spf13/afero" -) - -func TestResolveComponentInAssets(t *testing.T) { - c := qt.New(t) - - for _, test := range []struct { - name string - files []string - impPath string - expect string - }{ - {"Basic, extension", []string{"foo.js", "bar.js"}, "foo.js", "foo.js"}, - {"Basic, no extension", []string{"foo.js", "bar.js"}, "foo", "foo.js"}, - {"Basic, no extension, typescript", []string{"foo.ts", "bar.js"}, "foo", "foo.ts"}, - {"Not found", []string{"foo.js", "bar.js"}, "moo.js", ""}, - {"Not found, double js extension", []string{"foo.js.js", "bar.js"}, "foo.js", ""}, - {"Index file, folder only", []string{"foo/index.js", "bar.js"}, "foo", "foo/index.js"}, - {"Index file, folder and index", []string{"foo/index.js", "bar.js"}, "foo/index", "foo/index.js"}, - {"Index file, folder and index and suffix", []string{"foo/index.js", "bar.js"}, "foo/index.js", "foo/index.js"}, - {"Index ESM file, folder only", []string{"foo/index.esm.js", "bar.js"}, "foo", "foo/index.esm.js"}, - {"Index ESM file, folder and index", []string{"foo/index.esm.js", "bar.js"}, "foo/index", "foo/index.esm.js"}, - {"Index ESM file, folder and index and suffix", []string{"foo/index.esm.js", "bar.js"}, "foo/index.esm.js", "foo/index.esm.js"}, - // We added these index.esm.js cases in v0.101.0. The case below is unlikely to happen in the wild, but add a test - // to document Hugo's behavior. We pick the file with the name index.js; anything else would be breaking. - {"Index and Index ESM file, folder only", []string{"foo/index.esm.js", "foo/index.js", "bar.js"}, "foo", "foo/index.js"}, - - // Issue #8949 - {"Check file before directory", []string{"foo.js", "foo/index.js"}, "foo", "foo.js"}, - } { - c.Run(test.name, func(c *qt.C) { - baseDir := "assets" - mfs := afero.NewMemMapFs() - - for _, filename := range test.files { - c.Assert(afero.WriteFile(mfs, filepath.Join(baseDir, filename), []byte("let foo='bar';"), 0o777), qt.IsNil) - } - - conf := testconfig.GetTestConfig(mfs, config.New()) - fs := hugofs.NewFrom(mfs, conf.BaseConfig()) - - p, err := paths.New(fs, conf) - c.Assert(err, qt.IsNil) - bfs, err := filesystems.NewBase(p, nil) - c.Assert(err, qt.IsNil) - resolver := newFSResolver(bfs.Assets.Fs) - - got := resolver.resolveComponent(test.impPath) - - gotPath := "" - expect := test.expect - if got != nil { - gotPath = filepath.ToSlash(got.Filename) - expect = path.Join(baseDir, test.expect) - } - - c.Assert(gotPath, qt.Equals, expect) - }) - } -} diff --git a/internal/js/esbuild/sourcemap.go b/internal/js/esbuild/sourcemap.go deleted file mode 100644 index 647f0c081..000000000 --- a/internal/js/esbuild/sourcemap.go +++ /dev/null @@ -1,80 +0,0 @@ -// 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 esbuild - -import ( - "encoding/json" - "strings" - - "github.com/evanw/esbuild/pkg/api" - "github.com/gohugoio/hugo/common/paths" -) - -type sourceMap struct { - Version int `json:"version"` - Sources []string `json:"sources"` - SourcesContent []string `json:"sourcesContent"` - Mappings string `json:"mappings"` - Names []string `json:"names"` -} - -func fixOutputFile(o *api.OutputFile, resolve func(string) string) error { - if strings.HasSuffix(o.Path, ".map") { - b, err := fixSourceMap(o.Contents, resolve) - if err != nil { - return err - } - o.Contents = b - } - return nil -} - -func fixSourceMap(s []byte, resolve func(string) string) ([]byte, error) { - var sm sourceMap - if err := json.Unmarshal([]byte(s), &sm); err != nil { - return nil, err - } - - sm.Sources = fixSourceMapSources(sm.Sources, resolve) - - b, err := json.Marshal(sm) - if err != nil { - return nil, err - } - - return b, nil -} - -func fixSourceMapSources(s []string, resolve func(string) string) []string { - var result []string - for _, src := range s { - if s := resolve(src); s != "" { - // Absolute filenames works fine on U*ix (tested in Chrome on MacOs), but works very poorly on Windows (again Chrome). - // So, convert it to a URL. - if u, err := paths.UrlFromFilename(s); err == nil { - result = append(result, u.String()) - } - } - } - return result -} - -// Used in tests. -func SourcesFromSourceMap(s string) []string { - var sm sourceMap - if err := json.Unmarshal([]byte(s), &sm); err != nil { - return nil - } - return sm.Sources -} diff --git a/internal/warpc/build.sh b/internal/warpc/build.sh deleted file mode 100755 index 5e75aa381..000000000 --- a/internal/warpc/build.sh +++ /dev/null @@ -1,5 +0,0 @@ -# TODO1 clean up when done. -go generate ./gen -javy compile js/greet.bundle.js -d -o wasm/greet.wasm -javy compile js/renderkatex.bundle.js -d -o wasm/renderkatex.wasm -touch warpc_test.go \ No newline at end of file diff --git a/internal/warpc/gen/main.go b/internal/warpc/gen/main.go deleted file mode 100644 index d3d6562a9..000000000 --- a/internal/warpc/gen/main.go +++ /dev/null @@ -1,68 +0,0 @@ -// 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:generate go run main.go -package main - -import ( - "fmt" - "log" - "os" - "path/filepath" - "strings" - - "github.com/evanw/esbuild/pkg/api" -) - -var scripts = []string{ - "greet.js", - "renderkatex.js", -} - -func main() { - for _, script := range scripts { - filename := filepath.Join("../js", script) - err := buildJSBundle(filename) - if err != nil { - log.Fatal(err) - } - } -} - -func buildJSBundle(filename string) error { - minify := true - result := api.Build( - api.BuildOptions{ - EntryPoints: []string{filename}, - Bundle: true, - MinifyWhitespace: minify, - MinifyIdentifiers: minify, - MinifySyntax: minify, - Target: api.ES2020, - Outfile: strings.Replace(filename, ".js", ".bundle.js", 1), - SourceRoot: "../js", - }) - - if len(result.Errors) > 0 { - return fmt.Errorf("build failed: %v", result.Errors) - } - if len(result.OutputFiles) != 1 { - return fmt.Errorf("expected 1 output file, got %d", len(result.OutputFiles)) - } - - of := result.OutputFiles[0] - if err := os.WriteFile(filepath.FromSlash(of.Path), of.Contents, 0o644); err != nil { - return fmt.Errorf("write file failed: %v", err) - } - return nil -} diff --git a/internal/warpc/js/.gitignore b/internal/warpc/js/.gitignore deleted file mode 100644 index ccb2c800f..000000000 --- a/internal/warpc/js/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules/ -package-lock.json \ No newline at end of file diff --git a/internal/warpc/js/common.js b/internal/warpc/js/common.js deleted file mode 100644 index 61c535fb7..000000000 --- a/internal/warpc/js/common.js +++ /dev/null @@ -1,83 +0,0 @@ -// Read JSONL from stdin. -export function readInput(handle) { - const buffSize = 1024; - let currentLine = []; - const buffer = new Uint8Array(buffSize); - - // These are not implemented by QuickJS. - console.warn = (value) => { - console.log(value); - }; - - console.error = (value) => { - throw new Error(value); - }; - - // Read all the available bytes - while (true) { - // Stdin file descriptor - const fd = 0; - let bytesRead = 0; - try { - bytesRead = Javy.IO.readSync(fd, buffer); - } catch (e) { - // IO.readSync fails with os error 29 when stdin closes. - if (e.message.includes('os error 29')) { - break; - } - throw new Error('Error reading from stdin'); - } - - if (bytesRead < 0) { - throw new Error('Error reading from stdin'); - break; - } - - if (bytesRead === 0) { - break; - } - - currentLine = [...currentLine, ...buffer.subarray(0, bytesRead)]; - - // Check for newline. If not, we need to read more data. - if (!currentLine.includes(10)) { - continue; - } - - // Split array into chunks by newline. - let i = 0; - for (let j = 0; i < currentLine.length; i++) { - if (currentLine[i] === 10) { - const chunk = currentLine.splice(j, i + 1); - const arr = new Uint8Array(chunk); - let message; - try { - message = JSON.parse(new TextDecoder().decode(arr)); - } catch (e) { - throw new Error(`Error parsing JSON '${new TextDecoder().decode(arr)}' from stdin: ${e.message}`); - } - - try { - handle(message); - } catch (e) { - let header = message.header; - header.err = e.message; - writeOutput({ header: header }); - } - - j = i + 1; - } - } - // Remove processed data. - currentLine = currentLine.slice(i); - } -} - -// Write JSONL to stdout -export function writeOutput(output) { - const encodedOutput = new TextEncoder().encode(JSON.stringify(output) + '\n'); - const buffer = new Uint8Array(encodedOutput); - // Stdout file descriptor - const fd = 1; - Javy.IO.writeSync(fd, buffer); -} diff --git a/internal/warpc/js/greet.bundle.js b/internal/warpc/js/greet.bundle.js deleted file mode 100644 index 6828d582a..000000000 --- a/internal/warpc/js/greet.bundle.js +++ /dev/null @@ -1,2 +0,0 @@ -(()=>{function w(r){let e=[],c=new Uint8Array(1024);for(console.warn=n=>{console.log(n)},console.error=n=>{throw new Error(n)};;){let o=0;try{o=Javy.IO.readSync(0,c)}catch(a){if(a.message.includes("os error 29"))break;throw new Error("Error reading from stdin")}if(o<0)throw new Error("Error reading from stdin");if(o===0)break;if(e=[...e,...c.subarray(0,o)],!e.includes(10))continue;let t=0;for(let a=0;t{function Wt(r){let t=[],a=new Uint8Array(1024);for(console.warn=n=>{console.log(n)},console.error=n=>{throw new Error(n)};;){let s=0;try{s=Javy.IO.readSync(0,a)}catch(h){if(h.message.includes("os error 29"))break;throw new Error("Error reading from stdin")}if(s<0)throw new Error("Error reading from stdin");if(s===0)break;if(t=[...t,...a.subarray(0,s)],!t.includes(10))continue;let l=0;for(let h=0;l15?f="\u2026"+h.slice(n-15,n):f=h.slice(0,n);var v;s+15":">","<":"<",'"':""","'":"'"},Ba=/[&><"']/g;function Da(r){return String(r).replace(Ba,e=>qa[e])}var zr=function r(e){return e.type==="ordgroup"||e.type==="color"?e.body.length===1?r(e.body[0]):e:e.type==="font"?r(e.body):e},Ca=function(e){var t=zr(e);return t.type==="mathord"||t.type==="textord"||t.type==="atom"},_a=function(e){if(!e)throw new Error("Expected non-null, but got "+String(e));return e},Na=function(e){var t=/^[\x00-\x20]*([^\\/#?]*?)(:|�*58|�*3a|&colon)/i.exec(e);return t?t[2]!==":"||!/^[a-zA-Z][a-zA-Z0-9+\-.]*$/.test(t[1])?null:t[1].toLowerCase():"_relative"},O={contains:Ma,deflt:za,escape:Da,hyphenate:Ta,getBaseElem:zr,isCharacterBox:Ca,protocolFromUrl:Na},Oe={displayMode:{type:"boolean",description:"Render math in display mode, which puts the math in display style (so \\int and \\sum are large, for example), and centers the math on the page on its own line.",cli:"-d, --display-mode"},output:{type:{enum:["htmlAndMathml","html","mathml"]},description:"Determines the markup language of the output.",cli:"-F, --format "},leqno:{type:"boolean",description:"Render display math in leqno style (left-justified tags)."},fleqn:{type:"boolean",description:"Render display math flush left."},throwOnError:{type:"boolean",default:!0,cli:"-t, --no-throw-on-error",cliDescription:"Render errors (in the color given by --error-color) instead of throwing a ParseError exception when encountering an error."},errorColor:{type:"string",default:"#cc0000",cli:"-c, --error-color ",cliDescription:"A color string given in the format 'rgb' or 'rrggbb' (no #). This option determines the color of errors rendered by the -t option.",cliProcessor:r=>"#"+r},macros:{type:"object",cli:"-m, --macro ",cliDescription:"Define custom macro of the form '\\foo:expansion' (use multiple -m arguments for multiple macros).",cliDefault:[],cliProcessor:(r,e)=>(e.push(r),e)},minRuleThickness:{type:"number",description:"Specifies a minimum thickness, in ems, for fraction lines, `\\sqrt` top lines, `{array}` vertical lines, `\\hline`, `\\hdashline`, `\\underline`, `\\overline`, and the borders of `\\fbox`, `\\boxed`, and `\\fcolorbox`.",processor:r=>Math.max(0,r),cli:"--min-rule-thickness ",cliProcessor:parseFloat},colorIsTextColor:{type:"boolean",description:"Makes \\color behave like LaTeX's 2-argument \\textcolor, instead of LaTeX's one-argument \\color mode change.",cli:"-b, --color-is-text-color"},strict:{type:[{enum:["warn","ignore","error"]},"boolean","function"],description:"Turn on strict / LaTeX faithfulness mode, which throws an error if the input uses features that are not supported by LaTeX.",cli:"-S, --strict",cliDefault:!1},trust:{type:["boolean","function"],description:"Trust the input, enabling all HTML features such as \\url.",cli:"-T, --trust"},maxSize:{type:"number",default:1/0,description:"If non-zero, all user-specified sizes, e.g. in \\rule{500em}{500em}, will be capped to maxSize ems. Otherwise, elements and spaces can be arbitrarily large",processor:r=>Math.max(0,r),cli:"-s, --max-size ",cliProcessor:parseInt},maxExpand:{type:"number",default:1e3,description:"Limit the number of macro expansions to the specified number, to prevent e.g. infinite macro loops. If set to Infinity, the macro expander will try to fully expand as in LaTeX.",processor:r=>Math.max(0,r),cli:"-e, --max-expand ",cliProcessor:r=>r==="Infinity"?1/0:parseInt(r)},globalGroup:{type:"boolean",cli:!1}};function Oa(r){if(r.default)return r.default;var e=r.type,t=Array.isArray(e)?e[0]:e;if(typeof t!="string")return t.enum[0];switch(t){case"boolean":return!1;case"string":return"";case"number":return 0;case"object":return{}}}var de=class{constructor(e){this.displayMode=void 0,this.output=void 0,this.leqno=void 0,this.fleqn=void 0,this.throwOnError=void 0,this.errorColor=void 0,this.macros=void 0,this.minRuleThickness=void 0,this.colorIsTextColor=void 0,this.strict=void 0,this.trust=void 0,this.maxSize=void 0,this.maxExpand=void 0,this.globalGroup=void 0,e=e||{};for(var t in Oe)if(Oe.hasOwnProperty(t)){var a=Oe[t];this[t]=e[t]!==void 0?a.processor?a.processor(e[t]):e[t]:Oa(a)}}reportNonstrict(e,t,a){var n=this.strict;if(typeof n=="function"&&(n=n(e,t,a)),!(!n||n==="ignore")){if(n===!0||n==="error")throw new z("LaTeX-incompatible input and strict mode is set to 'error': "+(t+" ["+e+"]"),a);n==="warn"?typeof console<"u"&&console.warn("LaTeX-incompatible input and strict mode is set to 'warn': "+(t+" ["+e+"]")):typeof console<"u"&&console.warn("LaTeX-incompatible input and strict mode is set to "+("unrecognized '"+n+"': "+t+" ["+e+"]"))}}useStrictBehavior(e,t,a){var n=this.strict;if(typeof n=="function")try{n=n(e,t,a)}catch{n="error"}return!n||n==="ignore"?!1:n===!0||n==="error"?!0:n==="warn"?(typeof console<"u"&&console.warn("LaTeX-incompatible input and strict mode is set to 'warn': "+(t+" ["+e+"]")),!1):(typeof console<"u"&&console.warn("LaTeX-incompatible input and strict mode is set to "+("unrecognized '"+n+"': "+t+" ["+e+"]")),!1)}isTrusted(e){if(e.url&&!e.protocol){var t=O.protocolFromUrl(e.url);if(t==null)return!1;e.protocol=t}var a=typeof this.trust=="function"?this.trust(e):this.trust;return!!a}},k0=class{constructor(e,t,a){this.id=void 0,this.size=void 0,this.cramped=void 0,this.id=e,this.size=t,this.cramped=a}sup(){return M0[Ia[this.id]]}sub(){return M0[Ea[this.id]]}fracNum(){return M0[Ra[this.id]]}fracDen(){return M0[$a[this.id]]}cramp(){return M0[La[this.id]]}text(){return M0[Fa[this.id]]}isTight(){return this.size>=2}},At=0,Ee=1,ae=2,N0=3,pe=4,v0=5,ne=6,o0=7,M0=[new k0(At,0,!1),new k0(Ee,0,!0),new k0(ae,1,!1),new k0(N0,1,!0),new k0(pe,2,!1),new k0(v0,2,!0),new k0(ne,3,!1),new k0(o0,3,!0)],Ia=[pe,v0,pe,v0,ne,o0,ne,o0],Ea=[v0,v0,v0,v0,o0,o0,o0,o0],Ra=[ae,N0,pe,v0,ne,o0,ne,o0],$a=[N0,N0,v0,v0,o0,o0,o0,o0],La=[Ee,Ee,N0,N0,v0,v0,o0,o0],Fa=[At,Ee,ae,N0,ae,N0,ae,N0],E={DISPLAY:M0[At],TEXT:M0[ae],SCRIPT:M0[pe],SCRIPTSCRIPT:M0[ne]},pt=[{name:"latin",blocks:[[256,591],[768,879]]},{name:"cyrillic",blocks:[[1024,1279]]},{name:"armenian",blocks:[[1328,1423]]},{name:"brahmic",blocks:[[2304,4255]]},{name:"georgian",blocks:[[4256,4351]]},{name:"cjk",blocks:[[12288,12543],[19968,40879],[65280,65376]]},{name:"hangul",blocks:[[44032,55215]]}];function Ha(r){for(var e=0;e=n[0]&&r<=n[1])return t.name}return null}var Ie=[];pt.forEach(r=>r.blocks.forEach(e=>Ie.push(...e)));function Ar(r){for(var e=0;e=Ie[e]&&r<=Ie[e+1])return!0;return!1}var re=80,Pa=function(e,t){return"M95,"+(622+e+t)+` -c-2.7,0,-7.17,-2.7,-13.5,-8c-5.8,-5.3,-9.5,-10,-9.5,-14 -c0,-2,0.3,-3.3,1,-4c1.3,-2.7,23.83,-20.7,67.5,-54 -c44.2,-33.3,65.8,-50.3,66.5,-51c1.3,-1.3,3,-2,5,-2c4.7,0,8.7,3.3,12,10 -s173,378,173,378c0.7,0,35.3,-71,104,-213c68.7,-142,137.5,-285,206.5,-429 -c69,-144,104.5,-217.7,106.5,-221 -l`+e/2.075+" -"+e+` -c5.3,-9.3,12,-14,20,-14 -H400000v`+(40+e)+`H845.2724 -s-225.272,467,-225.272,467s-235,486,-235,486c-2.7,4.7,-9,7,-19,7 -c-6,0,-10,-1,-12,-3s-194,-422,-194,-422s-65,47,-65,47z -M`+(834+e)+" "+t+"h400000v"+(40+e)+"h-400000z"},Ga=function(e,t){return"M263,"+(601+e+t)+`c0.7,0,18,39.7,52,119 -c34,79.3,68.167,158.7,102.5,238c34.3,79.3,51.8,119.3,52.5,120 -c340,-704.7,510.7,-1060.3,512,-1067 -l`+e/2.084+" -"+e+` -c4.7,-7.3,11,-11,19,-11 -H40000v`+(40+e)+`H1012.3 -s-271.3,567,-271.3,567c-38.7,80.7,-84,175,-136,283c-52,108,-89.167,185.3,-111.5,232 -c-22.3,46.7,-33.8,70.3,-34.5,71c-4.7,4.7,-12.3,7,-23,7s-12,-1,-12,-1 -s-109,-253,-109,-253c-72.7,-168,-109.3,-252,-110,-252c-10.7,8,-22,16.7,-34,26 -c-22,17.3,-33.3,26,-34,26s-26,-26,-26,-26s76,-59,76,-59s76,-60,76,-60z -M`+(1001+e)+" "+t+"h400000v"+(40+e)+"h-400000z"},Va=function(e,t){return"M983 "+(10+e+t)+` -l`+e/3.13+" -"+e+` -c4,-6.7,10,-10,18,-10 H400000v`+(40+e)+` -H1013.1s-83.4,268,-264.1,840c-180.7,572,-277,876.3,-289,913c-4.7,4.7,-12.7,7,-24,7 -s-12,0,-12,0c-1.3,-3.3,-3.7,-11.7,-7,-25c-35.3,-125.3,-106.7,-373.3,-214,-744 -c-10,12,-21,25,-33,39s-32,39,-32,39c-6,-5.3,-15,-14,-27,-26s25,-30,25,-30 -c26.7,-32.7,52,-63,76,-91s52,-60,52,-60s208,722,208,722 -c56,-175.3,126.3,-397.3,211,-666c84.7,-268.7,153.8,-488.2,207.5,-658.5 -c53.7,-170.3,84.5,-266.8,92.5,-289.5z -M`+(1001+e)+" "+t+"h400000v"+(40+e)+"h-400000z"},Ua=function(e,t){return"M424,"+(2398+e+t)+` -c-1.3,-0.7,-38.5,-172,-111.5,-514c-73,-342,-109.8,-513.3,-110.5,-514 -c0,-2,-10.7,14.3,-32,49c-4.7,7.3,-9.8,15.7,-15.5,25c-5.7,9.3,-9.8,16,-12.5,20 -s-5,7,-5,7c-4,-3.3,-8.3,-7.7,-13,-13s-13,-13,-13,-13s76,-122,76,-122s77,-121,77,-121 -s209,968,209,968c0,-2,84.7,-361.7,254,-1079c169.3,-717.3,254.7,-1077.7,256,-1081 -l`+e/4.223+" -"+e+`c4,-6.7,10,-10,18,-10 H400000 -v`+(40+e)+`H1014.6 -s-87.3,378.7,-272.6,1166c-185.3,787.3,-279.3,1182.3,-282,1185 -c-2,6,-10,9,-24,9 -c-8,0,-12,-0.7,-12,-2z M`+(1001+e)+" "+t+` -h400000v`+(40+e)+"h-400000z"},Xa=function(e,t){return"M473,"+(2713+e+t)+` -c339.3,-1799.3,509.3,-2700,510,-2702 l`+e/5.298+" -"+e+` -c3.3,-7.3,9.3,-11,18,-11 H400000v`+(40+e)+`H1017.7 -s-90.5,478,-276.2,1466c-185.7,988,-279.5,1483,-281.5,1485c-2,6,-10,9,-24,9 -c-8,0,-12,-0.7,-12,-2c0,-1.3,-5.3,-32,-16,-92c-50.7,-293.3,-119.7,-693.3,-207,-1200 -c0,-1.3,-5.3,8.7,-16,30c-10.7,21.3,-21.3,42.7,-32,64s-16,33,-16,33s-26,-26,-26,-26 -s76,-153,76,-153s77,-151,77,-151c0.7,0.7,35.7,202,105,604c67.3,400.7,102,602.7,104, -606zM`+(1001+e)+" "+t+"h400000v"+(40+e)+"H1017.7z"},Wa=function(e){var t=e/2;return"M400000 "+e+" H0 L"+t+" 0 l65 45 L145 "+(e-80)+" H400000z"},Ya=function(e,t,a){var n=a-54-t-e;return"M702 "+(e+t)+"H400000"+(40+e)+` -H742v`+n+`l-4 4-4 4c-.667.7 -2 1.5-4 2.5s-4.167 1.833-6.5 2.5-5.5 1-9.5 1 -h-12l-28-84c-16.667-52-96.667 -294.333-240-727l-212 -643 -85 170 -c-4-3.333-8.333-7.667-13 -13l-13-13l77-155 77-156c66 199.333 139 419.667 -219 661 l218 661zM702 `+t+"H400000v"+(40+e)+"H742z"},Za=function(e,t,a){t=1e3*t;var n="";switch(e){case"sqrtMain":n=Pa(t,re);break;case"sqrtSize1":n=Ga(t,re);break;case"sqrtSize2":n=Va(t,re);break;case"sqrtSize3":n=Ua(t,re);break;case"sqrtSize4":n=Xa(t,re);break;case"sqrtTall":n=Ya(t,re,a)}return n},ja=function(e,t){switch(e){case"\u239C":return"M291 0 H417 V"+t+" H291z M291 0 H417 V"+t+" H291z";case"\u2223":return"M145 0 H188 V"+t+" H145z M145 0 H188 V"+t+" H145z";case"\u2225":return"M145 0 H188 V"+t+" H145z M145 0 H188 V"+t+" H145z"+("M367 0 H410 V"+t+" H367z M367 0 H410 V"+t+" H367z");case"\u239F":return"M457 0 H583 V"+t+" H457z M457 0 H583 V"+t+" H457z";case"\u23A2":return"M319 0 H403 V"+t+" H319z M319 0 H403 V"+t+" H319z";case"\u23A5":return"M263 0 H347 V"+t+" H263z M263 0 H347 V"+t+" H263z";case"\u23AA":return"M384 0 H504 V"+t+" H384z M384 0 H504 V"+t+" H384z";case"\u23D0":return"M312 0 H355 V"+t+" H312z M312 0 H355 V"+t+" H312z";case"\u2016":return"M257 0 H300 V"+t+" H257z M257 0 H300 V"+t+" H257z"+("M478 0 H521 V"+t+" H478z M478 0 H521 V"+t+" H478z");default:return""}},Yt={doubleleftarrow:`M262 157 -l10-10c34-36 62.7-77 86-123 3.3-8 5-13.3 5-16 0-5.3-6.7-8-20-8-7.3 - 0-12.2.5-14.5 1.5-2.3 1-4.8 4.5-7.5 10.5-49.3 97.3-121.7 169.3-217 216-28 - 14-57.3 25-88 33-6.7 2-11 3.8-13 5.5-2 1.7-3 4.2-3 7.5s1 5.8 3 7.5 -c2 1.7 6.3 3.5 13 5.5 68 17.3 128.2 47.8 180.5 91.5 52.3 43.7 93.8 96.2 124.5 - 157.5 9.3 8 15.3 12.3 18 13h6c12-.7 18-4 18-10 0-2-1.7-7-5-15-23.3-46-52-87 --86-123l-10-10h399738v-40H218c328 0 0 0 0 0l-10-8c-26.7-20-65.7-43-117-69 2.7 --2 6-3.7 10-5 36.7-16 72.3-37.3 107-64l10-8h399782v-40z -m8 0v40h399730v-40zm0 194v40h399730v-40z`,doublerightarrow:`M399738 392l --10 10c-34 36-62.7 77-86 123-3.3 8-5 13.3-5 16 0 5.3 6.7 8 20 8 7.3 0 12.2-.5 - 14.5-1.5 2.3-1 4.8-4.5 7.5-10.5 49.3-97.3 121.7-169.3 217-216 28-14 57.3-25 88 --33 6.7-2 11-3.8 13-5.5 2-1.7 3-4.2 3-7.5s-1-5.8-3-7.5c-2-1.7-6.3-3.5-13-5.5-68 --17.3-128.2-47.8-180.5-91.5-52.3-43.7-93.8-96.2-124.5-157.5-9.3-8-15.3-12.3-18 --13h-6c-12 .7-18 4-18 10 0 2 1.7 7 5 15 23.3 46 52 87 86 123l10 10H0v40h399782 -c-328 0 0 0 0 0l10 8c26.7 20 65.7 43 117 69-2.7 2-6 3.7-10 5-36.7 16-72.3 37.3 --107 64l-10 8H0v40zM0 157v40h399730v-40zm0 194v40h399730v-40z`,leftarrow:`M400000 241H110l3-3c68.7-52.7 113.7-120 - 135-202 4-14.7 6-23 6-25 0-7.3-7-11-21-11-8 0-13.2.8-15.5 2.5-2.3 1.7-4.2 5.8 --5.5 12.5-1.3 4.7-2.7 10.3-4 17-12 48.7-34.8 92-68.5 130S65.3 228.3 18 247 -c-10 4-16 7.7-18 11 0 8.7 6 14.3 18 17 47.3 18.7 87.8 47 121.5 85S196 441.3 208 - 490c.7 2 1.3 5 2 9s1.2 6.7 1.5 8c.3 1.3 1 3.3 2 6s2.2 4.5 3.5 5.5c1.3 1 3.3 - 1.8 6 2.5s6 1 10 1c14 0 21-3.7 21-11 0-2-2-10.3-6-25-20-79.3-65-146.7-135-202 - l-3-3h399890zM100 241v40h399900v-40z`,leftbrace:`M6 548l-6-6v-35l6-11c56-104 135.3-181.3 238-232 57.3-28.7 117 --45 179-50h399577v120H403c-43.3 7-81 15-113 26-100.7 33-179.7 91-237 174-2.7 - 5-6 9-10 13-.7 1-7.3 1-20 1H6z`,leftbraceunder:`M0 6l6-6h17c12.688 0 19.313.3 20 1 4 4 7.313 8.3 10 13 - 35.313 51.3 80.813 93.8 136.5 127.5 55.688 33.7 117.188 55.8 184.5 66.5.688 - 0 2 .3 4 1 18.688 2.7 76 4.3 172 5h399450v120H429l-6-1c-124.688-8-235-61.7 --331-161C60.687 138.7 32.312 99.3 7 54L0 41V6z`,leftgroup:`M400000 80 -H435C64 80 168.3 229.4 21 260c-5.9 1.2-18 0-18 0-2 0-3-1-3-3v-38C76 61 257 0 - 435 0h399565z`,leftgroupunder:`M400000 262 -H435C64 262 168.3 112.6 21 82c-5.9-1.2-18 0-18 0-2 0-3 1-3 3v38c76 158 257 219 - 435 219h399565z`,leftharpoon:`M0 267c.7 5.3 3 10 7 14h399993v-40H93c3.3 --3.3 10.2-9.5 20.5-18.5s17.8-15.8 22.5-20.5c50.7-52 88-110.3 112-175 4-11.3 5 --18.3 3-21-1.3-4-7.3-6-18-6-8 0-13 .7-15 2s-4.7 6.7-8 16c-42 98.7-107.3 174.7 --196 228-6.7 4.7-10.7 8-12 10-1.3 2-2 5.7-2 11zm100-26v40h399900v-40z`,leftharpoonplus:`M0 267c.7 5.3 3 10 7 14h399993v-40H93c3.3-3.3 10.2-9.5 - 20.5-18.5s17.8-15.8 22.5-20.5c50.7-52 88-110.3 112-175 4-11.3 5-18.3 3-21-1.3 --4-7.3-6-18-6-8 0-13 .7-15 2s-4.7 6.7-8 16c-42 98.7-107.3 174.7-196 228-6.7 4.7 --10.7 8-12 10-1.3 2-2 5.7-2 11zm100-26v40h399900v-40zM0 435v40h400000v-40z -m0 0v40h400000v-40z`,leftharpoondown:`M7 241c-4 4-6.333 8.667-7 14 0 5.333.667 9 2 11s5.333 - 5.333 12 10c90.667 54 156 130 196 228 3.333 10.667 6.333 16.333 9 17 2 .667 5 - 1 9 1h5c10.667 0 16.667-2 18-6 2-2.667 1-9.667-3-21-32-87.333-82.667-157.667 --152-211l-3-3h399907v-40zM93 281 H400000 v-40L7 241z`,leftharpoondownplus:`M7 435c-4 4-6.3 8.7-7 14 0 5.3.7 9 2 11s5.3 5.3 12 - 10c90.7 54 156 130 196 228 3.3 10.7 6.3 16.3 9 17 2 .7 5 1 9 1h5c10.7 0 16.7 --2 18-6 2-2.7 1-9.7-3-21-32-87.3-82.7-157.7-152-211l-3-3h399907v-40H7zm93 0 -v40h399900v-40zM0 241v40h399900v-40zm0 0v40h399900v-40z`,lefthook:`M400000 281 H103s-33-11.2-61-33.5S0 197.3 0 164s14.2-61.2 42.5 --83.5C70.8 58.2 104 47 142 47 c16.7 0 25 6.7 25 20 0 12-8.7 18.7-26 20-40 3.3 --68.7 15.7-86 37-10 12-15 25.3-15 40 0 22.7 9.8 40.7 29.5 54 19.7 13.3 43.5 21 - 71.5 23h399859zM103 281v-40h399897v40z`,leftlinesegment:`M40 281 V428 H0 V94 H40 V241 H400000 v40z -M40 281 V428 H0 V94 H40 V241 H400000 v40z`,leftmapsto:`M40 281 V448H0V74H40V241H400000v40z -M40 281 V448H0V74H40V241H400000v40z`,leftToFrom:`M0 147h400000v40H0zm0 214c68 40 115.7 95.7 143 167h22c15.3 0 23 --.3 23-1 0-1.3-5.3-13.7-16-37-18-35.3-41.3-69-70-101l-7-8h399905v-40H95l7-8 -c28.7-32 52-65.7 70-101 10.7-23.3 16-35.7 16-37 0-.7-7.7-1-23-1h-22C115.7 265.3 - 68 321 0 361zm0-174v-40h399900v40zm100 154v40h399900v-40z`,longequal:`M0 50 h400000 v40H0z m0 194h40000v40H0z -M0 50 h400000 v40H0z m0 194h40000v40H0z`,midbrace:`M200428 334 -c-100.7-8.3-195.3-44-280-108-55.3-42-101.7-93-139-153l-9-14c-2.7 4-5.7 8.7-9 14 --53.3 86.7-123.7 153-211 199-66.7 36-137.3 56.3-212 62H0V214h199568c178.3-11.7 - 311.7-78.3 403-201 6-8 9.7-12 11-12 .7-.7 6.7-1 18-1s17.3.3 18 1c1.3 0 5 4 11 - 12 44.7 59.3 101.3 106.3 170 141s145.3 54.3 229 60h199572v120z`,midbraceunder:`M199572 214 -c100.7 8.3 195.3 44 280 108 55.3 42 101.7 93 139 153l9 14c2.7-4 5.7-8.7 9-14 - 53.3-86.7 123.7-153 211-199 66.7-36 137.3-56.3 212-62h199568v120H200432c-178.3 - 11.7-311.7 78.3-403 201-6 8-9.7 12-11 12-.7.7-6.7 1-18 1s-17.3-.3-18-1c-1.3 0 --5-4-11-12-44.7-59.3-101.3-106.3-170-141s-145.3-54.3-229-60H0V214z`,oiintSize1:`M512.6 71.6c272.6 0 320.3 106.8 320.3 178.2 0 70.8-47.7 177.6 --320.3 177.6S193.1 320.6 193.1 249.8c0-71.4 46.9-178.2 319.5-178.2z -m368.1 178.2c0-86.4-60.9-215.4-368.1-215.4-306.4 0-367.3 129-367.3 215.4 0 85.8 -60.9 214.8 367.3 214.8 307.2 0 368.1-129 368.1-214.8z`,oiintSize2:`M757.8 100.1c384.7 0 451.1 137.6 451.1 230 0 91.3-66.4 228.8 --451.1 228.8-386.3 0-452.7-137.5-452.7-228.8 0-92.4 66.4-230 452.7-230z -m502.4 230c0-111.2-82.4-277.2-502.4-277.2s-504 166-504 277.2 -c0 110 84 276 504 276s502.4-166 502.4-276z`,oiiintSize1:`M681.4 71.6c408.9 0 480.5 106.8 480.5 178.2 0 70.8-71.6 177.6 --480.5 177.6S202.1 320.6 202.1 249.8c0-71.4 70.5-178.2 479.3-178.2z -m525.8 178.2c0-86.4-86.8-215.4-525.7-215.4-437.9 0-524.7 129-524.7 215.4 0 -85.8 86.8 214.8 524.7 214.8 438.9 0 525.7-129 525.7-214.8z`,oiiintSize2:`M1021.2 53c603.6 0 707.8 165.8 707.8 277.2 0 110-104.2 275.8 --707.8 275.8-606 0-710.2-165.8-710.2-275.8C311 218.8 415.2 53 1021.2 53z -m770.4 277.1c0-131.2-126.4-327.6-770.5-327.6S248.4 198.9 248.4 330.1 -c0 130 128.8 326.4 772.7 326.4s770.5-196.4 770.5-326.4z`,rightarrow:`M0 241v40h399891c-47.3 35.3-84 78-110 128 --16.7 32-27.7 63.7-33 95 0 1.3-.2 2.7-.5 4-.3 1.3-.5 2.3-.5 3 0 7.3 6.7 11 20 - 11 8 0 13.2-.8 15.5-2.5 2.3-1.7 4.2-5.5 5.5-11.5 2-13.3 5.7-27 11-41 14.7-44.7 - 39-84.5 73-119.5s73.7-60.2 119-75.5c6-2 9-5.7 9-11s-3-9-9-11c-45.3-15.3-85 --40.5-119-75.5s-58.3-74.8-73-119.5c-4.7-14-8.3-27.3-11-40-1.3-6.7-3.2-10.8-5.5 --12.5-2.3-1.7-7.5-2.5-15.5-2.5-14 0-21 3.7-21 11 0 2 2 10.3 6 25 20.7 83.3 67 - 151.7 139 205zm0 0v40h399900v-40z`,rightbrace:`M400000 542l --6 6h-17c-12.7 0-19.3-.3-20-1-4-4-7.3-8.3-10-13-35.3-51.3-80.8-93.8-136.5-127.5 -s-117.2-55.8-184.5-66.5c-.7 0-2-.3-4-1-18.7-2.7-76-4.3-172-5H0V214h399571l6 1 -c124.7 8 235 61.7 331 161 31.3 33.3 59.7 72.7 85 118l7 13v35z`,rightbraceunder:`M399994 0l6 6v35l-6 11c-56 104-135.3 181.3-238 232-57.3 - 28.7-117 45-179 50H-300V214h399897c43.3-7 81-15 113-26 100.7-33 179.7-91 237 --174 2.7-5 6-9 10-13 .7-1 7.3-1 20-1h17z`,rightgroup:`M0 80h399565c371 0 266.7 149.4 414 180 5.9 1.2 18 0 18 0 2 0 - 3-1 3-3v-38c-76-158-257-219-435-219H0z`,rightgroupunder:`M0 262h399565c371 0 266.7-149.4 414-180 5.9-1.2 18 0 18 - 0 2 0 3 1 3 3v38c-76 158-257 219-435 219H0z`,rightharpoon:`M0 241v40h399993c4.7-4.7 7-9.3 7-14 0-9.3 --3.7-15.3-11-18-92.7-56.7-159-133.7-199-231-3.3-9.3-6-14.7-8-16-2-1.3-7-2-15-2 --10.7 0-16.7 2-18 6-2 2.7-1 9.7 3 21 15.3 42 36.7 81.8 64 119.5 27.3 37.7 58 - 69.2 92 94.5zm0 0v40h399900v-40z`,rightharpoonplus:`M0 241v40h399993c4.7-4.7 7-9.3 7-14 0-9.3-3.7-15.3-11 --18-92.7-56.7-159-133.7-199-231-3.3-9.3-6-14.7-8-16-2-1.3-7-2-15-2-10.7 0-16.7 - 2-18 6-2 2.7-1 9.7 3 21 15.3 42 36.7 81.8 64 119.5 27.3 37.7 58 69.2 92 94.5z -m0 0v40h399900v-40z m100 194v40h399900v-40zm0 0v40h399900v-40z`,rightharpoondown:`M399747 511c0 7.3 6.7 11 20 11 8 0 13-.8 15-2.5s4.7-6.8 - 8-15.5c40-94 99.3-166.3 178-217 13.3-8 20.3-12.3 21-13 5.3-3.3 8.5-5.8 9.5 --7.5 1-1.7 1.5-5.2 1.5-10.5s-2.3-10.3-7-15H0v40h399908c-34 25.3-64.7 57-92 95 --27.3 38-48.7 77.7-64 119-3.3 8.7-5 14-5 16zM0 241v40h399900v-40z`,rightharpoondownplus:`M399747 705c0 7.3 6.7 11 20 11 8 0 13-.8 - 15-2.5s4.7-6.8 8-15.5c40-94 99.3-166.3 178-217 13.3-8 20.3-12.3 21-13 5.3-3.3 - 8.5-5.8 9.5-7.5 1-1.7 1.5-5.2 1.5-10.5s-2.3-10.3-7-15H0v40h399908c-34 25.3 --64.7 57-92 95-27.3 38-48.7 77.7-64 119-3.3 8.7-5 14-5 16zM0 435v40h399900v-40z -m0-194v40h400000v-40zm0 0v40h400000v-40z`,righthook:`M399859 241c-764 0 0 0 0 0 40-3.3 68.7-15.7 86-37 10-12 15-25.3 - 15-40 0-22.7-9.8-40.7-29.5-54-19.7-13.3-43.5-21-71.5-23-17.3-1.3-26-8-26-20 0 --13.3 8.7-20 26-20 38 0 71 11.2 99 33.5 0 0 7 5.6 21 16.7 14 11.2 21 33.5 21 - 66.8s-14 61.2-42 83.5c-28 22.3-61 33.5-99 33.5L0 241z M0 281v-40h399859v40z`,rightlinesegment:`M399960 241 V94 h40 V428 h-40 V281 H0 v-40z -M399960 241 V94 h40 V428 h-40 V281 H0 v-40z`,rightToFrom:`M400000 167c-70.7-42-118-97.7-142-167h-23c-15.3 0-23 .3-23 - 1 0 1.3 5.3 13.7 16 37 18 35.3 41.3 69 70 101l7 8H0v40h399905l-7 8c-28.7 32 --52 65.7-70 101-10.7 23.3-16 35.7-16 37 0 .7 7.7 1 23 1h23c24-69.3 71.3-125 142 --167z M100 147v40h399900v-40zM0 341v40h399900v-40z`,twoheadleftarrow:`M0 167c68 40 - 115.7 95.7 143 167h22c15.3 0 23-.3 23-1 0-1.3-5.3-13.7-16-37-18-35.3-41.3-69 --70-101l-7-8h125l9 7c50.7 39.3 85 86 103 140h46c0-4.7-6.3-18.7-19-42-18-35.3 --40-67.3-66-96l-9-9h399716v-40H284l9-9c26-28.7 48-60.7 66-96 12.7-23.333 19 --37.333 19-42h-46c-18 54-52.3 100.7-103 140l-9 7H95l7-8c28.7-32 52-65.7 70-101 - 10.7-23.333 16-35.7 16-37 0-.7-7.7-1-23-1h-22C115.7 71.3 68 127 0 167z`,twoheadrightarrow:`M400000 167 -c-68-40-115.7-95.7-143-167h-22c-15.3 0-23 .3-23 1 0 1.3 5.3 13.7 16 37 18 35.3 - 41.3 69 70 101l7 8h-125l-9-7c-50.7-39.3-85-86-103-140h-46c0 4.7 6.3 18.7 19 42 - 18 35.3 40 67.3 66 96l9 9H0v40h399716l-9 9c-26 28.7-48 60.7-66 96-12.7 23.333 --19 37.333-19 42h46c18-54 52.3-100.7 103-140l9-7h125l-7 8c-28.7 32-52 65.7-70 - 101-10.7 23.333-16 35.7-16 37 0 .7 7.7 1 23 1h22c27.3-71.3 75-127 143-167z`,tilde1:`M200 55.538c-77 0-168 73.953-177 73.953-3 0-7 --2.175-9-5.437L2 97c-1-2-2-4-2-6 0-4 2-7 5-9l20-12C116 12 171 0 207 0c86 0 - 114 68 191 68 78 0 168-68 177-68 4 0 7 2 9 5l12 19c1 2.175 2 4.35 2 6.525 0 - 4.35-2 7.613-5 9.788l-19 13.05c-92 63.077-116.937 75.308-183 76.128 --68.267.847-113-73.952-191-73.952z`,tilde2:`M344 55.266c-142 0-300.638 81.316-311.5 86.418 --8.01 3.762-22.5 10.91-23.5 5.562L1 120c-1-2-1-3-1-4 0-5 3-9 8-10l18.4-9C160.9 - 31.9 283 0 358 0c148 0 188 122 331 122s314-97 326-97c4 0 8 2 10 7l7 21.114 -c1 2.14 1 3.21 1 4.28 0 5.347-3 9.626-7 10.696l-22.3 12.622C852.6 158.372 751 - 181.476 676 181.476c-149 0-189-126.21-332-126.21z`,tilde3:`M786 59C457 59 32 175.242 13 175.242c-6 0-10-3.457 --11-10.37L.15 138c-1-7 3-12 10-13l19.2-6.4C378.4 40.7 634.3 0 804.3 0c337 0 - 411.8 157 746.8 157 328 0 754-112 773-112 5 0 10 3 11 9l1 14.075c1 8.066-.697 - 16.595-6.697 17.492l-21.052 7.31c-367.9 98.146-609.15 122.696-778.15 122.696 - -338 0-409-156.573-744-156.573z`,tilde4:`M786 58C457 58 32 177.487 13 177.487c-6 0-10-3.345 --11-10.035L.15 143c-1-7 3-12 10-13l22-6.7C381.2 35 637.15 0 807.15 0c337 0 409 - 177 744 177 328 0 754-127 773-127 5 0 10 3 11 9l1 14.794c1 7.805-3 13.38-9 - 14.495l-20.7 5.574c-366.85 99.79-607.3 139.372-776.3 139.372-338 0-409 - -175.236-744-175.236z`,vec:`M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5 -3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11 -10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63 --1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1 --7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59 -H213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359 -c-16-25.333-24-45-24-59z`,widehat1:`M529 0h5l519 115c5 1 9 5 9 10 0 1-1 2-1 3l-4 22 -c-1 5-5 9-11 9h-2L532 67 19 159h-2c-5 0-9-4-11-9l-5-22c-1-6 2-12 8-13z`,widehat2:`M1181 0h2l1171 176c6 0 10 5 10 11l-2 23c-1 6-5 10 --11 10h-1L1182 67 15 220h-1c-6 0-10-4-11-10l-2-23c-1-6 4-11 10-11z`,widehat3:`M1181 0h2l1171 236c6 0 10 5 10 11l-2 23c-1 6-5 10 --11 10h-1L1182 67 15 280h-1c-6 0-10-4-11-10l-2-23c-1-6 4-11 10-11z`,widehat4:`M1181 0h2l1171 296c6 0 10 5 10 11l-2 23c-1 6-5 10 --11 10h-1L1182 67 15 340h-1c-6 0-10-4-11-10l-2-23c-1-6 4-11 10-11z`,widecheck1:`M529,159h5l519,-115c5,-1,9,-5,9,-10c0,-1,-1,-2,-1,-3l-4,-22c-1, --5,-5,-9,-11,-9h-2l-512,92l-513,-92h-2c-5,0,-9,4,-11,9l-5,22c-1,6,2,12,8,13z`,widecheck2:`M1181,220h2l1171,-176c6,0,10,-5,10,-11l-2,-23c-1,-6,-5,-10, --11,-10h-1l-1168,153l-1167,-153h-1c-6,0,-10,4,-11,10l-2,23c-1,6,4,11,10,11z`,widecheck3:`M1181,280h2l1171,-236c6,0,10,-5,10,-11l-2,-23c-1,-6,-5,-10, --11,-10h-1l-1168,213l-1167,-213h-1c-6,0,-10,4,-11,10l-2,23c-1,6,4,11,10,11z`,widecheck4:`M1181,340h2l1171,-296c6,0,10,-5,10,-11l-2,-23c-1,-6,-5,-10, --11,-10h-1l-1168,273l-1167,-273h-1c-6,0,-10,4,-11,10l-2,23c-1,6,4,11,10,11z`,baraboveleftarrow:`M400000 620h-399890l3 -3c68.7 -52.7 113.7 -120 135 -202 -c4 -14.7 6 -23 6 -25c0 -7.3 -7 -11 -21 -11c-8 0 -13.2 0.8 -15.5 2.5 -c-2.3 1.7 -4.2 5.8 -5.5 12.5c-1.3 4.7 -2.7 10.3 -4 17c-12 48.7 -34.8 92 -68.5 130 -s-74.2 66.3 -121.5 85c-10 4 -16 7.7 -18 11c0 8.7 6 14.3 18 17c47.3 18.7 87.8 47 -121.5 85s56.5 81.3 68.5 130c0.7 2 1.3 5 2 9s1.2 6.7 1.5 8c0.3 1.3 1 3.3 2 6 -s2.2 4.5 3.5 5.5c1.3 1 3.3 1.8 6 2.5s6 1 10 1c14 0 21 -3.7 21 -11 -c0 -2 -2 -10.3 -6 -25c-20 -79.3 -65 -146.7 -135 -202l-3 -3h399890z -M100 620v40h399900v-40z M0 241v40h399900v-40zM0 241v40h399900v-40z`,rightarrowabovebar:`M0 241v40h399891c-47.3 35.3-84 78-110 128-16.7 32 --27.7 63.7-33 95 0 1.3-.2 2.7-.5 4-.3 1.3-.5 2.3-.5 3 0 7.3 6.7 11 20 11 8 0 -13.2-.8 15.5-2.5 2.3-1.7 4.2-5.5 5.5-11.5 2-13.3 5.7-27 11-41 14.7-44.7 39 --84.5 73-119.5s73.7-60.2 119-75.5c6-2 9-5.7 9-11s-3-9-9-11c-45.3-15.3-85-40.5 --119-75.5s-58.3-74.8-73-119.5c-4.7-14-8.3-27.3-11-40-1.3-6.7-3.2-10.8-5.5 --12.5-2.3-1.7-7.5-2.5-15.5-2.5-14 0-21 3.7-21 11 0 2 2 10.3 6 25 20.7 83.3 67 -151.7 139 205zm96 379h399894v40H0zm0 0h399904v40H0z`,baraboveshortleftharpoon:`M507,435c-4,4,-6.3,8.7,-7,14c0,5.3,0.7,9,2,11 -c1.3,2,5.3,5.3,12,10c90.7,54,156,130,196,228c3.3,10.7,6.3,16.3,9,17 -c2,0.7,5,1,9,1c0,0,5,0,5,0c10.7,0,16.7,-2,18,-6c2,-2.7,1,-9.7,-3,-21 -c-32,-87.3,-82.7,-157.7,-152,-211c0,0,-3,-3,-3,-3l399351,0l0,-40 -c-398570,0,-399437,0,-399437,0z M593 435 v40 H399500 v-40z -M0 281 v-40 H399908 v40z M0 281 v-40 H399908 v40z`,rightharpoonaboveshortbar:`M0,241 l0,40c399126,0,399993,0,399993,0 -c4.7,-4.7,7,-9.3,7,-14c0,-9.3,-3.7,-15.3,-11,-18c-92.7,-56.7,-159,-133.7,-199, --231c-3.3,-9.3,-6,-14.7,-8,-16c-2,-1.3,-7,-2,-15,-2c-10.7,0,-16.7,2,-18,6 -c-2,2.7,-1,9.7,3,21c15.3,42,36.7,81.8,64,119.5c27.3,37.7,58,69.2,92,94.5z -M0 241 v40 H399908 v-40z M0 475 v-40 H399500 v40z M0 475 v-40 H399500 v40z`,shortbaraboveleftharpoon:`M7,435c-4,4,-6.3,8.7,-7,14c0,5.3,0.7,9,2,11 -c1.3,2,5.3,5.3,12,10c90.7,54,156,130,196,228c3.3,10.7,6.3,16.3,9,17c2,0.7,5,1,9, -1c0,0,5,0,5,0c10.7,0,16.7,-2,18,-6c2,-2.7,1,-9.7,-3,-21c-32,-87.3,-82.7,-157.7, --152,-211c0,0,-3,-3,-3,-3l399907,0l0,-40c-399126,0,-399993,0,-399993,0z -M93 435 v40 H400000 v-40z M500 241 v40 H400000 v-40z M500 241 v40 H400000 v-40z`,shortrightharpoonabovebar:`M53,241l0,40c398570,0,399437,0,399437,0 -c4.7,-4.7,7,-9.3,7,-14c0,-9.3,-3.7,-15.3,-11,-18c-92.7,-56.7,-159,-133.7,-199, --231c-3.3,-9.3,-6,-14.7,-8,-16c-2,-1.3,-7,-2,-15,-2c-10.7,0,-16.7,2,-18,6 -c-2,2.7,-1,9.7,3,21c15.3,42,36.7,81.8,64,119.5c27.3,37.7,58,69.2,92,94.5z -M500 241 v40 H399408 v-40z M500 435 v40 H400000 v-40z`},Ka=function(e,t){switch(e){case"lbrack":return"M403 1759 V84 H666 V0 H319 V1759 v"+t+` v1759 h347 v-84 -H403z M403 1759 V0 H319 V1759 v`+t+" v1759 h84z";case"rbrack":return"M347 1759 V0 H0 V84 H263 V1759 v"+t+` v1759 H0 v84 H347z -M347 1759 V0 H263 V1759 v`+t+" v1759 h84z";case"vert":return"M145 15 v585 v"+t+` v585 c2.667,10,9.667,15,21,15 -c10,0,16.667,-5,20,-15 v-585 v`+-t+` v-585 c-2.667,-10,-9.667,-15,-21,-15 -c-10,0,-16.667,5,-20,15z M188 15 H145 v585 v`+t+" v585 h43z";case"doublevert":return"M145 15 v585 v"+t+` v585 c2.667,10,9.667,15,21,15 -c10,0,16.667,-5,20,-15 v-585 v`+-t+` v-585 c-2.667,-10,-9.667,-15,-21,-15 -c-10,0,-16.667,5,-20,15z M188 15 H145 v585 v`+t+` v585 h43z -M367 15 v585 v`+t+` v585 c2.667,10,9.667,15,21,15 -c10,0,16.667,-5,20,-15 v-585 v`+-t+` v-585 c-2.667,-10,-9.667,-15,-21,-15 -c-10,0,-16.667,5,-20,15z M410 15 H367 v585 v`+t+" v585 h43z";case"lfloor":return"M319 602 V0 H403 V602 v"+t+` v1715 h263 v84 H319z -MM319 602 V0 H403 V602 v`+t+" v1715 H319z";case"rfloor":return"M319 602 V0 H403 V602 v"+t+` v1799 H0 v-84 H319z -MM319 602 V0 H403 V602 v`+t+" v1715 H319z";case"lceil":return"M403 1759 V84 H666 V0 H319 V1759 v"+t+` v602 h84z -M403 1759 V0 H319 V1759 v`+t+" v602 h84z";case"rceil":return"M347 1759 V0 H0 V84 H263 V1759 v"+t+` v602 h84z -M347 1759 V0 h-84 V1759 v`+t+" v602 h84z";case"lparen":return`M863,9c0,-2,-2,-5,-6,-9c0,0,-17,0,-17,0c-12.7,0,-19.3,0.3,-20,1 -c-5.3,5.3,-10.3,11,-15,17c-242.7,294.7,-395.3,682,-458,1162c-21.3,163.3,-33.3,349, --36,557 l0,`+(t+84)+`c0.2,6,0,26,0,60c2,159.3,10,310.7,24,454c53.3,528,210, -949.7,470,1265c4.7,6,9.7,11.7,15,17c0.7,0.7,7,1,19,1c0,0,18,0,18,0c4,-4,6,-7,6,-9 -c0,-2.7,-3.3,-8.7,-10,-18c-135.3,-192.7,-235.5,-414.3,-300.5,-665c-65,-250.7,-102.5, --544.7,-112.5,-882c-2,-104,-3,-167,-3,-189 -l0,-`+(t+92)+`c0,-162.7,5.7,-314,17,-454c20.7,-272,63.7,-513,129,-723c65.3, --210,155.3,-396.3,270,-559c6.7,-9.3,10,-15.3,10,-18z`;case"rparen":return`M76,0c-16.7,0,-25,3,-25,9c0,2,2,6.3,6,13c21.3,28.7,42.3,60.3, -63,95c96.7,156.7,172.8,332.5,228.5,527.5c55.7,195,92.8,416.5,111.5,664.5 -c11.3,139.3,17,290.7,17,454c0,28,1.7,43,3.3,45l0,`+(t+9)+` -c-3,4,-3.3,16.7,-3.3,38c0,162,-5.7,313.7,-17,455c-18.7,248,-55.8,469.3,-111.5,664 -c-55.7,194.7,-131.8,370.3,-228.5,527c-20.7,34.7,-41.7,66.3,-63,95c-2,3.3,-4,7,-6,11 -c0,7.3,5.7,11,17,11c0,0,11,0,11,0c9.3,0,14.3,-0.3,15,-1c5.3,-5.3,10.3,-11,15,-17 -c242.7,-294.7,395.3,-681.7,458,-1161c21.3,-164.7,33.3,-350.7,36,-558 -l0,-`+(t+144)+`c-2,-159.3,-10,-310.7,-24,-454c-53.3,-528,-210,-949.7, --470,-1265c-4.7,-6,-9.7,-11.7,-15,-17c-0.7,-0.7,-6.7,-1,-18,-1z`;default:throw new Error("Unknown stretchy delimiter.")}},Y0=class{constructor(e){this.children=void 0,this.classes=void 0,this.height=void 0,this.depth=void 0,this.maxFontSize=void 0,this.style=void 0,this.children=e,this.classes=[],this.height=0,this.depth=0,this.maxFontSize=0,this.style={}}hasClass(e){return O.contains(this.classes,e)}toNode(){for(var e=document.createDocumentFragment(),t=0;tt.toText();return this.children.map(e).join("")}},z0={"AMS-Regular":{32:[0,0,0,0,.25],65:[0,.68889,0,0,.72222],66:[0,.68889,0,0,.66667],67:[0,.68889,0,0,.72222],68:[0,.68889,0,0,.72222],69:[0,.68889,0,0,.66667],70:[0,.68889,0,0,.61111],71:[0,.68889,0,0,.77778],72:[0,.68889,0,0,.77778],73:[0,.68889,0,0,.38889],74:[.16667,.68889,0,0,.5],75:[0,.68889,0,0,.77778],76:[0,.68889,0,0,.66667],77:[0,.68889,0,0,.94445],78:[0,.68889,0,0,.72222],79:[.16667,.68889,0,0,.77778],80:[0,.68889,0,0,.61111],81:[.16667,.68889,0,0,.77778],82:[0,.68889,0,0,.72222],83:[0,.68889,0,0,.55556],84:[0,.68889,0,0,.66667],85:[0,.68889,0,0,.72222],86:[0,.68889,0,0,.72222],87:[0,.68889,0,0,1],88:[0,.68889,0,0,.72222],89:[0,.68889,0,0,.72222],90:[0,.68889,0,0,.66667],107:[0,.68889,0,0,.55556],160:[0,0,0,0,.25],165:[0,.675,.025,0,.75],174:[.15559,.69224,0,0,.94666],240:[0,.68889,0,0,.55556],295:[0,.68889,0,0,.54028],710:[0,.825,0,0,2.33334],732:[0,.9,0,0,2.33334],770:[0,.825,0,0,2.33334],771:[0,.9,0,0,2.33334],989:[.08167,.58167,0,0,.77778],1008:[0,.43056,.04028,0,.66667],8245:[0,.54986,0,0,.275],8463:[0,.68889,0,0,.54028],8487:[0,.68889,0,0,.72222],8498:[0,.68889,0,0,.55556],8502:[0,.68889,0,0,.66667],8503:[0,.68889,0,0,.44445],8504:[0,.68889,0,0,.66667],8513:[0,.68889,0,0,.63889],8592:[-.03598,.46402,0,0,.5],8594:[-.03598,.46402,0,0,.5],8602:[-.13313,.36687,0,0,1],8603:[-.13313,.36687,0,0,1],8606:[.01354,.52239,0,0,1],8608:[.01354,.52239,0,0,1],8610:[.01354,.52239,0,0,1.11111],8611:[.01354,.52239,0,0,1.11111],8619:[0,.54986,0,0,1],8620:[0,.54986,0,0,1],8621:[-.13313,.37788,0,0,1.38889],8622:[-.13313,.36687,0,0,1],8624:[0,.69224,0,0,.5],8625:[0,.69224,0,0,.5],8630:[0,.43056,0,0,1],8631:[0,.43056,0,0,1],8634:[.08198,.58198,0,0,.77778],8635:[.08198,.58198,0,0,.77778],8638:[.19444,.69224,0,0,.41667],8639:[.19444,.69224,0,0,.41667],8642:[.19444,.69224,0,0,.41667],8643:[.19444,.69224,0,0,.41667],8644:[.1808,.675,0,0,1],8646:[.1808,.675,0,0,1],8647:[.1808,.675,0,0,1],8648:[.19444,.69224,0,0,.83334],8649:[.1808,.675,0,0,1],8650:[.19444,.69224,0,0,.83334],8651:[.01354,.52239,0,0,1],8652:[.01354,.52239,0,0,1],8653:[-.13313,.36687,0,0,1],8654:[-.13313,.36687,0,0,1],8655:[-.13313,.36687,0,0,1],8666:[.13667,.63667,0,0,1],8667:[.13667,.63667,0,0,1],8669:[-.13313,.37788,0,0,1],8672:[-.064,.437,0,0,1.334],8674:[-.064,.437,0,0,1.334],8705:[0,.825,0,0,.5],8708:[0,.68889,0,0,.55556],8709:[.08167,.58167,0,0,.77778],8717:[0,.43056,0,0,.42917],8722:[-.03598,.46402,0,0,.5],8724:[.08198,.69224,0,0,.77778],8726:[.08167,.58167,0,0,.77778],8733:[0,.69224,0,0,.77778],8736:[0,.69224,0,0,.72222],8737:[0,.69224,0,0,.72222],8738:[.03517,.52239,0,0,.72222],8739:[.08167,.58167,0,0,.22222],8740:[.25142,.74111,0,0,.27778],8741:[.08167,.58167,0,0,.38889],8742:[.25142,.74111,0,0,.5],8756:[0,.69224,0,0,.66667],8757:[0,.69224,0,0,.66667],8764:[-.13313,.36687,0,0,.77778],8765:[-.13313,.37788,0,0,.77778],8769:[-.13313,.36687,0,0,.77778],8770:[-.03625,.46375,0,0,.77778],8774:[.30274,.79383,0,0,.77778],8776:[-.01688,.48312,0,0,.77778],8778:[.08167,.58167,0,0,.77778],8782:[.06062,.54986,0,0,.77778],8783:[.06062,.54986,0,0,.77778],8785:[.08198,.58198,0,0,.77778],8786:[.08198,.58198,0,0,.77778],8787:[.08198,.58198,0,0,.77778],8790:[0,.69224,0,0,.77778],8791:[.22958,.72958,0,0,.77778],8796:[.08198,.91667,0,0,.77778],8806:[.25583,.75583,0,0,.77778],8807:[.25583,.75583,0,0,.77778],8808:[.25142,.75726,0,0,.77778],8809:[.25142,.75726,0,0,.77778],8812:[.25583,.75583,0,0,.5],8814:[.20576,.70576,0,0,.77778],8815:[.20576,.70576,0,0,.77778],8816:[.30274,.79383,0,0,.77778],8817:[.30274,.79383,0,0,.77778],8818:[.22958,.72958,0,0,.77778],8819:[.22958,.72958,0,0,.77778],8822:[.1808,.675,0,0,.77778],8823:[.1808,.675,0,0,.77778],8828:[.13667,.63667,0,0,.77778],8829:[.13667,.63667,0,0,.77778],8830:[.22958,.72958,0,0,.77778],8831:[.22958,.72958,0,0,.77778],8832:[.20576,.70576,0,0,.77778],8833:[.20576,.70576,0,0,.77778],8840:[.30274,.79383,0,0,.77778],8841:[.30274,.79383,0,0,.77778],8842:[.13597,.63597,0,0,.77778],8843:[.13597,.63597,0,0,.77778],8847:[.03517,.54986,0,0,.77778],8848:[.03517,.54986,0,0,.77778],8858:[.08198,.58198,0,0,.77778],8859:[.08198,.58198,0,0,.77778],8861:[.08198,.58198,0,0,.77778],8862:[0,.675,0,0,.77778],8863:[0,.675,0,0,.77778],8864:[0,.675,0,0,.77778],8865:[0,.675,0,0,.77778],8872:[0,.69224,0,0,.61111],8873:[0,.69224,0,0,.72222],8874:[0,.69224,0,0,.88889],8876:[0,.68889,0,0,.61111],8877:[0,.68889,0,0,.61111],8878:[0,.68889,0,0,.72222],8879:[0,.68889,0,0,.72222],8882:[.03517,.54986,0,0,.77778],8883:[.03517,.54986,0,0,.77778],8884:[.13667,.63667,0,0,.77778],8885:[.13667,.63667,0,0,.77778],8888:[0,.54986,0,0,1.11111],8890:[.19444,.43056,0,0,.55556],8891:[.19444,.69224,0,0,.61111],8892:[.19444,.69224,0,0,.61111],8901:[0,.54986,0,0,.27778],8903:[.08167,.58167,0,0,.77778],8905:[.08167,.58167,0,0,.77778],8906:[.08167,.58167,0,0,.77778],8907:[0,.69224,0,0,.77778],8908:[0,.69224,0,0,.77778],8909:[-.03598,.46402,0,0,.77778],8910:[0,.54986,0,0,.76042],8911:[0,.54986,0,0,.76042],8912:[.03517,.54986,0,0,.77778],8913:[.03517,.54986,0,0,.77778],8914:[0,.54986,0,0,.66667],8915:[0,.54986,0,0,.66667],8916:[0,.69224,0,0,.66667],8918:[.0391,.5391,0,0,.77778],8919:[.0391,.5391,0,0,.77778],8920:[.03517,.54986,0,0,1.33334],8921:[.03517,.54986,0,0,1.33334],8922:[.38569,.88569,0,0,.77778],8923:[.38569,.88569,0,0,.77778],8926:[.13667,.63667,0,0,.77778],8927:[.13667,.63667,0,0,.77778],8928:[.30274,.79383,0,0,.77778],8929:[.30274,.79383,0,0,.77778],8934:[.23222,.74111,0,0,.77778],8935:[.23222,.74111,0,0,.77778],8936:[.23222,.74111,0,0,.77778],8937:[.23222,.74111,0,0,.77778],8938:[.20576,.70576,0,0,.77778],8939:[.20576,.70576,0,0,.77778],8940:[.30274,.79383,0,0,.77778],8941:[.30274,.79383,0,0,.77778],8994:[.19444,.69224,0,0,.77778],8995:[.19444,.69224,0,0,.77778],9416:[.15559,.69224,0,0,.90222],9484:[0,.69224,0,0,.5],9488:[0,.69224,0,0,.5],9492:[0,.37788,0,0,.5],9496:[0,.37788,0,0,.5],9585:[.19444,.68889,0,0,.88889],9586:[.19444,.74111,0,0,.88889],9632:[0,.675,0,0,.77778],9633:[0,.675,0,0,.77778],9650:[0,.54986,0,0,.72222],9651:[0,.54986,0,0,.72222],9654:[.03517,.54986,0,0,.77778],9660:[0,.54986,0,0,.72222],9661:[0,.54986,0,0,.72222],9664:[.03517,.54986,0,0,.77778],9674:[.11111,.69224,0,0,.66667],9733:[.19444,.69224,0,0,.94445],10003:[0,.69224,0,0,.83334],10016:[0,.69224,0,0,.83334],10731:[.11111,.69224,0,0,.66667],10846:[.19444,.75583,0,0,.61111],10877:[.13667,.63667,0,0,.77778],10878:[.13667,.63667,0,0,.77778],10885:[.25583,.75583,0,0,.77778],10886:[.25583,.75583,0,0,.77778],10887:[.13597,.63597,0,0,.77778],10888:[.13597,.63597,0,0,.77778],10889:[.26167,.75726,0,0,.77778],10890:[.26167,.75726,0,0,.77778],10891:[.48256,.98256,0,0,.77778],10892:[.48256,.98256,0,0,.77778],10901:[.13667,.63667,0,0,.77778],10902:[.13667,.63667,0,0,.77778],10933:[.25142,.75726,0,0,.77778],10934:[.25142,.75726,0,0,.77778],10935:[.26167,.75726,0,0,.77778],10936:[.26167,.75726,0,0,.77778],10937:[.26167,.75726,0,0,.77778],10938:[.26167,.75726,0,0,.77778],10949:[.25583,.75583,0,0,.77778],10950:[.25583,.75583,0,0,.77778],10955:[.28481,.79383,0,0,.77778],10956:[.28481,.79383,0,0,.77778],57350:[.08167,.58167,0,0,.22222],57351:[.08167,.58167,0,0,.38889],57352:[.08167,.58167,0,0,.77778],57353:[0,.43056,.04028,0,.66667],57356:[.25142,.75726,0,0,.77778],57357:[.25142,.75726,0,0,.77778],57358:[.41951,.91951,0,0,.77778],57359:[.30274,.79383,0,0,.77778],57360:[.30274,.79383,0,0,.77778],57361:[.41951,.91951,0,0,.77778],57366:[.25142,.75726,0,0,.77778],57367:[.25142,.75726,0,0,.77778],57368:[.25142,.75726,0,0,.77778],57369:[.25142,.75726,0,0,.77778],57370:[.13597,.63597,0,0,.77778],57371:[.13597,.63597,0,0,.77778]},"Caligraphic-Regular":{32:[0,0,0,0,.25],65:[0,.68333,0,.19445,.79847],66:[0,.68333,.03041,.13889,.65681],67:[0,.68333,.05834,.13889,.52653],68:[0,.68333,.02778,.08334,.77139],69:[0,.68333,.08944,.11111,.52778],70:[0,.68333,.09931,.11111,.71875],71:[.09722,.68333,.0593,.11111,.59487],72:[0,.68333,.00965,.11111,.84452],73:[0,.68333,.07382,0,.54452],74:[.09722,.68333,.18472,.16667,.67778],75:[0,.68333,.01445,.05556,.76195],76:[0,.68333,0,.13889,.68972],77:[0,.68333,0,.13889,1.2009],78:[0,.68333,.14736,.08334,.82049],79:[0,.68333,.02778,.11111,.79611],80:[0,.68333,.08222,.08334,.69556],81:[.09722,.68333,0,.11111,.81667],82:[0,.68333,0,.08334,.8475],83:[0,.68333,.075,.13889,.60556],84:[0,.68333,.25417,0,.54464],85:[0,.68333,.09931,.08334,.62583],86:[0,.68333,.08222,0,.61278],87:[0,.68333,.08222,.08334,.98778],88:[0,.68333,.14643,.13889,.7133],89:[.09722,.68333,.08222,.08334,.66834],90:[0,.68333,.07944,.13889,.72473],160:[0,0,0,0,.25]},"Fraktur-Regular":{32:[0,0,0,0,.25],33:[0,.69141,0,0,.29574],34:[0,.69141,0,0,.21471],38:[0,.69141,0,0,.73786],39:[0,.69141,0,0,.21201],40:[.24982,.74947,0,0,.38865],41:[.24982,.74947,0,0,.38865],42:[0,.62119,0,0,.27764],43:[.08319,.58283,0,0,.75623],44:[0,.10803,0,0,.27764],45:[.08319,.58283,0,0,.75623],46:[0,.10803,0,0,.27764],47:[.24982,.74947,0,0,.50181],48:[0,.47534,0,0,.50181],49:[0,.47534,0,0,.50181],50:[0,.47534,0,0,.50181],51:[.18906,.47534,0,0,.50181],52:[.18906,.47534,0,0,.50181],53:[.18906,.47534,0,0,.50181],54:[0,.69141,0,0,.50181],55:[.18906,.47534,0,0,.50181],56:[0,.69141,0,0,.50181],57:[.18906,.47534,0,0,.50181],58:[0,.47534,0,0,.21606],59:[.12604,.47534,0,0,.21606],61:[-.13099,.36866,0,0,.75623],63:[0,.69141,0,0,.36245],65:[0,.69141,0,0,.7176],66:[0,.69141,0,0,.88397],67:[0,.69141,0,0,.61254],68:[0,.69141,0,0,.83158],69:[0,.69141,0,0,.66278],70:[.12604,.69141,0,0,.61119],71:[0,.69141,0,0,.78539],72:[.06302,.69141,0,0,.7203],73:[0,.69141,0,0,.55448],74:[.12604,.69141,0,0,.55231],75:[0,.69141,0,0,.66845],76:[0,.69141,0,0,.66602],77:[0,.69141,0,0,1.04953],78:[0,.69141,0,0,.83212],79:[0,.69141,0,0,.82699],80:[.18906,.69141,0,0,.82753],81:[.03781,.69141,0,0,.82699],82:[0,.69141,0,0,.82807],83:[0,.69141,0,0,.82861],84:[0,.69141,0,0,.66899],85:[0,.69141,0,0,.64576],86:[0,.69141,0,0,.83131],87:[0,.69141,0,0,1.04602],88:[0,.69141,0,0,.71922],89:[.18906,.69141,0,0,.83293],90:[.12604,.69141,0,0,.60201],91:[.24982,.74947,0,0,.27764],93:[.24982,.74947,0,0,.27764],94:[0,.69141,0,0,.49965],97:[0,.47534,0,0,.50046],98:[0,.69141,0,0,.51315],99:[0,.47534,0,0,.38946],100:[0,.62119,0,0,.49857],101:[0,.47534,0,0,.40053],102:[.18906,.69141,0,0,.32626],103:[.18906,.47534,0,0,.5037],104:[.18906,.69141,0,0,.52126],105:[0,.69141,0,0,.27899],106:[0,.69141,0,0,.28088],107:[0,.69141,0,0,.38946],108:[0,.69141,0,0,.27953],109:[0,.47534,0,0,.76676],110:[0,.47534,0,0,.52666],111:[0,.47534,0,0,.48885],112:[.18906,.52396,0,0,.50046],113:[.18906,.47534,0,0,.48912],114:[0,.47534,0,0,.38919],115:[0,.47534,0,0,.44266],116:[0,.62119,0,0,.33301],117:[0,.47534,0,0,.5172],118:[0,.52396,0,0,.5118],119:[0,.52396,0,0,.77351],120:[.18906,.47534,0,0,.38865],121:[.18906,.47534,0,0,.49884],122:[.18906,.47534,0,0,.39054],160:[0,0,0,0,.25],8216:[0,.69141,0,0,.21471],8217:[0,.69141,0,0,.21471],58112:[0,.62119,0,0,.49749],58113:[0,.62119,0,0,.4983],58114:[.18906,.69141,0,0,.33328],58115:[.18906,.69141,0,0,.32923],58116:[.18906,.47534,0,0,.50343],58117:[0,.69141,0,0,.33301],58118:[0,.62119,0,0,.33409],58119:[0,.47534,0,0,.50073]},"Main-Bold":{32:[0,0,0,0,.25],33:[0,.69444,0,0,.35],34:[0,.69444,0,0,.60278],35:[.19444,.69444,0,0,.95833],36:[.05556,.75,0,0,.575],37:[.05556,.75,0,0,.95833],38:[0,.69444,0,0,.89444],39:[0,.69444,0,0,.31944],40:[.25,.75,0,0,.44722],41:[.25,.75,0,0,.44722],42:[0,.75,0,0,.575],43:[.13333,.63333,0,0,.89444],44:[.19444,.15556,0,0,.31944],45:[0,.44444,0,0,.38333],46:[0,.15556,0,0,.31944],47:[.25,.75,0,0,.575],48:[0,.64444,0,0,.575],49:[0,.64444,0,0,.575],50:[0,.64444,0,0,.575],51:[0,.64444,0,0,.575],52:[0,.64444,0,0,.575],53:[0,.64444,0,0,.575],54:[0,.64444,0,0,.575],55:[0,.64444,0,0,.575],56:[0,.64444,0,0,.575],57:[0,.64444,0,0,.575],58:[0,.44444,0,0,.31944],59:[.19444,.44444,0,0,.31944],60:[.08556,.58556,0,0,.89444],61:[-.10889,.39111,0,0,.89444],62:[.08556,.58556,0,0,.89444],63:[0,.69444,0,0,.54305],64:[0,.69444,0,0,.89444],65:[0,.68611,0,0,.86944],66:[0,.68611,0,0,.81805],67:[0,.68611,0,0,.83055],68:[0,.68611,0,0,.88194],69:[0,.68611,0,0,.75555],70:[0,.68611,0,0,.72361],71:[0,.68611,0,0,.90416],72:[0,.68611,0,0,.9],73:[0,.68611,0,0,.43611],74:[0,.68611,0,0,.59444],75:[0,.68611,0,0,.90138],76:[0,.68611,0,0,.69166],77:[0,.68611,0,0,1.09166],78:[0,.68611,0,0,.9],79:[0,.68611,0,0,.86388],80:[0,.68611,0,0,.78611],81:[.19444,.68611,0,0,.86388],82:[0,.68611,0,0,.8625],83:[0,.68611,0,0,.63889],84:[0,.68611,0,0,.8],85:[0,.68611,0,0,.88472],86:[0,.68611,.01597,0,.86944],87:[0,.68611,.01597,0,1.18888],88:[0,.68611,0,0,.86944],89:[0,.68611,.02875,0,.86944],90:[0,.68611,0,0,.70277],91:[.25,.75,0,0,.31944],92:[.25,.75,0,0,.575],93:[.25,.75,0,0,.31944],94:[0,.69444,0,0,.575],95:[.31,.13444,.03194,0,.575],97:[0,.44444,0,0,.55902],98:[0,.69444,0,0,.63889],99:[0,.44444,0,0,.51111],100:[0,.69444,0,0,.63889],101:[0,.44444,0,0,.52708],102:[0,.69444,.10903,0,.35139],103:[.19444,.44444,.01597,0,.575],104:[0,.69444,0,0,.63889],105:[0,.69444,0,0,.31944],106:[.19444,.69444,0,0,.35139],107:[0,.69444,0,0,.60694],108:[0,.69444,0,0,.31944],109:[0,.44444,0,0,.95833],110:[0,.44444,0,0,.63889],111:[0,.44444,0,0,.575],112:[.19444,.44444,0,0,.63889],113:[.19444,.44444,0,0,.60694],114:[0,.44444,0,0,.47361],115:[0,.44444,0,0,.45361],116:[0,.63492,0,0,.44722],117:[0,.44444,0,0,.63889],118:[0,.44444,.01597,0,.60694],119:[0,.44444,.01597,0,.83055],120:[0,.44444,0,0,.60694],121:[.19444,.44444,.01597,0,.60694],122:[0,.44444,0,0,.51111],123:[.25,.75,0,0,.575],124:[.25,.75,0,0,.31944],125:[.25,.75,0,0,.575],126:[.35,.34444,0,0,.575],160:[0,0,0,0,.25],163:[0,.69444,0,0,.86853],168:[0,.69444,0,0,.575],172:[0,.44444,0,0,.76666],176:[0,.69444,0,0,.86944],177:[.13333,.63333,0,0,.89444],184:[.17014,0,0,0,.51111],198:[0,.68611,0,0,1.04166],215:[.13333,.63333,0,0,.89444],216:[.04861,.73472,0,0,.89444],223:[0,.69444,0,0,.59722],230:[0,.44444,0,0,.83055],247:[.13333,.63333,0,0,.89444],248:[.09722,.54167,0,0,.575],305:[0,.44444,0,0,.31944],338:[0,.68611,0,0,1.16944],339:[0,.44444,0,0,.89444],567:[.19444,.44444,0,0,.35139],710:[0,.69444,0,0,.575],711:[0,.63194,0,0,.575],713:[0,.59611,0,0,.575],714:[0,.69444,0,0,.575],715:[0,.69444,0,0,.575],728:[0,.69444,0,0,.575],729:[0,.69444,0,0,.31944],730:[0,.69444,0,0,.86944],732:[0,.69444,0,0,.575],733:[0,.69444,0,0,.575],915:[0,.68611,0,0,.69166],916:[0,.68611,0,0,.95833],920:[0,.68611,0,0,.89444],923:[0,.68611,0,0,.80555],926:[0,.68611,0,0,.76666],928:[0,.68611,0,0,.9],931:[0,.68611,0,0,.83055],933:[0,.68611,0,0,.89444],934:[0,.68611,0,0,.83055],936:[0,.68611,0,0,.89444],937:[0,.68611,0,0,.83055],8211:[0,.44444,.03194,0,.575],8212:[0,.44444,.03194,0,1.14999],8216:[0,.69444,0,0,.31944],8217:[0,.69444,0,0,.31944],8220:[0,.69444,0,0,.60278],8221:[0,.69444,0,0,.60278],8224:[.19444,.69444,0,0,.51111],8225:[.19444,.69444,0,0,.51111],8242:[0,.55556,0,0,.34444],8407:[0,.72444,.15486,0,.575],8463:[0,.69444,0,0,.66759],8465:[0,.69444,0,0,.83055],8467:[0,.69444,0,0,.47361],8472:[.19444,.44444,0,0,.74027],8476:[0,.69444,0,0,.83055],8501:[0,.69444,0,0,.70277],8592:[-.10889,.39111,0,0,1.14999],8593:[.19444,.69444,0,0,.575],8594:[-.10889,.39111,0,0,1.14999],8595:[.19444,.69444,0,0,.575],8596:[-.10889,.39111,0,0,1.14999],8597:[.25,.75,0,0,.575],8598:[.19444,.69444,0,0,1.14999],8599:[.19444,.69444,0,0,1.14999],8600:[.19444,.69444,0,0,1.14999],8601:[.19444,.69444,0,0,1.14999],8636:[-.10889,.39111,0,0,1.14999],8637:[-.10889,.39111,0,0,1.14999],8640:[-.10889,.39111,0,0,1.14999],8641:[-.10889,.39111,0,0,1.14999],8656:[-.10889,.39111,0,0,1.14999],8657:[.19444,.69444,0,0,.70277],8658:[-.10889,.39111,0,0,1.14999],8659:[.19444,.69444,0,0,.70277],8660:[-.10889,.39111,0,0,1.14999],8661:[.25,.75,0,0,.70277],8704:[0,.69444,0,0,.63889],8706:[0,.69444,.06389,0,.62847],8707:[0,.69444,0,0,.63889],8709:[.05556,.75,0,0,.575],8711:[0,.68611,0,0,.95833],8712:[.08556,.58556,0,0,.76666],8715:[.08556,.58556,0,0,.76666],8722:[.13333,.63333,0,0,.89444],8723:[.13333,.63333,0,0,.89444],8725:[.25,.75,0,0,.575],8726:[.25,.75,0,0,.575],8727:[-.02778,.47222,0,0,.575],8728:[-.02639,.47361,0,0,.575],8729:[-.02639,.47361,0,0,.575],8730:[.18,.82,0,0,.95833],8733:[0,.44444,0,0,.89444],8734:[0,.44444,0,0,1.14999],8736:[0,.69224,0,0,.72222],8739:[.25,.75,0,0,.31944],8741:[.25,.75,0,0,.575],8743:[0,.55556,0,0,.76666],8744:[0,.55556,0,0,.76666],8745:[0,.55556,0,0,.76666],8746:[0,.55556,0,0,.76666],8747:[.19444,.69444,.12778,0,.56875],8764:[-.10889,.39111,0,0,.89444],8768:[.19444,.69444,0,0,.31944],8771:[.00222,.50222,0,0,.89444],8773:[.027,.638,0,0,.894],8776:[.02444,.52444,0,0,.89444],8781:[.00222,.50222,0,0,.89444],8801:[.00222,.50222,0,0,.89444],8804:[.19667,.69667,0,0,.89444],8805:[.19667,.69667,0,0,.89444],8810:[.08556,.58556,0,0,1.14999],8811:[.08556,.58556,0,0,1.14999],8826:[.08556,.58556,0,0,.89444],8827:[.08556,.58556,0,0,.89444],8834:[.08556,.58556,0,0,.89444],8835:[.08556,.58556,0,0,.89444],8838:[.19667,.69667,0,0,.89444],8839:[.19667,.69667,0,0,.89444],8846:[0,.55556,0,0,.76666],8849:[.19667,.69667,0,0,.89444],8850:[.19667,.69667,0,0,.89444],8851:[0,.55556,0,0,.76666],8852:[0,.55556,0,0,.76666],8853:[.13333,.63333,0,0,.89444],8854:[.13333,.63333,0,0,.89444],8855:[.13333,.63333,0,0,.89444],8856:[.13333,.63333,0,0,.89444],8857:[.13333,.63333,0,0,.89444],8866:[0,.69444,0,0,.70277],8867:[0,.69444,0,0,.70277],8868:[0,.69444,0,0,.89444],8869:[0,.69444,0,0,.89444],8900:[-.02639,.47361,0,0,.575],8901:[-.02639,.47361,0,0,.31944],8902:[-.02778,.47222,0,0,.575],8968:[.25,.75,0,0,.51111],8969:[.25,.75,0,0,.51111],8970:[.25,.75,0,0,.51111],8971:[.25,.75,0,0,.51111],8994:[-.13889,.36111,0,0,1.14999],8995:[-.13889,.36111,0,0,1.14999],9651:[.19444,.69444,0,0,1.02222],9657:[-.02778,.47222,0,0,.575],9661:[.19444,.69444,0,0,1.02222],9667:[-.02778,.47222,0,0,.575],9711:[.19444,.69444,0,0,1.14999],9824:[.12963,.69444,0,0,.89444],9825:[.12963,.69444,0,0,.89444],9826:[.12963,.69444,0,0,.89444],9827:[.12963,.69444,0,0,.89444],9837:[0,.75,0,0,.44722],9838:[.19444,.69444,0,0,.44722],9839:[.19444,.69444,0,0,.44722],10216:[.25,.75,0,0,.44722],10217:[.25,.75,0,0,.44722],10815:[0,.68611,0,0,.9],10927:[.19667,.69667,0,0,.89444],10928:[.19667,.69667,0,0,.89444],57376:[.19444,.69444,0,0,0]},"Main-BoldItalic":{32:[0,0,0,0,.25],33:[0,.69444,.11417,0,.38611],34:[0,.69444,.07939,0,.62055],35:[.19444,.69444,.06833,0,.94444],37:[.05556,.75,.12861,0,.94444],38:[0,.69444,.08528,0,.88555],39:[0,.69444,.12945,0,.35555],40:[.25,.75,.15806,0,.47333],41:[.25,.75,.03306,0,.47333],42:[0,.75,.14333,0,.59111],43:[.10333,.60333,.03306,0,.88555],44:[.19444,.14722,0,0,.35555],45:[0,.44444,.02611,0,.41444],46:[0,.14722,0,0,.35555],47:[.25,.75,.15806,0,.59111],48:[0,.64444,.13167,0,.59111],49:[0,.64444,.13167,0,.59111],50:[0,.64444,.13167,0,.59111],51:[0,.64444,.13167,0,.59111],52:[.19444,.64444,.13167,0,.59111],53:[0,.64444,.13167,0,.59111],54:[0,.64444,.13167,0,.59111],55:[.19444,.64444,.13167,0,.59111],56:[0,.64444,.13167,0,.59111],57:[0,.64444,.13167,0,.59111],58:[0,.44444,.06695,0,.35555],59:[.19444,.44444,.06695,0,.35555],61:[-.10889,.39111,.06833,0,.88555],63:[0,.69444,.11472,0,.59111],64:[0,.69444,.09208,0,.88555],65:[0,.68611,0,0,.86555],66:[0,.68611,.0992,0,.81666],67:[0,.68611,.14208,0,.82666],68:[0,.68611,.09062,0,.87555],69:[0,.68611,.11431,0,.75666],70:[0,.68611,.12903,0,.72722],71:[0,.68611,.07347,0,.89527],72:[0,.68611,.17208,0,.8961],73:[0,.68611,.15681,0,.47166],74:[0,.68611,.145,0,.61055],75:[0,.68611,.14208,0,.89499],76:[0,.68611,0,0,.69777],77:[0,.68611,.17208,0,1.07277],78:[0,.68611,.17208,0,.8961],79:[0,.68611,.09062,0,.85499],80:[0,.68611,.0992,0,.78721],81:[.19444,.68611,.09062,0,.85499],82:[0,.68611,.02559,0,.85944],83:[0,.68611,.11264,0,.64999],84:[0,.68611,.12903,0,.7961],85:[0,.68611,.17208,0,.88083],86:[0,.68611,.18625,0,.86555],87:[0,.68611,.18625,0,1.15999],88:[0,.68611,.15681,0,.86555],89:[0,.68611,.19803,0,.86555],90:[0,.68611,.14208,0,.70888],91:[.25,.75,.1875,0,.35611],93:[.25,.75,.09972,0,.35611],94:[0,.69444,.06709,0,.59111],95:[.31,.13444,.09811,0,.59111],97:[0,.44444,.09426,0,.59111],98:[0,.69444,.07861,0,.53222],99:[0,.44444,.05222,0,.53222],100:[0,.69444,.10861,0,.59111],101:[0,.44444,.085,0,.53222],102:[.19444,.69444,.21778,0,.4],103:[.19444,.44444,.105,0,.53222],104:[0,.69444,.09426,0,.59111],105:[0,.69326,.11387,0,.35555],106:[.19444,.69326,.1672,0,.35555],107:[0,.69444,.11111,0,.53222],108:[0,.69444,.10861,0,.29666],109:[0,.44444,.09426,0,.94444],110:[0,.44444,.09426,0,.64999],111:[0,.44444,.07861,0,.59111],112:[.19444,.44444,.07861,0,.59111],113:[.19444,.44444,.105,0,.53222],114:[0,.44444,.11111,0,.50167],115:[0,.44444,.08167,0,.48694],116:[0,.63492,.09639,0,.385],117:[0,.44444,.09426,0,.62055],118:[0,.44444,.11111,0,.53222],119:[0,.44444,.11111,0,.76777],120:[0,.44444,.12583,0,.56055],121:[.19444,.44444,.105,0,.56166],122:[0,.44444,.13889,0,.49055],126:[.35,.34444,.11472,0,.59111],160:[0,0,0,0,.25],168:[0,.69444,.11473,0,.59111],176:[0,.69444,0,0,.94888],184:[.17014,0,0,0,.53222],198:[0,.68611,.11431,0,1.02277],216:[.04861,.73472,.09062,0,.88555],223:[.19444,.69444,.09736,0,.665],230:[0,.44444,.085,0,.82666],248:[.09722,.54167,.09458,0,.59111],305:[0,.44444,.09426,0,.35555],338:[0,.68611,.11431,0,1.14054],339:[0,.44444,.085,0,.82666],567:[.19444,.44444,.04611,0,.385],710:[0,.69444,.06709,0,.59111],711:[0,.63194,.08271,0,.59111],713:[0,.59444,.10444,0,.59111],714:[0,.69444,.08528,0,.59111],715:[0,.69444,0,0,.59111],728:[0,.69444,.10333,0,.59111],729:[0,.69444,.12945,0,.35555],730:[0,.69444,0,0,.94888],732:[0,.69444,.11472,0,.59111],733:[0,.69444,.11472,0,.59111],915:[0,.68611,.12903,0,.69777],916:[0,.68611,0,0,.94444],920:[0,.68611,.09062,0,.88555],923:[0,.68611,0,0,.80666],926:[0,.68611,.15092,0,.76777],928:[0,.68611,.17208,0,.8961],931:[0,.68611,.11431,0,.82666],933:[0,.68611,.10778,0,.88555],934:[0,.68611,.05632,0,.82666],936:[0,.68611,.10778,0,.88555],937:[0,.68611,.0992,0,.82666],8211:[0,.44444,.09811,0,.59111],8212:[0,.44444,.09811,0,1.18221],8216:[0,.69444,.12945,0,.35555],8217:[0,.69444,.12945,0,.35555],8220:[0,.69444,.16772,0,.62055],8221:[0,.69444,.07939,0,.62055]},"Main-Italic":{32:[0,0,0,0,.25],33:[0,.69444,.12417,0,.30667],34:[0,.69444,.06961,0,.51444],35:[.19444,.69444,.06616,0,.81777],37:[.05556,.75,.13639,0,.81777],38:[0,.69444,.09694,0,.76666],39:[0,.69444,.12417,0,.30667],40:[.25,.75,.16194,0,.40889],41:[.25,.75,.03694,0,.40889],42:[0,.75,.14917,0,.51111],43:[.05667,.56167,.03694,0,.76666],44:[.19444,.10556,0,0,.30667],45:[0,.43056,.02826,0,.35778],46:[0,.10556,0,0,.30667],47:[.25,.75,.16194,0,.51111],48:[0,.64444,.13556,0,.51111],49:[0,.64444,.13556,0,.51111],50:[0,.64444,.13556,0,.51111],51:[0,.64444,.13556,0,.51111],52:[.19444,.64444,.13556,0,.51111],53:[0,.64444,.13556,0,.51111],54:[0,.64444,.13556,0,.51111],55:[.19444,.64444,.13556,0,.51111],56:[0,.64444,.13556,0,.51111],57:[0,.64444,.13556,0,.51111],58:[0,.43056,.0582,0,.30667],59:[.19444,.43056,.0582,0,.30667],61:[-.13313,.36687,.06616,0,.76666],63:[0,.69444,.1225,0,.51111],64:[0,.69444,.09597,0,.76666],65:[0,.68333,0,0,.74333],66:[0,.68333,.10257,0,.70389],67:[0,.68333,.14528,0,.71555],68:[0,.68333,.09403,0,.755],69:[0,.68333,.12028,0,.67833],70:[0,.68333,.13305,0,.65277],71:[0,.68333,.08722,0,.77361],72:[0,.68333,.16389,0,.74333],73:[0,.68333,.15806,0,.38555],74:[0,.68333,.14028,0,.525],75:[0,.68333,.14528,0,.76888],76:[0,.68333,0,0,.62722],77:[0,.68333,.16389,0,.89666],78:[0,.68333,.16389,0,.74333],79:[0,.68333,.09403,0,.76666],80:[0,.68333,.10257,0,.67833],81:[.19444,.68333,.09403,0,.76666],82:[0,.68333,.03868,0,.72944],83:[0,.68333,.11972,0,.56222],84:[0,.68333,.13305,0,.71555],85:[0,.68333,.16389,0,.74333],86:[0,.68333,.18361,0,.74333],87:[0,.68333,.18361,0,.99888],88:[0,.68333,.15806,0,.74333],89:[0,.68333,.19383,0,.74333],90:[0,.68333,.14528,0,.61333],91:[.25,.75,.1875,0,.30667],93:[.25,.75,.10528,0,.30667],94:[0,.69444,.06646,0,.51111],95:[.31,.12056,.09208,0,.51111],97:[0,.43056,.07671,0,.51111],98:[0,.69444,.06312,0,.46],99:[0,.43056,.05653,0,.46],100:[0,.69444,.10333,0,.51111],101:[0,.43056,.07514,0,.46],102:[.19444,.69444,.21194,0,.30667],103:[.19444,.43056,.08847,0,.46],104:[0,.69444,.07671,0,.51111],105:[0,.65536,.1019,0,.30667],106:[.19444,.65536,.14467,0,.30667],107:[0,.69444,.10764,0,.46],108:[0,.69444,.10333,0,.25555],109:[0,.43056,.07671,0,.81777],110:[0,.43056,.07671,0,.56222],111:[0,.43056,.06312,0,.51111],112:[.19444,.43056,.06312,0,.51111],113:[.19444,.43056,.08847,0,.46],114:[0,.43056,.10764,0,.42166],115:[0,.43056,.08208,0,.40889],116:[0,.61508,.09486,0,.33222],117:[0,.43056,.07671,0,.53666],118:[0,.43056,.10764,0,.46],119:[0,.43056,.10764,0,.66444],120:[0,.43056,.12042,0,.46389],121:[.19444,.43056,.08847,0,.48555],122:[0,.43056,.12292,0,.40889],126:[.35,.31786,.11585,0,.51111],160:[0,0,0,0,.25],168:[0,.66786,.10474,0,.51111],176:[0,.69444,0,0,.83129],184:[.17014,0,0,0,.46],198:[0,.68333,.12028,0,.88277],216:[.04861,.73194,.09403,0,.76666],223:[.19444,.69444,.10514,0,.53666],230:[0,.43056,.07514,0,.71555],248:[.09722,.52778,.09194,0,.51111],338:[0,.68333,.12028,0,.98499],339:[0,.43056,.07514,0,.71555],710:[0,.69444,.06646,0,.51111],711:[0,.62847,.08295,0,.51111],713:[0,.56167,.10333,0,.51111],714:[0,.69444,.09694,0,.51111],715:[0,.69444,0,0,.51111],728:[0,.69444,.10806,0,.51111],729:[0,.66786,.11752,0,.30667],730:[0,.69444,0,0,.83129],732:[0,.66786,.11585,0,.51111],733:[0,.69444,.1225,0,.51111],915:[0,.68333,.13305,0,.62722],916:[0,.68333,0,0,.81777],920:[0,.68333,.09403,0,.76666],923:[0,.68333,0,0,.69222],926:[0,.68333,.15294,0,.66444],928:[0,.68333,.16389,0,.74333],931:[0,.68333,.12028,0,.71555],933:[0,.68333,.11111,0,.76666],934:[0,.68333,.05986,0,.71555],936:[0,.68333,.11111,0,.76666],937:[0,.68333,.10257,0,.71555],8211:[0,.43056,.09208,0,.51111],8212:[0,.43056,.09208,0,1.02222],8216:[0,.69444,.12417,0,.30667],8217:[0,.69444,.12417,0,.30667],8220:[0,.69444,.1685,0,.51444],8221:[0,.69444,.06961,0,.51444],8463:[0,.68889,0,0,.54028]},"Main-Regular":{32:[0,0,0,0,.25],33:[0,.69444,0,0,.27778],34:[0,.69444,0,0,.5],35:[.19444,.69444,0,0,.83334],36:[.05556,.75,0,0,.5],37:[.05556,.75,0,0,.83334],38:[0,.69444,0,0,.77778],39:[0,.69444,0,0,.27778],40:[.25,.75,0,0,.38889],41:[.25,.75,0,0,.38889],42:[0,.75,0,0,.5],43:[.08333,.58333,0,0,.77778],44:[.19444,.10556,0,0,.27778],45:[0,.43056,0,0,.33333],46:[0,.10556,0,0,.27778],47:[.25,.75,0,0,.5],48:[0,.64444,0,0,.5],49:[0,.64444,0,0,.5],50:[0,.64444,0,0,.5],51:[0,.64444,0,0,.5],52:[0,.64444,0,0,.5],53:[0,.64444,0,0,.5],54:[0,.64444,0,0,.5],55:[0,.64444,0,0,.5],56:[0,.64444,0,0,.5],57:[0,.64444,0,0,.5],58:[0,.43056,0,0,.27778],59:[.19444,.43056,0,0,.27778],60:[.0391,.5391,0,0,.77778],61:[-.13313,.36687,0,0,.77778],62:[.0391,.5391,0,0,.77778],63:[0,.69444,0,0,.47222],64:[0,.69444,0,0,.77778],65:[0,.68333,0,0,.75],66:[0,.68333,0,0,.70834],67:[0,.68333,0,0,.72222],68:[0,.68333,0,0,.76389],69:[0,.68333,0,0,.68056],70:[0,.68333,0,0,.65278],71:[0,.68333,0,0,.78472],72:[0,.68333,0,0,.75],73:[0,.68333,0,0,.36111],74:[0,.68333,0,0,.51389],75:[0,.68333,0,0,.77778],76:[0,.68333,0,0,.625],77:[0,.68333,0,0,.91667],78:[0,.68333,0,0,.75],79:[0,.68333,0,0,.77778],80:[0,.68333,0,0,.68056],81:[.19444,.68333,0,0,.77778],82:[0,.68333,0,0,.73611],83:[0,.68333,0,0,.55556],84:[0,.68333,0,0,.72222],85:[0,.68333,0,0,.75],86:[0,.68333,.01389,0,.75],87:[0,.68333,.01389,0,1.02778],88:[0,.68333,0,0,.75],89:[0,.68333,.025,0,.75],90:[0,.68333,0,0,.61111],91:[.25,.75,0,0,.27778],92:[.25,.75,0,0,.5],93:[.25,.75,0,0,.27778],94:[0,.69444,0,0,.5],95:[.31,.12056,.02778,0,.5],97:[0,.43056,0,0,.5],98:[0,.69444,0,0,.55556],99:[0,.43056,0,0,.44445],100:[0,.69444,0,0,.55556],101:[0,.43056,0,0,.44445],102:[0,.69444,.07778,0,.30556],103:[.19444,.43056,.01389,0,.5],104:[0,.69444,0,0,.55556],105:[0,.66786,0,0,.27778],106:[.19444,.66786,0,0,.30556],107:[0,.69444,0,0,.52778],108:[0,.69444,0,0,.27778],109:[0,.43056,0,0,.83334],110:[0,.43056,0,0,.55556],111:[0,.43056,0,0,.5],112:[.19444,.43056,0,0,.55556],113:[.19444,.43056,0,0,.52778],114:[0,.43056,0,0,.39167],115:[0,.43056,0,0,.39445],116:[0,.61508,0,0,.38889],117:[0,.43056,0,0,.55556],118:[0,.43056,.01389,0,.52778],119:[0,.43056,.01389,0,.72222],120:[0,.43056,0,0,.52778],121:[.19444,.43056,.01389,0,.52778],122:[0,.43056,0,0,.44445],123:[.25,.75,0,0,.5],124:[.25,.75,0,0,.27778],125:[.25,.75,0,0,.5],126:[.35,.31786,0,0,.5],160:[0,0,0,0,.25],163:[0,.69444,0,0,.76909],167:[.19444,.69444,0,0,.44445],168:[0,.66786,0,0,.5],172:[0,.43056,0,0,.66667],176:[0,.69444,0,0,.75],177:[.08333,.58333,0,0,.77778],182:[.19444,.69444,0,0,.61111],184:[.17014,0,0,0,.44445],198:[0,.68333,0,0,.90278],215:[.08333,.58333,0,0,.77778],216:[.04861,.73194,0,0,.77778],223:[0,.69444,0,0,.5],230:[0,.43056,0,0,.72222],247:[.08333,.58333,0,0,.77778],248:[.09722,.52778,0,0,.5],305:[0,.43056,0,0,.27778],338:[0,.68333,0,0,1.01389],339:[0,.43056,0,0,.77778],567:[.19444,.43056,0,0,.30556],710:[0,.69444,0,0,.5],711:[0,.62847,0,0,.5],713:[0,.56778,0,0,.5],714:[0,.69444,0,0,.5],715:[0,.69444,0,0,.5],728:[0,.69444,0,0,.5],729:[0,.66786,0,0,.27778],730:[0,.69444,0,0,.75],732:[0,.66786,0,0,.5],733:[0,.69444,0,0,.5],915:[0,.68333,0,0,.625],916:[0,.68333,0,0,.83334],920:[0,.68333,0,0,.77778],923:[0,.68333,0,0,.69445],926:[0,.68333,0,0,.66667],928:[0,.68333,0,0,.75],931:[0,.68333,0,0,.72222],933:[0,.68333,0,0,.77778],934:[0,.68333,0,0,.72222],936:[0,.68333,0,0,.77778],937:[0,.68333,0,0,.72222],8211:[0,.43056,.02778,0,.5],8212:[0,.43056,.02778,0,1],8216:[0,.69444,0,0,.27778],8217:[0,.69444,0,0,.27778],8220:[0,.69444,0,0,.5],8221:[0,.69444,0,0,.5],8224:[.19444,.69444,0,0,.44445],8225:[.19444,.69444,0,0,.44445],8230:[0,.123,0,0,1.172],8242:[0,.55556,0,0,.275],8407:[0,.71444,.15382,0,.5],8463:[0,.68889,0,0,.54028],8465:[0,.69444,0,0,.72222],8467:[0,.69444,0,.11111,.41667],8472:[.19444,.43056,0,.11111,.63646],8476:[0,.69444,0,0,.72222],8501:[0,.69444,0,0,.61111],8592:[-.13313,.36687,0,0,1],8593:[.19444,.69444,0,0,.5],8594:[-.13313,.36687,0,0,1],8595:[.19444,.69444,0,0,.5],8596:[-.13313,.36687,0,0,1],8597:[.25,.75,0,0,.5],8598:[.19444,.69444,0,0,1],8599:[.19444,.69444,0,0,1],8600:[.19444,.69444,0,0,1],8601:[.19444,.69444,0,0,1],8614:[.011,.511,0,0,1],8617:[.011,.511,0,0,1.126],8618:[.011,.511,0,0,1.126],8636:[-.13313,.36687,0,0,1],8637:[-.13313,.36687,0,0,1],8640:[-.13313,.36687,0,0,1],8641:[-.13313,.36687,0,0,1],8652:[.011,.671,0,0,1],8656:[-.13313,.36687,0,0,1],8657:[.19444,.69444,0,0,.61111],8658:[-.13313,.36687,0,0,1],8659:[.19444,.69444,0,0,.61111],8660:[-.13313,.36687,0,0,1],8661:[.25,.75,0,0,.61111],8704:[0,.69444,0,0,.55556],8706:[0,.69444,.05556,.08334,.5309],8707:[0,.69444,0,0,.55556],8709:[.05556,.75,0,0,.5],8711:[0,.68333,0,0,.83334],8712:[.0391,.5391,0,0,.66667],8715:[.0391,.5391,0,0,.66667],8722:[.08333,.58333,0,0,.77778],8723:[.08333,.58333,0,0,.77778],8725:[.25,.75,0,0,.5],8726:[.25,.75,0,0,.5],8727:[-.03472,.46528,0,0,.5],8728:[-.05555,.44445,0,0,.5],8729:[-.05555,.44445,0,0,.5],8730:[.2,.8,0,0,.83334],8733:[0,.43056,0,0,.77778],8734:[0,.43056,0,0,1],8736:[0,.69224,0,0,.72222],8739:[.25,.75,0,0,.27778],8741:[.25,.75,0,0,.5],8743:[0,.55556,0,0,.66667],8744:[0,.55556,0,0,.66667],8745:[0,.55556,0,0,.66667],8746:[0,.55556,0,0,.66667],8747:[.19444,.69444,.11111,0,.41667],8764:[-.13313,.36687,0,0,.77778],8768:[.19444,.69444,0,0,.27778],8771:[-.03625,.46375,0,0,.77778],8773:[-.022,.589,0,0,.778],8776:[-.01688,.48312,0,0,.77778],8781:[-.03625,.46375,0,0,.77778],8784:[-.133,.673,0,0,.778],8801:[-.03625,.46375,0,0,.77778],8804:[.13597,.63597,0,0,.77778],8805:[.13597,.63597,0,0,.77778],8810:[.0391,.5391,0,0,1],8811:[.0391,.5391,0,0,1],8826:[.0391,.5391,0,0,.77778],8827:[.0391,.5391,0,0,.77778],8834:[.0391,.5391,0,0,.77778],8835:[.0391,.5391,0,0,.77778],8838:[.13597,.63597,0,0,.77778],8839:[.13597,.63597,0,0,.77778],8846:[0,.55556,0,0,.66667],8849:[.13597,.63597,0,0,.77778],8850:[.13597,.63597,0,0,.77778],8851:[0,.55556,0,0,.66667],8852:[0,.55556,0,0,.66667],8853:[.08333,.58333,0,0,.77778],8854:[.08333,.58333,0,0,.77778],8855:[.08333,.58333,0,0,.77778],8856:[.08333,.58333,0,0,.77778],8857:[.08333,.58333,0,0,.77778],8866:[0,.69444,0,0,.61111],8867:[0,.69444,0,0,.61111],8868:[0,.69444,0,0,.77778],8869:[0,.69444,0,0,.77778],8872:[.249,.75,0,0,.867],8900:[-.05555,.44445,0,0,.5],8901:[-.05555,.44445,0,0,.27778],8902:[-.03472,.46528,0,0,.5],8904:[.005,.505,0,0,.9],8942:[.03,.903,0,0,.278],8943:[-.19,.313,0,0,1.172],8945:[-.1,.823,0,0,1.282],8968:[.25,.75,0,0,.44445],8969:[.25,.75,0,0,.44445],8970:[.25,.75,0,0,.44445],8971:[.25,.75,0,0,.44445],8994:[-.14236,.35764,0,0,1],8995:[-.14236,.35764,0,0,1],9136:[.244,.744,0,0,.412],9137:[.244,.745,0,0,.412],9651:[.19444,.69444,0,0,.88889],9657:[-.03472,.46528,0,0,.5],9661:[.19444,.69444,0,0,.88889],9667:[-.03472,.46528,0,0,.5],9711:[.19444,.69444,0,0,1],9824:[.12963,.69444,0,0,.77778],9825:[.12963,.69444,0,0,.77778],9826:[.12963,.69444,0,0,.77778],9827:[.12963,.69444,0,0,.77778],9837:[0,.75,0,0,.38889],9838:[.19444,.69444,0,0,.38889],9839:[.19444,.69444,0,0,.38889],10216:[.25,.75,0,0,.38889],10217:[.25,.75,0,0,.38889],10222:[.244,.744,0,0,.412],10223:[.244,.745,0,0,.412],10229:[.011,.511,0,0,1.609],10230:[.011,.511,0,0,1.638],10231:[.011,.511,0,0,1.859],10232:[.024,.525,0,0,1.609],10233:[.024,.525,0,0,1.638],10234:[.024,.525,0,0,1.858],10236:[.011,.511,0,0,1.638],10815:[0,.68333,0,0,.75],10927:[.13597,.63597,0,0,.77778],10928:[.13597,.63597,0,0,.77778],57376:[.19444,.69444,0,0,0]},"Math-BoldItalic":{32:[0,0,0,0,.25],48:[0,.44444,0,0,.575],49:[0,.44444,0,0,.575],50:[0,.44444,0,0,.575],51:[.19444,.44444,0,0,.575],52:[.19444,.44444,0,0,.575],53:[.19444,.44444,0,0,.575],54:[0,.64444,0,0,.575],55:[.19444,.44444,0,0,.575],56:[0,.64444,0,0,.575],57:[.19444,.44444,0,0,.575],65:[0,.68611,0,0,.86944],66:[0,.68611,.04835,0,.8664],67:[0,.68611,.06979,0,.81694],68:[0,.68611,.03194,0,.93812],69:[0,.68611,.05451,0,.81007],70:[0,.68611,.15972,0,.68889],71:[0,.68611,0,0,.88673],72:[0,.68611,.08229,0,.98229],73:[0,.68611,.07778,0,.51111],74:[0,.68611,.10069,0,.63125],75:[0,.68611,.06979,0,.97118],76:[0,.68611,0,0,.75555],77:[0,.68611,.11424,0,1.14201],78:[0,.68611,.11424,0,.95034],79:[0,.68611,.03194,0,.83666],80:[0,.68611,.15972,0,.72309],81:[.19444,.68611,0,0,.86861],82:[0,.68611,.00421,0,.87235],83:[0,.68611,.05382,0,.69271],84:[0,.68611,.15972,0,.63663],85:[0,.68611,.11424,0,.80027],86:[0,.68611,.25555,0,.67778],87:[0,.68611,.15972,0,1.09305],88:[0,.68611,.07778,0,.94722],89:[0,.68611,.25555,0,.67458],90:[0,.68611,.06979,0,.77257],97:[0,.44444,0,0,.63287],98:[0,.69444,0,0,.52083],99:[0,.44444,0,0,.51342],100:[0,.69444,0,0,.60972],101:[0,.44444,0,0,.55361],102:[.19444,.69444,.11042,0,.56806],103:[.19444,.44444,.03704,0,.5449],104:[0,.69444,0,0,.66759],105:[0,.69326,0,0,.4048],106:[.19444,.69326,.0622,0,.47083],107:[0,.69444,.01852,0,.6037],108:[0,.69444,.0088,0,.34815],109:[0,.44444,0,0,1.0324],110:[0,.44444,0,0,.71296],111:[0,.44444,0,0,.58472],112:[.19444,.44444,0,0,.60092],113:[.19444,.44444,.03704,0,.54213],114:[0,.44444,.03194,0,.5287],115:[0,.44444,0,0,.53125],116:[0,.63492,0,0,.41528],117:[0,.44444,0,0,.68102],118:[0,.44444,.03704,0,.56666],119:[0,.44444,.02778,0,.83148],120:[0,.44444,0,0,.65903],121:[.19444,.44444,.03704,0,.59028],122:[0,.44444,.04213,0,.55509],160:[0,0,0,0,.25],915:[0,.68611,.15972,0,.65694],916:[0,.68611,0,0,.95833],920:[0,.68611,.03194,0,.86722],923:[0,.68611,0,0,.80555],926:[0,.68611,.07458,0,.84125],928:[0,.68611,.08229,0,.98229],931:[0,.68611,.05451,0,.88507],933:[0,.68611,.15972,0,.67083],934:[0,.68611,0,0,.76666],936:[0,.68611,.11653,0,.71402],937:[0,.68611,.04835,0,.8789],945:[0,.44444,0,0,.76064],946:[.19444,.69444,.03403,0,.65972],947:[.19444,.44444,.06389,0,.59003],948:[0,.69444,.03819,0,.52222],949:[0,.44444,0,0,.52882],950:[.19444,.69444,.06215,0,.50833],951:[.19444,.44444,.03704,0,.6],952:[0,.69444,.03194,0,.5618],953:[0,.44444,0,0,.41204],954:[0,.44444,0,0,.66759],955:[0,.69444,0,0,.67083],956:[.19444,.44444,0,0,.70787],957:[0,.44444,.06898,0,.57685],958:[.19444,.69444,.03021,0,.50833],959:[0,.44444,0,0,.58472],960:[0,.44444,.03704,0,.68241],961:[.19444,.44444,0,0,.6118],962:[.09722,.44444,.07917,0,.42361],963:[0,.44444,.03704,0,.68588],964:[0,.44444,.13472,0,.52083],965:[0,.44444,.03704,0,.63055],966:[.19444,.44444,0,0,.74722],967:[.19444,.44444,0,0,.71805],968:[.19444,.69444,.03704,0,.75833],969:[0,.44444,.03704,0,.71782],977:[0,.69444,0,0,.69155],981:[.19444,.69444,0,0,.7125],982:[0,.44444,.03194,0,.975],1009:[.19444,.44444,0,0,.6118],1013:[0,.44444,0,0,.48333],57649:[0,.44444,0,0,.39352],57911:[.19444,.44444,0,0,.43889]},"Math-Italic":{32:[0,0,0,0,.25],48:[0,.43056,0,0,.5],49:[0,.43056,0,0,.5],50:[0,.43056,0,0,.5],51:[.19444,.43056,0,0,.5],52:[.19444,.43056,0,0,.5],53:[.19444,.43056,0,0,.5],54:[0,.64444,0,0,.5],55:[.19444,.43056,0,0,.5],56:[0,.64444,0,0,.5],57:[.19444,.43056,0,0,.5],65:[0,.68333,0,.13889,.75],66:[0,.68333,.05017,.08334,.75851],67:[0,.68333,.07153,.08334,.71472],68:[0,.68333,.02778,.05556,.82792],69:[0,.68333,.05764,.08334,.7382],70:[0,.68333,.13889,.08334,.64306],71:[0,.68333,0,.08334,.78625],72:[0,.68333,.08125,.05556,.83125],73:[0,.68333,.07847,.11111,.43958],74:[0,.68333,.09618,.16667,.55451],75:[0,.68333,.07153,.05556,.84931],76:[0,.68333,0,.02778,.68056],77:[0,.68333,.10903,.08334,.97014],78:[0,.68333,.10903,.08334,.80347],79:[0,.68333,.02778,.08334,.76278],80:[0,.68333,.13889,.08334,.64201],81:[.19444,.68333,0,.08334,.79056],82:[0,.68333,.00773,.08334,.75929],83:[0,.68333,.05764,.08334,.6132],84:[0,.68333,.13889,.08334,.58438],85:[0,.68333,.10903,.02778,.68278],86:[0,.68333,.22222,0,.58333],87:[0,.68333,.13889,0,.94445],88:[0,.68333,.07847,.08334,.82847],89:[0,.68333,.22222,0,.58056],90:[0,.68333,.07153,.08334,.68264],97:[0,.43056,0,0,.52859],98:[0,.69444,0,0,.42917],99:[0,.43056,0,.05556,.43276],100:[0,.69444,0,.16667,.52049],101:[0,.43056,0,.05556,.46563],102:[.19444,.69444,.10764,.16667,.48959],103:[.19444,.43056,.03588,.02778,.47697],104:[0,.69444,0,0,.57616],105:[0,.65952,0,0,.34451],106:[.19444,.65952,.05724,0,.41181],107:[0,.69444,.03148,0,.5206],108:[0,.69444,.01968,.08334,.29838],109:[0,.43056,0,0,.87801],110:[0,.43056,0,0,.60023],111:[0,.43056,0,.05556,.48472],112:[.19444,.43056,0,.08334,.50313],113:[.19444,.43056,.03588,.08334,.44641],114:[0,.43056,.02778,.05556,.45116],115:[0,.43056,0,.05556,.46875],116:[0,.61508,0,.08334,.36111],117:[0,.43056,0,.02778,.57246],118:[0,.43056,.03588,.02778,.48472],119:[0,.43056,.02691,.08334,.71592],120:[0,.43056,0,.02778,.57153],121:[.19444,.43056,.03588,.05556,.49028],122:[0,.43056,.04398,.05556,.46505],160:[0,0,0,0,.25],915:[0,.68333,.13889,.08334,.61528],916:[0,.68333,0,.16667,.83334],920:[0,.68333,.02778,.08334,.76278],923:[0,.68333,0,.16667,.69445],926:[0,.68333,.07569,.08334,.74236],928:[0,.68333,.08125,.05556,.83125],931:[0,.68333,.05764,.08334,.77986],933:[0,.68333,.13889,.05556,.58333],934:[0,.68333,0,.08334,.66667],936:[0,.68333,.11,.05556,.61222],937:[0,.68333,.05017,.08334,.7724],945:[0,.43056,.0037,.02778,.6397],946:[.19444,.69444,.05278,.08334,.56563],947:[.19444,.43056,.05556,0,.51773],948:[0,.69444,.03785,.05556,.44444],949:[0,.43056,0,.08334,.46632],950:[.19444,.69444,.07378,.08334,.4375],951:[.19444,.43056,.03588,.05556,.49653],952:[0,.69444,.02778,.08334,.46944],953:[0,.43056,0,.05556,.35394],954:[0,.43056,0,0,.57616],955:[0,.69444,0,0,.58334],956:[.19444,.43056,0,.02778,.60255],957:[0,.43056,.06366,.02778,.49398],958:[.19444,.69444,.04601,.11111,.4375],959:[0,.43056,0,.05556,.48472],960:[0,.43056,.03588,0,.57003],961:[.19444,.43056,0,.08334,.51702],962:[.09722,.43056,.07986,.08334,.36285],963:[0,.43056,.03588,0,.57141],964:[0,.43056,.1132,.02778,.43715],965:[0,.43056,.03588,.02778,.54028],966:[.19444,.43056,0,.08334,.65417],967:[.19444,.43056,0,.05556,.62569],968:[.19444,.69444,.03588,.11111,.65139],969:[0,.43056,.03588,0,.62245],977:[0,.69444,0,.08334,.59144],981:[.19444,.69444,0,.08334,.59583],982:[0,.43056,.02778,0,.82813],1009:[.19444,.43056,0,.08334,.51702],1013:[0,.43056,0,.05556,.4059],57649:[0,.43056,0,.02778,.32246],57911:[.19444,.43056,0,.08334,.38403]},"SansSerif-Bold":{32:[0,0,0,0,.25],33:[0,.69444,0,0,.36667],34:[0,.69444,0,0,.55834],35:[.19444,.69444,0,0,.91667],36:[.05556,.75,0,0,.55],37:[.05556,.75,0,0,1.02912],38:[0,.69444,0,0,.83056],39:[0,.69444,0,0,.30556],40:[.25,.75,0,0,.42778],41:[.25,.75,0,0,.42778],42:[0,.75,0,0,.55],43:[.11667,.61667,0,0,.85556],44:[.10556,.13056,0,0,.30556],45:[0,.45833,0,0,.36667],46:[0,.13056,0,0,.30556],47:[.25,.75,0,0,.55],48:[0,.69444,0,0,.55],49:[0,.69444,0,0,.55],50:[0,.69444,0,0,.55],51:[0,.69444,0,0,.55],52:[0,.69444,0,0,.55],53:[0,.69444,0,0,.55],54:[0,.69444,0,0,.55],55:[0,.69444,0,0,.55],56:[0,.69444,0,0,.55],57:[0,.69444,0,0,.55],58:[0,.45833,0,0,.30556],59:[.10556,.45833,0,0,.30556],61:[-.09375,.40625,0,0,.85556],63:[0,.69444,0,0,.51945],64:[0,.69444,0,0,.73334],65:[0,.69444,0,0,.73334],66:[0,.69444,0,0,.73334],67:[0,.69444,0,0,.70278],68:[0,.69444,0,0,.79445],69:[0,.69444,0,0,.64167],70:[0,.69444,0,0,.61111],71:[0,.69444,0,0,.73334],72:[0,.69444,0,0,.79445],73:[0,.69444,0,0,.33056],74:[0,.69444,0,0,.51945],75:[0,.69444,0,0,.76389],76:[0,.69444,0,0,.58056],77:[0,.69444,0,0,.97778],78:[0,.69444,0,0,.79445],79:[0,.69444,0,0,.79445],80:[0,.69444,0,0,.70278],81:[.10556,.69444,0,0,.79445],82:[0,.69444,0,0,.70278],83:[0,.69444,0,0,.61111],84:[0,.69444,0,0,.73334],85:[0,.69444,0,0,.76389],86:[0,.69444,.01528,0,.73334],87:[0,.69444,.01528,0,1.03889],88:[0,.69444,0,0,.73334],89:[0,.69444,.0275,0,.73334],90:[0,.69444,0,0,.67223],91:[.25,.75,0,0,.34306],93:[.25,.75,0,0,.34306],94:[0,.69444,0,0,.55],95:[.35,.10833,.03056,0,.55],97:[0,.45833,0,0,.525],98:[0,.69444,0,0,.56111],99:[0,.45833,0,0,.48889],100:[0,.69444,0,0,.56111],101:[0,.45833,0,0,.51111],102:[0,.69444,.07639,0,.33611],103:[.19444,.45833,.01528,0,.55],104:[0,.69444,0,0,.56111],105:[0,.69444,0,0,.25556],106:[.19444,.69444,0,0,.28611],107:[0,.69444,0,0,.53056],108:[0,.69444,0,0,.25556],109:[0,.45833,0,0,.86667],110:[0,.45833,0,0,.56111],111:[0,.45833,0,0,.55],112:[.19444,.45833,0,0,.56111],113:[.19444,.45833,0,0,.56111],114:[0,.45833,.01528,0,.37222],115:[0,.45833,0,0,.42167],116:[0,.58929,0,0,.40417],117:[0,.45833,0,0,.56111],118:[0,.45833,.01528,0,.5],119:[0,.45833,.01528,0,.74445],120:[0,.45833,0,0,.5],121:[.19444,.45833,.01528,0,.5],122:[0,.45833,0,0,.47639],126:[.35,.34444,0,0,.55],160:[0,0,0,0,.25],168:[0,.69444,0,0,.55],176:[0,.69444,0,0,.73334],180:[0,.69444,0,0,.55],184:[.17014,0,0,0,.48889],305:[0,.45833,0,0,.25556],567:[.19444,.45833,0,0,.28611],710:[0,.69444,0,0,.55],711:[0,.63542,0,0,.55],713:[0,.63778,0,0,.55],728:[0,.69444,0,0,.55],729:[0,.69444,0,0,.30556],730:[0,.69444,0,0,.73334],732:[0,.69444,0,0,.55],733:[0,.69444,0,0,.55],915:[0,.69444,0,0,.58056],916:[0,.69444,0,0,.91667],920:[0,.69444,0,0,.85556],923:[0,.69444,0,0,.67223],926:[0,.69444,0,0,.73334],928:[0,.69444,0,0,.79445],931:[0,.69444,0,0,.79445],933:[0,.69444,0,0,.85556],934:[0,.69444,0,0,.79445],936:[0,.69444,0,0,.85556],937:[0,.69444,0,0,.79445],8211:[0,.45833,.03056,0,.55],8212:[0,.45833,.03056,0,1.10001],8216:[0,.69444,0,0,.30556],8217:[0,.69444,0,0,.30556],8220:[0,.69444,0,0,.55834],8221:[0,.69444,0,0,.55834]},"SansSerif-Italic":{32:[0,0,0,0,.25],33:[0,.69444,.05733,0,.31945],34:[0,.69444,.00316,0,.5],35:[.19444,.69444,.05087,0,.83334],36:[.05556,.75,.11156,0,.5],37:[.05556,.75,.03126,0,.83334],38:[0,.69444,.03058,0,.75834],39:[0,.69444,.07816,0,.27778],40:[.25,.75,.13164,0,.38889],41:[.25,.75,.02536,0,.38889],42:[0,.75,.11775,0,.5],43:[.08333,.58333,.02536,0,.77778],44:[.125,.08333,0,0,.27778],45:[0,.44444,.01946,0,.33333],46:[0,.08333,0,0,.27778],47:[.25,.75,.13164,0,.5],48:[0,.65556,.11156,0,.5],49:[0,.65556,.11156,0,.5],50:[0,.65556,.11156,0,.5],51:[0,.65556,.11156,0,.5],52:[0,.65556,.11156,0,.5],53:[0,.65556,.11156,0,.5],54:[0,.65556,.11156,0,.5],55:[0,.65556,.11156,0,.5],56:[0,.65556,.11156,0,.5],57:[0,.65556,.11156,0,.5],58:[0,.44444,.02502,0,.27778],59:[.125,.44444,.02502,0,.27778],61:[-.13,.37,.05087,0,.77778],63:[0,.69444,.11809,0,.47222],64:[0,.69444,.07555,0,.66667],65:[0,.69444,0,0,.66667],66:[0,.69444,.08293,0,.66667],67:[0,.69444,.11983,0,.63889],68:[0,.69444,.07555,0,.72223],69:[0,.69444,.11983,0,.59722],70:[0,.69444,.13372,0,.56945],71:[0,.69444,.11983,0,.66667],72:[0,.69444,.08094,0,.70834],73:[0,.69444,.13372,0,.27778],74:[0,.69444,.08094,0,.47222],75:[0,.69444,.11983,0,.69445],76:[0,.69444,0,0,.54167],77:[0,.69444,.08094,0,.875],78:[0,.69444,.08094,0,.70834],79:[0,.69444,.07555,0,.73611],80:[0,.69444,.08293,0,.63889],81:[.125,.69444,.07555,0,.73611],82:[0,.69444,.08293,0,.64584],83:[0,.69444,.09205,0,.55556],84:[0,.69444,.13372,0,.68056],85:[0,.69444,.08094,0,.6875],86:[0,.69444,.1615,0,.66667],87:[0,.69444,.1615,0,.94445],88:[0,.69444,.13372,0,.66667],89:[0,.69444,.17261,0,.66667],90:[0,.69444,.11983,0,.61111],91:[.25,.75,.15942,0,.28889],93:[.25,.75,.08719,0,.28889],94:[0,.69444,.0799,0,.5],95:[.35,.09444,.08616,0,.5],97:[0,.44444,.00981,0,.48056],98:[0,.69444,.03057,0,.51667],99:[0,.44444,.08336,0,.44445],100:[0,.69444,.09483,0,.51667],101:[0,.44444,.06778,0,.44445],102:[0,.69444,.21705,0,.30556],103:[.19444,.44444,.10836,0,.5],104:[0,.69444,.01778,0,.51667],105:[0,.67937,.09718,0,.23889],106:[.19444,.67937,.09162,0,.26667],107:[0,.69444,.08336,0,.48889],108:[0,.69444,.09483,0,.23889],109:[0,.44444,.01778,0,.79445],110:[0,.44444,.01778,0,.51667],111:[0,.44444,.06613,0,.5],112:[.19444,.44444,.0389,0,.51667],113:[.19444,.44444,.04169,0,.51667],114:[0,.44444,.10836,0,.34167],115:[0,.44444,.0778,0,.38333],116:[0,.57143,.07225,0,.36111],117:[0,.44444,.04169,0,.51667],118:[0,.44444,.10836,0,.46111],119:[0,.44444,.10836,0,.68334],120:[0,.44444,.09169,0,.46111],121:[.19444,.44444,.10836,0,.46111],122:[0,.44444,.08752,0,.43472],126:[.35,.32659,.08826,0,.5],160:[0,0,0,0,.25],168:[0,.67937,.06385,0,.5],176:[0,.69444,0,0,.73752],184:[.17014,0,0,0,.44445],305:[0,.44444,.04169,0,.23889],567:[.19444,.44444,.04169,0,.26667],710:[0,.69444,.0799,0,.5],711:[0,.63194,.08432,0,.5],713:[0,.60889,.08776,0,.5],714:[0,.69444,.09205,0,.5],715:[0,.69444,0,0,.5],728:[0,.69444,.09483,0,.5],729:[0,.67937,.07774,0,.27778],730:[0,.69444,0,0,.73752],732:[0,.67659,.08826,0,.5],733:[0,.69444,.09205,0,.5],915:[0,.69444,.13372,0,.54167],916:[0,.69444,0,0,.83334],920:[0,.69444,.07555,0,.77778],923:[0,.69444,0,0,.61111],926:[0,.69444,.12816,0,.66667],928:[0,.69444,.08094,0,.70834],931:[0,.69444,.11983,0,.72222],933:[0,.69444,.09031,0,.77778],934:[0,.69444,.04603,0,.72222],936:[0,.69444,.09031,0,.77778],937:[0,.69444,.08293,0,.72222],8211:[0,.44444,.08616,0,.5],8212:[0,.44444,.08616,0,1],8216:[0,.69444,.07816,0,.27778],8217:[0,.69444,.07816,0,.27778],8220:[0,.69444,.14205,0,.5],8221:[0,.69444,.00316,0,.5]},"SansSerif-Regular":{32:[0,0,0,0,.25],33:[0,.69444,0,0,.31945],34:[0,.69444,0,0,.5],35:[.19444,.69444,0,0,.83334],36:[.05556,.75,0,0,.5],37:[.05556,.75,0,0,.83334],38:[0,.69444,0,0,.75834],39:[0,.69444,0,0,.27778],40:[.25,.75,0,0,.38889],41:[.25,.75,0,0,.38889],42:[0,.75,0,0,.5],43:[.08333,.58333,0,0,.77778],44:[.125,.08333,0,0,.27778],45:[0,.44444,0,0,.33333],46:[0,.08333,0,0,.27778],47:[.25,.75,0,0,.5],48:[0,.65556,0,0,.5],49:[0,.65556,0,0,.5],50:[0,.65556,0,0,.5],51:[0,.65556,0,0,.5],52:[0,.65556,0,0,.5],53:[0,.65556,0,0,.5],54:[0,.65556,0,0,.5],55:[0,.65556,0,0,.5],56:[0,.65556,0,0,.5],57:[0,.65556,0,0,.5],58:[0,.44444,0,0,.27778],59:[.125,.44444,0,0,.27778],61:[-.13,.37,0,0,.77778],63:[0,.69444,0,0,.47222],64:[0,.69444,0,0,.66667],65:[0,.69444,0,0,.66667],66:[0,.69444,0,0,.66667],67:[0,.69444,0,0,.63889],68:[0,.69444,0,0,.72223],69:[0,.69444,0,0,.59722],70:[0,.69444,0,0,.56945],71:[0,.69444,0,0,.66667],72:[0,.69444,0,0,.70834],73:[0,.69444,0,0,.27778],74:[0,.69444,0,0,.47222],75:[0,.69444,0,0,.69445],76:[0,.69444,0,0,.54167],77:[0,.69444,0,0,.875],78:[0,.69444,0,0,.70834],79:[0,.69444,0,0,.73611],80:[0,.69444,0,0,.63889],81:[.125,.69444,0,0,.73611],82:[0,.69444,0,0,.64584],83:[0,.69444,0,0,.55556],84:[0,.69444,0,0,.68056],85:[0,.69444,0,0,.6875],86:[0,.69444,.01389,0,.66667],87:[0,.69444,.01389,0,.94445],88:[0,.69444,0,0,.66667],89:[0,.69444,.025,0,.66667],90:[0,.69444,0,0,.61111],91:[.25,.75,0,0,.28889],93:[.25,.75,0,0,.28889],94:[0,.69444,0,0,.5],95:[.35,.09444,.02778,0,.5],97:[0,.44444,0,0,.48056],98:[0,.69444,0,0,.51667],99:[0,.44444,0,0,.44445],100:[0,.69444,0,0,.51667],101:[0,.44444,0,0,.44445],102:[0,.69444,.06944,0,.30556],103:[.19444,.44444,.01389,0,.5],104:[0,.69444,0,0,.51667],105:[0,.67937,0,0,.23889],106:[.19444,.67937,0,0,.26667],107:[0,.69444,0,0,.48889],108:[0,.69444,0,0,.23889],109:[0,.44444,0,0,.79445],110:[0,.44444,0,0,.51667],111:[0,.44444,0,0,.5],112:[.19444,.44444,0,0,.51667],113:[.19444,.44444,0,0,.51667],114:[0,.44444,.01389,0,.34167],115:[0,.44444,0,0,.38333],116:[0,.57143,0,0,.36111],117:[0,.44444,0,0,.51667],118:[0,.44444,.01389,0,.46111],119:[0,.44444,.01389,0,.68334],120:[0,.44444,0,0,.46111],121:[.19444,.44444,.01389,0,.46111],122:[0,.44444,0,0,.43472],126:[.35,.32659,0,0,.5],160:[0,0,0,0,.25],168:[0,.67937,0,0,.5],176:[0,.69444,0,0,.66667],184:[.17014,0,0,0,.44445],305:[0,.44444,0,0,.23889],567:[.19444,.44444,0,0,.26667],710:[0,.69444,0,0,.5],711:[0,.63194,0,0,.5],713:[0,.60889,0,0,.5],714:[0,.69444,0,0,.5],715:[0,.69444,0,0,.5],728:[0,.69444,0,0,.5],729:[0,.67937,0,0,.27778],730:[0,.69444,0,0,.66667],732:[0,.67659,0,0,.5],733:[0,.69444,0,0,.5],915:[0,.69444,0,0,.54167],916:[0,.69444,0,0,.83334],920:[0,.69444,0,0,.77778],923:[0,.69444,0,0,.61111],926:[0,.69444,0,0,.66667],928:[0,.69444,0,0,.70834],931:[0,.69444,0,0,.72222],933:[0,.69444,0,0,.77778],934:[0,.69444,0,0,.72222],936:[0,.69444,0,0,.77778],937:[0,.69444,0,0,.72222],8211:[0,.44444,.02778,0,.5],8212:[0,.44444,.02778,0,1],8216:[0,.69444,0,0,.27778],8217:[0,.69444,0,0,.27778],8220:[0,.69444,0,0,.5],8221:[0,.69444,0,0,.5]},"Script-Regular":{32:[0,0,0,0,.25],65:[0,.7,.22925,0,.80253],66:[0,.7,.04087,0,.90757],67:[0,.7,.1689,0,.66619],68:[0,.7,.09371,0,.77443],69:[0,.7,.18583,0,.56162],70:[0,.7,.13634,0,.89544],71:[0,.7,.17322,0,.60961],72:[0,.7,.29694,0,.96919],73:[0,.7,.19189,0,.80907],74:[.27778,.7,.19189,0,1.05159],75:[0,.7,.31259,0,.91364],76:[0,.7,.19189,0,.87373],77:[0,.7,.15981,0,1.08031],78:[0,.7,.3525,0,.9015],79:[0,.7,.08078,0,.73787],80:[0,.7,.08078,0,1.01262],81:[0,.7,.03305,0,.88282],82:[0,.7,.06259,0,.85],83:[0,.7,.19189,0,.86767],84:[0,.7,.29087,0,.74697],85:[0,.7,.25815,0,.79996],86:[0,.7,.27523,0,.62204],87:[0,.7,.27523,0,.80532],88:[0,.7,.26006,0,.94445],89:[0,.7,.2939,0,.70961],90:[0,.7,.24037,0,.8212],160:[0,0,0,0,.25]},"Size1-Regular":{32:[0,0,0,0,.25],40:[.35001,.85,0,0,.45834],41:[.35001,.85,0,0,.45834],47:[.35001,.85,0,0,.57778],91:[.35001,.85,0,0,.41667],92:[.35001,.85,0,0,.57778],93:[.35001,.85,0,0,.41667],123:[.35001,.85,0,0,.58334],125:[.35001,.85,0,0,.58334],160:[0,0,0,0,.25],710:[0,.72222,0,0,.55556],732:[0,.72222,0,0,.55556],770:[0,.72222,0,0,.55556],771:[0,.72222,0,0,.55556],8214:[-99e-5,.601,0,0,.77778],8593:[1e-5,.6,0,0,.66667],8595:[1e-5,.6,0,0,.66667],8657:[1e-5,.6,0,0,.77778],8659:[1e-5,.6,0,0,.77778],8719:[.25001,.75,0,0,.94445],8720:[.25001,.75,0,0,.94445],8721:[.25001,.75,0,0,1.05556],8730:[.35001,.85,0,0,1],8739:[-.00599,.606,0,0,.33333],8741:[-.00599,.606,0,0,.55556],8747:[.30612,.805,.19445,0,.47222],8748:[.306,.805,.19445,0,.47222],8749:[.306,.805,.19445,0,.47222],8750:[.30612,.805,.19445,0,.47222],8896:[.25001,.75,0,0,.83334],8897:[.25001,.75,0,0,.83334],8898:[.25001,.75,0,0,.83334],8899:[.25001,.75,0,0,.83334],8968:[.35001,.85,0,0,.47222],8969:[.35001,.85,0,0,.47222],8970:[.35001,.85,0,0,.47222],8971:[.35001,.85,0,0,.47222],9168:[-99e-5,.601,0,0,.66667],10216:[.35001,.85,0,0,.47222],10217:[.35001,.85,0,0,.47222],10752:[.25001,.75,0,0,1.11111],10753:[.25001,.75,0,0,1.11111],10754:[.25001,.75,0,0,1.11111],10756:[.25001,.75,0,0,.83334],10758:[.25001,.75,0,0,.83334]},"Size2-Regular":{32:[0,0,0,0,.25],40:[.65002,1.15,0,0,.59722],41:[.65002,1.15,0,0,.59722],47:[.65002,1.15,0,0,.81111],91:[.65002,1.15,0,0,.47222],92:[.65002,1.15,0,0,.81111],93:[.65002,1.15,0,0,.47222],123:[.65002,1.15,0,0,.66667],125:[.65002,1.15,0,0,.66667],160:[0,0,0,0,.25],710:[0,.75,0,0,1],732:[0,.75,0,0,1],770:[0,.75,0,0,1],771:[0,.75,0,0,1],8719:[.55001,1.05,0,0,1.27778],8720:[.55001,1.05,0,0,1.27778],8721:[.55001,1.05,0,0,1.44445],8730:[.65002,1.15,0,0,1],8747:[.86225,1.36,.44445,0,.55556],8748:[.862,1.36,.44445,0,.55556],8749:[.862,1.36,.44445,0,.55556],8750:[.86225,1.36,.44445,0,.55556],8896:[.55001,1.05,0,0,1.11111],8897:[.55001,1.05,0,0,1.11111],8898:[.55001,1.05,0,0,1.11111],8899:[.55001,1.05,0,0,1.11111],8968:[.65002,1.15,0,0,.52778],8969:[.65002,1.15,0,0,.52778],8970:[.65002,1.15,0,0,.52778],8971:[.65002,1.15,0,0,.52778],10216:[.65002,1.15,0,0,.61111],10217:[.65002,1.15,0,0,.61111],10752:[.55001,1.05,0,0,1.51112],10753:[.55001,1.05,0,0,1.51112],10754:[.55001,1.05,0,0,1.51112],10756:[.55001,1.05,0,0,1.11111],10758:[.55001,1.05,0,0,1.11111]},"Size3-Regular":{32:[0,0,0,0,.25],40:[.95003,1.45,0,0,.73611],41:[.95003,1.45,0,0,.73611],47:[.95003,1.45,0,0,1.04445],91:[.95003,1.45,0,0,.52778],92:[.95003,1.45,0,0,1.04445],93:[.95003,1.45,0,0,.52778],123:[.95003,1.45,0,0,.75],125:[.95003,1.45,0,0,.75],160:[0,0,0,0,.25],710:[0,.75,0,0,1.44445],732:[0,.75,0,0,1.44445],770:[0,.75,0,0,1.44445],771:[0,.75,0,0,1.44445],8730:[.95003,1.45,0,0,1],8968:[.95003,1.45,0,0,.58334],8969:[.95003,1.45,0,0,.58334],8970:[.95003,1.45,0,0,.58334],8971:[.95003,1.45,0,0,.58334],10216:[.95003,1.45,0,0,.75],10217:[.95003,1.45,0,0,.75]},"Size4-Regular":{32:[0,0,0,0,.25],40:[1.25003,1.75,0,0,.79167],41:[1.25003,1.75,0,0,.79167],47:[1.25003,1.75,0,0,1.27778],91:[1.25003,1.75,0,0,.58334],92:[1.25003,1.75,0,0,1.27778],93:[1.25003,1.75,0,0,.58334],123:[1.25003,1.75,0,0,.80556],125:[1.25003,1.75,0,0,.80556],160:[0,0,0,0,.25],710:[0,.825,0,0,1.8889],732:[0,.825,0,0,1.8889],770:[0,.825,0,0,1.8889],771:[0,.825,0,0,1.8889],8730:[1.25003,1.75,0,0,1],8968:[1.25003,1.75,0,0,.63889],8969:[1.25003,1.75,0,0,.63889],8970:[1.25003,1.75,0,0,.63889],8971:[1.25003,1.75,0,0,.63889],9115:[.64502,1.155,0,0,.875],9116:[1e-5,.6,0,0,.875],9117:[.64502,1.155,0,0,.875],9118:[.64502,1.155,0,0,.875],9119:[1e-5,.6,0,0,.875],9120:[.64502,1.155,0,0,.875],9121:[.64502,1.155,0,0,.66667],9122:[-99e-5,.601,0,0,.66667],9123:[.64502,1.155,0,0,.66667],9124:[.64502,1.155,0,0,.66667],9125:[-99e-5,.601,0,0,.66667],9126:[.64502,1.155,0,0,.66667],9127:[1e-5,.9,0,0,.88889],9128:[.65002,1.15,0,0,.88889],9129:[.90001,0,0,0,.88889],9130:[0,.3,0,0,.88889],9131:[1e-5,.9,0,0,.88889],9132:[.65002,1.15,0,0,.88889],9133:[.90001,0,0,0,.88889],9143:[.88502,.915,0,0,1.05556],10216:[1.25003,1.75,0,0,.80556],10217:[1.25003,1.75,0,0,.80556],57344:[-.00499,.605,0,0,1.05556],57345:[-.00499,.605,0,0,1.05556],57680:[0,.12,0,0,.45],57681:[0,.12,0,0,.45],57682:[0,.12,0,0,.45],57683:[0,.12,0,0,.45]},"Typewriter-Regular":{32:[0,0,0,0,.525],33:[0,.61111,0,0,.525],34:[0,.61111,0,0,.525],35:[0,.61111,0,0,.525],36:[.08333,.69444,0,0,.525],37:[.08333,.69444,0,0,.525],38:[0,.61111,0,0,.525],39:[0,.61111,0,0,.525],40:[.08333,.69444,0,0,.525],41:[.08333,.69444,0,0,.525],42:[0,.52083,0,0,.525],43:[-.08056,.53055,0,0,.525],44:[.13889,.125,0,0,.525],45:[-.08056,.53055,0,0,.525],46:[0,.125,0,0,.525],47:[.08333,.69444,0,0,.525],48:[0,.61111,0,0,.525],49:[0,.61111,0,0,.525],50:[0,.61111,0,0,.525],51:[0,.61111,0,0,.525],52:[0,.61111,0,0,.525],53:[0,.61111,0,0,.525],54:[0,.61111,0,0,.525],55:[0,.61111,0,0,.525],56:[0,.61111,0,0,.525],57:[0,.61111,0,0,.525],58:[0,.43056,0,0,.525],59:[.13889,.43056,0,0,.525],60:[-.05556,.55556,0,0,.525],61:[-.19549,.41562,0,0,.525],62:[-.05556,.55556,0,0,.525],63:[0,.61111,0,0,.525],64:[0,.61111,0,0,.525],65:[0,.61111,0,0,.525],66:[0,.61111,0,0,.525],67:[0,.61111,0,0,.525],68:[0,.61111,0,0,.525],69:[0,.61111,0,0,.525],70:[0,.61111,0,0,.525],71:[0,.61111,0,0,.525],72:[0,.61111,0,0,.525],73:[0,.61111,0,0,.525],74:[0,.61111,0,0,.525],75:[0,.61111,0,0,.525],76:[0,.61111,0,0,.525],77:[0,.61111,0,0,.525],78:[0,.61111,0,0,.525],79:[0,.61111,0,0,.525],80:[0,.61111,0,0,.525],81:[.13889,.61111,0,0,.525],82:[0,.61111,0,0,.525],83:[0,.61111,0,0,.525],84:[0,.61111,0,0,.525],85:[0,.61111,0,0,.525],86:[0,.61111,0,0,.525],87:[0,.61111,0,0,.525],88:[0,.61111,0,0,.525],89:[0,.61111,0,0,.525],90:[0,.61111,0,0,.525],91:[.08333,.69444,0,0,.525],92:[.08333,.69444,0,0,.525],93:[.08333,.69444,0,0,.525],94:[0,.61111,0,0,.525],95:[.09514,0,0,0,.525],96:[0,.61111,0,0,.525],97:[0,.43056,0,0,.525],98:[0,.61111,0,0,.525],99:[0,.43056,0,0,.525],100:[0,.61111,0,0,.525],101:[0,.43056,0,0,.525],102:[0,.61111,0,0,.525],103:[.22222,.43056,0,0,.525],104:[0,.61111,0,0,.525],105:[0,.61111,0,0,.525],106:[.22222,.61111,0,0,.525],107:[0,.61111,0,0,.525],108:[0,.61111,0,0,.525],109:[0,.43056,0,0,.525],110:[0,.43056,0,0,.525],111:[0,.43056,0,0,.525],112:[.22222,.43056,0,0,.525],113:[.22222,.43056,0,0,.525],114:[0,.43056,0,0,.525],115:[0,.43056,0,0,.525],116:[0,.55358,0,0,.525],117:[0,.43056,0,0,.525],118:[0,.43056,0,0,.525],119:[0,.43056,0,0,.525],120:[0,.43056,0,0,.525],121:[.22222,.43056,0,0,.525],122:[0,.43056,0,0,.525],123:[.08333,.69444,0,0,.525],124:[.08333,.69444,0,0,.525],125:[.08333,.69444,0,0,.525],126:[0,.61111,0,0,.525],127:[0,.61111,0,0,.525],160:[0,0,0,0,.525],176:[0,.61111,0,0,.525],184:[.19445,0,0,0,.525],305:[0,.43056,0,0,.525],567:[.22222,.43056,0,0,.525],711:[0,.56597,0,0,.525],713:[0,.56555,0,0,.525],714:[0,.61111,0,0,.525],715:[0,.61111,0,0,.525],728:[0,.61111,0,0,.525],730:[0,.61111,0,0,.525],770:[0,.61111,0,0,.525],771:[0,.61111,0,0,.525],776:[0,.61111,0,0,.525],915:[0,.61111,0,0,.525],916:[0,.61111,0,0,.525],920:[0,.61111,0,0,.525],923:[0,.61111,0,0,.525],926:[0,.61111,0,0,.525],928:[0,.61111,0,0,.525],931:[0,.61111,0,0,.525],933:[0,.61111,0,0,.525],934:[0,.61111,0,0,.525],936:[0,.61111,0,0,.525],937:[0,.61111,0,0,.525],8216:[0,.61111,0,0,.525],8217:[0,.61111,0,0,.525],8242:[0,.61111,0,0,.525],9251:[.11111,.21944,0,0,.525]}},ke={slant:[.25,.25,.25],space:[0,0,0],stretch:[0,0,0],shrink:[0,0,0],xHeight:[.431,.431,.431],quad:[1,1.171,1.472],extraSpace:[0,0,0],num1:[.677,.732,.925],num2:[.394,.384,.387],num3:[.444,.471,.504],denom1:[.686,.752,1.025],denom2:[.345,.344,.532],sup1:[.413,.503,.504],sup2:[.363,.431,.404],sup3:[.289,.286,.294],sub1:[.15,.143,.2],sub2:[.247,.286,.4],supDrop:[.386,.353,.494],subDrop:[.05,.071,.1],delim1:[2.39,1.7,1.98],delim2:[1.01,1.157,1.42],axisHeight:[.25,.25,.25],defaultRuleThickness:[.04,.049,.049],bigOpSpacing1:[.111,.111,.111],bigOpSpacing2:[.166,.166,.166],bigOpSpacing3:[.2,.2,.2],bigOpSpacing4:[.6,.611,.611],bigOpSpacing5:[.1,.143,.143],sqrtRuleThickness:[.04,.04,.04],ptPerEm:[10,10,10],doubleRuleSep:[.2,.2,.2],arrayRuleWidth:[.04,.04,.04],fboxsep:[.3,.3,.3],fboxrule:[.04,.04,.04]},Zt={\u00C5:"A",\u00D0:"D",\u00DE:"o",\u00E5:"a",\u00F0:"d",\u00FE:"o",\u0410:"A",\u0411:"B",\u0412:"B",\u0413:"F",\u0414:"A",\u0415:"E",\u0416:"K",\u0417:"3",\u0418:"N",\u0419:"N",\u041A:"K",\u041B:"N",\u041C:"M",\u041D:"H",\u041E:"O",\u041F:"N",\u0420:"P",\u0421:"C",\u0422:"T",\u0423:"y",\u0424:"O",\u0425:"X",\u0426:"U",\u0427:"h",\u0428:"W",\u0429:"W",\u042A:"B",\u042B:"X",\u042C:"B",\u042D:"3",\u042E:"X",\u042F:"R",\u0430:"a",\u0431:"b",\u0432:"a",\u0433:"r",\u0434:"y",\u0435:"e",\u0436:"m",\u0437:"e",\u0438:"n",\u0439:"n",\u043A:"n",\u043B:"n",\u043C:"m",\u043D:"n",\u043E:"o",\u043F:"n",\u0440:"p",\u0441:"c",\u0442:"o",\u0443:"y",\u0444:"b",\u0445:"x",\u0446:"n",\u0447:"n",\u0448:"w",\u0449:"w",\u044A:"a",\u044B:"m",\u044C:"a",\u044D:"e",\u044E:"m",\u044F:"r"};function Ja(r,e){z0[r]=e}function Tt(r,e,t){if(!z0[e])throw new Error("Font metrics not found for font: "+e+".");var a=r.charCodeAt(0),n=z0[e][a];if(!n&&r[0]in Zt&&(a=Zt[r[0]].charCodeAt(0),n=z0[e][a]),!n&&t==="text"&&Ar(a)&&(n=z0[e][77]),n)return{depth:n[0],height:n[1],italic:n[2],skew:n[3],width:n[4]}}var tt={};function Qa(r){var e;if(r>=5?e=0:r>=3?e=1:e=2,!tt[e]){var t=tt[e]={cssEmPerMu:ke.quad[e]/18};for(var a in ke)ke.hasOwnProperty(a)&&(t[a]=ke[a][e])}return tt[e]}var e1=[[1,1,1],[2,1,1],[3,1,1],[4,2,1],[5,2,1],[6,3,1],[7,4,2],[8,6,3],[9,7,6],[10,8,7],[11,10,9]],jt=[.5,.6,.7,.8,.9,1,1.2,1.44,1.728,2.074,2.488],Kt=function(e,t){return t.size<2?e:e1[e-1][t.size-1]},Re=class r{constructor(e){this.style=void 0,this.color=void 0,this.size=void 0,this.textSize=void 0,this.phantom=void 0,this.font=void 0,this.fontFamily=void 0,this.fontWeight=void 0,this.fontShape=void 0,this.sizeMultiplier=void 0,this.maxSize=void 0,this.minRuleThickness=void 0,this._fontMetrics=void 0,this.style=e.style,this.color=e.color,this.size=e.size||r.BASESIZE,this.textSize=e.textSize||this.size,this.phantom=!!e.phantom,this.font=e.font||"",this.fontFamily=e.fontFamily||"",this.fontWeight=e.fontWeight||"",this.fontShape=e.fontShape||"",this.sizeMultiplier=jt[this.size-1],this.maxSize=e.maxSize,this.minRuleThickness=e.minRuleThickness,this._fontMetrics=void 0}extend(e){var t={style:this.style,size:this.size,textSize:this.textSize,color:this.color,phantom:this.phantom,font:this.font,fontFamily:this.fontFamily,fontWeight:this.fontWeight,fontShape:this.fontShape,maxSize:this.maxSize,minRuleThickness:this.minRuleThickness};for(var a in e)e.hasOwnProperty(a)&&(t[a]=e[a]);return new r(t)}havingStyle(e){return this.style===e?this:this.extend({style:e,size:Kt(this.textSize,e)})}havingCrampedStyle(){return this.havingStyle(this.style.cramp())}havingSize(e){return this.size===e&&this.textSize===e?this:this.extend({style:this.style.text(),size:e,textSize:e,sizeMultiplier:jt[e-1]})}havingBaseStyle(e){e=e||this.style.text();var t=Kt(r.BASESIZE,e);return this.size===t&&this.textSize===r.BASESIZE&&this.style===e?this:this.extend({style:e,size:t})}havingBaseSizing(){var e;switch(this.style.id){case 4:case 5:e=3;break;case 6:case 7:e=1;break;default:e=6}return this.extend({style:this.style.text(),size:e})}withColor(e){return this.extend({color:e})}withPhantom(){return this.extend({phantom:!0})}withFont(e){return this.extend({font:e})}withTextFontFamily(e){return this.extend({fontFamily:e,font:""})}withTextFontWeight(e){return this.extend({fontWeight:e,font:""})}withTextFontShape(e){return this.extend({fontShape:e,font:""})}sizingClasses(e){return e.size!==this.size?["sizing","reset-size"+e.size,"size"+this.size]:[]}baseSizingClasses(){return this.size!==r.BASESIZE?["sizing","reset-size"+this.size,"size"+r.BASESIZE]:[]}fontMetrics(){return this._fontMetrics||(this._fontMetrics=Qa(this.size)),this._fontMetrics}getColor(){return this.phantom?"transparent":this.color}};Re.BASESIZE=6;var ft={pt:1,mm:7227/2540,cm:7227/254,in:72.27,bp:803/800,pc:12,dd:1238/1157,cc:14856/1157,nd:685/642,nc:1370/107,sp:1/65536,px:803/800},t1={ex:!0,em:!0,mu:!0},Tr=function(e){return typeof e!="string"&&(e=e.unit),e in ft||e in t1||e==="ex"},Q=function(e,t){var a;if(e.unit in ft)a=ft[e.unit]/t.fontMetrics().ptPerEm/t.sizeMultiplier;else if(e.unit==="mu")a=t.fontMetrics().cssEmPerMu;else{var n;if(t.style.isTight()?n=t.havingStyle(t.style.text()):n=t,e.unit==="ex")a=n.fontMetrics().xHeight;else if(e.unit==="em")a=n.fontMetrics().quad;else throw new z("Invalid unit: '"+e.unit+"'");n!==t&&(a*=n.sizeMultiplier/t.sizeMultiplier)}return Math.min(e.number*a,t.maxSize)},T=function(e){return+e.toFixed(4)+"em"},G0=function(e){return e.filter(t=>t).join(" ")},qr=function(e,t,a){if(this.classes=e||[],this.attributes={},this.height=0,this.depth=0,this.maxFontSize=0,this.style=a||{},t){t.style.isTight()&&this.classes.push("mtight");var n=t.getColor();n&&(this.style.color=n)}},Br=function(e){var t=document.createElement(e);t.className=G0(this.classes);for(var a in this.style)this.style.hasOwnProperty(a)&&(t.style[a]=this.style[a]);for(var n in this.attributes)this.attributes.hasOwnProperty(n)&&t.setAttribute(n,this.attributes[n]);for(var s=0;s/=\x00-\x1f]/,Dr=function(e){var t="<"+e;this.classes.length&&(t+=' class="'+O.escape(G0(this.classes))+'"');var a="";for(var n in this.style)this.style.hasOwnProperty(n)&&(a+=O.hyphenate(n)+":"+this.style[n]+";");a&&(t+=' style="'+O.escape(a)+'"');for(var s in this.attributes)if(this.attributes.hasOwnProperty(s)){if(r1.test(s))throw new z("Invalid attribute name '"+s+"'");t+=" "+s+'="'+O.escape(this.attributes[s])+'"'}t+=">";for(var l=0;l",t},Z0=class{constructor(e,t,a,n){this.children=void 0,this.attributes=void 0,this.classes=void 0,this.height=void 0,this.depth=void 0,this.width=void 0,this.maxFontSize=void 0,this.style=void 0,qr.call(this,e,a,n),this.children=t||[]}setAttribute(e,t){this.attributes[e]=t}hasClass(e){return O.contains(this.classes,e)}toNode(){return Br.call(this,"span")}toMarkup(){return Dr.call(this,"span")}},fe=class{constructor(e,t,a,n){this.children=void 0,this.attributes=void 0,this.classes=void 0,this.height=void 0,this.depth=void 0,this.maxFontSize=void 0,this.style=void 0,qr.call(this,t,n),this.children=a||[],this.setAttribute("href",e)}setAttribute(e,t){this.attributes[e]=t}hasClass(e){return O.contains(this.classes,e)}toNode(){return Br.call(this,"a")}toMarkup(){return Dr.call(this,"a")}},vt=class{constructor(e,t,a){this.src=void 0,this.alt=void 0,this.classes=void 0,this.height=void 0,this.depth=void 0,this.maxFontSize=void 0,this.style=void 0,this.alt=t,this.src=e,this.classes=["mord"],this.style=a}hasClass(e){return O.contains(this.classes,e)}toNode(){var e=document.createElement("img");e.src=this.src,e.alt=this.alt,e.className="mord";for(var t in this.style)this.style.hasOwnProperty(t)&&(e.style[t]=this.style[t]);return e}toMarkup(){var e=''+O.escape(this.alt)+'0&&(t=document.createElement("span"),t.style.marginRight=T(this.italic)),this.classes.length>0&&(t=t||document.createElement("span"),t.className=G0(this.classes));for(var a in this.style)this.style.hasOwnProperty(a)&&(t=t||document.createElement("span"),t.style[a]=this.style[a]);return t?(t.appendChild(e),t):e}toMarkup(){var e=!1,t="0&&(a+="margin-right:"+this.italic+"em;");for(var n in this.style)this.style.hasOwnProperty(n)&&(a+=O.hyphenate(n)+":"+this.style[n]+";");a&&(e=!0,t+=' style="'+O.escape(a)+'"');var s=O.escape(this.text);return e?(t+=">",t+=s,t+="",t):s}},S0=class{constructor(e,t){this.children=void 0,this.attributes=void 0,this.children=e||[],this.attributes=t||{}}toNode(){var e="http://www.w3.org/2000/svg",t=document.createElementNS(e,"svg");for(var a in this.attributes)Object.prototype.hasOwnProperty.call(this.attributes,a)&&t.setAttribute(a,this.attributes[a]);for(var n=0;n':''}},ve=class{constructor(e){this.attributes=void 0,this.attributes=e||{}}toNode(){var e="http://www.w3.org/2000/svg",t=document.createElementNS(e,"line");for(var a in this.attributes)Object.prototype.hasOwnProperty.call(this.attributes,a)&&t.setAttribute(a,this.attributes[a]);return t}toMarkup(){var e=" but got "+String(r)+".")}var i1={bin:1,close:1,inner:1,open:1,punct:1,rel:1},s1={"accent-token":1,mathord:1,"op-token":1,spacing:1,textord:1},Y={math:{},text:{}};function i(r,e,t,a,n,s){Y[r][n]={font:e,group:t,replace:a},s&&a&&(Y[r][a]=Y[r][n])}var o="math",k="text",u="main",d="ams",Z="accent-token",C="bin",l0="close",ie="inner",I="mathord",t0="op-token",p0="open",Ve="punct",p="rel",R0="spacing",g="textord";i(o,u,p,"\u2261","\\equiv",!0);i(o,u,p,"\u227A","\\prec",!0);i(o,u,p,"\u227B","\\succ",!0);i(o,u,p,"\u223C","\\sim",!0);i(o,u,p,"\u22A5","\\perp");i(o,u,p,"\u2AAF","\\preceq",!0);i(o,u,p,"\u2AB0","\\succeq",!0);i(o,u,p,"\u2243","\\simeq",!0);i(o,u,p,"\u2223","\\mid",!0);i(o,u,p,"\u226A","\\ll",!0);i(o,u,p,"\u226B","\\gg",!0);i(o,u,p,"\u224D","\\asymp",!0);i(o,u,p,"\u2225","\\parallel");i(o,u,p,"\u22C8","\\bowtie",!0);i(o,u,p,"\u2323","\\smile",!0);i(o,u,p,"\u2291","\\sqsubseteq",!0);i(o,u,p,"\u2292","\\sqsupseteq",!0);i(o,u,p,"\u2250","\\doteq",!0);i(o,u,p,"\u2322","\\frown",!0);i(o,u,p,"\u220B","\\ni",!0);i(o,u,p,"\u221D","\\propto",!0);i(o,u,p,"\u22A2","\\vdash",!0);i(o,u,p,"\u22A3","\\dashv",!0);i(o,u,p,"\u220B","\\owns");i(o,u,Ve,".","\\ldotp");i(o,u,Ve,"\u22C5","\\cdotp");i(o,u,g,"#","\\#");i(k,u,g,"#","\\#");i(o,u,g,"&","\\&");i(k,u,g,"&","\\&");i(o,u,g,"\u2135","\\aleph",!0);i(o,u,g,"\u2200","\\forall",!0);i(o,u,g,"\u210F","\\hbar",!0);i(o,u,g,"\u2203","\\exists",!0);i(o,u,g,"\u2207","\\nabla",!0);i(o,u,g,"\u266D","\\flat",!0);i(o,u,g,"\u2113","\\ell",!0);i(o,u,g,"\u266E","\\natural",!0);i(o,u,g,"\u2663","\\clubsuit",!0);i(o,u,g,"\u2118","\\wp",!0);i(o,u,g,"\u266F","\\sharp",!0);i(o,u,g,"\u2662","\\diamondsuit",!0);i(o,u,g,"\u211C","\\Re",!0);i(o,u,g,"\u2661","\\heartsuit",!0);i(o,u,g,"\u2111","\\Im",!0);i(o,u,g,"\u2660","\\spadesuit",!0);i(o,u,g,"\xA7","\\S",!0);i(k,u,g,"\xA7","\\S");i(o,u,g,"\xB6","\\P",!0);i(k,u,g,"\xB6","\\P");i(o,u,g,"\u2020","\\dag");i(k,u,g,"\u2020","\\dag");i(k,u,g,"\u2020","\\textdagger");i(o,u,g,"\u2021","\\ddag");i(k,u,g,"\u2021","\\ddag");i(k,u,g,"\u2021","\\textdaggerdbl");i(o,u,l0,"\u23B1","\\rmoustache",!0);i(o,u,p0,"\u23B0","\\lmoustache",!0);i(o,u,l0,"\u27EF","\\rgroup",!0);i(o,u,p0,"\u27EE","\\lgroup",!0);i(o,u,C,"\u2213","\\mp",!0);i(o,u,C,"\u2296","\\ominus",!0);i(o,u,C,"\u228E","\\uplus",!0);i(o,u,C,"\u2293","\\sqcap",!0);i(o,u,C,"\u2217","\\ast");i(o,u,C,"\u2294","\\sqcup",!0);i(o,u,C,"\u25EF","\\bigcirc",!0);i(o,u,C,"\u2219","\\bullet",!0);i(o,u,C,"\u2021","\\ddagger");i(o,u,C,"\u2240","\\wr",!0);i(o,u,C,"\u2A3F","\\amalg");i(o,u,C,"&","\\And");i(o,u,p,"\u27F5","\\longleftarrow",!0);i(o,u,p,"\u21D0","\\Leftarrow",!0);i(o,u,p,"\u27F8","\\Longleftarrow",!0);i(o,u,p,"\u27F6","\\longrightarrow",!0);i(o,u,p,"\u21D2","\\Rightarrow",!0);i(o,u,p,"\u27F9","\\Longrightarrow",!0);i(o,u,p,"\u2194","\\leftrightarrow",!0);i(o,u,p,"\u27F7","\\longleftrightarrow",!0);i(o,u,p,"\u21D4","\\Leftrightarrow",!0);i(o,u,p,"\u27FA","\\Longleftrightarrow",!0);i(o,u,p,"\u21A6","\\mapsto",!0);i(o,u,p,"\u27FC","\\longmapsto",!0);i(o,u,p,"\u2197","\\nearrow",!0);i(o,u,p,"\u21A9","\\hookleftarrow",!0);i(o,u,p,"\u21AA","\\hookrightarrow",!0);i(o,u,p,"\u2198","\\searrow",!0);i(o,u,p,"\u21BC","\\leftharpoonup",!0);i(o,u,p,"\u21C0","\\rightharpoonup",!0);i(o,u,p,"\u2199","\\swarrow",!0);i(o,u,p,"\u21BD","\\leftharpoondown",!0);i(o,u,p,"\u21C1","\\rightharpoondown",!0);i(o,u,p,"\u2196","\\nwarrow",!0);i(o,u,p,"\u21CC","\\rightleftharpoons",!0);i(o,d,p,"\u226E","\\nless",!0);i(o,d,p,"\uE010","\\@nleqslant");i(o,d,p,"\uE011","\\@nleqq");i(o,d,p,"\u2A87","\\lneq",!0);i(o,d,p,"\u2268","\\lneqq",!0);i(o,d,p,"\uE00C","\\@lvertneqq");i(o,d,p,"\u22E6","\\lnsim",!0);i(o,d,p,"\u2A89","\\lnapprox",!0);i(o,d,p,"\u2280","\\nprec",!0);i(o,d,p,"\u22E0","\\npreceq",!0);i(o,d,p,"\u22E8","\\precnsim",!0);i(o,d,p,"\u2AB9","\\precnapprox",!0);i(o,d,p,"\u2241","\\nsim",!0);i(o,d,p,"\uE006","\\@nshortmid");i(o,d,p,"\u2224","\\nmid",!0);i(o,d,p,"\u22AC","\\nvdash",!0);i(o,d,p,"\u22AD","\\nvDash",!0);i(o,d,p,"\u22EA","\\ntriangleleft");i(o,d,p,"\u22EC","\\ntrianglelefteq",!0);i(o,d,p,"\u228A","\\subsetneq",!0);i(o,d,p,"\uE01A","\\@varsubsetneq");i(o,d,p,"\u2ACB","\\subsetneqq",!0);i(o,d,p,"\uE017","\\@varsubsetneqq");i(o,d,p,"\u226F","\\ngtr",!0);i(o,d,p,"\uE00F","\\@ngeqslant");i(o,d,p,"\uE00E","\\@ngeqq");i(o,d,p,"\u2A88","\\gneq",!0);i(o,d,p,"\u2269","\\gneqq",!0);i(o,d,p,"\uE00D","\\@gvertneqq");i(o,d,p,"\u22E7","\\gnsim",!0);i(o,d,p,"\u2A8A","\\gnapprox",!0);i(o,d,p,"\u2281","\\nsucc",!0);i(o,d,p,"\u22E1","\\nsucceq",!0);i(o,d,p,"\u22E9","\\succnsim",!0);i(o,d,p,"\u2ABA","\\succnapprox",!0);i(o,d,p,"\u2246","\\ncong",!0);i(o,d,p,"\uE007","\\@nshortparallel");i(o,d,p,"\u2226","\\nparallel",!0);i(o,d,p,"\u22AF","\\nVDash",!0);i(o,d,p,"\u22EB","\\ntriangleright");i(o,d,p,"\u22ED","\\ntrianglerighteq",!0);i(o,d,p,"\uE018","\\@nsupseteqq");i(o,d,p,"\u228B","\\supsetneq",!0);i(o,d,p,"\uE01B","\\@varsupsetneq");i(o,d,p,"\u2ACC","\\supsetneqq",!0);i(o,d,p,"\uE019","\\@varsupsetneqq");i(o,d,p,"\u22AE","\\nVdash",!0);i(o,d,p,"\u2AB5","\\precneqq",!0);i(o,d,p,"\u2AB6","\\succneqq",!0);i(o,d,p,"\uE016","\\@nsubseteqq");i(o,d,C,"\u22B4","\\unlhd");i(o,d,C,"\u22B5","\\unrhd");i(o,d,p,"\u219A","\\nleftarrow",!0);i(o,d,p,"\u219B","\\nrightarrow",!0);i(o,d,p,"\u21CD","\\nLeftarrow",!0);i(o,d,p,"\u21CF","\\nRightarrow",!0);i(o,d,p,"\u21AE","\\nleftrightarrow",!0);i(o,d,p,"\u21CE","\\nLeftrightarrow",!0);i(o,d,p,"\u25B3","\\vartriangle");i(o,d,g,"\u210F","\\hslash");i(o,d,g,"\u25BD","\\triangledown");i(o,d,g,"\u25CA","\\lozenge");i(o,d,g,"\u24C8","\\circledS");i(o,d,g,"\xAE","\\circledR");i(k,d,g,"\xAE","\\circledR");i(o,d,g,"\u2221","\\measuredangle",!0);i(o,d,g,"\u2204","\\nexists");i(o,d,g,"\u2127","\\mho");i(o,d,g,"\u2132","\\Finv",!0);i(o,d,g,"\u2141","\\Game",!0);i(o,d,g,"\u2035","\\backprime");i(o,d,g,"\u25B2","\\blacktriangle");i(o,d,g,"\u25BC","\\blacktriangledown");i(o,d,g,"\u25A0","\\blacksquare");i(o,d,g,"\u29EB","\\blacklozenge");i(o,d,g,"\u2605","\\bigstar");i(o,d,g,"\u2222","\\sphericalangle",!0);i(o,d,g,"\u2201","\\complement",!0);i(o,d,g,"\xF0","\\eth",!0);i(k,u,g,"\xF0","\xF0");i(o,d,g,"\u2571","\\diagup");i(o,d,g,"\u2572","\\diagdown");i(o,d,g,"\u25A1","\\square");i(o,d,g,"\u25A1","\\Box");i(o,d,g,"\u25CA","\\Diamond");i(o,d,g,"\xA5","\\yen",!0);i(k,d,g,"\xA5","\\yen",!0);i(o,d,g,"\u2713","\\checkmark",!0);i(k,d,g,"\u2713","\\checkmark");i(o,d,g,"\u2136","\\beth",!0);i(o,d,g,"\u2138","\\daleth",!0);i(o,d,g,"\u2137","\\gimel",!0);i(o,d,g,"\u03DD","\\digamma",!0);i(o,d,g,"\u03F0","\\varkappa");i(o,d,p0,"\u250C","\\@ulcorner",!0);i(o,d,l0,"\u2510","\\@urcorner",!0);i(o,d,p0,"\u2514","\\@llcorner",!0);i(o,d,l0,"\u2518","\\@lrcorner",!0);i(o,d,p,"\u2266","\\leqq",!0);i(o,d,p,"\u2A7D","\\leqslant",!0);i(o,d,p,"\u2A95","\\eqslantless",!0);i(o,d,p,"\u2272","\\lesssim",!0);i(o,d,p,"\u2A85","\\lessapprox",!0);i(o,d,p,"\u224A","\\approxeq",!0);i(o,d,C,"\u22D6","\\lessdot");i(o,d,p,"\u22D8","\\lll",!0);i(o,d,p,"\u2276","\\lessgtr",!0);i(o,d,p,"\u22DA","\\lesseqgtr",!0);i(o,d,p,"\u2A8B","\\lesseqqgtr",!0);i(o,d,p,"\u2251","\\doteqdot");i(o,d,p,"\u2253","\\risingdotseq",!0);i(o,d,p,"\u2252","\\fallingdotseq",!0);i(o,d,p,"\u223D","\\backsim",!0);i(o,d,p,"\u22CD","\\backsimeq",!0);i(o,d,p,"\u2AC5","\\subseteqq",!0);i(o,d,p,"\u22D0","\\Subset",!0);i(o,d,p,"\u228F","\\sqsubset",!0);i(o,d,p,"\u227C","\\preccurlyeq",!0);i(o,d,p,"\u22DE","\\curlyeqprec",!0);i(o,d,p,"\u227E","\\precsim",!0);i(o,d,p,"\u2AB7","\\precapprox",!0);i(o,d,p,"\u22B2","\\vartriangleleft");i(o,d,p,"\u22B4","\\trianglelefteq");i(o,d,p,"\u22A8","\\vDash",!0);i(o,d,p,"\u22AA","\\Vvdash",!0);i(o,d,p,"\u2323","\\smallsmile");i(o,d,p,"\u2322","\\smallfrown");i(o,d,p,"\u224F","\\bumpeq",!0);i(o,d,p,"\u224E","\\Bumpeq",!0);i(o,d,p,"\u2267","\\geqq",!0);i(o,d,p,"\u2A7E","\\geqslant",!0);i(o,d,p,"\u2A96","\\eqslantgtr",!0);i(o,d,p,"\u2273","\\gtrsim",!0);i(o,d,p,"\u2A86","\\gtrapprox",!0);i(o,d,C,"\u22D7","\\gtrdot");i(o,d,p,"\u22D9","\\ggg",!0);i(o,d,p,"\u2277","\\gtrless",!0);i(o,d,p,"\u22DB","\\gtreqless",!0);i(o,d,p,"\u2A8C","\\gtreqqless",!0);i(o,d,p,"\u2256","\\eqcirc",!0);i(o,d,p,"\u2257","\\circeq",!0);i(o,d,p,"\u225C","\\triangleq",!0);i(o,d,p,"\u223C","\\thicksim");i(o,d,p,"\u2248","\\thickapprox");i(o,d,p,"\u2AC6","\\supseteqq",!0);i(o,d,p,"\u22D1","\\Supset",!0);i(o,d,p,"\u2290","\\sqsupset",!0);i(o,d,p,"\u227D","\\succcurlyeq",!0);i(o,d,p,"\u22DF","\\curlyeqsucc",!0);i(o,d,p,"\u227F","\\succsim",!0);i(o,d,p,"\u2AB8","\\succapprox",!0);i(o,d,p,"\u22B3","\\vartriangleright");i(o,d,p,"\u22B5","\\trianglerighteq");i(o,d,p,"\u22A9","\\Vdash",!0);i(o,d,p,"\u2223","\\shortmid");i(o,d,p,"\u2225","\\shortparallel");i(o,d,p,"\u226C","\\between",!0);i(o,d,p,"\u22D4","\\pitchfork",!0);i(o,d,p,"\u221D","\\varpropto");i(o,d,p,"\u25C0","\\blacktriangleleft");i(o,d,p,"\u2234","\\therefore",!0);i(o,d,p,"\u220D","\\backepsilon");i(o,d,p,"\u25B6","\\blacktriangleright");i(o,d,p,"\u2235","\\because",!0);i(o,d,p,"\u22D8","\\llless");i(o,d,p,"\u22D9","\\gggtr");i(o,d,C,"\u22B2","\\lhd");i(o,d,C,"\u22B3","\\rhd");i(o,d,p,"\u2242","\\eqsim",!0);i(o,u,p,"\u22C8","\\Join");i(o,d,p,"\u2251","\\Doteq",!0);i(o,d,C,"\u2214","\\dotplus",!0);i(o,d,C,"\u2216","\\smallsetminus");i(o,d,C,"\u22D2","\\Cap",!0);i(o,d,C,"\u22D3","\\Cup",!0);i(o,d,C,"\u2A5E","\\doublebarwedge",!0);i(o,d,C,"\u229F","\\boxminus",!0);i(o,d,C,"\u229E","\\boxplus",!0);i(o,d,C,"\u22C7","\\divideontimes",!0);i(o,d,C,"\u22C9","\\ltimes",!0);i(o,d,C,"\u22CA","\\rtimes",!0);i(o,d,C,"\u22CB","\\leftthreetimes",!0);i(o,d,C,"\u22CC","\\rightthreetimes",!0);i(o,d,C,"\u22CF","\\curlywedge",!0);i(o,d,C,"\u22CE","\\curlyvee",!0);i(o,d,C,"\u229D","\\circleddash",!0);i(o,d,C,"\u229B","\\circledast",!0);i(o,d,C,"\u22C5","\\centerdot");i(o,d,C,"\u22BA","\\intercal",!0);i(o,d,C,"\u22D2","\\doublecap");i(o,d,C,"\u22D3","\\doublecup");i(o,d,C,"\u22A0","\\boxtimes",!0);i(o,d,p,"\u21E2","\\dashrightarrow",!0);i(o,d,p,"\u21E0","\\dashleftarrow",!0);i(o,d,p,"\u21C7","\\leftleftarrows",!0);i(o,d,p,"\u21C6","\\leftrightarrows",!0);i(o,d,p,"\u21DA","\\Lleftarrow",!0);i(o,d,p,"\u219E","\\twoheadleftarrow",!0);i(o,d,p,"\u21A2","\\leftarrowtail",!0);i(o,d,p,"\u21AB","\\looparrowleft",!0);i(o,d,p,"\u21CB","\\leftrightharpoons",!0);i(o,d,p,"\u21B6","\\curvearrowleft",!0);i(o,d,p,"\u21BA","\\circlearrowleft",!0);i(o,d,p,"\u21B0","\\Lsh",!0);i(o,d,p,"\u21C8","\\upuparrows",!0);i(o,d,p,"\u21BF","\\upharpoonleft",!0);i(o,d,p,"\u21C3","\\downharpoonleft",!0);i(o,u,p,"\u22B6","\\origof",!0);i(o,u,p,"\u22B7","\\imageof",!0);i(o,d,p,"\u22B8","\\multimap",!0);i(o,d,p,"\u21AD","\\leftrightsquigarrow",!0);i(o,d,p,"\u21C9","\\rightrightarrows",!0);i(o,d,p,"\u21C4","\\rightleftarrows",!0);i(o,d,p,"\u21A0","\\twoheadrightarrow",!0);i(o,d,p,"\u21A3","\\rightarrowtail",!0);i(o,d,p,"\u21AC","\\looparrowright",!0);i(o,d,p,"\u21B7","\\curvearrowright",!0);i(o,d,p,"\u21BB","\\circlearrowright",!0);i(o,d,p,"\u21B1","\\Rsh",!0);i(o,d,p,"\u21CA","\\downdownarrows",!0);i(o,d,p,"\u21BE","\\upharpoonright",!0);i(o,d,p,"\u21C2","\\downharpoonright",!0);i(o,d,p,"\u21DD","\\rightsquigarrow",!0);i(o,d,p,"\u21DD","\\leadsto");i(o,d,p,"\u21DB","\\Rrightarrow",!0);i(o,d,p,"\u21BE","\\restriction");i(o,u,g,"\u2018","`");i(o,u,g,"$","\\$");i(k,u,g,"$","\\$");i(k,u,g,"$","\\textdollar");i(o,u,g,"%","\\%");i(k,u,g,"%","\\%");i(o,u,g,"_","\\_");i(k,u,g,"_","\\_");i(k,u,g,"_","\\textunderscore");i(o,u,g,"\u2220","\\angle",!0);i(o,u,g,"\u221E","\\infty",!0);i(o,u,g,"\u2032","\\prime");i(o,u,g,"\u25B3","\\triangle");i(o,u,g,"\u0393","\\Gamma",!0);i(o,u,g,"\u0394","\\Delta",!0);i(o,u,g,"\u0398","\\Theta",!0);i(o,u,g,"\u039B","\\Lambda",!0);i(o,u,g,"\u039E","\\Xi",!0);i(o,u,g,"\u03A0","\\Pi",!0);i(o,u,g,"\u03A3","\\Sigma",!0);i(o,u,g,"\u03A5","\\Upsilon",!0);i(o,u,g,"\u03A6","\\Phi",!0);i(o,u,g,"\u03A8","\\Psi",!0);i(o,u,g,"\u03A9","\\Omega",!0);i(o,u,g,"A","\u0391");i(o,u,g,"B","\u0392");i(o,u,g,"E","\u0395");i(o,u,g,"Z","\u0396");i(o,u,g,"H","\u0397");i(o,u,g,"I","\u0399");i(o,u,g,"K","\u039A");i(o,u,g,"M","\u039C");i(o,u,g,"N","\u039D");i(o,u,g,"O","\u039F");i(o,u,g,"P","\u03A1");i(o,u,g,"T","\u03A4");i(o,u,g,"X","\u03A7");i(o,u,g,"\xAC","\\neg",!0);i(o,u,g,"\xAC","\\lnot");i(o,u,g,"\u22A4","\\top");i(o,u,g,"\u22A5","\\bot");i(o,u,g,"\u2205","\\emptyset");i(o,d,g,"\u2205","\\varnothing");i(o,u,I,"\u03B1","\\alpha",!0);i(o,u,I,"\u03B2","\\beta",!0);i(o,u,I,"\u03B3","\\gamma",!0);i(o,u,I,"\u03B4","\\delta",!0);i(o,u,I,"\u03F5","\\epsilon",!0);i(o,u,I,"\u03B6","\\zeta",!0);i(o,u,I,"\u03B7","\\eta",!0);i(o,u,I,"\u03B8","\\theta",!0);i(o,u,I,"\u03B9","\\iota",!0);i(o,u,I,"\u03BA","\\kappa",!0);i(o,u,I,"\u03BB","\\lambda",!0);i(o,u,I,"\u03BC","\\mu",!0);i(o,u,I,"\u03BD","\\nu",!0);i(o,u,I,"\u03BE","\\xi",!0);i(o,u,I,"\u03BF","\\omicron",!0);i(o,u,I,"\u03C0","\\pi",!0);i(o,u,I,"\u03C1","\\rho",!0);i(o,u,I,"\u03C3","\\sigma",!0);i(o,u,I,"\u03C4","\\tau",!0);i(o,u,I,"\u03C5","\\upsilon",!0);i(o,u,I,"\u03D5","\\phi",!0);i(o,u,I,"\u03C7","\\chi",!0);i(o,u,I,"\u03C8","\\psi",!0);i(o,u,I,"\u03C9","\\omega",!0);i(o,u,I,"\u03B5","\\varepsilon",!0);i(o,u,I,"\u03D1","\\vartheta",!0);i(o,u,I,"\u03D6","\\varpi",!0);i(o,u,I,"\u03F1","\\varrho",!0);i(o,u,I,"\u03C2","\\varsigma",!0);i(o,u,I,"\u03C6","\\varphi",!0);i(o,u,C,"\u2217","*",!0);i(o,u,C,"+","+");i(o,u,C,"\u2212","-",!0);i(o,u,C,"\u22C5","\\cdot",!0);i(o,u,C,"\u2218","\\circ",!0);i(o,u,C,"\xF7","\\div",!0);i(o,u,C,"\xB1","\\pm",!0);i(o,u,C,"\xD7","\\times",!0);i(o,u,C,"\u2229","\\cap",!0);i(o,u,C,"\u222A","\\cup",!0);i(o,u,C,"\u2216","\\setminus",!0);i(o,u,C,"\u2227","\\land");i(o,u,C,"\u2228","\\lor");i(o,u,C,"\u2227","\\wedge",!0);i(o,u,C,"\u2228","\\vee",!0);i(o,u,g,"\u221A","\\surd");i(o,u,p0,"\u27E8","\\langle",!0);i(o,u,p0,"\u2223","\\lvert");i(o,u,p0,"\u2225","\\lVert");i(o,u,l0,"?","?");i(o,u,l0,"!","!");i(o,u,l0,"\u27E9","\\rangle",!0);i(o,u,l0,"\u2223","\\rvert");i(o,u,l0,"\u2225","\\rVert");i(o,u,p,"=","=");i(o,u,p,":",":");i(o,u,p,"\u2248","\\approx",!0);i(o,u,p,"\u2245","\\cong",!0);i(o,u,p,"\u2265","\\ge");i(o,u,p,"\u2265","\\geq",!0);i(o,u,p,"\u2190","\\gets");i(o,u,p,">","\\gt",!0);i(o,u,p,"\u2208","\\in",!0);i(o,u,p,"\uE020","\\@not");i(o,u,p,"\u2282","\\subset",!0);i(o,u,p,"\u2283","\\supset",!0);i(o,u,p,"\u2286","\\subseteq",!0);i(o,u,p,"\u2287","\\supseteq",!0);i(o,d,p,"\u2288","\\nsubseteq",!0);i(o,d,p,"\u2289","\\nsupseteq",!0);i(o,u,p,"\u22A8","\\models");i(o,u,p,"\u2190","\\leftarrow",!0);i(o,u,p,"\u2264","\\le");i(o,u,p,"\u2264","\\leq",!0);i(o,u,p,"<","\\lt",!0);i(o,u,p,"\u2192","\\rightarrow",!0);i(o,u,p,"\u2192","\\to");i(o,d,p,"\u2271","\\ngeq",!0);i(o,d,p,"\u2270","\\nleq",!0);i(o,u,R0,"\xA0","\\ ");i(o,u,R0,"\xA0","\\space");i(o,u,R0,"\xA0","\\nobreakspace");i(k,u,R0,"\xA0","\\ ");i(k,u,R0,"\xA0"," ");i(k,u,R0,"\xA0","\\space");i(k,u,R0,"\xA0","\\nobreakspace");i(o,u,R0,null,"\\nobreak");i(o,u,R0,null,"\\allowbreak");i(o,u,Ve,",",",");i(o,u,Ve,";",";");i(o,d,C,"\u22BC","\\barwedge",!0);i(o,d,C,"\u22BB","\\veebar",!0);i(o,u,C,"\u2299","\\odot",!0);i(o,u,C,"\u2295","\\oplus",!0);i(o,u,C,"\u2297","\\otimes",!0);i(o,u,g,"\u2202","\\partial",!0);i(o,u,C,"\u2298","\\oslash",!0);i(o,d,C,"\u229A","\\circledcirc",!0);i(o,d,C,"\u22A1","\\boxdot",!0);i(o,u,C,"\u25B3","\\bigtriangleup");i(o,u,C,"\u25BD","\\bigtriangledown");i(o,u,C,"\u2020","\\dagger");i(o,u,C,"\u22C4","\\diamond");i(o,u,C,"\u22C6","\\star");i(o,u,C,"\u25C3","\\triangleleft");i(o,u,C,"\u25B9","\\triangleright");i(o,u,p0,"{","\\{");i(k,u,g,"{","\\{");i(k,u,g,"{","\\textbraceleft");i(o,u,l0,"}","\\}");i(k,u,g,"}","\\}");i(k,u,g,"}","\\textbraceright");i(o,u,p0,"{","\\lbrace");i(o,u,l0,"}","\\rbrace");i(o,u,p0,"[","\\lbrack",!0);i(k,u,g,"[","\\lbrack",!0);i(o,u,l0,"]","\\rbrack",!0);i(k,u,g,"]","\\rbrack",!0);i(o,u,p0,"(","\\lparen",!0);i(o,u,l0,")","\\rparen",!0);i(k,u,g,"<","\\textless",!0);i(k,u,g,">","\\textgreater",!0);i(o,u,p0,"\u230A","\\lfloor",!0);i(o,u,l0,"\u230B","\\rfloor",!0);i(o,u,p0,"\u2308","\\lceil",!0);i(o,u,l0,"\u2309","\\rceil",!0);i(o,u,g,"\\","\\backslash");i(o,u,g,"\u2223","|");i(o,u,g,"\u2223","\\vert");i(k,u,g,"|","\\textbar",!0);i(o,u,g,"\u2225","\\|");i(o,u,g,"\u2225","\\Vert");i(k,u,g,"\u2225","\\textbardbl");i(k,u,g,"~","\\textasciitilde");i(k,u,g,"\\","\\textbackslash");i(k,u,g,"^","\\textasciicircum");i(o,u,p,"\u2191","\\uparrow",!0);i(o,u,p,"\u21D1","\\Uparrow",!0);i(o,u,p,"\u2193","\\downarrow",!0);i(o,u,p,"\u21D3","\\Downarrow",!0);i(o,u,p,"\u2195","\\updownarrow",!0);i(o,u,p,"\u21D5","\\Updownarrow",!0);i(o,u,t0,"\u2210","\\coprod");i(o,u,t0,"\u22C1","\\bigvee");i(o,u,t0,"\u22C0","\\bigwedge");i(o,u,t0,"\u2A04","\\biguplus");i(o,u,t0,"\u22C2","\\bigcap");i(o,u,t0,"\u22C3","\\bigcup");i(o,u,t0,"\u222B","\\int");i(o,u,t0,"\u222B","\\intop");i(o,u,t0,"\u222C","\\iint");i(o,u,t0,"\u222D","\\iiint");i(o,u,t0,"\u220F","\\prod");i(o,u,t0,"\u2211","\\sum");i(o,u,t0,"\u2A02","\\bigotimes");i(o,u,t0,"\u2A01","\\bigoplus");i(o,u,t0,"\u2A00","\\bigodot");i(o,u,t0,"\u222E","\\oint");i(o,u,t0,"\u222F","\\oiint");i(o,u,t0,"\u2230","\\oiiint");i(o,u,t0,"\u2A06","\\bigsqcup");i(o,u,t0,"\u222B","\\smallint");i(k,u,ie,"\u2026","\\textellipsis");i(o,u,ie,"\u2026","\\mathellipsis");i(k,u,ie,"\u2026","\\ldots",!0);i(o,u,ie,"\u2026","\\ldots",!0);i(o,u,ie,"\u22EF","\\@cdots",!0);i(o,u,ie,"\u22F1","\\ddots",!0);i(o,u,g,"\u22EE","\\varvdots");i(k,u,g,"\u22EE","\\varvdots");i(o,u,Z,"\u02CA","\\acute");i(o,u,Z,"\u02CB","\\grave");i(o,u,Z,"\xA8","\\ddot");i(o,u,Z,"~","\\tilde");i(o,u,Z,"\u02C9","\\bar");i(o,u,Z,"\u02D8","\\breve");i(o,u,Z,"\u02C7","\\check");i(o,u,Z,"^","\\hat");i(o,u,Z,"\u20D7","\\vec");i(o,u,Z,"\u02D9","\\dot");i(o,u,Z,"\u02DA","\\mathring");i(o,u,I,"\uE131","\\@imath");i(o,u,I,"\uE237","\\@jmath");i(o,u,g,"\u0131","\u0131");i(o,u,g,"\u0237","\u0237");i(k,u,g,"\u0131","\\i",!0);i(k,u,g,"\u0237","\\j",!0);i(k,u,g,"\xDF","\\ss",!0);i(k,u,g,"\xE6","\\ae",!0);i(k,u,g,"\u0153","\\oe",!0);i(k,u,g,"\xF8","\\o",!0);i(k,u,g,"\xC6","\\AE",!0);i(k,u,g,"\u0152","\\OE",!0);i(k,u,g,"\xD8","\\O",!0);i(k,u,Z,"\u02CA","\\'");i(k,u,Z,"\u02CB","\\`");i(k,u,Z,"\u02C6","\\^");i(k,u,Z,"\u02DC","\\~");i(k,u,Z,"\u02C9","\\=");i(k,u,Z,"\u02D8","\\u");i(k,u,Z,"\u02D9","\\.");i(k,u,Z,"\xB8","\\c");i(k,u,Z,"\u02DA","\\r");i(k,u,Z,"\u02C7","\\v");i(k,u,Z,"\xA8",'\\"');i(k,u,Z,"\u02DD","\\H");i(k,u,Z,"\u25EF","\\textcircled");var Cr={"--":!0,"---":!0,"``":!0,"''":!0};i(k,u,g,"\u2013","--",!0);i(k,u,g,"\u2013","\\textendash");i(k,u,g,"\u2014","---",!0);i(k,u,g,"\u2014","\\textemdash");i(k,u,g,"\u2018","`",!0);i(k,u,g,"\u2018","\\textquoteleft");i(k,u,g,"\u2019","'",!0);i(k,u,g,"\u2019","\\textquoteright");i(k,u,g,"\u201C","``",!0);i(k,u,g,"\u201C","\\textquotedblleft");i(k,u,g,"\u201D","''",!0);i(k,u,g,"\u201D","\\textquotedblright");i(o,u,g,"\xB0","\\degree",!0);i(k,u,g,"\xB0","\\degree");i(k,u,g,"\xB0","\\textdegree",!0);i(o,u,g,"\xA3","\\pounds");i(o,u,g,"\xA3","\\mathsterling",!0);i(k,u,g,"\xA3","\\pounds");i(k,u,g,"\xA3","\\textsterling",!0);i(o,d,g,"\u2720","\\maltese");i(k,d,g,"\u2720","\\maltese");var Qt='0123456789/@."';for(Me=0;Me0)return w0(s,f,n,t,l.concat(v));if(c){var b,x;if(c==="boldsymbol"){var w=u1(s,n,t,l,a);b=w.fontName,x=[w.fontClass]}else h?(b=Or[c].fontName,x=[c]):(b=Be(c,t.fontWeight,t.fontShape),x=[c,t.fontWeight,t.fontShape]);if(Ue(s,b,n).metrics)return w0(s,b,n,t,l.concat(x));if(Cr.hasOwnProperty(s)&&b.slice(0,10)==="Typewriter"){for(var A=[],q=0;q{if(G0(r.classes)!==G0(e.classes)||r.skew!==e.skew||r.maxFontSize!==e.maxFontSize)return!1;if(r.classes.length===1){var t=r.classes[0];if(t==="mbin"||t==="mord")return!1}for(var a in r.style)if(r.style.hasOwnProperty(a)&&r.style[a]!==e.style[a])return!1;for(var n in e.style)if(e.style.hasOwnProperty(n)&&r.style[n]!==e.style[n])return!1;return!0},m1=r=>{for(var e=0;et&&(t=l.height),l.depth>a&&(a=l.depth),l.maxFontSize>n&&(n=l.maxFontSize)}e.height=t,e.depth=a,e.maxFontSize=n},h0=function(e,t,a,n){var s=new Z0(e,t,a,n);return qt(s),s},_r=(r,e,t,a)=>new Z0(r,e,t,a),d1=function(e,t,a){var n=h0([e],[],t);return n.height=Math.max(a||t.fontMetrics().defaultRuleThickness,t.minRuleThickness),n.style.borderBottomWidth=T(n.height),n.maxFontSize=1,n},p1=function(e,t,a,n){var s=new fe(e,t,a,n);return qt(s),s},Nr=function(e){var t=new Y0(e);return qt(t),t},f1=function(e,t){return e instanceof Y0?h0([],[e],t):e},v1=function(e){if(e.positionType==="individualShift"){for(var t=e.children,a=[t[0]],n=-t[0].shift-t[0].elem.depth,s=n,l=1;l{var t=h0(["mspace"],[],e),a=Q(r,e);return t.style.marginRight=T(a),t},Be=function(e,t,a){var n="";switch(e){case"amsrm":n="AMS";break;case"textrm":n="Main";break;case"textsf":n="SansSerif";break;case"texttt":n="Typewriter";break;default:n=e}var s;return t==="textbf"&&a==="textit"?s="BoldItalic":t==="textbf"?s="Bold":t==="textit"?s="Italic":s="Regular",n+"-"+s},Or={mathbf:{variant:"bold",fontName:"Main-Bold"},mathrm:{variant:"normal",fontName:"Main-Regular"},textit:{variant:"italic",fontName:"Main-Italic"},mathit:{variant:"italic",fontName:"Main-Italic"},mathnormal:{variant:"italic",fontName:"Math-Italic"},mathsfit:{variant:"sans-serif-italic",fontName:"SansSerif-Italic"},mathbb:{variant:"double-struck",fontName:"AMS-Regular"},mathcal:{variant:"script",fontName:"Caligraphic-Regular"},mathfrak:{variant:"fraktur",fontName:"Fraktur-Regular"},mathscr:{variant:"script",fontName:"Script-Regular"},mathsf:{variant:"sans-serif",fontName:"SansSerif-Regular"},mathtt:{variant:"monospace",fontName:"Typewriter-Regular"}},Ir={vec:["vec",.471,.714],oiintSize1:["oiintSize1",.957,.499],oiintSize2:["oiintSize2",1.472,.659],oiiintSize1:["oiiintSize1",1.304,.499],oiiintSize2:["oiiintSize2",1.98,.659]},y1=function(e,t){var[a,n,s]=Ir[e],l=new A0(a),h=new S0([l],{width:T(n),height:T(s),style:"width:"+T(n),viewBox:"0 0 "+1e3*n+" "+1e3*s,preserveAspectRatio:"xMinYMin"}),c=_r(["overlay"],[h],t);return c.height=s,c.style.height=T(s),c.style.width=T(n),c},y={fontMap:Or,makeSymbol:w0,mathsym:l1,makeSpan:h0,makeSvgSpan:_r,makeLineSpan:d1,makeAnchor:p1,makeFragment:Nr,wrapFragment:f1,makeVList:g1,makeOrd:h1,makeGlue:b1,staticSvg:y1,svgData:Ir,tryCombineChars:m1},J={number:3,unit:"mu"},W0={number:4,unit:"mu"},_0={number:5,unit:"mu"},x1={mord:{mop:J,mbin:W0,mrel:_0,minner:J},mop:{mord:J,mop:J,mrel:_0,minner:J},mbin:{mord:W0,mop:W0,mopen:W0,minner:W0},mrel:{mord:_0,mop:_0,mopen:_0,minner:_0},mopen:{},mclose:{mop:J,mbin:W0,mrel:_0,minner:J},mpunct:{mord:J,mop:J,mrel:_0,mopen:J,mclose:J,mpunct:J,minner:J},minner:{mord:J,mop:J,mbin:W0,mrel:_0,mopen:J,mpunct:J,minner:J}},w1={mord:{mop:J},mop:{mord:J,mop:J},mbin:{},mrel:{},mopen:{},mclose:{mop:J},mpunct:{},minner:{mop:J}},Er={},Le={},Fe={};function B(r){for(var{type:e,names:t,props:a,handler:n,htmlBuilder:s,mathmlBuilder:l}=r,h={type:e,numArgs:a.numArgs,argTypes:a.argTypes,allowedInArgument:!!a.allowedInArgument,allowedInText:!!a.allowedInText,allowedInMath:a.allowedInMath===void 0?!0:a.allowedInMath,numOptionalArgs:a.numOptionalArgs||0,infix:!!a.infix,primitive:!!a.primitive,handler:n},c=0;c{var _=q.classes[0],D=A.classes[0];_==="mbin"&&O.contains(k1,D)?q.classes[0]="mord":D==="mbin"&&O.contains(S1,_)&&(A.classes[0]="mord")},{node:b},x,w),rr(s,(A,q)=>{var _=bt(q),D=bt(A),N=_&&D?A.hasClass("mtight")?w1[_][D]:x1[_][D]:null;if(N)return y.makeGlue(N,f)},{node:b},x,w),s},rr=function r(e,t,a,n,s){n&&e.push(n);for(var l=0;lx=>{e.splice(b+1,0,x),l++})(l)}n&&e.pop()},Rr=function(e){return e instanceof Y0||e instanceof fe||e instanceof Z0&&e.hasClass("enclosing")?e:null},A1=function r(e,t){var a=Rr(e);if(a){var n=a.children;if(n.length){if(t==="right")return r(n[n.length-1],"right");if(t==="left")return r(n[0],"left")}}return e},bt=function(e,t){return e?(t&&(e=A1(e,t)),z1[e.classes[0]]||null):null},ge=function(e,t){var a=["nulldelimiter"].concat(e.baseSizingClasses());return I0(t.concat(a))},G=function(e,t,a){if(!e)return I0();if(Le[e.type]){var n=Le[e.type](e,t);if(a&&t.size!==a.size){n=I0(t.sizingClasses(a),[n],t);var s=t.sizeMultiplier/a.sizeMultiplier;n.height*=s,n.depth*=s}return n}else throw new z("Got group of unknown type: '"+e.type+"'")};function De(r,e){var t=I0(["base"],r,e),a=I0(["strut"]);return a.style.height=T(t.height+t.depth),t.depth&&(a.style.verticalAlign=T(-t.depth)),t.children.unshift(a),t}function yt(r,e){var t=null;r.length===1&&r[0].type==="tag"&&(t=r[0].tag,r=r[0].body);var a=a0(r,e,"root"),n;a.length===2&&a[1].hasClass("tag")&&(n=a.pop());for(var s=[],l=[],h=0;h0&&(s.push(De(l,e)),l=[]),s.push(a[h]));l.length>0&&s.push(De(l,e));var f;t?(f=De(a0(t,e,!0)),f.classes=["tag"],s.push(f)):n&&s.push(n);var v=I0(["katex-html"],s);if(v.setAttribute("aria-hidden","true"),f){var b=f.children[0];b.style.height=T(v.height+v.depth),v.depth&&(b.style.verticalAlign=T(-v.depth))}return v}function $r(r){return new Y0(r)}var s0=class{constructor(e,t,a){this.type=void 0,this.attributes=void 0,this.children=void 0,this.classes=void 0,this.type=e,this.attributes={},this.children=t||[],this.classes=a||[]}setAttribute(e,t){this.attributes[e]=t}getAttribute(e){return this.attributes[e]}toNode(){var e=document.createElementNS("http://www.w3.org/1998/Math/MathML",this.type);for(var t in this.attributes)Object.prototype.hasOwnProperty.call(this.attributes,t)&&e.setAttribute(t,this.attributes[t]);this.classes.length>0&&(e.className=G0(this.classes));for(var a=0;a0&&(e+=' class ="'+O.escape(G0(this.classes))+'"'),e+=">";for(var a=0;a",e}toText(){return this.children.map(e=>e.toText()).join("")}},g0=class{constructor(e){this.text=void 0,this.text=e}toNode(){return document.createTextNode(this.text)}toMarkup(){return O.escape(this.toText())}toText(){return this.text}},xt=class{constructor(e){this.width=void 0,this.character=void 0,this.width=e,e>=.05555&&e<=.05556?this.character="\u200A":e>=.1666&&e<=.1667?this.character="\u2009":e>=.2222&&e<=.2223?this.character="\u2005":e>=.2777&&e<=.2778?this.character="\u2005\u200A":e>=-.05556&&e<=-.05555?this.character="\u200A\u2063":e>=-.1667&&e<=-.1666?this.character="\u2009\u2063":e>=-.2223&&e<=-.2222?this.character="\u205F\u2063":e>=-.2778&&e<=-.2777?this.character="\u2005\u2063":this.character=null}toNode(){if(this.character)return document.createTextNode(this.character);var e=document.createElementNS("http://www.w3.org/1998/Math/MathML","mspace");return e.setAttribute("width",T(this.width)),e}toMarkup(){return this.character?""+this.character+"":''}toText(){return this.character?this.character:" "}},M={MathNode:s0,TextNode:g0,SpaceNode:xt,newDocumentFragment:$r},y0=function(e,t,a){return Y[t][e]&&Y[t][e].replace&&e.charCodeAt(0)!==55349&&!(Cr.hasOwnProperty(e)&&a&&(a.fontFamily&&a.fontFamily.slice(4,6)==="tt"||a.font&&a.font.slice(4,6)==="tt"))&&(e=Y[t][e].replace),new M.TextNode(e)},Bt=function(e){return e.length===1?e[0]:new M.MathNode("mrow",e)},Dt=function(e,t){if(t.fontFamily==="texttt")return"monospace";if(t.fontFamily==="textsf")return t.fontShape==="textit"&&t.fontWeight==="textbf"?"sans-serif-bold-italic":t.fontShape==="textit"?"sans-serif-italic":t.fontWeight==="textbf"?"bold-sans-serif":"sans-serif";if(t.fontShape==="textit"&&t.fontWeight==="textbf")return"bold-italic";if(t.fontShape==="textit")return"italic";if(t.fontWeight==="textbf")return"bold";var a=t.font;if(!a||a==="mathnormal")return null;var n=e.mode;if(a==="mathit")return"italic";if(a==="boldsymbol")return e.type==="textord"?"bold":"bold-italic";if(a==="mathbf")return"bold";if(a==="mathbb")return"double-struck";if(a==="mathsfit")return"sans-serif-italic";if(a==="mathfrak")return"fraktur";if(a==="mathscr"||a==="mathcal")return"script";if(a==="mathsf")return"sans-serif";if(a==="mathtt")return"monospace";var s=e.text;if(O.contains(["\\imath","\\jmath"],s))return null;Y[n][s]&&Y[n][s].replace&&(s=Y[n][s].replace);var l=y.fontMap[a].fontName;return Tt(s,l,n)?y.fontMap[a].variant:null};function nt(r){if(!r)return!1;if(r.type==="mi"&&r.children.length===1){var e=r.children[0];return e instanceof g0&&e.text==="."}else if(r.type==="mo"&&r.children.length===1&&r.getAttribute("separator")==="true"&&r.getAttribute("lspace")==="0em"&&r.getAttribute("rspace")==="0em"){var t=r.children[0];return t instanceof g0&&t.text===","}else return!1}var m0=function(e,t,a){if(e.length===1){var n=X(e[0],t);return a&&n instanceof s0&&n.type==="mo"&&(n.setAttribute("lspace","0em"),n.setAttribute("rspace","0em")),[n]}for(var s=[],l,h=0;h=1&&(l.type==="mn"||nt(l))){var f=c.children[0];f instanceof s0&&f.type==="mn"&&(f.children=[...l.children,...f.children],s.pop())}else if(l.type==="mi"&&l.children.length===1){var v=l.children[0];if(v instanceof g0&&v.text==="\u0338"&&(c.type==="mo"||c.type==="mi"||c.type==="mn")){var b=c.children[0];b instanceof g0&&b.text.length>0&&(b.text=b.text.slice(0,1)+"\u0338"+b.text.slice(1),s.pop())}}}s.push(c),l=c}return s},V0=function(e,t,a){return Bt(m0(e,t,a))},X=function(e,t){if(!e)return new M.MathNode("mrow");if(Fe[e.type]){var a=Fe[e.type](e,t);return a}else throw new z("Got group of unknown type: '"+e.type+"'")};function ar(r,e,t,a,n){var s=m0(r,t),l;s.length===1&&s[0]instanceof s0&&O.contains(["mrow","mtable"],s[0].type)?l=s[0]:l=new M.MathNode("mrow",s);var h=new M.MathNode("annotation",[new M.TextNode(e)]);h.setAttribute("encoding","application/x-tex");var c=new M.MathNode("semantics",[l,h]),f=new M.MathNode("math",[c]);f.setAttribute("xmlns","http://www.w3.org/1998/Math/MathML"),a&&f.setAttribute("display","block");var v=n?"katex":"katex-mathml";return y.makeSpan([v],[f])}var Lr=function(e){return new Re({style:e.displayMode?E.DISPLAY:E.TEXT,maxSize:e.maxSize,minRuleThickness:e.minRuleThickness})},Fr=function(e,t){if(t.displayMode){var a=["katex-display"];t.leqno&&a.push("leqno"),t.fleqn&&a.push("fleqn"),e=y.makeSpan(a,[e])}return e},T1=function(e,t,a){var n=Lr(a),s;if(a.output==="mathml")return ar(e,t,n,a.displayMode,!0);if(a.output==="html"){var l=yt(e,n);s=y.makeSpan(["katex"],[l])}else{var h=ar(e,t,n,a.displayMode,!1),c=yt(e,n);s=y.makeSpan(["katex"],[h,c])}return Fr(s,a)},q1=function(e,t,a){var n=Lr(a),s=yt(e,n),l=y.makeSpan(["katex"],[s]);return Fr(l,a)},B1={widehat:"^",widecheck:"\u02C7",widetilde:"~",utilde:"~",overleftarrow:"\u2190",underleftarrow:"\u2190",xleftarrow:"\u2190",overrightarrow:"\u2192",underrightarrow:"\u2192",xrightarrow:"\u2192",underbrace:"\u23DF",overbrace:"\u23DE",overgroup:"\u23E0",undergroup:"\u23E1",overleftrightarrow:"\u2194",underleftrightarrow:"\u2194",xleftrightarrow:"\u2194",Overrightarrow:"\u21D2",xRightarrow:"\u21D2",overleftharpoon:"\u21BC",xleftharpoonup:"\u21BC",overrightharpoon:"\u21C0",xrightharpoonup:"\u21C0",xLeftarrow:"\u21D0",xLeftrightarrow:"\u21D4",xhookleftarrow:"\u21A9",xhookrightarrow:"\u21AA",xmapsto:"\u21A6",xrightharpoondown:"\u21C1",xleftharpoondown:"\u21BD",xrightleftharpoons:"\u21CC",xleftrightharpoons:"\u21CB",xtwoheadleftarrow:"\u219E",xtwoheadrightarrow:"\u21A0",xlongequal:"=",xtofrom:"\u21C4",xrightleftarrows:"\u21C4",xrightequilibrium:"\u21CC",xleftequilibrium:"\u21CB","\\cdrightarrow":"\u2192","\\cdleftarrow":"\u2190","\\cdlongequal":"="},D1=function(e){var t=new M.MathNode("mo",[new M.TextNode(B1[e.replace(/^\\/,"")])]);return t.setAttribute("stretchy","true"),t},C1={overrightarrow:[["rightarrow"],.888,522,"xMaxYMin"],overleftarrow:[["leftarrow"],.888,522,"xMinYMin"],underrightarrow:[["rightarrow"],.888,522,"xMaxYMin"],underleftarrow:[["leftarrow"],.888,522,"xMinYMin"],xrightarrow:[["rightarrow"],1.469,522,"xMaxYMin"],"\\cdrightarrow":[["rightarrow"],3,522,"xMaxYMin"],xleftarrow:[["leftarrow"],1.469,522,"xMinYMin"],"\\cdleftarrow":[["leftarrow"],3,522,"xMinYMin"],Overrightarrow:[["doublerightarrow"],.888,560,"xMaxYMin"],xRightarrow:[["doublerightarrow"],1.526,560,"xMaxYMin"],xLeftarrow:[["doubleleftarrow"],1.526,560,"xMinYMin"],overleftharpoon:[["leftharpoon"],.888,522,"xMinYMin"],xleftharpoonup:[["leftharpoon"],.888,522,"xMinYMin"],xleftharpoondown:[["leftharpoondown"],.888,522,"xMinYMin"],overrightharpoon:[["rightharpoon"],.888,522,"xMaxYMin"],xrightharpoonup:[["rightharpoon"],.888,522,"xMaxYMin"],xrightharpoondown:[["rightharpoondown"],.888,522,"xMaxYMin"],xlongequal:[["longequal"],.888,334,"xMinYMin"],"\\cdlongequal":[["longequal"],3,334,"xMinYMin"],xtwoheadleftarrow:[["twoheadleftarrow"],.888,334,"xMinYMin"],xtwoheadrightarrow:[["twoheadrightarrow"],.888,334,"xMaxYMin"],overleftrightarrow:[["leftarrow","rightarrow"],.888,522],overbrace:[["leftbrace","midbrace","rightbrace"],1.6,548],underbrace:[["leftbraceunder","midbraceunder","rightbraceunder"],1.6,548],underleftrightarrow:[["leftarrow","rightarrow"],.888,522],xleftrightarrow:[["leftarrow","rightarrow"],1.75,522],xLeftrightarrow:[["doubleleftarrow","doublerightarrow"],1.75,560],xrightleftharpoons:[["leftharpoondownplus","rightharpoonplus"],1.75,716],xleftrightharpoons:[["leftharpoonplus","rightharpoondownplus"],1.75,716],xhookleftarrow:[["leftarrow","righthook"],1.08,522],xhookrightarrow:[["lefthook","rightarrow"],1.08,522],overlinesegment:[["leftlinesegment","rightlinesegment"],.888,522],underlinesegment:[["leftlinesegment","rightlinesegment"],.888,522],overgroup:[["leftgroup","rightgroup"],.888,342],undergroup:[["leftgroupunder","rightgroupunder"],.888,342],xmapsto:[["leftmapsto","rightarrow"],1.5,522],xtofrom:[["leftToFrom","rightToFrom"],1.75,528],xrightleftarrows:[["baraboveleftarrow","rightarrowabovebar"],1.75,901],xrightequilibrium:[["baraboveshortleftharpoon","rightharpoonaboveshortbar"],1.75,716],xleftequilibrium:[["shortbaraboveleftharpoon","shortrightharpoonabovebar"],1.75,716]},_1=function(e){return e.type==="ordgroup"?e.body.length:1},N1=function(e,t){function a(){var h=4e5,c=e.label.slice(1);if(O.contains(["widehat","widecheck","widetilde","utilde"],c)){var f=e,v=_1(f.base),b,x,w;if(v>5)c==="widehat"||c==="widecheck"?(b=420,h=2364,w=.42,x=c+"4"):(b=312,h=2340,w=.34,x="tilde4");else{var A=[1,1,2,2,3,3][v];c==="widehat"||c==="widecheck"?(h=[0,1062,2364,2364,2364][A],b=[0,239,300,360,420][A],w=[0,.24,.3,.3,.36,.42][A],x=c+A):(h=[0,600,1033,2339,2340][A],b=[0,260,286,306,312][A],w=[0,.26,.286,.3,.306,.34][A],x="tilde"+A)}var q=new A0(x),_=new S0([q],{width:"100%",height:T(w),viewBox:"0 0 "+h+" "+b,preserveAspectRatio:"none"});return{span:y.makeSvgSpan([],[_],t),minWidth:0,height:w}}else{var D=[],N=C1[c],[$,H,F]=N,P=F/1e3,V=$.length,j,U;if(V===1){var D0=N[3];j=["hide-tail"],U=[D0]}else if(V===2)j=["halfarrow-left","halfarrow-right"],U=["xMinYMin","xMaxYMin"];else if(V===3)j=["brace-left","brace-center","brace-right"],U=["xMinYMin","xMidYMin","xMaxYMin"];else throw new Error(`Correct katexImagesData or update code here to support - `+V+" children.");for(var i0=0;i00&&(n.style.minWidth=T(s)),n},O1=function(e,t,a,n,s){var l,h=e.height+e.depth+a+n;if(/fbox|color|angl/.test(t)){if(l=y.makeSpan(["stretchy",t],[],s),t==="fbox"){var c=s.color&&s.getColor();c&&(l.style.borderColor=c)}}else{var f=[];/^[bx]cancel$/.test(t)&&f.push(new ve({x1:"0",y1:"0",x2:"100%",y2:"100%","stroke-width":"0.046em"})),/^x?cancel$/.test(t)&&f.push(new ve({x1:"0",y1:"100%",x2:"100%",y2:"0","stroke-width":"0.046em"}));var v=new S0(f,{width:"100%",height:T(h)});l=y.makeSvgSpan([],[v],s)}return l.height=h,l.style.height=T(h),l},E0={encloseSpan:O1,mathMLnode:D1,svgSpan:N1};function L(r,e){if(!r||r.type!==e)throw new Error("Expected node of type "+e+", but got "+(r?"node of type "+r.type:String(r)));return r}function Ct(r){var e=Xe(r);if(!e)throw new Error("Expected node of symbol group type, but got "+(r?"node of type "+r.type:String(r)));return e}function Xe(r){return r&&(r.type==="atom"||s1.hasOwnProperty(r.type))?r:null}var _t=(r,e)=>{var t,a,n;r&&r.type==="supsub"?(a=L(r.base,"accent"),t=a.base,r.base=t,n=n1(G(r,e)),r.base=a):(a=L(r,"accent"),t=a.base);var s=G(t,e.havingCrampedStyle()),l=a.isShifty&&O.isCharacterBox(t),h=0;if(l){var c=O.getBaseElem(t),f=G(c,e.havingCrampedStyle());h=Jt(f).skew}var v=a.label==="\\c",b=v?s.height+s.depth:Math.min(s.height,e.fontMetrics().xHeight),x;if(a.isStretchy)x=E0.svgSpan(a,e),x=y.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:s},{type:"elem",elem:x,wrapperClasses:["svg-align"],wrapperStyle:h>0?{width:"calc(100% - "+T(2*h)+")",marginLeft:T(2*h)}:void 0}]},e);else{var w,A;a.label==="\\vec"?(w=y.staticSvg("vec",e),A=y.svgData.vec[1]):(w=y.makeOrd({mode:a.mode,text:a.label},e,"textord"),w=Jt(w),w.italic=0,A=w.width,v&&(b+=w.depth)),x=y.makeSpan(["accent-body"],[w]);var q=a.label==="\\textcircled";q&&(x.classes.push("accent-full"),b=s.height);var _=h;q||(_-=A/2),x.style.left=T(_),a.label==="\\textcircled"&&(x.style.top=".2em"),x=y.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:s},{type:"kern",size:-b},{type:"elem",elem:x}]},e)}var D=y.makeSpan(["mord","accent"],[x],e);return n?(n.children[0]=D,n.height=Math.max(D.height,n.height),n.classes[0]="mord",n):D},Hr=(r,e)=>{var t=r.isStretchy?E0.mathMLnode(r.label):new M.MathNode("mo",[y0(r.label,r.mode)]),a=new M.MathNode("mover",[X(r.base,e),t]);return a.setAttribute("accent","true"),a},I1=new RegExp(["\\acute","\\grave","\\ddot","\\tilde","\\bar","\\breve","\\check","\\hat","\\vec","\\dot","\\mathring"].map(r=>"\\"+r).join("|"));B({type:"accent",names:["\\acute","\\grave","\\ddot","\\tilde","\\bar","\\breve","\\check","\\hat","\\vec","\\dot","\\mathring","\\widecheck","\\widehat","\\widetilde","\\overrightarrow","\\overleftarrow","\\Overrightarrow","\\overleftrightarrow","\\overgroup","\\overlinesegment","\\overleftharpoon","\\overrightharpoon"],props:{numArgs:1},handler:(r,e)=>{var t=He(e[0]),a=!I1.test(r.funcName),n=!a||r.funcName==="\\widehat"||r.funcName==="\\widetilde"||r.funcName==="\\widecheck";return{type:"accent",mode:r.parser.mode,label:r.funcName,isStretchy:a,isShifty:n,base:t}},htmlBuilder:_t,mathmlBuilder:Hr});B({type:"accent",names:["\\'","\\`","\\^","\\~","\\=","\\u","\\.",'\\"',"\\c","\\r","\\H","\\v","\\textcircled"],props:{numArgs:1,allowedInText:!0,allowedInMath:!0,argTypes:["primitive"]},handler:(r,e)=>{var t=e[0],a=r.parser.mode;return a==="math"&&(r.parser.settings.reportNonstrict("mathVsTextAccents","LaTeX's accent "+r.funcName+" works only in text mode"),a="text"),{type:"accent",mode:a,label:r.funcName,isStretchy:!1,isShifty:!0,base:t}},htmlBuilder:_t,mathmlBuilder:Hr});B({type:"accentUnder",names:["\\underleftarrow","\\underrightarrow","\\underleftrightarrow","\\undergroup","\\underlinesegment","\\utilde"],props:{numArgs:1},handler:(r,e)=>{var{parser:t,funcName:a}=r,n=e[0];return{type:"accentUnder",mode:t.mode,label:a,base:n}},htmlBuilder:(r,e)=>{var t=G(r.base,e),a=E0.svgSpan(r,e),n=r.label==="\\utilde"?.12:0,s=y.makeVList({positionType:"top",positionData:t.height,children:[{type:"elem",elem:a,wrapperClasses:["svg-align"]},{type:"kern",size:n},{type:"elem",elem:t}]},e);return y.makeSpan(["mord","accentunder"],[s],e)},mathmlBuilder:(r,e)=>{var t=E0.mathMLnode(r.label),a=new M.MathNode("munder",[X(r.base,e),t]);return a.setAttribute("accentunder","true"),a}});var Ce=r=>{var e=new M.MathNode("mpadded",r?[r]:[]);return e.setAttribute("width","+0.6em"),e.setAttribute("lspace","0.3em"),e};B({type:"xArrow",names:["\\xleftarrow","\\xrightarrow","\\xLeftarrow","\\xRightarrow","\\xleftrightarrow","\\xLeftrightarrow","\\xhookleftarrow","\\xhookrightarrow","\\xmapsto","\\xrightharpoondown","\\xrightharpoonup","\\xleftharpoondown","\\xleftharpoonup","\\xrightleftharpoons","\\xleftrightharpoons","\\xlongequal","\\xtwoheadrightarrow","\\xtwoheadleftarrow","\\xtofrom","\\xrightleftarrows","\\xrightequilibrium","\\xleftequilibrium","\\\\cdrightarrow","\\\\cdleftarrow","\\\\cdlongequal"],props:{numArgs:1,numOptionalArgs:1},handler(r,e,t){var{parser:a,funcName:n}=r;return{type:"xArrow",mode:a.mode,label:n,body:e[0],below:t[0]}},htmlBuilder(r,e){var t=e.style,a=e.havingStyle(t.sup()),n=y.wrapFragment(G(r.body,a,e),e),s=r.label.slice(0,2)==="\\x"?"x":"cd";n.classes.push(s+"-arrow-pad");var l;r.below&&(a=e.havingStyle(t.sub()),l=y.wrapFragment(G(r.below,a,e),e),l.classes.push(s+"-arrow-pad"));var h=E0.svgSpan(r,e),c=-e.fontMetrics().axisHeight+.5*h.height,f=-e.fontMetrics().axisHeight-.5*h.height-.111;(n.depth>.25||r.label==="\\xleftequilibrium")&&(f-=n.depth);var v;if(l){var b=-e.fontMetrics().axisHeight+l.height+.5*h.height+.111;v=y.makeVList({positionType:"individualShift",children:[{type:"elem",elem:n,shift:f},{type:"elem",elem:h,shift:c},{type:"elem",elem:l,shift:b}]},e)}else v=y.makeVList({positionType:"individualShift",children:[{type:"elem",elem:n,shift:f},{type:"elem",elem:h,shift:c}]},e);return v.children[0].children[0].children[1].classes.push("svg-align"),y.makeSpan(["mrel","x-arrow"],[v],e)},mathmlBuilder(r,e){var t=E0.mathMLnode(r.label);t.setAttribute("minsize",r.label.charAt(0)==="x"?"1.75em":"3.0em");var a;if(r.body){var n=Ce(X(r.body,e));if(r.below){var s=Ce(X(r.below,e));a=new M.MathNode("munderover",[t,s,n])}else a=new M.MathNode("mover",[t,n])}else if(r.below){var l=Ce(X(r.below,e));a=new M.MathNode("munder",[t,l])}else a=Ce(),a=new M.MathNode("mover",[t,a]);return a}});var E1=y.makeSpan;function Pr(r,e){var t=a0(r.body,e,!0);return E1([r.mclass],t,e)}function Gr(r,e){var t,a=m0(r.body,e);return r.mclass==="minner"?t=new M.MathNode("mpadded",a):r.mclass==="mord"?r.isCharacterBox?(t=a[0],t.type="mi"):t=new M.MathNode("mi",a):(r.isCharacterBox?(t=a[0],t.type="mo"):t=new M.MathNode("mo",a),r.mclass==="mbin"?(t.attributes.lspace="0.22em",t.attributes.rspace="0.22em"):r.mclass==="mpunct"?(t.attributes.lspace="0em",t.attributes.rspace="0.17em"):r.mclass==="mopen"||r.mclass==="mclose"?(t.attributes.lspace="0em",t.attributes.rspace="0em"):r.mclass==="minner"&&(t.attributes.lspace="0.0556em",t.attributes.width="+0.1111em")),t}B({type:"mclass",names:["\\mathord","\\mathbin","\\mathrel","\\mathopen","\\mathclose","\\mathpunct","\\mathinner"],props:{numArgs:1,primitive:!0},handler(r,e){var{parser:t,funcName:a}=r,n=e[0];return{type:"mclass",mode:t.mode,mclass:"m"+a.slice(5),body:e0(n),isCharacterBox:O.isCharacterBox(n)}},htmlBuilder:Pr,mathmlBuilder:Gr});var We=r=>{var e=r.type==="ordgroup"&&r.body.length?r.body[0]:r;return e.type==="atom"&&(e.family==="bin"||e.family==="rel")?"m"+e.family:"mord"};B({type:"mclass",names:["\\@binrel"],props:{numArgs:2},handler(r,e){var{parser:t}=r;return{type:"mclass",mode:t.mode,mclass:We(e[0]),body:e0(e[1]),isCharacterBox:O.isCharacterBox(e[1])}}});B({type:"mclass",names:["\\stackrel","\\overset","\\underset"],props:{numArgs:2},handler(r,e){var{parser:t,funcName:a}=r,n=e[1],s=e[0],l;a!=="\\stackrel"?l=We(n):l="mrel";var h={type:"op",mode:n.mode,limits:!0,alwaysHandleSupSub:!0,parentIsSupSub:!1,symbol:!1,suppressBaseShift:a!=="\\stackrel",body:e0(n)},c={type:"supsub",mode:s.mode,base:h,sup:a==="\\underset"?null:s,sub:a==="\\underset"?s:null};return{type:"mclass",mode:t.mode,mclass:l,body:[c],isCharacterBox:O.isCharacterBox(c)}},htmlBuilder:Pr,mathmlBuilder:Gr});B({type:"pmb",names:["\\pmb"],props:{numArgs:1,allowedInText:!0},handler(r,e){var{parser:t}=r;return{type:"pmb",mode:t.mode,mclass:We(e[0]),body:e0(e[0])}},htmlBuilder(r,e){var t=a0(r.body,e,!0),a=y.makeSpan([r.mclass],t,e);return a.style.textShadow="0.02em 0.01em 0.04px",a},mathmlBuilder(r,e){var t=m0(r.body,e),a=new M.MathNode("mstyle",t);return a.setAttribute("style","text-shadow: 0.02em 0.01em 0.04px"),a}});var R1={">":"\\\\cdrightarrow","<":"\\\\cdleftarrow","=":"\\\\cdlongequal",A:"\\uparrow",V:"\\downarrow","|":"\\Vert",".":"no arrow"},nr=()=>({type:"styling",body:[],mode:"math",style:"display"}),ir=r=>r.type==="textord"&&r.text==="@",$1=(r,e)=>(r.type==="mathord"||r.type==="atom")&&r.text===e;function L1(r,e,t){var a=R1[r];switch(a){case"\\\\cdrightarrow":case"\\\\cdleftarrow":return t.callFunction(a,[e[0]],[e[1]]);case"\\uparrow":case"\\downarrow":{var n=t.callFunction("\\\\cdleft",[e[0]],[]),s={type:"atom",text:a,mode:"math",family:"rel"},l=t.callFunction("\\Big",[s],[]),h=t.callFunction("\\\\cdright",[e[1]],[]),c={type:"ordgroup",mode:"math",body:[n,l,h]};return t.callFunction("\\\\cdparent",[c],[])}case"\\\\cdlongequal":return t.callFunction("\\\\cdlongequal",[],[]);case"\\Vert":{var f={type:"textord",text:"\\Vert",mode:"math"};return t.callFunction("\\Big",[f],[])}default:return{type:"textord",text:" ",mode:"math"}}}function F1(r){var e=[];for(r.gullet.beginGroup(),r.gullet.macros.set("\\cr","\\\\\\relax"),r.gullet.beginGroup();;){e.push(r.parseExpression(!1,"\\\\")),r.gullet.endGroup(),r.gullet.beginGroup();var t=r.fetch().text;if(t==="&"||t==="\\\\")r.consume();else if(t==="\\end"){e[e.length-1].length===0&&e.pop();break}else throw new z("Expected \\\\ or \\cr or \\end",r.nextToken)}for(var a=[],n=[a],s=0;s-1))if("<>AV".indexOf(f)>-1)for(var b=0;b<2;b++){for(var x=!0,w=c+1;wAV=|." after @',l[c]);var A=L1(f,v,r),q={type:"styling",body:[A],mode:"math",style:"display"};a.push(q),h=nr()}s%2===0?a.push(h):a.shift(),a=[],n.push(a)}r.gullet.endGroup(),r.gullet.endGroup();var _=new Array(n[0].length).fill({type:"align",align:"c",pregap:.25,postgap:.25});return{type:"array",mode:"math",body:n,arraystretch:1,addJot:!0,rowGaps:[null],cols:_,colSeparationType:"CD",hLinesBeforeRow:new Array(n.length+1).fill([])}}B({type:"cdlabel",names:["\\\\cdleft","\\\\cdright"],props:{numArgs:1},handler(r,e){var{parser:t,funcName:a}=r;return{type:"cdlabel",mode:t.mode,side:a.slice(4),label:e[0]}},htmlBuilder(r,e){var t=e.havingStyle(e.style.sup()),a=y.wrapFragment(G(r.label,t,e),e);return a.classes.push("cd-label-"+r.side),a.style.bottom=T(.8-a.depth),a.height=0,a.depth=0,a},mathmlBuilder(r,e){var t=new M.MathNode("mrow",[X(r.label,e)]);return t=new M.MathNode("mpadded",[t]),t.setAttribute("width","0"),r.side==="left"&&t.setAttribute("lspace","-1width"),t.setAttribute("voffset","0.7em"),t=new M.MathNode("mstyle",[t]),t.setAttribute("displaystyle","false"),t.setAttribute("scriptlevel","1"),t}});B({type:"cdlabelparent",names:["\\\\cdparent"],props:{numArgs:1},handler(r,e){var{parser:t}=r;return{type:"cdlabelparent",mode:t.mode,fragment:e[0]}},htmlBuilder(r,e){var t=y.wrapFragment(G(r.fragment,e),e);return t.classes.push("cd-vert-arrow"),t},mathmlBuilder(r,e){return new M.MathNode("mrow",[X(r.fragment,e)])}});B({type:"textord",names:["\\@char"],props:{numArgs:1,allowedInText:!0},handler(r,e){for(var{parser:t}=r,a=L(e[0],"ordgroup"),n=a.body,s="",l=0;l=1114111)throw new z("\\@char with invalid code point "+s);return c<=65535?f=String.fromCharCode(c):(c-=65536,f=String.fromCharCode((c>>10)+55296,(c&1023)+56320)),{type:"textord",mode:t.mode,text:f}}});var Vr=(r,e)=>{var t=a0(r.body,e.withColor(r.color),!1);return y.makeFragment(t)},Ur=(r,e)=>{var t=m0(r.body,e.withColor(r.color)),a=new M.MathNode("mstyle",t);return a.setAttribute("mathcolor",r.color),a};B({type:"color",names:["\\textcolor"],props:{numArgs:2,allowedInText:!0,argTypes:["color","original"]},handler(r,e){var{parser:t}=r,a=L(e[0],"color-token").color,n=e[1];return{type:"color",mode:t.mode,color:a,body:e0(n)}},htmlBuilder:Vr,mathmlBuilder:Ur});B({type:"color",names:["\\color"],props:{numArgs:1,allowedInText:!0,argTypes:["color"]},handler(r,e){var{parser:t,breakOnTokenText:a}=r,n=L(e[0],"color-token").color;t.gullet.macros.set("\\current@color",n);var s=t.parseExpression(!0,a);return{type:"color",mode:t.mode,color:n,body:s}},htmlBuilder:Vr,mathmlBuilder:Ur});B({type:"cr",names:["\\\\"],props:{numArgs:0,numOptionalArgs:0,allowedInText:!0},handler(r,e,t){var{parser:a}=r,n=a.gullet.future().text==="["?a.parseSizeGroup(!0):null,s=!a.settings.displayMode||!a.settings.useStrictBehavior("newLineInDisplayMode","In LaTeX, \\\\ or \\newline does nothing in display mode");return{type:"cr",mode:a.mode,newLine:s,size:n&&L(n,"size").value}},htmlBuilder(r,e){var t=y.makeSpan(["mspace"],[],e);return r.newLine&&(t.classes.push("newline"),r.size&&(t.style.marginTop=T(Q(r.size,e)))),t},mathmlBuilder(r,e){var t=new M.MathNode("mspace");return r.newLine&&(t.setAttribute("linebreak","newline"),r.size&&t.setAttribute("height",T(Q(r.size,e)))),t}});var wt={"\\global":"\\global","\\long":"\\\\globallong","\\\\globallong":"\\\\globallong","\\def":"\\gdef","\\gdef":"\\gdef","\\edef":"\\xdef","\\xdef":"\\xdef","\\let":"\\\\globallet","\\futurelet":"\\\\globalfuture"},Xr=r=>{var e=r.text;if(/^(?:[\\{}$&#^_]|EOF)$/.test(e))throw new z("Expected a control sequence",r);return e},H1=r=>{var e=r.gullet.popToken();return e.text==="="&&(e=r.gullet.popToken(),e.text===" "&&(e=r.gullet.popToken())),e},Wr=(r,e,t,a)=>{var n=r.gullet.macros.get(t.text);n==null&&(t.noexpand=!0,n={tokens:[t],numArgs:0,unexpandable:!r.gullet.isExpandable(t.text)}),r.gullet.macros.set(e,n,a)};B({type:"internal",names:["\\global","\\long","\\\\globallong"],props:{numArgs:0,allowedInText:!0},handler(r){var{parser:e,funcName:t}=r;e.consumeSpaces();var a=e.fetch();if(wt[a.text])return(t==="\\global"||t==="\\\\globallong")&&(a.text=wt[a.text]),L(e.parseFunction(),"internal");throw new z("Invalid token after macro prefix",a)}});B({type:"internal",names:["\\def","\\gdef","\\edef","\\xdef"],props:{numArgs:0,allowedInText:!0,primitive:!0},handler(r){var{parser:e,funcName:t}=r,a=e.gullet.popToken(),n=a.text;if(/^(?:[\\{}$&#^_]|EOF)$/.test(n))throw new z("Expected a control sequence",a);for(var s=0,l,h=[[]];e.gullet.future().text!=="{";)if(a=e.gullet.popToken(),a.text==="#"){if(e.gullet.future().text==="{"){l=e.gullet.future(),h[s].push("{");break}if(a=e.gullet.popToken(),!/^[1-9]$/.test(a.text))throw new z('Invalid argument number "'+a.text+'"');if(parseInt(a.text)!==s+1)throw new z('Argument number "'+a.text+'" out of order');s++,h.push([])}else{if(a.text==="EOF")throw new z("Expected a macro definition");h[s].push(a.text)}var{tokens:c}=e.gullet.consumeArg();return l&&c.unshift(l),(t==="\\edef"||t==="\\xdef")&&(c=e.gullet.expandTokens(c),c.reverse()),e.gullet.macros.set(n,{tokens:c,numArgs:s,delimiters:h},t===wt[t]),{type:"internal",mode:e.mode}}});B({type:"internal",names:["\\let","\\\\globallet"],props:{numArgs:0,allowedInText:!0,primitive:!0},handler(r){var{parser:e,funcName:t}=r,a=Xr(e.gullet.popToken());e.gullet.consumeSpaces();var n=H1(e);return Wr(e,a,n,t==="\\\\globallet"),{type:"internal",mode:e.mode}}});B({type:"internal",names:["\\futurelet","\\\\globalfuture"],props:{numArgs:0,allowedInText:!0,primitive:!0},handler(r){var{parser:e,funcName:t}=r,a=Xr(e.gullet.popToken()),n=e.gullet.popToken(),s=e.gullet.popToken();return Wr(e,a,s,t==="\\\\globalfuture"),e.gullet.pushToken(s),e.gullet.pushToken(n),{type:"internal",mode:e.mode}}});var ce=function(e,t,a){var n=Y.math[e]&&Y.math[e].replace,s=Tt(n||e,t,a);if(!s)throw new Error("Unsupported symbol "+e+" and font size "+t+".");return s},Nt=function(e,t,a,n){var s=a.havingBaseStyle(t),l=y.makeSpan(n.concat(s.sizingClasses(a)),[e],a),h=s.sizeMultiplier/a.sizeMultiplier;return l.height*=h,l.depth*=h,l.maxFontSize=s.sizeMultiplier,l},Yr=function(e,t,a){var n=t.havingBaseStyle(a),s=(1-t.sizeMultiplier/n.sizeMultiplier)*t.fontMetrics().axisHeight;e.classes.push("delimcenter"),e.style.top=T(s),e.height-=s,e.depth+=s},P1=function(e,t,a,n,s,l){var h=y.makeSymbol(e,"Main-Regular",s,n),c=Nt(h,t,n,l);return a&&Yr(c,n,t),c},G1=function(e,t,a,n){return y.makeSymbol(e,"Size"+t+"-Regular",a,n)},Zr=function(e,t,a,n,s,l){var h=G1(e,t,s,n),c=Nt(y.makeSpan(["delimsizing","size"+t],[h],n),E.TEXT,n,l);return a&&Yr(c,n,E.TEXT),c},it=function(e,t,a){var n;t==="Size1-Regular"?n="delim-size1":n="delim-size4";var s=y.makeSpan(["delimsizinginner",n],[y.makeSpan([],[y.makeSymbol(e,t,a)])]);return{type:"elem",elem:s}},st=function(e,t,a){var n=z0["Size4-Regular"][e.charCodeAt(0)]?z0["Size4-Regular"][e.charCodeAt(0)][4]:z0["Size1-Regular"][e.charCodeAt(0)][4],s=new A0("inner",ja(e,Math.round(1e3*t))),l=new S0([s],{width:T(n),height:T(t),style:"width:"+T(n),viewBox:"0 0 "+1e3*n+" "+Math.round(1e3*t),preserveAspectRatio:"xMinYMin"}),h=y.makeSvgSpan([],[l],a);return h.height=t,h.style.height=T(t),h.style.width=T(n),{type:"elem",elem:h}},St=.008,_e={type:"kern",size:-1*St},V1=["|","\\lvert","\\rvert","\\vert"],U1=["\\|","\\lVert","\\rVert","\\Vert"],jr=function(e,t,a,n,s,l){var h,c,f,v,b="",x=0;h=f=v=e,c=null;var w="Size1-Regular";e==="\\uparrow"?f=v="\u23D0":e==="\\Uparrow"?f=v="\u2016":e==="\\downarrow"?h=f="\u23D0":e==="\\Downarrow"?h=f="\u2016":e==="\\updownarrow"?(h="\\uparrow",f="\u23D0",v="\\downarrow"):e==="\\Updownarrow"?(h="\\Uparrow",f="\u2016",v="\\Downarrow"):O.contains(V1,e)?(f="\u2223",b="vert",x=333):O.contains(U1,e)?(f="\u2225",b="doublevert",x=556):e==="["||e==="\\lbrack"?(h="\u23A1",f="\u23A2",v="\u23A3",w="Size4-Regular",b="lbrack",x=667):e==="]"||e==="\\rbrack"?(h="\u23A4",f="\u23A5",v="\u23A6",w="Size4-Regular",b="rbrack",x=667):e==="\\lfloor"||e==="\u230A"?(f=h="\u23A2",v="\u23A3",w="Size4-Regular",b="lfloor",x=667):e==="\\lceil"||e==="\u2308"?(h="\u23A1",f=v="\u23A2",w="Size4-Regular",b="lceil",x=667):e==="\\rfloor"||e==="\u230B"?(f=h="\u23A5",v="\u23A6",w="Size4-Regular",b="rfloor",x=667):e==="\\rceil"||e==="\u2309"?(h="\u23A4",f=v="\u23A5",w="Size4-Regular",b="rceil",x=667):e==="("||e==="\\lparen"?(h="\u239B",f="\u239C",v="\u239D",w="Size4-Regular",b="lparen",x=875):e===")"||e==="\\rparen"?(h="\u239E",f="\u239F",v="\u23A0",w="Size4-Regular",b="rparen",x=875):e==="\\{"||e==="\\lbrace"?(h="\u23A7",c="\u23A8",v="\u23A9",f="\u23AA",w="Size4-Regular"):e==="\\}"||e==="\\rbrace"?(h="\u23AB",c="\u23AC",v="\u23AD",f="\u23AA",w="Size4-Regular"):e==="\\lgroup"||e==="\u27EE"?(h="\u23A7",v="\u23A9",f="\u23AA",w="Size4-Regular"):e==="\\rgroup"||e==="\u27EF"?(h="\u23AB",v="\u23AD",f="\u23AA",w="Size4-Regular"):e==="\\lmoustache"||e==="\u23B0"?(h="\u23A7",v="\u23AD",f="\u23AA",w="Size4-Regular"):(e==="\\rmoustache"||e==="\u23B1")&&(h="\u23AB",v="\u23A9",f="\u23AA",w="Size4-Regular");var A=ce(h,w,s),q=A.height+A.depth,_=ce(f,w,s),D=_.height+_.depth,N=ce(v,w,s),$=N.height+N.depth,H=0,F=1;if(c!==null){var P=ce(c,w,s);H=P.height+P.depth,F=2}var V=q+$+H,j=Math.max(0,Math.ceil((t-V)/(F*D))),U=V+j*F*D,D0=n.fontMetrics().axisHeight;a&&(D0*=n.sizeMultiplier);var i0=U/2-D0,r0=[];if(b.length>0){var X0=U-q-$,u0=Math.round(U*1e3),x0=Ka(b,Math.round(X0*1e3)),$0=new A0(b,x0),K0=(x/1e3).toFixed(3)+"em",J0=(u0/1e3).toFixed(3)+"em",je=new S0([$0],{width:K0,height:J0,viewBox:"0 0 "+x+" "+u0}),L0=y.makeSvgSpan([],[je],n);L0.height=u0/1e3,L0.style.width=K0,L0.style.height=J0,r0.push({type:"elem",elem:L0})}else{if(r0.push(it(v,w,s)),r0.push(_e),c===null){var F0=U-q-$+2*St;r0.push(st(f,F0,n))}else{var f0=(U-q-$-H)/2+2*St;r0.push(st(f,f0,n)),r0.push(_e),r0.push(it(c,w,s)),r0.push(_e),r0.push(st(f,f0,n))}r0.push(_e),r0.push(it(h,w,s))}var le=n.havingBaseStyle(E.TEXT),Ke=y.makeVList({positionType:"bottom",positionData:i0,children:r0},le);return Nt(y.makeSpan(["delimsizing","mult"],[Ke],le),E.TEXT,n,l)},ot=80,lt=.08,ut=function(e,t,a,n,s){var l=Za(e,n,a),h=new A0(e,l),c=new S0([h],{width:"400em",height:T(t),viewBox:"0 0 400000 "+a,preserveAspectRatio:"xMinYMin slice"});return y.makeSvgSpan(["hide-tail"],[c],s)},X1=function(e,t){var a=t.havingBaseSizing(),n=ea("\\surd",e*a.sizeMultiplier,Qr,a),s=a.sizeMultiplier,l=Math.max(0,t.minRuleThickness-t.fontMetrics().sqrtRuleThickness),h,c=0,f=0,v=0,b;return n.type==="small"?(v=1e3+1e3*l+ot,e<1?s=1:e<1.4&&(s=.7),c=(1+l+lt)/s,f=(1+l)/s,h=ut("sqrtMain",c,v,l,t),h.style.minWidth="0.853em",b=.833/s):n.type==="large"?(v=(1e3+ot)*me[n.size],f=(me[n.size]+l)/s,c=(me[n.size]+l+lt)/s,h=ut("sqrtSize"+n.size,c,v,l,t),h.style.minWidth="1.02em",b=1/s):(c=e+l+lt,f=e+l,v=Math.floor(1e3*e+l)+ot,h=ut("sqrtTall",c,v,l,t),h.style.minWidth="0.742em",b=1.056),h.height=f,h.style.height=T(c),{span:h,advanceWidth:b,ruleWidth:(t.fontMetrics().sqrtRuleThickness+l)*s}},Kr=["(","\\lparen",")","\\rparen","[","\\lbrack","]","\\rbrack","\\{","\\lbrace","\\}","\\rbrace","\\lfloor","\\rfloor","\u230A","\u230B","\\lceil","\\rceil","\u2308","\u2309","\\surd"],W1=["\\uparrow","\\downarrow","\\updownarrow","\\Uparrow","\\Downarrow","\\Updownarrow","|","\\|","\\vert","\\Vert","\\lvert","\\rvert","\\lVert","\\rVert","\\lgroup","\\rgroup","\u27EE","\u27EF","\\lmoustache","\\rmoustache","\u23B0","\u23B1"],Jr=["<",">","\\langle","\\rangle","/","\\backslash","\\lt","\\gt"],me=[0,1.2,1.8,2.4,3],Y1=function(e,t,a,n,s){if(e==="<"||e==="\\lt"||e==="\u27E8"?e="\\langle":(e===">"||e==="\\gt"||e==="\u27E9")&&(e="\\rangle"),O.contains(Kr,e)||O.contains(Jr,e))return Zr(e,t,!1,a,n,s);if(O.contains(W1,e))return jr(e,me[t],!1,a,n,s);throw new z("Illegal delimiter: '"+e+"'")},Z1=[{type:"small",style:E.SCRIPTSCRIPT},{type:"small",style:E.SCRIPT},{type:"small",style:E.TEXT},{type:"large",size:1},{type:"large",size:2},{type:"large",size:3},{type:"large",size:4}],j1=[{type:"small",style:E.SCRIPTSCRIPT},{type:"small",style:E.SCRIPT},{type:"small",style:E.TEXT},{type:"stack"}],Qr=[{type:"small",style:E.SCRIPTSCRIPT},{type:"small",style:E.SCRIPT},{type:"small",style:E.TEXT},{type:"large",size:1},{type:"large",size:2},{type:"large",size:3},{type:"large",size:4},{type:"stack"}],K1=function(e){if(e.type==="small")return"Main-Regular";if(e.type==="large")return"Size"+e.size+"-Regular";if(e.type==="stack")return"Size4-Regular";throw new Error("Add support for delim type '"+e.type+"' here.")},ea=function(e,t,a,n){for(var s=Math.min(2,3-n.style.size),l=s;lt)return a[l]}return a[a.length-1]},ta=function(e,t,a,n,s,l){e==="<"||e==="\\lt"||e==="\u27E8"?e="\\langle":(e===">"||e==="\\gt"||e==="\u27E9")&&(e="\\rangle");var h;O.contains(Jr,e)?h=Z1:O.contains(Kr,e)?h=Qr:h=j1;var c=ea(e,t,h,n);return c.type==="small"?P1(e,c.style,a,n,s,l):c.type==="large"?Zr(e,c.size,a,n,s,l):jr(e,t,a,n,s,l)},J1=function(e,t,a,n,s,l){var h=n.fontMetrics().axisHeight*n.sizeMultiplier,c=901,f=5/n.fontMetrics().ptPerEm,v=Math.max(t-h,a+h),b=Math.max(v/500*c,2*v-f);return ta(e,b,!0,n,s,l)},O0={sqrtImage:X1,sizedDelim:Y1,sizeToMaxHeight:me,customSizedDelim:ta,leftRightDelim:J1},sr={"\\bigl":{mclass:"mopen",size:1},"\\Bigl":{mclass:"mopen",size:2},"\\biggl":{mclass:"mopen",size:3},"\\Biggl":{mclass:"mopen",size:4},"\\bigr":{mclass:"mclose",size:1},"\\Bigr":{mclass:"mclose",size:2},"\\biggr":{mclass:"mclose",size:3},"\\Biggr":{mclass:"mclose",size:4},"\\bigm":{mclass:"mrel",size:1},"\\Bigm":{mclass:"mrel",size:2},"\\biggm":{mclass:"mrel",size:3},"\\Biggm":{mclass:"mrel",size:4},"\\big":{mclass:"mord",size:1},"\\Big":{mclass:"mord",size:2},"\\bigg":{mclass:"mord",size:3},"\\Bigg":{mclass:"mord",size:4}},Q1=["(","\\lparen",")","\\rparen","[","\\lbrack","]","\\rbrack","\\{","\\lbrace","\\}","\\rbrace","\\lfloor","\\rfloor","\u230A","\u230B","\\lceil","\\rceil","\u2308","\u2309","<",">","\\langle","\u27E8","\\rangle","\u27E9","\\lt","\\gt","\\lvert","\\rvert","\\lVert","\\rVert","\\lgroup","\\rgroup","\u27EE","\u27EF","\\lmoustache","\\rmoustache","\u23B0","\u23B1","/","\\backslash","|","\\vert","\\|","\\Vert","\\uparrow","\\Uparrow","\\downarrow","\\Downarrow","\\updownarrow","\\Updownarrow","."];function Ye(r,e){var t=Xe(r);if(t&&O.contains(Q1,t.text))return t;throw t?new z("Invalid delimiter '"+t.text+"' after '"+e.funcName+"'",r):new z("Invalid delimiter type '"+r.type+"'",r)}B({type:"delimsizing",names:["\\bigl","\\Bigl","\\biggl","\\Biggl","\\bigr","\\Bigr","\\biggr","\\Biggr","\\bigm","\\Bigm","\\biggm","\\Biggm","\\big","\\Big","\\bigg","\\Bigg"],props:{numArgs:1,argTypes:["primitive"]},handler:(r,e)=>{var t=Ye(e[0],r);return{type:"delimsizing",mode:r.parser.mode,size:sr[r.funcName].size,mclass:sr[r.funcName].mclass,delim:t.text}},htmlBuilder:(r,e)=>r.delim==="."?y.makeSpan([r.mclass]):O0.sizedDelim(r.delim,r.size,e,r.mode,[r.mclass]),mathmlBuilder:r=>{var e=[];r.delim!=="."&&e.push(y0(r.delim,r.mode));var t=new M.MathNode("mo",e);r.mclass==="mopen"||r.mclass==="mclose"?t.setAttribute("fence","true"):t.setAttribute("fence","false"),t.setAttribute("stretchy","true");var a=T(O0.sizeToMaxHeight[r.size]);return t.setAttribute("minsize",a),t.setAttribute("maxsize",a),t}});function or(r){if(!r.body)throw new Error("Bug: The leftright ParseNode wasn't fully parsed.")}B({type:"leftright-right",names:["\\right"],props:{numArgs:1,primitive:!0},handler:(r,e)=>{var t=r.parser.gullet.macros.get("\\current@color");if(t&&typeof t!="string")throw new z("\\current@color set to non-string in \\right");return{type:"leftright-right",mode:r.parser.mode,delim:Ye(e[0],r).text,color:t}}});B({type:"leftright",names:["\\left"],props:{numArgs:1,primitive:!0},handler:(r,e)=>{var t=Ye(e[0],r),a=r.parser;++a.leftrightDepth;var n=a.parseExpression(!1);--a.leftrightDepth,a.expect("\\right",!1);var s=L(a.parseFunction(),"leftright-right");return{type:"leftright",mode:a.mode,body:n,left:t.text,right:s.delim,rightColor:s.color}},htmlBuilder:(r,e)=>{or(r);for(var t=a0(r.body,e,!0,["mopen","mclose"]),a=0,n=0,s=!1,l=0;l{or(r);var t=m0(r.body,e);if(r.left!=="."){var a=new M.MathNode("mo",[y0(r.left,r.mode)]);a.setAttribute("fence","true"),t.unshift(a)}if(r.right!=="."){var n=new M.MathNode("mo",[y0(r.right,r.mode)]);n.setAttribute("fence","true"),r.rightColor&&n.setAttribute("mathcolor",r.rightColor),t.push(n)}return Bt(t)}});B({type:"middle",names:["\\middle"],props:{numArgs:1,primitive:!0},handler:(r,e)=>{var t=Ye(e[0],r);if(!r.parser.leftrightDepth)throw new z("\\middle without preceding \\left",t);return{type:"middle",mode:r.parser.mode,delim:t.text}},htmlBuilder:(r,e)=>{var t;if(r.delim===".")t=ge(e,[]);else{t=O0.sizedDelim(r.delim,1,e,r.mode,[]);var a={delim:r.delim,options:e};t.isMiddle=a}return t},mathmlBuilder:(r,e)=>{var t=r.delim==="\\vert"||r.delim==="|"?y0("|","text"):y0(r.delim,r.mode),a=new M.MathNode("mo",[t]);return a.setAttribute("fence","true"),a.setAttribute("lspace","0.05em"),a.setAttribute("rspace","0.05em"),a}});var Ot=(r,e)=>{var t=y.wrapFragment(G(r.body,e),e),a=r.label.slice(1),n=e.sizeMultiplier,s,l=0,h=O.isCharacterBox(r.body);if(a==="sout")s=y.makeSpan(["stretchy","sout"]),s.height=e.fontMetrics().defaultRuleThickness/n,l=-.5*e.fontMetrics().xHeight;else if(a==="phase"){var c=Q({number:.6,unit:"pt"},e),f=Q({number:.35,unit:"ex"},e),v=e.havingBaseSizing();n=n/v.sizeMultiplier;var b=t.height+t.depth+c+f;t.style.paddingLeft=T(b/2+c);var x=Math.floor(1e3*b*n),w=Wa(x),A=new S0([new A0("phase",w)],{width:"400em",height:T(x/1e3),viewBox:"0 0 400000 "+x,preserveAspectRatio:"xMinYMin slice"});s=y.makeSvgSpan(["hide-tail"],[A],e),s.style.height=T(b),l=t.depth+c+f}else{/cancel/.test(a)?h||t.classes.push("cancel-pad"):a==="angl"?t.classes.push("anglpad"):t.classes.push("boxpad");var q=0,_=0,D=0;/box/.test(a)?(D=Math.max(e.fontMetrics().fboxrule,e.minRuleThickness),q=e.fontMetrics().fboxsep+(a==="colorbox"?0:D),_=q):a==="angl"?(D=Math.max(e.fontMetrics().defaultRuleThickness,e.minRuleThickness),q=4*D,_=Math.max(0,.25-t.depth)):(q=h?.2:0,_=q),s=E0.encloseSpan(t,a,q,_,e),/fbox|boxed|fcolorbox/.test(a)?(s.style.borderStyle="solid",s.style.borderWidth=T(D)):a==="angl"&&D!==.049&&(s.style.borderTopWidth=T(D),s.style.borderRightWidth=T(D)),l=t.depth+_,r.backgroundColor&&(s.style.backgroundColor=r.backgroundColor,r.borderColor&&(s.style.borderColor=r.borderColor))}var N;if(r.backgroundColor)N=y.makeVList({positionType:"individualShift",children:[{type:"elem",elem:s,shift:l},{type:"elem",elem:t,shift:0}]},e);else{var $=/cancel|phase/.test(a)?["svg-align"]:[];N=y.makeVList({positionType:"individualShift",children:[{type:"elem",elem:t,shift:0},{type:"elem",elem:s,shift:l,wrapperClasses:$}]},e)}return/cancel/.test(a)&&(N.height=t.height,N.depth=t.depth),/cancel/.test(a)&&!h?y.makeSpan(["mord","cancel-lap"],[N],e):y.makeSpan(["mord"],[N],e)},It=(r,e)=>{var t=0,a=new M.MathNode(r.label.indexOf("colorbox")>-1?"mpadded":"menclose",[X(r.body,e)]);switch(r.label){case"\\cancel":a.setAttribute("notation","updiagonalstrike");break;case"\\bcancel":a.setAttribute("notation","downdiagonalstrike");break;case"\\phase":a.setAttribute("notation","phasorangle");break;case"\\sout":a.setAttribute("notation","horizontalstrike");break;case"\\fbox":a.setAttribute("notation","box");break;case"\\angl":a.setAttribute("notation","actuarial");break;case"\\fcolorbox":case"\\colorbox":if(t=e.fontMetrics().fboxsep*e.fontMetrics().ptPerEm,a.setAttribute("width","+"+2*t+"pt"),a.setAttribute("height","+"+2*t+"pt"),a.setAttribute("lspace",t+"pt"),a.setAttribute("voffset",t+"pt"),r.label==="\\fcolorbox"){var n=Math.max(e.fontMetrics().fboxrule,e.minRuleThickness);a.setAttribute("style","border: "+n+"em solid "+String(r.borderColor))}break;case"\\xcancel":a.setAttribute("notation","updiagonalstrike downdiagonalstrike");break}return r.backgroundColor&&a.setAttribute("mathbackground",r.backgroundColor),a};B({type:"enclose",names:["\\colorbox"],props:{numArgs:2,allowedInText:!0,argTypes:["color","text"]},handler(r,e,t){var{parser:a,funcName:n}=r,s=L(e[0],"color-token").color,l=e[1];return{type:"enclose",mode:a.mode,label:n,backgroundColor:s,body:l}},htmlBuilder:Ot,mathmlBuilder:It});B({type:"enclose",names:["\\fcolorbox"],props:{numArgs:3,allowedInText:!0,argTypes:["color","color","text"]},handler(r,e,t){var{parser:a,funcName:n}=r,s=L(e[0],"color-token").color,l=L(e[1],"color-token").color,h=e[2];return{type:"enclose",mode:a.mode,label:n,backgroundColor:l,borderColor:s,body:h}},htmlBuilder:Ot,mathmlBuilder:It});B({type:"enclose",names:["\\fbox"],props:{numArgs:1,argTypes:["hbox"],allowedInText:!0},handler(r,e){var{parser:t}=r;return{type:"enclose",mode:t.mode,label:"\\fbox",body:e[0]}}});B({type:"enclose",names:["\\cancel","\\bcancel","\\xcancel","\\sout","\\phase"],props:{numArgs:1},handler(r,e){var{parser:t,funcName:a}=r,n=e[0];return{type:"enclose",mode:t.mode,label:a,body:n}},htmlBuilder:Ot,mathmlBuilder:It});B({type:"enclose",names:["\\angl"],props:{numArgs:1,argTypes:["hbox"],allowedInText:!1},handler(r,e){var{parser:t}=r;return{type:"enclose",mode:t.mode,label:"\\angl",body:e[0]}}});var ra={};function T0(r){for(var{type:e,names:t,props:a,handler:n,htmlBuilder:s,mathmlBuilder:l}=r,h={type:e,numArgs:a.numArgs||0,allowedInText:!1,numOptionalArgs:0,handler:n},c=0;c{var e=r.parser.settings;if(!e.displayMode)throw new z("{"+r.envName+"} can be used only in display mode.")};function Et(r){if(r.indexOf("ed")===-1)return r.indexOf("*")===-1}function U0(r,e,t){var{hskipBeforeAndAfter:a,addJot:n,cols:s,arraystretch:l,colSeparationType:h,autoTag:c,singleRow:f,emptySingleRow:v,maxNumCols:b,leqno:x}=e;if(r.gullet.beginGroup(),f||r.gullet.macros.set("\\cr","\\\\\\relax"),!l){var w=r.gullet.expandMacroAsText("\\arraystretch");if(w==null)l=1;else if(l=parseFloat(w),!l||l<0)throw new z("Invalid \\arraystretch: "+w)}r.gullet.beginGroup();var A=[],q=[A],_=[],D=[],N=c!=null?[]:void 0;function $(){c&&r.gullet.macros.set("\\@eqnsw","1",!0)}function H(){N&&(r.gullet.macros.get("\\df@tag")?(N.push(r.subparse([new b0("\\df@tag")])),r.gullet.macros.set("\\df@tag",void 0,!0)):N.push(!!c&&r.gullet.macros.get("\\@eqnsw")==="1"))}for($(),D.push(lr(r));;){var F=r.parseExpression(!1,f?"\\end":"\\\\");r.gullet.endGroup(),r.gullet.beginGroup(),F={type:"ordgroup",mode:r.mode,body:F},t&&(F={type:"styling",mode:r.mode,style:t,body:[F]}),A.push(F);var P=r.fetch().text;if(P==="&"){if(b&&A.length===b){if(f||h)throw new z("Too many tab characters: &",r.nextToken);r.settings.reportNonstrict("textEnv","Too few columns specified in the {array} column argument.")}r.consume()}else if(P==="\\end"){H(),A.length===1&&F.type==="styling"&&F.body[0].body.length===0&&(q.length>1||!v)&&q.pop(),D.length0&&($+=.25),f.push({pos:$,isDashed:we[Se]})}for(H(l[0]),a=0;a0&&(i0+=N,Vwe))for(a=0;a=h)){var ee=void 0;(n>0||e.hskipBeforeAndAfter)&&(ee=O.deflt(f0.pregap,x),ee!==0&&(x0=y.makeSpan(["arraycolsep"],[]),x0.style.width=T(ee),u0.push(x0)));var te=[];for(a=0;a0){for(var Sa=y.makeLineSpan("hline",t,v),ka=y.makeLineSpan("hdashline",t,v),Je=[{type:"elem",elem:c,shift:0}];f.length>0;){var Ut=f.pop(),Xt=Ut.pos-r0;Ut.isDashed?Je.push({type:"elem",elem:ka,shift:Xt}):Je.push({type:"elem",elem:Sa,shift:Xt})}c=y.makeVList({positionType:"individualShift",children:Je},t)}if(K0.length===0)return y.makeSpan(["mord"],[c],t);var Qe=y.makeVList({positionType:"individualShift",children:K0},t);return Qe=y.makeSpan(["tag"],[Qe],t),y.makeFragment([c,Qe])},en={c:"center ",l:"left ",r:"right "},B0=function(e,t){for(var a=[],n=new M.MathNode("mtd",[],["mtr-glue"]),s=new M.MathNode("mtd",[],["mml-eqn-num"]),l=0;l0){var A=e.cols,q="",_=!1,D=0,N=A.length;A[0].type==="separator"&&(x+="top ",D=1),A[A.length-1].type==="separator"&&(x+="bottom ",N-=1);for(var $=D;$0?"left ":"",x+=j[j.length-1].length>0?"right ":"";for(var U=1;U-1?"alignat":"align",s=e.envName==="split",l=U0(e.parser,{cols:a,addJot:!0,autoTag:s?void 0:Et(e.envName),emptySingleRow:!0,colSeparationType:n,maxNumCols:s?2:void 0,leqno:e.parser.settings.leqno},"display"),h,c=0,f={type:"ordgroup",mode:e.mode,body:[]};if(t[0]&&t[0].type==="ordgroup"){for(var v="",b=0;b0&&w&&(_=1),a[A]={type:"align",align:q,pregap:_,postgap:0}}return l.colSeparationType=w?"align":"alignat",l};T0({type:"array",names:["array","darray"],props:{numArgs:1},handler(r,e){var t=Xe(e[0]),a=t?[e[0]]:L(e[0],"ordgroup").body,n=a.map(function(l){var h=Ct(l),c=h.text;if("lcr".indexOf(c)!==-1)return{type:"align",align:c};if(c==="|")return{type:"separator",separator:"|"};if(c===":")return{type:"separator",separator:":"};throw new z("Unknown column alignment: "+c,l)}),s={cols:n,hskipBeforeAndAfter:!0,maxNumCols:n.length};return U0(r.parser,s,Rt(r.envName))},htmlBuilder:q0,mathmlBuilder:B0});T0({type:"array",names:["matrix","pmatrix","bmatrix","Bmatrix","vmatrix","Vmatrix","matrix*","pmatrix*","bmatrix*","Bmatrix*","vmatrix*","Vmatrix*"],props:{numArgs:0},handler(r){var e={matrix:null,pmatrix:["(",")"],bmatrix:["[","]"],Bmatrix:["\\{","\\}"],vmatrix:["|","|"],Vmatrix:["\\Vert","\\Vert"]}[r.envName.replace("*","")],t="c",a={hskipBeforeAndAfter:!1,cols:[{type:"align",align:t}]};if(r.envName.charAt(r.envName.length-1)==="*"){var n=r.parser;if(n.consumeSpaces(),n.fetch().text==="["){if(n.consume(),n.consumeSpaces(),t=n.fetch().text,"lcr".indexOf(t)===-1)throw new z("Expected l or c or r",n.nextToken);n.consume(),n.consumeSpaces(),n.expect("]"),n.consume(),a.cols=[{type:"align",align:t}]}}var s=U0(r.parser,a,Rt(r.envName)),l=Math.max(0,...s.body.map(h=>h.length));return s.cols=new Array(l).fill({type:"align",align:t}),e?{type:"leftright",mode:r.mode,body:[s],left:e[0],right:e[1],rightColor:void 0}:s},htmlBuilder:q0,mathmlBuilder:B0});T0({type:"array",names:["smallmatrix"],props:{numArgs:0},handler(r){var e={arraystretch:.5},t=U0(r.parser,e,"script");return t.colSeparationType="small",t},htmlBuilder:q0,mathmlBuilder:B0});T0({type:"array",names:["subarray"],props:{numArgs:1},handler(r,e){var t=Xe(e[0]),a=t?[e[0]]:L(e[0],"ordgroup").body,n=a.map(function(l){var h=Ct(l),c=h.text;if("lc".indexOf(c)!==-1)return{type:"align",align:c};throw new z("Unknown column alignment: "+c,l)});if(n.length>1)throw new z("{subarray} can contain only one column");var s={cols:n,hskipBeforeAndAfter:!1,arraystretch:.5};if(s=U0(r.parser,s,"script"),s.body.length>0&&s.body[0].length>1)throw new z("{subarray} can contain only one column");return s},htmlBuilder:q0,mathmlBuilder:B0});T0({type:"array",names:["cases","dcases","rcases","drcases"],props:{numArgs:0},handler(r){var e={arraystretch:1.2,cols:[{type:"align",align:"l",pregap:0,postgap:1},{type:"align",align:"l",pregap:0,postgap:0}]},t=U0(r.parser,e,Rt(r.envName));return{type:"leftright",mode:r.mode,body:[t],left:r.envName.indexOf("r")>-1?".":"\\{",right:r.envName.indexOf("r")>-1?"\\}":".",rightColor:void 0}},htmlBuilder:q0,mathmlBuilder:B0});T0({type:"array",names:["align","align*","aligned","split"],props:{numArgs:0},handler:na,htmlBuilder:q0,mathmlBuilder:B0});T0({type:"array",names:["gathered","gather","gather*"],props:{numArgs:0},handler(r){O.contains(["gather","gather*"],r.envName)&&Ze(r);var e={cols:[{type:"align",align:"c"}],addJot:!0,colSeparationType:"gather",autoTag:Et(r.envName),emptySingleRow:!0,leqno:r.parser.settings.leqno};return U0(r.parser,e,"display")},htmlBuilder:q0,mathmlBuilder:B0});T0({type:"array",names:["alignat","alignat*","alignedat"],props:{numArgs:1},handler:na,htmlBuilder:q0,mathmlBuilder:B0});T0({type:"array",names:["equation","equation*"],props:{numArgs:0},handler(r){Ze(r);var e={autoTag:Et(r.envName),emptySingleRow:!0,singleRow:!0,maxNumCols:1,leqno:r.parser.settings.leqno};return U0(r.parser,e,"display")},htmlBuilder:q0,mathmlBuilder:B0});T0({type:"array",names:["CD"],props:{numArgs:0},handler(r){return Ze(r),F1(r.parser)},htmlBuilder:q0,mathmlBuilder:B0});m("\\nonumber","\\gdef\\@eqnsw{0}");m("\\notag","\\nonumber");B({type:"text",names:["\\hline","\\hdashline"],props:{numArgs:0,allowedInText:!0,allowedInMath:!0},handler(r,e){throw new z(r.funcName+" valid only within array environment")}});var ur=ra;B({type:"environment",names:["\\begin","\\end"],props:{numArgs:1,argTypes:["text"]},handler(r,e){var{parser:t,funcName:a}=r,n=e[0];if(n.type!=="ordgroup")throw new z("Invalid environment name",n);for(var s="",l=0;l{var t=r.font,a=e.withFont(t);return G(r.body,a)},sa=(r,e)=>{var t=r.font,a=e.withFont(t);return X(r.body,a)},hr={"\\Bbb":"\\mathbb","\\bold":"\\mathbf","\\frak":"\\mathfrak","\\bm":"\\boldsymbol"};B({type:"font",names:["\\mathrm","\\mathit","\\mathbf","\\mathnormal","\\mathsfit","\\mathbb","\\mathcal","\\mathfrak","\\mathscr","\\mathsf","\\mathtt","\\Bbb","\\bold","\\frak"],props:{numArgs:1,allowedInArgument:!0},handler:(r,e)=>{var{parser:t,funcName:a}=r,n=He(e[0]),s=a;return s in hr&&(s=hr[s]),{type:"font",mode:t.mode,font:s.slice(1),body:n}},htmlBuilder:ia,mathmlBuilder:sa});B({type:"mclass",names:["\\boldsymbol","\\bm"],props:{numArgs:1},handler:(r,e)=>{var{parser:t}=r,a=e[0],n=O.isCharacterBox(a);return{type:"mclass",mode:t.mode,mclass:We(a),body:[{type:"font",mode:t.mode,font:"boldsymbol",body:a}],isCharacterBox:n}}});B({type:"font",names:["\\rm","\\sf","\\tt","\\bf","\\it","\\cal"],props:{numArgs:0,allowedInText:!0},handler:(r,e)=>{var{parser:t,funcName:a,breakOnTokenText:n}=r,{mode:s}=t,l=t.parseExpression(!0,n),h="math"+a.slice(1);return{type:"font",mode:s,font:h,body:{type:"ordgroup",mode:t.mode,body:l}}},htmlBuilder:ia,mathmlBuilder:sa});var oa=(r,e)=>{var t=e;return r==="display"?t=t.id>=E.SCRIPT.id?t.text():E.DISPLAY:r==="text"&&t.size===E.DISPLAY.size?t=E.TEXT:r==="script"?t=E.SCRIPT:r==="scriptscript"&&(t=E.SCRIPTSCRIPT),t},$t=(r,e)=>{var t=oa(r.size,e.style),a=t.fracNum(),n=t.fracDen(),s;s=e.havingStyle(a);var l=G(r.numer,s,e);if(r.continued){var h=8.5/e.fontMetrics().ptPerEm,c=3.5/e.fontMetrics().ptPerEm;l.height=l.height0?A=3*x:A=7*x,q=e.fontMetrics().denom1):(b>0?(w=e.fontMetrics().num2,A=x):(w=e.fontMetrics().num3,A=3*x),q=e.fontMetrics().denom2);var _;if(v){var N=e.fontMetrics().axisHeight;w-l.depth-(N+.5*b){var t=new M.MathNode("mfrac",[X(r.numer,e),X(r.denom,e)]);if(!r.hasBarLine)t.setAttribute("linethickness","0px");else if(r.barSize){var a=Q(r.barSize,e);t.setAttribute("linethickness",T(a))}var n=oa(r.size,e.style);if(n.size!==e.style.size){t=new M.MathNode("mstyle",[t]);var s=n.size===E.DISPLAY.size?"true":"false";t.setAttribute("displaystyle",s),t.setAttribute("scriptlevel","0")}if(r.leftDelim!=null||r.rightDelim!=null){var l=[];if(r.leftDelim!=null){var h=new M.MathNode("mo",[new M.TextNode(r.leftDelim.replace("\\",""))]);h.setAttribute("fence","true"),l.push(h)}if(l.push(t),r.rightDelim!=null){var c=new M.MathNode("mo",[new M.TextNode(r.rightDelim.replace("\\",""))]);c.setAttribute("fence","true"),l.push(c)}return Bt(l)}return t};B({type:"genfrac",names:["\\dfrac","\\frac","\\tfrac","\\dbinom","\\binom","\\tbinom","\\\\atopfrac","\\\\bracefrac","\\\\brackfrac"],props:{numArgs:2,allowedInArgument:!0},handler:(r,e)=>{var{parser:t,funcName:a}=r,n=e[0],s=e[1],l,h=null,c=null,f="auto";switch(a){case"\\dfrac":case"\\frac":case"\\tfrac":l=!0;break;case"\\\\atopfrac":l=!1;break;case"\\dbinom":case"\\binom":case"\\tbinom":l=!1,h="(",c=")";break;case"\\\\bracefrac":l=!1,h="\\{",c="\\}";break;case"\\\\brackfrac":l=!1,h="[",c="]";break;default:throw new Error("Unrecognized genfrac command")}switch(a){case"\\dfrac":case"\\dbinom":f="display";break;case"\\tfrac":case"\\tbinom":f="text";break}return{type:"genfrac",mode:t.mode,continued:!1,numer:n,denom:s,hasBarLine:l,leftDelim:h,rightDelim:c,size:f,barSize:null}},htmlBuilder:$t,mathmlBuilder:Lt});B({type:"genfrac",names:["\\cfrac"],props:{numArgs:2},handler:(r,e)=>{var{parser:t,funcName:a}=r,n=e[0],s=e[1];return{type:"genfrac",mode:t.mode,continued:!0,numer:n,denom:s,hasBarLine:!0,leftDelim:null,rightDelim:null,size:"display",barSize:null}}});B({type:"infix",names:["\\over","\\choose","\\atop","\\brace","\\brack"],props:{numArgs:0,infix:!0},handler(r){var{parser:e,funcName:t,token:a}=r,n;switch(t){case"\\over":n="\\frac";break;case"\\choose":n="\\binom";break;case"\\atop":n="\\\\atopfrac";break;case"\\brace":n="\\\\bracefrac";break;case"\\brack":n="\\\\brackfrac";break;default:throw new Error("Unrecognized infix genfrac command")}return{type:"infix",mode:e.mode,replaceWith:n,token:a}}});var cr=["display","text","script","scriptscript"],mr=function(e){var t=null;return e.length>0&&(t=e,t=t==="."?null:t),t};B({type:"genfrac",names:["\\genfrac"],props:{numArgs:6,allowedInArgument:!0,argTypes:["math","math","size","text","math","math"]},handler(r,e){var{parser:t}=r,a=e[4],n=e[5],s=He(e[0]),l=s.type==="atom"&&s.family==="open"?mr(s.text):null,h=He(e[1]),c=h.type==="atom"&&h.family==="close"?mr(h.text):null,f=L(e[2],"size"),v,b=null;f.isBlank?v=!0:(b=f.value,v=b.number>0);var x="auto",w=e[3];if(w.type==="ordgroup"){if(w.body.length>0){var A=L(w.body[0],"textord");x=cr[Number(A.text)]}}else w=L(w,"textord"),x=cr[Number(w.text)];return{type:"genfrac",mode:t.mode,numer:a,denom:n,continued:!1,hasBarLine:v,barSize:b,leftDelim:l,rightDelim:c,size:x}},htmlBuilder:$t,mathmlBuilder:Lt});B({type:"infix",names:["\\above"],props:{numArgs:1,argTypes:["size"],infix:!0},handler(r,e){var{parser:t,funcName:a,token:n}=r;return{type:"infix",mode:t.mode,replaceWith:"\\\\abovefrac",size:L(e[0],"size").value,token:n}}});B({type:"genfrac",names:["\\\\abovefrac"],props:{numArgs:3,argTypes:["math","size","math"]},handler:(r,e)=>{var{parser:t,funcName:a}=r,n=e[0],s=_a(L(e[1],"infix").size),l=e[2],h=s.number>0;return{type:"genfrac",mode:t.mode,numer:n,denom:l,continued:!1,hasBarLine:h,barSize:s,leftDelim:null,rightDelim:null,size:"auto"}},htmlBuilder:$t,mathmlBuilder:Lt});var la=(r,e)=>{var t=e.style,a,n;r.type==="supsub"?(a=r.sup?G(r.sup,e.havingStyle(t.sup()),e):G(r.sub,e.havingStyle(t.sub()),e),n=L(r.base,"horizBrace")):n=L(r,"horizBrace");var s=G(n.base,e.havingBaseStyle(E.DISPLAY)),l=E0.svgSpan(n,e),h;if(n.isOver?(h=y.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:s},{type:"kern",size:.1},{type:"elem",elem:l}]},e),h.children[0].children[0].children[1].classes.push("svg-align")):(h=y.makeVList({positionType:"bottom",positionData:s.depth+.1+l.height,children:[{type:"elem",elem:l},{type:"kern",size:.1},{type:"elem",elem:s}]},e),h.children[0].children[0].children[0].classes.push("svg-align")),a){var c=y.makeSpan(["mord",n.isOver?"mover":"munder"],[h],e);n.isOver?h=y.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:c},{type:"kern",size:.2},{type:"elem",elem:a}]},e):h=y.makeVList({positionType:"bottom",positionData:c.depth+.2+a.height+a.depth,children:[{type:"elem",elem:a},{type:"kern",size:.2},{type:"elem",elem:c}]},e)}return y.makeSpan(["mord",n.isOver?"mover":"munder"],[h],e)},tn=(r,e)=>{var t=E0.mathMLnode(r.label);return new M.MathNode(r.isOver?"mover":"munder",[X(r.base,e),t])};B({type:"horizBrace",names:["\\overbrace","\\underbrace"],props:{numArgs:1},handler(r,e){var{parser:t,funcName:a}=r;return{type:"horizBrace",mode:t.mode,label:a,isOver:/^\\over/.test(a),base:e[0]}},htmlBuilder:la,mathmlBuilder:tn});B({type:"href",names:["\\href"],props:{numArgs:2,argTypes:["url","original"],allowedInText:!0},handler:(r,e)=>{var{parser:t}=r,a=e[1],n=L(e[0],"url").url;return t.settings.isTrusted({command:"\\href",url:n})?{type:"href",mode:t.mode,href:n,body:e0(a)}:t.formatUnsupportedCmd("\\href")},htmlBuilder:(r,e)=>{var t=a0(r.body,e,!1);return y.makeAnchor(r.href,[],t,e)},mathmlBuilder:(r,e)=>{var t=V0(r.body,e);return t instanceof s0||(t=new s0("mrow",[t])),t.setAttribute("href",r.href),t}});B({type:"href",names:["\\url"],props:{numArgs:1,argTypes:["url"],allowedInText:!0},handler:(r,e)=>{var{parser:t}=r,a=L(e[0],"url").url;if(!t.settings.isTrusted({command:"\\url",url:a}))return t.formatUnsupportedCmd("\\url");for(var n=[],s=0;s{var{parser:t,funcName:a,token:n}=r,s=L(e[0],"raw").string,l=e[1];t.settings.strict&&t.settings.reportNonstrict("htmlExtension","HTML extension is disabled on strict mode");var h,c={};switch(a){case"\\htmlClass":c.class=s,h={command:"\\htmlClass",class:s};break;case"\\htmlId":c.id=s,h={command:"\\htmlId",id:s};break;case"\\htmlStyle":c.style=s,h={command:"\\htmlStyle",style:s};break;case"\\htmlData":{for(var f=s.split(","),v=0;v{var t=a0(r.body,e,!1),a=["enclosing"];r.attributes.class&&a.push(...r.attributes.class.trim().split(/\s+/));var n=y.makeSpan(a,t,e);for(var s in r.attributes)s!=="class"&&r.attributes.hasOwnProperty(s)&&n.setAttribute(s,r.attributes[s]);return n},mathmlBuilder:(r,e)=>V0(r.body,e)});B({type:"htmlmathml",names:["\\html@mathml"],props:{numArgs:2,allowedInText:!0},handler:(r,e)=>{var{parser:t}=r;return{type:"htmlmathml",mode:t.mode,html:e0(e[0]),mathml:e0(e[1])}},htmlBuilder:(r,e)=>{var t=a0(r.html,e,!1);return y.makeFragment(t)},mathmlBuilder:(r,e)=>V0(r.mathml,e)});var ht=function(e){if(/^[-+]? *(\d+(\.\d*)?|\.\d+)$/.test(e))return{number:+e,unit:"bp"};var t=/([-+]?) *(\d+(?:\.\d*)?|\.\d+) *([a-z]{2})/.exec(e);if(!t)throw new z("Invalid size: '"+e+"' in \\includegraphics");var a={number:+(t[1]+t[2]),unit:t[3]};if(!Tr(a))throw new z("Invalid unit: '"+a.unit+"' in \\includegraphics.");return a};B({type:"includegraphics",names:["\\includegraphics"],props:{numArgs:1,numOptionalArgs:1,argTypes:["raw","url"],allowedInText:!1},handler:(r,e,t)=>{var{parser:a}=r,n={number:0,unit:"em"},s={number:.9,unit:"em"},l={number:0,unit:"em"},h="";if(t[0])for(var c=L(t[0],"raw").string,f=c.split(","),v=0;v{var t=Q(r.height,e),a=0;r.totalheight.number>0&&(a=Q(r.totalheight,e)-t);var n=0;r.width.number>0&&(n=Q(r.width,e));var s={height:T(t+a)};n>0&&(s.width=T(n)),a>0&&(s.verticalAlign=T(-a));var l=new vt(r.src,r.alt,s);return l.height=t,l.depth=a,l},mathmlBuilder:(r,e)=>{var t=new M.MathNode("mglyph",[]);t.setAttribute("alt",r.alt);var a=Q(r.height,e),n=0;if(r.totalheight.number>0&&(n=Q(r.totalheight,e)-a,t.setAttribute("valign",T(-n))),t.setAttribute("height",T(a+n)),r.width.number>0){var s=Q(r.width,e);t.setAttribute("width",T(s))}return t.setAttribute("src",r.src),t}});B({type:"kern",names:["\\kern","\\mkern","\\hskip","\\mskip"],props:{numArgs:1,argTypes:["size"],primitive:!0,allowedInText:!0},handler(r,e){var{parser:t,funcName:a}=r,n=L(e[0],"size");if(t.settings.strict){var s=a[1]==="m",l=n.value.unit==="mu";s?(l||t.settings.reportNonstrict("mathVsTextUnits","LaTeX's "+a+" supports only mu units, "+("not "+n.value.unit+" units")),t.mode!=="math"&&t.settings.reportNonstrict("mathVsTextUnits","LaTeX's "+a+" works only in math mode")):l&&t.settings.reportNonstrict("mathVsTextUnits","LaTeX's "+a+" doesn't support mu units")}return{type:"kern",mode:t.mode,dimension:n.value}},htmlBuilder(r,e){return y.makeGlue(r.dimension,e)},mathmlBuilder(r,e){var t=Q(r.dimension,e);return new M.SpaceNode(t)}});B({type:"lap",names:["\\mathllap","\\mathrlap","\\mathclap"],props:{numArgs:1,allowedInText:!0},handler:(r,e)=>{var{parser:t,funcName:a}=r,n=e[0];return{type:"lap",mode:t.mode,alignment:a.slice(5),body:n}},htmlBuilder:(r,e)=>{var t;r.alignment==="clap"?(t=y.makeSpan([],[G(r.body,e)]),t=y.makeSpan(["inner"],[t],e)):t=y.makeSpan(["inner"],[G(r.body,e)]);var a=y.makeSpan(["fix"],[]),n=y.makeSpan([r.alignment],[t,a],e),s=y.makeSpan(["strut"]);return s.style.height=T(n.height+n.depth),n.depth&&(s.style.verticalAlign=T(-n.depth)),n.children.unshift(s),n=y.makeSpan(["thinbox"],[n],e),y.makeSpan(["mord","vbox"],[n],e)},mathmlBuilder:(r,e)=>{var t=new M.MathNode("mpadded",[X(r.body,e)]);if(r.alignment!=="rlap"){var a=r.alignment==="llap"?"-1":"-0.5";t.setAttribute("lspace",a+"width")}return t.setAttribute("width","0px"),t}});B({type:"styling",names:["\\(","$"],props:{numArgs:0,allowedInText:!0,allowedInMath:!1},handler(r,e){var{funcName:t,parser:a}=r,n=a.mode;a.switchMode("math");var s=t==="\\("?"\\)":"$",l=a.parseExpression(!1,s);return a.expect(s),a.switchMode(n),{type:"styling",mode:a.mode,style:"text",body:l}}});B({type:"text",names:["\\)","\\]"],props:{numArgs:0,allowedInText:!0,allowedInMath:!1},handler(r,e){throw new z("Mismatched "+r.funcName)}});var dr=(r,e)=>{switch(e.style.size){case E.DISPLAY.size:return r.display;case E.TEXT.size:return r.text;case E.SCRIPT.size:return r.script;case E.SCRIPTSCRIPT.size:return r.scriptscript;default:return r.text}};B({type:"mathchoice",names:["\\mathchoice"],props:{numArgs:4,primitive:!0},handler:(r,e)=>{var{parser:t}=r;return{type:"mathchoice",mode:t.mode,display:e0(e[0]),text:e0(e[1]),script:e0(e[2]),scriptscript:e0(e[3])}},htmlBuilder:(r,e)=>{var t=dr(r,e),a=a0(t,e,!1);return y.makeFragment(a)},mathmlBuilder:(r,e)=>{var t=dr(r,e);return V0(t,e)}});var ua=(r,e,t,a,n,s,l)=>{r=y.makeSpan([],[r]);var h=t&&O.isCharacterBox(t),c,f;if(e){var v=G(e,a.havingStyle(n.sup()),a);f={elem:v,kern:Math.max(a.fontMetrics().bigOpSpacing1,a.fontMetrics().bigOpSpacing3-v.depth)}}if(t){var b=G(t,a.havingStyle(n.sub()),a);c={elem:b,kern:Math.max(a.fontMetrics().bigOpSpacing2,a.fontMetrics().bigOpSpacing4-b.height)}}var x;if(f&&c){var w=a.fontMetrics().bigOpSpacing5+c.elem.height+c.elem.depth+c.kern+r.depth+l;x=y.makeVList({positionType:"bottom",positionData:w,children:[{type:"kern",size:a.fontMetrics().bigOpSpacing5},{type:"elem",elem:c.elem,marginLeft:T(-s)},{type:"kern",size:c.kern},{type:"elem",elem:r},{type:"kern",size:f.kern},{type:"elem",elem:f.elem,marginLeft:T(s)},{type:"kern",size:a.fontMetrics().bigOpSpacing5}]},a)}else if(c){var A=r.height-l;x=y.makeVList({positionType:"top",positionData:A,children:[{type:"kern",size:a.fontMetrics().bigOpSpacing5},{type:"elem",elem:c.elem,marginLeft:T(-s)},{type:"kern",size:c.kern},{type:"elem",elem:r}]},a)}else if(f){var q=r.depth+l;x=y.makeVList({positionType:"bottom",positionData:q,children:[{type:"elem",elem:r},{type:"kern",size:f.kern},{type:"elem",elem:f.elem,marginLeft:T(s)},{type:"kern",size:a.fontMetrics().bigOpSpacing5}]},a)}else return r;var _=[x];if(c&&s!==0&&!h){var D=y.makeSpan(["mspace"],[],a);D.style.marginRight=T(s),_.unshift(D)}return y.makeSpan(["mop","op-limits"],_,a)},ha=["\\smallint"],se=(r,e)=>{var t,a,n=!1,s;r.type==="supsub"?(t=r.sup,a=r.sub,s=L(r.base,"op"),n=!0):s=L(r,"op");var l=e.style,h=!1;l.size===E.DISPLAY.size&&s.symbol&&!O.contains(ha,s.name)&&(h=!0);var c;if(s.symbol){var f=h?"Size2-Regular":"Size1-Regular",v="";if((s.name==="\\oiint"||s.name==="\\oiiint")&&(v=s.name.slice(1),s.name=v==="oiint"?"\\iint":"\\iiint"),c=y.makeSymbol(s.name,f,"math",e,["mop","op-symbol",h?"large-op":"small-op"]),v.length>0){var b=c.italic,x=y.staticSvg(v+"Size"+(h?"2":"1"),e);c=y.makeVList({positionType:"individualShift",children:[{type:"elem",elem:c,shift:0},{type:"elem",elem:x,shift:h?.08:0}]},e),s.name="\\"+v,c.classes.unshift("mop"),c.italic=b}}else if(s.body){var w=a0(s.body,e,!0);w.length===1&&w[0]instanceof c0?(c=w[0],c.classes[0]="mop"):c=y.makeSpan(["mop"],w,e)}else{for(var A=[],q=1;q{var t;if(r.symbol)t=new s0("mo",[y0(r.name,r.mode)]),O.contains(ha,r.name)&&t.setAttribute("largeop","false");else if(r.body)t=new s0("mo",m0(r.body,e));else{t=new s0("mi",[new g0(r.name.slice(1))]);var a=new s0("mo",[y0("\u2061","text")]);r.parentIsSupSub?t=new s0("mrow",[t,a]):t=$r([t,a])}return t},rn={"\u220F":"\\prod","\u2210":"\\coprod","\u2211":"\\sum","\u22C0":"\\bigwedge","\u22C1":"\\bigvee","\u22C2":"\\bigcap","\u22C3":"\\bigcup","\u2A00":"\\bigodot","\u2A01":"\\bigoplus","\u2A02":"\\bigotimes","\u2A04":"\\biguplus","\u2A06":"\\bigsqcup"};B({type:"op",names:["\\coprod","\\bigvee","\\bigwedge","\\biguplus","\\bigcap","\\bigcup","\\intop","\\prod","\\sum","\\bigotimes","\\bigoplus","\\bigodot","\\bigsqcup","\\smallint","\u220F","\u2210","\u2211","\u22C0","\u22C1","\u22C2","\u22C3","\u2A00","\u2A01","\u2A02","\u2A04","\u2A06"],props:{numArgs:0},handler:(r,e)=>{var{parser:t,funcName:a}=r,n=a;return n.length===1&&(n=rn[n]),{type:"op",mode:t.mode,limits:!0,parentIsSupSub:!1,symbol:!0,name:n}},htmlBuilder:se,mathmlBuilder:be});B({type:"op",names:["\\mathop"],props:{numArgs:1,primitive:!0},handler:(r,e)=>{var{parser:t}=r,a=e[0];return{type:"op",mode:t.mode,limits:!1,parentIsSupSub:!1,symbol:!1,body:e0(a)}},htmlBuilder:se,mathmlBuilder:be});var an={"\u222B":"\\int","\u222C":"\\iint","\u222D":"\\iiint","\u222E":"\\oint","\u222F":"\\oiint","\u2230":"\\oiiint"};B({type:"op",names:["\\arcsin","\\arccos","\\arctan","\\arctg","\\arcctg","\\arg","\\ch","\\cos","\\cosec","\\cosh","\\cot","\\cotg","\\coth","\\csc","\\ctg","\\cth","\\deg","\\dim","\\exp","\\hom","\\ker","\\lg","\\ln","\\log","\\sec","\\sin","\\sinh","\\sh","\\tan","\\tanh","\\tg","\\th"],props:{numArgs:0},handler(r){var{parser:e,funcName:t}=r;return{type:"op",mode:e.mode,limits:!1,parentIsSupSub:!1,symbol:!1,name:t}},htmlBuilder:se,mathmlBuilder:be});B({type:"op",names:["\\det","\\gcd","\\inf","\\lim","\\max","\\min","\\Pr","\\sup"],props:{numArgs:0},handler(r){var{parser:e,funcName:t}=r;return{type:"op",mode:e.mode,limits:!0,parentIsSupSub:!1,symbol:!1,name:t}},htmlBuilder:se,mathmlBuilder:be});B({type:"op",names:["\\int","\\iint","\\iiint","\\oint","\\oiint","\\oiiint","\u222B","\u222C","\u222D","\u222E","\u222F","\u2230"],props:{numArgs:0},handler(r){var{parser:e,funcName:t}=r,a=t;return a.length===1&&(a=an[a]),{type:"op",mode:e.mode,limits:!1,parentIsSupSub:!1,symbol:!0,name:a}},htmlBuilder:se,mathmlBuilder:be});var ca=(r,e)=>{var t,a,n=!1,s;r.type==="supsub"?(t=r.sup,a=r.sub,s=L(r.base,"operatorname"),n=!0):s=L(r,"operatorname");var l;if(s.body.length>0){for(var h=s.body.map(b=>{var x=b.text;return typeof x=="string"?{type:"textord",mode:b.mode,text:x}:b}),c=a0(h,e.withFont("mathrm"),!0),f=0;f{for(var t=m0(r.body,e.withFont("mathrm")),a=!0,n=0;nv.toText()).join("");t=[new M.TextNode(h)]}var c=new M.MathNode("mi",t);c.setAttribute("mathvariant","normal");var f=new M.MathNode("mo",[y0("\u2061","text")]);return r.parentIsSupSub?new M.MathNode("mrow",[c,f]):M.newDocumentFragment([c,f])};B({type:"operatorname",names:["\\operatorname@","\\operatornamewithlimits"],props:{numArgs:1},handler:(r,e)=>{var{parser:t,funcName:a}=r,n=e[0];return{type:"operatorname",mode:t.mode,body:e0(n),alwaysHandleSupSub:a==="\\operatornamewithlimits",limits:!1,parentIsSupSub:!1}},htmlBuilder:ca,mathmlBuilder:nn});m("\\operatorname","\\@ifstar\\operatornamewithlimits\\operatorname@");j0({type:"ordgroup",htmlBuilder(r,e){return r.semisimple?y.makeFragment(a0(r.body,e,!1)):y.makeSpan(["mord"],a0(r.body,e,!0),e)},mathmlBuilder(r,e){return V0(r.body,e,!0)}});B({type:"overline",names:["\\overline"],props:{numArgs:1},handler(r,e){var{parser:t}=r,a=e[0];return{type:"overline",mode:t.mode,body:a}},htmlBuilder(r,e){var t=G(r.body,e.havingCrampedStyle()),a=y.makeLineSpan("overline-line",e),n=e.fontMetrics().defaultRuleThickness,s=y.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:t},{type:"kern",size:3*n},{type:"elem",elem:a},{type:"kern",size:n}]},e);return y.makeSpan(["mord","overline"],[s],e)},mathmlBuilder(r,e){var t=new M.MathNode("mo",[new M.TextNode("\u203E")]);t.setAttribute("stretchy","true");var a=new M.MathNode("mover",[X(r.body,e),t]);return a.setAttribute("accent","true"),a}});B({type:"phantom",names:["\\phantom"],props:{numArgs:1,allowedInText:!0},handler:(r,e)=>{var{parser:t}=r,a=e[0];return{type:"phantom",mode:t.mode,body:e0(a)}},htmlBuilder:(r,e)=>{var t=a0(r.body,e.withPhantom(),!1);return y.makeFragment(t)},mathmlBuilder:(r,e)=>{var t=m0(r.body,e);return new M.MathNode("mphantom",t)}});B({type:"hphantom",names:["\\hphantom"],props:{numArgs:1,allowedInText:!0},handler:(r,e)=>{var{parser:t}=r,a=e[0];return{type:"hphantom",mode:t.mode,body:a}},htmlBuilder:(r,e)=>{var t=y.makeSpan([],[G(r.body,e.withPhantom())]);if(t.height=0,t.depth=0,t.children)for(var a=0;a{var t=m0(e0(r.body),e),a=new M.MathNode("mphantom",t),n=new M.MathNode("mpadded",[a]);return n.setAttribute("height","0px"),n.setAttribute("depth","0px"),n}});B({type:"vphantom",names:["\\vphantom"],props:{numArgs:1,allowedInText:!0},handler:(r,e)=>{var{parser:t}=r,a=e[0];return{type:"vphantom",mode:t.mode,body:a}},htmlBuilder:(r,e)=>{var t=y.makeSpan(["inner"],[G(r.body,e.withPhantom())]),a=y.makeSpan(["fix"],[]);return y.makeSpan(["mord","rlap"],[t,a],e)},mathmlBuilder:(r,e)=>{var t=m0(e0(r.body),e),a=new M.MathNode("mphantom",t),n=new M.MathNode("mpadded",[a]);return n.setAttribute("width","0px"),n}});B({type:"raisebox",names:["\\raisebox"],props:{numArgs:2,argTypes:["size","hbox"],allowedInText:!0},handler(r,e){var{parser:t}=r,a=L(e[0],"size").value,n=e[1];return{type:"raisebox",mode:t.mode,dy:a,body:n}},htmlBuilder(r,e){var t=G(r.body,e),a=Q(r.dy,e);return y.makeVList({positionType:"shift",positionData:-a,children:[{type:"elem",elem:t}]},e)},mathmlBuilder(r,e){var t=new M.MathNode("mpadded",[X(r.body,e)]),a=r.dy.number+r.dy.unit;return t.setAttribute("voffset",a),t}});B({type:"internal",names:["\\relax"],props:{numArgs:0,allowedInText:!0,allowedInArgument:!0},handler(r){var{parser:e}=r;return{type:"internal",mode:e.mode}}});B({type:"rule",names:["\\rule"],props:{numArgs:2,numOptionalArgs:1,allowedInText:!0,allowedInMath:!0,argTypes:["size","size","size"]},handler(r,e,t){var{parser:a}=r,n=t[0],s=L(e[0],"size"),l=L(e[1],"size");return{type:"rule",mode:a.mode,shift:n&&L(n,"size").value,width:s.value,height:l.value}},htmlBuilder(r,e){var t=y.makeSpan(["mord","rule"],[],e),a=Q(r.width,e),n=Q(r.height,e),s=r.shift?Q(r.shift,e):0;return t.style.borderRightWidth=T(a),t.style.borderTopWidth=T(n),t.style.bottom=T(s),t.width=a,t.height=n+s,t.depth=-s,t.maxFontSize=n*1.125*e.sizeMultiplier,t},mathmlBuilder(r,e){var t=Q(r.width,e),a=Q(r.height,e),n=r.shift?Q(r.shift,e):0,s=e.color&&e.getColor()||"black",l=new M.MathNode("mspace");l.setAttribute("mathbackground",s),l.setAttribute("width",T(t)),l.setAttribute("height",T(a));var h=new M.MathNode("mpadded",[l]);return n>=0?h.setAttribute("height",T(n)):(h.setAttribute("height",T(n)),h.setAttribute("depth",T(-n))),h.setAttribute("voffset",T(n)),h}});function ma(r,e,t){for(var a=a0(r,e,!1),n=e.sizeMultiplier/t.sizeMultiplier,s=0;s{var t=e.havingSize(r.size);return ma(r.body,t,e)};B({type:"sizing",names:pr,props:{numArgs:0,allowedInText:!0},handler:(r,e)=>{var{breakOnTokenText:t,funcName:a,parser:n}=r,s=n.parseExpression(!1,t);return{type:"sizing",mode:n.mode,size:pr.indexOf(a)+1,body:s}},htmlBuilder:sn,mathmlBuilder:(r,e)=>{var t=e.havingSize(r.size),a=m0(r.body,t),n=new M.MathNode("mstyle",a);return n.setAttribute("mathsize",T(t.sizeMultiplier)),n}});B({type:"smash",names:["\\smash"],props:{numArgs:1,numOptionalArgs:1,allowedInText:!0},handler:(r,e,t)=>{var{parser:a}=r,n=!1,s=!1,l=t[0]&&L(t[0],"ordgroup");if(l)for(var h="",c=0;c{var t=y.makeSpan([],[G(r.body,e)]);if(!r.smashHeight&&!r.smashDepth)return t;if(r.smashHeight&&(t.height=0,t.children))for(var a=0;a{var t=new M.MathNode("mpadded",[X(r.body,e)]);return r.smashHeight&&t.setAttribute("height","0px"),r.smashDepth&&t.setAttribute("depth","0px"),t}});B({type:"sqrt",names:["\\sqrt"],props:{numArgs:1,numOptionalArgs:1},handler(r,e,t){var{parser:a}=r,n=t[0],s=e[0];return{type:"sqrt",mode:a.mode,body:s,index:n}},htmlBuilder(r,e){var t=G(r.body,e.havingCrampedStyle());t.height===0&&(t.height=e.fontMetrics().xHeight),t=y.wrapFragment(t,e);var a=e.fontMetrics(),n=a.defaultRuleThickness,s=n;e.style.idt.height+t.depth+l&&(l=(l+b-t.height-t.depth)/2);var x=c.height-t.height-l-f;t.style.paddingLeft=T(v);var w=y.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:t,wrapperClasses:["svg-align"]},{type:"kern",size:-(t.height+x)},{type:"elem",elem:c},{type:"kern",size:f}]},e);if(r.index){var A=e.havingStyle(E.SCRIPTSCRIPT),q=G(r.index,A,e),_=.6*(w.height-w.depth),D=y.makeVList({positionType:"shift",positionData:-_,children:[{type:"elem",elem:q}]},e),N=y.makeSpan(["root"],[D]);return y.makeSpan(["mord","sqrt"],[N,w],e)}else return y.makeSpan(["mord","sqrt"],[w],e)},mathmlBuilder(r,e){var{body:t,index:a}=r;return a?new M.MathNode("mroot",[X(t,e),X(a,e)]):new M.MathNode("msqrt",[X(t,e)])}});var fr={display:E.DISPLAY,text:E.TEXT,script:E.SCRIPT,scriptscript:E.SCRIPTSCRIPT};B({type:"styling",names:["\\displaystyle","\\textstyle","\\scriptstyle","\\scriptscriptstyle"],props:{numArgs:0,allowedInText:!0,primitive:!0},handler(r,e){var{breakOnTokenText:t,funcName:a,parser:n}=r,s=n.parseExpression(!0,t),l=a.slice(1,a.length-5);return{type:"styling",mode:n.mode,style:l,body:s}},htmlBuilder(r,e){var t=fr[r.style],a=e.havingStyle(t).withFont("");return ma(r.body,a,e)},mathmlBuilder(r,e){var t=fr[r.style],a=e.havingStyle(t),n=m0(r.body,a),s=new M.MathNode("mstyle",n),l={display:["0","true"],text:["0","false"],script:["1","false"],scriptscript:["2","false"]},h=l[r.style];return s.setAttribute("scriptlevel",h[0]),s.setAttribute("displaystyle",h[1]),s}});var on=function(e,t){var a=e.base;if(a)if(a.type==="op"){var n=a.limits&&(t.style.size===E.DISPLAY.size||a.alwaysHandleSupSub);return n?se:null}else if(a.type==="operatorname"){var s=a.alwaysHandleSupSub&&(t.style.size===E.DISPLAY.size||a.limits);return s?ca:null}else{if(a.type==="accent")return O.isCharacterBox(a.base)?_t:null;if(a.type==="horizBrace"){var l=!e.sub;return l===a.isOver?la:null}else return null}else return null};j0({type:"supsub",htmlBuilder(r,e){var t=on(r,e);if(t)return t(r,e);var{base:a,sup:n,sub:s}=r,l=G(a,e),h,c,f=e.fontMetrics(),v=0,b=0,x=a&&O.isCharacterBox(a);if(n){var w=e.havingStyle(e.style.sup());h=G(n,w,e),x||(v=l.height-w.fontMetrics().supDrop*w.sizeMultiplier/e.sizeMultiplier)}if(s){var A=e.havingStyle(e.style.sub());c=G(s,A,e),x||(b=l.depth+A.fontMetrics().subDrop*A.sizeMultiplier/e.sizeMultiplier)}var q;e.style===E.DISPLAY?q=f.sup1:e.style.cramped?q=f.sup3:q=f.sup2;var _=e.sizeMultiplier,D=T(.5/f.ptPerEm/_),N=null;if(c){var $=r.base&&r.base.type==="op"&&r.base.name&&(r.base.name==="\\oiint"||r.base.name==="\\oiiint");(l instanceof c0||$)&&(N=T(-l.italic))}var H;if(h&&c){v=Math.max(v,q,h.depth+.25*f.xHeight),b=Math.max(b,f.sub2);var F=f.defaultRuleThickness,P=4*F;if(v-h.depth-(c.height-b)0&&(v+=V,b-=V)}var j=[{type:"elem",elem:c,shift:b,marginRight:D,marginLeft:N},{type:"elem",elem:h,shift:-v,marginRight:D}];H=y.makeVList({positionType:"individualShift",children:j},e)}else if(c){b=Math.max(b,f.sub1,c.height-.8*f.xHeight);var U=[{type:"elem",elem:c,marginLeft:N,marginRight:D}];H=y.makeVList({positionType:"shift",positionData:b,children:U},e)}else if(h)v=Math.max(v,q,h.depth+.25*f.xHeight),H=y.makeVList({positionType:"shift",positionData:-v,children:[{type:"elem",elem:h,marginRight:D}]},e);else throw new Error("supsub must have either sup or sub.");var D0=bt(l,"right")||"mord";return y.makeSpan([D0],[l,y.makeSpan(["msupsub"],[H])],e)},mathmlBuilder(r,e){var t=!1,a,n;r.base&&r.base.type==="horizBrace"&&(n=!!r.sup,n===r.base.isOver&&(t=!0,a=r.base.isOver)),r.base&&(r.base.type==="op"||r.base.type==="operatorname")&&(r.base.parentIsSupSub=!0);var s=[X(r.base,e)];r.sub&&s.push(X(r.sub,e)),r.sup&&s.push(X(r.sup,e));var l;if(t)l=a?"mover":"munder";else if(r.sub)if(r.sup){var f=r.base;f&&f.type==="op"&&f.limits&&e.style===E.DISPLAY||f&&f.type==="operatorname"&&f.alwaysHandleSupSub&&(e.style===E.DISPLAY||f.limits)?l="munderover":l="msubsup"}else{var c=r.base;c&&c.type==="op"&&c.limits&&(e.style===E.DISPLAY||c.alwaysHandleSupSub)||c&&c.type==="operatorname"&&c.alwaysHandleSupSub&&(c.limits||e.style===E.DISPLAY)?l="munder":l="msub"}else{var h=r.base;h&&h.type==="op"&&h.limits&&(e.style===E.DISPLAY||h.alwaysHandleSupSub)||h&&h.type==="operatorname"&&h.alwaysHandleSupSub&&(h.limits||e.style===E.DISPLAY)?l="mover":l="msup"}return new M.MathNode(l,s)}});j0({type:"atom",htmlBuilder(r,e){return y.mathsym(r.text,r.mode,e,["m"+r.family])},mathmlBuilder(r,e){var t=new M.MathNode("mo",[y0(r.text,r.mode)]);if(r.family==="bin"){var a=Dt(r,e);a==="bold-italic"&&t.setAttribute("mathvariant",a)}else r.family==="punct"?t.setAttribute("separator","true"):(r.family==="open"||r.family==="close")&&t.setAttribute("stretchy","false");return t}});var da={mi:"italic",mn:"normal",mtext:"normal"};j0({type:"mathord",htmlBuilder(r,e){return y.makeOrd(r,e,"mathord")},mathmlBuilder(r,e){var t=new M.MathNode("mi",[y0(r.text,r.mode,e)]),a=Dt(r,e)||"italic";return a!==da[t.type]&&t.setAttribute("mathvariant",a),t}});j0({type:"textord",htmlBuilder(r,e){return y.makeOrd(r,e,"textord")},mathmlBuilder(r,e){var t=y0(r.text,r.mode,e),a=Dt(r,e)||"normal",n;return r.mode==="text"?n=new M.MathNode("mtext",[t]):/[0-9]/.test(r.text)?n=new M.MathNode("mn",[t]):r.text==="\\prime"?n=new M.MathNode("mo",[t]):n=new M.MathNode("mi",[t]),a!==da[n.type]&&n.setAttribute("mathvariant",a),n}});var ct={"\\nobreak":"nobreak","\\allowbreak":"allowbreak"},mt={" ":{},"\\ ":{},"~":{className:"nobreak"},"\\space":{},"\\nobreakspace":{className:"nobreak"}};j0({type:"spacing",htmlBuilder(r,e){if(mt.hasOwnProperty(r.text)){var t=mt[r.text].className||"";if(r.mode==="text"){var a=y.makeOrd(r,e,"textord");return a.classes.push(t),a}else return y.makeSpan(["mspace",t],[y.mathsym(r.text,r.mode,e)],e)}else{if(ct.hasOwnProperty(r.text))return y.makeSpan(["mspace",ct[r.text]],[],e);throw new z('Unknown type of space "'+r.text+'"')}},mathmlBuilder(r,e){var t;if(mt.hasOwnProperty(r.text))t=new M.MathNode("mtext",[new M.TextNode("\xA0")]);else{if(ct.hasOwnProperty(r.text))return new M.MathNode("mspace");throw new z('Unknown type of space "'+r.text+'"')}return t}});var vr=()=>{var r=new M.MathNode("mtd",[]);return r.setAttribute("width","50%"),r};j0({type:"tag",mathmlBuilder(r,e){var t=new M.MathNode("mtable",[new M.MathNode("mtr",[vr(),new M.MathNode("mtd",[V0(r.body,e)]),vr(),new M.MathNode("mtd",[V0(r.tag,e)])])]);return t.setAttribute("width","100%"),t}});var gr={"\\text":void 0,"\\textrm":"textrm","\\textsf":"textsf","\\texttt":"texttt","\\textnormal":"textrm"},br={"\\textbf":"textbf","\\textmd":"textmd"},ln={"\\textit":"textit","\\textup":"textup"},yr=(r,e)=>{var t=r.font;if(t){if(gr[t])return e.withTextFontFamily(gr[t]);if(br[t])return e.withTextFontWeight(br[t]);if(t==="\\emph")return e.fontShape==="textit"?e.withTextFontShape("textup"):e.withTextFontShape("textit")}else return e;return e.withTextFontShape(ln[t])};B({type:"text",names:["\\text","\\textrm","\\textsf","\\texttt","\\textnormal","\\textbf","\\textmd","\\textit","\\textup","\\emph"],props:{numArgs:1,argTypes:["text"],allowedInArgument:!0,allowedInText:!0},handler(r,e){var{parser:t,funcName:a}=r,n=e[0];return{type:"text",mode:t.mode,body:e0(n),font:a}},htmlBuilder(r,e){var t=yr(r,e),a=a0(r.body,t,!0);return y.makeSpan(["mord","text"],a,t)},mathmlBuilder(r,e){var t=yr(r,e);return V0(r.body,t)}});B({type:"underline",names:["\\underline"],props:{numArgs:1,allowedInText:!0},handler(r,e){var{parser:t}=r;return{type:"underline",mode:t.mode,body:e[0]}},htmlBuilder(r,e){var t=G(r.body,e),a=y.makeLineSpan("underline-line",e),n=e.fontMetrics().defaultRuleThickness,s=y.makeVList({positionType:"top",positionData:t.height,children:[{type:"kern",size:n},{type:"elem",elem:a},{type:"kern",size:3*n},{type:"elem",elem:t}]},e);return y.makeSpan(["mord","underline"],[s],e)},mathmlBuilder(r,e){var t=new M.MathNode("mo",[new M.TextNode("\u203E")]);t.setAttribute("stretchy","true");var a=new M.MathNode("munder",[X(r.body,e),t]);return a.setAttribute("accentunder","true"),a}});B({type:"vcenter",names:["\\vcenter"],props:{numArgs:1,argTypes:["original"],allowedInText:!1},handler(r,e){var{parser:t}=r;return{type:"vcenter",mode:t.mode,body:e[0]}},htmlBuilder(r,e){var t=G(r.body,e),a=e.fontMetrics().axisHeight,n=.5*(t.height-a-(t.depth+a));return y.makeVList({positionType:"shift",positionData:n,children:[{type:"elem",elem:t}]},e)},mathmlBuilder(r,e){return new M.MathNode("mpadded",[X(r.body,e)],["vcenter"])}});B({type:"verb",names:["\\verb"],props:{numArgs:0,allowedInText:!0},handler(r,e,t){throw new z("\\verb ended by end of line instead of matching delimiter")},htmlBuilder(r,e){for(var t=xr(r),a=[],n=e.havingStyle(e.style.text()),s=0;sr.body.replace(/ /g,r.star?"\u2423":"\xA0"),P0=Er,pa=`[ \r - ]`,un="\\\\[a-zA-Z@]+",hn="\\\\[^\uD800-\uDFFF]",cn="("+un+")"+pa+"*",mn=`\\\\( -|[ \r ]+ -?)[ \r ]*`,kt="[\u0300-\u036F]",dn=new RegExp(kt+"+$"),pn="("+pa+"+)|"+(mn+"|")+"([!-\\[\\]-\u2027\u202A-\uD7FF\uF900-\uFFFF]"+(kt+"*")+"|[\uD800-\uDBFF][\uDC00-\uDFFF]"+(kt+"*")+"|\\\\verb\\*([^]).*?\\4|\\\\verb([^*a-zA-Z]).*?\\5"+("|"+cn)+("|"+hn+")"),Pe=class{constructor(e,t){this.input=void 0,this.settings=void 0,this.tokenRegex=void 0,this.catcodes=void 0,this.input=e,this.settings=t,this.tokenRegex=new RegExp(pn,"g"),this.catcodes={"%":14,"~":13}}setCatcode(e,t){this.catcodes[e]=t}lex(){var e=this.input,t=this.tokenRegex.lastIndex;if(t===e.length)return new b0("EOF",new d0(this,t,t));var a=this.tokenRegex.exec(e);if(a===null||a.index!==t)throw new z("Unexpected character: '"+e[t]+"'",new b0(e[t],new d0(this,t,t+1)));var n=a[6]||a[3]||(a[2]?"\\ ":" ");if(this.catcodes[n]===14){var s=e.indexOf(` -`,this.tokenRegex.lastIndex);return s===-1?(this.tokenRegex.lastIndex=e.length,this.settings.reportNonstrict("commentAtEnd","% comment has no terminating newline; LaTeX would fail because of commenting the end of math mode (e.g. $)")):this.tokenRegex.lastIndex=s+1,this.lex()}return new b0(n,new d0(this,t,this.tokenRegex.lastIndex))}},Mt=class{constructor(e,t){e===void 0&&(e={}),t===void 0&&(t={}),this.current=void 0,this.builtins=void 0,this.undefStack=void 0,this.current=t,this.builtins=e,this.undefStack=[]}beginGroup(){this.undefStack.push({})}endGroup(){if(this.undefStack.length===0)throw new z("Unbalanced namespace destruction: attempt to pop global namespace; please report this as a bug");var e=this.undefStack.pop();for(var t in e)e.hasOwnProperty(t)&&(e[t]==null?delete this.current[t]:this.current[t]=e[t])}endGroups(){for(;this.undefStack.length>0;)this.endGroup()}has(e){return this.current.hasOwnProperty(e)||this.builtins.hasOwnProperty(e)}get(e){return this.current.hasOwnProperty(e)?this.current[e]:this.builtins[e]}set(e,t,a){if(a===void 0&&(a=!1),a){for(var n=0;n0&&(this.undefStack[this.undefStack.length-1][e]=t)}else{var s=this.undefStack[this.undefStack.length-1];s&&!s.hasOwnProperty(e)&&(s[e]=this.current[e])}t==null?delete this.current[e]:this.current[e]=t}},fn=aa;m("\\noexpand",function(r){var e=r.popToken();return r.isExpandable(e.text)&&(e.noexpand=!0,e.treatAsRelax=!0),{tokens:[e],numArgs:0}});m("\\expandafter",function(r){var e=r.popToken();return r.expandOnce(!0),{tokens:[e],numArgs:0}});m("\\@firstoftwo",function(r){var e=r.consumeArgs(2);return{tokens:e[0],numArgs:0}});m("\\@secondoftwo",function(r){var e=r.consumeArgs(2);return{tokens:e[1],numArgs:0}});m("\\@ifnextchar",function(r){var e=r.consumeArgs(3);r.consumeSpaces();var t=r.future();return e[0].length===1&&e[0][0].text===t.text?{tokens:e[1],numArgs:0}:{tokens:e[2],numArgs:0}});m("\\@ifstar","\\@ifnextchar *{\\@firstoftwo{#1}}");m("\\TextOrMath",function(r){var e=r.consumeArgs(2);return r.mode==="text"?{tokens:e[0],numArgs:0}:{tokens:e[1],numArgs:0}});var wr={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,a:10,A:10,b:11,B:11,c:12,C:12,d:13,D:13,e:14,E:14,f:15,F:15};m("\\char",function(r){var e=r.popToken(),t,a="";if(e.text==="'")t=8,e=r.popToken();else if(e.text==='"')t=16,e=r.popToken();else if(e.text==="`")if(e=r.popToken(),e.text[0]==="\\")a=e.text.charCodeAt(1);else{if(e.text==="EOF")throw new z("\\char` missing argument");a=e.text.charCodeAt(0)}else t=10;if(t){if(a=wr[e.text],a==null||a>=t)throw new z("Invalid base-"+t+" digit "+e.text);for(var n;(n=wr[r.future().text])!=null&&n{var n=r.consumeArg().tokens;if(n.length!==1)throw new z("\\newcommand's first argument must be a macro name");var s=n[0].text,l=r.isDefined(s);if(l&&!e)throw new z("\\newcommand{"+s+"} attempting to redefine "+(s+"; use \\renewcommand"));if(!l&&!t)throw new z("\\renewcommand{"+s+"} when command "+s+" does not yet exist; use \\newcommand");var h=0;if(n=r.consumeArg().tokens,n.length===1&&n[0].text==="["){for(var c="",f=r.expandNextToken();f.text!=="]"&&f.text!=="EOF";)c+=f.text,f=r.expandNextToken();if(!c.match(/^\s*[0-9]+\s*$/))throw new z("Invalid number of arguments: "+c);h=parseInt(c),n=r.consumeArg().tokens}return l&&a||r.macros.set(s,{tokens:n,numArgs:h}),""};m("\\newcommand",r=>Ft(r,!1,!0,!1));m("\\renewcommand",r=>Ft(r,!0,!1,!1));m("\\providecommand",r=>Ft(r,!0,!0,!0));m("\\message",r=>{var e=r.consumeArgs(1)[0];return console.log(e.reverse().map(t=>t.text).join("")),""});m("\\errmessage",r=>{var e=r.consumeArgs(1)[0];return console.error(e.reverse().map(t=>t.text).join("")),""});m("\\show",r=>{var e=r.popToken(),t=e.text;return console.log(e,r.macros.get(t),P0[t],Y.math[t],Y.text[t]),""});m("\\bgroup","{");m("\\egroup","}");m("~","\\nobreakspace");m("\\lq","`");m("\\rq","'");m("\\aa","\\r a");m("\\AA","\\r A");m("\\textcopyright","\\html@mathml{\\textcircled{c}}{\\char`\xA9}");m("\\copyright","\\TextOrMath{\\textcopyright}{\\text{\\textcopyright}}");m("\\textregistered","\\html@mathml{\\textcircled{\\scriptsize R}}{\\char`\xAE}");m("\u212C","\\mathscr{B}");m("\u2130","\\mathscr{E}");m("\u2131","\\mathscr{F}");m("\u210B","\\mathscr{H}");m("\u2110","\\mathscr{I}");m("\u2112","\\mathscr{L}");m("\u2133","\\mathscr{M}");m("\u211B","\\mathscr{R}");m("\u212D","\\mathfrak{C}");m("\u210C","\\mathfrak{H}");m("\u2128","\\mathfrak{Z}");m("\\Bbbk","\\Bbb{k}");m("\xB7","\\cdotp");m("\\llap","\\mathllap{\\textrm{#1}}");m("\\rlap","\\mathrlap{\\textrm{#1}}");m("\\clap","\\mathclap{\\textrm{#1}}");m("\\mathstrut","\\vphantom{(}");m("\\underbar","\\underline{\\text{#1}}");m("\\not",'\\html@mathml{\\mathrel{\\mathrlap\\@not}}{\\char"338}');m("\\neq","\\html@mathml{\\mathrel{\\not=}}{\\mathrel{\\char`\u2260}}");m("\\ne","\\neq");m("\u2260","\\neq");m("\\notin","\\html@mathml{\\mathrel{{\\in}\\mathllap{/\\mskip1mu}}}{\\mathrel{\\char`\u2209}}");m("\u2209","\\notin");m("\u2258","\\html@mathml{\\mathrel{=\\kern{-1em}\\raisebox{0.4em}{$\\scriptsize\\frown$}}}{\\mathrel{\\char`\u2258}}");m("\u2259","\\html@mathml{\\stackrel{\\tiny\\wedge}{=}}{\\mathrel{\\char`\u2258}}");m("\u225A","\\html@mathml{\\stackrel{\\tiny\\vee}{=}}{\\mathrel{\\char`\u225A}}");m("\u225B","\\html@mathml{\\stackrel{\\scriptsize\\star}{=}}{\\mathrel{\\char`\u225B}}");m("\u225D","\\html@mathml{\\stackrel{\\tiny\\mathrm{def}}{=}}{\\mathrel{\\char`\u225D}}");m("\u225E","\\html@mathml{\\stackrel{\\tiny\\mathrm{m}}{=}}{\\mathrel{\\char`\u225E}}");m("\u225F","\\html@mathml{\\stackrel{\\tiny?}{=}}{\\mathrel{\\char`\u225F}}");m("\u27C2","\\perp");m("\u203C","\\mathclose{!\\mkern-0.8mu!}");m("\u220C","\\notni");m("\u231C","\\ulcorner");m("\u231D","\\urcorner");m("\u231E","\\llcorner");m("\u231F","\\lrcorner");m("\xA9","\\copyright");m("\xAE","\\textregistered");m("\uFE0F","\\textregistered");m("\\ulcorner",'\\html@mathml{\\@ulcorner}{\\mathop{\\char"231c}}');m("\\urcorner",'\\html@mathml{\\@urcorner}{\\mathop{\\char"231d}}');m("\\llcorner",'\\html@mathml{\\@llcorner}{\\mathop{\\char"231e}}');m("\\lrcorner",'\\html@mathml{\\@lrcorner}{\\mathop{\\char"231f}}');m("\\vdots","{\\varvdots\\rule{0pt}{15pt}}");m("\u22EE","\\vdots");m("\\varGamma","\\mathit{\\Gamma}");m("\\varDelta","\\mathit{\\Delta}");m("\\varTheta","\\mathit{\\Theta}");m("\\varLambda","\\mathit{\\Lambda}");m("\\varXi","\\mathit{\\Xi}");m("\\varPi","\\mathit{\\Pi}");m("\\varSigma","\\mathit{\\Sigma}");m("\\varUpsilon","\\mathit{\\Upsilon}");m("\\varPhi","\\mathit{\\Phi}");m("\\varPsi","\\mathit{\\Psi}");m("\\varOmega","\\mathit{\\Omega}");m("\\substack","\\begin{subarray}{c}#1\\end{subarray}");m("\\colon","\\nobreak\\mskip2mu\\mathpunct{}\\mathchoice{\\mkern-3mu}{\\mkern-3mu}{}{}{:}\\mskip6mu\\relax");m("\\boxed","\\fbox{$\\displaystyle{#1}$}");m("\\iff","\\DOTSB\\;\\Longleftrightarrow\\;");m("\\implies","\\DOTSB\\;\\Longrightarrow\\;");m("\\impliedby","\\DOTSB\\;\\Longleftarrow\\;");m("\\dddot","{\\overset{\\raisebox{-0.1ex}{\\normalsize ...}}{#1}}");m("\\ddddot","{\\overset{\\raisebox{-0.1ex}{\\normalsize ....}}{#1}}");var Sr={",":"\\dotsc","\\not":"\\dotsb","+":"\\dotsb","=":"\\dotsb","<":"\\dotsb",">":"\\dotsb","-":"\\dotsb","*":"\\dotsb",":":"\\dotsb","\\DOTSB":"\\dotsb","\\coprod":"\\dotsb","\\bigvee":"\\dotsb","\\bigwedge":"\\dotsb","\\biguplus":"\\dotsb","\\bigcap":"\\dotsb","\\bigcup":"\\dotsb","\\prod":"\\dotsb","\\sum":"\\dotsb","\\bigotimes":"\\dotsb","\\bigoplus":"\\dotsb","\\bigodot":"\\dotsb","\\bigsqcup":"\\dotsb","\\And":"\\dotsb","\\longrightarrow":"\\dotsb","\\Longrightarrow":"\\dotsb","\\longleftarrow":"\\dotsb","\\Longleftarrow":"\\dotsb","\\longleftrightarrow":"\\dotsb","\\Longleftrightarrow":"\\dotsb","\\mapsto":"\\dotsb","\\longmapsto":"\\dotsb","\\hookrightarrow":"\\dotsb","\\doteq":"\\dotsb","\\mathbin":"\\dotsb","\\mathrel":"\\dotsb","\\relbar":"\\dotsb","\\Relbar":"\\dotsb","\\xrightarrow":"\\dotsb","\\xleftarrow":"\\dotsb","\\DOTSI":"\\dotsi","\\int":"\\dotsi","\\oint":"\\dotsi","\\iint":"\\dotsi","\\iiint":"\\dotsi","\\iiiint":"\\dotsi","\\idotsint":"\\dotsi","\\DOTSX":"\\dotsx"};m("\\dots",function(r){var e="\\dotso",t=r.expandAfterFuture().text;return t in Sr?e=Sr[t]:(t.slice(0,4)==="\\not"||t in Y.math&&O.contains(["bin","rel"],Y.math[t].group))&&(e="\\dotsb"),e});var Ht={")":!0,"]":!0,"\\rbrack":!0,"\\}":!0,"\\rbrace":!0,"\\rangle":!0,"\\rceil":!0,"\\rfloor":!0,"\\rgroup":!0,"\\rmoustache":!0,"\\right":!0,"\\bigr":!0,"\\biggr":!0,"\\Bigr":!0,"\\Biggr":!0,$:!0,";":!0,".":!0,",":!0};m("\\dotso",function(r){var e=r.future().text;return e in Ht?"\\ldots\\,":"\\ldots"});m("\\dotsc",function(r){var e=r.future().text;return e in Ht&&e!==","?"\\ldots\\,":"\\ldots"});m("\\cdots",function(r){var e=r.future().text;return e in Ht?"\\@cdots\\,":"\\@cdots"});m("\\dotsb","\\cdots");m("\\dotsm","\\cdots");m("\\dotsi","\\!\\cdots");m("\\dotsx","\\ldots\\,");m("\\DOTSI","\\relax");m("\\DOTSB","\\relax");m("\\DOTSX","\\relax");m("\\tmspace","\\TextOrMath{\\kern#1#3}{\\mskip#1#2}\\relax");m("\\,","\\tmspace+{3mu}{.1667em}");m("\\thinspace","\\,");m("\\>","\\mskip{4mu}");m("\\:","\\tmspace+{4mu}{.2222em}");m("\\medspace","\\:");m("\\;","\\tmspace+{5mu}{.2777em}");m("\\thickspace","\\;");m("\\!","\\tmspace-{3mu}{.1667em}");m("\\negthinspace","\\!");m("\\negmedspace","\\tmspace-{4mu}{.2222em}");m("\\negthickspace","\\tmspace-{5mu}{.277em}");m("\\enspace","\\kern.5em ");m("\\enskip","\\hskip.5em\\relax");m("\\quad","\\hskip1em\\relax");m("\\qquad","\\hskip2em\\relax");m("\\tag","\\@ifstar\\tag@literal\\tag@paren");m("\\tag@paren","\\tag@literal{({#1})}");m("\\tag@literal",r=>{if(r.macros.get("\\df@tag"))throw new z("Multiple \\tag");return"\\gdef\\df@tag{\\text{#1}}"});m("\\bmod","\\mathchoice{\\mskip1mu}{\\mskip1mu}{\\mskip5mu}{\\mskip5mu}\\mathbin{\\rm mod}\\mathchoice{\\mskip1mu}{\\mskip1mu}{\\mskip5mu}{\\mskip5mu}");m("\\pod","\\allowbreak\\mathchoice{\\mkern18mu}{\\mkern8mu}{\\mkern8mu}{\\mkern8mu}(#1)");m("\\pmod","\\pod{{\\rm mod}\\mkern6mu#1}");m("\\mod","\\allowbreak\\mathchoice{\\mkern18mu}{\\mkern12mu}{\\mkern12mu}{\\mkern12mu}{\\rm mod}\\,\\,#1");m("\\newline","\\\\\\relax");m("\\TeX","\\textrm{\\html@mathml{T\\kern-.1667em\\raisebox{-.5ex}{E}\\kern-.125emX}{TeX}}");var fa=T(z0["Main-Regular"][84][1]-.7*z0["Main-Regular"][65][1]);m("\\LaTeX","\\textrm{\\html@mathml{"+("L\\kern-.36em\\raisebox{"+fa+"}{\\scriptstyle A}")+"\\kern-.15em\\TeX}{LaTeX}}");m("\\KaTeX","\\textrm{\\html@mathml{"+("K\\kern-.17em\\raisebox{"+fa+"}{\\scriptstyle A}")+"\\kern-.15em\\TeX}{KaTeX}}");m("\\hspace","\\@ifstar\\@hspacer\\@hspace");m("\\@hspace","\\hskip #1\\relax");m("\\@hspacer","\\rule{0pt}{0pt}\\hskip #1\\relax");m("\\ordinarycolon",":");m("\\vcentcolon","\\mathrel{\\mathop\\ordinarycolon}");m("\\dblcolon",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-.9mu}\\vcentcolon}}{\\mathop{\\char"2237}}');m("\\coloneqq",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}=}}{\\mathop{\\char"2254}}');m("\\Coloneqq",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}=}}{\\mathop{\\char"2237\\char"3d}}');m("\\coloneq",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\mathrel{-}}}{\\mathop{\\char"3a\\char"2212}}');m("\\Coloneq",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\mathrel{-}}}{\\mathop{\\char"2237\\char"2212}}');m("\\eqqcolon",'\\html@mathml{\\mathrel{=\\mathrel{\\mkern-1.2mu}\\vcentcolon}}{\\mathop{\\char"2255}}');m("\\Eqqcolon",'\\html@mathml{\\mathrel{=\\mathrel{\\mkern-1.2mu}\\dblcolon}}{\\mathop{\\char"3d\\char"2237}}');m("\\eqcolon",'\\html@mathml{\\mathrel{\\mathrel{-}\\mathrel{\\mkern-1.2mu}\\vcentcolon}}{\\mathop{\\char"2239}}');m("\\Eqcolon",'\\html@mathml{\\mathrel{\\mathrel{-}\\mathrel{\\mkern-1.2mu}\\dblcolon}}{\\mathop{\\char"2212\\char"2237}}');m("\\colonapprox",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\approx}}{\\mathop{\\char"3a\\char"2248}}');m("\\Colonapprox",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\approx}}{\\mathop{\\char"2237\\char"2248}}');m("\\colonsim",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\sim}}{\\mathop{\\char"3a\\char"223c}}');m("\\Colonsim",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\sim}}{\\mathop{\\char"2237\\char"223c}}');m("\u2237","\\dblcolon");m("\u2239","\\eqcolon");m("\u2254","\\coloneqq");m("\u2255","\\eqqcolon");m("\u2A74","\\Coloneqq");m("\\ratio","\\vcentcolon");m("\\coloncolon","\\dblcolon");m("\\colonequals","\\coloneqq");m("\\coloncolonequals","\\Coloneqq");m("\\equalscolon","\\eqqcolon");m("\\equalscoloncolon","\\Eqqcolon");m("\\colonminus","\\coloneq");m("\\coloncolonminus","\\Coloneq");m("\\minuscolon","\\eqcolon");m("\\minuscoloncolon","\\Eqcolon");m("\\coloncolonapprox","\\Colonapprox");m("\\coloncolonsim","\\Colonsim");m("\\simcolon","\\mathrel{\\sim\\mathrel{\\mkern-1.2mu}\\vcentcolon}");m("\\simcoloncolon","\\mathrel{\\sim\\mathrel{\\mkern-1.2mu}\\dblcolon}");m("\\approxcolon","\\mathrel{\\approx\\mathrel{\\mkern-1.2mu}\\vcentcolon}");m("\\approxcoloncolon","\\mathrel{\\approx\\mathrel{\\mkern-1.2mu}\\dblcolon}");m("\\notni","\\html@mathml{\\not\\ni}{\\mathrel{\\char`\u220C}}");m("\\limsup","\\DOTSB\\operatorname*{lim\\,sup}");m("\\liminf","\\DOTSB\\operatorname*{lim\\,inf}");m("\\injlim","\\DOTSB\\operatorname*{inj\\,lim}");m("\\projlim","\\DOTSB\\operatorname*{proj\\,lim}");m("\\varlimsup","\\DOTSB\\operatorname*{\\overline{lim}}");m("\\varliminf","\\DOTSB\\operatorname*{\\underline{lim}}");m("\\varinjlim","\\DOTSB\\operatorname*{\\underrightarrow{lim}}");m("\\varprojlim","\\DOTSB\\operatorname*{\\underleftarrow{lim}}");m("\\gvertneqq","\\html@mathml{\\@gvertneqq}{\u2269}");m("\\lvertneqq","\\html@mathml{\\@lvertneqq}{\u2268}");m("\\ngeqq","\\html@mathml{\\@ngeqq}{\u2271}");m("\\ngeqslant","\\html@mathml{\\@ngeqslant}{\u2271}");m("\\nleqq","\\html@mathml{\\@nleqq}{\u2270}");m("\\nleqslant","\\html@mathml{\\@nleqslant}{\u2270}");m("\\nshortmid","\\html@mathml{\\@nshortmid}{\u2224}");m("\\nshortparallel","\\html@mathml{\\@nshortparallel}{\u2226}");m("\\nsubseteqq","\\html@mathml{\\@nsubseteqq}{\u2288}");m("\\nsupseteqq","\\html@mathml{\\@nsupseteqq}{\u2289}");m("\\varsubsetneq","\\html@mathml{\\@varsubsetneq}{\u228A}");m("\\varsubsetneqq","\\html@mathml{\\@varsubsetneqq}{\u2ACB}");m("\\varsupsetneq","\\html@mathml{\\@varsupsetneq}{\u228B}");m("\\varsupsetneqq","\\html@mathml{\\@varsupsetneqq}{\u2ACC}");m("\\imath","\\html@mathml{\\@imath}{\u0131}");m("\\jmath","\\html@mathml{\\@jmath}{\u0237}");m("\\llbracket","\\html@mathml{\\mathopen{[\\mkern-3.2mu[}}{\\mathopen{\\char`\u27E6}}");m("\\rrbracket","\\html@mathml{\\mathclose{]\\mkern-3.2mu]}}{\\mathclose{\\char`\u27E7}}");m("\u27E6","\\llbracket");m("\u27E7","\\rrbracket");m("\\lBrace","\\html@mathml{\\mathopen{\\{\\mkern-3.2mu[}}{\\mathopen{\\char`\u2983}}");m("\\rBrace","\\html@mathml{\\mathclose{]\\mkern-3.2mu\\}}}{\\mathclose{\\char`\u2984}}");m("\u2983","\\lBrace");m("\u2984","\\rBrace");m("\\minuso","\\mathbin{\\html@mathml{{\\mathrlap{\\mathchoice{\\kern{0.145em}}{\\kern{0.145em}}{\\kern{0.1015em}}{\\kern{0.0725em}}\\circ}{-}}}{\\char`\u29B5}}");m("\u29B5","\\minuso");m("\\darr","\\downarrow");m("\\dArr","\\Downarrow");m("\\Darr","\\Downarrow");m("\\lang","\\langle");m("\\rang","\\rangle");m("\\uarr","\\uparrow");m("\\uArr","\\Uparrow");m("\\Uarr","\\Uparrow");m("\\N","\\mathbb{N}");m("\\R","\\mathbb{R}");m("\\Z","\\mathbb{Z}");m("\\alef","\\aleph");m("\\alefsym","\\aleph");m("\\Alpha","\\mathrm{A}");m("\\Beta","\\mathrm{B}");m("\\bull","\\bullet");m("\\Chi","\\mathrm{X}");m("\\clubs","\\clubsuit");m("\\cnums","\\mathbb{C}");m("\\Complex","\\mathbb{C}");m("\\Dagger","\\ddagger");m("\\diamonds","\\diamondsuit");m("\\empty","\\emptyset");m("\\Epsilon","\\mathrm{E}");m("\\Eta","\\mathrm{H}");m("\\exist","\\exists");m("\\harr","\\leftrightarrow");m("\\hArr","\\Leftrightarrow");m("\\Harr","\\Leftrightarrow");m("\\hearts","\\heartsuit");m("\\image","\\Im");m("\\infin","\\infty");m("\\Iota","\\mathrm{I}");m("\\isin","\\in");m("\\Kappa","\\mathrm{K}");m("\\larr","\\leftarrow");m("\\lArr","\\Leftarrow");m("\\Larr","\\Leftarrow");m("\\lrarr","\\leftrightarrow");m("\\lrArr","\\Leftrightarrow");m("\\Lrarr","\\Leftrightarrow");m("\\Mu","\\mathrm{M}");m("\\natnums","\\mathbb{N}");m("\\Nu","\\mathrm{N}");m("\\Omicron","\\mathrm{O}");m("\\plusmn","\\pm");m("\\rarr","\\rightarrow");m("\\rArr","\\Rightarrow");m("\\Rarr","\\Rightarrow");m("\\real","\\Re");m("\\reals","\\mathbb{R}");m("\\Reals","\\mathbb{R}");m("\\Rho","\\mathrm{P}");m("\\sdot","\\cdot");m("\\sect","\\S");m("\\spades","\\spadesuit");m("\\sub","\\subset");m("\\sube","\\subseteq");m("\\supe","\\supseteq");m("\\Tau","\\mathrm{T}");m("\\thetasym","\\vartheta");m("\\weierp","\\wp");m("\\Zeta","\\mathrm{Z}");m("\\argmin","\\DOTSB\\operatorname*{arg\\,min}");m("\\argmax","\\DOTSB\\operatorname*{arg\\,max}");m("\\plim","\\DOTSB\\mathop{\\operatorname{plim}}\\limits");m("\\bra","\\mathinner{\\langle{#1}|}");m("\\ket","\\mathinner{|{#1}\\rangle}");m("\\braket","\\mathinner{\\langle{#1}\\rangle}");m("\\Bra","\\left\\langle#1\\right|");m("\\Ket","\\left|#1\\right\\rangle");var va=r=>e=>{var t=e.consumeArg().tokens,a=e.consumeArg().tokens,n=e.consumeArg().tokens,s=e.consumeArg().tokens,l=e.macros.get("|"),h=e.macros.get("\\|");e.macros.beginGroup();var c=b=>x=>{r&&(x.macros.set("|",l),n.length&&x.macros.set("\\|",h));var w=b;if(!b&&n.length){var A=x.future();A.text==="|"&&(x.popToken(),w=!0)}return{tokens:w?n:a,numArgs:0}};e.macros.set("|",c(!1)),n.length&&e.macros.set("\\|",c(!0));var f=e.consumeArg().tokens,v=e.expandTokens([...s,...f,...t]);return e.macros.endGroup(),{tokens:v.reverse(),numArgs:0}};m("\\bra@ket",va(!1));m("\\bra@set",va(!0));m("\\Braket","\\bra@ket{\\left\\langle}{\\,\\middle\\vert\\,}{\\,\\middle\\vert\\,}{\\right\\rangle}");m("\\Set","\\bra@set{\\left\\{\\:}{\\;\\middle\\vert\\;}{\\;\\middle\\Vert\\;}{\\:\\right\\}}");m("\\set","\\bra@set{\\{\\,}{\\mid}{}{\\,\\}}");m("\\angln","{\\angl n}");m("\\blue","\\textcolor{##6495ed}{#1}");m("\\orange","\\textcolor{##ffa500}{#1}");m("\\pink","\\textcolor{##ff00af}{#1}");m("\\red","\\textcolor{##df0030}{#1}");m("\\green","\\textcolor{##28ae7b}{#1}");m("\\gray","\\textcolor{gray}{#1}");m("\\purple","\\textcolor{##9d38bd}{#1}");m("\\blueA","\\textcolor{##ccfaff}{#1}");m("\\blueB","\\textcolor{##80f6ff}{#1}");m("\\blueC","\\textcolor{##63d9ea}{#1}");m("\\blueD","\\textcolor{##11accd}{#1}");m("\\blueE","\\textcolor{##0c7f99}{#1}");m("\\tealA","\\textcolor{##94fff5}{#1}");m("\\tealB","\\textcolor{##26edd5}{#1}");m("\\tealC","\\textcolor{##01d1c1}{#1}");m("\\tealD","\\textcolor{##01a995}{#1}");m("\\tealE","\\textcolor{##208170}{#1}");m("\\greenA","\\textcolor{##b6ffb0}{#1}");m("\\greenB","\\textcolor{##8af281}{#1}");m("\\greenC","\\textcolor{##74cf70}{#1}");m("\\greenD","\\textcolor{##1fab54}{#1}");m("\\greenE","\\textcolor{##0d923f}{#1}");m("\\goldA","\\textcolor{##ffd0a9}{#1}");m("\\goldB","\\textcolor{##ffbb71}{#1}");m("\\goldC","\\textcolor{##ff9c39}{#1}");m("\\goldD","\\textcolor{##e07d10}{#1}");m("\\goldE","\\textcolor{##a75a05}{#1}");m("\\redA","\\textcolor{##fca9a9}{#1}");m("\\redB","\\textcolor{##ff8482}{#1}");m("\\redC","\\textcolor{##f9685d}{#1}");m("\\redD","\\textcolor{##e84d39}{#1}");m("\\redE","\\textcolor{##bc2612}{#1}");m("\\maroonA","\\textcolor{##ffbde0}{#1}");m("\\maroonB","\\textcolor{##ff92c6}{#1}");m("\\maroonC","\\textcolor{##ed5fa6}{#1}");m("\\maroonD","\\textcolor{##ca337c}{#1}");m("\\maroonE","\\textcolor{##9e034e}{#1}");m("\\purpleA","\\textcolor{##ddd7ff}{#1}");m("\\purpleB","\\textcolor{##c6b9fc}{#1}");m("\\purpleC","\\textcolor{##aa87ff}{#1}");m("\\purpleD","\\textcolor{##7854ab}{#1}");m("\\purpleE","\\textcolor{##543b78}{#1}");m("\\mintA","\\textcolor{##f5f9e8}{#1}");m("\\mintB","\\textcolor{##edf2df}{#1}");m("\\mintC","\\textcolor{##e0e5cc}{#1}");m("\\grayA","\\textcolor{##f6f7f7}{#1}");m("\\grayB","\\textcolor{##f0f1f2}{#1}");m("\\grayC","\\textcolor{##e3e5e6}{#1}");m("\\grayD","\\textcolor{##d6d8da}{#1}");m("\\grayE","\\textcolor{##babec2}{#1}");m("\\grayF","\\textcolor{##888d93}{#1}");m("\\grayG","\\textcolor{##626569}{#1}");m("\\grayH","\\textcolor{##3b3e40}{#1}");m("\\grayI","\\textcolor{##21242c}{#1}");m("\\kaBlue","\\textcolor{##314453}{#1}");m("\\kaGreen","\\textcolor{##71B307}{#1}");var ga={"^":!0,_:!0,"\\limits":!0,"\\nolimits":!0},zt=class{constructor(e,t,a){this.settings=void 0,this.expansionCount=void 0,this.lexer=void 0,this.macros=void 0,this.stack=void 0,this.mode=void 0,this.settings=t,this.expansionCount=0,this.feed(e),this.macros=new Mt(fn,t.macros),this.mode=a,this.stack=[]}feed(e){this.lexer=new Pe(e,this.settings)}switchMode(e){this.mode=e}beginGroup(){this.macros.beginGroup()}endGroup(){this.macros.endGroup()}endGroups(){this.macros.endGroups()}future(){return this.stack.length===0&&this.pushToken(this.lexer.lex()),this.stack[this.stack.length-1]}popToken(){return this.future(),this.stack.pop()}pushToken(e){this.stack.push(e)}pushTokens(e){this.stack.push(...e)}scanArgument(e){var t,a,n;if(e){if(this.consumeSpaces(),this.future().text!=="[")return null;t=this.popToken(),{tokens:n,end:a}=this.consumeArg(["]"])}else({tokens:n,start:t,end:a}=this.consumeArg());return this.pushToken(new b0("EOF",a.loc)),this.pushTokens(n),t.range(a,"")}consumeSpaces(){for(;;){var e=this.future();if(e.text===" ")this.stack.pop();else break}}consumeArg(e){var t=[],a=e&&e.length>0;a||this.consumeSpaces();var n=this.future(),s,l=0,h=0;do{if(s=this.popToken(),t.push(s),s.text==="{")++l;else if(s.text==="}"){if(--l,l===-1)throw new z("Extra }",s)}else if(s.text==="EOF")throw new z("Unexpected end of input in a macro argument, expected '"+(e&&a?e[h]:"}")+"'",s);if(e&&a)if((l===0||l===1&&e[h]==="{")&&s.text===e[h]){if(++h,h===e.length){t.splice(-h,h);break}}else h=0}while(l!==0||a);return n.text==="{"&&t[t.length-1].text==="}"&&(t.pop(),t.shift()),t.reverse(),{tokens:t,start:n,end:s}}consumeArgs(e,t){if(t){if(t.length!==e+1)throw new z("The length of delimiters doesn't match the number of args!");for(var a=t[0],n=0;nthis.settings.maxExpand)throw new z("Too many expansions: infinite loop or need to increase maxExpand setting")}expandOnce(e){var t=this.popToken(),a=t.text,n=t.noexpand?null:this._getExpansion(a);if(n==null||e&&n.unexpandable){if(e&&n==null&&a[0]==="\\"&&!this.isDefined(a))throw new z("Undefined control sequence: "+a);return this.pushToken(t),!1}this.countExpansion(1);var s=n.tokens,l=this.consumeArgs(n.numArgs,n.delimiters);if(n.numArgs){s=s.slice();for(var h=s.length-1;h>=0;--h){var c=s[h];if(c.text==="#"){if(h===0)throw new z("Incomplete placeholder at end of macro body",c);if(c=s[--h],c.text==="#")s.splice(h+1,1);else if(/^[1-9]$/.test(c.text))s.splice(h,2,...l[+c.text-1]);else throw new z("Not a valid argument number",c)}}}return this.pushTokens(s),s.length}expandAfterFuture(){return this.expandOnce(),this.future()}expandNextToken(){for(;;)if(this.expandOnce()===!1){var e=this.stack.pop();return e.treatAsRelax&&(e.text="\\relax"),e}throw new Error}expandMacro(e){return this.macros.has(e)?this.expandTokens([new b0(e)]):void 0}expandTokens(e){var t=[],a=this.stack.length;for(this.pushTokens(e);this.stack.length>a;)if(this.expandOnce(!0)===!1){var n=this.stack.pop();n.treatAsRelax&&(n.noexpand=!1,n.treatAsRelax=!1),t.push(n)}return this.countExpansion(t.length),t}expandMacroAsText(e){var t=this.expandMacro(e);return t&&t.map(a=>a.text).join("")}_getExpansion(e){var t=this.macros.get(e);if(t==null)return t;if(e.length===1){var a=this.lexer.catcodes[e];if(a!=null&&a!==13)return}var n=typeof t=="function"?t(this):t;if(typeof n=="string"){var s=0;if(n.indexOf("#")!==-1)for(var l=n.replace(/##/g,"");l.indexOf("#"+(s+1))!==-1;)++s;for(var h=new Pe(n,this.settings),c=[],f=h.lex();f.text!=="EOF";)c.push(f),f=h.lex();c.reverse();var v={tokens:c,numArgs:s};return v}return n}isDefined(e){return this.macros.has(e)||P0.hasOwnProperty(e)||Y.math.hasOwnProperty(e)||Y.text.hasOwnProperty(e)||ga.hasOwnProperty(e)}isExpandable(e){var t=this.macros.get(e);return t!=null?typeof t=="string"||typeof t=="function"||!t.unexpandable:P0.hasOwnProperty(e)&&!P0[e].primitive}},kr=/^[₊₋₌₍₎₀₁₂₃₄₅₆₇₈₉ₐₑₕᵢⱼₖₗₘₙₒₚᵣₛₜᵤᵥₓᵦᵧᵨᵩᵪ]/,Ne=Object.freeze({"\u208A":"+","\u208B":"-","\u208C":"=","\u208D":"(","\u208E":")","\u2080":"0","\u2081":"1","\u2082":"2","\u2083":"3","\u2084":"4","\u2085":"5","\u2086":"6","\u2087":"7","\u2088":"8","\u2089":"9","\u2090":"a","\u2091":"e","\u2095":"h","\u1D62":"i","\u2C7C":"j","\u2096":"k","\u2097":"l","\u2098":"m","\u2099":"n","\u2092":"o","\u209A":"p","\u1D63":"r","\u209B":"s","\u209C":"t","\u1D64":"u","\u1D65":"v","\u2093":"x","\u1D66":"\u03B2","\u1D67":"\u03B3","\u1D68":"\u03C1","\u1D69":"\u03D5","\u1D6A":"\u03C7","\u207A":"+","\u207B":"-","\u207C":"=","\u207D":"(","\u207E":")","\u2070":"0","\xB9":"1","\xB2":"2","\xB3":"3","\u2074":"4","\u2075":"5","\u2076":"6","\u2077":"7","\u2078":"8","\u2079":"9","\u1D2C":"A","\u1D2E":"B","\u1D30":"D","\u1D31":"E","\u1D33":"G","\u1D34":"H","\u1D35":"I","\u1D36":"J","\u1D37":"K","\u1D38":"L","\u1D39":"M","\u1D3A":"N","\u1D3C":"O","\u1D3E":"P","\u1D3F":"R","\u1D40":"T","\u1D41":"U","\u2C7D":"V","\u1D42":"W","\u1D43":"a","\u1D47":"b","\u1D9C":"c","\u1D48":"d","\u1D49":"e","\u1DA0":"f","\u1D4D":"g",\u02B0:"h","\u2071":"i",\u02B2:"j","\u1D4F":"k",\u02E1:"l","\u1D50":"m",\u207F:"n","\u1D52":"o","\u1D56":"p",\u02B3:"r",\u02E2:"s","\u1D57":"t","\u1D58":"u","\u1D5B":"v",\u02B7:"w",\u02E3:"x",\u02B8:"y","\u1DBB":"z","\u1D5D":"\u03B2","\u1D5E":"\u03B3","\u1D5F":"\u03B4","\u1D60":"\u03D5","\u1D61":"\u03C7","\u1DBF":"\u03B8"}),dt={"\u0301":{text:"\\'",math:"\\acute"},"\u0300":{text:"\\`",math:"\\grave"},"\u0308":{text:'\\"',math:"\\ddot"},"\u0303":{text:"\\~",math:"\\tilde"},"\u0304":{text:"\\=",math:"\\bar"},"\u0306":{text:"\\u",math:"\\breve"},"\u030C":{text:"\\v",math:"\\check"},"\u0302":{text:"\\^",math:"\\hat"},"\u0307":{text:"\\.",math:"\\dot"},"\u030A":{text:"\\r",math:"\\mathring"},"\u030B":{text:"\\H"},"\u0327":{text:"\\c"}},Mr={\u00E1:"a\u0301",\u00E0:"a\u0300",\u00E4:"a\u0308",\u01DF:"a\u0308\u0304",\u00E3:"a\u0303",\u0101:"a\u0304",\u0103:"a\u0306",\u1EAF:"a\u0306\u0301",\u1EB1:"a\u0306\u0300",\u1EB5:"a\u0306\u0303",\u01CE:"a\u030C",\u00E2:"a\u0302",\u1EA5:"a\u0302\u0301",\u1EA7:"a\u0302\u0300",\u1EAB:"a\u0302\u0303",\u0227:"a\u0307",\u01E1:"a\u0307\u0304",\u00E5:"a\u030A",\u01FB:"a\u030A\u0301",\u1E03:"b\u0307",\u0107:"c\u0301",\u1E09:"c\u0327\u0301",\u010D:"c\u030C",\u0109:"c\u0302",\u010B:"c\u0307",\u00E7:"c\u0327",\u010F:"d\u030C",\u1E0B:"d\u0307",\u1E11:"d\u0327",\u00E9:"e\u0301",\u00E8:"e\u0300",\u00EB:"e\u0308",\u1EBD:"e\u0303",\u0113:"e\u0304",\u1E17:"e\u0304\u0301",\u1E15:"e\u0304\u0300",\u0115:"e\u0306",\u1E1D:"e\u0327\u0306",\u011B:"e\u030C",\u00EA:"e\u0302",\u1EBF:"e\u0302\u0301",\u1EC1:"e\u0302\u0300",\u1EC5:"e\u0302\u0303",\u0117:"e\u0307",\u0229:"e\u0327",\u1E1F:"f\u0307",\u01F5:"g\u0301",\u1E21:"g\u0304",\u011F:"g\u0306",\u01E7:"g\u030C",\u011D:"g\u0302",\u0121:"g\u0307",\u0123:"g\u0327",\u1E27:"h\u0308",\u021F:"h\u030C",\u0125:"h\u0302",\u1E23:"h\u0307",\u1E29:"h\u0327",\u00ED:"i\u0301",\u00EC:"i\u0300",\u00EF:"i\u0308",\u1E2F:"i\u0308\u0301",\u0129:"i\u0303",\u012B:"i\u0304",\u012D:"i\u0306",\u01D0:"i\u030C",\u00EE:"i\u0302",\u01F0:"j\u030C",\u0135:"j\u0302",\u1E31:"k\u0301",\u01E9:"k\u030C",\u0137:"k\u0327",\u013A:"l\u0301",\u013E:"l\u030C",\u013C:"l\u0327",\u1E3F:"m\u0301",\u1E41:"m\u0307",\u0144:"n\u0301",\u01F9:"n\u0300",\u00F1:"n\u0303",\u0148:"n\u030C",\u1E45:"n\u0307",\u0146:"n\u0327",\u00F3:"o\u0301",\u00F2:"o\u0300",\u00F6:"o\u0308",\u022B:"o\u0308\u0304",\u00F5:"o\u0303",\u1E4D:"o\u0303\u0301",\u1E4F:"o\u0303\u0308",\u022D:"o\u0303\u0304",\u014D:"o\u0304",\u1E53:"o\u0304\u0301",\u1E51:"o\u0304\u0300",\u014F:"o\u0306",\u01D2:"o\u030C",\u00F4:"o\u0302",\u1ED1:"o\u0302\u0301",\u1ED3:"o\u0302\u0300",\u1ED7:"o\u0302\u0303",\u022F:"o\u0307",\u0231:"o\u0307\u0304",\u0151:"o\u030B",\u1E55:"p\u0301",\u1E57:"p\u0307",\u0155:"r\u0301",\u0159:"r\u030C",\u1E59:"r\u0307",\u0157:"r\u0327",\u015B:"s\u0301",\u1E65:"s\u0301\u0307",\u0161:"s\u030C",\u1E67:"s\u030C\u0307",\u015D:"s\u0302",\u1E61:"s\u0307",\u015F:"s\u0327",\u1E97:"t\u0308",\u0165:"t\u030C",\u1E6B:"t\u0307",\u0163:"t\u0327",\u00FA:"u\u0301",\u00F9:"u\u0300",\u00FC:"u\u0308",\u01D8:"u\u0308\u0301",\u01DC:"u\u0308\u0300",\u01D6:"u\u0308\u0304",\u01DA:"u\u0308\u030C",\u0169:"u\u0303",\u1E79:"u\u0303\u0301",\u016B:"u\u0304",\u1E7B:"u\u0304\u0308",\u016D:"u\u0306",\u01D4:"u\u030C",\u00FB:"u\u0302",\u016F:"u\u030A",\u0171:"u\u030B",\u1E7D:"v\u0303",\u1E83:"w\u0301",\u1E81:"w\u0300",\u1E85:"w\u0308",\u0175:"w\u0302",\u1E87:"w\u0307",\u1E98:"w\u030A",\u1E8D:"x\u0308",\u1E8B:"x\u0307",\u00FD:"y\u0301",\u1EF3:"y\u0300",\u00FF:"y\u0308",\u1EF9:"y\u0303",\u0233:"y\u0304",\u0177:"y\u0302",\u1E8F:"y\u0307",\u1E99:"y\u030A",\u017A:"z\u0301",\u017E:"z\u030C",\u1E91:"z\u0302",\u017C:"z\u0307",\u00C1:"A\u0301",\u00C0:"A\u0300",\u00C4:"A\u0308",\u01DE:"A\u0308\u0304",\u00C3:"A\u0303",\u0100:"A\u0304",\u0102:"A\u0306",\u1EAE:"A\u0306\u0301",\u1EB0:"A\u0306\u0300",\u1EB4:"A\u0306\u0303",\u01CD:"A\u030C",\u00C2:"A\u0302",\u1EA4:"A\u0302\u0301",\u1EA6:"A\u0302\u0300",\u1EAA:"A\u0302\u0303",\u0226:"A\u0307",\u01E0:"A\u0307\u0304",\u00C5:"A\u030A",\u01FA:"A\u030A\u0301",\u1E02:"B\u0307",\u0106:"C\u0301",\u1E08:"C\u0327\u0301",\u010C:"C\u030C",\u0108:"C\u0302",\u010A:"C\u0307",\u00C7:"C\u0327",\u010E:"D\u030C",\u1E0A:"D\u0307",\u1E10:"D\u0327",\u00C9:"E\u0301",\u00C8:"E\u0300",\u00CB:"E\u0308",\u1EBC:"E\u0303",\u0112:"E\u0304",\u1E16:"E\u0304\u0301",\u1E14:"E\u0304\u0300",\u0114:"E\u0306",\u1E1C:"E\u0327\u0306",\u011A:"E\u030C",\u00CA:"E\u0302",\u1EBE:"E\u0302\u0301",\u1EC0:"E\u0302\u0300",\u1EC4:"E\u0302\u0303",\u0116:"E\u0307",\u0228:"E\u0327",\u1E1E:"F\u0307",\u01F4:"G\u0301",\u1E20:"G\u0304",\u011E:"G\u0306",\u01E6:"G\u030C",\u011C:"G\u0302",\u0120:"G\u0307",\u0122:"G\u0327",\u1E26:"H\u0308",\u021E:"H\u030C",\u0124:"H\u0302",\u1E22:"H\u0307",\u1E28:"H\u0327",\u00CD:"I\u0301",\u00CC:"I\u0300",\u00CF:"I\u0308",\u1E2E:"I\u0308\u0301",\u0128:"I\u0303",\u012A:"I\u0304",\u012C:"I\u0306",\u01CF:"I\u030C",\u00CE:"I\u0302",\u0130:"I\u0307",\u0134:"J\u0302",\u1E30:"K\u0301",\u01E8:"K\u030C",\u0136:"K\u0327",\u0139:"L\u0301",\u013D:"L\u030C",\u013B:"L\u0327",\u1E3E:"M\u0301",\u1E40:"M\u0307",\u0143:"N\u0301",\u01F8:"N\u0300",\u00D1:"N\u0303",\u0147:"N\u030C",\u1E44:"N\u0307",\u0145:"N\u0327",\u00D3:"O\u0301",\u00D2:"O\u0300",\u00D6:"O\u0308",\u022A:"O\u0308\u0304",\u00D5:"O\u0303",\u1E4C:"O\u0303\u0301",\u1E4E:"O\u0303\u0308",\u022C:"O\u0303\u0304",\u014C:"O\u0304",\u1E52:"O\u0304\u0301",\u1E50:"O\u0304\u0300",\u014E:"O\u0306",\u01D1:"O\u030C",\u00D4:"O\u0302",\u1ED0:"O\u0302\u0301",\u1ED2:"O\u0302\u0300",\u1ED6:"O\u0302\u0303",\u022E:"O\u0307",\u0230:"O\u0307\u0304",\u0150:"O\u030B",\u1E54:"P\u0301",\u1E56:"P\u0307",\u0154:"R\u0301",\u0158:"R\u030C",\u1E58:"R\u0307",\u0156:"R\u0327",\u015A:"S\u0301",\u1E64:"S\u0301\u0307",\u0160:"S\u030C",\u1E66:"S\u030C\u0307",\u015C:"S\u0302",\u1E60:"S\u0307",\u015E:"S\u0327",\u0164:"T\u030C",\u1E6A:"T\u0307",\u0162:"T\u0327",\u00DA:"U\u0301",\u00D9:"U\u0300",\u00DC:"U\u0308",\u01D7:"U\u0308\u0301",\u01DB:"U\u0308\u0300",\u01D5:"U\u0308\u0304",\u01D9:"U\u0308\u030C",\u0168:"U\u0303",\u1E78:"U\u0303\u0301",\u016A:"U\u0304",\u1E7A:"U\u0304\u0308",\u016C:"U\u0306",\u01D3:"U\u030C",\u00DB:"U\u0302",\u016E:"U\u030A",\u0170:"U\u030B",\u1E7C:"V\u0303",\u1E82:"W\u0301",\u1E80:"W\u0300",\u1E84:"W\u0308",\u0174:"W\u0302",\u1E86:"W\u0307",\u1E8C:"X\u0308",\u1E8A:"X\u0307",\u00DD:"Y\u0301",\u1EF2:"Y\u0300",\u0178:"Y\u0308",\u1EF8:"Y\u0303",\u0232:"Y\u0304",\u0176:"Y\u0302",\u1E8E:"Y\u0307",\u0179:"Z\u0301",\u017D:"Z\u030C",\u1E90:"Z\u0302",\u017B:"Z\u0307",\u03AC:"\u03B1\u0301",\u1F70:"\u03B1\u0300",\u1FB1:"\u03B1\u0304",\u1FB0:"\u03B1\u0306",\u03AD:"\u03B5\u0301",\u1F72:"\u03B5\u0300",\u03AE:"\u03B7\u0301",\u1F74:"\u03B7\u0300",\u03AF:"\u03B9\u0301",\u1F76:"\u03B9\u0300",\u03CA:"\u03B9\u0308",\u0390:"\u03B9\u0308\u0301",\u1FD2:"\u03B9\u0308\u0300",\u1FD1:"\u03B9\u0304",\u1FD0:"\u03B9\u0306",\u03CC:"\u03BF\u0301",\u1F78:"\u03BF\u0300",\u03CD:"\u03C5\u0301",\u1F7A:"\u03C5\u0300",\u03CB:"\u03C5\u0308",\u03B0:"\u03C5\u0308\u0301",\u1FE2:"\u03C5\u0308\u0300",\u1FE1:"\u03C5\u0304",\u1FE0:"\u03C5\u0306",\u03CE:"\u03C9\u0301",\u1F7C:"\u03C9\u0300",\u038E:"\u03A5\u0301",\u1FEA:"\u03A5\u0300",\u03AB:"\u03A5\u0308",\u1FE9:"\u03A5\u0304",\u1FE8:"\u03A5\u0306",\u038F:"\u03A9\u0301",\u1FFA:"\u03A9\u0300"},Ge=class r{constructor(e,t){this.mode=void 0,this.gullet=void 0,this.settings=void 0,this.leftrightDepth=void 0,this.nextToken=void 0,this.mode="math",this.gullet=new zt(e,t,this.mode),this.settings=t,this.leftrightDepth=0}expect(e,t){if(t===void 0&&(t=!0),this.fetch().text!==e)throw new z("Expected '"+e+"', got '"+this.fetch().text+"'",this.fetch());t&&this.consume()}consume(){this.nextToken=null}fetch(){return this.nextToken==null&&(this.nextToken=this.gullet.expandNextToken()),this.nextToken}switchMode(e){this.mode=e,this.gullet.switchMode(e)}parse(){this.settings.globalGroup||this.gullet.beginGroup(),this.settings.colorIsTextColor&&this.gullet.macros.set("\\color","\\textcolor");try{var e=this.parseExpression(!1);return this.expect("EOF"),this.settings.globalGroup||this.gullet.endGroup(),e}finally{this.gullet.endGroups()}}subparse(e){var t=this.nextToken;this.consume(),this.gullet.pushToken(new b0("}")),this.gullet.pushTokens(e);var a=this.parseExpression(!1);return this.expect("}"),this.nextToken=t,a}parseExpression(e,t){for(var a=[];;){this.mode==="math"&&this.consumeSpaces();var n=this.fetch();if(r.endOfExpression.indexOf(n.text)!==-1||t&&n.text===t||e&&P0[n.text]&&P0[n.text].infix)break;var s=this.parseAtom(t);if(s){if(s.type==="internal")continue}else break;a.push(s)}return this.mode==="text"&&this.formLigatures(a),this.handleInfixNodes(a)}handleInfixNodes(e){for(var t=-1,a,n=0;n=0&&this.settings.reportNonstrict("unicodeTextInMathMode",'Latin-1/Unicode text character "'+t[0]+'" used in math mode',e);var h=Y[this.mode][t].group,c=d0.range(e),f;if(i1.hasOwnProperty(h)){var v=h;f={type:"atom",mode:this.mode,family:v,loc:c,text:t}}else f={type:h,mode:this.mode,loc:c,text:t};l=f}else if(t.charCodeAt(0)>=128)this.settings.strict&&(Ar(t.charCodeAt(0))?this.mode==="math"&&this.settings.reportNonstrict("unicodeTextInMathMode",'Unicode text character "'+t[0]+'" used in math mode',e):this.settings.reportNonstrict("unknownSymbol",'Unrecognized Unicode character "'+t[0]+'"'+(" ("+t.charCodeAt(0)+")"),e)),l={type:"textord",mode:"text",loc:d0.range(e),text:t};else return null;if(this.consume(),s)for(var b=0;b=0;n--)r[n].loc.start>a&&(t+=" ",a=r[n].loc.start),t+=r[n].text,a+=r[n].text.length;var s=W.go(S.go(t,e));return s},S={go:function(r,e){if(!r)return[];e===void 0&&(e="ce");var t="0",a={};a.parenthesisLevel=0,r=r.replace(/\n/g," "),r=r.replace(/[\u2212\u2013\u2014\u2010]/g,"-"),r=r.replace(/[\u2026]/g,"...");for(var n,s=10,l=[];;){n!==r?(s=10,n=r):s--;var h=S.stateMachines[e],c=h.transitions[t]||h.transitions["*"];e:for(var f=0;f0){if(b.revisit||(r=v.remainder),!b.toContinue)break e}else return l}}if(s<=0)throw["MhchemBugU","mhchem bug U. Please report."]}},concatArray:function(r,e){if(e)if(Array.isArray(e))for(var t=0;t":/^[=<>]/,"#":/^[#\u2261]/,"+":/^\+/,"-$":/^-(?=[\s_},;\]/]|$|\([a-z]+\))/,"-9":/^-(?=[0-9])/,"- orbital overlap":/^-(?=(?:[spd]|sp)(?:$|[\s,;\)\]\}]))/,"-":/^-/,"pm-operator":/^(?:\\pm|\$\\pm\$|\+-|\+\/-)/,operator:/^(?:\+|(?:[\-=<>]|<<|>>|\\approx|\$\\approx\$)(?=\s|$|-?[0-9]))/,arrowUpDown:/^(?:v|\(v\)|\^|\(\^\))(?=$|[\s,;\)\]\}])/,"\\bond{(...)}":function(r){return S.patterns.findObserveGroups(r,"\\bond{","","","}")},"->":/^(?:<->|<-->|->|<-|<=>>|<<=>|<=>|[\u2192\u27F6\u21CC])/,CMT:/^[CMT](?=\[)/,"[(...)]":function(r){return S.patterns.findObserveGroups(r,"[","","","]")},"1st-level escape":/^(&|\\\\|\\hline)\s*/,"\\,":/^(?:\\[,\ ;:])/,"\\x{}{}":function(r){return S.patterns.findObserveGroups(r,"",/^\\[a-zA-Z]+\{/,"}","","","{","}","",!0)},"\\x{}":function(r){return S.patterns.findObserveGroups(r,"",/^\\[a-zA-Z]+\{/,"}","")},"\\ca":/^\\ca(?:\s+|(?![a-zA-Z]))/,"\\x":/^(?:\\[a-zA-Z]+\s*|\\[_&{}%])/,orbital:/^(?:[0-9]{1,2}[spdfgh]|[0-9]{0,2}sp)(?=$|[^a-zA-Z])/,others:/^[\/~|]/,"\\frac{(...)}":function(r){return S.patterns.findObserveGroups(r,"\\frac{","","","}","{","","","}")},"\\overset{(...)}":function(r){return S.patterns.findObserveGroups(r,"\\overset{","","","}","{","","","}")},"\\underset{(...)}":function(r){return S.patterns.findObserveGroups(r,"\\underset{","","","}","{","","","}")},"\\underbrace{(...)}":function(r){return S.patterns.findObserveGroups(r,"\\underbrace{","","","}_","{","","","}")},"\\color{(...)}0":function(r){return S.patterns.findObserveGroups(r,"\\color{","","","}")},"\\color{(...)}{(...)}1":function(r){return S.patterns.findObserveGroups(r,"\\color{","","","}","{","","","}")},"\\color(...){(...)}2":function(r){return S.patterns.findObserveGroups(r,"\\color","\\","",/^(?=\{)/,"{","","","}")},"\\ce{(...)}":function(r){return S.patterns.findObserveGroups(r,"\\ce{","","","}")},oxidation$:/^(?:[+-][IVX]+|\\pm\s*0|\$\\pm\$\s*0)$/,"d-oxidation$":/^(?:[+-]?\s?[IVX]+|\\pm\s*0|\$\\pm\$\s*0)$/,"roman numeral":/^[IVX]+/,"1/2$":/^[+\-]?(?:[0-9]+|\$[a-z]\$|[a-z])\/[0-9]+(?:\$[a-z]\$|[a-z])?$/,amount:function(r){var e;if(e=r.match(/^(?:(?:(?:\([+\-]?[0-9]+\/[0-9]+\)|[+\-]?(?:[0-9]+|\$[a-z]\$|[a-z])\/[0-9]+|[+\-]?[0-9]+[.,][0-9]+|[+\-]?\.[0-9]+|[+\-]?[0-9]+)(?:[a-z](?=\s*[A-Z]))?)|[+\-]?[a-z](?=\s*[A-Z])|\+(?!\s))/),e)return{match_:e[0],remainder:r.substr(e[0].length)};var t=S.patterns.findObserveGroups(r,"","$","$","");return t&&(e=t.match_.match(/^\$(?:\(?[+\-]?(?:[0-9]*[a-z]?[+\-])?[0-9]*[a-z](?:[+\-][0-9]*[a-z]?)?\)?|\+|-)\$$/),e)?{match_:e[0],remainder:r.substr(e[0].length)}:null},amount2:function(r){return this.amount(r)},"(KV letters),":/^(?:[A-Z][a-z]{0,2}|i)(?=,)/,formula$:function(r){if(r.match(/^\([a-z]+\)$/))return null;var e=r.match(/^(?:[a-z]|(?:[0-9\ \+\-\,\.\(\)]+[a-z])+[0-9\ \+\-\,\.\(\)]*|(?:[a-z][0-9\ \+\-\,\.\(\)]+)+[a-z]?)$/);return e?{match_:e[0],remainder:r.substr(e[0].length)}:null},uprightEntities:/^(?:pH|pOH|pC|pK|iPr|iBu)(?=$|[^a-zA-Z])/,"/":/^\s*(\/)\s*/,"//":/^\s*(\/\/)\s*/,"*":/^\s*[*.]\s*/},findObserveGroups:function(r,e,t,a,n,s,l,h,c,f){var v=function(D,N){if(typeof N=="string")return D.indexOf(N)!==0?null:N;var $=D.match(N);return $?$[0]:null},b=function(D,N,$){for(var H=0;N0,null},x=v(r,e);if(x===null||(r=r.substr(x.length),x=v(r,t),x===null))return null;var w=b(r,x.length,a||n);if(w===null)return null;var A=r.substring(0,a?w.endMatchEnd:w.endMatchBegin);if(s||l){var q=this.findObserveGroups(r.substr(w.endMatchEnd),s,l,h,c);if(q===null)return null;var _=[A,q.match_];return{match_:f?_.join(""):_,remainder:q.remainder}}else return{match_:A,remainder:r.substr(w.endMatchEnd)}},match_:function(r,e){var t=S.patterns.patterns[r];if(t===void 0)throw["MhchemBugP","mhchem bug P. Please report. ("+r+")"];if(typeof t=="function")return S.patterns.patterns[r](e);var a=e.match(t);if(a){var n;return a[2]?n=[a[1],a[2]]:a[1]?n=a[1]:n=a[0],{match_:n,remainder:e.substr(a[0].length)}}return null}},actions:{"a=":function(r,e){r.a=(r.a||"")+e},"b=":function(r,e){r.b=(r.b||"")+e},"p=":function(r,e){r.p=(r.p||"")+e},"o=":function(r,e){r.o=(r.o||"")+e},"q=":function(r,e){r.q=(r.q||"")+e},"d=":function(r,e){r.d=(r.d||"")+e},"rm=":function(r,e){r.rm=(r.rm||"")+e},"text=":function(r,e){r.text_=(r.text_||"")+e},insert:function(r,e,t){return{type_:t}},"insert+p1":function(r,e,t){return{type_:t,p1:e}},"insert+p1+p2":function(r,e,t){return{type_:t,p1:e[0],p2:e[1]}},copy:function(r,e){return e},rm:function(r,e){return{type_:"rm",p1:e||""}},text:function(r,e){return S.go(e,"text")},"{text}":function(r,e){var t=["{"];return S.concatArray(t,S.go(e,"text")),t.push("}"),t},"tex-math":function(r,e){return S.go(e,"tex-math")},"tex-math tight":function(r,e){return S.go(e,"tex-math tight")},bond:function(r,e,t){return{type_:"bond",kind_:t||e}},"color0-output":function(r,e){return{type_:"color0",color:e[0]}},ce:function(r,e){return S.go(e)},"1/2":function(r,e){var t=[];e.match(/^[+\-]/)&&(t.push(e.substr(0,1)),e=e.substr(1));var a=e.match(/^([0-9]+|\$[a-z]\$|[a-z])\/([0-9]+)(\$[a-z]\$|[a-z])?$/);return a[1]=a[1].replace(/\$/g,""),t.push({type_:"frac",p1:a[1],p2:a[2]}),a[3]&&(a[3]=a[3].replace(/\$/g,""),t.push({type_:"tex-math",p1:a[3]})),t},"9,9":function(r,e){return S.go(e,"9,9")}},createTransitions:function(r){var e,t,a,n,s={};for(e in r)for(t in r[e])for(a=t.split("|"),r[e][t].stateArray=a,n=0;n":{"0|1|2|3":{action_:"r=",nextState:"r"},"a|as":{action_:["output","r="],nextState:"r"},"*":{action_:["output","r="],nextState:"r"}},"+":{o:{action_:"d= kv",nextState:"d"},"d|D":{action_:"d=",nextState:"d"},q:{action_:"d=",nextState:"qd"},"qd|qD":{action_:"d=",nextState:"qd"},dq:{action_:["output","d="],nextState:"d"},3:{action_:["sb=false","output","operator"],nextState:"0"}},amount:{"0|2":{action_:"a=",nextState:"a"}},"pm-operator":{"0|1|2|a|as":{action_:["sb=false","output",{type_:"operator",option:"\\pm"}],nextState:"0"}},operator:{"0|1|2|a|as":{action_:["sb=false","output","operator"],nextState:"0"}},"-$":{"o|q":{action_:["charge or bond","output"],nextState:"qd"},d:{action_:"d=",nextState:"d"},D:{action_:["output",{type_:"bond",option:"-"}],nextState:"3"},q:{action_:"d=",nextState:"qd"},qd:{action_:"d=",nextState:"qd"},"qD|dq":{action_:["output",{type_:"bond",option:"-"}],nextState:"3"}},"-9":{"3|o":{action_:["output",{type_:"insert",option:"hyphen"}],nextState:"3"}},"- orbital overlap":{o:{action_:["output",{type_:"insert",option:"hyphen"}],nextState:"2"},d:{action_:["output",{type_:"insert",option:"hyphen"}],nextState:"2"}},"-":{"0|1|2":{action_:[{type_:"output",option:1},"beginsWithBond=true",{type_:"bond",option:"-"}],nextState:"3"},3:{action_:{type_:"bond",option:"-"}},a:{action_:["output",{type_:"insert",option:"hyphen"}],nextState:"2"},as:{action_:[{type_:"output",option:2},{type_:"bond",option:"-"}],nextState:"3"},b:{action_:"b="},o:{action_:{type_:"- after o/d",option:!1},nextState:"2"},q:{action_:{type_:"- after o/d",option:!1},nextState:"2"},"d|qd|dq":{action_:{type_:"- after o/d",option:!0},nextState:"2"},"D|qD|p":{action_:["output",{type_:"bond",option:"-"}],nextState:"3"}},amount2:{"1|3":{action_:"a=",nextState:"a"}},letters:{"0|1|2|3|a|as|b|p|bp|o":{action_:"o=",nextState:"o"},"q|dq":{action_:["output","o="],nextState:"o"},"d|D|qd|qD":{action_:"o after d",nextState:"o"}},digits:{o:{action_:"q=",nextState:"q"},"d|D":{action_:"q=",nextState:"dq"},q:{action_:["output","o="],nextState:"o"},a:{action_:"o=",nextState:"o"}},"space A":{"b|p|bp":{}},space:{a:{nextState:"as"},0:{action_:"sb=false"},"1|2":{action_:"sb=true"},"r|rt|rd|rdt|rdq":{action_:"output",nextState:"0"},"*":{action_:["output","sb=true"],nextState:"1"}},"1st-level escape":{"1|2":{action_:["output",{type_:"insert+p1",option:"1st-level escape"}]},"*":{action_:["output",{type_:"insert+p1",option:"1st-level escape"}],nextState:"0"}},"[(...)]":{"r|rt":{action_:"rd=",nextState:"rd"},"rd|rdt":{action_:"rq=",nextState:"rdq"}},"...":{"o|d|D|dq|qd|qD":{action_:["output",{type_:"bond",option:"..."}],nextState:"3"},"*":{action_:[{type_:"output",option:1},{type_:"insert",option:"ellipsis"}],nextState:"1"}},". |* ":{"*":{action_:["output",{type_:"insert",option:"addition compound"}],nextState:"1"}},"state of aggregation $":{"*":{action_:["output","state of aggregation"],nextState:"1"}},"{[(":{"a|as|o":{action_:["o=","output","parenthesisLevel++"],nextState:"2"},"0|1|2|3":{action_:["o=","output","parenthesisLevel++"],nextState:"2"},"*":{action_:["output","o=","output","parenthesisLevel++"],nextState:"2"}},")]}":{"0|1|2|3|b|p|bp|o":{action_:["o=","parenthesisLevel--"],nextState:"o"},"a|as|d|D|q|qd|qD|dq":{action_:["output","o=","parenthesisLevel--"],nextState:"o"}},", ":{"*":{action_:["output","comma"],nextState:"0"}},"^_":{"*":{}},"^{(...)}|^($...$)":{"0|1|2|as":{action_:"b=",nextState:"b"},p:{action_:"b=",nextState:"bp"},"3|o":{action_:"d= kv",nextState:"D"},q:{action_:"d=",nextState:"qD"},"d|D|qd|qD|dq":{action_:["output","d="],nextState:"D"}},"^a|^\\x{}{}|^\\x{}|^\\x|'":{"0|1|2|as":{action_:"b=",nextState:"b"},p:{action_:"b=",nextState:"bp"},"3|o":{action_:"d= kv",nextState:"d"},q:{action_:"d=",nextState:"qd"},"d|qd|D|qD":{action_:"d="},dq:{action_:["output","d="],nextState:"d"}},"_{(state of aggregation)}$":{"d|D|q|qd|qD|dq":{action_:["output","q="],nextState:"q"}},"_{(...)}|_($...$)|_9|_\\x{}{}|_\\x{}|_\\x":{"0|1|2|as":{action_:"p=",nextState:"p"},b:{action_:"p=",nextState:"bp"},"3|o":{action_:"q=",nextState:"q"},"d|D":{action_:"q=",nextState:"dq"},"q|qd|qD|dq":{action_:["output","q="],nextState:"q"}},"=<>":{"0|1|2|3|a|as|o|q|d|D|qd|qD|dq":{action_:[{type_:"output",option:2},"bond"],nextState:"3"}},"#":{"0|1|2|3|a|as|o":{action_:[{type_:"output",option:2},{type_:"bond",option:"#"}],nextState:"3"}},"{}":{"*":{action_:{type_:"output",option:1},nextState:"1"}},"{...}":{"0|1|2|3|a|as|b|p|bp":{action_:"o=",nextState:"o"},"o|d|D|q|qd|qD|dq":{action_:["output","o="],nextState:"o"}},"$...$":{a:{action_:"a="},"0|1|2|3|as|b|p|bp|o":{action_:"o=",nextState:"o"},"as|o":{action_:"o="},"q|d|D|qd|qD|dq":{action_:["output","o="],nextState:"o"}},"\\bond{(...)}":{"*":{action_:[{type_:"output",option:2},"bond"],nextState:"3"}},"\\frac{(...)}":{"*":{action_:[{type_:"output",option:1},"frac-output"],nextState:"3"}},"\\overset{(...)}":{"*":{action_:[{type_:"output",option:2},"overset-output"],nextState:"3"}},"\\underset{(...)}":{"*":{action_:[{type_:"output",option:2},"underset-output"],nextState:"3"}},"\\underbrace{(...)}":{"*":{action_:[{type_:"output",option:2},"underbrace-output"],nextState:"3"}},"\\color{(...)}{(...)}1|\\color(...){(...)}2":{"*":{action_:[{type_:"output",option:2},"color-output"],nextState:"3"}},"\\color{(...)}0":{"*":{action_:[{type_:"output",option:2},"color0-output"]}},"\\ce{(...)}":{"*":{action_:[{type_:"output",option:2},"ce"],nextState:"3"}},"\\,":{"*":{action_:[{type_:"output",option:1},"copy"],nextState:"1"}},"\\x{}{}|\\x{}|\\x":{"0|1|2|3|a|as|b|p|bp|o|c0":{action_:["o=","output"],nextState:"3"},"*":{action_:["output","o=","output"],nextState:"3"}},others:{"*":{action_:[{type_:"output",option:1},"copy"],nextState:"3"}},else2:{a:{action_:"a to o",nextState:"o",revisit:!0},as:{action_:["output","sb=true"],nextState:"1",revisit:!0},"r|rt|rd|rdt|rdq":{action_:["output"],nextState:"0",revisit:!0},"*":{action_:["output","copy"],nextState:"3"}}}),actions:{"o after d":function(r,e){var t;if((r.d||"").match(/^[0-9]+$/)){var a=r.d;r.d=void 0,t=this.output(r),r.b=a}else t=this.output(r);return S.actions["o="](r,e),t},"d= kv":function(r,e){r.d=e,r.dType="kv"},"charge or bond":function(r,e){if(r.beginsWithBond){var t=[];return S.concatArray(t,this.output(r)),S.concatArray(t,S.actions.bond(r,e,"-")),t}else r.d=e},"- after o/d":function(r,e,t){var a=S.patterns.match_("orbital",r.o||""),n=S.patterns.match_("one lowercase greek letter $",r.o||""),s=S.patterns.match_("one lowercase latin letter $",r.o||""),l=S.patterns.match_("$one lowercase latin letter$ $",r.o||""),h=e==="-"&&(a&&a.remainder===""||n||s||l);h&&!r.a&&!r.b&&!r.p&&!r.d&&!r.q&&!a&&s&&(r.o="$"+r.o+"$");var c=[];return h?(S.concatArray(c,this.output(r)),c.push({type_:"hyphen"})):(a=S.patterns.match_("digits",r.d||""),t&&a&&a.remainder===""?(S.concatArray(c,S.actions["d="](r,e)),S.concatArray(c,this.output(r))):(S.concatArray(c,this.output(r)),S.concatArray(c,S.actions.bond(r,e,"-")))),c},"a to o":function(r){r.o=r.a,r.a=void 0},"sb=true":function(r){r.sb=!0},"sb=false":function(r){r.sb=!1},"beginsWithBond=true":function(r){r.beginsWithBond=!0},"beginsWithBond=false":function(r){r.beginsWithBond=!1},"parenthesisLevel++":function(r){r.parenthesisLevel++},"parenthesisLevel--":function(r){r.parenthesisLevel--},"state of aggregation":function(r,e){return{type_:"state of aggregation",p1:S.go(e,"o")}},comma:function(r,e){var t=e.replace(/\s*$/,""),a=t!==e;return a&&r.parenthesisLevel===0?{type_:"comma enumeration L",p1:t}:{type_:"comma enumeration M",p1:t}},output:function(r,e,t){var a;if(!r.r)a=[],!r.a&&!r.b&&!r.p&&!r.o&&!r.q&&!r.d&&!t||(r.sb&&a.push({type_:"entitySkip"}),!r.o&&!r.q&&!r.d&&!r.b&&!r.p&&t!==2?(r.o=r.a,r.a=void 0):!r.o&&!r.q&&!r.d&&(r.b||r.p)?(r.o=r.a,r.d=r.b,r.q=r.p,r.a=r.b=r.p=void 0):r.o&&r.dType==="kv"&&S.patterns.match_("d-oxidation$",r.d||"")?r.dType="oxidation":r.o&&r.dType==="kv"&&!r.q&&(r.dType=void 0),a.push({type_:"chemfive",a:S.go(r.a,"a"),b:S.go(r.b,"bd"),p:S.go(r.p,"pq"),o:S.go(r.o,"o"),q:S.go(r.q,"pq"),d:S.go(r.d,r.dType==="oxidation"?"oxidation":"bd"),dType:r.dType}));else{var n;r.rdt==="M"?n=S.go(r.rd,"tex-math"):r.rdt==="T"?n=[{type_:"text",p1:r.rd||""}]:n=S.go(r.rd);var s;r.rqt==="M"?s=S.go(r.rq,"tex-math"):r.rqt==="T"?s=[{type_:"text",p1:r.rq||""}]:s=S.go(r.rq),a={type_:"arrow",r:r.r,rd:n,rq:s}}for(var l in r)l!=="parenthesisLevel"&&l!=="beginsWithBond"&&delete r[l];return a},"oxidation-output":function(r,e){var t=["{"];return S.concatArray(t,S.go(e,"oxidation")),t.push("}"),t},"frac-output":function(r,e){return{type_:"frac-ce",p1:S.go(e[0]),p2:S.go(e[1])}},"overset-output":function(r,e){return{type_:"overset",p1:S.go(e[0]),p2:S.go(e[1])}},"underset-output":function(r,e){return{type_:"underset",p1:S.go(e[0]),p2:S.go(e[1])}},"underbrace-output":function(r,e){return{type_:"underbrace",p1:S.go(e[0]),p2:S.go(e[1])}},"color-output":function(r,e){return{type_:"color",color1:e[0],color2:S.go(e[1])}},"r=":function(r,e){r.r=e},"rdt=":function(r,e){r.rdt=e},"rd=":function(r,e){r.rd=e},"rqt=":function(r,e){r.rqt=e},"rq=":function(r,e){r.rq=e},operator:function(r,e,t){return{type_:"operator",kind_:t||e}}}},a:{transitions:S.createTransitions({empty:{"*":{}},"1/2$":{0:{action_:"1/2"}},else:{0:{nextState:"1",revisit:!0}},"$(...)$":{"*":{action_:"tex-math tight",nextState:"1"}},",":{"*":{action_:{type_:"insert",option:"commaDecimal"}}},else2:{"*":{action_:"copy"}}}),actions:{}},o:{transitions:S.createTransitions({empty:{"*":{}},"1/2$":{0:{action_:"1/2"}},else:{0:{nextState:"1",revisit:!0}},letters:{"*":{action_:"rm"}},"\\ca":{"*":{action_:{type_:"insert",option:"circa"}}},"\\x{}{}|\\x{}|\\x":{"*":{action_:"copy"}},"${(...)}$|$(...)$":{"*":{action_:"tex-math"}},"{(...)}":{"*":{action_:"{text}"}},else2:{"*":{action_:"copy"}}}),actions:{}},text:{transitions:S.createTransitions({empty:{"*":{action_:"output"}},"{...}":{"*":{action_:"text="}},"${(...)}$|$(...)$":{"*":{action_:"tex-math"}},"\\greek":{"*":{action_:["output","rm"]}},"\\,|\\x{}{}|\\x{}|\\x":{"*":{action_:["output","copy"]}},else:{"*":{action_:"text="}}}),actions:{output:function(r){if(r.text_){var e={type_:"text",p1:r.text_};for(var t in r)delete r[t];return e}}}},pq:{transitions:S.createTransitions({empty:{"*":{}},"state of aggregation $":{"*":{action_:"state of aggregation"}},i$:{0:{nextState:"!f",revisit:!0}},"(KV letters),":{0:{action_:"rm",nextState:"0"}},formula$:{0:{nextState:"f",revisit:!0}},"1/2$":{0:{action_:"1/2"}},else:{0:{nextState:"!f",revisit:!0}},"${(...)}$|$(...)$":{"*":{action_:"tex-math"}},"{(...)}":{"*":{action_:"text"}},"a-z":{f:{action_:"tex-math"}},letters:{"*":{action_:"rm"}},"-9.,9":{"*":{action_:"9,9"}},",":{"*":{action_:{type_:"insert+p1",option:"comma enumeration S"}}},"\\color{(...)}{(...)}1|\\color(...){(...)}2":{"*":{action_:"color-output"}},"\\color{(...)}0":{"*":{action_:"color0-output"}},"\\ce{(...)}":{"*":{action_:"ce"}},"\\,|\\x{}{}|\\x{}|\\x":{"*":{action_:"copy"}},else2:{"*":{action_:"copy"}}}),actions:{"state of aggregation":function(r,e){return{type_:"state of aggregation subscript",p1:S.go(e,"o")}},"color-output":function(r,e){return{type_:"color",color1:e[0],color2:S.go(e[1],"pq")}}}},bd:{transitions:S.createTransitions({empty:{"*":{}},x$:{0:{nextState:"!f",revisit:!0}},formula$:{0:{nextState:"f",revisit:!0}},else:{0:{nextState:"!f",revisit:!0}},"-9.,9 no missing 0":{"*":{action_:"9,9"}},".":{"*":{action_:{type_:"insert",option:"electron dot"}}},"a-z":{f:{action_:"tex-math"}},x:{"*":{action_:{type_:"insert",option:"KV x"}}},letters:{"*":{action_:"rm"}},"'":{"*":{action_:{type_:"insert",option:"prime"}}},"${(...)}$|$(...)$":{"*":{action_:"tex-math"}},"{(...)}":{"*":{action_:"text"}},"\\color{(...)}{(...)}1|\\color(...){(...)}2":{"*":{action_:"color-output"}},"\\color{(...)}0":{"*":{action_:"color0-output"}},"\\ce{(...)}":{"*":{action_:"ce"}},"\\,|\\x{}{}|\\x{}|\\x":{"*":{action_:"copy"}},else2:{"*":{action_:"copy"}}}),actions:{"color-output":function(r,e){return{type_:"color",color1:e[0],color2:S.go(e[1],"bd")}}}},oxidation:{transitions:S.createTransitions({empty:{"*":{}},"roman numeral":{"*":{action_:"roman-numeral"}},"${(...)}$|$(...)$":{"*":{action_:"tex-math"}},else:{"*":{action_:"copy"}}}),actions:{"roman-numeral":function(r,e){return{type_:"roman numeral",p1:e||""}}}},"tex-math":{transitions:S.createTransitions({empty:{"*":{action_:"output"}},"\\ce{(...)}":{"*":{action_:["output","ce"]}},"{...}|\\,|\\x{}{}|\\x{}|\\x":{"*":{action_:"o="}},else:{"*":{action_:"o="}}}),actions:{output:function(r){if(r.o){var e={type_:"tex-math",p1:r.o};for(var t in r)delete r[t];return e}}}},"tex-math tight":{transitions:S.createTransitions({empty:{"*":{action_:"output"}},"\\ce{(...)}":{"*":{action_:["output","ce"]}},"{...}|\\,|\\x{}{}|\\x{}|\\x":{"*":{action_:"o="}},"-|+":{"*":{action_:"tight operator"}},else:{"*":{action_:"o="}}}),actions:{"tight operator":function(r,e){r.o=(r.o||"")+"{"+e+"}"},output:function(r){if(r.o){var e={type_:"tex-math",p1:r.o};for(var t in r)delete r[t];return e}}}},"9,9":{transitions:S.createTransitions({empty:{"*":{}},",":{"*":{action_:"comma"}},else:{"*":{action_:"copy"}}}),actions:{comma:function(){return{type_:"commaDecimal"}}}},pu:{transitions:S.createTransitions({empty:{"*":{action_:"output"}},space$:{"*":{action_:["output","space"]}},"{[(|)]}":{"0|a":{action_:"copy"}},"(-)(9)^(-9)":{0:{action_:"number^",nextState:"a"}},"(-)(9.,9)(e)(99)":{0:{action_:"enumber",nextState:"a"}},space:{"0|a":{}},"pm-operator":{"0|a":{action_:{type_:"operator",option:"\\pm"},nextState:"0"}},operator:{"0|a":{action_:"copy",nextState:"0"}},"//":{d:{action_:"o=",nextState:"/"}},"/":{d:{action_:"o=",nextState:"/"}},"{...}|else":{"0|d":{action_:"d=",nextState:"d"},a:{action_:["space","d="],nextState:"d"},"/|q":{action_:"q=",nextState:"q"}}}),actions:{enumber:function(r,e){var t=[];return e[0]==="+-"||e[0]==="+/-"?t.push("\\pm "):e[0]&&t.push(e[0]),e[1]&&(S.concatArray(t,S.go(e[1],"pu-9,9")),e[2]&&(e[2].match(/[,.]/)?S.concatArray(t,S.go(e[2],"pu-9,9")):t.push(e[2])),e[3]=e[4]||e[3],e[3]&&(e[3]=e[3].trim(),e[3]==="e"||e[3].substr(0,1)==="*"?t.push({type_:"cdot"}):t.push({type_:"times"}))),e[3]&&t.push("10^{"+e[5]+"}"),t},"number^":function(r,e){var t=[];return e[0]==="+-"||e[0]==="+/-"?t.push("\\pm "):e[0]&&t.push(e[0]),S.concatArray(t,S.go(e[1],"pu-9,9")),t.push("^{"+e[2]+"}"),t},operator:function(r,e,t){return{type_:"operator",kind_:t||e}},space:function(){return{type_:"pu-space-1"}},output:function(r){var e,t=S.patterns.match_("{(...)}",r.d||"");t&&t.remainder===""&&(r.d=t.match_);var a=S.patterns.match_("{(...)}",r.q||"");if(a&&a.remainder===""&&(r.q=a.match_),r.d&&(r.d=r.d.replace(/\u00B0C|\^oC|\^{o}C/g,"{}^{\\circ}C"),r.d=r.d.replace(/\u00B0F|\^oF|\^{o}F/g,"{}^{\\circ}F")),r.q){r.q=r.q.replace(/\u00B0C|\^oC|\^{o}C/g,"{}^{\\circ}C"),r.q=r.q.replace(/\u00B0F|\^oF|\^{o}F/g,"{}^{\\circ}F");var n={d:S.go(r.d,"pu"),q:S.go(r.q,"pu")};r.o==="//"?e={type_:"pu-frac",p1:n.d,p2:n.q}:(e=n.d,n.d.length>1||n.q.length>1?e.push({type_:" / "}):e.push({type_:"/"}),S.concatArray(e,n.q))}else e=S.go(r.d,"pu-2");for(var s in r)delete r[s];return e}}},"pu-2":{transitions:S.createTransitions({empty:{"*":{action_:"output"}},"*":{"*":{action_:["output","cdot"],nextState:"0"}},"\\x":{"*":{action_:"rm="}},space:{"*":{action_:["output","space"],nextState:"0"}},"^{(...)}|^(-1)":{1:{action_:"^(-1)"}},"-9.,9":{0:{action_:"rm=",nextState:"0"},1:{action_:"^(-1)",nextState:"0"}},"{...}|else":{"*":{action_:"rm=",nextState:"1"}}}),actions:{cdot:function(){return{type_:"tight cdot"}},"^(-1)":function(r,e){r.rm+="^{"+e+"}"},space:function(){return{type_:"pu-space-2"}},output:function(r){var e=[];if(r.rm){var t=S.patterns.match_("{(...)}",r.rm||"");t&&t.remainder===""?e=S.go(t.match_,"pu"):e={type_:"rm",p1:r.rm}}for(var a in r)delete r[a];return e}}},"pu-9,9":{transitions:S.createTransitions({empty:{0:{action_:"output-0"},o:{action_:"output-o"}},",":{0:{action_:["output-0","comma"],nextState:"o"}},".":{0:{action_:["output-0","copy"],nextState:"o"}},else:{"*":{action_:"text="}}}),actions:{comma:function(){return{type_:"commaDecimal"}},"output-0":function(r){var e=[];if(r.text_=r.text_||"",r.text_.length>4){var t=r.text_.length%3;t===0&&(t=3);for(var a=r.text_.length-3;a>0;a-=3)e.push(r.text_.substr(a,3)),e.push({type_:"1000 separator"});e.push(r.text_.substr(0,t)),e.reverse()}else e.push(r.text_);for(var n in r)delete r[n];return e},"output-o":function(r){var e=[];if(r.text_=r.text_||"",r.text_.length>4){for(var t=r.text_.length-3,a=0;a":return"rightarrow";case"\u2192":return"rightarrow";case"\u27F6":return"rightarrow";case"<-":return"leftarrow";case"<->":return"leftrightarrow";case"<-->":return"rightleftarrows";case"<=>":return"rightleftharpoons";case"\u21CC":return"rightleftharpoons";case"<=>>":return"rightequilibrium";case"<<=>":return"leftequilibrium";default:throw["MhchemBugT","mhchem bug T. Please report."]}},_getBond:function(r){switch(r){case"-":return"{-}";case"1":return"{-}";case"=":return"{=}";case"2":return"{=}";case"#":return"{\\equiv}";case"3":return"{\\equiv}";case"~":return"{\\tripledash}";case"~-":return"{\\mathrlap{\\raisebox{-.1em}{$-$}}\\raisebox{.1em}{$\\tripledash$}}";case"~=":return"{\\mathrlap{\\raisebox{-.2em}{$-$}}\\mathrlap{\\raisebox{.2em}{$\\tripledash$}}-}";case"~--":return"{\\mathrlap{\\raisebox{-.2em}{$-$}}\\mathrlap{\\raisebox{.2em}{$\\tripledash$}}-}";case"-~-":return"{\\mathrlap{\\raisebox{-.2em}{$-$}}\\mathrlap{\\raisebox{.2em}{$-$}}\\tripledash}";case"...":return"{{\\cdot}{\\cdot}{\\cdot}}";case"....":return"{{\\cdot}{\\cdot}{\\cdot}{\\cdot}}";case"->":return"{\\rightarrow}";case"<-":return"{\\leftarrow}";case"<":return"{<}";case">":return"{>}";default:throw["MhchemBugT","mhchem bug T. Please report."]}},_getOperator:function(r){switch(r){case"+":return" {}+{} ";case"-":return" {}-{} ";case"=":return" {}={} ";case"<":return" {}<{} ";case">":return" {}>{} ";case"<<":return" {}\\ll{} ";case">>":return" {}\\gg{} ";case"\\pm":return" {}\\pm{} ";case"\\approx":return" {}\\approx{} ";case"$\\approx$":return" {}\\approx{} ";case"v":return" \\downarrow{} ";case"(v)":return" \\downarrow{} ";case"^":return" \\uparrow{} ";case"(^)":return" \\uparrow{} ";default:throw["MhchemBugT","mhchem bug T. Please report."]}}};var wn=function(r){let e=r.data,t=e.expression,a=e.options,n=r.header;n.warnings=[],a.strict=="warn"&&(a.strict=(l,h)=>{n.warnings.push(`katex: LaTeX-incompatible input and strict mode is set to 'warn': ${h} [${l}]`)});let s=oe.renderToString(t,a);et({header:n,data:{output:s}})};Wt(wn);})(); diff --git a/internal/warpc/js/renderkatex.js b/internal/warpc/js/renderkatex.js deleted file mode 100644 index 7c8ac25ee..000000000 --- a/internal/warpc/js/renderkatex.js +++ /dev/null @@ -1,25 +0,0 @@ -import { readInput, writeOutput } from './common'; -import katex from 'katex'; -import 'katex/contrib/mhchem/mhchem.js'; - -const render = function (input) { - const data = input.data; - const expression = data.expression; - const options = data.options; - const header = input.header; - header.warnings = []; - - if (options.strict == 'warn') { - // By default, KaTeX will write to console.warn, that's a little hard to handle. - options.strict = (errorCode, errorMsg) => { - header.warnings.push( - `katex: LaTeX-incompatible input and strict mode is set to 'warn': ${errorMsg} [${errorCode}]`, - ); - }; - } - // Any error thrown here will be caught by the common.js readInput function. - const output = katex.renderToString(expression, options); - writeOutput({ header: header, data: { output: output } }); -}; - -readInput(render); diff --git a/internal/warpc/katex.go b/internal/warpc/katex.go deleted file mode 100644 index 75c20117f..000000000 --- a/internal/warpc/katex.go +++ /dev/null @@ -1,76 +0,0 @@ -// 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 warpc - -import ( - _ "embed" -) - -//go:embed wasm/renderkatex.wasm -var katexWasm []byte - -// See https://katex.org/docs/options.html -type KatexInput struct { - Expression string `json:"expression"` - Options KatexOptions `json:"options"` -} - -// KatexOptions defines the options for the KaTeX rendering. -// See https://katex.org/docs/options.html -type KatexOptions struct { - // html, mathml (default), htmlAndMathml - Output string `json:"output"` - - // If true, display math in display mode, false in inline mode. - DisplayMode bool `json:"displayMode"` - - // Render \tags on the left side instead of the right. - Leqno bool `json:"leqno"` - - // If true, render flush left with a 2em left margin. - Fleqn bool `json:"fleqn"` - - // The color used for typesetting errors. - // A color string given in the format "#XXX" or "#XXXXXX" - ErrorColor string `json:"errorColor"` - - // A collection of custom macros. - Macros map[string]string `json:"macros,omitempty"` - - // Specifies a minimum thickness, in ems, for fraction lines. - MinRuleThickness float64 `json:"minRuleThickness"` - - // If true, KaTeX will throw a ParseError when it encounters an unsupported command. - ThrowOnError bool `json:"throwOnError"` - - // Controls how KaTeX handles LaTeX features that offer convenience but - // aren't officially supported, one of error (default), ignore, or warn. - // - // - error: Throws an error when convenient, unsupported LaTeX features - // are encountered. - // - ignore: Allows convenient, unsupported LaTeX features without any - // feedback. - // - warn: Emits a warning when convenient, unsupported LaTeX features are - // encountered. - // - // The "newLineInDisplayMode" error code, which flags the use of \\ - // or \newline in display mode outside an array or tabular environment, is - // intentionally designed not to throw an error, despite this behavior - // being questionable. - Strict string `json:"strict"` -} - -type KatexOutput struct { - Output string `json:"output"` -} diff --git a/internal/warpc/warpc.go b/internal/warpc/warpc.go deleted file mode 100644 index e21fefa8a..000000000 --- a/internal/warpc/warpc.go +++ /dev/null @@ -1,589 +0,0 @@ -// 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 warpc - -import ( - "bytes" - "context" - _ "embed" - "encoding/json" - "errors" - "fmt" - "io" - "strings" - "sync" - "sync/atomic" - "time" - - "github.com/gohugoio/hugo/common/hugio" - "golang.org/x/sync/errgroup" - - "github.com/tetratelabs/wazero" - "github.com/tetratelabs/wazero/api" - "github.com/tetratelabs/wazero/experimental" - "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" -) - -const currentVersion = 1 - -//go:embed wasm/quickjs.wasm -var quickjsWasm []byte - -// Header is in both the request and response. -type Header struct { - // Major version of the protocol. - Version uint16 `json:"version"` - - // Unique ID for the request. - // Note that this only needs to be unique within the current request set time window. - ID uint32 `json:"id"` - - // Set in the response if there was an error. - Err string `json:"err"` - - // Warnings is a list of warnings that may be returned in the response. - Warnings []string `json:"warnings,omitempty"` -} - -type Message[T any] struct { - Header Header `json:"header"` - Data T `json:"data"` -} - -func (m Message[T]) GetID() uint32 { - return m.Header.ID -} - -type Dispatcher[Q, R any] interface { - Execute(ctx context.Context, q Message[Q]) (Message[R], error) - Close() error -} - -func (p *dispatcherPool[Q, R]) getDispatcher() *dispatcher[Q, R] { - i := int(p.counter.Add(1)) % len(p.dispatchers) - return p.dispatchers[i] -} - -func (p *dispatcherPool[Q, R]) Close() error { - return p.close() -} - -type dispatcher[Q, R any] struct { - zero Message[R] - - mu sync.RWMutex - encMu sync.Mutex - - pending map[uint32]*call[Q, R] - - inOut *inOut - - shutdown bool - closing bool -} - -type inOut struct { - sync.Mutex - stdin hugio.ReadWriteCloser - stdout hugio.ReadWriteCloser - dec *json.Decoder - enc *json.Encoder -} - -var ErrShutdown = fmt.Errorf("dispatcher is shutting down") - -var timerPool = sync.Pool{} - -func getTimer(d time.Duration) *time.Timer { - if v := timerPool.Get(); v != nil { - timer := v.(*time.Timer) - timer.Reset(d) - return timer - } - return time.NewTimer(d) -} - -func putTimer(t *time.Timer) { - if !t.Stop() { - select { - case <-t.C: - default: - } - } - timerPool.Put(t) -} - -// Execute sends a request to the dispatcher and waits for the response. -func (p *dispatcherPool[Q, R]) Execute(ctx context.Context, q Message[Q]) (Message[R], error) { - d := p.getDispatcher() - if q.GetID() == 0 { - return d.zero, errors.New("ID must not be 0 (note that this must be unique within the current request set time window)") - } - - call, err := d.newCall(q) - if err != nil { - return d.zero, err - } - - if err := d.send(call); err != nil { - return d.zero, err - } - - timer := getTimer(30 * time.Second) - defer putTimer(timer) - - select { - case call = <-call.donec: - case <-p.donec: - return d.zero, p.Err() - case <-ctx.Done(): - return d.zero, ctx.Err() - case <-timer.C: - return d.zero, errors.New("timeout") - } - - if call.err != nil { - return d.zero, call.err - } - - resp, err := call.response, p.Err() - - if err == nil && resp.Header.Err != "" { - err = errors.New(resp.Header.Err) - } - return resp, err -} - -func (d *dispatcher[Q, R]) newCall(q Message[Q]) (*call[Q, R], error) { - call := &call[Q, R]{ - donec: make(chan *call[Q, R], 1), - request: q, - } - - if d.shutdown || d.closing { - call.err = ErrShutdown - call.done() - return call, nil - } - - d.mu.Lock() - d.pending[q.GetID()] = call - d.mu.Unlock() - - return call, nil -} - -func (d *dispatcher[Q, R]) send(call *call[Q, R]) error { - d.mu.RLock() - if d.closing || d.shutdown { - d.mu.RUnlock() - return ErrShutdown - } - d.mu.RUnlock() - - d.encMu.Lock() - defer d.encMu.Unlock() - err := d.inOut.enc.Encode(call.request) - if err != nil { - return err - } - return nil -} - -func (d *dispatcher[Q, R]) input() { - var inputErr error - - for d.inOut.dec.More() { - var r Message[R] - if err := d.inOut.dec.Decode(&r); err != nil { - inputErr = fmt.Errorf("decoding response: %w", err) - break - } - - d.mu.Lock() - call, found := d.pending[r.GetID()] - if !found { - d.mu.Unlock() - panic(fmt.Errorf("call with ID %d not found", r.GetID())) - } - delete(d.pending, r.GetID()) - d.mu.Unlock() - call.response = r - call.done() - } - - // Terminate pending calls. - d.shutdown = true - if inputErr != nil { - isEOF := inputErr == io.EOF || strings.Contains(inputErr.Error(), "already closed") - if isEOF { - if d.closing { - inputErr = ErrShutdown - } else { - inputErr = io.ErrUnexpectedEOF - } - } - } - - d.mu.Lock() - defer d.mu.Unlock() - for _, call := range d.pending { - call.err = inputErr - call.done() - } -} - -type call[Q, R any] struct { - request Message[Q] - response Message[R] - err error - donec chan *call[Q, R] -} - -func (call *call[Q, R]) done() { - select { - case call.donec <- call: - default: - } -} - -// Binary represents a WebAssembly binary. -type Binary struct { - // The name of the binary. - // For quickjs, this must match the instance import name, "javy_quickjs_provider_v2". - // For the main module, we only use this for caching. - Name string - - // THe wasm binary. - Data []byte -} - -type Options struct { - Ctx context.Context - - Infof func(format string, v ...any) - - Warnf func(format string, v ...any) - - // E.g. quickjs wasm. May be omitted if not needed. - Runtime Binary - - // The main module to instantiate. - Main Binary - - CompilationCacheDir string - PoolSize int - - // Memory limit in MiB. - Memory int -} - -type CompileModuleContext struct { - Opts Options - Runtime wazero.Runtime -} - -type CompiledModule struct { - // Runtime (e.g. QuickJS) may be nil if not needed (e.g. embedded in Module). - Runtime wazero.CompiledModule - - // If Runtime is not nil, this should be the name of the instance. - RuntimeName string - - // The main module to instantiate. - // This will be insantiated multiple times in a pool, - // so it does not need a name. - Module wazero.CompiledModule -} - -// Start creates a new dispatcher pool. -func Start[Q, R any](opts Options) (Dispatcher[Q, R], error) { - if opts.Main.Data == nil { - return nil, errors.New("Main.Data must be set") - } - if opts.Main.Name == "" { - return nil, errors.New("Main.Name must be set") - } - - if opts.Runtime.Data != nil && opts.Runtime.Name == "" { - return nil, errors.New("Runtime.Name must be set") - } - - if opts.PoolSize == 0 { - opts.PoolSize = 1 - } - - return newDispatcher[Q, R](opts) -} - -type dispatcherPool[Q, R any] struct { - counter atomic.Uint32 - dispatchers []*dispatcher[Q, R] - close func() error - opts Options - - errc chan error - donec chan struct{} -} - -func (p *dispatcherPool[Q, R]) SendIfErr(err error) { - if err != nil { - p.errc <- err - } -} - -func (p *dispatcherPool[Q, R]) Err() error { - select { - case err := <-p.errc: - return err - default: - return nil - } -} - -func newDispatcher[Q, R any](opts Options) (*dispatcherPool[Q, R], error) { - if opts.Ctx == nil { - opts.Ctx = context.Background() - } - - if opts.Infof == nil { - opts.Infof = func(format string, v ...any) { - // noop - } - } - if opts.Warnf == nil { - opts.Warnf = func(format string, v ...any) { - // noop - } - } - - if opts.Memory <= 0 { - // 32 MiB - opts.Memory = 32 - } - - ctx := opts.Ctx - - // Page size is 64KB. - numPages := opts.Memory * 1024 / 64 - runtimeConfig := wazero.NewRuntimeConfig().WithMemoryLimitPages(uint32(numPages)) - - if opts.CompilationCacheDir != "" { - compilationCache, err := wazero.NewCompilationCacheWithDir(opts.CompilationCacheDir) - if err != nil { - return nil, err - } - runtimeConfig = runtimeConfig.WithCompilationCache(compilationCache) - } - - // Create a new WebAssembly Runtime. - r := wazero.NewRuntimeWithConfig(opts.Ctx, runtimeConfig) - - // Instantiate WASI, which implements system I/O such as console output. - if _, err := wasi_snapshot_preview1.Instantiate(ctx, r); err != nil { - return nil, err - } - - inOuts := make([]*inOut, opts.PoolSize) - for i := range opts.PoolSize { - var stdin, stdout hugio.ReadWriteCloser - - stdin = hugio.NewPipeReadWriteCloser() - stdout = hugio.NewPipeReadWriteCloser() - - inOuts[i] = &inOut{ - stdin: stdin, - stdout: stdout, - dec: json.NewDecoder(stdout), - enc: json.NewEncoder(stdin), - } - } - - var ( - runtimeModule wazero.CompiledModule - mainModule wazero.CompiledModule - err error - ) - - if opts.Runtime.Data != nil { - runtimeModule, err = r.CompileModule(ctx, opts.Runtime.Data) - if err != nil { - return nil, err - } - } - - mainModule, err = r.CompileModule(ctx, opts.Main.Data) - if err != nil { - return nil, err - } - - toErr := func(what string, errBuff bytes.Buffer, err error) error { - return fmt.Errorf("%s: %s: %w", what, errBuff.String(), err) - } - - run := func() error { - g, ctx := errgroup.WithContext(ctx) - for _, c := range inOuts { - c := c - g.Go(func() error { - var errBuff bytes.Buffer - ctx := context.WithoutCancel(ctx) - configBase := wazero.NewModuleConfig().WithStderr(&errBuff).WithStdout(c.stdout).WithStdin(c.stdin).WithStartFunctions() - if opts.Runtime.Data != nil { - // This needs to be anonymous, it will be resolved in the import resolver below. - runtimeInstance, err := r.InstantiateModule(ctx, runtimeModule, configBase.WithName("")) - if err != nil { - return toErr("quickjs", errBuff, err) - } - ctx = experimental.WithImportResolver(ctx, - func(name string) api.Module { - if name == opts.Runtime.Name { - return runtimeInstance - } - return nil - }, - ) - } - - mainInstance, err := r.InstantiateModule(ctx, mainModule, configBase.WithName("")) - if err != nil { - return toErr(opts.Main.Name, errBuff, err) - } - if _, err := mainInstance.ExportedFunction("_start").Call(ctx); err != nil { - return toErr(opts.Main.Name, errBuff, err) - } - - // The console.log in the Javy/quickjs WebAssembly module will write to stderr. - // In non-error situations, write that to the provided infof logger. - if errBuff.Len() > 0 { - opts.Infof("%s", errBuff.String()) - } - - return nil - }) - } - return g.Wait() - } - - dp := &dispatcherPool[Q, R]{ - dispatchers: make([]*dispatcher[Q, R], len(inOuts)), - opts: opts, - - errc: make(chan error, 10), - donec: make(chan struct{}), - } - - go func() { - // This will block until stdin is closed or it encounters an error. - err := run() - dp.SendIfErr(err) - close(dp.donec) - }() - - for i := range inOuts { - d := &dispatcher[Q, R]{ - pending: make(map[uint32]*call[Q, R]), - inOut: inOuts[i], - } - go d.input() - dp.dispatchers[i] = d - } - - dp.close = func() error { - for _, d := range dp.dispatchers { - d.closing = true - if err := d.inOut.stdin.Close(); err != nil { - return err - } - if err := d.inOut.stdout.Close(); err != nil { - return err - } - } - - // We need to wait for the WebAssembly instances to finish executing before we can close the runtime. - <-dp.donec - - if err := r.Close(ctx); err != nil { - return err - } - - // Return potential late compilation errors. - return dp.Err() - } - - return dp, dp.Err() -} - -type lazyDispatcher[Q, R any] struct { - opts Options - - dispatcher Dispatcher[Q, R] - startOnce sync.Once - started bool - startErr error -} - -func (d *lazyDispatcher[Q, R]) start() (Dispatcher[Q, R], error) { - d.startOnce.Do(func() { - start := time.Now() - d.dispatcher, d.startErr = Start[Q, R](d.opts) - d.started = true - d.opts.Infof("started dispatcher in %s", time.Since(start)) - }) - return d.dispatcher, d.startErr -} - -// Dispatchers holds all the dispatchers for the warpc package. -type Dispatchers struct { - katex *lazyDispatcher[KatexInput, KatexOutput] -} - -func (d *Dispatchers) Katex() (Dispatcher[KatexInput, KatexOutput], error) { - return d.katex.start() -} - -func (d *Dispatchers) Close() error { - var errs []error - if d.katex.started { - if err := d.katex.dispatcher.Close(); err != nil { - errs = append(errs, err) - } - } - if len(errs) == 0 { - return nil - } - return fmt.Errorf("%v", errs) -} - -// AllDispatchers creates all the dispatchers for the warpc package. -// Note that the individual dispatchers are started lazily. -// Remember to call Close on the returned Dispatchers when done. -func AllDispatchers(katexOpts Options) *Dispatchers { - if katexOpts.Runtime.Data == nil { - katexOpts.Runtime = Binary{Name: "javy_quickjs_provider_v2", Data: quickjsWasm} - } - if katexOpts.Main.Data == nil { - katexOpts.Main = Binary{Name: "renderkatex", Data: katexWasm} - } - - if katexOpts.Infof == nil { - katexOpts.Infof = func(format string, v ...any) { - // noop - } - } - - return &Dispatchers{ - katex: &lazyDispatcher[KatexInput, KatexOutput]{opts: katexOpts}, - } -} diff --git a/internal/warpc/warpc_test.go b/internal/warpc/warpc_test.go deleted file mode 100644 index 2ee4c3de5..000000000 --- a/internal/warpc/warpc_test.go +++ /dev/null @@ -1,475 +0,0 @@ -// 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 warpc - -import ( - "context" - _ "embed" - "fmt" - "sync" - "sync/atomic" - "testing" - - qt "github.com/frankban/quicktest" -) - -//go:embed wasm/greet.wasm -var greetWasm []byte - -type person struct { - Name string `json:"name"` -} - -func TestKatex(t *testing.T) { - c := qt.New(t) - - opts := Options{ - PoolSize: 8, - Runtime: quickjsBinary, - Main: katexBinary, - } - - d, err := Start[KatexInput, KatexOutput](opts) - c.Assert(err, qt.IsNil) - - defer d.Close() - - runExpression := func(c *qt.C, id uint32, expression string) (Message[KatexOutput], error) { - c.Helper() - - ctx := context.Background() - - input := KatexInput{ - Expression: expression, - Options: KatexOptions{ - Output: "html", - DisplayMode: true, - ThrowOnError: true, - }, - } - - message := Message[KatexInput]{ - Header: Header{ - Version: currentVersion, - ID: uint32(id), - }, - Data: input, - } - - return d.Execute(ctx, message) - } - - c.Run("Simple", func(c *qt.C) { - id := uint32(32) - result, err := runExpression(c, id, "c = \\pm\\sqrt{a^2 + b^2}") - c.Assert(err, qt.IsNil) - c.Assert(result.GetID(), qt.Equals, id) - }) - - c.Run("Chemistry", func(c *qt.C) { - id := uint32(32) - result, err := runExpression(c, id, "C_p[\\ce{H2O(l)}] = \\pu{75.3 J // mol K}") - c.Assert(err, qt.IsNil) - c.Assert(result.GetID(), qt.Equals, id) - }) - - c.Run("Invalid expression", func(c *qt.C) { - id := uint32(32) - result, err := runExpression(c, id, "c & \\foo\\") - c.Assert(err, qt.IsNotNil) - c.Assert(result.GetID(), qt.Equals, id) - }) -} - -func TestGreet(t *testing.T) { - c := qt.New(t) - opts := Options{ - PoolSize: 1, - Runtime: quickjsBinary, - Main: greetBinary, - Infof: t.Logf, - } - - for range 2 { - func() { - d, err := Start[person, greeting](opts) - if err != nil { - t.Fatal(err) - } - - defer func() { - c.Assert(d.Close(), qt.IsNil) - }() - - ctx := context.Background() - - inputMessage := Message[person]{ - Header: Header{ - Version: currentVersion, - }, - Data: person{ - Name: "Person", - }, - } - - for j := range 20 { - inputMessage.Header.ID = uint32(j + 1) - g, err := d.Execute(ctx, inputMessage) - if err != nil { - t.Fatal(err) - } - if g.Data.Greeting != "Hello Person!" { - t.Fatalf("got: %v", g) - } - if g.GetID() != inputMessage.GetID() { - t.Fatalf("%d vs %d", g.GetID(), inputMessage.GetID()) - } - } - }() - } -} - -func TestGreetParallel(t *testing.T) { - c := qt.New(t) - - opts := Options{ - Runtime: quickjsBinary, - Main: greetBinary, - PoolSize: 4, - } - d, err := Start[person, greeting](opts) - c.Assert(err, qt.IsNil) - defer func() { - c.Assert(d.Close(), qt.IsNil) - }() - - var wg sync.WaitGroup - - for i := 1; i <= 10; i++ { - wg.Add(1) - go func(i int) { - defer wg.Done() - - ctx := context.Background() - - for j := range 5 { - base := i * 100 - id := uint32(base + j) - - inputPerson := person{ - Name: fmt.Sprintf("Person %d", id), - } - inputMessage := Message[person]{ - Header: Header{ - Version: currentVersion, - ID: id, - }, - Data: inputPerson, - } - g, err := d.Execute(ctx, inputMessage) - if err != nil { - t.Error(err) - return - } - - c.Assert(g.Data.Greeting, qt.Equals, fmt.Sprintf("Hello Person %d!", id)) - c.Assert(g.GetID(), qt.Equals, inputMessage.GetID()) - - } - }(i) - - } - - wg.Wait() -} - -func TestKatexParallel(t *testing.T) { - c := qt.New(t) - - opts := Options{ - Runtime: quickjsBinary, - Main: katexBinary, - PoolSize: 6, - } - d, err := Start[KatexInput, KatexOutput](opts) - c.Assert(err, qt.IsNil) - defer func() { - c.Assert(d.Close(), qt.IsNil) - }() - - var wg sync.WaitGroup - - for i := 1; i <= 10; i++ { - wg.Add(1) - go func(i int) { - defer wg.Done() - - ctx := context.Background() - - for j := range 1 { - base := i * 100 - id := uint32(base + j) - - input := katexInputTemplate - inputMessage := Message[KatexInput]{ - Header: Header{ - Version: currentVersion, - ID: id, - }, - Data: input, - } - - result, err := d.Execute(ctx, inputMessage) - if err != nil { - t.Error(err) - return - } - - if result.GetID() != inputMessage.GetID() { - t.Errorf("%d vs %d", result.GetID(), inputMessage.GetID()) - return - } - } - }(i) - - } - - wg.Wait() -} - -func BenchmarkExecuteKatex(b *testing.B) { - opts := Options{ - Runtime: quickjsBinary, - Main: katexBinary, - } - d, err := Start[KatexInput, KatexOutput](opts) - if err != nil { - b.Fatal(err) - } - defer d.Close() - - ctx := context.Background() - - input := katexInputTemplate - - b.ResetTimer() - for i := 0; i < b.N; i++ { - message := Message[KatexInput]{ - Header: Header{ - Version: currentVersion, - ID: uint32(i + 1), - }, - Data: input, - } - - result, err := d.Execute(ctx, message) - if err != nil { - b.Fatal(err) - } - - if result.GetID() != message.GetID() { - b.Fatalf("%d vs %d", result.GetID(), message.GetID()) - } - - } -} - -func BenchmarkKatexStartStop(b *testing.B) { - optsTemplate := Options{ - Runtime: quickjsBinary, - Main: katexBinary, - CompilationCacheDir: b.TempDir(), - } - - runBench := func(b *testing.B, opts Options) { - for i := 0; i < b.N; i++ { - d, err := Start[KatexInput, KatexOutput](opts) - if err != nil { - b.Fatal(err) - } - if err := d.Close(); err != nil { - b.Fatal(err) - } - } - } - - for _, poolSize := range []int{1, 8, 16} { - - name := fmt.Sprintf("PoolSize%d", poolSize) - - b.Run(name, func(b *testing.B) { - opts := optsTemplate - opts.PoolSize = poolSize - runBench(b, opts) - }) - - } -} - -var katexInputTemplate = KatexInput{ - Expression: "c = \\pm\\sqrt{a^2 + b^2}", - Options: KatexOptions{Output: "html", DisplayMode: true}, -} - -func BenchmarkExecuteKatexPara(b *testing.B) { - optsTemplate := Options{ - Runtime: quickjsBinary, - Main: katexBinary, - } - - runBench := func(b *testing.B, opts Options) { - d, err := Start[KatexInput, KatexOutput](opts) - if err != nil { - b.Fatal(err) - } - defer d.Close() - - ctx := context.Background() - - b.ResetTimer() - - var id atomic.Uint32 - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - message := Message[KatexInput]{ - Header: Header{ - Version: currentVersion, - ID: id.Add(1), - }, - Data: katexInputTemplate, - } - - result, err := d.Execute(ctx, message) - if err != nil { - b.Fatal(err) - } - if result.GetID() != message.GetID() { - b.Fatalf("%d vs %d", result.GetID(), message.GetID()) - } - } - }) - } - - for _, poolSize := range []int{1, 8, 16} { - name := fmt.Sprintf("PoolSize%d", poolSize) - - b.Run(name, func(b *testing.B) { - opts := optsTemplate - opts.PoolSize = poolSize - runBench(b, opts) - }) - } -} - -func BenchmarkExecuteGreet(b *testing.B) { - opts := Options{ - Runtime: quickjsBinary, - Main: greetBinary, - } - d, err := Start[person, greeting](opts) - if err != nil { - b.Fatal(err) - } - defer d.Close() - - ctx := context.Background() - - input := person{ - Name: "Person", - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - message := Message[person]{ - Header: Header{ - Version: currentVersion, - ID: uint32(i + 1), - }, - Data: input, - } - result, err := d.Execute(ctx, message) - if err != nil { - b.Fatal(err) - } - - if result.GetID() != message.GetID() { - b.Fatalf("%d vs %d", result.GetID(), message.GetID()) - } - - } -} - -func BenchmarkExecuteGreetPara(b *testing.B) { - opts := Options{ - Runtime: quickjsBinary, - Main: greetBinary, - PoolSize: 8, - } - - d, err := Start[person, greeting](opts) - if err != nil { - b.Fatal(err) - } - defer d.Close() - - ctx := context.Background() - - inputTemplate := person{ - Name: "Person", - } - - b.ResetTimer() - - var id atomic.Uint32 - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - message := Message[person]{ - Header: Header{ - Version: currentVersion, - ID: id.Add(1), - }, - Data: inputTemplate, - } - - result, err := d.Execute(ctx, message) - if err != nil { - b.Fatal(err) - } - if result.GetID() != message.GetID() { - b.Fatalf("%d vs %d", result.GetID(), message.GetID()) - } - } - }) -} - -type greeting struct { - Greeting string `json:"greeting"` -} - -var ( - greetBinary = Binary{ - Name: "greet", - Data: greetWasm, - } - - katexBinary = Binary{ - Name: "renderkatex", - Data: katexWasm, - } - - quickjsBinary = Binary{ - Name: "javy_quickjs_provider_v2", - Data: quickjsWasm, - } -) diff --git a/internal/warpc/wasm/greet.wasm b/internal/warpc/wasm/greet.wasm deleted file mode 100644 index 944199b40..000000000 Binary files a/internal/warpc/wasm/greet.wasm and /dev/null differ diff --git a/internal/warpc/wasm/quickjs.wasm b/internal/warpc/wasm/quickjs.wasm deleted file mode 100644 index 569c53a23..000000000 Binary files a/internal/warpc/wasm/quickjs.wasm and /dev/null differ diff --git a/internal/warpc/wasm/renderkatex.wasm b/internal/warpc/wasm/renderkatex.wasm deleted file mode 100644 index b8b21c16b..000000000 Binary files a/internal/warpc/wasm/renderkatex.wasm and /dev/null differ diff --git a/internal/warpc/watchtestscripts.sh b/internal/warpc/watchtestscripts.sh deleted file mode 100755 index fbc90b648..000000000 --- a/internal/warpc/watchtestscripts.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -trap exit SIGINT - -while true; do find . -type f -name "*.js" | entr -pd ./build.sh; done \ No newline at end of file diff --git a/langs/config.go b/langs/config.go deleted file mode 100644 index 7cca0f5e7..000000000 --- a/langs/config.go +++ /dev/null @@ -1,58 +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 langs - -import ( - "errors" - - "github.com/gohugoio/hugo/common/maps" - "github.com/mitchellh/mapstructure" -) - -// LanguageConfig holds the configuration for a single language. -// This is what is read from the config file. -type LanguageConfig struct { - // The language name, e.g. "English". - LanguageName string - - // The language code, e.g. "en-US". - LanguageCode string - - // The language title. When set, this will - // override site.Title for this language. - Title string - - // The language direction, e.g. "ltr" or "rtl". - LanguageDirection string - - // The language weight. When set to a non-zero value, this will - // be the main sort criteria for the language. - Weight int - - // Set to true to disable this language. - Disabled bool -} - -func DecodeConfig(m map[string]any) (map[string]LanguageConfig, error) { - m = maps.CleanConfigStringMap(m) - var langs map[string]LanguageConfig - - if err := mapstructure.WeakDecode(m, &langs); err != nil { - return nil, err - } - if len(langs) == 0 { - return nil, errors.New("no languages configured") - } - return langs, nil -} diff --git a/langs/i18n/i18n.go b/langs/i18n/i18n.go deleted file mode 100644 index e97ec8b8d..000000000 --- a/langs/i18n/i18n.go +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright 2017 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 i18n - -import ( - "context" - "fmt" - "reflect" - "strings" - - "github.com/spf13/cast" - - "github.com/gohugoio/hugo/common/hreflect" - "github.com/gohugoio/hugo/common/loggers" - "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/resources/page" - - "github.com/gohugoio/go-i18n/v2/i18n" -) - -type translateFunc func(ctx context.Context, translationID string, templateData any) string - -// Translator handles i18n translations. -type Translator struct { - translateFuncs map[string]translateFunc - cfg config.AllProvider - logger loggers.Logger -} - -// NewTranslator creates a new Translator for the given language bundle and configuration. -func NewTranslator(b *i18n.Bundle, cfg config.AllProvider, logger loggers.Logger) Translator { - t := Translator{cfg: cfg, logger: logger, translateFuncs: make(map[string]translateFunc)} - t.initFuncs(b) - return t -} - -// Func gets the translate func for the given language, or for the default -// configured language if not found. -func (t Translator) Func(lang string) translateFunc { - if f, ok := t.translateFuncs[lang]; ok { - return f - } - t.logger.Infof("Translation func for language %v not found, use default.", lang) - if f, ok := t.translateFuncs[t.cfg.DefaultContentLanguage()]; ok { - return f - } - - t.logger.Infoln("i18n not initialized; if you need string translations, check that you have a bundle in /i18n that matches the site language or the default language.") - return func(ctx context.Context, translationID string, args any) string { - return "" - } -} - -func (t Translator) initFuncs(bndl *i18n.Bundle) { - enableMissingTranslationPlaceholders := t.cfg.EnableMissingTranslationPlaceholders() - for _, lang := range bndl.LanguageTags() { - currentLang := lang - currentLangStr := currentLang.String() - // This may be pt-BR; make it case insensitive. - currentLangKey := strings.ToLower(strings.TrimPrefix(currentLangStr, artificialLangTagPrefix)) - localizer := i18n.NewLocalizer(bndl, currentLangStr) - t.translateFuncs[currentLangKey] = func(ctx context.Context, translationID string, templateData any) string { - pluralCount := getPluralCount(templateData) - - if templateData != nil { - tp := reflect.TypeOf(templateData) - if hreflect.IsInt(tp.Kind()) { - // This was how go-i18n worked in v1, - // and we keep it like this to avoid breaking - // lots of sites in the wild. - templateData = intCount(cast.ToInt(templateData)) - } else { - if p, ok := templateData.(page.Page); ok { - // See issue 10782. - // The i18n has its own template handling and does not know about - // the context.Context. - // A common pattern is to pass Page to i18n, and use .ReadingTime etc. - // We need to improve this, but that requires some upstream changes. - // For now, just create a wrapper. - templateData = page.PageWithContext{Page: p, Ctx: ctx} - } - } - } - - translated, translatedLang, err := localizer.LocalizeWithTag(&i18n.LocalizeConfig{ - MessageID: translationID, - TemplateData: templateData, - PluralCount: pluralCount, - }) - - sameLang := currentLang == translatedLang - - if err == nil && sameLang { - return translated - } - - if err != nil && sameLang && translated != "" { - // See #8492 - // TODO(bep) this needs to be improved/fixed upstream, - // but currently we get an error even if the fallback to - // "other" succeeds. - if fmt.Sprintf("%T", err) == "i18n.pluralFormNotFoundError" { - return translated - } - } - - if _, ok := err.(*i18n.MessageNotFoundErr); !ok { - t.logger.Warnf("Failed to get translated string for language %q and ID %q: %s", currentLangStr, translationID, err) - } - - if t.cfg.PrintI18nWarnings() { - t.logger.Warnf("i18n|MISSING_TRANSLATION|%s|%s", currentLangStr, translationID) - } - - if enableMissingTranslationPlaceholders { - return "[i18n] " + translationID - } - - return translated - } - } -} - -// intCount wraps the Count method. -type intCount int - -func (c intCount) Count() int { - return int(c) -} - -const countFieldName = "Count" - -// getPluralCount gets the plural count as a string (floats) or an integer. -// If v is nil, nil is returned. -func getPluralCount(v any) any { - if v == nil { - // i18n called without any argument, make sure it does not - // get any plural count. - return nil - } - - switch v := v.(type) { - case map[string]any: - for k, vv := range v { - if strings.EqualFold(k, countFieldName) { - return toPluralCountValue(vv) - } - } - default: - vv := reflect.Indirect(reflect.ValueOf(v)) - if vv.Kind() == reflect.Interface && !vv.IsNil() { - vv = vv.Elem() - } - tp := vv.Type() - - if tp.Kind() == reflect.Struct { - f := vv.FieldByName(countFieldName) - if f.IsValid() { - return toPluralCountValue(f.Interface()) - } - m := hreflect.GetMethodByName(vv, countFieldName) - if m.IsValid() && m.Type().NumIn() == 0 && m.Type().NumOut() == 1 { - c := m.Call(nil) - return toPluralCountValue(c[0].Interface()) - } - } - } - - return toPluralCountValue(v) -} - -// go-i18n expects floats to be represented by string. -func toPluralCountValue(in any) any { - k := reflect.TypeOf(in).Kind() - switch { - case hreflect.IsFloat(k): - f := cast.ToString(in) - if !strings.Contains(f, ".") { - f += ".0" - } - return f - case k == reflect.String: - if _, err := cast.ToFloat64E(in); err == nil { - return in - } - // A non-numeric value. - return nil - default: - if i, err := cast.ToIntE(in); err == nil { - return i - } - return nil - } -} diff --git a/langs/i18n/i18n_integration_test.go b/langs/i18n/i18n_integration_test.go deleted file mode 100644 index b62a2900e..000000000 --- a/langs/i18n/i18n_integration_test.go +++ /dev/null @@ -1,128 +0,0 @@ -// 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 i18n_test - -import ( - "testing" - - "github.com/gohugoio/hugo/hugolib" -) - -func TestI18nFromTheme(t *testing.T) { - t.Parallel() - - files := ` --- config.toml -- -[module] -[[module.imports]] -path = "mytheme" --- i18n/en.toml -- -[l1] -other = 'l1main' -[l2] -other = 'l2main' --- themes/mytheme/i18n/en.toml -- -[l1] -other = 'l1theme' -[l2] -other = 'l2theme' -[l3] -other = 'l3theme' --- layouts/index.html -- -l1: {{ i18n "l1" }}|l2: {{ i18n "l2" }}|l3: {{ i18n "l3" }} - -` - - b := hugolib.Test(t, files) - - b.AssertFileContent("public/index.html", ` -l1: l1main|l2: l2main|l3: l3theme - `) -} - -func TestPassPageToI18n(t *testing.T) { - t.Parallel() - - files := ` --- config.toml -- --- content/_index.md -- ---- -title: "Home" ---- -Duis quis irure id nisi sunt minim aliqua occaecat. Aliqua cillum labore consectetur quis culpa tempor quis non officia cupidatat in ad cillum. Velit irure pariatur nisi adipisicing officia reprehenderit commodo esse non. - -Ullamco cupidatat nostrud ut reprehenderit. Consequat nisi culpa magna amet tempor velit reprehenderit. Ad minim eiusmod tempor nostrud eu aliquip consectetur commodo ut in aliqua enim. Cupidatat voluptate laborum consequat qui nulla laborum laborum aute ea culpa nulla dolor cillum veniam. Commodo esse tempor qui labore aute aliqua sint nulla do. - -Ad deserunt esse nostrud labore. Amet reprehenderit fugiat nostrud eu reprehenderit sit reprehenderit minim deserunt esse id occaecat cillum. Ad qui Lorem cillum laboris ipsum anim in culpa ad dolor consectetur minim culpa. - -Lorem cupidatat officia aute in eu commodo anim nulla deserunt occaecat reprehenderit dolore. Eu cupidatat reprehenderit ipsum sit laboris proident. Duis quis nulla tempor adipisicing. Adipisicing amet ad reprehenderit non mollit. Cupidatat proident tempor laborum sit ipsum adipisicing sunt magna labore. Eu irure nostrud cillum exercitation tempor proident. Laborum magna nisi consequat do sint occaecat magna incididunt. - -Sit mollit amet esse dolore in labore aliquip eu duis officia incididunt. Esse veniam labore excepteur eiusmod occaecat ullamco magna sunt. Ipsum occaecat exercitation anim fugiat in amet excepteur excepteur aliquip laborum. Aliquip aliqua consequat officia sit sint amet aliqua ipsum eu veniam. Id enim quis ea in eu consequat exercitation occaecat veniam consequat anim nulla adipisicing minim. Ut duis cillum laboris duis non commodo eu aliquip tempor nisi aute do. - -Ipsum nulla esse excepteur ut aliqua esse incididunt deserunt veniam dolore est laborum nisi veniam. Magna eiusmod Lorem do tempor incididunt ut aute aliquip ipsum ea laboris culpa. Occaecat do officia velit fugiat culpa eu minim magna sint occaecat sunt. Duis magna proident incididunt est cupidatat proident esse proident ut ipsum non dolor Lorem eiusmod. Officia quis irure id eu aliquip. - -Duis anim elit in officia in in aliquip est. Aliquip nisi labore qui elit elit cupidatat ut labore incididunt eiusmod ipsum. Sit irure nulla non cupidatat exercitation sit culpa nisi ex dolore. Culpa nisi duis duis eiusmod commodo nulla. - -Et magna aliqua amet qui mollit. Eiusmod aute ut anim ea est fugiat non nisi in laborum ullamco. Proident mollit sunt nostrud irure esse sunt eiusmod deserunt dolor. Irure aute ad magna est consequat duis cupidatat consequat. Enim tempor aute cillum quis ea do enim proident incididunt aliquip cillum tempor minim. Nulla minim tempor proident in excepteur consectetur veniam. - -Exercitation tempor nulla incididunt deserunt laboris ad incididunt aliqua exercitation. Adipisicing laboris veniam aute eiusmod qui magna fugiat velit. Aute quis officia anim commodo id fugiat nostrud est. Quis ipsum amet velit adipisicing eu anim minim eu est in culpa aute. Esse in commodo irure enim proident reprehenderit ullamco in dolore aute cillum. - -Irure excepteur ex occaecat ipsum laboris fugiat exercitation. Exercitation adipisicing velit excepteur eu culpa consequat exercitation dolore. In laboris aute quis qui mollit minim culpa. Magna velit ea aliquip veniam fugiat mollit veniam. --- i18n/en.toml -- -[a] -other = 'Reading time: {{ .ReadingTime }}' --- layouts/index.html -- -i18n: {{ i18n "a" . }}| - -` - - b := hugolib.Test(t, files) - - b.AssertFileContent("public/index.html", ` - i18n: Reading time: 3| - `) -} - -// Issue 9216 -func TestI18nDefaultContentLanguage(t *testing.T) { - t.Parallel() - - files := ` --- config.toml -- -disableKinds = ['RSS','sitemap','taxonomy','term','page','section'] -defaultContentLanguage = 'es' -defaultContentLanguageInSubdir = true -[languages.es] -[languages.fr] --- i18n/es.toml -- -cat = 'gato' --- i18n/fr.toml -- -# this file intentionally empty --- layouts/index.html -- -{{ .Title }}_{{ T "cat" }} --- content/_index.fr.md -- ---- -title: home_fr ---- --- content/_index.md -- ---- -title: home_es ---- -` - - b := hugolib.Test(t, files) - - b.AssertFileContent("public/es/index.html", `home_es_gato`) - b.AssertFileContent("public/fr/index.html", `home_fr_gato`) -} diff --git a/langs/i18n/i18n_test.go b/langs/i18n/i18n_test.go deleted file mode 100644 index a23cee539..000000000 --- a/langs/i18n/i18n_test.go +++ /dev/null @@ -1,519 +0,0 @@ -// Copyright 2017 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 i18n - -import ( - "context" - "fmt" - "path/filepath" - "testing" - - "github.com/bep/logg" - "github.com/gohugoio/hugo/common/types" - "github.com/gohugoio/hugo/config/testconfig" - - "github.com/gohugoio/hugo/resources/page" - "github.com/spf13/afero" - - "github.com/gohugoio/hugo/deps" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/config" -) - -type i18nTest struct { - name string - data map[string][]byte - args any - lang, id, expected, expectedFlag string -} - -var i18nTests = []i18nTest{ - // All translations present - { - name: "all-present", - data: map[string][]byte{ - "en.toml": []byte("[hello]\nother = \"Hello, World!\""), - "es.toml": []byte("[hello]\nother = \"¡Hola, Mundo!\""), - }, - args: nil, - lang: "es", - id: "hello", - expected: "¡Hola, Mundo!", - expectedFlag: "¡Hola, Mundo!", - }, - // Translation missing in current language but present in default - { - name: "present-in-default", - data: map[string][]byte{ - "en.toml": []byte("[hello]\nother = \"Hello, World!\""), - "es.toml": []byte("[goodbye]\nother = \"¡Adiós, Mundo!\""), - }, - args: nil, - lang: "es", - id: "hello", - expected: "Hello, World!", - expectedFlag: "[i18n] hello", - }, - // Translation missing in default language but present in current - { - name: "present-in-current", - data: map[string][]byte{ - "en.toml": []byte("[goodbye]\nother = \"Goodbye, World!\""), - "es.toml": []byte("[hello]\nother = \"¡Hola, Mundo!\""), - }, - args: nil, - lang: "es", - id: "hello", - expected: "¡Hola, Mundo!", - expectedFlag: "¡Hola, Mundo!", - }, - // Translation missing in both default and current language - { - name: "missing", - data: map[string][]byte{ - "en.toml": []byte("[goodbye]\nother = \"Goodbye, World!\""), - "es.toml": []byte("[goodbye]\nother = \"¡Adiós, Mundo!\""), - }, - args: nil, - lang: "es", - id: "hello", - expected: "", - expectedFlag: "[i18n] hello", - }, - // Default translation file missing or empty - { - name: "file-missing", - data: map[string][]byte{ - "en.toml": []byte(""), - }, - args: nil, - lang: "es", - id: "hello", - expected: "", - expectedFlag: "[i18n] hello", - }, - // Context provided - { - name: "context-provided", - data: map[string][]byte{ - "en.toml": []byte("[wordCount]\nother = \"Hello, {{.WordCount}} people!\""), - "es.toml": []byte("[wordCount]\nother = \"¡Hola, {{.WordCount}} gente!\""), - }, - args: struct { - WordCount int - }{ - 50, - }, - lang: "es", - id: "wordCount", - expected: "¡Hola, 50 gente!", - expectedFlag: "¡Hola, 50 gente!", - }, - // https://github.com/gohugoio/hugo/issues/7787 - { - name: "readingTime-one", - data: map[string][]byte{ - "en.toml": []byte(`[readingTime] -one = "One minute to read" -other = "{{ .Count }} minutes to read" -`), - }, - args: 1, - lang: "en", - id: "readingTime", - expected: "One minute to read", - expectedFlag: "One minute to read", - }, - { - name: "readingTime-many-dot", - data: map[string][]byte{ - "en.toml": []byte(`[readingTime] -one = "One minute to read" -other = "{{ . }} minutes to read" -`), - }, - args: 21, - lang: "en", - id: "readingTime", - expected: "21 minutes to read", - expectedFlag: "21 minutes to read", - }, - { - name: "readingTime-many", - data: map[string][]byte{ - "en.toml": []byte(`[readingTime] -one = "One minute to read" -other = "{{ .Count }} minutes to read" -`), - }, - args: 21, - lang: "en", - id: "readingTime", - expected: "21 minutes to read", - expectedFlag: "21 minutes to read", - }, - // Issue #8454 - { - name: "readingTime-map-one", - data: map[string][]byte{ - "en.toml": []byte(`[readingTime] -one = "One minute to read" -other = "{{ .Count }} minutes to read" -`), - }, - args: map[string]any{"Count": 1}, - lang: "en", - id: "readingTime", - expected: "One minute to read", - expectedFlag: "One minute to read", - }, - { - name: "readingTime-string-one", - data: map[string][]byte{ - "en.toml": []byte(`[readingTime] -one = "One minute to read" -other = "{{ . }} minutes to read" -`), - }, - args: "1", - lang: "en", - id: "readingTime", - expected: "One minute to read", - expectedFlag: "One minute to read", - }, - { - name: "readingTime-map-many", - data: map[string][]byte{ - "en.toml": []byte(`[readingTime] -one = "One minute to read" -other = "{{ .Count }} minutes to read" -`), - }, - args: map[string]any{"Count": 21}, - lang: "en", - id: "readingTime", - expected: "21 minutes to read", - expectedFlag: "21 minutes to read", - }, - { - name: "argument-float", - data: map[string][]byte{ - "en.toml": []byte(`[float] -other = "Number is {{ . }}" -`), - }, - args: 22.5, - lang: "en", - id: "float", - expected: "Number is 22.5", - expectedFlag: "Number is 22.5", - }, - // Same id and translation in current language - // https://github.com/gohugoio/hugo/issues/2607 - { - name: "same-id-and-translation", - data: map[string][]byte{ - "es.toml": []byte("[hello]\nother = \"hello\""), - "en.toml": []byte("[hello]\nother = \"hi\""), - }, - args: nil, - lang: "es", - id: "hello", - expected: "hello", - expectedFlag: "hello", - }, - // Translation missing in current language, but same id and translation in default - { - name: "same-id-and-translation-default", - data: map[string][]byte{ - "es.toml": []byte("[bye]\nother = \"bye\""), - "en.toml": []byte("[hello]\nother = \"hello\""), - }, - args: nil, - lang: "es", - id: "hello", - expected: "hello", - expectedFlag: "[i18n] hello", - }, - // Unknown language code should get its plural spec from en - { - name: "unknown-language-code", - data: map[string][]byte{ - "en.toml": []byte(`[readingTime] -one ="one minute read" -other = "{{.Count}} minutes read"`), - "klingon.toml": []byte(`[readingTime] -one = "eitt minutt med lesing" -other = "{{ .Count }} minuttar lesing"`), - }, - args: 3, - lang: "klingon", - id: "readingTime", - expected: "3 minuttar lesing", - expectedFlag: "3 minuttar lesing", - }, - // Issue #7838 - { - name: "unknown-language-codes", - data: map[string][]byte{ - "en.toml": []byte(`[readingTime] -one ="en one" -other = "en count {{.Count}}"`), - "a1.toml": []byte(`[readingTime] -one = "a1 one" -other = "a1 count {{ .Count }}"`), - "a2.toml": []byte(`[readingTime] -one = "a2 one" -other = "a2 count {{ .Count }}"`), - }, - args: 3, - lang: "a2", - id: "readingTime", - expected: "a2 count 3", - expectedFlag: "a2 count 3", - }, - // https://github.com/gohugoio/hugo/issues/7798 - { - name: "known-language-missing-plural", - data: map[string][]byte{ - "oc.toml": []byte(`[oc] -one = "abc"`), - }, - args: 1, - lang: "oc", - id: "oc", - expected: "abc", - expectedFlag: "abc", - }, - // https://github.com/gohugoio/hugo/issues/7794 - { - name: "dotted-bare-key", - data: map[string][]byte{ - "en.toml": []byte(`"shop_nextPage.one" = "Show Me The Money" -`), - }, - args: nil, - lang: "en", - id: "shop_nextPage.one", - expected: "Show Me The Money", - expectedFlag: "Show Me The Money", - }, - // https: //github.com/gohugoio/hugo/issues/7804 - { - name: "lang-with-hyphen", - data: map[string][]byte{ - "pt-br.toml": []byte(`foo.one = "abc"`), - }, - args: 1, - lang: "pt-br", - id: "foo", - expected: "abc", - expectedFlag: "abc", - }, -} - -func TestPlural(t *testing.T) { - c := qt.New(t) - - for _, test := range []struct { - name string - lang string - id string - templ string - variants []types.KeyValue - }{ - { - name: "English", - lang: "en", - id: "hour", - templ: ` -[hour] -one = "{{ . }} hour" -other = "{{ . }} hours"`, - variants: []types.KeyValue{ - {Key: 1, Value: "1 hour"}, - {Key: "1", Value: "1 hour"}, - {Key: 1.5, Value: "1.5 hours"}, - {Key: "1.5", Value: "1.5 hours"}, - {Key: 2, Value: "2 hours"}, - {Key: "2", Value: "2 hours"}, - }, - }, - { - name: "Other only", - lang: "en", - id: "hour", - templ: ` -[hour] -other = "{{ with . }}{{ . }}{{ end }} hours"`, - variants: []types.KeyValue{ - {Key: 1, Value: "1 hours"}, - {Key: "1", Value: "1 hours"}, - {Key: 2, Value: "2 hours"}, - {Key: nil, Value: " hours"}, - }, - }, - { - name: "Polish", - lang: "pl", - id: "day", - templ: ` -[day] -one = "{{ . }} miesiąc" -few = "{{ . }} miesiące" -many = "{{ . }} miesięcy" -other = "{{ . }} miesiąca" -`, - variants: []types.KeyValue{ - {Key: 1, Value: "1 miesiąc"}, - {Key: 2, Value: "2 miesiące"}, - {Key: 100, Value: "100 miesięcy"}, - {Key: "100.0", Value: "100.0 miesiąca"}, - {Key: 100.0, Value: "100 miesiąca"}, - }, - }, - } { - c.Run(test.name, func(c *qt.C) { - cfg := config.New() - cfg.Set("enableMissingTranslationPlaceholders", true) - cfg.Set("publishDir", "public") - afs := afero.NewMemMapFs() - - err := afero.WriteFile(afs, filepath.Join("i18n", test.lang+".toml"), []byte(test.templ), 0o755) - c.Assert(err, qt.IsNil) - - d, tp := prepareDeps(afs, cfg) - - f := tp.t.Func(test.lang) - ctx := context.Background() - - for _, variant := range test.variants { - c.Assert(f(ctx, test.id, variant.Key), qt.Equals, variant.Value, qt.Commentf("input: %v", variant.Key)) - c.Assert(d.Log.LoggCount(logg.LevelWarn), qt.Equals, 0) - } - }) - } -} - -func doTestI18nTranslate(t testing.TB, test i18nTest, cfg config.Provider) string { - tp := prepareTranslationProvider(t, test, cfg) - f := tp.t.Func(test.lang) - return f(context.Background(), test.id, test.args) -} - -type countField struct { - Count any -} - -type noCountField struct { - Counts int -} - -type countMethod struct{} - -func (c countMethod) Count() any { - return 32.5 -} - -func TestGetPluralCount(t *testing.T) { - c := qt.New(t) - - c.Assert(getPluralCount(map[string]any{"Count": 32}), qt.Equals, 32) - c.Assert(getPluralCount(map[string]any{"Count": 1}), qt.Equals, 1) - c.Assert(getPluralCount(map[string]any{"Count": 1.5}), qt.Equals, "1.5") - c.Assert(getPluralCount(map[string]any{"Count": "32"}), qt.Equals, "32") - c.Assert(getPluralCount(map[string]any{"Count": "32.5"}), qt.Equals, "32.5") - c.Assert(getPluralCount(map[string]any{"count": 32}), qt.Equals, 32) - c.Assert(getPluralCount(map[string]any{"Count": "32"}), qt.Equals, "32") - c.Assert(getPluralCount(map[string]any{"Counts": 32}), qt.Equals, nil) - c.Assert(getPluralCount("foo"), qt.Equals, nil) - c.Assert(getPluralCount(countField{Count: 22}), qt.Equals, 22) - c.Assert(getPluralCount(countField{Count: 1.5}), qt.Equals, "1.5") - c.Assert(getPluralCount(&countField{Count: 22}), qt.Equals, 22) - c.Assert(getPluralCount(noCountField{Counts: 23}), qt.Equals, nil) - c.Assert(getPluralCount(countMethod{}), qt.Equals, "32.5") - c.Assert(getPluralCount(&countMethod{}), qt.Equals, "32.5") - - c.Assert(getPluralCount(1234), qt.Equals, 1234) - c.Assert(getPluralCount(1234.4), qt.Equals, "1234.4") - c.Assert(getPluralCount(1234.0), qt.Equals, "1234.0") - c.Assert(getPluralCount("1234"), qt.Equals, "1234") - c.Assert(getPluralCount("0.5"), qt.Equals, "0.5") - c.Assert(getPluralCount(nil), qt.Equals, nil) -} - -func prepareTranslationProvider(t testing.TB, test i18nTest, cfg config.Provider) *TranslationProvider { - c := qt.New(t) - afs := afero.NewMemMapFs() - - for file, content := range test.data { - err := afero.WriteFile(afs, filepath.Join("i18n", file), []byte(content), 0o755) - c.Assert(err, qt.IsNil) - } - - _, tp := prepareDeps(afs, cfg) - return tp -} - -func prepareDeps(afs afero.Fs, cfg config.Provider) (*deps.Deps, *TranslationProvider) { - d := testconfig.GetTestDeps(afs, cfg) - translationProvider := NewTranslationProvider() - d.TranslationProvider = translationProvider - d.Site = page.NewDummyHugoSite(d.Conf) - if err := d.Compile(nil); err != nil { - panic(err) - } - return d, translationProvider -} - -func TestI18nTranslate(t *testing.T) { - c := qt.New(t) - var actual, expected string - v := config.New() - - // Test without and with placeholders - for _, enablePlaceholders := range []bool{false, true} { - v.Set("enableMissingTranslationPlaceholders", enablePlaceholders) - - for _, test := range i18nTests { - c.Run(fmt.Sprintf("%s-%t", test.name, enablePlaceholders), func(c *qt.C) { - if enablePlaceholders { - expected = test.expectedFlag - } else { - expected = test.expected - } - actual = doTestI18nTranslate(c, test, v) - c.Assert(actual, qt.Equals, expected) - }) - } - } -} - -func BenchmarkI18nTranslate(b *testing.B) { - v := config.New() - for _, test := range i18nTests { - b.Run(test.name, func(b *testing.B) { - tp := prepareTranslationProvider(b, test, v) - b.ResetTimer() - for i := 0; i < b.N; i++ { - f := tp.t.Func(test.lang) - actual := f(context.Background(), test.id, test.args) - if actual != test.expected { - b.Fatalf("expected %v got %v", test.expected, actual) - } - } - }) - } -} diff --git a/langs/i18n/translationProvider.go b/langs/i18n/translationProvider.go deleted file mode 100644 index 9ede538d2..000000000 --- a/langs/i18n/translationProvider.go +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright 2017 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 i18n - -import ( - "encoding/json" - "fmt" - "strings" - - "github.com/gohugoio/hugo/common/paths" - - "github.com/gohugoio/hugo/common/herrors" - "golang.org/x/text/language" - yaml "gopkg.in/yaml.v2" - - "github.com/gohugoio/go-i18n/v2/i18n" - "github.com/gohugoio/hugo/helpers" - toml "github.com/pelletier/go-toml/v2" - - "github.com/gohugoio/hugo/deps" - "github.com/gohugoio/hugo/hugofs" - "github.com/gohugoio/hugo/source" -) - -// TranslationProvider provides translation handling, i.e. loading -// of bundles etc. -type TranslationProvider struct { - t Translator -} - -// NewTranslationProvider creates a new translation provider. -func NewTranslationProvider() *TranslationProvider { - return &TranslationProvider{} -} - -// Update updates the i18n func in the provided Deps. -func (tp *TranslationProvider) NewResource(dst *deps.Deps) error { - defaultLangTag, err := language.Parse(dst.Conf.DefaultContentLanguage()) - if err != nil { - defaultLangTag = language.English - } - bundle := i18n.NewBundle(defaultLangTag) - - bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) - bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal) - bundle.RegisterUnmarshalFunc("yml", yaml.Unmarshal) - bundle.RegisterUnmarshalFunc("json", json.Unmarshal) - - w := hugofs.NewWalkway( - hugofs.WalkwayConfig{ - Fs: dst.BaseFs.I18n.Fs, - IgnoreFile: dst.SourceSpec.IgnoreFile, - PathParser: dst.SourceSpec.Cfg.PathParser(), - WalkFn: func(path string, info hugofs.FileMetaInfo) error { - if info.IsDir() { - return nil - } - return addTranslationFile(bundle, source.NewFileInfo(info)) - }, - }) - - if err := w.Walk(); err != nil { - return err - } - - tp.t = NewTranslator(bundle, dst.Conf, dst.Log) - - dst.Translate = tp.t.Func(dst.Conf.Language().Lang) - - return nil -} - -const artificialLangTagPrefix = "art-x-" - -func addTranslationFile(bundle *i18n.Bundle, r *source.File) error { - f, err := r.FileInfo().Meta().Open() - if err != nil { - return fmt.Errorf("failed to open translations file %q:: %w", r.LogicalName(), err) - } - - b := helpers.ReaderToBytes(f) - f.Close() - - name := r.LogicalName() - lang := paths.Filename(name) - tag := language.Make(lang) - if tag == language.Und { - try := artificialLangTagPrefix + lang - _, err = language.Parse(try) - if err != nil { - return fmt.Errorf("%q: %s", try, err) - } - name = artificialLangTagPrefix + name - } - - _, err = bundle.ParseMessageFileBytes(b, name) - if err != nil { - if strings.Contains(err.Error(), "no plural rule") { - // https://github.com/gohugoio/hugo/issues/7798 - name = artificialLangTagPrefix + name - _, err = bundle.ParseMessageFileBytes(b, name) - if err == nil { - return nil - } - } - return errWithFileContext(fmt.Errorf("failed to load translations: %w", err), r) - } - - return nil -} - -// CloneResource sets the language func for the new language. -func (tp *TranslationProvider) CloneResource(dst, src *deps.Deps) error { - dst.Translate = tp.t.Func(dst.Conf.Language().Lang) - return nil -} - -func errWithFileContext(inerr error, r *source.File) error { - meta := r.FileInfo().Meta() - realFilename := meta.Filename - f, err := meta.Open() - if err != nil { - return inerr - } - defer f.Close() - - return herrors.NewFileErrorFromName(inerr, realFilename).UpdateContent(f, nil) -} diff --git a/langs/language.go b/langs/language.go deleted file mode 100644 index d34ea1cc7..000000000 --- a/langs/language.go +++ /dev/null @@ -1,189 +0,0 @@ -// 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 langs contains the language related types and function. -package langs - -import ( - "fmt" - "sync" - "time" - - "golang.org/x/text/collate" - "golang.org/x/text/language" - - "github.com/gohugoio/hugo/common/htime" - "github.com/gohugoio/hugo/common/maps" - "github.com/gohugoio/locales" - translators "github.com/gohugoio/localescompressed" -) - -type Language struct { - // The language code, e.g. "en" or "no". - // This is currently only settable as the key in the language map in the config. - Lang string - - // Fields from the language config. - LanguageConfig - - // Used for date formatting etc. We don't want these exported to the - // templates. - translator locales.Translator - timeFormatter htime.TimeFormatter - tag language.Tag - // collator1 and collator2 are the same, we have 2 to prevent deadlocks. - collator1 *Collator - collator2 *Collator - - location *time.Location - - // This is just an alias of Site.Params. - params maps.Params -} - -// NewLanguage creates a new language. -func NewLanguage(lang, defaultContentLanguage, timeZone string, languageConfig LanguageConfig) (*Language, error) { - translator := translators.GetTranslator(lang) - if translator == nil { - translator = translators.GetTranslator(defaultContentLanguage) - if translator == nil { - translator = translators.GetTranslator("en") - } - } - - var coll1, coll2 *Collator - tag, err := language.Parse(lang) - if err == nil { - coll1 = &Collator{ - c: collate.New(tag), - } - coll2 = &Collator{ - c: collate.New(tag), - } - } else { - coll1 = &Collator{ - c: collate.New(language.English), - } - coll2 = &Collator{ - c: collate.New(language.English), - } - } - - l := &Language{ - Lang: lang, - LanguageConfig: languageConfig, - translator: translator, - timeFormatter: htime.NewTimeFormatter(translator), - tag: tag, - collator1: coll1, - collator2: coll2, - } - - return l, l.loadLocation(timeZone) -} - -// This is injected from hugolib to avoid circular dependencies. -var DeprecationFunc = func(item, alternative string, err bool) {} - -// Params returns the language params. -// Note that this is the same as the Site.Params, but we keep it here for legacy reasons. -// Deprecated: Use the site.Params instead. -func (l *Language) Params() maps.Params { - // TODO(bep) Remove this for now as it created a little too much noise. Need to think about this. - // See https://github.com/gohugoio/hugo/issues/11025 - // DeprecationFunc(".Language.Params", paramsDeprecationWarning, false) - return l.params -} - -func (l *Language) LanguageCode() string { - if l.LanguageConfig.LanguageCode != "" { - return l.LanguageConfig.LanguageCode - } - return l.Lang -} - -func (l *Language) loadLocation(tzStr string) error { - location, err := time.LoadLocation(tzStr) - if err != nil { - return fmt.Errorf("invalid timeZone for language %q: %w", l.Lang, err) - } - l.location = location - - return nil -} - -func (l *Language) String() string { - return l.Lang -} - -// Languages is a sortable list of languages. -type Languages []*Language - -func (l Languages) AsSet() map[string]bool { - m := make(map[string]bool) - for _, lang := range l { - m[lang.Lang] = true - } - - return m -} - -// AsIndexSet returns a map with the language code as key and index in l as value. -func (l Languages) AsIndexSet() map[string]int { - m := make(map[string]int) - for i, lang := range l { - m[lang.Lang] = i - } - - return m -} - -// Internal access to unexported Language fields. -// This construct is to prevent them from leaking to the templates. - -func SetParams(l *Language, params maps.Params) { - l.params = params -} - -func GetTimeFormatter(l *Language) htime.TimeFormatter { - return l.timeFormatter -} - -func GetTranslator(l *Language) locales.Translator { - return l.translator -} - -func GetLocation(l *Language) *time.Location { - return l.location -} - -func GetCollator1(l *Language) *Collator { - return l.collator1 -} - -func GetCollator2(l *Language) *Collator { - return l.collator2 -} - -type Collator struct { - sync.Mutex - c *collate.Collator -} - -// CompareStrings compares a and b. -// It returns -1 if a < b, 1 if a > b and 0 if a == b. -// Note that the Collator is not thread safe, so you may want -// to acquire a lock on it before calling this method. -func (c *Collator) CompareStrings(a, b string) int { - return c.c.CompareString(a, b) -} diff --git a/langs/language_test.go b/langs/language_test.go deleted file mode 100644 index 33240f3f4..000000000 --- a/langs/language_test.go +++ /dev/null @@ -1,76 +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 langs - -import ( - "sync" - "testing" - - qt "github.com/frankban/quicktest" - "golang.org/x/text/collate" - "golang.org/x/text/language" -) - -func TestCollator(t *testing.T) { - c := qt.New(t) - - var wg sync.WaitGroup - - coll := &Collator{c: collate.New(language.English, collate.Loose)} - - for range 10 { - wg.Add(1) - go func() { - coll.Lock() - defer coll.Unlock() - defer wg.Done() - for range 10 { - k := coll.CompareStrings("abc", "def") - c.Assert(k, qt.Equals, -1) - } - }() - } - wg.Wait() -} - -func BenchmarkCollator(b *testing.B) { - s := []string{"foo", "bar", "éntre", "baz", "qux", "quux", "corge", "grault", "garply", "waldo", "fred", "plugh", "xyzzy", "thud"} - - doWork := func(coll *Collator) { - for i := range s { - for j := i + 1; j < len(s); j++ { - _ = coll.CompareStrings(s[i], s[j]) - } - } - } - - b.Run("Single", func(b *testing.B) { - coll := &Collator{c: collate.New(language.English, collate.Loose)} - for i := 0; i < b.N; i++ { - doWork(coll) - } - }) - - b.Run("Para", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - coll := &Collator{c: collate.New(language.English, collate.Loose)} - - for pb.Next() { - coll.Lock() - doWork(coll) - coll.Unlock() - } - }) - }) -} diff --git a/lazy/init.go b/lazy/init.go deleted file mode 100644 index bef3867a9..000000000 --- a/lazy/init.go +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright 2019 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 lazy - -import ( - "context" - "errors" - "sync" - "sync/atomic" - "time" -) - -// New creates a new empty Init. -func New() *Init { - return &Init{} -} - -// Init holds a graph of lazily initialized dependencies. -type Init struct { - // Used mainly for testing. - initCount uint64 - - mu sync.Mutex - - prev *Init - children []*Init - - init OnceMore - out any - err error - f func(context.Context) (any, error) -} - -// Add adds a func as a new child dependency. -func (ini *Init) Add(initFn func(context.Context) (any, error)) *Init { - if ini == nil { - ini = New() - } - return ini.add(false, initFn) -} - -// InitCount gets the number of this this Init has been initialized. -func (ini *Init) InitCount() int { - i := atomic.LoadUint64(&ini.initCount) - return int(i) -} - -// AddWithTimeout is same as Add, but with a timeout that aborts initialization. -func (ini *Init) AddWithTimeout(timeout time.Duration, f func(ctx context.Context) (any, error)) *Init { - return ini.Add(func(ctx context.Context) (any, error) { - return ini.withTimeout(ctx, timeout, f) - }) -} - -// Branch creates a new dependency branch based on an existing and adds -// the given dependency as a child. -func (ini *Init) Branch(initFn func(context.Context) (any, error)) *Init { - if ini == nil { - ini = New() - } - return ini.add(true, initFn) -} - -// BranchWithTimeout is same as Branch, but with a timeout. -func (ini *Init) BranchWithTimeout(timeout time.Duration, f func(ctx context.Context) (any, error)) *Init { - return ini.Branch(func(ctx context.Context) (any, error) { - return ini.withTimeout(ctx, timeout, f) - }) -} - -// Do initializes the entire dependency graph. -func (ini *Init) Do(ctx context.Context) (any, error) { - if ini == nil { - panic("init is nil") - } - - ini.init.Do(func() { - atomic.AddUint64(&ini.initCount, 1) - prev := ini.prev - if prev != nil { - // A branch. Initialize the ancestors. - if prev.shouldInitialize() { - _, err := prev.Do(ctx) - if err != nil { - ini.err = err - return - } - } else if prev.inProgress() { - // Concurrent initialization. The following init func - // may depend on earlier state, so wait. - prev.wait() - } - } - - if ini.f != nil { - ini.out, ini.err = ini.f(ctx) - } - - for _, child := range ini.children { - if child.shouldInitialize() { - _, err := child.Do(ctx) - if err != nil { - ini.err = err - return - } - } - } - }) - - ini.wait() - - return ini.out, ini.err -} - -// TODO(bep) investigate if we can use sync.Cond for this. -func (ini *Init) wait() { - var counter time.Duration - for !ini.init.Done() { - counter += 10 - if counter > 600000000 { - panic("BUG: timed out in lazy init") - } - time.Sleep(counter * time.Microsecond) - } -} - -func (ini *Init) inProgress() bool { - return ini != nil && ini.init.InProgress() -} - -func (ini *Init) shouldInitialize() bool { - return !(ini == nil || ini.init.Done() || ini.init.InProgress()) -} - -// Reset resets the current and all its dependencies. -func (ini *Init) Reset() { - mu := ini.init.ResetWithLock() - ini.err = nil - defer mu.Unlock() - for _, d := range ini.children { - d.Reset() - } -} - -func (ini *Init) add(branch bool, initFn func(context.Context) (any, error)) *Init { - ini.mu.Lock() - defer ini.mu.Unlock() - - if branch { - return &Init{ - f: initFn, - prev: ini, - } - } - - ini.checkDone() - ini.children = append(ini.children, &Init{ - f: initFn, - }) - - return ini -} - -func (ini *Init) checkDone() { - if ini.init.Done() { - panic("init cannot be added to after it has run") - } -} - -func (ini *Init) withTimeout(ctx context.Context, timeout time.Duration, f func(ctx context.Context) (any, error)) (any, error) { - // Create a new context with a timeout not connected to the incoming context. - waitCtx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - c := make(chan verr, 1) - - go func() { - v, err := f(ctx) - select { - case <-waitCtx.Done(): - return - default: - c <- verr{v: v, err: err} - } - }() - - select { - case <-waitCtx.Done(): - //lint:ignore ST1005 end user message. - return nil, errors.New("timed out initializing value. You may have a circular loop in a shortcode, or your site may have resources that take longer to build than the `timeout` limit in your Hugo config file.") - case ve := <-c: - return ve.v, ve.err - } -} - -type verr struct { - v any - err error -} diff --git a/lazy/init_test.go b/lazy/init_test.go deleted file mode 100644 index 94736fab8..000000000 --- a/lazy/init_test.go +++ /dev/null @@ -1,237 +0,0 @@ -// Copyright 2019 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 lazy - -import ( - "context" - "errors" - "math/rand" - "strings" - "sync" - "testing" - "time" - - qt "github.com/frankban/quicktest" -) - -var ( - rnd = rand.New(rand.NewSource(time.Now().UnixNano())) - bigOrSmall = func() int { - if rnd.Intn(10) < 5 { - return 10000 + rnd.Intn(100000) - } - return 1 + rnd.Intn(50) - } -) - -func doWork() { - doWorkOfSize(bigOrSmall()) -} - -func doWorkOfSize(size int) { - _ = strings.Repeat("Hugo Rocks! ", size) -} - -func TestInit(t *testing.T) { - c := qt.New(t) - - var result string - - f1 := func(name string) func(context.Context) (any, error) { - return func(context.Context) (any, error) { - result += name + "|" - doWork() - return name, nil - } - } - - f2 := func() func(context.Context) (any, error) { - return func(context.Context) (any, error) { - doWork() - return nil, nil - } - } - - root := New() - - root.Add(f1("root(1)")) - root.Add(f1("root(2)")) - - branch1 := root.Branch(f1("branch_1")) - branch1.Add(f1("branch_1_1")) - branch1_2 := branch1.Add(f1("branch_1_2")) - branch1_2_1 := branch1_2.Add(f1("branch_1_2_1")) - - var wg sync.WaitGroup - - ctx := context.Background() - - // Add some concurrency and randomness to verify thread safety and - // init order. - for i := range 100 { - wg.Add(1) - go func(i int) { - defer wg.Done() - var err error - if rnd.Intn(10) < 5 { - _, err = root.Do(ctx) - c.Assert(err, qt.IsNil) - } - - // Add a new branch on the fly. - if rnd.Intn(10) > 5 { - branch := branch1_2.Branch(f2()) - _, err = branch.Do(ctx) - c.Assert(err, qt.IsNil) - } else { - _, err = branch1_2_1.Do(ctx) - c.Assert(err, qt.IsNil) - } - _, err = branch1_2.Do(ctx) - c.Assert(err, qt.IsNil) - }(i) - - wg.Wait() - - c.Assert(result, qt.Equals, "root(1)|root(2)|branch_1|branch_1_1|branch_1_2|branch_1_2_1|") - - } -} - -func TestInitAddWithTimeout(t *testing.T) { - c := qt.New(t) - - init := New().AddWithTimeout(100*time.Millisecond, func(ctx context.Context) (any, error) { - return nil, nil - }) - - _, err := init.Do(context.Background()) - - c.Assert(err, qt.IsNil) -} - -func TestInitAddWithTimeoutTimeout(t *testing.T) { - c := qt.New(t) - - init := New().AddWithTimeout(100*time.Millisecond, func(ctx context.Context) (any, error) { - time.Sleep(500 * time.Millisecond) - return nil, nil - }) - - _, err := init.Do(context.Background()) - - c.Assert(err, qt.Not(qt.IsNil)) - - c.Assert(err.Error(), qt.Contains, "timed out") - - time.Sleep(1 * time.Second) -} - -func TestInitAddWithTimeoutError(t *testing.T) { - c := qt.New(t) - - init := New().AddWithTimeout(100*time.Millisecond, func(ctx context.Context) (any, error) { - return nil, errors.New("failed") - }) - - _, err := init.Do(context.Background()) - - c.Assert(err, qt.Not(qt.IsNil)) -} - -type T struct { - sync.Mutex - V1 string - V2 string -} - -func (t *T) Add1(v string) { - t.Lock() - t.V1 += v - t.Unlock() -} - -func (t *T) Add2(v string) { - t.Lock() - t.V2 += v - t.Unlock() -} - -// https://github.com/gohugoio/hugo/issues/5901 -func TestInitBranchOrder(t *testing.T) { - c := qt.New(t) - - base := New() - - work := func(size int, f func()) func(context.Context) (any, error) { - return func(context.Context) (any, error) { - doWorkOfSize(size) - if f != nil { - f() - } - - return nil, nil - } - } - - state := &T{} - - base = base.Add(work(10000, func() { - state.Add1("A") - })) - - inits := make([]*Init, 2) - for i := range inits { - inits[i] = base.Branch(work(i+1*100, func() { - // V1 is A - ab := state.V1 + "B" - state.Add2(ab) - })) - } - - var wg sync.WaitGroup - ctx := context.Background() - - for _, v := range inits { - v := v - wg.Add(1) - go func() { - defer wg.Done() - _, err := v.Do(ctx) - c.Assert(err, qt.IsNil) - }() - } - - wg.Wait() - - c.Assert(state.V2, qt.Equals, "ABAB") -} - -// See issue 7043 -func TestResetError(t *testing.T) { - c := qt.New(t) - r := false - i := New().Add(func(context.Context) (any, error) { - if r { - return nil, nil - } - return nil, errors.New("r is false") - }) - _, err := i.Do(context.Background()) - c.Assert(err, qt.IsNotNil) - i.Reset() - r = true - _, err = i.Do(context.Background()) - c.Assert(err, qt.IsNil) -} diff --git a/lazy/once.go b/lazy/once.go deleted file mode 100644 index dac689df3..000000000 --- a/lazy/once.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2019 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 lazy - -import ( - "sync" - "sync/atomic" -) - -// OnceMore is similar to sync.Once. -// -// Additional features are: -// * it can be reset, so the action can be repeated if needed -// * it has methods to check if it's done or in progress - -type OnceMore struct { - mu sync.Mutex - lock uint32 - done uint32 -} - -func (t *OnceMore) Do(f func()) { - if atomic.LoadUint32(&t.done) == 1 { - return - } - - // f may call this Do and we would get a deadlock. - locked := atomic.CompareAndSwapUint32(&t.lock, 0, 1) - if !locked { - return - } - defer atomic.StoreUint32(&t.lock, 0) - - t.mu.Lock() - defer t.mu.Unlock() - - // Double check - if t.done == 1 { - return - } - defer atomic.StoreUint32(&t.done, 1) - f() -} - -func (t *OnceMore) InProgress() bool { - return atomic.LoadUint32(&t.lock) == 1 -} - -func (t *OnceMore) Done() bool { - return atomic.LoadUint32(&t.done) == 1 -} - -func (t *OnceMore) ResetWithLock() *sync.Mutex { - t.mu.Lock() - defer atomic.StoreUint32(&t.done, 0) - return &t.mu -} diff --git a/livereload/connection.go b/livereload/connection.go index 0c6c6e108..4e94e2ee0 100644 --- a/livereload/connection.go +++ b/livereload/connection.go @@ -28,7 +28,7 @@ type connection struct { send chan []byte // There is a potential data race, especially visible with large files. - // This is protected by synchronization of the send channel's close. + // This is protected by synchronisation of the send channel's close. closer sync.Once } diff --git a/livereload/gen/livereload-hugo-plugin.js b/livereload/gen/livereload-hugo-plugin.js deleted file mode 100644 index c4c6aa487..000000000 --- a/livereload/gen/livereload-hugo-plugin.js +++ /dev/null @@ -1,34 +0,0 @@ -/* -Hugo adds a specific prefix, "__hugo_navigate", to the path in certain situations to signal -navigation to another content page. -*/ -function HugoReload() {} - -HugoReload.identifier = 'hugoReloader'; -HugoReload.version = '0.9'; - -HugoReload.prototype.reload = function (path, options) { - var prefix = '__hugo_navigate'; - - if (path.lastIndexOf(prefix, 0) !== 0) { - return false; - } - - path = path.substring(prefix.length); - - var portChanged = options.overrideURL && options.overrideURL != window.location.port; - - if (!portChanged && window.location.pathname === path) { - window.location.reload(); - } else { - if (portChanged) { - window.location = location.protocol + '//' + location.hostname + ':' + options.overrideURL + path; - } else { - window.location.pathname = path; - } - } - - return true; -}; - -LiveReload.addPlugin(HugoReload); diff --git a/livereload/gen/main.go b/livereload/gen/main.go deleted file mode 100644 index d69ff9206..000000000 --- a/livereload/gen/main.go +++ /dev/null @@ -1,61 +0,0 @@ -//go:generate go run main.go -package main - -import ( - _ "embed" - "fmt" - "io" - "log" - "net/http" - "os" - - "github.com/evanw/esbuild/pkg/api" -) - -//go:embed livereload-hugo-plugin.js -var livereloadHugoPluginJS string - -func main() { - // 4.0.2 - // To upgrade to a new version, change to the commit hash of the version you want to upgrade to - // then run mage generate from the root. - const liveReloadCommit = "d803a41804d2d71e0814c4e9e3233e78991024d9" - liveReloadSourceURL := fmt.Sprintf("https://raw.githubusercontent.com/livereload/livereload-js/%s/dist/livereload.js", liveReloadCommit) - - func() { - resp, err := http.Get(liveReloadSourceURL) - must(err) - defer resp.Body.Close() - - b, err := io.ReadAll(resp.Body) - must(err) - - // Write the unminified livereload.js file. - err = os.WriteFile("../livereload.js", b, 0o644) - must(err) - - // Bundle and minify with ESBuild. - result := api.Build(api.BuildOptions{ - Stdin: &api.StdinOptions{ - Contents: string(b) + livereloadHugoPluginJS, - }, - Outfile: "../livereload.min.js", - Bundle: true, - Target: api.ES2015, - Write: true, - MinifyWhitespace: true, - MinifyIdentifiers: true, - MinifySyntax: true, - }) - - if len(result.Errors) > 0 { - log.Fatal(result.Errors) - } - }() -} - -func must(err error) { - if err != nil { - log.Fatal(err) - } -} diff --git a/livereload/livereload.go b/livereload/livereload.go index 0d24ada98..a5da891fc 100644 --- a/livereload/livereload.go +++ b/livereload/livereload.go @@ -1,4 +1,4 @@ -// Copyright 2024 The Hugo Authors. All rights reserved. +// Copyright 2015 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. @@ -38,57 +38,16 @@ package livereload import ( "fmt" - "net" "net/http" - "net/url" "path/filepath" - _ "embed" - - "github.com/gohugoio/hugo/media" "github.com/gorilla/websocket" ) // Prefix to signal to LiveReload that we need to navigate to another path. -// Do not change this. const hugoNavigatePrefix = "__hugo_navigate" -var upgrader = &websocket.Upgrader{ - // Hugo may potentially spin up multiple HTTP servers, so we need to exclude the - // port when checking the origin. - CheckOrigin: func(r *http.Request) bool { - origin := r.Header["Origin"] - if len(origin) == 0 { - return true - } - u, err := url.Parse(origin[0]) - if err != nil { - return false - } - - rHost := r.Host - // For Github codespace in browser #9936 - if forwardedHost := r.Header.Get("X-Forwarded-Host"); forwardedHost != "" { - rHost = forwardedHost - } - - if u.Host == rHost { - return true - } - - h1, _, err := net.SplitHostPort(u.Host) - if err != nil { - return false - } - h2, _, err := net.SplitHostPort(r.Host) - if err != nil { - return false - } - - return h1 == h2 - }, - ReadBufferSize: 1024, WriteBufferSize: 1024, -} +var upgrader = &websocket.Upgrader{ReadBufferSize: 1024, WriteBufferSize: 1024} // Handler is a HandlerFunc handling the livereload // Websocket interaction. @@ -114,39 +73,60 @@ func ForceRefresh() { RefreshPath("/x.js") } -// NavigateToPathForPort is similar to NavigateToPath but will also -// set window.location.port to the given port value. -func NavigateToPathForPort(path string, port int) { - refreshPathForPort(hugoNavigatePrefix+path, port) +// NavigateToPath tells livereload to navigate to the given path. +// This translates to `window.location.href = path` in the client. +func NavigateToPath(path string) { + RefreshPath(hugoNavigatePrefix + path) } // RefreshPath tells livereload to refresh only the given path. // If that path points to a CSS stylesheet or an image, only the changes // will be updated in the browser, not the entire page. func RefreshPath(s string) { - refreshPathForPort(s, -1) -} - -func refreshPathForPort(s string, port int) { // Tell livereload a file has changed - will force a hard refresh if not CSS or an image urlPath := filepath.ToSlash(s) - portStr := "" - if port > 0 { - portStr = fmt.Sprintf(`, "overrideURL": %d`, port) - } - msg := fmt.Sprintf(`{"command":"reload","path":%q,"originalPath":"","liveCSS":true,"liveImg":true%s}`, urlPath, portStr) - wsHub.broadcast <- []byte(msg) + wsHub.broadcast <- []byte(`{"command":"reload","path":"` + urlPath + `","originalPath":"","liveCSS":true,"liveImg":true}`) } -// ServeJS serves the livereload.js who's reference is injected into the page. +// ServeJS serves the liverreload.js who's reference is injected into the page. func ServeJS(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", media.Builtin.JavascriptType.Type) + w.Header().Set("Content-Type", "application/javascript") w.Write(liveReloadJS()) } func liveReloadJS() []byte { - return []byte(livereloadJS) + return []byte(livereloadJS + hugoLiveReloadPlugin) } -//go:embed livereload.min.js -var livereloadJS string +var ( + // This is temporary patched with this PR (enables sensible error messages): + // https://github.com/livereload/livereload-js/pull/64 + // TODO(bep) replace with distribution once merged. + livereloadJS = `(function e(t,n,o){function i(s,l){if(!n[s]){if(!t[s]){var c=typeof require=="function"&&require;if(!l&&c)return c(s,!0);if(r)return r(s,!0);var a=new Error("Cannot find module '"+s+"'");throw a.code="MODULE_NOT_FOUND",a}var h=n[s]={exports:{}};t[s][0].call(h.exports,function(e){var n=t[s][1][e];return i(n?n:e)},h,h.exports,e,t,n,o)}return n[s].exports}var r=typeof require=="function"&&require;for(var s=0;s tag");return}}this.reloader=new s(this.window,this.console,l);this.connector=new t(this.options,this.WebSocket,l,{connecting:function(e){return function(){}}(this),socketConnected:function(e){return function(){}}(this),connected:function(e){return function(t){var n;if(typeof(n=e.listeners).connect==="function"){n.connect()}e.log("LiveReload is connected to "+e.options.host+":"+e.options.port+" (protocol v"+t+").");return e.analyze()}}(this),error:function(e){return function(e){if(e instanceof r){if(typeof console!=="undefined"&&console!==null){return console.log(""+e.message+".")}}else{if(typeof console!=="undefined"&&console!==null){return console.log("LiveReload internal error: "+e.message)}}}}(this),disconnected:function(e){return function(t,n){var o;if(typeof(o=e.listeners).disconnect==="function"){o.disconnect()}switch(t){case"cannot-connect":return e.log("LiveReload cannot connect to "+e.options.host+":"+e.options.port+", will retry in "+n+" sec.");case"broken":return e.log("LiveReload disconnected from "+e.options.host+":"+e.options.port+", reconnecting in "+n+" sec.");case"handshake-timeout":return e.log("LiveReload cannot connect to "+e.options.host+":"+e.options.port+" (handshake timeout), will retry in "+n+" sec.");case"handshake-failed":return e.log("LiveReload cannot connect to "+e.options.host+":"+e.options.port+" (handshake failed), will retry in "+n+" sec.");case"manual":break;case"error":break;default:return e.log("LiveReload disconnected from "+e.options.host+":"+e.options.port+" ("+t+"), reconnecting in "+n+" sec.")}}}(this),message:function(e){return function(t){switch(t.command){case"reload":return e.performReload(t);case"alert":return e.performAlert(t)}}}(this)});this.initialized=true}e.prototype.on=function(e,t){return this.listeners[e]=t};e.prototype.log=function(e){return this.console.log(""+e)};e.prototype.performReload=function(e){var t,n;this.log("LiveReload received reload request: "+JSON.stringify(e,null,2));return this.reloader.reload(e.path,{liveCSS:(t=e.liveCSS)!=null?t:true,liveImg:(n=e.liveImg)!=null?n:true,originalPath:e.originalPath||"",overrideURL:e.overrideURL||"",serverURL:"http://"+this.options.host+":"+this.options.port})};e.prototype.performAlert=function(e){return alert(e.message)};e.prototype.shutDown=function(){var e;if(!this.initialized){return}this.connector.disconnect();this.log("LiveReload disconnected.");return typeof(e=this.listeners).shutdown==="function"?e.shutdown():void 0};e.prototype.hasPlugin=function(e){return!!this.pluginIdentifiers[e]};e.prototype.addPlugin=function(e){var t;if(!this.initialized){return}if(this.hasPlugin(e.identifier)){return}this.pluginIdentifiers[e.identifier]=true;t=new e(this.window,{_livereload:this,_reloader:this.reloader,_connector:this.connector,console:this.console,Timer:l,generateCacheBustUrl:function(e){return function(t){return e.reloader.generateCacheBustUrl(t)}}(this)});this.plugins.push(t);this.reloader.addPlugin(t)};e.prototype.analyze=function(){var e,t,n,o,i,r;if(!this.initialized){return}if(!(this.connector.protocol>=7)){return}n={};r=this.plugins;for(o=0,i=r.length;o1){s.set(o[0].replace(/-/g,"_"),o.slice(1).join("="))}}}return s}}return null}}).call(this)},{}],6:[function(e,t,n){(function(){var e,t,o,i,r=[].indexOf||function(e){for(var t=0,n=this.length;t=0){this.protocol=7}else if(r.call(l.protocols,e)>=0){this.protocol=6}else{throw new i("no supported protocols found")}}return this.handlers.connected(this.protocol)}else if(this.protocol===6){l=JSON.parse(n);if(!l.length){throw new i("protocol 6 messages must be arrays")}o=l[0],c=l[1];if(o!=="refresh"){throw new i("unknown protocol 6 command")}return this.handlers.message({command:"reload",path:c.path,liveCSS:(a=c.apply_css_live)!=null?a:true})}else{l=this._parseMessage(n,["reload","alert"]);return this.handlers.message(l)}}catch(e){s=e;if(s instanceof i){return this.handlers.error(s)}else{throw s}}};n.prototype._parseMessage=function(e,t){var n,o,s;try{o=JSON.parse(e)}catch(t){n=t;throw new i("unparsable JSON",e)}if(!o.command){throw new i('missing "command" key',e)}if(s=o.command,r.call(t,s)<0){throw new i("invalid command '"+o.command+"', only valid commands are: "+t.join(", ")+")",e)}return o};return n}()}).call(this)},{}],7:[function(e,t,n){(function(){var e,t,o,i,r,s,l;l=function(e){var t,n,o;if((n=e.indexOf("#"))>=0){t=e.slice(n);e=e.slice(0,n)}else{t=""}if((n=e.indexOf("?"))>=0){o=e.slice(n);e=e.slice(0,n)}else{o=""}return{url:e,params:o,hash:t}};i=function(e){var t;e=l(e).url;if(e.indexOf("file://")===0){t=e.replace(/^file:\/\/(localhost)?/,"")}else{t=e.replace(/^([^:]+:)?\/\/([^:\/]+)(:\d*)?\//,"/")}return decodeURIComponent(t)};s=function(e,t,n){var i,r,s,l,c;i={score:0};for(l=0,c=t.length;li.score){i={object:r,score:s}}}if(i.score>0){return i}else{return null}};o=function(e,t){var n,o,i,r;e=e.replace(/^\/+/,"").toLowerCase();t=t.replace(/^\/+/,"").toLowerCase();if(e===t){return 1e4}n=e.split("/").reverse();o=t.split("/").reverse();r=Math.min(n.length,o.length);i=0;while(i0};e=[{selector:"background",styleNames:["backgroundImage"]},{selector:"border",styleNames:["borderImage","webkitBorderImage","MozBorderImage"]}];n.Reloader=t=function(){function t(e,t,n){this.window=e;this.console=t;this.Timer=n;this.document=this.window.document;this.importCacheWaitPeriod=200;this.plugins=[]}t.prototype.addPlugin=function(e){return this.plugins.push(e)};t.prototype.analyze=function(e){return results};t.prototype.reload=function(e,t){var n,o,i,r,s;this.options=t;if((o=this.options).stylesheetReloadTimeout==null){o.stylesheetReloadTimeout=15e3}s=this.plugins;for(i=0,r=s.length;i Array#indexOf -// true -> Array#includes -var toIObject = require('./_to-iobject'); -var toLength = require('./_to-length'); -var toAbsoluteIndex = require('./_to-absolute-index'); -module.exports = function (IS_INCLUDES) { - return function ($this, el, fromIndex) { - var O = toIObject($this); - var length = toLength(O.length); - var index = toAbsoluteIndex(fromIndex, length); - var value; - // Array#includes uses SameValueZero equality algorithm - // eslint-disable-next-line no-self-compare - if (IS_INCLUDES && el != el) while (length > index) { - value = O[index++]; - // eslint-disable-next-line no-self-compare - if (value != value) return true; - // Array#indexOf ignores holes, Array#includes - not - } else for (;length > index; index++) if (IS_INCLUDES || index in O) { - if (O[index] === el) return IS_INCLUDES || index || 0; - } return !IS_INCLUDES && -1; - }; -}; - -},{"./_to-absolute-index":72,"./_to-iobject":74,"./_to-length":75}],6:[function(require,module,exports){ -// 0 -> Array#forEach -// 1 -> Array#map -// 2 -> Array#filter -// 3 -> Array#some -// 4 -> Array#every -// 5 -> Array#find -// 6 -> Array#findIndex -var ctx = require('./_ctx'); -var IObject = require('./_iobject'); -var toObject = require('./_to-object'); -var toLength = require('./_to-length'); -var asc = require('./_array-species-create'); -module.exports = function (TYPE, $create) { - var IS_MAP = TYPE == 1; - var IS_FILTER = TYPE == 2; - var IS_SOME = TYPE == 3; - var IS_EVERY = TYPE == 4; - var IS_FIND_INDEX = TYPE == 6; - var NO_HOLES = TYPE == 5 || IS_FIND_INDEX; - var create = $create || asc; - return function ($this, callbackfn, that) { - var O = toObject($this); - var self = IObject(O); - var f = ctx(callbackfn, that, 3); - var length = toLength(self.length); - var index = 0; - var result = IS_MAP ? create($this, length) : IS_FILTER ? create($this, 0) : undefined; - var val, res; - for (;length > index; index++) if (NO_HOLES || index in self) { - val = self[index]; - res = f(val, index, O); - if (TYPE) { - if (IS_MAP) result[index] = res; // map - else if (res) switch (TYPE) { - case 3: return true; // some - case 5: return val; // find - case 6: return index; // findIndex - case 2: result.push(val); // filter - } else if (IS_EVERY) return false; // every - } - } - return IS_FIND_INDEX ? -1 : IS_SOME || IS_EVERY ? IS_EVERY : result; - }; -}; - -},{"./_array-species-create":8,"./_ctx":13,"./_iobject":31,"./_to-length":75,"./_to-object":76}],7:[function(require,module,exports){ -var isObject = require('./_is-object'); -var isArray = require('./_is-array'); -var SPECIES = require('./_wks')('species'); - -module.exports = function (original) { - var C; - if (isArray(original)) { - C = original.constructor; - // cross-realm fallback - if (typeof C == 'function' && (C === Array || isArray(C.prototype))) C = undefined; - if (isObject(C)) { - C = C[SPECIES]; - if (C === null) C = undefined; - } - } return C === undefined ? Array : C; -}; - -},{"./_is-array":33,"./_is-object":34,"./_wks":81}],8:[function(require,module,exports){ -// 9.4.2.3 ArraySpeciesCreate(originalArray, length) -var speciesConstructor = require('./_array-species-constructor'); - -module.exports = function (original, length) { - return new (speciesConstructor(original))(length); -}; - -},{"./_array-species-constructor":7}],9:[function(require,module,exports){ -// getting tag from 19.1.3.6 Object.prototype.toString() -var cof = require('./_cof'); -var TAG = require('./_wks')('toStringTag'); -// ES3 wrong here -var ARG = cof(function () { return arguments; }()) == 'Arguments'; - -// fallback for IE11 Script Access Denied error -var tryGet = function (it, key) { - try { - return it[key]; - } catch (e) { /* empty */ } -}; - -module.exports = function (it) { - var O, T, B; - return it === undefined ? 'Undefined' : it === null ? 'Null' - // @@toStringTag case - : typeof (T = tryGet(O = Object(it), TAG)) == 'string' ? T - // builtinTag case - : ARG ? cof(O) - // ES3 arguments fallback - : (B = cof(O)) == 'Object' && typeof O.callee == 'function' ? 'Arguments' : B; -}; - -},{"./_cof":10,"./_wks":81}],10:[function(require,module,exports){ -var toString = {}.toString; - -module.exports = function (it) { - return toString.call(it).slice(8, -1); -}; - -},{}],11:[function(require,module,exports){ -var core = module.exports = { version: '2.6.12' }; -if (typeof __e == 'number') __e = core; // eslint-disable-line no-undef - -},{}],12:[function(require,module,exports){ -'use strict'; -var $defineProperty = require('./_object-dp'); -var createDesc = require('./_property-desc'); - -module.exports = function (object, index, value) { - if (index in object) $defineProperty.f(object, index, createDesc(0, value)); - else object[index] = value; -}; - -},{"./_object-dp":45,"./_property-desc":57}],13:[function(require,module,exports){ -// optional / simple context binding -var aFunction = require('./_a-function'); -module.exports = function (fn, that, length) { - aFunction(fn); - if (that === undefined) return fn; - switch (length) { - case 1: return function (a) { - return fn.call(that, a); - }; - case 2: return function (a, b) { - return fn.call(that, a, b); - }; - case 3: return function (a, b, c) { - return fn.call(that, a, b, c); - }; - } - return function (/* ...args */) { - return fn.apply(that, arguments); - }; -}; - -},{"./_a-function":1}],14:[function(require,module,exports){ -// 7.2.1 RequireObjectCoercible(argument) -module.exports = function (it) { - if (it == undefined) throw TypeError("Can't call method on " + it); - return it; -}; - -},{}],15:[function(require,module,exports){ -// Thank's IE8 for his funny defineProperty -module.exports = !require('./_fails')(function () { - return Object.defineProperty({}, 'a', { get: function () { return 7; } }).a != 7; -}); - -},{"./_fails":21}],16:[function(require,module,exports){ -var isObject = require('./_is-object'); -var document = require('./_global').document; -// typeof document.createElement is 'object' in old IE -var is = isObject(document) && isObject(document.createElement); -module.exports = function (it) { - return is ? document.createElement(it) : {}; -}; - -},{"./_global":25,"./_is-object":34}],17:[function(require,module,exports){ -// IE 8- don't enum bug keys -module.exports = ( - 'constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf' -).split(','); - -},{}],18:[function(require,module,exports){ -// all enumerable object keys, includes symbols -var getKeys = require('./_object-keys'); -var gOPS = require('./_object-gops'); -var pIE = require('./_object-pie'); -module.exports = function (it) { - var result = getKeys(it); - var getSymbols = gOPS.f; - if (getSymbols) { - var symbols = getSymbols(it); - var isEnum = pIE.f; - var i = 0; - var key; - while (symbols.length > i) if (isEnum.call(it, key = symbols[i++])) result.push(key); - } return result; -}; - -},{"./_object-gops":50,"./_object-keys":53,"./_object-pie":54}],19:[function(require,module,exports){ -var global = require('./_global'); -var core = require('./_core'); -var hide = require('./_hide'); -var redefine = require('./_redefine'); -var ctx = require('./_ctx'); -var PROTOTYPE = 'prototype'; - -var $export = function (type, name, source) { - var IS_FORCED = type & $export.F; - var IS_GLOBAL = type & $export.G; - var IS_STATIC = type & $export.S; - var IS_PROTO = type & $export.P; - var IS_BIND = type & $export.B; - var target = IS_GLOBAL ? global : IS_STATIC ? global[name] || (global[name] = {}) : (global[name] || {})[PROTOTYPE]; - var exports = IS_GLOBAL ? core : core[name] || (core[name] = {}); - var expProto = exports[PROTOTYPE] || (exports[PROTOTYPE] = {}); - var key, own, out, exp; - if (IS_GLOBAL) source = name; - for (key in source) { - // contains in native - own = !IS_FORCED && target && target[key] !== undefined; - // export native or passed - out = (own ? target : source)[key]; - // bind timers to global for call from export context - exp = IS_BIND && own ? ctx(out, global) : IS_PROTO && typeof out == 'function' ? ctx(Function.call, out) : out; - // extend global - if (target) redefine(target, key, out, type & $export.U); - // export - if (exports[key] != out) hide(exports, key, exp); - if (IS_PROTO && expProto[key] != out) expProto[key] = out; - } -}; -global.core = core; -// type bitmap -$export.F = 1; // forced -$export.G = 2; // global -$export.S = 4; // static -$export.P = 8; // proto -$export.B = 16; // bind -$export.W = 32; // wrap -$export.U = 64; // safe -$export.R = 128; // real proto method for `library` -module.exports = $export; - -},{"./_core":11,"./_ctx":13,"./_global":25,"./_hide":27,"./_redefine":58}],20:[function(require,module,exports){ -var MATCH = require('./_wks')('match'); -module.exports = function (KEY) { - var re = /./; - try { - '/./'[KEY](re); - } catch (e) { - try { - re[MATCH] = false; - return !'/./'[KEY](re); - } catch (f) { /* empty */ } - } return true; -}; - -},{"./_wks":81}],21:[function(require,module,exports){ -module.exports = function (exec) { - try { - return !!exec(); - } catch (e) { - return true; - } -}; - -},{}],22:[function(require,module,exports){ -'use strict'; -require('./es6.regexp.exec'); -var redefine = require('./_redefine'); -var hide = require('./_hide'); -var fails = require('./_fails'); -var defined = require('./_defined'); -var wks = require('./_wks'); -var regexpExec = require('./_regexp-exec'); - -var SPECIES = wks('species'); - -var REPLACE_SUPPORTS_NAMED_GROUPS = !fails(function () { - // #replace needs built-in support for named groups. - // #match works fine because it just return the exec results, even if it has - // a "grops" property. - var re = /./; - re.exec = function () { - var result = []; - result.groups = { a: '7' }; - return result; - }; - return ''.replace(re, '$
    ') !== '7'; -}); - -var SPLIT_WORKS_WITH_OVERWRITTEN_EXEC = (function () { - // Chrome 51 has a buggy "split" implementation when RegExp#exec !== nativeExec - var re = /(?:)/; - var originalExec = re.exec; - re.exec = function () { return originalExec.apply(this, arguments); }; - var result = 'ab'.split(re); - return result.length === 2 && result[0] === 'a' && result[1] === 'b'; -})(); - -module.exports = function (KEY, length, exec) { - var SYMBOL = wks(KEY); - - var DELEGATES_TO_SYMBOL = !fails(function () { - // String methods call symbol-named RegEp methods - var O = {}; - O[SYMBOL] = function () { return 7; }; - return ''[KEY](O) != 7; - }); - - var DELEGATES_TO_EXEC = DELEGATES_TO_SYMBOL ? !fails(function () { - // Symbol-named RegExp methods call .exec - var execCalled = false; - var re = /a/; - re.exec = function () { execCalled = true; return null; }; - if (KEY === 'split') { - // RegExp[@@split] doesn't call the regex's exec method, but first creates - // a new one. We need to return the patched regex when creating the new one. - re.constructor = {}; - re.constructor[SPECIES] = function () { return re; }; - } - re[SYMBOL](''); - return !execCalled; - }) : undefined; - - if ( - !DELEGATES_TO_SYMBOL || - !DELEGATES_TO_EXEC || - (KEY === 'replace' && !REPLACE_SUPPORTS_NAMED_GROUPS) || - (KEY === 'split' && !SPLIT_WORKS_WITH_OVERWRITTEN_EXEC) - ) { - var nativeRegExpMethod = /./[SYMBOL]; - var fns = exec( - defined, - SYMBOL, - ''[KEY], - function maybeCallNative(nativeMethod, regexp, str, arg2, forceStringMethod) { - if (regexp.exec === regexpExec) { - if (DELEGATES_TO_SYMBOL && !forceStringMethod) { - // The native String method already delegates to @@method (this - // polyfilled function), leasing to infinite recursion. - // We avoid it by directly calling the native @@method method. - return { done: true, value: nativeRegExpMethod.call(regexp, str, arg2) }; - } - return { done: true, value: nativeMethod.call(str, regexp, arg2) }; - } - return { done: false }; - } - ); - var strfn = fns[0]; - var rxfn = fns[1]; - - redefine(String.prototype, KEY, strfn); - hide(RegExp.prototype, SYMBOL, length == 2 - // 21.2.5.8 RegExp.prototype[@@replace](string, replaceValue) - // 21.2.5.11 RegExp.prototype[@@split](string, limit) - ? function (string, arg) { return rxfn.call(string, this, arg); } - // 21.2.5.6 RegExp.prototype[@@match](string) - // 21.2.5.9 RegExp.prototype[@@search](string) - : function (string) { return rxfn.call(string, this); } - ); - } -}; - -},{"./_defined":14,"./_fails":21,"./_hide":27,"./_redefine":58,"./_regexp-exec":60,"./_wks":81,"./es6.regexp.exec":93}],23:[function(require,module,exports){ -'use strict'; -// 21.2.5.3 get RegExp.prototype.flags -var anObject = require('./_an-object'); -module.exports = function () { - var that = anObject(this); - var result = ''; - if (that.global) result += 'g'; - if (that.ignoreCase) result += 'i'; - if (that.multiline) result += 'm'; - if (that.unicode) result += 'u'; - if (that.sticky) result += 'y'; - return result; -}; - -},{"./_an-object":4}],24:[function(require,module,exports){ -module.exports = require('./_shared')('native-function-to-string', Function.toString); - -},{"./_shared":65}],25:[function(require,module,exports){ -// https://github.com/zloirock/core-js/issues/86#issuecomment-115759028 -var global = module.exports = typeof window != 'undefined' && window.Math == Math - ? window : typeof self != 'undefined' && self.Math == Math ? self - // eslint-disable-next-line no-new-func - : Function('return this')(); -if (typeof __g == 'number') __g = global; // eslint-disable-line no-undef - -},{}],26:[function(require,module,exports){ -var hasOwnProperty = {}.hasOwnProperty; -module.exports = function (it, key) { - return hasOwnProperty.call(it, key); -}; - -},{}],27:[function(require,module,exports){ -var dP = require('./_object-dp'); -var createDesc = require('./_property-desc'); -module.exports = require('./_descriptors') ? function (object, key, value) { - return dP.f(object, key, createDesc(1, value)); -} : function (object, key, value) { - object[key] = value; - return object; -}; - -},{"./_descriptors":15,"./_object-dp":45,"./_property-desc":57}],28:[function(require,module,exports){ -var document = require('./_global').document; -module.exports = document && document.documentElement; - -},{"./_global":25}],29:[function(require,module,exports){ -module.exports = !require('./_descriptors') && !require('./_fails')(function () { - return Object.defineProperty(require('./_dom-create')('div'), 'a', { get: function () { return 7; } }).a != 7; -}); - -},{"./_descriptors":15,"./_dom-create":16,"./_fails":21}],30:[function(require,module,exports){ -var isObject = require('./_is-object'); -var setPrototypeOf = require('./_set-proto').set; -module.exports = function (that, target, C) { - var S = target.constructor; - var P; - if (S !== C && typeof S == 'function' && (P = S.prototype) !== C.prototype && isObject(P) && setPrototypeOf) { - setPrototypeOf(that, P); - } return that; -}; - -},{"./_is-object":34,"./_set-proto":61}],31:[function(require,module,exports){ -// fallback for non-array-like ES3 and non-enumerable old V8 strings -var cof = require('./_cof'); -// eslint-disable-next-line no-prototype-builtins -module.exports = Object('z').propertyIsEnumerable(0) ? Object : function (it) { - return cof(it) == 'String' ? it.split('') : Object(it); -}; - -},{"./_cof":10}],32:[function(require,module,exports){ -// check on default Array iterator -var Iterators = require('./_iterators'); -var ITERATOR = require('./_wks')('iterator'); -var ArrayProto = Array.prototype; - -module.exports = function (it) { - return it !== undefined && (Iterators.Array === it || ArrayProto[ITERATOR] === it); -}; - -},{"./_iterators":41,"./_wks":81}],33:[function(require,module,exports){ -// 7.2.2 IsArray(argument) -var cof = require('./_cof'); -module.exports = Array.isArray || function isArray(arg) { - return cof(arg) == 'Array'; -}; - -},{"./_cof":10}],34:[function(require,module,exports){ -module.exports = function (it) { - return typeof it === 'object' ? it !== null : typeof it === 'function'; -}; - -},{}],35:[function(require,module,exports){ -// 7.2.8 IsRegExp(argument) -var isObject = require('./_is-object'); -var cof = require('./_cof'); -var MATCH = require('./_wks')('match'); -module.exports = function (it) { - var isRegExp; - return isObject(it) && ((isRegExp = it[MATCH]) !== undefined ? !!isRegExp : cof(it) == 'RegExp'); -}; - -},{"./_cof":10,"./_is-object":34,"./_wks":81}],36:[function(require,module,exports){ -// call something on iterator step with safe closing on error -var anObject = require('./_an-object'); -module.exports = function (iterator, fn, value, entries) { - try { - return entries ? fn(anObject(value)[0], value[1]) : fn(value); - // 7.4.6 IteratorClose(iterator, completion) - } catch (e) { - var ret = iterator['return']; - if (ret !== undefined) anObject(ret.call(iterator)); - throw e; - } -}; - -},{"./_an-object":4}],37:[function(require,module,exports){ -'use strict'; -var create = require('./_object-create'); -var descriptor = require('./_property-desc'); -var setToStringTag = require('./_set-to-string-tag'); -var IteratorPrototype = {}; - -// 25.1.2.1.1 %IteratorPrototype%[@@iterator]() -require('./_hide')(IteratorPrototype, require('./_wks')('iterator'), function () { return this; }); - -module.exports = function (Constructor, NAME, next) { - Constructor.prototype = create(IteratorPrototype, { next: descriptor(1, next) }); - setToStringTag(Constructor, NAME + ' Iterator'); -}; - -},{"./_hide":27,"./_object-create":44,"./_property-desc":57,"./_set-to-string-tag":63,"./_wks":81}],38:[function(require,module,exports){ -'use strict'; -var LIBRARY = require('./_library'); -var $export = require('./_export'); -var redefine = require('./_redefine'); -var hide = require('./_hide'); -var Iterators = require('./_iterators'); -var $iterCreate = require('./_iter-create'); -var setToStringTag = require('./_set-to-string-tag'); -var getPrototypeOf = require('./_object-gpo'); -var ITERATOR = require('./_wks')('iterator'); -var BUGGY = !([].keys && 'next' in [].keys()); // Safari has buggy iterators w/o `next` -var FF_ITERATOR = '@@iterator'; -var KEYS = 'keys'; -var VALUES = 'values'; - -var returnThis = function () { return this; }; - -module.exports = function (Base, NAME, Constructor, next, DEFAULT, IS_SET, FORCED) { - $iterCreate(Constructor, NAME, next); - var getMethod = function (kind) { - if (!BUGGY && kind in proto) return proto[kind]; - switch (kind) { - case KEYS: return function keys() { return new Constructor(this, kind); }; - case VALUES: return function values() { return new Constructor(this, kind); }; - } return function entries() { return new Constructor(this, kind); }; - }; - var TAG = NAME + ' Iterator'; - var DEF_VALUES = DEFAULT == VALUES; - var VALUES_BUG = false; - var proto = Base.prototype; - var $native = proto[ITERATOR] || proto[FF_ITERATOR] || DEFAULT && proto[DEFAULT]; - var $default = $native || getMethod(DEFAULT); - var $entries = DEFAULT ? !DEF_VALUES ? $default : getMethod('entries') : undefined; - var $anyNative = NAME == 'Array' ? proto.entries || $native : $native; - var methods, key, IteratorPrototype; - // Fix native - if ($anyNative) { - IteratorPrototype = getPrototypeOf($anyNative.call(new Base())); - if (IteratorPrototype !== Object.prototype && IteratorPrototype.next) { - // Set @@toStringTag to native iterators - setToStringTag(IteratorPrototype, TAG, true); - // fix for some old engines - if (!LIBRARY && typeof IteratorPrototype[ITERATOR] != 'function') hide(IteratorPrototype, ITERATOR, returnThis); - } - } - // fix Array#{values, @@iterator}.name in V8 / FF - if (DEF_VALUES && $native && $native.name !== VALUES) { - VALUES_BUG = true; - $default = function values() { return $native.call(this); }; - } - // Define iterator - if ((!LIBRARY || FORCED) && (BUGGY || VALUES_BUG || !proto[ITERATOR])) { - hide(proto, ITERATOR, $default); - } - // Plug for library - Iterators[NAME] = $default; - Iterators[TAG] = returnThis; - if (DEFAULT) { - methods = { - values: DEF_VALUES ? $default : getMethod(VALUES), - keys: IS_SET ? $default : getMethod(KEYS), - entries: $entries - }; - if (FORCED) for (key in methods) { - if (!(key in proto)) redefine(proto, key, methods[key]); - } else $export($export.P + $export.F * (BUGGY || VALUES_BUG), NAME, methods); - } - return methods; -}; - -},{"./_export":19,"./_hide":27,"./_iter-create":37,"./_iterators":41,"./_library":42,"./_object-gpo":51,"./_redefine":58,"./_set-to-string-tag":63,"./_wks":81}],39:[function(require,module,exports){ -var ITERATOR = require('./_wks')('iterator'); -var SAFE_CLOSING = false; - -try { - var riter = [7][ITERATOR](); - riter['return'] = function () { SAFE_CLOSING = true; }; - // eslint-disable-next-line no-throw-literal - Array.from(riter, function () { throw 2; }); -} catch (e) { /* empty */ } - -module.exports = function (exec, skipClosing) { - if (!skipClosing && !SAFE_CLOSING) return false; - var safe = false; - try { - var arr = [7]; - var iter = arr[ITERATOR](); - iter.next = function () { return { done: safe = true }; }; - arr[ITERATOR] = function () { return iter; }; - exec(arr); - } catch (e) { /* empty */ } - return safe; -}; - -},{"./_wks":81}],40:[function(require,module,exports){ -module.exports = function (done, value) { - return { value: value, done: !!done }; -}; - -},{}],41:[function(require,module,exports){ -module.exports = {}; - -},{}],42:[function(require,module,exports){ -module.exports = false; - -},{}],43:[function(require,module,exports){ -var META = require('./_uid')('meta'); -var isObject = require('./_is-object'); -var has = require('./_has'); -var setDesc = require('./_object-dp').f; -var id = 0; -var isExtensible = Object.isExtensible || function () { - return true; -}; -var FREEZE = !require('./_fails')(function () { - return isExtensible(Object.preventExtensions({})); -}); -var setMeta = function (it) { - setDesc(it, META, { value: { - i: 'O' + ++id, // object ID - w: {} // weak collections IDs - } }); -}; -var fastKey = function (it, create) { - // return primitive with prefix - if (!isObject(it)) return typeof it == 'symbol' ? it : (typeof it == 'string' ? 'S' : 'P') + it; - if (!has(it, META)) { - // can't set metadata to uncaught frozen object - if (!isExtensible(it)) return 'F'; - // not necessary to add metadata - if (!create) return 'E'; - // add missing metadata - setMeta(it); - // return object ID - } return it[META].i; -}; -var getWeak = function (it, create) { - if (!has(it, META)) { - // can't set metadata to uncaught frozen object - if (!isExtensible(it)) return true; - // not necessary to add metadata - if (!create) return false; - // add missing metadata - setMeta(it); - // return hash weak collections IDs - } return it[META].w; -}; -// add metadata on freeze-family methods calling -var onFreeze = function (it) { - if (FREEZE && meta.NEED && isExtensible(it) && !has(it, META)) setMeta(it); - return it; -}; -var meta = module.exports = { - KEY: META, - NEED: false, - fastKey: fastKey, - getWeak: getWeak, - onFreeze: onFreeze -}; - -},{"./_fails":21,"./_has":26,"./_is-object":34,"./_object-dp":45,"./_uid":78}],44:[function(require,module,exports){ -// 19.1.2.2 / 15.2.3.5 Object.create(O [, Properties]) -var anObject = require('./_an-object'); -var dPs = require('./_object-dps'); -var enumBugKeys = require('./_enum-bug-keys'); -var IE_PROTO = require('./_shared-key')('IE_PROTO'); -var Empty = function () { /* empty */ }; -var PROTOTYPE = 'prototype'; - -// Create object with fake `null` prototype: use iframe Object with cleared prototype -var createDict = function () { - // Thrash, waste and sodomy: IE GC bug - var iframe = require('./_dom-create')('iframe'); - var i = enumBugKeys.length; - var lt = '<'; - var gt = '>'; - var iframeDocument; - iframe.style.display = 'none'; - require('./_html').appendChild(iframe); - iframe.src = 'javascript:'; // eslint-disable-line no-script-url - // createDict = iframe.contentWindow.Object; - // html.removeChild(iframe); - iframeDocument = iframe.contentWindow.document; - iframeDocument.open(); - iframeDocument.write(lt + 'script' + gt + 'document.F=Object' + lt + '/script' + gt); - iframeDocument.close(); - createDict = iframeDocument.F; - while (i--) delete createDict[PROTOTYPE][enumBugKeys[i]]; - return createDict(); -}; - -module.exports = Object.create || function create(O, Properties) { - var result; - if (O !== null) { - Empty[PROTOTYPE] = anObject(O); - result = new Empty(); - Empty[PROTOTYPE] = null; - // add "__proto__" for Object.getPrototypeOf polyfill - result[IE_PROTO] = O; - } else result = createDict(); - return Properties === undefined ? result : dPs(result, Properties); -}; - -},{"./_an-object":4,"./_dom-create":16,"./_enum-bug-keys":17,"./_html":28,"./_object-dps":46,"./_shared-key":64}],45:[function(require,module,exports){ -var anObject = require('./_an-object'); -var IE8_DOM_DEFINE = require('./_ie8-dom-define'); -var toPrimitive = require('./_to-primitive'); -var dP = Object.defineProperty; - -exports.f = require('./_descriptors') ? Object.defineProperty : function defineProperty(O, P, Attributes) { - anObject(O); - P = toPrimitive(P, true); - anObject(Attributes); - if (IE8_DOM_DEFINE) try { - return dP(O, P, Attributes); - } catch (e) { /* empty */ } - if ('get' in Attributes || 'set' in Attributes) throw TypeError('Accessors not supported!'); - if ('value' in Attributes) O[P] = Attributes.value; - return O; -}; - -},{"./_an-object":4,"./_descriptors":15,"./_ie8-dom-define":29,"./_to-primitive":77}],46:[function(require,module,exports){ -var dP = require('./_object-dp'); -var anObject = require('./_an-object'); -var getKeys = require('./_object-keys'); - -module.exports = require('./_descriptors') ? Object.defineProperties : function defineProperties(O, Properties) { - anObject(O); - var keys = getKeys(Properties); - var length = keys.length; - var i = 0; - var P; - while (length > i) dP.f(O, P = keys[i++], Properties[P]); - return O; -}; - -},{"./_an-object":4,"./_descriptors":15,"./_object-dp":45,"./_object-keys":53}],47:[function(require,module,exports){ -var pIE = require('./_object-pie'); -var createDesc = require('./_property-desc'); -var toIObject = require('./_to-iobject'); -var toPrimitive = require('./_to-primitive'); -var has = require('./_has'); -var IE8_DOM_DEFINE = require('./_ie8-dom-define'); -var gOPD = Object.getOwnPropertyDescriptor; - -exports.f = require('./_descriptors') ? gOPD : function getOwnPropertyDescriptor(O, P) { - O = toIObject(O); - P = toPrimitive(P, true); - if (IE8_DOM_DEFINE) try { - return gOPD(O, P); - } catch (e) { /* empty */ } - if (has(O, P)) return createDesc(!pIE.f.call(O, P), O[P]); -}; - -},{"./_descriptors":15,"./_has":26,"./_ie8-dom-define":29,"./_object-pie":54,"./_property-desc":57,"./_to-iobject":74,"./_to-primitive":77}],48:[function(require,module,exports){ -// fallback for IE11 buggy Object.getOwnPropertyNames with iframe and window -var toIObject = require('./_to-iobject'); -var gOPN = require('./_object-gopn').f; -var toString = {}.toString; - -var windowNames = typeof window == 'object' && window && Object.getOwnPropertyNames - ? Object.getOwnPropertyNames(window) : []; - -var getWindowNames = function (it) { - try { - return gOPN(it); - } catch (e) { - return windowNames.slice(); - } -}; - -module.exports.f = function getOwnPropertyNames(it) { - return windowNames && toString.call(it) == '[object Window]' ? getWindowNames(it) : gOPN(toIObject(it)); -}; - -},{"./_object-gopn":49,"./_to-iobject":74}],49:[function(require,module,exports){ -// 19.1.2.7 / 15.2.3.4 Object.getOwnPropertyNames(O) -var $keys = require('./_object-keys-internal'); -var hiddenKeys = require('./_enum-bug-keys').concat('length', 'prototype'); - -exports.f = Object.getOwnPropertyNames || function getOwnPropertyNames(O) { - return $keys(O, hiddenKeys); -}; - -},{"./_enum-bug-keys":17,"./_object-keys-internal":52}],50:[function(require,module,exports){ -exports.f = Object.getOwnPropertySymbols; - -},{}],51:[function(require,module,exports){ -// 19.1.2.9 / 15.2.3.2 Object.getPrototypeOf(O) -var has = require('./_has'); -var toObject = require('./_to-object'); -var IE_PROTO = require('./_shared-key')('IE_PROTO'); -var ObjectProto = Object.prototype; - -module.exports = Object.getPrototypeOf || function (O) { - O = toObject(O); - if (has(O, IE_PROTO)) return O[IE_PROTO]; - if (typeof O.constructor == 'function' && O instanceof O.constructor) { - return O.constructor.prototype; - } return O instanceof Object ? ObjectProto : null; -}; - -},{"./_has":26,"./_shared-key":64,"./_to-object":76}],52:[function(require,module,exports){ -var has = require('./_has'); -var toIObject = require('./_to-iobject'); -var arrayIndexOf = require('./_array-includes')(false); -var IE_PROTO = require('./_shared-key')('IE_PROTO'); - -module.exports = function (object, names) { - var O = toIObject(object); - var i = 0; - var result = []; - var key; - for (key in O) if (key != IE_PROTO) has(O, key) && result.push(key); - // Don't enum bug & hidden keys - while (names.length > i) if (has(O, key = names[i++])) { - ~arrayIndexOf(result, key) || result.push(key); - } - return result; -}; - -},{"./_array-includes":5,"./_has":26,"./_shared-key":64,"./_to-iobject":74}],53:[function(require,module,exports){ -// 19.1.2.14 / 15.2.3.14 Object.keys(O) -var $keys = require('./_object-keys-internal'); -var enumBugKeys = require('./_enum-bug-keys'); - -module.exports = Object.keys || function keys(O) { - return $keys(O, enumBugKeys); -}; - -},{"./_enum-bug-keys":17,"./_object-keys-internal":52}],54:[function(require,module,exports){ -exports.f = {}.propertyIsEnumerable; - -},{}],55:[function(require,module,exports){ -// most Object methods by ES6 should accept primitives -var $export = require('./_export'); -var core = require('./_core'); -var fails = require('./_fails'); -module.exports = function (KEY, exec) { - var fn = (core.Object || {})[KEY] || Object[KEY]; - var exp = {}; - exp[KEY] = exec(fn); - $export($export.S + $export.F * fails(function () { fn(1); }), 'Object', exp); -}; - -},{"./_core":11,"./_export":19,"./_fails":21}],56:[function(require,module,exports){ -// all object keys, includes non-enumerable and symbols -var gOPN = require('./_object-gopn'); -var gOPS = require('./_object-gops'); -var anObject = require('./_an-object'); -var Reflect = require('./_global').Reflect; -module.exports = Reflect && Reflect.ownKeys || function ownKeys(it) { - var keys = gOPN.f(anObject(it)); - var getSymbols = gOPS.f; - return getSymbols ? keys.concat(getSymbols(it)) : keys; -}; - -},{"./_an-object":4,"./_global":25,"./_object-gopn":49,"./_object-gops":50}],57:[function(require,module,exports){ -module.exports = function (bitmap, value) { - return { - enumerable: !(bitmap & 1), - configurable: !(bitmap & 2), - writable: !(bitmap & 4), - value: value - }; -}; - -},{}],58:[function(require,module,exports){ -var global = require('./_global'); -var hide = require('./_hide'); -var has = require('./_has'); -var SRC = require('./_uid')('src'); -var $toString = require('./_function-to-string'); -var TO_STRING = 'toString'; -var TPL = ('' + $toString).split(TO_STRING); - -require('./_core').inspectSource = function (it) { - return $toString.call(it); -}; - -(module.exports = function (O, key, val, safe) { - var isFunction = typeof val == 'function'; - if (isFunction) has(val, 'name') || hide(val, 'name', key); - if (O[key] === val) return; - if (isFunction) has(val, SRC) || hide(val, SRC, O[key] ? '' + O[key] : TPL.join(String(key))); - if (O === global) { - O[key] = val; - } else if (!safe) { - delete O[key]; - hide(O, key, val); - } else if (O[key]) { - O[key] = val; - } else { - hide(O, key, val); - } -// add fake Function#toString for correct work wrapped methods / constructors with methods like LoDash isNative -})(Function.prototype, TO_STRING, function toString() { - return typeof this == 'function' && this[SRC] || $toString.call(this); -}); - -},{"./_core":11,"./_function-to-string":24,"./_global":25,"./_has":26,"./_hide":27,"./_uid":78}],59:[function(require,module,exports){ -'use strict'; - -var classof = require('./_classof'); -var builtinExec = RegExp.prototype.exec; - - // `RegExpExec` abstract operation -// https://tc39.github.io/ecma262/#sec-regexpexec -module.exports = function (R, S) { - var exec = R.exec; - if (typeof exec === 'function') { - var result = exec.call(R, S); - if (typeof result !== 'object') { - throw new TypeError('RegExp exec method returned something other than an Object or null'); - } - return result; - } - if (classof(R) !== 'RegExp') { - throw new TypeError('RegExp#exec called on incompatible receiver'); - } - return builtinExec.call(R, S); -}; - -},{"./_classof":9}],60:[function(require,module,exports){ -'use strict'; - -var regexpFlags = require('./_flags'); - -var nativeExec = RegExp.prototype.exec; -// This always refers to the native implementation, because the -// String#replace polyfill uses ./fix-regexp-well-known-symbol-logic.js, -// which loads this file before patching the method. -var nativeReplace = String.prototype.replace; - -var patchedExec = nativeExec; - -var LAST_INDEX = 'lastIndex'; - -var UPDATES_LAST_INDEX_WRONG = (function () { - var re1 = /a/, - re2 = /b*/g; - nativeExec.call(re1, 'a'); - nativeExec.call(re2, 'a'); - return re1[LAST_INDEX] !== 0 || re2[LAST_INDEX] !== 0; -})(); - -// nonparticipating capturing group, copied from es5-shim's String#split patch. -var NPCG_INCLUDED = /()??/.exec('')[1] !== undefined; - -var PATCH = UPDATES_LAST_INDEX_WRONG || NPCG_INCLUDED; - -if (PATCH) { - patchedExec = function exec(str) { - var re = this; - var lastIndex, reCopy, match, i; - - if (NPCG_INCLUDED) { - reCopy = new RegExp('^' + re.source + '$(?!\\s)', regexpFlags.call(re)); - } - if (UPDATES_LAST_INDEX_WRONG) lastIndex = re[LAST_INDEX]; - - match = nativeExec.call(re, str); - - if (UPDATES_LAST_INDEX_WRONG && match) { - re[LAST_INDEX] = re.global ? match.index + match[0].length : lastIndex; - } - if (NPCG_INCLUDED && match && match.length > 1) { - // Fix browsers whose `exec` methods don't consistently return `undefined` - // for NPCG, like IE8. NOTE: This doesn' work for /(.?)?/ - // eslint-disable-next-line no-loop-func - nativeReplace.call(match[0], reCopy, function () { - for (i = 1; i < arguments.length - 2; i++) { - if (arguments[i] === undefined) match[i] = undefined; - } - }); - } - - return match; - }; -} - -module.exports = patchedExec; - -},{"./_flags":23}],61:[function(require,module,exports){ -// Works with __proto__ only. Old v8 can't work with null proto objects. -/* eslint-disable no-proto */ -var isObject = require('./_is-object'); -var anObject = require('./_an-object'); -var check = function (O, proto) { - anObject(O); - if (!isObject(proto) && proto !== null) throw TypeError(proto + ": can't set as prototype!"); -}; -module.exports = { - set: Object.setPrototypeOf || ('__proto__' in {} ? // eslint-disable-line - function (test, buggy, set) { - try { - set = require('./_ctx')(Function.call, require('./_object-gopd').f(Object.prototype, '__proto__').set, 2); - set(test, []); - buggy = !(test instanceof Array); - } catch (e) { buggy = true; } - return function setPrototypeOf(O, proto) { - check(O, proto); - if (buggy) O.__proto__ = proto; - else set(O, proto); - return O; - }; - }({}, false) : undefined), - check: check -}; - -},{"./_an-object":4,"./_ctx":13,"./_is-object":34,"./_object-gopd":47}],62:[function(require,module,exports){ -'use strict'; -var global = require('./_global'); -var dP = require('./_object-dp'); -var DESCRIPTORS = require('./_descriptors'); -var SPECIES = require('./_wks')('species'); - -module.exports = function (KEY) { - var C = global[KEY]; - if (DESCRIPTORS && C && !C[SPECIES]) dP.f(C, SPECIES, { - configurable: true, - get: function () { return this; } - }); -}; - -},{"./_descriptors":15,"./_global":25,"./_object-dp":45,"./_wks":81}],63:[function(require,module,exports){ -var def = require('./_object-dp').f; -var has = require('./_has'); -var TAG = require('./_wks')('toStringTag'); - -module.exports = function (it, tag, stat) { - if (it && !has(it = stat ? it : it.prototype, TAG)) def(it, TAG, { configurable: true, value: tag }); -}; - -},{"./_has":26,"./_object-dp":45,"./_wks":81}],64:[function(require,module,exports){ -var shared = require('./_shared')('keys'); -var uid = require('./_uid'); -module.exports = function (key) { - return shared[key] || (shared[key] = uid(key)); -}; - -},{"./_shared":65,"./_uid":78}],65:[function(require,module,exports){ -var core = require('./_core'); -var global = require('./_global'); -var SHARED = '__core-js_shared__'; -var store = global[SHARED] || (global[SHARED] = {}); - -(module.exports = function (key, value) { - return store[key] || (store[key] = value !== undefined ? value : {}); -})('versions', []).push({ - version: core.version, - mode: require('./_library') ? 'pure' : 'global', - copyright: '© 2020 Denis Pushkarev (zloirock.ru)' -}); - -},{"./_core":11,"./_global":25,"./_library":42}],66:[function(require,module,exports){ -// 7.3.20 SpeciesConstructor(O, defaultConstructor) -var anObject = require('./_an-object'); -var aFunction = require('./_a-function'); -var SPECIES = require('./_wks')('species'); -module.exports = function (O, D) { - var C = anObject(O).constructor; - var S; - return C === undefined || (S = anObject(C)[SPECIES]) == undefined ? D : aFunction(S); -}; - -},{"./_a-function":1,"./_an-object":4,"./_wks":81}],67:[function(require,module,exports){ -'use strict'; -var fails = require('./_fails'); - -module.exports = function (method, arg) { - return !!method && fails(function () { - // eslint-disable-next-line no-useless-call - arg ? method.call(null, function () { /* empty */ }, 1) : method.call(null); - }); -}; - -},{"./_fails":21}],68:[function(require,module,exports){ -var toInteger = require('./_to-integer'); -var defined = require('./_defined'); -// true -> String#at -// false -> String#codePointAt -module.exports = function (TO_STRING) { - return function (that, pos) { - var s = String(defined(that)); - var i = toInteger(pos); - var l = s.length; - var a, b; - if (i < 0 || i >= l) return TO_STRING ? '' : undefined; - a = s.charCodeAt(i); - return a < 0xd800 || a > 0xdbff || i + 1 === l || (b = s.charCodeAt(i + 1)) < 0xdc00 || b > 0xdfff - ? TO_STRING ? s.charAt(i) : a - : TO_STRING ? s.slice(i, i + 2) : (a - 0xd800 << 10) + (b - 0xdc00) + 0x10000; - }; -}; - -},{"./_defined":14,"./_to-integer":73}],69:[function(require,module,exports){ -// helper for String#{startsWith, endsWith, includes} -var isRegExp = require('./_is-regexp'); -var defined = require('./_defined'); - -module.exports = function (that, searchString, NAME) { - if (isRegExp(searchString)) throw TypeError('String#' + NAME + " doesn't accept regex!"); - return String(defined(that)); -}; - -},{"./_defined":14,"./_is-regexp":35}],70:[function(require,module,exports){ -var $export = require('./_export'); -var defined = require('./_defined'); -var fails = require('./_fails'); -var spaces = require('./_string-ws'); -var space = '[' + spaces + ']'; -var non = '\u200b\u0085'; -var ltrim = RegExp('^' + space + space + '*'); -var rtrim = RegExp(space + space + '*$'); - -var exporter = function (KEY, exec, ALIAS) { - var exp = {}; - var FORCE = fails(function () { - return !!spaces[KEY]() || non[KEY]() != non; - }); - var fn = exp[KEY] = FORCE ? exec(trim) : spaces[KEY]; - if (ALIAS) exp[ALIAS] = fn; - $export($export.P + $export.F * FORCE, 'String', exp); -}; - -// 1 -> String#trimLeft -// 2 -> String#trimRight -// 3 -> String#trim -var trim = exporter.trim = function (string, TYPE) { - string = String(defined(string)); - if (TYPE & 1) string = string.replace(ltrim, ''); - if (TYPE & 2) string = string.replace(rtrim, ''); - return string; -}; - -module.exports = exporter; - -},{"./_defined":14,"./_export":19,"./_fails":21,"./_string-ws":71}],71:[function(require,module,exports){ -module.exports = '\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003' + - '\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028\u2029\uFEFF'; - -},{}],72:[function(require,module,exports){ -var toInteger = require('./_to-integer'); -var max = Math.max; -var min = Math.min; -module.exports = function (index, length) { - index = toInteger(index); - return index < 0 ? max(index + length, 0) : min(index, length); -}; - -},{"./_to-integer":73}],73:[function(require,module,exports){ -// 7.1.4 ToInteger -var ceil = Math.ceil; -var floor = Math.floor; -module.exports = function (it) { - return isNaN(it = +it) ? 0 : (it > 0 ? floor : ceil)(it); -}; - -},{}],74:[function(require,module,exports){ -// to indexed object, toObject with fallback for non-array-like ES3 strings -var IObject = require('./_iobject'); -var defined = require('./_defined'); -module.exports = function (it) { - return IObject(defined(it)); -}; - -},{"./_defined":14,"./_iobject":31}],75:[function(require,module,exports){ -// 7.1.15 ToLength -var toInteger = require('./_to-integer'); -var min = Math.min; -module.exports = function (it) { - return it > 0 ? min(toInteger(it), 0x1fffffffffffff) : 0; // pow(2, 53) - 1 == 9007199254740991 -}; - -},{"./_to-integer":73}],76:[function(require,module,exports){ -// 7.1.13 ToObject(argument) -var defined = require('./_defined'); -module.exports = function (it) { - return Object(defined(it)); -}; - -},{"./_defined":14}],77:[function(require,module,exports){ -// 7.1.1 ToPrimitive(input [, PreferredType]) -var isObject = require('./_is-object'); -// instead of the ES6 spec version, we didn't implement @@toPrimitive case -// and the second argument - flag - preferred type is a string -module.exports = function (it, S) { - if (!isObject(it)) return it; - var fn, val; - if (S && typeof (fn = it.toString) == 'function' && !isObject(val = fn.call(it))) return val; - if (typeof (fn = it.valueOf) == 'function' && !isObject(val = fn.call(it))) return val; - if (!S && typeof (fn = it.toString) == 'function' && !isObject(val = fn.call(it))) return val; - throw TypeError("Can't convert object to primitive value"); -}; - -},{"./_is-object":34}],78:[function(require,module,exports){ -var id = 0; -var px = Math.random(); -module.exports = function (key) { - return 'Symbol('.concat(key === undefined ? '' : key, ')_', (++id + px).toString(36)); -}; - -},{}],79:[function(require,module,exports){ -var global = require('./_global'); -var core = require('./_core'); -var LIBRARY = require('./_library'); -var wksExt = require('./_wks-ext'); -var defineProperty = require('./_object-dp').f; -module.exports = function (name) { - var $Symbol = core.Symbol || (core.Symbol = LIBRARY ? {} : global.Symbol || {}); - if (name.charAt(0) != '_' && !(name in $Symbol)) defineProperty($Symbol, name, { value: wksExt.f(name) }); -}; - -},{"./_core":11,"./_global":25,"./_library":42,"./_object-dp":45,"./_wks-ext":80}],80:[function(require,module,exports){ -exports.f = require('./_wks'); - -},{"./_wks":81}],81:[function(require,module,exports){ -var store = require('./_shared')('wks'); -var uid = require('./_uid'); -var Symbol = require('./_global').Symbol; -var USE_SYMBOL = typeof Symbol == 'function'; - -var $exports = module.exports = function (name) { - return store[name] || (store[name] = - USE_SYMBOL && Symbol[name] || (USE_SYMBOL ? Symbol : uid)('Symbol.' + name)); -}; - -$exports.store = store; - -},{"./_global":25,"./_shared":65,"./_uid":78}],82:[function(require,module,exports){ -var classof = require('./_classof'); -var ITERATOR = require('./_wks')('iterator'); -var Iterators = require('./_iterators'); -module.exports = require('./_core').getIteratorMethod = function (it) { - if (it != undefined) return it[ITERATOR] - || it['@@iterator'] - || Iterators[classof(it)]; -}; - -},{"./_classof":9,"./_core":11,"./_iterators":41,"./_wks":81}],83:[function(require,module,exports){ -'use strict'; -var $export = require('./_export'); -var $filter = require('./_array-methods')(2); - -$export($export.P + $export.F * !require('./_strict-method')([].filter, true), 'Array', { - // 22.1.3.7 / 15.4.4.20 Array.prototype.filter(callbackfn [, thisArg]) - filter: function filter(callbackfn /* , thisArg */) { - return $filter(this, callbackfn, arguments[1]); - } -}); - -},{"./_array-methods":6,"./_export":19,"./_strict-method":67}],84:[function(require,module,exports){ -'use strict'; -var ctx = require('./_ctx'); -var $export = require('./_export'); -var toObject = require('./_to-object'); -var call = require('./_iter-call'); -var isArrayIter = require('./_is-array-iter'); -var toLength = require('./_to-length'); -var createProperty = require('./_create-property'); -var getIterFn = require('./core.get-iterator-method'); - -$export($export.S + $export.F * !require('./_iter-detect')(function (iter) { Array.from(iter); }), 'Array', { - // 22.1.2.1 Array.from(arrayLike, mapfn = undefined, thisArg = undefined) - from: function from(arrayLike /* , mapfn = undefined, thisArg = undefined */) { - var O = toObject(arrayLike); - var C = typeof this == 'function' ? this : Array; - var aLen = arguments.length; - var mapfn = aLen > 1 ? arguments[1] : undefined; - var mapping = mapfn !== undefined; - var index = 0; - var iterFn = getIterFn(O); - var length, result, step, iterator; - if (mapping) mapfn = ctx(mapfn, aLen > 2 ? arguments[2] : undefined, 2); - // if object isn't iterable or it's array with default iterator - use simple case - if (iterFn != undefined && !(C == Array && isArrayIter(iterFn))) { - for (iterator = iterFn.call(O), result = new C(); !(step = iterator.next()).done; index++) { - createProperty(result, index, mapping ? call(iterator, mapfn, [step.value, index], true) : step.value); - } - } else { - length = toLength(O.length); - for (result = new C(length); length > index; index++) { - createProperty(result, index, mapping ? mapfn(O[index], index) : O[index]); - } - } - result.length = index; - return result; - } -}); - -},{"./_create-property":12,"./_ctx":13,"./_export":19,"./_is-array-iter":32,"./_iter-call":36,"./_iter-detect":39,"./_to-length":75,"./_to-object":76,"./core.get-iterator-method":82}],85:[function(require,module,exports){ -'use strict'; -var addToUnscopables = require('./_add-to-unscopables'); -var step = require('./_iter-step'); -var Iterators = require('./_iterators'); -var toIObject = require('./_to-iobject'); - -// 22.1.3.4 Array.prototype.entries() -// 22.1.3.13 Array.prototype.keys() -// 22.1.3.29 Array.prototype.values() -// 22.1.3.30 Array.prototype[@@iterator]() -module.exports = require('./_iter-define')(Array, 'Array', function (iterated, kind) { - this._t = toIObject(iterated); // target - this._i = 0; // next index - this._k = kind; // kind -// 22.1.5.2.1 %ArrayIteratorPrototype%.next() -}, function () { - var O = this._t; - var kind = this._k; - var index = this._i++; - if (!O || index >= O.length) { - this._t = undefined; - return step(1); - } - if (kind == 'keys') return step(0, index); - if (kind == 'values') return step(0, O[index]); - return step(0, [index, O[index]]); -}, 'values'); - -// argumentsList[@@iterator] is %ArrayProto_values% (9.4.4.6, 9.4.4.7) -Iterators.Arguments = Iterators.Array; - -addToUnscopables('keys'); -addToUnscopables('values'); -addToUnscopables('entries'); - -},{"./_add-to-unscopables":2,"./_iter-define":38,"./_iter-step":40,"./_iterators":41,"./_to-iobject":74}],86:[function(require,module,exports){ -'use strict'; -var $export = require('./_export'); -var $map = require('./_array-methods')(1); - -$export($export.P + $export.F * !require('./_strict-method')([].map, true), 'Array', { - // 22.1.3.15 / 15.4.4.19 Array.prototype.map(callbackfn [, thisArg]) - map: function map(callbackfn /* , thisArg */) { - return $map(this, callbackfn, arguments[1]); - } -}); - -},{"./_array-methods":6,"./_export":19,"./_strict-method":67}],87:[function(require,module,exports){ -'use strict'; -var $export = require('./_export'); -var html = require('./_html'); -var cof = require('./_cof'); -var toAbsoluteIndex = require('./_to-absolute-index'); -var toLength = require('./_to-length'); -var arraySlice = [].slice; - -// fallback for not array-like ES3 strings and DOM objects -$export($export.P + $export.F * require('./_fails')(function () { - if (html) arraySlice.call(html); -}), 'Array', { - slice: function slice(begin, end) { - var len = toLength(this.length); - var klass = cof(this); - end = end === undefined ? len : end; - if (klass == 'Array') return arraySlice.call(this, begin, end); - var start = toAbsoluteIndex(begin, len); - var upTo = toAbsoluteIndex(end, len); - var size = toLength(upTo - start); - var cloned = new Array(size); - var i = 0; - for (; i < size; i++) cloned[i] = klass == 'String' - ? this.charAt(start + i) - : this[start + i]; - return cloned; - } -}); - -},{"./_cof":10,"./_export":19,"./_fails":21,"./_html":28,"./_to-absolute-index":72,"./_to-length":75}],88:[function(require,module,exports){ -'use strict'; -var global = require('./_global'); -var has = require('./_has'); -var cof = require('./_cof'); -var inheritIfRequired = require('./_inherit-if-required'); -var toPrimitive = require('./_to-primitive'); -var fails = require('./_fails'); -var gOPN = require('./_object-gopn').f; -var gOPD = require('./_object-gopd').f; -var dP = require('./_object-dp').f; -var $trim = require('./_string-trim').trim; -var NUMBER = 'Number'; -var $Number = global[NUMBER]; -var Base = $Number; -var proto = $Number.prototype; -// Opera ~12 has broken Object#toString -var BROKEN_COF = cof(require('./_object-create')(proto)) == NUMBER; -var TRIM = 'trim' in String.prototype; - -// 7.1.3 ToNumber(argument) -var toNumber = function (argument) { - var it = toPrimitive(argument, false); - if (typeof it == 'string' && it.length > 2) { - it = TRIM ? it.trim() : $trim(it, 3); - var first = it.charCodeAt(0); - var third, radix, maxCode; - if (first === 43 || first === 45) { - third = it.charCodeAt(2); - if (third === 88 || third === 120) return NaN; // Number('+0x1') should be NaN, old V8 fix - } else if (first === 48) { - switch (it.charCodeAt(1)) { - case 66: case 98: radix = 2; maxCode = 49; break; // fast equal /^0b[01]+$/i - case 79: case 111: radix = 8; maxCode = 55; break; // fast equal /^0o[0-7]+$/i - default: return +it; - } - for (var digits = it.slice(2), i = 0, l = digits.length, code; i < l; i++) { - code = digits.charCodeAt(i); - // parseInt parses a string to a first unavailable symbol - // but ToNumber should return NaN if a string contains unavailable symbols - if (code < 48 || code > maxCode) return NaN; - } return parseInt(digits, radix); - } - } return +it; -}; - -if (!$Number(' 0o1') || !$Number('0b1') || $Number('+0x1')) { - $Number = function Number(value) { - var it = arguments.length < 1 ? 0 : value; - var that = this; - return that instanceof $Number - // check on 1..constructor(foo) case - && (BROKEN_COF ? fails(function () { proto.valueOf.call(that); }) : cof(that) != NUMBER) - ? inheritIfRequired(new Base(toNumber(it)), that, $Number) : toNumber(it); - }; - for (var keys = require('./_descriptors') ? gOPN(Base) : ( - // ES3: - 'MAX_VALUE,MIN_VALUE,NaN,NEGATIVE_INFINITY,POSITIVE_INFINITY,' + - // ES6 (in case, if modules with ES6 Number statics required before): - 'EPSILON,isFinite,isInteger,isNaN,isSafeInteger,MAX_SAFE_INTEGER,' + - 'MIN_SAFE_INTEGER,parseFloat,parseInt,isInteger' - ).split(','), j = 0, key; keys.length > j; j++) { - if (has(Base, key = keys[j]) && !has($Number, key)) { - dP($Number, key, gOPD(Base, key)); - } - } - $Number.prototype = proto; - proto.constructor = $Number; - require('./_redefine')(global, NUMBER, $Number); -} - -},{"./_cof":10,"./_descriptors":15,"./_fails":21,"./_global":25,"./_has":26,"./_inherit-if-required":30,"./_object-create":44,"./_object-dp":45,"./_object-gopd":47,"./_object-gopn":49,"./_redefine":58,"./_string-trim":70,"./_to-primitive":77}],89:[function(require,module,exports){ -// 19.1.2.6 Object.getOwnPropertyDescriptor(O, P) -var toIObject = require('./_to-iobject'); -var $getOwnPropertyDescriptor = require('./_object-gopd').f; - -require('./_object-sap')('getOwnPropertyDescriptor', function () { - return function getOwnPropertyDescriptor(it, key) { - return $getOwnPropertyDescriptor(toIObject(it), key); - }; -}); - -},{"./_object-gopd":47,"./_object-sap":55,"./_to-iobject":74}],90:[function(require,module,exports){ -// 19.1.2.14 Object.keys(O) -var toObject = require('./_to-object'); -var $keys = require('./_object-keys'); - -require('./_object-sap')('keys', function () { - return function keys(it) { - return $keys(toObject(it)); - }; -}); - -},{"./_object-keys":53,"./_object-sap":55,"./_to-object":76}],91:[function(require,module,exports){ -'use strict'; -// 19.1.3.6 Object.prototype.toString() -var classof = require('./_classof'); -var test = {}; -test[require('./_wks')('toStringTag')] = 'z'; -if (test + '' != '[object z]') { - require('./_redefine')(Object.prototype, 'toString', function toString() { - return '[object ' + classof(this) + ']'; - }, true); -} - -},{"./_classof":9,"./_redefine":58,"./_wks":81}],92:[function(require,module,exports){ -var global = require('./_global'); -var inheritIfRequired = require('./_inherit-if-required'); -var dP = require('./_object-dp').f; -var gOPN = require('./_object-gopn').f; -var isRegExp = require('./_is-regexp'); -var $flags = require('./_flags'); -var $RegExp = global.RegExp; -var Base = $RegExp; -var proto = $RegExp.prototype; -var re1 = /a/g; -var re2 = /a/g; -// "new" creates a new object, old webkit buggy here -var CORRECT_NEW = new $RegExp(re1) !== re1; - -if (require('./_descriptors') && (!CORRECT_NEW || require('./_fails')(function () { - re2[require('./_wks')('match')] = false; - // RegExp constructor can alter flags and IsRegExp works correct with @@match - return $RegExp(re1) != re1 || $RegExp(re2) == re2 || $RegExp(re1, 'i') != '/a/i'; -}))) { - $RegExp = function RegExp(p, f) { - var tiRE = this instanceof $RegExp; - var piRE = isRegExp(p); - var fiU = f === undefined; - return !tiRE && piRE && p.constructor === $RegExp && fiU ? p - : inheritIfRequired(CORRECT_NEW - ? new Base(piRE && !fiU ? p.source : p, f) - : Base((piRE = p instanceof $RegExp) ? p.source : p, piRE && fiU ? $flags.call(p) : f) - , tiRE ? this : proto, $RegExp); - }; - var proxy = function (key) { - key in $RegExp || dP($RegExp, key, { - configurable: true, - get: function () { return Base[key]; }, - set: function (it) { Base[key] = it; } - }); - }; - for (var keys = gOPN(Base), i = 0; keys.length > i;) proxy(keys[i++]); - proto.constructor = $RegExp; - $RegExp.prototype = proto; - require('./_redefine')(global, 'RegExp', $RegExp); -} - -require('./_set-species')('RegExp'); - -},{"./_descriptors":15,"./_fails":21,"./_flags":23,"./_global":25,"./_inherit-if-required":30,"./_is-regexp":35,"./_object-dp":45,"./_object-gopn":49,"./_redefine":58,"./_set-species":62,"./_wks":81}],93:[function(require,module,exports){ -'use strict'; -var regexpExec = require('./_regexp-exec'); -require('./_export')({ - target: 'RegExp', - proto: true, - forced: regexpExec !== /./.exec -}, { - exec: regexpExec -}); - -},{"./_export":19,"./_regexp-exec":60}],94:[function(require,module,exports){ -'use strict'; - -var anObject = require('./_an-object'); -var toLength = require('./_to-length'); -var advanceStringIndex = require('./_advance-string-index'); -var regExpExec = require('./_regexp-exec-abstract'); - -// @@match logic -require('./_fix-re-wks')('match', 1, function (defined, MATCH, $match, maybeCallNative) { - return [ - // `String.prototype.match` method - // https://tc39.github.io/ecma262/#sec-string.prototype.match - function match(regexp) { - var O = defined(this); - var fn = regexp == undefined ? undefined : regexp[MATCH]; - return fn !== undefined ? fn.call(regexp, O) : new RegExp(regexp)[MATCH](String(O)); - }, - // `RegExp.prototype[@@match]` method - // https://tc39.github.io/ecma262/#sec-regexp.prototype-@@match - function (regexp) { - var res = maybeCallNative($match, regexp, this); - if (res.done) return res.value; - var rx = anObject(regexp); - var S = String(this); - if (!rx.global) return regExpExec(rx, S); - var fullUnicode = rx.unicode; - rx.lastIndex = 0; - var A = []; - var n = 0; - var result; - while ((result = regExpExec(rx, S)) !== null) { - var matchStr = String(result[0]); - A[n] = matchStr; - if (matchStr === '') rx.lastIndex = advanceStringIndex(S, toLength(rx.lastIndex), fullUnicode); - n++; - } - return n === 0 ? null : A; - } - ]; -}); - -},{"./_advance-string-index":3,"./_an-object":4,"./_fix-re-wks":22,"./_regexp-exec-abstract":59,"./_to-length":75}],95:[function(require,module,exports){ -'use strict'; - -var anObject = require('./_an-object'); -var toObject = require('./_to-object'); -var toLength = require('./_to-length'); -var toInteger = require('./_to-integer'); -var advanceStringIndex = require('./_advance-string-index'); -var regExpExec = require('./_regexp-exec-abstract'); -var max = Math.max; -var min = Math.min; -var floor = Math.floor; -var SUBSTITUTION_SYMBOLS = /\$([$&`']|\d\d?|<[^>]*>)/g; -var SUBSTITUTION_SYMBOLS_NO_NAMED = /\$([$&`']|\d\d?)/g; - -var maybeToString = function (it) { - return it === undefined ? it : String(it); -}; - -// @@replace logic -require('./_fix-re-wks')('replace', 2, function (defined, REPLACE, $replace, maybeCallNative) { - return [ - // `String.prototype.replace` method - // https://tc39.github.io/ecma262/#sec-string.prototype.replace - function replace(searchValue, replaceValue) { - var O = defined(this); - var fn = searchValue == undefined ? undefined : searchValue[REPLACE]; - return fn !== undefined - ? fn.call(searchValue, O, replaceValue) - : $replace.call(String(O), searchValue, replaceValue); - }, - // `RegExp.prototype[@@replace]` method - // https://tc39.github.io/ecma262/#sec-regexp.prototype-@@replace - function (regexp, replaceValue) { - var res = maybeCallNative($replace, regexp, this, replaceValue); - if (res.done) return res.value; - - var rx = anObject(regexp); - var S = String(this); - var functionalReplace = typeof replaceValue === 'function'; - if (!functionalReplace) replaceValue = String(replaceValue); - var global = rx.global; - if (global) { - var fullUnicode = rx.unicode; - rx.lastIndex = 0; - } - var results = []; - while (true) { - var result = regExpExec(rx, S); - if (result === null) break; - results.push(result); - if (!global) break; - var matchStr = String(result[0]); - if (matchStr === '') rx.lastIndex = advanceStringIndex(S, toLength(rx.lastIndex), fullUnicode); - } - var accumulatedResult = ''; - var nextSourcePosition = 0; - for (var i = 0; i < results.length; i++) { - result = results[i]; - var matched = String(result[0]); - var position = max(min(toInteger(result.index), S.length), 0); - var captures = []; - // NOTE: This is equivalent to - // captures = result.slice(1).map(maybeToString) - // but for some reason `nativeSlice.call(result, 1, result.length)` (called in - // the slice polyfill when slicing native arrays) "doesn't work" in safari 9 and - // causes a crash (https://pastebin.com/N21QzeQA) when trying to debug it. - for (var j = 1; j < result.length; j++) captures.push(maybeToString(result[j])); - var namedCaptures = result.groups; - if (functionalReplace) { - var replacerArgs = [matched].concat(captures, position, S); - if (namedCaptures !== undefined) replacerArgs.push(namedCaptures); - var replacement = String(replaceValue.apply(undefined, replacerArgs)); - } else { - replacement = getSubstitution(matched, S, position, captures, namedCaptures, replaceValue); - } - if (position >= nextSourcePosition) { - accumulatedResult += S.slice(nextSourcePosition, position) + replacement; - nextSourcePosition = position + matched.length; - } - } - return accumulatedResult + S.slice(nextSourcePosition); - } - ]; - - // https://tc39.github.io/ecma262/#sec-getsubstitution - function getSubstitution(matched, str, position, captures, namedCaptures, replacement) { - var tailPos = position + matched.length; - var m = captures.length; - var symbols = SUBSTITUTION_SYMBOLS_NO_NAMED; - if (namedCaptures !== undefined) { - namedCaptures = toObject(namedCaptures); - symbols = SUBSTITUTION_SYMBOLS; - } - return $replace.call(replacement, symbols, function (match, ch) { - var capture; - switch (ch.charAt(0)) { - case '$': return '$'; - case '&': return matched; - case '`': return str.slice(0, position); - case "'": return str.slice(tailPos); - case '<': - capture = namedCaptures[ch.slice(1, -1)]; - break; - default: // \d\d? - var n = +ch; - if (n === 0) return match; - if (n > m) { - var f = floor(n / 10); - if (f === 0) return match; - if (f <= m) return captures[f - 1] === undefined ? ch.charAt(1) : captures[f - 1] + ch.charAt(1); - return match; - } - capture = captures[n - 1]; - } - return capture === undefined ? '' : capture; - }); - } -}); - -},{"./_advance-string-index":3,"./_an-object":4,"./_fix-re-wks":22,"./_regexp-exec-abstract":59,"./_to-integer":73,"./_to-length":75,"./_to-object":76}],96:[function(require,module,exports){ -'use strict'; - -var isRegExp = require('./_is-regexp'); -var anObject = require('./_an-object'); -var speciesConstructor = require('./_species-constructor'); -var advanceStringIndex = require('./_advance-string-index'); -var toLength = require('./_to-length'); -var callRegExpExec = require('./_regexp-exec-abstract'); -var regexpExec = require('./_regexp-exec'); -var fails = require('./_fails'); -var $min = Math.min; -var $push = [].push; -var $SPLIT = 'split'; -var LENGTH = 'length'; -var LAST_INDEX = 'lastIndex'; -var MAX_UINT32 = 0xffffffff; - -// babel-minify transpiles RegExp('x', 'y') -> /x/y and it causes SyntaxError -var SUPPORTS_Y = !fails(function () { RegExp(MAX_UINT32, 'y'); }); - -// @@split logic -require('./_fix-re-wks')('split', 2, function (defined, SPLIT, $split, maybeCallNative) { - var internalSplit; - if ( - 'abbc'[$SPLIT](/(b)*/)[1] == 'c' || - 'test'[$SPLIT](/(?:)/, -1)[LENGTH] != 4 || - 'ab'[$SPLIT](/(?:ab)*/)[LENGTH] != 2 || - '.'[$SPLIT](/(.?)(.?)/)[LENGTH] != 4 || - '.'[$SPLIT](/()()/)[LENGTH] > 1 || - ''[$SPLIT](/.?/)[LENGTH] - ) { - // based on es5-shim implementation, need to rework it - internalSplit = function (separator, limit) { - var string = String(this); - if (separator === undefined && limit === 0) return []; - // If `separator` is not a regex, use native split - if (!isRegExp(separator)) return $split.call(string, separator, limit); - var output = []; - var flags = (separator.ignoreCase ? 'i' : '') + - (separator.multiline ? 'm' : '') + - (separator.unicode ? 'u' : '') + - (separator.sticky ? 'y' : ''); - var lastLastIndex = 0; - var splitLimit = limit === undefined ? MAX_UINT32 : limit >>> 0; - // Make `global` and avoid `lastIndex` issues by working with a copy - var separatorCopy = new RegExp(separator.source, flags + 'g'); - var match, lastIndex, lastLength; - while (match = regexpExec.call(separatorCopy, string)) { - lastIndex = separatorCopy[LAST_INDEX]; - if (lastIndex > lastLastIndex) { - output.push(string.slice(lastLastIndex, match.index)); - if (match[LENGTH] > 1 && match.index < string[LENGTH]) $push.apply(output, match.slice(1)); - lastLength = match[0][LENGTH]; - lastLastIndex = lastIndex; - if (output[LENGTH] >= splitLimit) break; - } - if (separatorCopy[LAST_INDEX] === match.index) separatorCopy[LAST_INDEX]++; // Avoid an infinite loop - } - if (lastLastIndex === string[LENGTH]) { - if (lastLength || !separatorCopy.test('')) output.push(''); - } else output.push(string.slice(lastLastIndex)); - return output[LENGTH] > splitLimit ? output.slice(0, splitLimit) : output; - }; - // Chakra, V8 - } else if ('0'[$SPLIT](undefined, 0)[LENGTH]) { - internalSplit = function (separator, limit) { - return separator === undefined && limit === 0 ? [] : $split.call(this, separator, limit); - }; - } else { - internalSplit = $split; - } - - return [ - // `String.prototype.split` method - // https://tc39.github.io/ecma262/#sec-string.prototype.split - function split(separator, limit) { - var O = defined(this); - var splitter = separator == undefined ? undefined : separator[SPLIT]; - return splitter !== undefined - ? splitter.call(separator, O, limit) - : internalSplit.call(String(O), separator, limit); - }, - // `RegExp.prototype[@@split]` method - // https://tc39.github.io/ecma262/#sec-regexp.prototype-@@split - // - // NOTE: This cannot be properly polyfilled in engines that don't support - // the 'y' flag. - function (regexp, limit) { - var res = maybeCallNative(internalSplit, regexp, this, limit, internalSplit !== $split); - if (res.done) return res.value; - - var rx = anObject(regexp); - var S = String(this); - var C = speciesConstructor(rx, RegExp); - - var unicodeMatching = rx.unicode; - var flags = (rx.ignoreCase ? 'i' : '') + - (rx.multiline ? 'm' : '') + - (rx.unicode ? 'u' : '') + - (SUPPORTS_Y ? 'y' : 'g'); - - // ^(? + rx + ) is needed, in combination with some S slicing, to - // simulate the 'y' flag. - var splitter = new C(SUPPORTS_Y ? rx : '^(?:' + rx.source + ')', flags); - var lim = limit === undefined ? MAX_UINT32 : limit >>> 0; - if (lim === 0) return []; - if (S.length === 0) return callRegExpExec(splitter, S) === null ? [S] : []; - var p = 0; - var q = 0; - var A = []; - while (q < S.length) { - splitter.lastIndex = SUPPORTS_Y ? q : 0; - var z = callRegExpExec(splitter, SUPPORTS_Y ? S : S.slice(q)); - var e; - if ( - z === null || - (e = $min(toLength(splitter.lastIndex + (SUPPORTS_Y ? 0 : q)), S.length)) === p - ) { - q = advanceStringIndex(S, q, unicodeMatching); - } else { - A.push(S.slice(p, q)); - if (A.length === lim) return A; - for (var i = 1; i <= z.length - 1; i++) { - A.push(z[i]); - if (A.length === lim) return A; - } - q = p = e; - } - } - A.push(S.slice(p)); - return A; - } - ]; -}); - -},{"./_advance-string-index":3,"./_an-object":4,"./_fails":21,"./_fix-re-wks":22,"./_is-regexp":35,"./_regexp-exec":60,"./_regexp-exec-abstract":59,"./_species-constructor":66,"./_to-length":75}],97:[function(require,module,exports){ -// 21.1.3.7 String.prototype.includes(searchString, position = 0) -'use strict'; -var $export = require('./_export'); -var context = require('./_string-context'); -var INCLUDES = 'includes'; - -$export($export.P + $export.F * require('./_fails-is-regexp')(INCLUDES), 'String', { - includes: function includes(searchString /* , position = 0 */) { - return !!~context(this, searchString, INCLUDES) - .indexOf(searchString, arguments.length > 1 ? arguments[1] : undefined); - } -}); - -},{"./_export":19,"./_fails-is-regexp":20,"./_string-context":69}],98:[function(require,module,exports){ -'use strict'; -var $at = require('./_string-at')(true); - -// 21.1.3.27 String.prototype[@@iterator]() -require('./_iter-define')(String, 'String', function (iterated) { - this._t = String(iterated); // target - this._i = 0; // next index -// 21.1.5.2.1 %StringIteratorPrototype%.next() -}, function () { - var O = this._t; - var index = this._i; - var point; - if (index >= O.length) return { value: undefined, done: true }; - point = $at(O, index); - this._i += point.length; - return { value: point, done: false }; -}); - -},{"./_iter-define":38,"./_string-at":68}],99:[function(require,module,exports){ -'use strict'; -// ECMAScript 6 symbols shim -var global = require('./_global'); -var has = require('./_has'); -var DESCRIPTORS = require('./_descriptors'); -var $export = require('./_export'); -var redefine = require('./_redefine'); -var META = require('./_meta').KEY; -var $fails = require('./_fails'); -var shared = require('./_shared'); -var setToStringTag = require('./_set-to-string-tag'); -var uid = require('./_uid'); -var wks = require('./_wks'); -var wksExt = require('./_wks-ext'); -var wksDefine = require('./_wks-define'); -var enumKeys = require('./_enum-keys'); -var isArray = require('./_is-array'); -var anObject = require('./_an-object'); -var isObject = require('./_is-object'); -var toObject = require('./_to-object'); -var toIObject = require('./_to-iobject'); -var toPrimitive = require('./_to-primitive'); -var createDesc = require('./_property-desc'); -var _create = require('./_object-create'); -var gOPNExt = require('./_object-gopn-ext'); -var $GOPD = require('./_object-gopd'); -var $GOPS = require('./_object-gops'); -var $DP = require('./_object-dp'); -var $keys = require('./_object-keys'); -var gOPD = $GOPD.f; -var dP = $DP.f; -var gOPN = gOPNExt.f; -var $Symbol = global.Symbol; -var $JSON = global.JSON; -var _stringify = $JSON && $JSON.stringify; -var PROTOTYPE = 'prototype'; -var HIDDEN = wks('_hidden'); -var TO_PRIMITIVE = wks('toPrimitive'); -var isEnum = {}.propertyIsEnumerable; -var SymbolRegistry = shared('symbol-registry'); -var AllSymbols = shared('symbols'); -var OPSymbols = shared('op-symbols'); -var ObjectProto = Object[PROTOTYPE]; -var USE_NATIVE = typeof $Symbol == 'function' && !!$GOPS.f; -var QObject = global.QObject; -// Don't use setters in Qt Script, https://github.com/zloirock/core-js/issues/173 -var setter = !QObject || !QObject[PROTOTYPE] || !QObject[PROTOTYPE].findChild; - -// fallback for old Android, https://code.google.com/p/v8/issues/detail?id=687 -var setSymbolDesc = DESCRIPTORS && $fails(function () { - return _create(dP({}, 'a', { - get: function () { return dP(this, 'a', { value: 7 }).a; } - })).a != 7; -}) ? function (it, key, D) { - var protoDesc = gOPD(ObjectProto, key); - if (protoDesc) delete ObjectProto[key]; - dP(it, key, D); - if (protoDesc && it !== ObjectProto) dP(ObjectProto, key, protoDesc); -} : dP; - -var wrap = function (tag) { - var sym = AllSymbols[tag] = _create($Symbol[PROTOTYPE]); - sym._k = tag; - return sym; -}; - -var isSymbol = USE_NATIVE && typeof $Symbol.iterator == 'symbol' ? function (it) { - return typeof it == 'symbol'; -} : function (it) { - return it instanceof $Symbol; -}; - -var $defineProperty = function defineProperty(it, key, D) { - if (it === ObjectProto) $defineProperty(OPSymbols, key, D); - anObject(it); - key = toPrimitive(key, true); - anObject(D); - if (has(AllSymbols, key)) { - if (!D.enumerable) { - if (!has(it, HIDDEN)) dP(it, HIDDEN, createDesc(1, {})); - it[HIDDEN][key] = true; - } else { - if (has(it, HIDDEN) && it[HIDDEN][key]) it[HIDDEN][key] = false; - D = _create(D, { enumerable: createDesc(0, false) }); - } return setSymbolDesc(it, key, D); - } return dP(it, key, D); -}; -var $defineProperties = function defineProperties(it, P) { - anObject(it); - var keys = enumKeys(P = toIObject(P)); - var i = 0; - var l = keys.length; - var key; - while (l > i) $defineProperty(it, key = keys[i++], P[key]); - return it; -}; -var $create = function create(it, P) { - return P === undefined ? _create(it) : $defineProperties(_create(it), P); -}; -var $propertyIsEnumerable = function propertyIsEnumerable(key) { - var E = isEnum.call(this, key = toPrimitive(key, true)); - if (this === ObjectProto && has(AllSymbols, key) && !has(OPSymbols, key)) return false; - return E || !has(this, key) || !has(AllSymbols, key) || has(this, HIDDEN) && this[HIDDEN][key] ? E : true; -}; -var $getOwnPropertyDescriptor = function getOwnPropertyDescriptor(it, key) { - it = toIObject(it); - key = toPrimitive(key, true); - if (it === ObjectProto && has(AllSymbols, key) && !has(OPSymbols, key)) return; - var D = gOPD(it, key); - if (D && has(AllSymbols, key) && !(has(it, HIDDEN) && it[HIDDEN][key])) D.enumerable = true; - return D; -}; -var $getOwnPropertyNames = function getOwnPropertyNames(it) { - var names = gOPN(toIObject(it)); - var result = []; - var i = 0; - var key; - while (names.length > i) { - if (!has(AllSymbols, key = names[i++]) && key != HIDDEN && key != META) result.push(key); - } return result; -}; -var $getOwnPropertySymbols = function getOwnPropertySymbols(it) { - var IS_OP = it === ObjectProto; - var names = gOPN(IS_OP ? OPSymbols : toIObject(it)); - var result = []; - var i = 0; - var key; - while (names.length > i) { - if (has(AllSymbols, key = names[i++]) && (IS_OP ? has(ObjectProto, key) : true)) result.push(AllSymbols[key]); - } return result; -}; - -// 19.4.1.1 Symbol([description]) -if (!USE_NATIVE) { - $Symbol = function Symbol() { - if (this instanceof $Symbol) throw TypeError('Symbol is not a constructor!'); - var tag = uid(arguments.length > 0 ? arguments[0] : undefined); - var $set = function (value) { - if (this === ObjectProto) $set.call(OPSymbols, value); - if (has(this, HIDDEN) && has(this[HIDDEN], tag)) this[HIDDEN][tag] = false; - setSymbolDesc(this, tag, createDesc(1, value)); - }; - if (DESCRIPTORS && setter) setSymbolDesc(ObjectProto, tag, { configurable: true, set: $set }); - return wrap(tag); - }; - redefine($Symbol[PROTOTYPE], 'toString', function toString() { - return this._k; - }); - - $GOPD.f = $getOwnPropertyDescriptor; - $DP.f = $defineProperty; - require('./_object-gopn').f = gOPNExt.f = $getOwnPropertyNames; - require('./_object-pie').f = $propertyIsEnumerable; - $GOPS.f = $getOwnPropertySymbols; - - if (DESCRIPTORS && !require('./_library')) { - redefine(ObjectProto, 'propertyIsEnumerable', $propertyIsEnumerable, true); - } - - wksExt.f = function (name) { - return wrap(wks(name)); - }; -} - -$export($export.G + $export.W + $export.F * !USE_NATIVE, { Symbol: $Symbol }); - -for (var es6Symbols = ( - // 19.4.2.2, 19.4.2.3, 19.4.2.4, 19.4.2.6, 19.4.2.8, 19.4.2.9, 19.4.2.10, 19.4.2.11, 19.4.2.12, 19.4.2.13, 19.4.2.14 - 'hasInstance,isConcatSpreadable,iterator,match,replace,search,species,split,toPrimitive,toStringTag,unscopables' -).split(','), j = 0; es6Symbols.length > j;)wks(es6Symbols[j++]); - -for (var wellKnownSymbols = $keys(wks.store), k = 0; wellKnownSymbols.length > k;) wksDefine(wellKnownSymbols[k++]); - -$export($export.S + $export.F * !USE_NATIVE, 'Symbol', { - // 19.4.2.1 Symbol.for(key) - 'for': function (key) { - return has(SymbolRegistry, key += '') - ? SymbolRegistry[key] - : SymbolRegistry[key] = $Symbol(key); - }, - // 19.4.2.5 Symbol.keyFor(sym) - keyFor: function keyFor(sym) { - if (!isSymbol(sym)) throw TypeError(sym + ' is not a symbol!'); - for (var key in SymbolRegistry) if (SymbolRegistry[key] === sym) return key; - }, - useSetter: function () { setter = true; }, - useSimple: function () { setter = false; } -}); - -$export($export.S + $export.F * !USE_NATIVE, 'Object', { - // 19.1.2.2 Object.create(O [, Properties]) - create: $create, - // 19.1.2.4 Object.defineProperty(O, P, Attributes) - defineProperty: $defineProperty, - // 19.1.2.3 Object.defineProperties(O, Properties) - defineProperties: $defineProperties, - // 19.1.2.6 Object.getOwnPropertyDescriptor(O, P) - getOwnPropertyDescriptor: $getOwnPropertyDescriptor, - // 19.1.2.7 Object.getOwnPropertyNames(O) - getOwnPropertyNames: $getOwnPropertyNames, - // 19.1.2.8 Object.getOwnPropertySymbols(O) - getOwnPropertySymbols: $getOwnPropertySymbols -}); - -// Chrome 38 and 39 `Object.getOwnPropertySymbols` fails on primitives -// https://bugs.chromium.org/p/v8/issues/detail?id=3443 -var FAILS_ON_PRIMITIVES = $fails(function () { $GOPS.f(1); }); - -$export($export.S + $export.F * FAILS_ON_PRIMITIVES, 'Object', { - getOwnPropertySymbols: function getOwnPropertySymbols(it) { - return $GOPS.f(toObject(it)); - } -}); - -// 24.3.2 JSON.stringify(value [, replacer [, space]]) -$JSON && $export($export.S + $export.F * (!USE_NATIVE || $fails(function () { - var S = $Symbol(); - // MS Edge converts symbol values to JSON as {} - // WebKit converts symbol values to JSON as null - // V8 throws on boxed symbols - return _stringify([S]) != '[null]' || _stringify({ a: S }) != '{}' || _stringify(Object(S)) != '{}'; -})), 'JSON', { - stringify: function stringify(it) { - var args = [it]; - var i = 1; - var replacer, $replacer; - while (arguments.length > i) args.push(arguments[i++]); - $replacer = replacer = args[1]; - if (!isObject(replacer) && it === undefined || isSymbol(it)) return; // IE8 returns string on undefined - if (!isArray(replacer)) replacer = function (key, value) { - if (typeof $replacer == 'function') value = $replacer.call(this, key, value); - if (!isSymbol(value)) return value; - }; - args[1] = replacer; - return _stringify.apply($JSON, args); - } -}); - -// 19.4.3.4 Symbol.prototype[@@toPrimitive](hint) -$Symbol[PROTOTYPE][TO_PRIMITIVE] || require('./_hide')($Symbol[PROTOTYPE], TO_PRIMITIVE, $Symbol[PROTOTYPE].valueOf); -// 19.4.3.5 Symbol.prototype[@@toStringTag] -setToStringTag($Symbol, 'Symbol'); -// 20.2.1.9 Math[@@toStringTag] -setToStringTag(Math, 'Math', true); -// 24.3.3 JSON[@@toStringTag] -setToStringTag(global.JSON, 'JSON', true); - -},{"./_an-object":4,"./_descriptors":15,"./_enum-keys":18,"./_export":19,"./_fails":21,"./_global":25,"./_has":26,"./_hide":27,"./_is-array":33,"./_is-object":34,"./_library":42,"./_meta":43,"./_object-create":44,"./_object-dp":45,"./_object-gopd":47,"./_object-gopn":49,"./_object-gopn-ext":48,"./_object-gops":50,"./_object-keys":53,"./_object-pie":54,"./_property-desc":57,"./_redefine":58,"./_set-to-string-tag":63,"./_shared":65,"./_to-iobject":74,"./_to-object":76,"./_to-primitive":77,"./_uid":78,"./_wks":81,"./_wks-define":79,"./_wks-ext":80}],100:[function(require,module,exports){ -'use strict'; -// https://github.com/tc39/Array.prototype.includes -var $export = require('./_export'); -var $includes = require('./_array-includes')(true); - -$export($export.P, 'Array', { - includes: function includes(el /* , fromIndex = 0 */) { - return $includes(this, el, arguments.length > 1 ? arguments[1] : undefined); - } -}); - -require('./_add-to-unscopables')('includes'); - -},{"./_add-to-unscopables":2,"./_array-includes":5,"./_export":19}],101:[function(require,module,exports){ -// https://github.com/tc39/proposal-object-getownpropertydescriptors -var $export = require('./_export'); -var ownKeys = require('./_own-keys'); -var toIObject = require('./_to-iobject'); -var gOPD = require('./_object-gopd'); -var createProperty = require('./_create-property'); - -$export($export.S, 'Object', { - getOwnPropertyDescriptors: function getOwnPropertyDescriptors(object) { - var O = toIObject(object); - var getDesc = gOPD.f; - var keys = ownKeys(O); - var result = {}; - var i = 0; - var key, desc; - while (keys.length > i) { - desc = getDesc(O, key = keys[i++]); - if (desc !== undefined) createProperty(result, key, desc); - } - return result; - } -}); - -},{"./_create-property":12,"./_export":19,"./_object-gopd":47,"./_own-keys":56,"./_to-iobject":74}],102:[function(require,module,exports){ -var $iterators = require('./es6.array.iterator'); -var getKeys = require('./_object-keys'); -var redefine = require('./_redefine'); -var global = require('./_global'); -var hide = require('./_hide'); -var Iterators = require('./_iterators'); -var wks = require('./_wks'); -var ITERATOR = wks('iterator'); -var TO_STRING_TAG = wks('toStringTag'); -var ArrayValues = Iterators.Array; - -var DOMIterables = { - CSSRuleList: true, // TODO: Not spec compliant, should be false. - CSSStyleDeclaration: false, - CSSValueList: false, - ClientRectList: false, - DOMRectList: false, - DOMStringList: false, - DOMTokenList: true, - DataTransferItemList: false, - FileList: false, - HTMLAllCollection: false, - HTMLCollection: false, - HTMLFormElement: false, - HTMLSelectElement: false, - MediaList: true, // TODO: Not spec compliant, should be false. - MimeTypeArray: false, - NamedNodeMap: false, - NodeList: true, - PaintRequestList: false, - Plugin: false, - PluginArray: false, - SVGLengthList: false, - SVGNumberList: false, - SVGPathSegList: false, - SVGPointList: false, - SVGStringList: false, - SVGTransformList: false, - SourceBufferList: false, - StyleSheetList: true, // TODO: Not spec compliant, should be false. - TextTrackCueList: false, - TextTrackList: false, - TouchList: false -}; - -for (var collections = getKeys(DOMIterables), i = 0; i < collections.length; i++) { - var NAME = collections[i]; - var explicit = DOMIterables[NAME]; - var Collection = global[NAME]; - var proto = Collection && Collection.prototype; - var key; - if (proto) { - if (!proto[ITERATOR]) hide(proto, ITERATOR, ArrayValues); - if (!proto[TO_STRING_TAG]) hide(proto, TO_STRING_TAG, NAME); - Iterators[NAME] = ArrayValues; - if (explicit) for (key in $iterators) if (!proto[key]) redefine(proto, key, $iterators[key], true); - } -} - -},{"./_global":25,"./_hide":27,"./_iterators":41,"./_object-keys":53,"./_redefine":58,"./_wks":81,"./es6.array.iterator":85}],103:[function(require,module,exports){ -"use strict"; - -require("core-js/modules/es6.symbol.js"); -require("core-js/modules/es6.number.constructor.js"); -require("core-js/modules/es6.string.iterator.js"); -require("core-js/modules/es6.object.to-string.js"); -require("core-js/modules/es6.array.iterator.js"); -require("core-js/modules/web.dom.iterable.js"); -function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } -function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } } -function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } -function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); } -function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } -var _require = require('./protocol'), - Parser = _require.Parser, - PROTOCOL_6 = _require.PROTOCOL_6, - PROTOCOL_7 = _require.PROTOCOL_7; -var VERSION = "4.0.2"; -var Connector = /*#__PURE__*/function () { - function Connector(options, WebSocket, Timer, handlers) { - var _this = this; - _classCallCheck(this, Connector); - this.options = options; - this.WebSocket = WebSocket; - this.Timer = Timer; - this.handlers = handlers; - var path = this.options.path ? "".concat(this.options.path) : 'livereload'; - var port = this.options.port ? ":".concat(this.options.port) : ''; - this._uri = "ws".concat(this.options.https ? 's' : '', "://").concat(this.options.host).concat(port, "/").concat(path); - this._nextDelay = this.options.mindelay; - this._connectionDesired = false; - this.protocol = 0; - this.protocolParser = new Parser({ - connected: function connected(protocol) { - _this.protocol = protocol; - _this._handshakeTimeout.stop(); - _this._nextDelay = _this.options.mindelay; - _this._disconnectionReason = 'broken'; - return _this.handlers.connected(_this.protocol); - }, - error: function error(e) { - _this.handlers.error(e); - return _this._closeOnError(); - }, - message: function message(_message) { - return _this.handlers.message(_message); - } - }); - this._handshakeTimeout = new this.Timer(function () { - if (!_this._isSocketConnected()) { - return; - } - _this._disconnectionReason = 'handshake-timeout'; - return _this.socket.close(); - }); - this._reconnectTimer = new this.Timer(function () { - if (!_this._connectionDesired) { - // shouldn't hit this, but just in case - return; - } - return _this.connect(); - }); - this.connect(); - } - _createClass(Connector, [{ - key: "_isSocketConnected", - value: function _isSocketConnected() { - return this.socket && this.socket.readyState === this.WebSocket.OPEN; - } - }, { - key: "connect", - value: function connect() { - var _this2 = this; - this._connectionDesired = true; - if (this._isSocketConnected()) { - return; - } - - // prepare for a new connection - this._reconnectTimer.stop(); - this._disconnectionReason = 'cannot-connect'; - this.protocolParser.reset(); - this.handlers.connecting(); - this.socket = new this.WebSocket(this._uri); - this.socket.onopen = function (e) { - return _this2._onopen(e); - }; - this.socket.onclose = function (e) { - return _this2._onclose(e); - }; - this.socket.onmessage = function (e) { - return _this2._onmessage(e); - }; - this.socket.onerror = function (e) { - return _this2._onerror(e); - }; - } - }, { - key: "disconnect", - value: function disconnect() { - this._connectionDesired = false; - this._reconnectTimer.stop(); // in case it was running - - if (!this._isSocketConnected()) { - return; - } - this._disconnectionReason = 'manual'; - return this.socket.close(); - } - }, { - key: "_scheduleReconnection", - value: function _scheduleReconnection() { - if (!this._connectionDesired) { - // don't reconnect after manual disconnection - return; - } - if (!this._reconnectTimer.running) { - this._reconnectTimer.start(this._nextDelay); - this._nextDelay = Math.min(this.options.maxdelay, this._nextDelay * 2); - } - } - }, { - key: "sendCommand", - value: function sendCommand(command) { - if (!this.protocol) { - return; - } - return this._sendCommand(command); - } - }, { - key: "_sendCommand", - value: function _sendCommand(command) { - return this.socket.send(JSON.stringify(command)); - } - }, { - key: "_closeOnError", - value: function _closeOnError() { - this._handshakeTimeout.stop(); - this._disconnectionReason = 'error'; - return this.socket.close(); - } - }, { - key: "_onopen", - value: function _onopen(e) { - this.handlers.socketConnected(); - this._disconnectionReason = 'handshake-failed'; - - // start handshake - var hello = { - command: 'hello', - protocols: [PROTOCOL_6, PROTOCOL_7] - }; - hello.ver = VERSION; - if (this.options.ext) { - hello.ext = this.options.ext; - } - if (this.options.extver) { - hello.extver = this.options.extver; - } - if (this.options.snipver) { - hello.snipver = this.options.snipver; - } - this._sendCommand(hello); - return this._handshakeTimeout.start(this.options.handshake_timeout); - } - }, { - key: "_onclose", - value: function _onclose(e) { - this.protocol = 0; - this.handlers.disconnected(this._disconnectionReason, this._nextDelay); - return this._scheduleReconnection(); - } - }, { - key: "_onerror", - value: function _onerror(e) {} - }, { - key: "_onmessage", - value: function _onmessage(e) { - return this.protocolParser.process(e.data); - } - }]); - return Connector; -}(); -; -exports.Connector = Connector; - -},{"./protocol":108,"core-js/modules/es6.array.iterator.js":85,"core-js/modules/es6.number.constructor.js":88,"core-js/modules/es6.object.to-string.js":91,"core-js/modules/es6.string.iterator.js":98,"core-js/modules/es6.symbol.js":99,"core-js/modules/web.dom.iterable.js":102}],104:[function(require,module,exports){ -"use strict"; - -var CustomEvents = { - bind: function bind(element, eventName, handler) { - if (element.addEventListener) { - return element.addEventListener(eventName, handler, false); - } - if (element.attachEvent) { - element[eventName] = 1; - return element.attachEvent('onpropertychange', function (event) { - if (event.propertyName === eventName) { - return handler(); - } - }); - } - throw new Error("Attempt to attach custom event ".concat(eventName, " to something which isn't a DOMElement")); - }, - fire: function fire(element, eventName) { - if (element.addEventListener) { - var event = document.createEvent('HTMLEvents'); - event.initEvent(eventName, true, true); - return document.dispatchEvent(event); - } else if (element.attachEvent) { - if (element[eventName]) { - return element[eventName]++; - } - } else { - throw new Error("Attempt to fire custom event ".concat(eventName, " on something which isn't a DOMElement")); - } - } -}; -exports.bind = CustomEvents.bind; -exports.fire = CustomEvents.fire; - -},{}],105:[function(require,module,exports){ -"use strict"; - -require("core-js/modules/es6.regexp.match.js"); -require("core-js/modules/es6.symbol.js"); -require("core-js/modules/es6.array.from.js"); -require("core-js/modules/es6.string.iterator.js"); -require("core-js/modules/es6.object.to-string.js"); -require("core-js/modules/es6.array.iterator.js"); -require("core-js/modules/web.dom.iterable.js"); -require("core-js/modules/es6.number.constructor.js"); -function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } -function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } } -function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } -function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); } -function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } -var LessPlugin = /*#__PURE__*/function () { - function LessPlugin(window, host) { - _classCallCheck(this, LessPlugin); - this.window = window; - this.host = host; - } - _createClass(LessPlugin, [{ - key: "reload", - value: function reload(path, options) { - if (this.window.less && this.window.less.refresh) { - if (path.match(/\.less$/i)) { - return this.reloadLess(path); - } - if (options.originalPath.match(/\.less$/i)) { - return this.reloadLess(options.originalPath); - } - } - return false; - } - }, { - key: "reloadLess", - value: function reloadLess(path) { - var link; - var links = function () { - var result = []; - for (var _i = 0, _Array$from = Array.from(document.getElementsByTagName('link')); _i < _Array$from.length; _i++) { - link = _Array$from[_i]; - if (link.href && link.rel.match(/^stylesheet\/less$/i) || link.rel.match(/stylesheet/i) && link.type.match(/^text\/(x-)?less$/i)) { - result.push(link); - } - } - return result; - }(); - if (links.length === 0) { - return false; - } - for (var _i2 = 0, _Array$from2 = Array.from(links); _i2 < _Array$from2.length; _i2++) { - link = _Array$from2[_i2]; - link.href = this.host.generateCacheBustUrl(link.href); - } - this.host.console.log('LiveReload is asking LESS to recompile all stylesheets'); - this.window.less.refresh(true); - return true; - } - }, { - key: "analyze", - value: function analyze() { - return { - disable: !!(this.window.less && this.window.less.refresh) - }; - } - }]); - return LessPlugin; -}(); -; -LessPlugin.identifier = 'less'; -LessPlugin.version = '1.0'; -module.exports = LessPlugin; - -},{"core-js/modules/es6.array.from.js":84,"core-js/modules/es6.array.iterator.js":85,"core-js/modules/es6.number.constructor.js":88,"core-js/modules/es6.object.to-string.js":91,"core-js/modules/es6.regexp.match.js":94,"core-js/modules/es6.string.iterator.js":98,"core-js/modules/es6.symbol.js":99,"core-js/modules/web.dom.iterable.js":102}],106:[function(require,module,exports){ -"use strict"; - -require("core-js/modules/es6.symbol.js"); -require("core-js/modules/es6.number.constructor.js"); -require("core-js/modules/es6.array.slice.js"); -require("core-js/modules/es6.object.to-string.js"); -require("core-js/modules/es6.array.from.js"); -require("core-js/modules/es6.string.iterator.js"); -require("core-js/modules/es6.array.iterator.js"); -require("core-js/modules/web.dom.iterable.js"); -function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } -require("core-js/modules/es6.regexp.match.js"); -require("core-js/modules/es6.object.keys.js"); -function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; } -function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } -function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } -function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } } -function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } -function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); } -function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } -/* global alert */ -var _require = require('./connector'), - Connector = _require.Connector; -var _require2 = require('./timer'), - Timer = _require2.Timer; -var _require3 = require('./options'), - Options = _require3.Options; -var _require4 = require('./reloader'), - Reloader = _require4.Reloader; -var _require5 = require('./protocol'), - ProtocolError = _require5.ProtocolError; -var LiveReload = /*#__PURE__*/function () { - function LiveReload(window) { - var _this = this; - _classCallCheck(this, LiveReload); - this.window = window; - this.listeners = {}; - this.plugins = []; - this.pluginIdentifiers = {}; - - // i can haz console? - this.console = this.window.console && this.window.console.log && this.window.console.error ? this.window.location.href.match(/LR-verbose/) ? this.window.console : { - log: function log() {}, - error: this.window.console.error.bind(this.window.console) - } : { - log: function log() {}, - error: function error() {} - }; - - // i can haz sockets? - if (!(this.WebSocket = this.window.WebSocket || this.window.MozWebSocket)) { - this.console.error('LiveReload disabled because the browser does not seem to support web sockets'); - return; - } - - // i can haz options? - if ('LiveReloadOptions' in window) { - this.options = new Options(); - for (var _i = 0, _Object$keys = Object.keys(window.LiveReloadOptions || {}); _i < _Object$keys.length; _i++) { - var k = _Object$keys[_i]; - var v = window.LiveReloadOptions[k]; - this.options.set(k, v); - } - } else { - this.options = Options.extract(this.window.document); - if (!this.options) { - this.console.error('LiveReload disabled because it could not find its own - - -## More - -This is a word. - -This is a word. - -This is a word. - -This is a word. - -This is a word. - - --- layouts/_default/single.html -- -{{ .Content }} -` - - b := hugolib.Test(t, files, hugolib.TestOptWarn()) - - b.AssertFileContent("public/p1/index.html", - "! ", - "! ", - "! ", - "! script", - ) - b.AssertLogContains("! WARN") - - b = hugolib.Test(t, strings.ReplaceAll(files, "markup.goldmark.renderer.unsafe = false", "markup.goldmark.renderer.unsafe = true"), hugolib.TestOptWarn()) - b.AssertFileContent("public/p1/index.html", - "! ", - "", - "", - ) - b.AssertLogContains("! WARN") -} diff --git a/markup/goldmark/hugocontext/hugocontext.go b/markup/goldmark/hugocontext/hugocontext.go deleted file mode 100644 index 7a556083c..000000000 --- a/markup/goldmark/hugocontext/hugocontext.go +++ /dev/null @@ -1,317 +0,0 @@ -// 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 hugocontext - -import ( - "bytes" - "fmt" - "regexp" - "strconv" - - "github.com/gohugoio/hugo/bufferpool" - "github.com/gohugoio/hugo/common/constants" - "github.com/gohugoio/hugo/common/loggers" - "github.com/gohugoio/hugo/markup/goldmark/internal/render" - "github.com/yuin/goldmark" - "github.com/yuin/goldmark/ast" - "github.com/yuin/goldmark/parser" - "github.com/yuin/goldmark/renderer" - "github.com/yuin/goldmark/renderer/html" - "github.com/yuin/goldmark/text" - "github.com/yuin/goldmark/util" -) - -func New(logger loggers.Logger) goldmark.Extender { - return &hugoContextExtension{logger: logger} -} - -// Wrap wraps the given byte slice in a Hugo context that used to determine the correct Page -// in .RenderShortcodes. -func Wrap(b []byte, pid uint64) string { - buf := bufferpool.GetBuffer() - defer bufferpool.PutBuffer(buf) - buf.Write(hugoCtxPrefix) - buf.WriteString(" pid=") - buf.WriteString(strconv.FormatUint(pid, 10)) - buf.Write(hugoCtxEndDelim) - buf.WriteByte('\n') - buf.Write(b) - // To make sure that we're able to parse it, make sure it ends with a newline. - if len(b) > 0 && b[len(b)-1] != '\n' { - buf.WriteByte('\n') - } - buf.Write(hugoCtxPrefix) - buf.Write(hugoCtxClosingDelim) - buf.WriteByte('\n') - return buf.String() -} - -var kindHugoContext = ast.NewNodeKind("HugoContext") - -// HugoContext is a node that represents a Hugo context. -type HugoContext struct { - ast.BaseInline - - Closing bool - - // Internal page ID. Not persisted. - Pid uint64 -} - -// Dump implements Node.Dump. -func (n *HugoContext) Dump(source []byte, level int) { - m := map[string]string{} - m["Pid"] = fmt.Sprintf("%v", n.Pid) - ast.DumpHelper(n, source, level, m, nil) -} - -func (n *HugoContext) parseAttrs(attrBytes []byte) { - keyPairs := bytes.Split(attrBytes, []byte(" ")) - for _, keyPair := range keyPairs { - kv := bytes.Split(keyPair, []byte("=")) - if len(kv) != 2 { - continue - } - key := string(kv[0]) - val := string(kv[1]) - switch key { - case "pid": - pid, _ := strconv.ParseUint(val, 10, 64) - n.Pid = pid - } - } -} - -func (h *HugoContext) Kind() ast.NodeKind { - return kindHugoContext -} - -var ( - hugoCtxPrefix = []byte("{{__hugo_ctx") - hugoCtxEndDelim = []byte("}}") - hugoCtxClosingDelim = []byte("/}}") - hugoCtxRe = regexp.MustCompile(`{{__hugo_ctx( pid=\d+)?/?}}\n?`) -) - -var _ parser.InlineParser = (*hugoContextParser)(nil) - -type hugoContextParser struct{} - -func (a *hugoContextParser) Trigger() []byte { - return []byte{'{'} -} - -func (s *hugoContextParser) Parse(parent ast.Node, reader text.Reader, pc parser.Context) ast.Node { - line, _ := reader.PeekLine() - if !bytes.HasPrefix(line, hugoCtxPrefix) { - return nil - } - end := bytes.Index(line, hugoCtxEndDelim) - if end == -1 { - return nil - } - - reader.Advance(end + len(hugoCtxEndDelim) + 1) // +1 for the newline - - if line[end-1] == '/' { - return &HugoContext{Closing: true} - } - - attrBytes := line[len(hugoCtxPrefix)+1 : end] - h := &HugoContext{} - h.parseAttrs(attrBytes) - return h -} - -type hugoContextRenderer struct { - logger loggers.Logger - html.Config -} - -func (r *hugoContextRenderer) SetOption(name renderer.OptionName, value any) { - r.Config.SetOption(name, value) -} - -func (r *hugoContextRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { - reg.Register(kindHugoContext, r.handleHugoContext) - reg.Register(ast.KindRawHTML, r.renderRawHTML) - reg.Register(ast.KindHTMLBlock, r.renderHTMLBlock) -} - -func (r *hugoContextRenderer) stripHugoCtx(b []byte) ([]byte, bool) { - if !bytes.Contains(b, hugoCtxPrefix) { - return b, false - } - return hugoCtxRe.ReplaceAll(b, nil), true -} - -func (r *hugoContextRenderer) logRawHTMLEmittedWarn(w util.BufWriter) { - r.logger.Warnidf(constants.WarnGoldmarkRawHTML, "Raw HTML omitted while rendering %q; see https://gohugo.io/getting-started/configuration-markup/#rendererunsafe", r.getPage(w)) -} - -func (r *hugoContextRenderer) getPage(w util.BufWriter) any { - var p any - ctx, ok := w.(*render.Context) - if ok { - p, _ = render.GetPageAndPageInner(ctx) - } - return p -} - -func (r *hugoContextRenderer) isHTMLComment(b []byte) bool { - return len(b) > 4 && b[0] == '<' && b[1] == '!' && b[2] == '-' && b[3] == '-' -} - -// HTML rendering based on Goldmark implementation. -func (r *hugoContextRenderer) renderHTMLBlock( - w util.BufWriter, source []byte, node ast.Node, entering bool, -) (ast.WalkStatus, error) { - n := node.(*ast.HTMLBlock) - - if entering { - if r.Unsafe { - l := n.Lines().Len() - for i := range l { - line := n.Lines().At(i) - linev := line.Value(source) - var stripped bool - linev, stripped = r.stripHugoCtx(linev) - if stripped { - r.logger.Warnidf(constants.WarnRenderShortcodesInHTML, ".RenderShortcodes detected inside HTML block in %q; this may not be what you intended, see https://gohugo.io/methods/page/rendershortcodes/#limitations", r.getPage(w)) - } - r.Writer.SecureWrite(w, linev) - } - } else { - l := n.Lines().At(0) - v := l.Value(source) - if !r.isHTMLComment(v) { - r.logRawHTMLEmittedWarn(w) - _, _ = w.WriteString("\n") - } - } - } else { - if n.HasClosure() { - if r.Unsafe { - closure := n.ClosureLine - r.Writer.SecureWrite(w, closure.Value(source)) - } else { - l := n.Lines().At(0) - v := l.Value(source) - if !r.isHTMLComment(v) { - _, _ = w.WriteString("\n") - } - } - } - } - return ast.WalkContinue, nil -} - -func (r *hugoContextRenderer) renderRawHTML( - w util.BufWriter, source []byte, node ast.Node, entering bool, -) (ast.WalkStatus, error) { - if !entering { - return ast.WalkSkipChildren, nil - } - n := node.(*ast.RawHTML) - l := n.Segments.Len() - if r.Unsafe { - for i := range l { - segment := n.Segments.At(i) - _, _ = w.Write(segment.Value(source)) - } - return ast.WalkSkipChildren, nil - } - segment := n.Segments.At(0) - v := segment.Value(source) - if !r.isHTMLComment(v) { - r.logRawHTMLEmittedWarn(w) - _, _ = w.WriteString("") - } - return ast.WalkSkipChildren, nil -} - -func (r *hugoContextRenderer) handleHugoContext(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { - if !entering { - return ast.WalkContinue, nil - } - - hctx := node.(*HugoContext) - ctx, ok := w.(*render.Context) - if !ok { - return ast.WalkContinue, nil - } - if hctx.Closing { - _ = ctx.PopPid() - } else { - ctx.PushPid(hctx.Pid) - } - return ast.WalkContinue, nil -} - -type hugoContextTransformer struct{} - -var _ parser.ASTTransformer = (*hugoContextTransformer)(nil) - -func (a *hugoContextTransformer) Transform(n *ast.Document, reader text.Reader, pc parser.Context) { - ast.Walk(n, func(n ast.Node, entering bool) (ast.WalkStatus, error) { - s := ast.WalkContinue - if !entering || n.Kind() != kindHugoContext { - return s, nil - } - - if p, ok := n.Parent().(*ast.Paragraph); ok { - if p.ChildCount() == 1 { - // Avoid empty paragraphs. - p.Parent().ReplaceChild(p.Parent(), p, n) - } else { - if t, ok := n.PreviousSibling().(*ast.Text); ok { - // Remove the newline produced by the Hugo context markers. - if t.SoftLineBreak() { - if t.Segment.Len() == 0 { - p.RemoveChild(p, t) - } else { - t.SetSoftLineBreak(false) - } - } - } - } - } - - return s, nil - }) -} - -type hugoContextExtension struct { - logger loggers.Logger -} - -func (a *hugoContextExtension) Extend(m goldmark.Markdown) { - m.Parser().AddOptions( - parser.WithInlineParsers( - util.Prioritized(&hugoContextParser{}, 50), - ), - parser.WithASTTransformers(util.Prioritized(&hugoContextTransformer{}, 10)), - ) - - m.Renderer().AddOptions( - renderer.WithNodeRenderers( - util.Prioritized(&hugoContextRenderer{ - logger: a.logger, - Config: html.Config{ - Writer: html.DefaultWriter, - }, - }, 50), - ), - ) -} diff --git a/markup/goldmark/hugocontext/hugocontext_test.go b/markup/goldmark/hugocontext/hugocontext_test.go deleted file mode 100644 index 62769f4d0..000000000 --- a/markup/goldmark/hugocontext/hugocontext_test.go +++ /dev/null @@ -1,34 +0,0 @@ -// 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 hugocontext - -import ( - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestWrap(t *testing.T) { - c := qt.New(t) - - b := []byte("test") - - c.Assert(Wrap(b, 42), qt.Equals, "{{__hugo_ctx pid=42}}\ntest\n{{__hugo_ctx/}}\n") -} - -func BenchmarkWrap(b *testing.B) { - for i := 0; i < b.N; i++ { - Wrap([]byte("test"), 42) - } -} diff --git a/markup/goldmark/images/images_integration_test.go b/markup/goldmark/images/images_integration_test.go deleted file mode 100644 index 387287e7a..000000000 --- a/markup/goldmark/images/images_integration_test.go +++ /dev/null @@ -1,88 +0,0 @@ -package images_test - -import ( - "strings" - "testing" - - "github.com/gohugoio/hugo/hugolib" -) - -func TestDisableWrapStandAloneImageWithinParagraph(t *testing.T) { - t.Parallel() - - filesTemplate := ` --- config.toml -- -[markup.goldmark.renderer] - unsafe = false -[markup.goldmark.parser] -wrapStandAloneImageWithinParagraph = CONFIG_VALUE -[markup.goldmark.parser.attribute] - block = true - title = true --- content/p1.md -- ---- -title: "p1" ---- - -This is an inline image: ![Inline Image](/inline.jpg). Some more text. - -![Block Image](/block.jpg) -{.b} - - --- layouts/_default/single.html -- -{{ .Content }} -` - - t.Run("With Hook, no wrap", func(t *testing.T) { - files := strings.ReplaceAll(filesTemplate, "CONFIG_VALUE", "false") - files = files + `-- layouts/_default/_markup/render-image.html -- -{{ if .IsBlock }} -
    - {{ .Text }}|{{ .Ordinal }} -
    -{{ else }} - {{ .Text }}|{{ .Ordinal }} -{{ end }} -` - b := hugolib.Test(t, files) - - b.AssertFileContent("public/p1/index.html", - "This is an inline image: \n\t\"Inline\n. Some more text.

    ", - "
    \n\t\"Block", - ) - }) - - t.Run("With Hook, wrap", func(t *testing.T) { - files := strings.ReplaceAll(filesTemplate, "CONFIG_VALUE", "true") - files = files + `-- layouts/_default/_markup/render-image.html -- -{{ if .IsBlock }} -
    - {{ .Text }} -
    -{{ else }} - {{ .Text }} -{{ end }} -` - b := hugolib.Test(t, files) - - b.AssertFileContent("public/p1/index.html", - "This is an inline image: \n\t\"Inline\n. Some more text.

    ", - "

    \n\t\"Block\n

    ", - ) - }) - - t.Run("No Hook, no wrap", func(t *testing.T) { - files := strings.ReplaceAll(filesTemplate, "CONFIG_VALUE", "false") - b := hugolib.Test(t, files) - - b.AssertFileContent("public/p1/index.html", "

    This is an inline image: \"Inline. Some more text.

    \n\"Block") - }) - - t.Run("No Hook, wrap", func(t *testing.T) { - files := strings.ReplaceAll(filesTemplate, "CONFIG_VALUE", "true") - b := hugolib.Test(t, files) - - b.AssertFileContent("public/p1/index.html", "

    \"Block

    ") - }) -} diff --git a/markup/goldmark/images/transform.go b/markup/goldmark/images/transform.go deleted file mode 100644 index 2e97a6791..000000000 --- a/markup/goldmark/images/transform.go +++ /dev/null @@ -1,75 +0,0 @@ -package images - -import ( - "github.com/yuin/goldmark" - "github.com/yuin/goldmark/ast" - "github.com/yuin/goldmark/parser" - "github.com/yuin/goldmark/text" - "github.com/yuin/goldmark/util" -) - -type ( - imagesExtension struct { - wrapStandAloneImageWithinParagraph bool - } -) - -const ( - // Used to signal to the rendering step that an image is used in a block context. - // Dont's change this; the prefix must match the internalAttrPrefix in the root goldmark package. - AttrIsBlock = "_h__isBlock" - AttrOrdinal = "_h__ordinal" -) - -func New(wrapStandAloneImageWithinParagraph bool) goldmark.Extender { - return &imagesExtension{wrapStandAloneImageWithinParagraph: wrapStandAloneImageWithinParagraph} -} - -func (e *imagesExtension) Extend(m goldmark.Markdown) { - m.Parser().AddOptions( - parser.WithASTTransformers( - util.Prioritized(&Transformer{wrapStandAloneImageWithinParagraph: e.wrapStandAloneImageWithinParagraph}, 300), - ), - ) -} - -type Transformer struct { - wrapStandAloneImageWithinParagraph bool -} - -// Transform transforms the provided Markdown AST. -func (t *Transformer) Transform(doc *ast.Document, reader text.Reader, pctx parser.Context) { - var ordinal int - ast.Walk(doc, func(node ast.Node, enter bool) (ast.WalkStatus, error) { - if !enter { - return ast.WalkContinue, nil - } - - if n, ok := node.(*ast.Image); ok { - parent := n.Parent() - n.SetAttributeString(AttrOrdinal, ordinal) - ordinal++ - - if !t.wrapStandAloneImageWithinParagraph { - isBlock := parent.ChildCount() == 1 - if isBlock { - n.SetAttributeString(AttrIsBlock, true) - } - - if isBlock && parent.Kind() == ast.KindParagraph { - for _, attr := range parent.Attributes() { - // Transfer any attribute set down to the image. - // Image elements does not support attributes on its own, - // so it's safe to just set without checking first. - n.SetAttribute(attr.Name, attr.Value) - } - grandParent := parent.Parent() - grandParent.ReplaceChild(grandParent, parent, n) - } - } - - } - - return ast.WalkContinue, nil - }) -} diff --git a/markup/goldmark/internal/extensions/attributes/attributes.go b/markup/goldmark/internal/extensions/attributes/attributes.go deleted file mode 100644 index 50ccb2ed4..000000000 --- a/markup/goldmark/internal/extensions/attributes/attributes.go +++ /dev/null @@ -1,204 +0,0 @@ -package attributes - -import ( - "strings" - - "github.com/gohugoio/hugo/markup/goldmark/goldmark_config" - "github.com/gohugoio/hugo/markup/goldmark/internal/render" - "github.com/yuin/goldmark" - "github.com/yuin/goldmark/ast" - east "github.com/yuin/goldmark/extension/ast" - "github.com/yuin/goldmark/parser" - "github.com/yuin/goldmark/text" - "github.com/yuin/goldmark/util" -) - -// This extension is based on/inspired by https://github.com/mdigger/goldmark-attributes -// MIT License -// Copyright (c) 2019 Dmitry Sedykh - -var ( - kindAttributesBlock = ast.NewNodeKind("AttributesBlock") - attrNameID = []byte("id") - - defaultParser = new(attrParser) -) - -func New(cfg goldmark_config.Parser) goldmark.Extender { - return &attrExtension{cfg: cfg} -} - -type attrExtension struct { - cfg goldmark_config.Parser -} - -func (a *attrExtension) Extend(m goldmark.Markdown) { - if a.cfg.Attribute.Block { - m.Parser().AddOptions( - parser.WithBlockParsers( - util.Prioritized(defaultParser, 100)), - ) - } - m.Parser().AddOptions( - parser.WithASTTransformers( - util.Prioritized(&transformer{cfg: a.cfg}, 100), - ), - ) -} - -type attrParser struct{} - -func (a *attrParser) CanAcceptIndentedLine() bool { - return false -} - -func (a *attrParser) CanInterruptParagraph() bool { - return true -} - -func (a *attrParser) Close(node ast.Node, reader text.Reader, pc parser.Context) { -} - -func (a *attrParser) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State { - return parser.Close -} - -func (a *attrParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) { - if attrs, ok := parser.ParseAttributes(reader); ok { - // add attributes - node := &attributesBlock{ - BaseBlock: ast.BaseBlock{}, - } - for _, attr := range attrs { - node.SetAttribute(attr.Name, attr.Value) - } - return node, parser.NoChildren - } - return nil, parser.RequireParagraph -} - -func (a *attrParser) Trigger() []byte { - return []byte{'{'} -} - -type attributesBlock struct { - ast.BaseBlock -} - -func (a *attributesBlock) Dump(source []byte, level int) { - attrs := a.Attributes() - list := make(map[string]string, len(attrs)) - for _, attr := range attrs { - var ( - name = util.BytesToReadOnlyString(attr.Name) - value = util.BytesToReadOnlyString(util.EscapeHTML(attr.Value.([]byte))) - ) - list[name] = value - } - ast.DumpHelper(a, source, level, list, nil) -} - -func (a *attributesBlock) Kind() ast.NodeKind { - return kindAttributesBlock -} - -type transformer struct { - cfg goldmark_config.Parser -} - -func (a *transformer) isFragmentNode(n ast.Node) bool { - switch n.Kind() { - case east.KindDefinitionTerm, ast.KindHeading: - return true - default: - return false - } -} - -func (a *transformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) { - var attributes []ast.Node - var solitaryAttributeNodes []ast.Node - if a.cfg.Attribute.Block { - attributes = make([]ast.Node, 0, 100) - } - ast.Walk(node, func(node ast.Node, entering bool) (ast.WalkStatus, error) { - if !entering { - return ast.WalkContinue, nil - } - - if a.isFragmentNode(node) { - if id, found := node.Attribute(attrNameID); !found { - a.generateAutoID(node, reader, pc) - } else { - pc.IDs().Put(id.([]byte)) - } - } - - if a.cfg.Attribute.Block && node.Kind() == kindAttributesBlock { - // Attributes for fenced code blocks are handled in their own extension, - // but note that we currently only support code block attributes when - // CodeFences=true. - if node.PreviousSibling() != nil && node.PreviousSibling().Kind() != ast.KindFencedCodeBlock && !node.HasBlankPreviousLines() { - attributes = append(attributes, node) - return ast.WalkSkipChildren, nil - } else { - solitaryAttributeNodes = append(solitaryAttributeNodes, node) - } - } - - return ast.WalkContinue, nil - }) - - for _, attr := range attributes { - if prev := attr.PreviousSibling(); prev != nil && - prev.Type() == ast.TypeBlock { - for _, attr := range attr.Attributes() { - if _, found := prev.Attribute(attr.Name); !found { - prev.SetAttribute(attr.Name, attr.Value) - } - } - } - // remove attributes node - attr.Parent().RemoveChild(attr.Parent(), attr) - } - - // Remove any solitary attribute nodes. - for _, n := range solitaryAttributeNodes { - n.Parent().RemoveChild(n.Parent(), n) - } -} - -func (a *transformer) generateAutoID(n ast.Node, reader text.Reader, pc parser.Context) { - var text []byte - switch n := n.(type) { - case *ast.Heading: - if a.cfg.AutoHeadingID { - text = textHeadingID(n, reader) - } - case *east.DefinitionTerm: - if a.cfg.AutoDefinitionTermID { - text = []byte(render.TextPlain(n, reader.Source())) - } - } - - if len(text) > 0 { - headingID := pc.IDs().Generate(text, n.Kind()) - n.SetAttribute(attrNameID, headingID) - } -} - -// Markdown settext headers can have multiple lines, use the last line for the ID. -func textHeadingID(n *ast.Heading, reader text.Reader) []byte { - text := render.TextPlain(n, reader.Source()) - if n.Lines().Len() > 1 { - - // For multiline headings, Goldmark's extension for headings returns the last line. - // We have a slightly different approach, but in most cases the end result should be the same. - // Instead of looking at the text segments in Lines (see #13405 for issues with that), - // we split the text above and use the last line. - parts := strings.Split(text, "\n") - text = parts[len(parts)-1] - } - - return []byte(text) -} diff --git a/markup/goldmark/internal/extensions/attributes/attributes_integration_test.go b/markup/goldmark/internal/extensions/attributes/attributes_integration_test.go deleted file mode 100644 index e56c52550..000000000 --- a/markup/goldmark/internal/extensions/attributes/attributes_integration_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package attributes_test - -import ( - "testing" - - "github.com/gohugoio/hugo/hugolib" -) - -func TestDescriptionListAutoID(t *testing.T) { - t.Parallel() - - files := ` --- hugo.toml -- -[markup.goldmark.parser] -autoHeadingID = true -autoDefinitionTermID = true -autoIDType = 'github-ascii' --- content/p1.md -- ---- -title: "Title" ---- - -## Title with id set {#title-with-id} - -## Title with id set duplicate {#title-with-id} - -## My Title - -Base Name -: Base name of the file. - -Base Name -: Duplicate term name. - -My Title -: Term with same name as title. - -Foo@Bar -: The foo bar. - -foo [something](/a/b/) bar -: A foo bar. - -良善天父 -: The good father. - -Ā ā Ă ă Ą ą Ć ć Ĉ ĉ Ċ ċ Č č Ď -: Testing accents. - -Multiline set text header -Second line ---------------- - -## Example [hyperlink](https://example.com/) in a header - --- layouts/_default/single.html -- -{{ .Content }}|Identifiers: {{ .Fragments.Identifiers }}| -` - - b := hugolib.Test(t, files) - - b.AssertFileContent("public/p1/index.html", - `
    Base Name
    `, - `
    Base Name
    `, - `
    Foo@Bar
    `, - `

    My Title

    `, - `
    foo something bar
    `, - `

    Title with id set

    `, - `

    Title with id set duplicate

    `, - `
    My Title
    `, - `
    良善天父
    `, - `
    Ā ā Ă ă Ą ą Ć ć Ĉ ĉ Ċ ċ Č č Ď
    `, - `

    `, - `