diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 0aad99cda..000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,51 +0,0 @@ -defaults: &defaults - docker: - - image: bepsays/ci-goreleaser:1.13-1 - environment: - CGO_ENABLED: "0" - -version: 2 -jobs: - build: - <<: *defaults - steps: - - checkout: - path: hugo - - run: - command: | - git clone git@github.com:gohugoio/hugoDocs.git - cd hugo - go mod download - sleep 5 - go test -p 1 ./... - - persist_to_workspace: - root: . - paths: . - release: - <<: *defaults - steps: - - attach_workspace: - at: /root/project - - run: - command: | - cd hugo - git config --global user.email "bjorn.erik.pedersen+hugoreleaser@gmail.com" - git config --global user.name "hugoreleaser" - go run -tags release main.go release -r ${CIRCLE_BRANCH} - -workflows: - version: 2 - release: - jobs: - - build: - filters: - branches: - only: /release-.*/ - - hold: - type: approval - requires: - - build - - release: - context: org-global - requires: - - hold 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/.editorconfig b/.editorconfig new file mode 100644 index 000000000..dd2a0096f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +# 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/.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 12e4b5541..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -name: 'Bug report' -labels: '' -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/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index da14802fb..000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: Proposal -about: Suggest an idea for Hugo -title: '' -labels: 'Proposal' -assignees: '' - ---- diff --git a/.github/ISSUE_TEMPLATE/support.md b/.github/ISSUE_TEMPLATE/support.md deleted file mode 100644 index 31837330e..000000000 --- a/.github/ISSUE_TEMPLATE/support.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -name: Support (Do not use) -about: Please do not use Github for support requests. Visit https://discourse.gohugo.io for support -title: '' -labels: support -assignees: '' - ---- - -Issues created with this template will be automatically closed. Please visit https://discourse.gohugo.io for the support you really, really, want! \ 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/calibre/image-actions.yml b/.github/calibre/image-actions.yml new file mode 100644 index 000000000..938660243 --- /dev/null +++ b/.github/calibre/image-actions.yml @@ -0,0 +1,2 @@ +ignorePaths: + - "resources/**" \ No newline at end of file diff --git a/.github/stale.yml b/.github/stale.yml index 692c59659..389205294 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -6,6 +6,7 @@ daysUntilClose: 30 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 @@ -13,9 +14,7 @@ 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 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. + 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. diff --git a/.github/workflows/auto_close_support.yml b/.github/workflows/auto_close_support.yml deleted file mode 100644 index 28a0e695f..000000000 --- a/.github/workflows/auto_close_support.yml +++ /dev/null @@ -1,14 +0,0 @@ -on: - schedule: - - cron: 0 5 * * 3 -name: Weekly Issue Closure -jobs: - cycle-weekly-close: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - name: weekly-issue-closure - uses: bdougie/close-issues-based-on-label@master - env: - LABEL: support - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/calibreapp-image-actions.yml b/.github/workflows/calibreapp-image-actions.yml new file mode 100644 index 000000000..3d95fd0c8 --- /dev/null +++ b/.github/workflows/calibreapp-image-actions.yml @@ -0,0 +1,12 @@ +name: Compress images +on: pull_request +jobs: + build: + name: calibreapp/image-actions + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: calibreapp/image-actions + uses: docker://calibreapp/github-image-actions + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index e71fe6c00..b203a37cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,5 @@ -/hugo -docs/public* /.idea -hugo.exe -*.test -*.prof +/public nohup.out -cover.out -*.swp -*.swo .DS_Store -*~ -vendor/*/ -*.bench -*.debug -coverage*.out - -dock.sh - -GoBuilds -dist - -resources/sunset.jpg - -vendor \ No newline at end of file +trace.out \ No newline at end of file diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e69de29bb..000000000 diff --git a/.mailmap b/.mailmap deleted file mode 100644 index e93adabc1..000000000 --- a/.mailmap +++ /dev/null @@ -1,3 +0,0 @@ -spf13 Steve Francia -bep Bjørn Erik Pedersen - diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e614099d0..000000000 --- a/.travis.yml +++ /dev/null @@ -1,46 +0,0 @@ -language: go -sudo: false -dist: xenial -env: - global: - - GOPROXY="https://proxy.golang.org" - - HUGO_BUILD_TAGS="extended" -git: - depth: false -go: - - "1.12.10" - - "1.13.1" - - tip -os: - - linux - - osx - - windows -matrix: - allow_failures: - - go: tip - fast_finish: true - exclude: - - os: windows - go: tip - -install: - - mkdir -p $HOME/src - - mv $TRAVIS_BUILD_DIR $HOME/src - - export TRAVIS_BUILD_DIR=$HOME/src/hugo - - cd $HOME/src/hugo - - go get github.com/magefile/mage -script: - - go mod download || true - - mage -v test - - mage -v check - - mage -v hugo - - ./hugo -s docs/ - - ./hugo --renderToMemory -s docs/ - - df -h - -before_install: - - df -h - # https://travis-ci.community/t/go-cant-find-gcc-with-go1-11-1-on-windows/293/5 - - if [[ "$TRAVIS_OS_NAME" == "windows" ]]; then choco install mingw -y; export PATH=/c/tools/mingw64/bin:"$PATH"; fi - - gem install asciidoctor - - type asciidoctor diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 124e5b754..000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,191 +0,0 @@ -# Contributing to Hugo - -We welcome contributions to Hugo of any kind including documentation, themes, -organization, tutorials, blog posts, bug reports, issues, feature requests, -feature implementations, pull requests, answering questions on the forum, -helping to manage issues, etc. - -The Hugo community and maintainers are [very active](https://github.com/gohugoio/hugo/pulse/monthly) and helpful, and the project benefits greatly from this activity. 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) -* [Reporting Issues](#reporting-issues) -* [Submitting Patches](#submitting-patches) - * [Code Contribution Guidelines](#code-contribution-guidelines) - * [Git Commit Message Guidelines](#git-commit-message-guidelines) - * [Fetching the Sources From GitHub](#fetching-the-sources-from-github) - * [Building Hugo with Your Changes](#building-hugo-with-your-changes) - -## Asking Support Questions - -We have an active [discussion forum](https://discourse.gohugo.io) where users and developers can ask questions. -Please don't use the GitHub issue tracker to ask questions. - -## Reporting Issues - -If you believe you have found a defect in Hugo or its documentation, use -the GitHub [issue tracker](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. - -## 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 future (answer questions on the forum, fix any bugs etc.). - -It is recommended to open up a discussion on the [Hugo Forum](https://discourse.gohugo.io/) to get feedback on your idea before you begin. If you are submitting a complex feature, create a small design proposal on the [Hugo issue tracker](https://github.com/gohugoio/hugo/issues) before you start. - - -**Bug fixes are, of course, always welcome.** - - - -## 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. - -### 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. -* When you’re ready to create a pull request, be sure to: - * Sign the [CLA](https://cla-assistant.io/gohugoio/hugo). - * Have test cases for the new code. If you have questions about how to do this, please ask in your pull request. - * 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. - * Follow the **Git Commit Message Guidelines** below. - -### Git Commit Message Guidelines - -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: -*"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 package name (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 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 -tpl: Add custom index function - -Add a custom index template function that deviates from the stdlib simply by not -returning an "index out of range" error if an array, slice or string index is -out of range. Instead, we just return nil values. This should help make the -new default function more useful for Hugo users. - -Fixes #1949 -``` - -### Fetching the Sources From GitHub - -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: - -```bash -mkdir $HOME/src -cd $HOME/src -git clone https://github.com/gohugoio/hugo.git -cd hugo -go install -``` - ->Note: Some Go tools may not be fully updated to support Go Modules yet. One example would be LiteIDE. Follow [this workaround](https://github.com/visualfc/liteide/issues/986#issuecomment-428117702) for how to continue to work with Hugo below `GOPATH`. - -For some convenient build and test targets, you also will want to install Mage: - -```bash -go get github.com/magefile/mage -``` - -Now, to make a change to Hugo's source: - -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 - ``` - -1. Fork Hugo in GitHub. - -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 - ``` - -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. - -```bash -cd $HOME/go/src/github.com/gohugoio/hugo -``` - -To build Hugo: - -```bash -mage hugo -``` - -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 deleted file mode 100755 index 4728a0f2e..000000000 --- a/Dockerfile +++ /dev/null @@ -1,33 +0,0 @@ -# GitHub: https://github.com/gohugoio -# Twitter: https://twitter.com/gohugoio -# Website: https://gohugo.io/ - -FROM golang:1.11-stretch AS build - - -WORKDIR /go/src/github.com/gohugoio/hugo -RUN apt-get install \ - git gcc g++ binutils -COPY . /go/src/github.com/gohugoio/hugo/ -ENV GO111MODULE=on -RUN go get -d . - -ARG CGO=0 -ENV CGO_ENABLED=${CGO} -ENV GOOS=linux - -# default non-existent build tag so -tags always has an arg -ARG BUILD_TAGS="99notag" -RUN go install -ldflags '-w -extldflags "-static"' -tags ${BUILD_TAGS} - -# --- - -FROM alpine:3.9 -RUN apk add --no-cache ca-certificates -COPY --from=build /go/bin/hugo /hugo -ARG WORKDIR="/site" -WORKDIR ${WORKDIR} -VOLUME ${WORKDIR} -EXPOSE 1313 -ENTRYPOINT [ "/hugo" ] -CMD [ "--help" ] diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 261eeb9e9..000000000 --- a/LICENSE +++ /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/LICENSE.md b/LICENSE.md new file mode 100644 index 000000000..b62a9b5ff --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,194 @@ +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/README.md b/README.md index cef49b617..7ca9d734c 100644 --- a/README.md +++ b/README.md @@ -1,187 +1,48 @@ -Hugo +[![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/) -A Fast and Flexible Static Site Generator built with love by [bep](https://github.com/bep), [spf13](http://spf13.com/) and [friends](https://github.com/gohugoio/hugo/graphs/contributors) in [Go][]. +# Hugo Docs -[Website](https://gohugo.io) | -[Forum](https://discourse.gohugo.io) | -[Documentation](https://gohugo.io/getting-started/) | -[Installation Guide](https://gohugo.io/getting-started/installing/) | -[Contribution Guide](CONTRIBUTING.md) | -[Twitter](https://twitter.com/gohugoio) +Documentation site for [Hugo](https://github.com/gohugoio/hugo), the very fast and flexible static site generator built with love in Go. -[![GoDoc](https://godoc.org/github.com/gohugoio/hugo?status.svg)](https://godoc.org/github.com/gohugoio/hugo) -[![Linux and macOS Build Status](https://api.travis-ci.org/gohugoio/hugo.svg?branch=master&label=Windows+and+Linux+and+macOS+build "Windows, Linux and macOS Build Status")](https://travis-ci.org/gohugoio/hugo) -[![Go Report Card](https://goreportcard.com/badge/github.com/gohugoio/hugo)](https://goreportcard.com/report/github.com/gohugoio/hugo) +## Contributing -## Overview +We welcome contributions to Hugo of any kind including documentation, suggestions, bug reports, pull requests etc. Also check out our [contribution guide](https://gohugo.io/contribute/documentation/). We would love to hear from you. -Hugo is a static HTML and CSS website generator written in [Go][]. -It is optimized for speed, ease of use, and configurability. -Hugo takes a directory with content and templates and renders them into a full HTML website. +Note that this repository contains solely the documentation for Hugo. For contributions that aren't documentation-related please refer to the [hugo](https://github.com/gohugoio/hugo) repository. -Hugo relies on Markdown files with front matter for metadata, and you can run Hugo from any directory. -This works well for shared hosts and other systems where you don’t have a privileged account. +*Pull requests shall **only** contain changes to the actual documentation. However, changes on the code base of Hugo **and** the documentation shall be a single, atomic pull request in the [hugo](https://github.com/gohugoio/hugo) repository.* -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. +Spelling fixes are most welcomed, and if you want to contribute longer sections to the documentation, it would be great if you had these in mind when writing: -Hugo is designed to work well for any kind of website including blogs, tumbles, and docs. +* Short is good. People go to the library to read novels. If there is more than one way to _do a thing_ in Hugo, describe the current _best practice_ (avoid "… but you can also do …" and "… in older versions of Hugo you had to …". +* For examples, try to find short snippets that teaches people about the concept. If the example is also useful as-is (copy and paste), then great, but don't list long and similar examples just so people can use them on their sites. +* Hugo has users from all over the world, so an easy to understand and [simple English](https://simple.wikipedia.org/wiki/Basic_English) is good. -#### Supported Architectures +## Branches -Currently, we provide pre-built Hugo binaries for Windows, Linux, FreeBSD, NetBSD, DragonFly BSD, Open BSD, macOS (Darwin), and [Android](https://gist.github.com/bep/a0d8a26cf6b4f8bc992729b8e50b480b) for x64, i386 and ARM architectures. +* The `master` branch is where the site is automatically built from, and is the place to put changes relevant to the current Hugo version. +* The `next` branch is where we store changes that is related to the next Hugo release. This can be previewed here: https://next--gohugoio.netlify.com/ -Hugo may also be compiled from source wherever the Go compiler tool chain can run, e.g. for other operating systems including Plan 9 and Solaris. +## Build -**Complete documentation is available at [Hugo Documentation](https://gohugo.io/getting-started/).** - -## Choose How to Install - -If you want to use Hugo as your site generator, simply install the Hugo binaries. -The Hugo binaries have no external dependencies. - -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. - -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. - -### Install Hugo as Your Site Generator (Binary Install) - -Use the [installation instructions in the Hugo documentation](https://gohugo.io/getting-started/installing/). - -### Build and Install the Binaries from Source (Advanced Install) - -#### Prerequisite Tools - -* [Git](https://git-scm.com/) -* [Go (at least Go 1.11)](https://golang.org/dl/) - -#### Fetch from GitHub - -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: +To view the documentation site locally, you need to clone this repository: ```bash -mkdir $HOME/src -cd $HOME/src -git clone https://github.com/gohugoio/hugo.git -cd hugo -go install +git clone https://github.com/gohugoio/hugoDocs.git ``` -**If you are a Windows user, substitute the `$HOME` environment variable above with `%USERPROFILE%`.** - -## The Hugo Documentation +Also note that the documentation version for a given version of Hugo can also be found in the `/docs` sub-folder of the [Hugo source repository](https://github.com/gohugoio/hugo). -The Hugo documentation now lives in its own repository, see https://github.com/gohugoio/hugoDocs. But we do keep a version of that documentation as a `git subtree` in this repository. To build the sub folder `/docs` as a Hugo site, you need to clone this repo: +Then to view the docs in your browser, run Hugo and open up the link: ```bash -git clone git@github.com:gohugoio/hugo.git +▶ hugo server + +Started building sites ... +. +. +Serving pages from memory +Web Server is available at http://localhost:1313/ (bind address 127.0.0.1) +Press Ctrl+C to stop ``` -## Contributing to Hugo - -For a complete guide to contributing to Hugo, see the [Contribution Guide](CONTRIBUTING.md). - -We welcome contributions to Hugo of any kind including documentation, themes, -organization, tutorials, blog posts, bug reports, issues, feature requests, -feature implementations, pull requests, answering questions on the forum, -helping to manage issues, etc. - -The Hugo community and maintainers are [very active](https://github.com/gohugoio/hugo/pulse/monthly) and helpful, and the project benefits greatly from this activity. - -### Asking Support Questions - -We have an active [discussion forum](https://discourse.gohugo.io) where users and developers can ask questions. -Please don't use the GitHub issue tracker to ask questions. - -### Reporting Issues - -If you believe you have found a defect in Hugo or its documentation, use -the GitHub issue tracker to report the problem to the Hugo maintainers. -If you're not sure if it's a bug or not, start by asking in the [discussion forum](https://discourse.gohugo.io). -When reporting the issue, please provide the version of Hugo in use (`hugo version`). - -### 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/ - -## Dependencies - -Hugo stands on the shoulder of many great open source libraries, in lexical order: - - | Dependency | License | - | :------------- | :------------- | - | [github.com/BurntSushi/locker](https://github.com/BurntSushi/locker) | The Unlicense | - | [github.com/BurntSushi/toml](https://github.com/BurntSushi/toml) | MIT License | - | [github.com/PuerkitoBio/purell](https://github.com/PuerkitoBio/purell) | BSD 3-Clause "New" or "Revised" License | - | [github.com/PuerkitoBio/urlesc](https://github.com/PuerkitoBio/urlesc) | BSD 3-Clause "New" or "Revised" License | - | [github.com/alecthomas/chroma](https://github.com/alecthomas/chroma) | MIT License | - | [github.com/bep/debounce](https://github.com/bep/debounce) | MIT License | - | [github.com/bep/gitmap](https://github.com/bep/gitmap) | MIT License | - | [github.com/bep/go-tocss](https://github.com/bep/go-tocss) | MIT License | - | [github.com/niklasfasching/go-org](https://github.com/niklasfasching/go-org) | MIT License | - | [github.com/cpuguy83/go-md2man](https://github.com/cpuguy83/go-md2man) | MIT License | - | [github.com/danwakefield/fnmatch](https://github.com/danwakefield/fnmatch) | BSD 2-Clause "Simplified" License | - | [github.com/disintegration/imaging](https://github.com/disintegration/imaging) | MIT License | - | [github.com/dlclark/regexp2](https://github.com/dlclark/regexp2) | MIT License | - | [github.com/eknkc/amber](https://github.com/eknkc/amber) | MIT License | - | [github.com/fsnotify/fsnotify](https://github.com/fsnotify/fsnotify) | BSD 3-Clause "New" or "Revised" License | - | [github.com/gobwas/glob](https://github.com/gobwas/glob) | MIT License | - | [github.com/gorilla/websocket](https://github.com/gorilla/websocket) | BSD 2-Clause "Simplified" License | - | [github.com/hashicorp/go-immutable-radix](https://github.com/hashicorp/go-immutable-radix) | Mozilla Public License 2.0 | - | [github.com/hashicorp/golang-lru](https://github.com/hashicorp/golang-lru) | Mozilla Public License 2.0 | - | [github.com/hashicorp/hcl](https://github.com/hashicorp/hcl) | Mozilla Public License 2.0 | - | [github.com/jdkato/prose](https://github.com/jdkato/prose) | MIT License | - | [github.com/kyokomi/emoji](https://github.com/kyokomi/emoji) | MIT License | - | [github.com/magiconair/properties](https://github.com/magiconair/properties) | BSD 2-Clause "Simplified" License | - | [github.com/markbates/inflect](https://github.com/markbates/inflect) | MIT License | - | [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) | MIT License | - | [github.com/mattn/go-runewidth](https://github.com/mattn/go-runewidth) | MIT License | - | [github.com/miekg/mmark](https://github.com/miekg/mmark) | Simplified BSD License | - | [github.com/mitchellh/hashstructure](https://github.com/mitchellh/hashstructure) | MIT License | - | [github.com/mitchellh/mapstructure](https://github.com/mitchellh/mapstructure) | MIT License | - | [github.com/muesli/smartcrop](https://github.com/muesli/smartcrop) | MIT License | - | [github.com/nicksnyder/go-i18n](https://github.com/nicksnyder/go-i18n) | MIT License | - | [github.com/olekukonko/tablewriter](https://github.com/olekukonko/tablewriter) | MIT License | - | [github.com/pelletier/go-toml](https://github.com/pelletier/go-toml) | MIT License | - | [github.com/pkg/errors](https://github.com/pkg/errors) | BSD 2-Clause "Simplified" License | - | [github.com/russross/blackfriday](https://github.com/russross/blackfriday) | Simplified BSD License | - | [github.com/shurcooL/sanitized_anchor_name](https://github.com/shurcooL/sanitized_anchor_name) | MIT License | - | [github.com/spf13/afero](https://github.com/spf13/afero) | Apache License 2.0 | - | [github.com/spf13/cast](https://github.com/spf13/cast) | MIT License | - | [github.com/spf13/cobra](https://github.com/spf13/cobra) | Apache License 2.0 | - | [github.com/spf13/fsync](https://github.com/spf13/fsync) | MIT License | - | [github.com/spf13/jwalterweatherman](https://github.com/spf13/jwalterweatherman) | MIT License | - | [github.com/spf13/nitro](https://github.com/spf13/nitro) | Apache License 2.0 | - | [github.com/spf13/pflag](https://github.com/spf13/pflag) | BSD 3-Clause "New" or "Revised" License | - | [github.com/spf13/viper](https://github.com/spf13/viper) | MIT License | - | [github.com/tdewolff/minify](https://github.com/tdewolff/minify) | MIT License | - | [github.com/tdewolff/parse](https://github.com/tdewolff/parse) | MIT License | - | [github.com/wellington/go-libsass](https://github.com/wellington/go-libsass) | Apache License 2.0 | - | [github.com/yosssi/ace](https://github.com/yosssi/ace) | MIT License | - | [golang.org/x/image](https://golang.org/x/image) | BSD 3-Clause "New" or "Revised" License | - | [golang.org/x/net](https://golang.org/x/net) | BSD 3-Clause "New" or "Revised" License | - | [golang.org/x/sync](https://golang.org/x/sync) | BSD 3-Clause "New" or "Revised" License | - | [golang.org/x/sys](https://golang.org/x/sys) | BSD 3-Clause "New" or "Revised" License | - | [golang.org/x/text](https://golang.org/x/text) | BSD 3-Clause "New" or "Revised" License - | [gopkg.in/yaml.v2](https://gopkg.in/yaml.v2) | Apache License 2.0 | - - - - - - - diff --git a/archetypes/default.md b/archetypes/default.md new file mode 100644 index 000000000..f30f01f74 --- /dev/null +++ b/archetypes/default.md @@ -0,0 +1,13 @@ +--- +linktitle: "" +description: "" +godocref: "" +publishdate: "" +lastmod: "" +categories: [] +tags: [] +weight: 00 +slug: "" +aliases: [] +toc: false +--- \ No newline at end of file diff --git a/archetypes/functions.md b/archetypes/functions.md new file mode 100644 index 000000000..0a5dd344f --- /dev/null +++ b/archetypes/functions.md @@ -0,0 +1,17 @@ +--- +linktitle: "" +description: "" +godocref: "" +publishdate: "" +lastmod: "" +categories: [functions] +tags: [] +ns: "" +signature: [] +workson: [] +hugoversion: "" +aliases: [] +relatedfuncs: [] +toc: false +deprecated: false +--- \ No newline at end of file diff --git a/archetypes/showcase/bio.md b/archetypes/showcase/bio.md new file mode 100644 index 000000000..2443c2f35 --- /dev/null +++ b/archetypes/showcase/bio.md @@ -0,0 +1,8 @@ + +Add some **general info** about {{ replace .Name "-" " " | title }} here. + +The site is built by: + +* [Person 1](https://example.com) +* [Person 1](https://example.com) + diff --git a/archetypes/showcase/featured.png b/archetypes/showcase/featured.png new file mode 100644 index 000000000..4f390132e Binary files /dev/null and b/archetypes/showcase/featured.png differ diff --git a/archetypes/showcase/index.md b/archetypes/showcase/index.md new file mode 100644 index 000000000..a21bb9726 --- /dev/null +++ b/archetypes/showcase/index.md @@ -0,0 +1,37 @@ +--- + +title: {{ replace .Name "-" " " | title }} +date: {{ now.Format "2006-01-02" }} + +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. +2. Add a summary to the `bio.md` file in this folder. +3. Replace the `featured-template.png` with a screenshot of your site. You can rename it, but it must contain the word `featured`. +4. 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/bench.sh b/bench.sh deleted file mode 100755 index c6a20a7e3..000000000 --- a/bench.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash - -# allow user to override go executable by running as GOEXE=xxx make ... -GOEXE="${GOEXE-go}" - -# Convenience script to -# - For a given branch -# - Run benchmark tests for a given package -# - Do the same for master -# - then compare the two runs with benchcmp - -benchFilter=".*" - -if (( $# < 2 )); - then - echo "USAGE: ./bench.sh (and (regexp, optional))" - exit 1 -fi - - - -if [ $# -eq 3 ]; then - benchFilter=$3 -fi - - -BRANCH=$1 -PACKAGE=$2 - -git checkout $BRANCH -"${GOEXE}" test -test.run=NONE -bench="$benchFilter" -test.benchmem=true ./$PACKAGE > /tmp/bench-$PACKAGE-$BRANCH.txt - -git checkout master -"${GOEXE}" test -test.run=NONE -bench="$benchFilter" -test.benchmem=true ./$PACKAGE > /tmp/bench-$PACKAGE-master.txt - - -benchcmp /tmp/bench-$PACKAGE-master.txt /tmp/bench-$PACKAGE-$BRANCH.txt diff --git a/benchSite.sh b/benchSite.sh deleted file mode 100755 index 623086708..000000000 --- a/benchSite.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -# allow user to override go executable by running as GOEXE=xxx make ... -GOEXE="${GOEXE-go}" - -# Send in a regexp 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}" - -"${GOEXE}" test -run="NONE" -bench="BenchmarkSiteBuilding/${1}" -test.benchmem=true ./hugolib -memprofile mem.prof -count 3 -cpuprofile cpu.prof diff --git a/benchbep.sh b/benchbep.sh deleted file mode 100755 index efd616c88..000000000 --- a/benchbep.sh +++ /dev/null @@ -1 +0,0 @@ -gobench -package=./hugolib -bench="BenchmarkSiteNew/Deep_content_tree" \ No newline at end of file diff --git a/bepdock.sh b/bepdock.sh deleted file mode 100755 index a7ac0c639..000000000 --- a/bepdock.sh +++ /dev/null @@ -1 +0,0 @@ -docker run --rm --mount type=bind,source="$(pwd)",target=/hugo -w /hugo -i -t bepsays/ci-goreleaser:1.11-2 /bin/bash \ No newline at end of file diff --git a/bufferpool/bufpool.go b/bufferpool/bufpool.go deleted file mode 100644 index c1e4105d0..000000000 --- a/bufferpool/bufpool.go +++ /dev/null @@ -1,38 +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 bufferpool provides a pool of bytes buffers. -package bufferpool - -import ( - "bytes" - "sync" -) - -var bufferPool = &sync.Pool{ - New: func() interface{} { - return &bytes.Buffer{} - }, -} - -// GetBuffer returns a buffer from the pool. -func GetBuffer() (buf *bytes.Buffer) { - return bufferPool.Get().(*bytes.Buffer) -} - -// PutBuffer returns a buffer to the pool. -// The buffer is reset before it is put back into circulation. -func PutBuffer(buf *bytes.Buffer) { - buf.Reset() - bufferPool.Put(buf) -} diff --git a/bufferpool/bufpool_test.go b/bufferpool/bufpool_test.go deleted file mode 100644 index 023724b97..000000000 --- a/bufferpool/bufpool_test.go +++ /dev/null @@ -1,31 +0,0 @@ -// 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 bufferpool - -import ( - "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") - PutBuffer(buff) - - c.Assert(buff.Len(), qt.Equals, 0) -} diff --git a/cache/filecache/filecache.go b/cache/filecache/filecache.go deleted file mode 100644 index 3628300fb..000000000 --- a/cache/filecache/filecache.go +++ /dev/null @@ -1,376 +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 ( - "bytes" - "errors" - "io" - "io/ioutil" - "os" - "path/filepath" - "strings" - "sync" - "time" - - "github.com/gohugoio/hugo/common/hugio" - - "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 -} - -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() -} - -// 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) { - 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.Reader) error, - create func(info ItemInfo, w io.WriteCloser) error) (info ItemInfo, err error) { - 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 - -} - -// 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) { - 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 - } - - 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), - afero.WriteReader(c.Fs, id, io.TeeReader(r, &buff)) -} - -// GetOrCreateBytes is the same as GetOrCreate, but produces a byte slice. -func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (ItemInfo, []byte, error) { - 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 := ioutil.ReadAll(r) - return info, b, err - } - - b, err := create() - if err != nil { - return info, nil, err - } - - if c.maxAge == 0 { - return info, b, nil - } - - if err := afero.WriteReader(c.Fs, 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 cahce, nil if none found. -func (c *Cache) GetBytes(id string) (ItemInfo, []byte, error) { - 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 := ioutil.ReadAll(r) - return info, b, err - } - - return info, nil, nil -} - -// Get gets the file with the given id from the cahce, nil if none found. -func (c *Cache) Get(id string) (ItemInfo, io.ReadCloser, error) { - 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 c.maxAge > 0 { - fi, err := c.Fs.Stat(id) - if err != nil { - return nil - } - - if c.isExpired(fi.ModTime()) { - c.Fs.Remove(id) - return nil - } - } - - f, err := c.Fs.Open(id) - - if err != nil { - return nil - } - - return f -} - -func (c *Cache) isExpired(modTime time.Time) bool { - if c.maxAge < 0 { - return false - } - 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, _ := ioutil.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) { - var dcfg Configs - if c, ok := p.Cfg.Get("filecacheConfigs").(Configs); ok { - dcfg = c - } else { - var err error - dcfg, err = DecodeConfig(p.Fs.Source, p.Cfg) - if err != nil { - return nil, err - } - } - - 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 { - // TODO(bep) we still have some places that do not initialize the - // full dependencies of a site, e.g. the import Jekyll command. - // That command does not need these caches, so let us just continue - // for now. - continue - } - - baseDir := v.Dir - - if err := cfs.MkdirAll(baseDir, 0777); err != nil && !os.IsExist(err) { - return nil, err - } - - bfs := afero.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) -} diff --git a/cache/filecache/filecache_config.go b/cache/filecache/filecache_config.go deleted file mode 100644 index 0c6b569c1..000000000 --- a/cache/filecache/filecache_config.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 filecache - -import ( - "path" - "path/filepath" - "strings" - "time" - - "github.com/gohugoio/hugo/config" - - "github.com/gohugoio/hugo/helpers" - - "github.com/mitchellh/mapstructure" - "github.com/pkg/errors" - "github.com/spf13/afero" -) - -const ( - cachesConfigKey = "caches" - - resourcesGenDir = ":resourceDir/_gen" -) - -var defaultCacheConfig = Config{ - MaxAge: -1, // Never expire - Dir: ":cacheDir/:project", -} - -const ( - cacheKeyGetJSON = "getjson" - cacheKeyGetCSV = "getcsv" - cacheKeyImages = "images" - cacheKeyAssets = "assets" - cacheKeyModules = "modules" -) - -type Configs map[string]Config - -func (c Configs) CacheDirModules() string { - return c[cacheKeyModules].Dir -} - -var defaultCacheConfigs = Configs{ - cacheKeyModules: { - MaxAge: -1, - Dir: ":cacheDir/modules", - }, - cacheKeyGetJSON: defaultCacheConfig, - cacheKeyGetCSV: defaultCacheConfig, - cacheKeyImages: { - MaxAge: -1, - Dir: resourcesGenDir, - }, - cacheKeyAssets: { - MaxAge: -1, - Dir: resourcesGenDir, - }, -} - -type Config 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. - MaxAge time.Duration - - // The directory where files are stored. - Dir string - - // Will resources/_gen will get its own composite filesystem that - // also checks any theme. - isResourceDir bool -} - -// 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] -} - -func DecodeConfig(fs afero.Fs, cfg config.Provider) (Configs, error) { - c := make(Configs) - valid := make(map[string]bool) - // Add defaults - for k, v := range defaultCacheConfigs { - c[k] = v - valid[k] = true - } - - m := cfg.GetStringMap(cachesConfigKey) - - _, isOsFs := fs.(*afero.OsFs) - - for k, v := range m { - 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, err - } - - if cc.Dir == "" { - return c, errors.New("must provide cache Dir") - } - - name := strings.ToLower(k) - if !valid[name] { - return nil, errors.Errorf("%q is not a valid cache name", name) - } - - c[name] = cc - } - - // This is a very old flag in Hugo, but we need to respect it. - disabled := cfg.GetBool("ignoreCache") - - 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, cfg, part) - if err != nil { - return c, err - } - if isResource { - v.isResourceDir = true - } - parts[i] = resolved - } - } - - dir = path.Join(parts...) - if hadSlash { - dir = "/" + dir - } - v.Dir = filepath.Clean(filepath.FromSlash(dir)) - - if !v.isResourceDir { - if isOsFs && !filepath.IsAbs(v.Dir) { - return c, errors.Errorf("%q must resolve to an absolute directory", v.Dir) - } - - // Avoid cache in root, e.g. / (Unix) or c:\ (Windows) - if len(strings.TrimPrefix(v.Dir, filepath.VolumeName(v.Dir))) == 1 { - return c, errors.Errorf("%q is a root folder and not allowed as cache dir", v.Dir) - } - } - - if !strings.HasPrefix(v.Dir, "_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.Dir = filepath.Join(v.Dir, filecacheRootDirname, k) - } else { - v.Dir = filepath.Join(v.Dir, k) - } - - if disabled { - v.MaxAge = 0 - } - - c[k] = v - } - - return c, nil -} - -// Resolves :resourceDir => /myproject/resources etc., :cacheDir => ... -func resolveDirPlaceholder(fs afero.Fs, cfg config.Provider, placeholder string) (cacheDir string, isResource bool, err error) { - workingDir := cfg.GetString("workingDir") - - switch strings.ToLower(placeholder) { - case ":resourcedir": - return "", true, nil - case ":cachedir": - d, err := helpers.GetCacheDir(fs, cfg) - return d, false, err - case ":project": - return filepath.Base(workingDir), false, nil - } - - return "", false, errors.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 9f80a4f90..000000000 --- a/cache/filecache/filecache_config_test.go +++ /dev/null @@ -1,196 +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 ( - "path/filepath" - "runtime" - "strings" - "testing" - "time" - - "github.com/spf13/afero" - - "github.com/gohugoio/hugo/config" - - qt "github.com/frankban/quicktest" - "github.com/spf13/viper" -) - -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" - -` - - cfg, err := config.FromConfigString(configStr, "toml") - c.Assert(err, qt.IsNil) - fs := afero.NewMemMapFs() - decoded, err := DecodeConfig(fs, cfg) - c.Assert(err, qt.IsNil) - - c.Assert(len(decoded), qt.Equals, 5) - - c2 := decoded["getcsv"] - c.Assert(c2.MaxAge.String(), qt.Equals, "11h0m0s") - c.Assert(c2.Dir, qt.Equals, filepath.FromSlash("/path/to/c2/filecache/getcsv")) - - c3 := decoded["images"] - c.Assert(c3.MaxAge, qt.Equals, time.Duration(-1)) - c.Assert(c3.Dir, qt.Equals, filepath.FromSlash("/path/to/c3/filecache/images")) - -} - -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" - -` - - cfg, err := config.FromConfigString(configStr, "toml") - c.Assert(err, qt.IsNil) - fs := afero.NewMemMapFs() - decoded, err := DecodeConfig(fs, cfg) - c.Assert(err, qt.IsNil) - - c.Assert(len(decoded), qt.Equals, 5) - - for _, v := range decoded { - c.Assert(v.MaxAge, qt.Equals, time.Duration(0)) - } - -} - -func TestDecodeConfigDefault(t *testing.T) { - c := qt.New(t) - cfg := newTestConfig() - - 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") - } - - fs := afero.NewMemMapFs() - - decoded, err := DecodeConfig(fs, cfg) - - c.Assert(err, qt.IsNil) - - c.Assert(len(decoded), qt.Equals, 5) - - imgConfig := decoded[cacheKeyImages] - jsonConfig := decoded[cacheKeyGetJSON] - - if runtime.GOOS == "windows" { - c.Assert(imgConfig.Dir, qt.Equals, filepath.FromSlash("_gen/images")) - } else { - c.Assert(imgConfig.Dir, qt.Equals, "_gen/images") - c.Assert(jsonConfig.Dir, qt.Equals, "/cache/thecache/hugoproject/filecache/getjson") - } - - c.Assert(imgConfig.isResourceDir, qt.Equals, true) - c.Assert(jsonConfig.isResourceDir, qt.Equals, false) -} - -func TestDecodeConfigInvalidDir(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 = "/" - -` - if runtime.GOOS == "windows" { - configStr = strings.Replace(configStr, "/", "c:\\\\", 1) - } - - cfg, err := config.FromConfigString(configStr, "toml") - c.Assert(err, qt.IsNil) - fs := afero.NewMemMapFs() - - _, err = DecodeConfig(fs, cfg) - c.Assert(err, qt.Not(qt.IsNil)) - -} - -func newTestConfig() *viper.Viper { - cfg := viper.New() - cfg.Set("workingDir", filepath.FromSlash("/my/cool/hugoproject")) - cfg.Set("contentDir", "content") - cfg.Set("dataDir", "data") - cfg.Set("resourceDir", "resources") - cfg.Set("i18nDir", "i18n") - cfg.Set("layoutDir", "layouts") - cfg.Set("archetypeDir", "archetypes") - cfg.Set("assetDir", "assets") - - return cfg -} diff --git a/cache/filecache/filecache_pruner.go b/cache/filecache/filecache_pruner.go deleted file mode 100644 index 7f68c8b82..000000000 --- a/cache/filecache/filecache_pruner.go +++ /dev/null @@ -1,138 +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 ( - "io" - "os" - - "github.com/pkg/errors" - "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 os.IsNotExist(err) { - continue - } - return counter, errors.Wrapf(err, "failed to prune cache %q", k) - } - - } - - 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) - } - - 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 - } - defer f.Close() - _, err = f.Readdirnames(1) - if err == io.EOF { - // Empty dir. - err = c.Fs.Remove(name) - } - - if err != nil && !os.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 && !os.IsNotExist(err) { - return err - } - - } - - return nil - }) - - return counter, err -} - -func (c *Cache) pruneRootDir(force bool) (int, error) { - - info, err := c.Fs.Stat(c.pruneAllRootDir) - if err != nil { - if os.IsNotExist(err) { - return 0, nil - } - return 0, err - } - - if !force && !c.isExpired(info.ModTime()) { - return 0, nil - } - - counter := 0 - // Module cache has 0555 directories; make them writable in order to remove content. - afero.Walk(c.Fs, c.pruneAllRootDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return nil - } - if info.IsDir() { - counter++ - c.Fs.Chmod(path, 0777) - } - return nil - }) - return 1, c.Fs.RemoveAll(c.pruneAllRootDir) - -} diff --git a/cache/filecache/filecache_pruner_test.go b/cache/filecache/filecache_pruner_test.go deleted file mode 100644 index 48bce723e..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 - -import ( - "fmt" - "testing" - "time" - - "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{cacheKeyGetCSV, cacheKeyGetJSON, cacheKeyAssets, cacheKeyImages} { - msg := qt.Commentf("cache: %s", name) - p := newPathsSpec(t, afero.NewMemMapFs(), configStr) - caches, err := NewCaches(p) - c.Assert(err, qt.IsNil) - cache := caches[name] - for i := 0; i < 10; i++ { - 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 := 0; i < 10; i++ { - 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 = 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 := 0; i < 10; i++ { - 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 a4bf45fe0..000000000 --- a/cache/filecache/filecache_test.go +++ /dev/null @@ -1,348 +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 ( - "errors" - "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" - "strings" - "sync" - "testing" - "time" - - "github.com/gohugoio/hugo/langs" - "github.com/gohugoio/hugo/modules" - - "github.com/gohugoio/hugo/common/hugio" - "github.com/gohugoio/hugo/config" - "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, err := ioutil.TempDir("", "hugo_filecache_test_work") - c.Assert(err, qt.IsNil) - defer os.Remove(tempWorkingDir) - - tempCacheDir, err := ioutil.TempDir("", "hugo_filecache_test_cache") - c.Assert(err, qt.IsNil) - defer os.Remove(tempCacheDir) - - 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 := NewCaches(p) - c.Assert(err, qt.IsNil) - - cache := caches.Get("GetJSON") - c.Assert(cache, qt.Not(qt.IsNil)) - c.Assert(cache.maxAge.String(), qt.Equals, "10h0m0s") - - bfs, ok := cache.Fs.(*afero.BasePathFs) - c.Assert(ok, qt.Equals, true) - filename, err := bfs.RealPath("key") - c.Assert(err, qt.IsNil) - if test.cacheDir != "" { - c.Assert(filename, qt.Equals, filepath.Join(test.cacheDir, "c/"+filecacheRootDirname+"/getjson/key")) - } else { - // Temp dir. - c.Assert(filename, qt.Matches, ".*hugo_cache.*"+filecacheRootDirname+".*key") - } - - cache = caches.Get("Images") - c.Assert(cache, qt.Not(qt.IsNil)) - c.Assert(cache.maxAge, qt.Equals, time.Duration(-1)) - bfs, ok = cache.Fs.(*afero.BasePathFs) - c.Assert(ok, qt.Equals, true) - filename, _ = bfs.RealPath("key") - c.Assert(filename, qt.Equals, filepath.FromSlash("_gen/images/key")) - - rf := func(s string) func() (io.ReadCloser, error) { - return func() (io.ReadCloser, error) { - return struct { - io.ReadSeeker - io.Closer - }{ - strings.NewReader(s), - ioutil.NopCloser(nil), - }, nil - } - } - - bf := func() ([]byte, error) { - return []byte("bcd"), nil - } - - for _, ca := range []*Cache{caches.ImageCache(), caches.AssetsCache(), caches.GetJSONCache(), caches.GetCSVCache()} { - for i := 0; i < 2; i++ { - 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, _ := ioutil.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, _ = ioutil.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, _ := ioutil.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 := 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 := 0; i < 50; i++ { - wg.Add(1) - go func(i int) { - defer wg.Done() - for j := 0; j < 20; j++ { - 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, _ := ioutil.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 ItemInfo, r io.Reader) error { - - return func(info ItemInfo, r io.Reader) error { - if failLevel > 0 { - if failLevel > 1 { - return ErrFatal - } - return errors.New("fail") - } - - b, _ := ioutil.ReadAll(r) - result = string(b) - - return nil - } - } - - bf := func(s string) func(info ItemInfo, w io.WriteCloser) error { - return func(info ItemInfo, w io.WriteCloser) error { - defer w.Close() - result = s - _, err := w.Write([]byte(s)) - return err - } - } - - cache := 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, ErrFatal) -} - -func TestCleanID(t *testing.T) { - c := qt.New(t) - c.Assert(cleanID(filepath.FromSlash("/a/b//c.txt")), qt.Equals, filepath.FromSlash("a/b/c.txt")) - c.Assert(cleanID(filepath.FromSlash("a/b//c.txt")), qt.Equals, filepath.FromSlash("a/b/c.txt")) -} - -func initConfig(fs afero.Fs, cfg config.Provider) error { - if _, err := langs.LoadLanguageSettings(cfg, nil); err != nil { - return err - } - - modConfig, err := modules.DecodeConfig(cfg) - if err != nil { - return err - } - - workingDir := cfg.GetString("workingDir") - themesDir := cfg.GetString("themesDir") - if !filepath.IsAbs(themesDir) { - themesDir = filepath.Join(workingDir, themesDir) - } - modulesClient := modules.NewClient(modules.ClientConfig{ - Fs: fs, - WorkingDir: workingDir, - ThemesDir: themesDir, - ModuleConfig: modConfig, - IgnoreVendor: true, - }) - - moduleConfig, err := modulesClient.Collect() - if err != nil { - return err - } - - if err := modules.ApplyProjectConfigDefaults(cfg, moduleConfig.ActiveModules[len(moduleConfig.ActiveModules)-1]); err != nil { - return err - } - - cfg.Set("allModules", moduleConfig.ActiveModules) - - return nil -} - -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) - initConfig(fs, cfg) - p, err := helpers.NewPathSpec(hugofs.NewFrom(fs, cfg), cfg, nil) - c.Assert(err, qt.IsNil) - return p - -} diff --git a/cache/namedmemcache/named_cache.go b/cache/namedmemcache/named_cache.go deleted file mode 100644 index d8c229a01..000000000 --- a/cache/namedmemcache/named_cache.go +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright 2018 The Hugo Authors. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package namedmemcache provides a memory cache with a named lock. This is suitable -// for situations where creating the cached resource can be time consuming or otherwise -// resource hungry, or in situations where a "once only per key" is a requirement. -package namedmemcache - -import ( - "sync" - - "github.com/BurntSushi/locker" -) - -// Cache holds the cached values. -type Cache struct { - nlocker *locker.Locker - cache map[string]cacheEntry - mu sync.RWMutex -} - -type cacheEntry struct { - value interface{} - err error -} - -// New creates a new cache. -func New() *Cache { - return &Cache{ - nlocker: locker.NewLocker(), - cache: make(map[string]cacheEntry), - } -} - -// Clear clears the cache state. -func (c *Cache) Clear() { - c.mu.Lock() - defer c.mu.Unlock() - - c.cache = make(map[string]cacheEntry) - c.nlocker = locker.NewLocker() - -} - -// GetOrCreate tries to get the value with the given cache key, if not found -// create will be called and cached. -// This method is thread safe. It also guarantees that the create func for a given -// key is invoced only once for this cache. -func (c *Cache) GetOrCreate(key string, create func() (interface{}, error)) (interface{}, error) { - c.mu.RLock() - entry, found := c.cache[key] - c.mu.RUnlock() - - if found { - return entry.value, entry.err - } - - c.nlocker.Lock(key) - defer c.nlocker.Unlock(key) - - // Create it. - value, err := create() - - c.mu.Lock() - c.cache[key] = cacheEntry{value: value, err: err} - c.mu.Unlock() - - return value, err -} diff --git a/cache/namedmemcache/named_cache_test.go b/cache/namedmemcache/named_cache_test.go deleted file mode 100644 index 9feddb11f..000000000 --- a/cache/namedmemcache/named_cache_test.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2018 The Hugo Authors. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package namedmemcache - -import ( - "fmt" - "sync" - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestNamedCache(t *testing.T) { - t.Parallel() - c := qt.New(t) - - cache := New() - - counter := 0 - create := func() (interface{}, error) { - counter++ - return counter, nil - } - - for i := 0; i < 5; i++ { - v1, err := cache.GetOrCreate("a1", create) - c.Assert(err, qt.IsNil) - c.Assert(v1, qt.Equals, 1) - v2, err := cache.GetOrCreate("a2", create) - c.Assert(err, qt.IsNil) - c.Assert(v2, qt.Equals, 2) - } - - cache.Clear() - - v3, err := cache.GetOrCreate("a2", create) - c.Assert(err, qt.IsNil) - c.Assert(v3, qt.Equals, 3) -} - -func TestNamedCacheConcurrent(t *testing.T) { - t.Parallel() - - c := qt.New(t) - - var wg sync.WaitGroup - - cache := New() - - create := func(i int) func() (interface{}, error) { - return func() (interface{}, error) { - return i, nil - } - } - - for i := 0; i < 10; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < 100; j++ { - id := fmt.Sprintf("id%d", j) - v, err := cache.GetOrCreate(id, create(j)) - c.Assert(err, qt.IsNil) - c.Assert(v, qt.Equals, j) - } - }() - } - wg.Wait() -} diff --git a/cache/partitioned_lazy_cache.go b/cache/partitioned_lazy_cache.go deleted file mode 100644 index 31e66e127..000000000 --- a/cache/partitioned_lazy_cache.go +++ /dev/null @@ -1,99 +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 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) -} - -// Lazy represents a lazily loaded cache. -type Lazy struct { - initSync sync.Once - initErr error - cache map[string]interface{} - load func() (map[string]interface{}, error) -} - -// NewLazy creates a lazy cache with the given load func. -func NewLazy(load func() (map[string]interface{}, error)) *Lazy { - return &Lazy{load: load} -} - -func (l *Lazy) init() error { - l.initSync.Do(func() { - c, err := l.load() - l.cache = c - l.initErr = err - - }) - - return l.initErr -} - -// Get initializes the cache if not already initialized, then looks up the -// given key. -func (l *Lazy) Get(key string) (interface{}, bool, error) { - l.init() - if l.initErr != nil { - return nil, false, l.initErr - } - v, found := l.cache[key] - return v, found, nil -} - -// PartitionedLazyCache is a lazily loaded cache paritioned by a supplied string key. -type PartitionedLazyCache struct { - partitions map[string]*Lazy -} - -// NewPartitionedLazyCache creates a new NewPartitionedLazyCache with the supplied -// partitions. -func NewPartitionedLazyCache(partitions ...Partition) *PartitionedLazyCache { - lazyPartitions := make(map[string]*Lazy, len(partitions)) - for _, partition := range partitions { - lazyPartitions[partition.Key] = NewLazy(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 - } - - v, found, err := p.Get(key) - if err != nil { - return nil, err - } - - if found { - return v, nil - } - - return nil, nil - -} diff --git a/cache/partitioned_lazy_cache_test.go b/cache/partitioned_lazy_cache_test.go deleted file mode 100644 index 2c61a6560..000000000 --- a/cache/partitioned_lazy_cache_test.go +++ /dev/null @@ -1,138 +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 cache - -import ( - "errors" - "sync" - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestNewPartitionedLazyCache(t *testing.T) { - t.Parallel() - - c := qt.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") - c.Assert(err, qt.IsNil) - c.Assert(v, qt.Equals, "p1v1") - - v, err = cache.Get("p1", "p2_1") - c.Assert(err, qt.IsNil) - c.Assert(v, qt.IsNil) - - v, err = cache.Get("p1", "p1_nil") - c.Assert(err, qt.IsNil) - c.Assert(v, qt.IsNil) - - v, err = cache.Get("p2", "p2_3") - c.Assert(err, qt.IsNil) - c.Assert(v, qt.Equals, "p2v3") - - v, err = cache.Get("doesnotexist", "p1_1") - c.Assert(err, qt.IsNil) - c.Assert(v, qt.IsNil) - - v, err = cache.Get("p1", "doesnotexist") - c.Assert(err, qt.IsNil) - c.Assert(v, qt.IsNil) - - errorP := Partition{ - Key: "p3", - Load: func() (map[string]interface{}, error) { - return nil, errors.New("Failed") - }, - } - - cache = NewPartitionedLazyCache(errorP) - - v, err = cache.Get("p1", "doesnotexist") - c.Assert(err, qt.IsNil) - c.Assert(v, qt.IsNil) - - _, err = cache.Get("p3", "doesnotexist") - c.Assert(err, qt.Not(qt.IsNil)) - -} - -func TestConcurrentPartitionedLazyCache(t *testing.T) { - t.Parallel() - - c := qt.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") - c.Assert(err, qt.IsNil) - c.Assert(v, qt.Equals, "p1v1") - } - }() - } - wg.Wait() -} diff --git a/codegen/methods.go b/codegen/methods.go deleted file mode 100644 index ed8dba923..000000000 --- a/codegen/methods.go +++ /dev/null @@ -1,548 +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" - "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 - - var 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 := 0; i < t.NumMethod(); i++ { - - 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 := 0; i < numIn; i++ { - 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 := 0; i < numOut; i++ { - 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 := 0; i < len(args); i++ { - 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 := 0; i < len(args); i++ { - 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 := 0; i < len(outs); i++ { - 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 -// matchin 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 - var 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 = append(pkgImports[:i], pkgImports[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 { - // Embedded interface - methodNames = append( - methodNames, - collectMethodsRecursive( - pkg, - ident.Obj.Decl.(*ast.TypeSpec).Type.(*ast.InterfaceType).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]interface{}{} - 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 77399f4e4..000000000 --- a/codegen/methods_test.go +++ /dev/null @@ -1,100 +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() interface{} - Method8() herrors.ErrorContext - method2() - method9() os.FileInfo -} - -type IEOnly interface { - IEmbed -} diff --git a/commands/check.go b/commands/check.go deleted file mode 100644 index f36f23969..000000000 --- a/commands/check.go +++ /dev/null @@ -1,34 +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. - -// +build !darwin - -package commands - -import ( - "github.com/spf13/cobra" -) - -var _ cmder = (*checkCmd)(nil) - -type checkCmd struct { - *baseCmd -} - -func newCheckCmd() *checkCmd { - return &checkCmd{baseCmd: &baseCmd{cmd: &cobra.Command{ - Use: "check", - Short: "Contains some verification checks", - }, - }} -} diff --git a/commands/check_darwin.go b/commands/check_darwin.go deleted file mode 100644 index 9291be84c..000000000 --- a/commands/check_darwin.go +++ /dev/null @@ -1,36 +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 commands - -import ( - "github.com/spf13/cobra" -) - -var _ cmder = (*checkCmd)(nil) - -type checkCmd struct { - *baseCmd -} - -func newCheckCmd() *checkCmd { - cc := &checkCmd{baseCmd: &baseCmd{cmd: &cobra.Command{ - Use: "check", - Short: "Contains some verification checks", - }, - }} - - cc.cmd.AddCommand(newLimitCmd().getCommand()) - - return cc -} diff --git a/commands/commandeer.go b/commands/commandeer.go deleted file mode 100644 index 55526a857..000000000 --- a/commands/commandeer.go +++ /dev/null @@ -1,414 +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 commands - -import ( - "bytes" - "errors" - "sync" - - "golang.org/x/sync/semaphore" - - "github.com/gohugoio/hugo/modules" - - "io/ioutil" - - "github.com/gohugoio/hugo/common/herrors" - "github.com/gohugoio/hugo/common/hugo" - - jww "github.com/spf13/jwalterweatherman" - - "os" - "path/filepath" - "regexp" - "time" - - "github.com/gohugoio/hugo/common/loggers" - "github.com/gohugoio/hugo/config" - - "github.com/spf13/cobra" - - "github.com/gohugoio/hugo/hugolib" - "github.com/spf13/afero" - - "github.com/bep/debounce" - "github.com/gohugoio/hugo/common/types" - "github.com/gohugoio/hugo/deps" - "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/hugofs" - "github.com/gohugoio/hugo/langs" -) - -type commandeerHugoState struct { - *deps.DepsCfg - hugoSites *hugolib.HugoSites - fsCreate sync.Once - created chan struct{} -} - -type commandeer struct { - *commandeerHugoState - - logger *loggers.Logger - - // Currently only set when in "fast render mode". But it seems to - // be fast enough that we could maybe just add it for all server modes. - changeDetector *fileChangeDetector - - // We need to reuse this on server rebuilds. - destinationFs afero.Fs - - h *hugoBuilderCommon - ftch flagsToConfigHandler - - visitedURLs *types.EvictingStringQueue - - doWithCommandeer func(c *commandeer) error - - // We watch these for changes. - configFiles []string - - // Used in cases where we get flooded with events in server mode. - debounce func(f func()) - - serverPorts []int - languagesConfigured bool - languages langs.Languages - doLiveReload bool - fastRenderMode bool - showErrorInBrowser bool - - configured bool - paused bool - - fullRebuildSem *semaphore.Weighted - - // Any error from the last build. - buildErr error -} - -func newCommandeerHugoState() *commandeerHugoState { - return &commandeerHugoState{ - created: make(chan struct{}), - } -} - -func (c *commandeerHugoState) hugo() *hugolib.HugoSites { - <-c.created - return c.hugoSites -} - -func (c *commandeer) errCount() int { - return int(c.logger.ErrorCounter.Count()) -} - -func (c *commandeer) getErrorWithContext() interface{} { - errCount := c.errCount() - - if errCount == 0 { - return nil - } - - m := make(map[string]interface{}) - - m["Error"] = errors.New(removeErrorPrefixFromLog(c.logger.Errors())) - m["Version"] = hugo.BuildVersionString() - - fe := herrors.UnwrapErrorWithFileContext(c.buildErr) - if fe != nil { - m["File"] = fe - } - - if c.h.verbose { - var b bytes.Buffer - herrors.FprintStackTrace(&b, c.buildErr) - m["StackTrace"] = b.String() - } - - return m -} - -func (c *commandeer) Set(key string, value interface{}) { - if c.configured { - panic("commandeer cannot be changed") - } - c.Cfg.Set(key, value) -} - -func (c *commandeer) initFs(fs *hugofs.Fs) error { - c.destinationFs = fs.Destination - c.DepsCfg.Fs = fs - - return nil -} - -func newCommandeer(mustHaveConfigFile, running bool, h *hugoBuilderCommon, f flagsToConfigHandler, doWithCommandeer func(c *commandeer) error, subCmdVs ...*cobra.Command) (*commandeer, error) { - - var rebuildDebouncer func(f func()) - if running { - // The time value used is tested with mass content replacements in a fairly big Hugo site. - // It is better to wait for some seconds in those cases rather than get flooded - // with rebuilds. - rebuildDebouncer = debounce.New(4 * time.Second) - } - - c := &commandeer{ - h: h, - ftch: f, - commandeerHugoState: newCommandeerHugoState(), - doWithCommandeer: doWithCommandeer, - visitedURLs: types.NewEvictingStringQueue(10), - debounce: rebuildDebouncer, - fullRebuildSem: semaphore.NewWeighted(1), - // This will be replaced later, but we need something to log to before the configuration is read. - logger: loggers.NewLogger(jww.LevelError, jww.LevelError, os.Stdout, ioutil.Discard, running), - } - - return c, c.loadConfig(mustHaveConfigFile, running) -} - -type fileChangeDetector struct { - sync.Mutex - current map[string]string - prev map[string]string - - irrelevantRe *regexp.Regexp -} - -func (f *fileChangeDetector) OnFileClose(name, md5sum string) { - f.Lock() - defer f.Unlock() - f.current[name] = md5sum -} - -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.filterIrrelevant(c) -} - -func (f *fileChangeDetector) filterIrrelevant(in []string) []string { - var filtered []string - for _, v := range in { - if !f.irrelevantRe.MatchString(v) { - filtered = append(filtered, v) - } - } - return filtered -} - -func (f *fileChangeDetector) PrepareNew() { - if f == nil { - return - } - - f.Lock() - defer f.Unlock() - - if f.current == nil { - f.current = make(map[string]string) - f.prev = make(map[string]string) - return - } - - f.prev = make(map[string]string) - for k, v := range f.current { - f.prev[k] = v - } - f.current = make(map[string]string) -} - -func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error { - - if c.DepsCfg == nil { - c.DepsCfg = &deps.DepsCfg{} - } - - if c.logger != nil { - // Truncate the error log if this is a reload. - c.logger.Reset() - } - - cfg := c.DepsCfg - c.configured = false - cfg.Running = running - - var dir string - if c.h.source != "" { - dir, _ = filepath.Abs(c.h.source) - } else { - dir, _ = os.Getwd() - } - - var sourceFs afero.Fs = hugofs.Os - if c.DepsCfg.Fs != nil { - sourceFs = c.DepsCfg.Fs.Source - } - - environment := c.h.getEnvironment(running) - - doWithConfig := func(cfg config.Provider) error { - - if c.ftch != nil { - c.ftch.flagsToConfig(cfg) - } - - cfg.Set("workingDir", dir) - cfg.Set("environment", environment) - return nil - } - - doWithCommandeer := func(cfg config.Provider) error { - c.Cfg = cfg - if c.doWithCommandeer == nil { - return nil - } - err := c.doWithCommandeer(c) - return err - } - - configPath := c.h.source - if configPath == "" { - configPath = dir - } - config, configFiles, err := hugolib.LoadConfig( - hugolib.ConfigSourceDescriptor{ - Fs: sourceFs, - Path: configPath, - WorkingDir: dir, - Filename: c.h.cfgFile, - AbsConfigDir: c.h.getConfigDir(dir), - Environ: os.Environ(), - Environment: environment}, - doWithCommandeer, - doWithConfig) - - if err != nil { - if mustHaveConfigFile { - return err - } - if err != hugolib.ErrNoConfigFile && !modules.IsNotExist(err) { - return err - } - - } - - c.configFiles = configFiles - - if l, ok := c.Cfg.Get("languagesSorted").(langs.Languages); ok { - c.languagesConfigured = true - c.languages = l - } - - // Set some commonly used flags - c.doLiveReload = running && !c.Cfg.GetBool("disableLiveReload") - c.fastRenderMode = c.doLiveReload && !c.Cfg.GetBool("disableFastRender") - c.showErrorInBrowser = c.doLiveReload && !c.Cfg.GetBool("disableBrowserError") - - // This is potentially double work, but we need to do this one more time now - // that all the languages have been configured. - if c.doWithCommandeer != nil { - if err := c.doWithCommandeer(c); err != nil { - return err - } - } - - logger, err := c.createLogger(config, running) - if err != nil { - return err - } - - cfg.Logger = logger - c.logger = logger - - createMemFs := config.GetBool("renderToMemory") - - if createMemFs { - // Rendering to memoryFS, publish to Root regardless of publishDir. - config.Set("publishDir", "/") - } - - c.fsCreate.Do(func() { - fs := hugofs.NewFrom(sourceFs, config) - - if c.destinationFs != nil { - // Need to reuse the destination on server rebuilds. - fs.Destination = c.destinationFs - } else if createMemFs { - // Hugo writes the output to memory instead of the disk. - fs.Destination = new(afero.MemMapFs) - } - - if c.fastRenderMode { - // For now, fast render mode only. It should, however, be fast enough - // for the full variant, too. - 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$`), - } - - changeDetector.PrepareNew() - fs.Destination = hugofs.NewHashingFs(fs.Destination, changeDetector) - c.changeDetector = changeDetector - } - - if c.Cfg.GetBool("logPathWarnings") { - fs.Destination = hugofs.NewCreateCountingFs(fs.Destination) - } - - // To debug hard-to-find path issues. - //fs.Destination = hugofs.NewStacktracerFs(fs.Destination, `fr/fr`) - - err = c.initFs(fs) - if err != nil { - close(c.created) - return - } - - var h *hugolib.HugoSites - - h, err = hugolib.NewHugoSites(*c.DepsCfg) - c.hugoSites = h - close(c.created) - - }) - - if err != nil { - return err - } - - cacheDir, err := helpers.GetCacheDir(sourceFs, config) - if err != nil { - return err - } - config.Set("cacheDir", cacheDir) - - cfg.Logger.INFO.Println("Using config file:", config.ConfigFileUsed()) - - return nil - -} diff --git a/commands/commands.go b/commands/commands.go deleted file mode 100644 index 3096e1fe0..000000000 --- a/commands/commands.go +++ /dev/null @@ -1,312 +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 commands - -import ( - "os" - - "github.com/gohugoio/hugo/hugolib/paths" - - "github.com/gohugoio/hugo/common/hugo" - "github.com/gohugoio/hugo/common/loggers" - "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/helpers" - "github.com/spf13/cobra" -) - -type commandsBuilder struct { - hugoBuilderCommon - - commands []cmder -} - -func newCommandsBuilder() *commandsBuilder { - return &commandsBuilder{} -} - -func (b *commandsBuilder) addCommands(commands ...cmder) *commandsBuilder { - b.commands = append(b.commands, commands...) - return b -} - -func (b *commandsBuilder) addAll() *commandsBuilder { - b.addCommands( - b.newServerCmd(), - newVersionCmd(), - newEnvCmd(), - newConfigCmd(), - newCheckCmd(), - newDeployCmd(), - newConvertCmd(), - b.newNewCmd(), - newListCmd(), - newImportCmd(), - newGenCmd(), - createReleaser(), - b.newModCmd(), - ) - - return b -} - -func (b *commandsBuilder) build() *hugoCmd { - h := b.newHugoCmd() - addCommands(h.getCommand(), b.commands...) - return h -} - -func addCommands(root *cobra.Command, commands ...cmder) { - for _, command := range commands { - cmd := command.getCommand() - if cmd == nil { - continue - } - root.AddCommand(cmd) - } -} - -type baseCmd struct { - cmd *cobra.Command -} - -var _ commandsBuilderGetter = (*baseBuilderCmd)(nil) - -// Used in tests. -type commandsBuilderGetter interface { - getCommandsBuilder() *commandsBuilder -} -type baseBuilderCmd struct { - *baseCmd - *commandsBuilder -} - -func (b *baseBuilderCmd) getCommandsBuilder() *commandsBuilder { - return b.commandsBuilder -} - -func (c *baseCmd) getCommand() *cobra.Command { - return c.cmd -} - -func newBaseCmd(cmd *cobra.Command) *baseCmd { - return &baseCmd{cmd: cmd} -} - -func (b *commandsBuilder) newBuilderCmd(cmd *cobra.Command) *baseBuilderCmd { - bcmd := &baseBuilderCmd{commandsBuilder: b, baseCmd: &baseCmd{cmd: cmd}} - bcmd.hugoBuilderCommon.handleFlags(cmd) - return bcmd -} - -func (c *baseCmd) flagsToConfig(cfg config.Provider) { - initializeFlags(c.cmd, cfg) -} - -type hugoCmd struct { - *baseBuilderCmd - - // Need to get the sites once built. - c *commandeer -} - -var _ cmder = (*nilCommand)(nil) - -type nilCommand struct { -} - -func (c *nilCommand) getCommand() *cobra.Command { - return nil -} - -func (c *nilCommand) flagsToConfig(cfg config.Provider) { - -} - -func (b *commandsBuilder) newHugoCmd() *hugoCmd { - cc := &hugoCmd{} - - cc.baseBuilderCmd = b.newBuilderCmd(&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 { - cfgInit := func(c *commandeer) error { - if cc.buildWatch { - c.Set("disableLiveReload", true) - } - return nil - } - - c, err := initializeConfig(true, cc.buildWatch, &cc.hugoBuilderCommon, cc, cfgInit) - if err != nil { - return err - } - cc.c = c - - return c.build() - }, - }) - - cc.cmd.PersistentFlags().StringVar(&cc.cfgFile, "config", "", "config file (default is path/config.yaml|json|toml)") - cc.cmd.PersistentFlags().StringVar(&cc.cfgDir, "configDir", "config", "config dir") - cc.cmd.PersistentFlags().BoolVar(&cc.quiet, "quiet", false, "build in quiet mode") - - // Set bash-completion - _ = cc.cmd.PersistentFlags().SetAnnotation("config", cobra.BashCompFilenameExt, config.ValidConfigFileExtensions) - - cc.cmd.PersistentFlags().BoolVarP(&cc.verbose, "verbose", "v", false, "verbose output") - cc.cmd.PersistentFlags().BoolVarP(&cc.debug, "debug", "", false, "debug output") - cc.cmd.PersistentFlags().BoolVar(&cc.logging, "log", false, "enable Logging") - cc.cmd.PersistentFlags().StringVar(&cc.logFile, "logFile", "", "log File path (if set, logging enabled automatically)") - cc.cmd.PersistentFlags().BoolVar(&cc.verboseLog, "verboseLog", false, "verbose logging") - - cc.cmd.Flags().BoolVarP(&cc.buildWatch, "watch", "w", false, "watch filesystem for changes and recreate as needed") - - cc.cmd.Flags().Bool("renderToMemory", false, "render to memory (only useful for benchmark testing)") - - // Set bash-completion - _ = cc.cmd.PersistentFlags().SetAnnotation("logFile", cobra.BashCompFilenameExt, []string{}) - - cc.cmd.SetGlobalNormalizationFunc(helpers.NormalizeHugoFlags) - cc.cmd.SilenceUsage = true - - return cc -} - -type hugoBuilderCommon struct { - source string - baseURL string - environment string - - buildWatch bool - - gc bool - - // Profile flags (for debugging of performance problems) - cpuprofile string - memprofile string - mutexprofile string - traceprofile string - - // TODO(bep) var vs string - logging bool - verbose bool - verboseLog bool - debug bool - quiet bool - - cfgFile string - cfgDir string - logFile string -} - -func (cc *hugoBuilderCommon) getConfigDir(baseDir string) string { - if cc.cfgDir != "" { - return paths.AbsPathify(baseDir, cc.cfgDir) - } - - if v, found := os.LookupEnv("HUGO_CONFIGDIR"); found { - return paths.AbsPathify(baseDir, v) - } - - return paths.AbsPathify(baseDir, "config") -} - -func (cc *hugoBuilderCommon) getEnvironment(isServer bool) string { - if cc.environment != "" { - return cc.environment - } - - if v, found := os.LookupEnv("HUGO_ENVIRONMENT"); found { - return v - } - - if isServer { - return hugo.EnvironmentDevelopment - } - - return hugo.EnvironmentProduction -} - -func (cc *hugoBuilderCommon) handleCommonBuilderFlags(cmd *cobra.Command) { - cmd.PersistentFlags().StringVarP(&cc.source, "source", "s", "", "filesystem path to read files relative from") - cmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{}) - cmd.PersistentFlags().StringVarP(&cc.environment, "environment", "e", "", "build environment") - cmd.PersistentFlags().StringP("themesDir", "", "", "filesystem path to themes directory") - cmd.PersistentFlags().BoolP("ignoreVendor", "", false, "ignores any _vendor directory") -} - -func (cc *hugoBuilderCommon) handleFlags(cmd *cobra.Command) { - cc.handleCommonBuilderFlags(cmd) - 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().StringP("contentDir", "c", "", "filesystem path to content directory") - cmd.Flags().StringP("layoutDir", "l", "", "filesystem path to layout directory") - cmd.Flags().StringP("cacheDir", "", "", "filesystem path to cache directory. Defaults: $TMPDIR/hugo_cache/") - cmd.Flags().BoolP("ignoreCache", "", false, "ignores the cache directory") - cmd.Flags().StringP("destination", "d", "", "filesystem path to write files to") - cmd.Flags().StringSliceP("theme", "t", []string{}, "themes to use (located in /themes/THEMENAME/)") - cmd.Flags().StringVarP(&cc.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(&cc.gc, "gc", false, "enable to run some cleanup tasks (remove unused cache files) after the build") - - 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().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().BoolP("i18n-warnings", "", false, "print missing translations") - cmd.Flags().BoolP("path-warnings", "", false, "print warnings on duplicate target paths etc.") - cmd.Flags().StringVarP(&cc.cpuprofile, "profile-cpu", "", "", "write cpu profile to `file`") - cmd.Flags().StringVarP(&cc.memprofile, "profile-mem", "", "", "write memory profile to `file`") - cmd.Flags().StringVarP(&cc.mutexprofile, "profile-mutex", "", "", "write Mutex profile to `file`") - cmd.Flags().StringVarP(&cc.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.Flags().Bool("minify", false, "minify any supported output format (HTML, XML 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 checkErr(logger *loggers.Logger, err error, s ...string) { - if err == nil { - return - } - if len(s) == 0 { - logger.CRITICAL.Println(err) - return - } - for _, message := range s { - logger.ERROR.Println(message) - } - logger.ERROR.Println(err) -} diff --git a/commands/commands_test.go b/commands/commands_test.go deleted file mode 100644 index 565736793..000000000 --- a/commands/commands_test.go +++ /dev/null @@ -1,286 +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 commands - -import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - "testing" - - "github.com/gohugoio/hugo/common/types" - - "github.com/spf13/cobra" - "github.com/spf13/viper" - - qt "github.com/frankban/quicktest" -) - -func TestExecute(t *testing.T) { - - c := qt.New(t) - - dir, err := createSimpleTestSite(t, testSiteConfig{}) - c.Assert(err, qt.IsNil) - - defer func() { - os.RemoveAll(dir) - }() - - resp := Execute([]string{"-s=" + dir}) - c.Assert(resp.Err, qt.IsNil) - result := resp.Result - c.Assert(len(result.Sites) == 1, qt.Equals, true) - c.Assert(len(result.Sites[0].RegularPages()) == 1, qt.Equals, true) -} - -func TestCommandsPersistentFlags(t *testing.T) { - c := qt.New(t) - - noOpRunE := func(cmd *cobra.Command, args []string) error { - return nil - } - - tests := []struct { - args []string - check func(command []cmder) - }{{[]string{"server", - "--config=myconfig.toml", - "--configDir=myconfigdir", - "--contentDir=mycontent", - "--disableKinds=page,home", - "--environment=testing", - "--configDir=myconfigdir", - "--layoutDir=mylayouts", - "--theme=mytheme", - "--gc", - "--themesDir=mythemes", - "--cleanDestinationDir", - "--navigateToChanged", - "--disableLiveReload", - "--noHTTPCache", - "--i18n-warnings", - "--destination=/tmp/mydestination", - "-b=https://example.com/b/", - "--port=1366", - "--renderToDisk", - "--source=mysource", - "--path-warnings", - }, func(commands []cmder) { - var sc *serverCmd - for _, command := range commands { - if b, ok := command.(commandsBuilderGetter); ok { - v := b.getCommandsBuilder().hugoBuilderCommon - c.Assert(v.cfgFile, qt.Equals, "myconfig.toml") - c.Assert(v.cfgDir, qt.Equals, "myconfigdir") - c.Assert(v.source, qt.Equals, "mysource") - c.Assert(v.baseURL, qt.Equals, "https://example.com/b/") - } - - if srvCmd, ok := command.(*serverCmd); ok { - sc = srvCmd - } - } - - c.Assert(sc, qt.Not(qt.IsNil)) - c.Assert(sc.navigateToChanged, qt.Equals, true) - c.Assert(sc.disableLiveReload, qt.Equals, true) - c.Assert(sc.noHTTPCache, qt.Equals, true) - c.Assert(sc.renderToDisk, qt.Equals, true) - c.Assert(sc.serverPort, qt.Equals, 1366) - c.Assert(sc.environment, qt.Equals, "testing") - - cfg := viper.New() - sc.flagsToConfig(cfg) - c.Assert(cfg.GetString("publishDir"), qt.Equals, "/tmp/mydestination") - c.Assert(cfg.GetString("contentDir"), qt.Equals, "mycontent") - c.Assert(cfg.GetString("layoutDir"), qt.Equals, "mylayouts") - c.Assert(cfg.GetStringSlice("theme"), qt.DeepEquals, []string{"mytheme"}) - c.Assert(cfg.GetString("themesDir"), qt.Equals, "mythemes") - c.Assert(cfg.GetString("baseURL"), qt.Equals, "https://example.com/b/") - - c.Assert(cfg.Get("disableKinds"), qt.DeepEquals, []string{"page", "home"}) - - c.Assert(cfg.GetBool("gc"), qt.Equals, true) - - // The flag is named path-warnings - c.Assert(cfg.GetBool("logPathWarnings"), qt.Equals, true) - - // The flag is named i18n-warnings - c.Assert(cfg.GetBool("logI18nWarnings"), qt.Equals, true) - - }}} - - for _, test := range tests { - b := newCommandsBuilder() - root := b.addAll().build() - - for _, c := range b.commands { - if c.getCommand() == nil { - continue - } - // We are only intereseted in the flag handling here. - c.getCommand().RunE = noOpRunE - } - rootCmd := root.getCommand() - rootCmd.SetArgs(test.args) - c.Assert(rootCmd.Execute(), qt.IsNil) - test.check(b.commands) - } - -} - -func TestCommandsExecute(t *testing.T) { - - c := qt.New(t) - - dir, err := createSimpleTestSite(t, testSiteConfig{}) - c.Assert(err, qt.IsNil) - - dirOut, err := ioutil.TempDir("", "hugo-cli-out") - c.Assert(err, qt.IsNil) - - defer func() { - os.RemoveAll(dir) - os.RemoveAll(dirOut) - }() - - sourceFlag := fmt.Sprintf("-s=%s", dir) - - tests := []struct { - commands []string - flags []string - expectErrToContain string - }{ - // TODO(bep) permission issue on my OSX? "operation not permitted" {[]string{"check", "ulimit"}, nil, false}, - {[]string{"env"}, nil, ""}, - {[]string{"version"}, nil, ""}, - // no args = hugo build - {nil, []string{sourceFlag}, ""}, - {nil, []string{sourceFlag, "--renderToMemory"}, ""}, - {[]string{"config"}, []string{sourceFlag}, ""}, - {[]string{"convert", "toTOML"}, []string{sourceFlag, "-o=" + filepath.Join(dirOut, "toml")}, ""}, - {[]string{"convert", "toYAML"}, []string{sourceFlag, "-o=" + filepath.Join(dirOut, "yaml")}, ""}, - {[]string{"convert", "toJSON"}, []string{sourceFlag, "-o=" + filepath.Join(dirOut, "json")}, ""}, - {[]string{"gen", "autocomplete"}, []string{"--completionfile=" + filepath.Join(dirOut, "autocomplete.txt")}, ""}, - {[]string{"gen", "chromastyles"}, []string{"--style=manni"}, ""}, - {[]string{"gen", "doc"}, []string{"--dir=" + filepath.Join(dirOut, "doc")}, ""}, - {[]string{"gen", "man"}, []string{"--dir=" + filepath.Join(dirOut, "man")}, ""}, - {[]string{"list", "drafts"}, []string{sourceFlag}, ""}, - {[]string{"list", "expired"}, []string{sourceFlag}, ""}, - {[]string{"list", "future"}, []string{sourceFlag}, ""}, - {[]string{"new", "new-page.md"}, []string{sourceFlag}, ""}, - {[]string{"new", "site", filepath.Join(dirOut, "new-site")}, nil, ""}, - {[]string{"unknowncommand"}, nil, "unknown command"}, - // TODO(bep) cli refactor fix https://github.com/gohugoio/hugo/issues/4450 - //{[]string{"new", "theme", filepath.Join(dirOut, "new-theme")}, nil,false}, - } - - for _, test := range tests { - b := newCommandsBuilder().addAll().build() - hugoCmd := b.getCommand() - test.flags = append(test.flags, "--quiet") - hugoCmd.SetArgs(append(test.commands, test.flags...)) - - // TODO(bep) capture output and add some simple asserts - // TODO(bep) misspelled subcommands does not return an error. We should investigate this - // but before that, check for "Error: unknown command". - - _, err := hugoCmd.ExecuteC() - if test.expectErrToContain != "" { - c.Assert(err, qt.Not(qt.IsNil)) - c.Assert(err.Error(), qt.Contains, test.expectErrToContain) - } else { - c.Assert(err, qt.IsNil) - } - - // Assert that we have not left any development debug artifacts in - // the code. - if b.c != nil { - _, ok := b.c.destinationFs.(types.DevMarker) - c.Assert(ok, qt.Equals, false) - } - - } - -} - -type testSiteConfig struct { - configTOML string - contentDir string -} - -func createSimpleTestSite(t *testing.T, cfg testSiteConfig) (string, error) { - d, e := ioutil.TempDir("", "hugo-cli") - if e != nil { - return "", e - } - - cfgStr := ` - -baseURL = "https://example.org" -title = "Hugo Commands" - -` - - contentDir := "content" - - if cfg.configTOML != "" { - cfgStr = cfg.configTOML - } - if cfg.contentDir != "" { - contentDir = cfg.contentDir - } - - // Just the basic. These are for CLI tests, not site testing. - writeFile(t, filepath.Join(d, "config.toml"), cfgStr) - - writeFile(t, filepath.Join(d, contentDir, "p1.md"), ` ---- -title: "P1" -weight: 1 ---- - -Content - -`) - - writeFile(t, filepath.Join(d, "layouts", "_default", "single.html"), ` - -Single: {{ .Title }} - -`) - - writeFile(t, filepath.Join(d, "layouts", "_default", "list.html"), ` - -List: {{ .Title }} -Environment: {{ hugo.Environment }} - -`) - - return d, nil - -} - -func writeFile(t *testing.T, filename, content string) { - must(t, os.MkdirAll(filepath.Dir(filename), os.FileMode(0755))) - must(t, ioutil.WriteFile(filename, []byte(content), os.FileMode(0755))) -} - -func must(t *testing.T, err error) { - if err != nil { - t.Fatal(err) - } -} diff --git a/commands/config.go b/commands/config.go deleted file mode 100644 index 72c2a0d97..000000000 --- a/commands/config.go +++ /dev/null @@ -1,147 +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.Print the version number of Hug - -package commands - -import ( - "encoding/json" - "os" - "reflect" - "regexp" - "sort" - "strings" - - "github.com/gohugoio/hugo/parser" - "github.com/gohugoio/hugo/parser/metadecoders" - - "github.com/gohugoio/hugo/modules" - - "github.com/spf13/cobra" - jww "github.com/spf13/jwalterweatherman" - "github.com/spf13/viper" -) - -var _ cmder = (*configCmd)(nil) - -type configCmd struct { - hugoBuilderCommon - *baseCmd -} - -func newConfigCmd() *configCmd { - cc := &configCmd{} - cc.baseCmd = newBaseCmd(&cobra.Command{ - Use: "config", - Short: "Print the site configuration", - Long: `Print the site configuration, both default and custom settings.`, - RunE: cc.printConfig, - }) - - cc.cmd.PersistentFlags().StringVarP(&cc.source, "source", "s", "", "filesystem path to read files relative from") - - printMountsCmd := &cobra.Command{ - Use: "mounts", - Short: "Print the configured file mounts", - RunE: cc.printMounts, - } - - cc.cmd.AddCommand(printMountsCmd) - - return cc -} - -func (c *configCmd) printMounts(cmd *cobra.Command, args []string) error { - cfg, err := initializeConfig(true, false, &c.hugoBuilderCommon, c, nil) - if err != nil { - return err - } - - allModules := cfg.Cfg.Get("allmodules").(modules.Modules) - - for _, m := range allModules { - if err := parser.InterfaceToConfig(&modMounts{m: m}, metadecoders.JSON, os.Stdout); err != nil { - return err - } - } - return nil -} - -func (c *configCmd) printConfig(cmd *cobra.Command, args []string) error { - cfg, err := initializeConfig(true, false, &c.hugoBuilderCommon, c, nil) - if err != nil { - return err - } - - allSettings := cfg.Cfg.(*viper.Viper).AllSettings() - - // We need to clean up this, but we store objects in the config that - // isn't really interesting to the end user, so filter these. - ignoreKeysRe := regexp.MustCompile("client|sorted|filecacheconfigs|allmodules|multilingual") - - separator := ": " - - if len(cfg.configFiles) > 0 && strings.HasSuffix(cfg.configFiles[0], ".toml") { - separator = " = " - } - - var keys []string - for k := range allSettings { - if ignoreKeysRe.MatchString(k) { - continue - } - 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 -} - -type modMounts struct { - m modules.Module -} - -type modMount struct { - Source string `json:"source"` - Target string `json:"target"` - Lang string `json:"lang,omitempty"` -} - -func (m *modMounts) MarshalJSON() ([]byte, error) { - var mounts []modMount - - for _, mount := range m.m.Mounts() { - mounts = append(mounts, modMount{ - Source: mount.Source, - Target: mount.Target, - Lang: mount.Lang, - }) - } - - return json.Marshal(&struct { - Path string `json:"path"` - Dir string `json:"dir"` - Mounts []modMount `json:"mounts"` - }{ - Path: m.m.Path(), - Dir: m.m.Dir(), - Mounts: mounts, - }) -} diff --git a/commands/convert.go b/commands/convert.go deleted file mode 100644 index e4ff1ac61..000000000 --- a/commands/convert.go +++ /dev/null @@ -1,250 +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 commands - -import ( - "bytes" - "fmt" - "io" - "strings" - "time" - - "github.com/gohugoio/hugo/resources/page" - - "github.com/gohugoio/hugo/hugofs" - - "github.com/gohugoio/hugo/helpers" - - "github.com/gohugoio/hugo/parser" - "github.com/gohugoio/hugo/parser/metadecoders" - "github.com/gohugoio/hugo/parser/pageparser" - - "github.com/pkg/errors" - - "github.com/gohugoio/hugo/hugolib" - - "path/filepath" - - "github.com/spf13/cobra" -) - -var ( - _ cmder = (*convertCmd)(nil) -) - -type convertCmd struct { - hugoBuilderCommon - - outputDir string - unsafe bool - - *baseCmd -} - -func newConvertCmd() *convertCmd { - cc := &convertCmd{} - - cc.baseCmd = newBaseCmd(&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, - }) - - cc.cmd.AddCommand( - &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.`, - RunE: func(cmd *cobra.Command, args []string) error { - return cc.convertContents(metadecoders.JSON) - }, - }, - &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.`, - RunE: func(cmd *cobra.Command, args []string) error { - return cc.convertContents(metadecoders.TOML) - }, - }, - &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.`, - RunE: func(cmd *cobra.Command, args []string) error { - return cc.convertContents(metadecoders.YAML) - }, - }, - ) - - cc.cmd.PersistentFlags().StringVarP(&cc.outputDir, "output", "o", "", "filesystem path to write files to") - cc.cmd.PersistentFlags().StringVarP(&cc.source, "source", "s", "", "filesystem path to read files relative from") - cc.cmd.PersistentFlags().BoolVar(&cc.unsafe, "unsafe", false, "enable less safe operations, please backup first") - cc.cmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{}) - - return cc -} - -func (cc *convertCmd) convertContents(format metadecoders.Format) error { - if cc.outputDir == "" && !cc.unsafe { - return newUserError("Unsafe operation not allowed, use --unsafe or set a different output path") - } - - c, err := initializeConfig(true, false, &cc.hugoBuilderCommon, cc, nil) - if err != nil { - return err - } - - c.Cfg.Set("buildDrafts", true) - - h, err := hugolib.NewHugoSites(*c.DepsCfg) - if err != nil { - return err - } - - if err := h.Build(hugolib.BuildCfg{SkipRender: true}); err != nil { - return err - } - - site := h.Sites[0] - - site.Log.FEEDBACK.Println("processing", len(site.AllPages()), "content files") - for _, p := range site.AllPages() { - if err := cc.convertAndSavePage(p, site, format); err != nil { - return err - } - } - return nil -} - -func (cc *convertCmd) 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 := cc.convertAndSavePage(r.(page.Page), site, targetFormat); err != nil { - return err - } - } - - if p.File().IsZero() { - // No content file. - return nil - } - - errMsg := fmt.Errorf("Error processing file %q", p.Path()) - - site.Log.INFO.Println("Attempting to convert", p.File().Filename()) - - f := p.File() - file, err := f.FileInfo().Meta().Open() - if err != nil { - site.Log.ERROR.Println(errMsg) - file.Close() - return nil - } - - pf, err := parseContentFile(file) - if err != nil { - site.Log.ERROR.Println(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) - } - } - } - - var newContent bytes.Buffer - err = parser.InterfaceToFrontMatter(pf.frontMatter, targetFormat, &newContent) - if err != nil { - site.Log.ERROR.Println(errMsg) - return err - } - - newContent.Write(pf.content) - - newFilename := p.File().Filename() - - if cc.outputDir != "" { - contentDir := strings.TrimSuffix(newFilename, p.Path()) - contentDir = filepath.Base(contentDir) - - newFilename = filepath.Join(cc.outputDir, contentDir, p.Path()) - } - - fs := hugofs.Os - if err := helpers.WriteToDisk(newFilename, &newContent, fs); err != nil { - return errors.Wrapf(err, "Failed to save file %q:", newFilename) - } - - return nil -} - -type parsedFile struct { - frontMatterFormat metadecoders.Format - frontMatterSource []byte - frontMatter map[string]interface{} - - // Everything after Front Matter - content []byte -} - -func parseContentFile(r io.Reader) (parsedFile, error) { - var pf parsedFile - - psr, err := pageparser.Parse(r, pageparser.Config{}) - if err != nil { - return pf, err - } - - iter := psr.Iterator() - - walkFn := func(item pageparser.Item) bool { - if pf.frontMatterSource != nil { - // The rest is content. - pf.content = psr.Input()[item.Pos:] - // Done - return false - } else if item.IsFrontMatter() { - pf.frontMatterFormat = metadecoders.FormatFromFrontMatterType(item.Type) - pf.frontMatterSource = item.Val - } - return true - - } - - iter.PeekWalk(walkFn) - - metadata, err := metadecoders.Default.UnmarshalToMap(pf.frontMatterSource, pf.frontMatterFormat) - if err != nil { - return pf, err - } - pf.frontMatter = metadata - - return pf, nil - -} diff --git a/commands/deploy.go b/commands/deploy.go deleted file mode 100644 index d74d93709..000000000 --- a/commands/deploy.go +++ /dev/null @@ -1,77 +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 commands - -import ( - "context" - - "github.com/gohugoio/hugo/deploy" - "github.com/spf13/cobra" -) - -var _ cmder = (*deployCmd)(nil) - -// deployCmd supports deploying sites to Cloud providers. -type deployCmd struct { - hugoBuilderCommon - *baseCmd -} - -// TODO: In addition to the "deploy" command, consider adding a "--deploy" -// flag for the default command; this would build the site and then deploy it. -// It's not obvious how to do this; would all of the deploy-specific flags -// have to exist at the top level as well? - -// TODO: The output files change every time "hugo" is executed, it looks -// like because of map order randomization. This means that you can -// run "hugo && hugo deploy" again and again and upload new stuff every time. Is -// this intended? - -func newDeployCmd() *deployCmd { - cc := &deployCmd{} - - cc.baseCmd = newBaseCmd(&cobra.Command{ - Use: "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. -`, - - RunE: func(cmd *cobra.Command, args []string) error { - cfgInit := func(c *commandeer) error { - return nil - } - comm, err := initializeConfig(true, false, &cc.hugoBuilderCommon, cc, cfgInit) - if err != nil { - return err - } - deployer, err := deploy.New(comm.Cfg, comm.hugo().PathSpec.PublishFs) - if err != nil { - return err - } - return deployer.Deploy(context.Background()) - }, - }) - - cc.cmd.Flags().String("target", "", "target deployment from deployments section in config file; defaults to the first one") - cc.cmd.Flags().Bool("confirm", false, "ask for confirmation before making changes to the target") - cc.cmd.Flags().Bool("dryRun", false, "dry run") - cc.cmd.Flags().Bool("force", false, "force upload of all files") - cc.cmd.Flags().Bool("invalidateCDN", true, "invalidate the CDN cache listed in the deployment target") - cc.cmd.Flags().Int("maxDeletes", 256, "maximum # of files to delete, or -1 to disable") - - return cc -} diff --git a/commands/env.go b/commands/env.go deleted file mode 100644 index 76c16b93b..000000000 --- a/commands/env.go +++ /dev/null @@ -1,44 +0,0 @@ -// 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 ( - "runtime" - - "github.com/spf13/cobra" - jww "github.com/spf13/jwalterweatherman" -) - -var _ cmder = (*envCmd)(nil) - -type envCmd struct { - *baseCmd -} - -func newEnvCmd() *envCmd { - return &envCmd{baseCmd: newBaseCmd(&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()) - - return nil - }, - }), - } -} diff --git a/commands/gen.go b/commands/gen.go deleted file mode 100644 index 6878cfe70..000000000 --- a/commands/gen.go +++ /dev/null @@ -1,41 +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 commands - -import ( - "github.com/spf13/cobra" -) - -var _ cmder = (*genCmd)(nil) - -type genCmd struct { - *baseCmd -} - -func newGenCmd() *genCmd { - cc := &genCmd{} - cc.baseCmd = newBaseCmd(&cobra.Command{ - Use: "gen", - Short: "A collection of several useful generators.", - }) - - cc.cmd.AddCommand( - newGenautocompleteCmd().getCommand(), - newGenDocCmd().getCommand(), - newGenManCmd().getCommand(), - createGenDocsHelper().getCommand(), - createGenChromaStyles().getCommand()) - - return cc -} diff --git a/commands/genautocomplete.go b/commands/genautocomplete.go deleted file mode 100644 index b0b98abb4..000000000 --- a/commands/genautocomplete.go +++ /dev/null @@ -1,80 +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 commands - -import ( - "github.com/spf13/cobra" - jww "github.com/spf13/jwalterweatherman" -) - -var _ cmder = (*genautocompleteCmd)(nil) - -type genautocompleteCmd struct { - autocompleteTarget string - - // bash for now (zsh and others will come) - autocompleteType string - - *baseCmd -} - -func newGenautocompleteCmd() *genautocompleteCmd { - cc := &genautocompleteCmd{} - - cc.baseCmd = newBaseCmd(&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 cc.autocompleteType != "bash" { - return newUserError("Only Bash is supported for now") - } - - err := cmd.Root().GenBashCompletionFile(cc.autocompleteTarget) - - if err != nil { - return err - } - - jww.FEEDBACK.Println("Bash completion file for Hugo saved to", cc.autocompleteTarget) - - return nil - }, - }) - - cc.cmd.PersistentFlags().StringVarP(&cc.autocompleteTarget, "completionfile", "", "/etc/bash_completion.d/hugo.sh", "autocompletion file") - cc.cmd.PersistentFlags().StringVarP(&cc.autocompleteType, "type", "", "bash", "autocompletion type (currently only bash supported)") - - // For bash-completion - cc.cmd.PersistentFlags().SetAnnotation("completionfile", cobra.BashCompFilenameExt, []string{}) - - return cc -} diff --git a/commands/genchromastyles.go b/commands/genchromastyles.go deleted file mode 100644 index a2231e56e..000000000 --- a/commands/genchromastyles.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 commands - -import ( - "os" - - "github.com/alecthomas/chroma" - "github.com/alecthomas/chroma/formatters/html" - "github.com/alecthomas/chroma/styles" - "github.com/spf13/cobra" -) - -var ( - _ cmder = (*genChromaStyles)(nil) -) - -type genChromaStyles struct { - style string - highlightStyle string - linesStyle string - *baseCmd -} - -// TODO(bep) highlight -func createGenChromaStyles() *genChromaStyles { - g := &genChromaStyles{ - baseCmd: newBaseCmd(&cobra.Command{ - Use: "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 pygmentsUseClasses is enabled in config. - -See https://help.farbox.com/pygments.html for preview of available styles`, - }), - } - - g.cmd.RunE = func(cmd *cobra.Command, args []string) error { - return g.generate() - } - - g.cmd.PersistentFlags().StringVar(&g.style, "style", "friendly", "highlighter style (see https://help.farbox.com/pygments.html)") - g.cmd.PersistentFlags().StringVar(&g.highlightStyle, "highlightStyle", "bg:#ffffcc", "style used for highlighting lines (see https://github.com/alecthomas/chroma)") - g.cmd.PersistentFlags().StringVar(&g.linesStyle, "linesStyle", "", "style used for line numbers (see https://github.com/alecthomas/chroma)") - - return g -} - -func (g *genChromaStyles) generate() error { - builder := styles.Get(g.style).Builder() - if g.highlightStyle != "" { - builder.Add(chroma.LineHighlight, g.highlightStyle) - } - if g.linesStyle != "" { - builder.Add(chroma.LineNumbers, g.linesStyle) - } - style, err := builder.Build() - if err != nil { - return err - } - formatter := html.New(html.WithClasses()) - formatter.WriteCSS(os.Stdout, style) - return nil -} diff --git a/commands/gendoc.go b/commands/gendoc.go deleted file mode 100644 index 8312191f2..000000000 --- a/commands/gendoc.go +++ /dev/null @@ -1,96 +0,0 @@ -// 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" -) - -var _ cmder = (*genDocCmd)(nil) - -type genDocCmd struct { - gendocdir string - *baseCmd -} - -func newGenDocCmd() *genDocCmd { - const gendocFrontmatterTemplate = `--- -date: %s -title: "%s" -slug: %s -url: %s ---- -` - - cc := &genDocCmd{} - - cc.baseCmd = newBaseCmd(&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(cc.gendocdir, helpers.FilePathSeparator) { - cc.gendocdir += helpers.FilePathSeparator - } - if found, _ := helpers.Exists(cc.gendocdir, hugofs.Os); !found { - jww.FEEDBACK.Println("Directory", cc.gendocdir, "does not exist, creating...") - if err := hugofs.Os.MkdirAll(cc.gendocdir, 0777); err != nil { - return err - } - } - now := time.Now().Format("2006-01-02") - 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", cc.gendocdir, "...") - doc.GenMarkdownTreeCustom(cmd.Root(), cc.gendocdir, prepender, linkHandler) - jww.FEEDBACK.Println("Done.") - - return nil - }, - }) - - cc.cmd.PersistentFlags().StringVar(&cc.gendocdir, "dir", "/tmp/hugodoc/", "the directory to write the doc.") - - // For bash-completion - cc.cmd.PersistentFlags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{}) - - return cc -} diff --git a/commands/gendocshelper.go b/commands/gendocshelper.go deleted file mode 100644 index c243581f6..000000000 --- a/commands/gendocshelper.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 commands - -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - - "github.com/gohugoio/hugo/docshelper" - "github.com/spf13/cobra" -) - -var ( - _ cmder = (*genDocsHelper)(nil) -) - -type genDocsHelper struct { - target string - *baseCmd -} - -func createGenDocsHelper() *genDocsHelper { - g := &genDocsHelper{ - baseCmd: newBaseCmd(&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 deleted file mode 100644 index 720046289..000000000 --- a/commands/genman.go +++ /dev/null @@ -1,77 +0,0 @@ -// 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/common/hugo" - "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 _ cmder = (*genManCmd)(nil) - -type genManCmd struct { - genmandir string - *baseCmd -} - -func newGenManCmd() *genManCmd { - cc := &genManCmd{} - - cc.baseCmd = newBaseCmd(&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", hugo.CurrentVersion), - } - if !strings.HasSuffix(cc.genmandir, helpers.FilePathSeparator) { - cc.genmandir += helpers.FilePathSeparator - } - if found, _ := helpers.Exists(cc.genmandir, hugofs.Os); !found { - jww.FEEDBACK.Println("Directory", cc.genmandir, "does not exist, creating...") - if err := hugofs.Os.MkdirAll(cc.genmandir, 0777); err != nil { - return err - } - } - cmd.Root().DisableAutoGenTag = true - - jww.FEEDBACK.Println("Generating Hugo man pages in", cc.genmandir, "...") - doc.GenManTree(cmd.Root(), header, cc.genmandir) - - jww.FEEDBACK.Println("Done.") - - return nil - }, - }) - - cc.cmd.PersistentFlags().StringVar(&cc.genmandir, "dir", "man/", "the directory to write the man pages.") - - // For bash-completion - cc.cmd.PersistentFlags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{}) - - return cc -} diff --git a/commands/helpers.go b/commands/helpers.go deleted file mode 100644 index 1386e425f..000000000 --- a/commands/helpers.go +++ /dev/null @@ -1,79 +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 commands defines and implements command-line commands and flags -// used by Hugo. Commands and flags are implemented using Cobra. -package commands - -import ( - "fmt" - "regexp" - - "github.com/gohugoio/hugo/config" - "github.com/spf13/cobra" -) - -const ( - ansiEsc = "\u001B" - clearLine = "\r\033[K" - hideCursor = ansiEsc + "[?25l" - showCursor = ansiEsc + "[?25h" -) - -type flagsToConfigHandler interface { - flagsToConfig(cfg config.Provider) -} - -type cmder interface { - flagsToConfigHandler - getCommand() *cobra.Command -} - -// 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()) -} diff --git a/commands/hugo.go b/commands/hugo.go deleted file mode 100644 index 3da059cc5..000000000 --- a/commands/hugo.go +++ /dev/null @@ -1,1169 +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 commands defines and implements command-line commands and flags -// used by Hugo. Commands and flags are implemented using Cobra. -package commands - -import ( - "context" - "fmt" - "io/ioutil" - "os/signal" - "runtime/pprof" - "runtime/trace" - "sync/atomic" - - "github.com/gohugoio/hugo/hugofs" - - "github.com/gohugoio/hugo/resources/page" - - "github.com/pkg/errors" - - "github.com/gohugoio/hugo/common/herrors" - "github.com/gohugoio/hugo/common/loggers" - "github.com/gohugoio/hugo/common/terminal" - - "syscall" - - "github.com/gohugoio/hugo/hugolib/filesystems" - - "golang.org/x/sync/errgroup" - - "os" - "path/filepath" - "runtime" - "strings" - "time" - - "github.com/gohugoio/hugo/config" - - flag "github.com/spf13/pflag" - - "github.com/fsnotify/fsnotify" - "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/hugolib" - "github.com/gohugoio/hugo/livereload" - "github.com/gohugoio/hugo/watcher" - "github.com/spf13/afero" - "github.com/spf13/cobra" - "github.com/spf13/fsync" - jww "github.com/spf13/jwalterweatherman" -) - -// The Response value from Execute. -type Response struct { - // The build Result will only be set in the hugo build command. - Result *hugolib.HugoSites - - // Err is set when the command failed to execute. - Err error - - // The command that was executed. - Cmd *cobra.Command -} - -// IsUserError returns true is the Response error is a user error rather than a -// system error. -func (r Response) IsUserError() bool { - return r.Err != nil && isUserError(r.Err) -} - -// Execute adds all child commands to the root command HugoCmd and sets flags appropriately. -// The args are usually filled with os.Args[1:]. -func Execute(args []string) Response { - hugoCmd := newCommandsBuilder().addAll().build() - cmd := hugoCmd.getCommand() - cmd.SetArgs(args) - - c, err := cmd.ExecuteC() - - var resp Response - - if c == cmd && hugoCmd.c != nil { - // Root command executed - resp.Result = hugoCmd.c.hugo() - } - - if err == nil { - errCount := int(loggers.GlobalErrorCounter.Count()) - if errCount > 0 { - err = fmt.Errorf("logged %d errors", errCount) - } else if resp.Result != nil { - errCount = resp.Result.NumLogErrors() - if errCount > 0 { - err = fmt.Errorf("logged %d errors", errCount) - } - } - - } - - resp.Err = err - resp.Cmd = c - - return resp -} - -// InitializeConfig initializes a config file with sensible default configuration flags. -func initializeConfig(mustHaveConfigFile, running bool, - h *hugoBuilderCommon, - f flagsToConfigHandler, - doWithCommandeer func(c *commandeer) error) (*commandeer, error) { - - c, err := newCommandeer(mustHaveConfigFile, running, h, f, doWithCommandeer) - if err != nil { - return nil, err - } - - return c, nil - -} - -func (c *commandeer) createLogger(cfg config.Provider, running bool) (*loggers.Logger, error) { - var ( - logHandle = ioutil.Discard - logThreshold = jww.LevelWarn - logFile = cfg.GetString("logFile") - outHandle = os.Stdout - stdoutThreshold = jww.LevelWarn - ) - - if c.h.verboseLog || c.h.logging || (c.h.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 !c.h.quiet && cfg.GetBool("verbose") { - stdoutThreshold = jww.LevelInfo - } - - if cfg.GetBool("debug") { - stdoutThreshold = jww.LevelDebug - } - - if c.h.verboseLog { - logThreshold = jww.LevelInfo - if cfg.GetBool("debug") { - logThreshold = jww.LevelDebug - } - } - - loggers.InitGlobalLogger(stdoutThreshold, logThreshold, outHandle, logHandle) - helpers.InitLoggers() - - return loggers.NewLogger(stdoutThreshold, logThreshold, outHandle, logHandle, running), nil -} - -func initializeFlags(cmd *cobra.Command, cfg config.Provider) { - persFlagKeys := []string{ - "debug", - "verbose", - "logFile", - // Moved from vars - } - flagKeys := []string{ - "cleanDestinationDir", - "buildDrafts", - "buildFuture", - "buildExpired", - "uglyURLs", - "canonifyURLs", - "enableRobotsTXT", - "enableGitInfo", - "pluralizeListTitles", - "preserveTaxonomyNames", - "ignoreCache", - "forceSyncStatic", - "noTimes", - "noChmod", - "ignoreVendor", - "templateMetrics", - "templateMetricsHints", - - // Moved from vars. - "baseURL", - "buildWatch", - "cacheDir", - "cfgFile", - "confirm", - "contentDir", - "debug", - "destination", - "disableKinds", - "dryRun", - "force", - "gc", - "i18n-warnings", - "invalidateCDN", - "layoutDir", - "logFile", - "maxDeletes", - "quiet", - "renderToMemory", - "source", - "target", - "theme", - "themesDir", - "verbose", - "verboseLog", - "duplicateTargetPaths", - } - - // Will set a value even if it is the default. - flagKeysForced := []string{ - "minify", - } - - for _, key := range persFlagKeys { - setValueFromFlag(cmd.PersistentFlags(), key, cfg, "", false) - } - for _, key := range flagKeys { - setValueFromFlag(cmd.Flags(), key, cfg, "", false) - } - - for _, key := range flagKeysForced { - setValueFromFlag(cmd.Flags(), key, cfg, "", true) - } - - // Set some "config aliases" - setValueFromFlag(cmd.Flags(), "destination", cfg, "publishDir", false) - setValueFromFlag(cmd.Flags(), "i18n-warnings", cfg, "logI18nWarnings", false) - setValueFromFlag(cmd.Flags(), "path-warnings", cfg, "logPathWarnings", false) - -} - -func setValueFromFlag(flags *flag.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 isTerminal() bool { - return terminal.IsTerminal(os.Stdout) - -} -func ifTerminal(s string) string { - if !isTerminal() { - return "" - } - return s -} - -func (c *commandeer) fullBuild() error { - - var ( - g errgroup.Group - langCount map[string]uint64 - ) - - if !c.h.quiet { - fmt.Print(ifTerminal(hideCursor) + "Building sites … ") - if isTerminal() { - defer func() { - fmt.Print(showCursor + clearLine) - }() - } - } - - copyStaticFunc := func() error { - - cnt, err := c.copyStatic() - if err != nil { - return errors.Wrap(err, "Error copying static files") - } - langCount = cnt - return nil - } - buildSitesFunc := func() error { - if err := c.buildSites(); err != nil { - return errors.Wrap(err, "Error building site") - } - 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. - if c.Cfg.GetBool("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 - } - } - - for _, s := range c.hugo().Sites { - s.ProcessingStats.Static = langCount[s.Language().Lang] - } - - if c.h.gc { - count, err := c.hugo().GC() - if err != nil { - return err - } - for _, s := range c.hugo().Sites { - // We have no way of knowing what site the garbage belonged to. - s.ProcessingStats.Cleaned = uint64(count) - } - } - - return nil - -} - -func (c *commandeer) initCPUProfile() (func(), error) { - if c.h.cpuprofile == "" { - return nil, nil - } - - f, err := os.Create(c.h.cpuprofile) - if err != nil { - return nil, errors.Wrap(err, "failed to create CPU profile") - } - if err := pprof.StartCPUProfile(f); err != nil { - return nil, errors.Wrap(err, "failed to start CPU profile") - } - return func() { - pprof.StopCPUProfile() - f.Close() - }, nil -} - -func (c *commandeer) initMemProfile() { - if c.h.memprofile == "" { - return - } - - f, err := os.Create(c.h.memprofile) - if err != nil { - c.logger.ERROR.Println("could not create memory profile: ", err) - } - defer f.Close() - runtime.GC() // get up-to-date statistics - if err := pprof.WriteHeapProfile(f); err != nil { - c.logger.ERROR.Println("could not write memory profile: ", err) - } -} - -func (c *commandeer) initTraceProfile() (func(), error) { - if c.h.traceprofile == "" { - return nil, nil - } - - f, err := os.Create(c.h.traceprofile) - if err != nil { - return nil, errors.Wrap(err, "failed to create trace file") - } - - if err := trace.Start(f); err != nil { - return nil, errors.Wrap(err, "failed to start trace") - } - - return func() { - trace.Stop() - f.Close() - }, nil -} - -func (c *commandeer) initMutexProfile() (func(), error) { - if c.h.mutexprofile == "" { - return nil, nil - } - - f, err := os.Create(c.h.mutexprofile) - if err != nil { - return nil, err - } - - runtime.SetMutexProfileFraction(1) - - return func() { - pprof.Lookup("mutex").WriteTo(f, 0) - f.Close() - }, nil - -} - -func (c *commandeer) 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 - } - - return func() { - c.initMemProfile() - - if stopCPUProf != nil { - stopCPUProf() - } - if stopMutexProf != nil { - stopMutexProf() - } - - if stopTraceProf != nil { - stopTraceProf() - } - }, nil -} - -func (c *commandeer) build() error { - defer c.timeTrack(time.Now(), "Total") - - stopProfiling, err := c.initProfiling() - if err != nil { - return err - } - - defer func() { - if stopProfiling != nil { - stopProfiling() - } - }() - - if err := c.fullBuild(); err != nil { - return err - } - - // TODO(bep) Feedback? - if !c.h.quiet { - fmt.Println() - c.hugo().PrintProcessingStats(os.Stdout) - fmt.Println() - - if createCounter, ok := c.destinationFs.(hugofs.DuplicatesReporter); ok { - dupes := createCounter.ReportDuplicates() - if dupes != "" { - c.logger.WARN.Println("Duplicate target paths:", dupes) - } - } - } - - if c.h.buildWatch { - watchDirs, err := c.getDirList() - if err != nil { - return err - } - - baseWatchDir := c.Cfg.GetString("workingDir") - rootWatchDirs := getRootWatchDirsStr(baseWatchDir, watchDirs) - - c.logger.FEEDBACK.Printf("Watching for changes in %s%s{%s}\n", baseWatchDir, helpers.FilePathSeparator, rootWatchDirs) - c.logger.FEEDBACK.Println("Press Ctrl+C to stop") - watcher, err := c.newWatcher(watchDirs...) - checkErr(c.Logger, err) - defer watcher.Close() - - var sigs = make(chan os.Signal, 1) - signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) - - <-sigs - } - - return nil -} - -func (c *commandeer) serverBuild() error { - defer c.timeTrack(time.Now(), "Total") - - stopProfiling, err := c.initProfiling() - if err != nil { - return err - } - - defer func() { - if stopProfiling != nil { - stopProfiling() - } - }() - - if err := c.fullBuild(); err != nil { - return err - } - - // TODO(bep) Feedback? - if !c.h.quiet { - fmt.Println() - c.hugo().PrintProcessingStats(os.Stdout) - fmt.Println() - } - - return nil -} - -func (c *commandeer) copyStatic() (map[string]uint64, error) { - m, err := c.doWithPublishDirs(c.copyStaticTo) - if err == nil || os.IsNotExist(err) { - return m, nil - } - return m, err -} - -func (c *commandeer) doWithPublishDirs(f func(sourceFs *filesystems.SourceFilesystem) (uint64, error)) (map[string]uint64, error) { - - langCount := make(map[string]uint64) - - staticFilesystems := c.hugo().BaseFs.SourceFilesystems.Static - - if len(staticFilesystems) == 0 { - c.logger.INFO.Println("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 - for _, l := range c.languages { - langCount[l.Lang] = cnt - } - } else { - langCount[lang] = cnt - } - } - - return langCount, nil -} - -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 -} - -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 (c *commandeer) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint64, error) { - publishDir := c.hugo().PathSpec.PublishDir - // If root, remove the second '/' - if publishDir == "//" { - publishDir = helpers.FilePathSeparator - } - - if sourceFs.PublishFolder != "" { - publishDir = filepath.Join(publishDir, sourceFs.PublishFolder) - } - - fs := &countingStatFs{Fs: sourceFs.Fs} - - syncer := fsync.NewSyncer() - syncer.NoTimes = c.Cfg.GetBool("noTimes") - syncer.NoChmod = c.Cfg.GetBool("noChmod") - syncer.ChmodFilter = chmodFilter - syncer.SrcFs = fs - 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 - err := syncer.Sync(publishDir, helpers.FilePathSeparator) - if err != nil { - return 0, err - } - - // Sync runs Stat 3 times for every source file (which sounds much) - numFiles := fs.statCounter / 3 - - return numFiles, err -} - -func (c *commandeer) firstPathSpec() *helpers.PathSpec { - return c.hugo().Sites[0].PathSpec -} - -func (c *commandeer) timeTrack(start time.Time, name string) { - if c.h.quiet { - return - } - elapsed := time.Since(start) - c.logger.FEEDBACK.Printf("%s in %v ms", name, int(1000*elapsed.Seconds())) -} - -// getDirList provides NewWatcher() with a list of directories to watch for changes. -func (c *commandeer) getDirList() ([]string, error) { - var dirnames []string - - walkFn := func(path string, fi hugofs.FileMetaInfo, err error) error { - if err != nil { - c.logger.ERROR.Println("walker: ", err) - return nil - } - - if fi.IsDir() { - if fi.Name() == ".git" || - fi.Name() == "node_modules" || fi.Name() == "bower_components" { - return filepath.SkipDir - } - - dirnames = append(dirnames, fi.Meta().Filename()) - } - - return nil - - } - - watchDirs := c.hugo().PathSpec.BaseFs.WatchDirs() - for _, watchDir := range watchDirs { - - w := hugofs.NewWalkway(hugofs.WalkwayConfig{Logger: c.logger, Info: watchDir, WalkFn: walkFn}) - if err := w.Walk(); err != nil { - c.logger.ERROR.Println("walker: ", err) - } - } - - dirnames = helpers.UniqueStringsSorted(dirnames) - - return dirnames, nil -} - -func (c *commandeer) buildSites() (err error) { - return c.hugo().Build(hugolib.BuildCfg{}) -} - -func (c *commandeer) handleBuildErr(err error, msg string) { - c.buildErr = err - - c.logger.ERROR.Print(msg + ":\n\n") - c.logger.ERROR.Println(helpers.FirstUpper(err.Error())) - if !c.h.quiet && c.h.verbose { - herrors.PrintStackTrace(err) - } -} - -func (c *commandeer) rebuildSites(events []fsnotify.Event) error { - defer c.timeTrack(time.Now(), "Total") - - c.buildErr = nil - visited := c.visitedURLs.PeekAllSet() - if c.fastRenderMode { - - // Make sure we always render the home pages - for _, l := range c.languages { - langPath := c.hugo().PathSpec.GetLangSubDir(l.Lang) - if langPath != "" { - langPath = langPath + "/" - } - home := c.hugo().PathSpec.PrependBasePath("/"+langPath, false) - visited[home] = true - } - - } - return c.hugo().Build(hugolib.BuildCfg{RecentlyVisited: visited}, events...) -} - -func (c *commandeer) partialReRender(urls ...string) error { - c.buildErr = nil - visited := make(map[string]bool) - for _, url := range urls { - visited[url] = true - } - return c.hugo().Build(hugolib.BuildCfg{RecentlyVisited: visited, PartialReRender: true}) -} - -func (c *commandeer) 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 back. - // This will block any rebuild on config changes for the - // duration of the sleep. - time.Sleep(2 * time.Second) - }() - - defer c.timeTrack(time.Now(), "Total") - - c.commandeerHugoState = newCommandeerHugoState() - err := c.loadConfig(true, true) - if err != nil { - // Set the processing on pause until the state is recovered. - c.paused = true - c.handleBuildErr(err, "Failed to reload config") - - } else { - c.paused = false - } - - if !c.paused { - _, err := c.copyStatic() - if err != nil { - c.logger.ERROR.Println(err) - return - } - - err = c.buildSites() - if err != nil { - c.logger.ERROR.Println(err) - } else if !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") { - livereload.ForceRefresh() - } - } - }() -} - -// newWatcher creates a new watcher to watch filesystem events. -func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) { - if runtime.GOOS == "darwin" { - tweakLimit() - } - - staticSyncer, err := newStaticSyncer(c) - if err != nil { - return nil, err - } - - watcher, err := watcher.New(1 * time.Second) - - if err != nil { - return nil, err - } - - for _, d := range dirList { - if d != "" { - _ = watcher.Add(d) - } - } - - // Identifies changes to config (config.toml) files. - configSet := make(map[string]bool) - - c.logger.FEEDBACK.Println("Watching for config changes in", strings.Join(c.configFiles, ", ")) - for _, configFile := range c.configFiles { - watcher.Add(configFile) - configSet[configFile] = true - } - - go func() { - for { - select { - case evs := <-watcher.Events: - c.handleEvents(watcher, staticSyncer, evs, configSet) - if c.showErrorInBrowser && c.errCount() > 0 { - // Need to reload browser to show the error - livereload.ForceRefresh() - } - case err := <-watcher.Errors: - if err != nil { - c.logger.ERROR.Println("Error while watching:", err) - } - } - } - }() - - return watcher, nil -} - -func (c *commandeer) printChangeDetected(typ string) { - msg := "\nChange" - if typ != "" { - msg += " of " + typ - } - msg += " detected, rebuilding site." - - c.logger.FEEDBACK.Println(msg) - const layout = "2006-01-02 15:04:05.000 -0700" - c.logger.FEEDBACK.Println(time.Now().Format(layout)) -} - -const ( - configChangeConfig = "config file" - configChangeGoMod = "go.mod file" -) - -func (c *commandeer) handleEvents(watcher *watcher.Batcher, - staticSyncer *staticSyncer, - evs []fsnotify.Event, - configSet map[string]bool) { - - var isHandled bool - - for _, ev := range evs { - isConfig := configSet[ev.Name] - configChangeType := configChangeConfig - if isConfig { - if strings.Contains(ev.Name, "go.mod") { - configChangeType = configChangeGoMod - } - } - 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 { - for _, configFile := range c.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.paused { - // 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.logger.INFO.Println("Received System Events:", evs) - - staticEvents := []fsnotify.Event{} - dynamicEvents := []fsnotify.Event{} - - // Special handling for symbolic links inside /content. - filtered := []fsnotify.Event{} - for _, ev := range evs { - // Check the most specific first, i.e. files. - contentMapped := c.hugo().ContentChanges.GetSymbolicLinkMappings(ev.Name) - if len(contentMapped) > 0 { - for _, mapped := range contentMapped { - filtered = append(filtered, fsnotify.Event{Name: mapped, Op: ev.Op}) - } - continue - } - - // Check for any symbolic directory mapping. - - dir, name := filepath.Split(ev.Name) - - contentMapped = c.hugo().ContentChanges.GetSymbolicLinkMappings(dir) - - if len(contentMapped) == 0 { - filtered = append(filtered, ev) - continue - } - - for _, mapped := range contentMapped { - mappedFilename := filepath.Join(mapped, name) - filtered = append(filtered, fsnotify.Event{Name: mappedFilename, Op: ev.Op}) - } - } - - evs = filtered - - 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 - } - if c.hugo().Deps.SourceSpec.IgnoreFile(ev.Name) { - 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 hugofs.FileMetaInfo, 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 !staticSyncer.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 staticSyncer.isStatic(ev.Name) { - staticEvents = append(staticEvents, ev) - } else { - dynamicEvents = append(dynamicEvents, ev) - } - } - - if len(staticEvents) > 0 { - c.printChangeDetected("Static files") - - if c.Cfg.GetBool("forceSyncStatic") { - c.logger.FEEDBACK.Printf("Syncing all static files\n") - _, err := c.copyStatic() - if err != nil { - c.logger.ERROR.Println("Error copying static files to publish dir:", err) - return - } - } else { - if err := staticSyncer.syncsStaticEvents(staticEvents); err != nil { - c.logger.ERROR.Println("Error syncing static files to publish dir:", err) - return - } - } - - if !c.h.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) == 1 { - ev := staticEvents[0] - path := c.hugo().BaseFs.SourceFilesystems.MakeStaticPathRelative(ev.Name) - path = c.firstPathSpec().RelURL(helpers.ToSlashTrimLeading(path), false) - livereload.RefreshPath(path) - } else { - livereload.ForceRefresh() - } - } - } - - if len(dynamicEvents) > 0 { - partitionedEvents := partitionDynamicEvents( - c.firstPathSpec().BaseFs.SourceFilesystems, - dynamicEvents) - - doLiveReload := !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") - onePageName := pickOneWriteOrCreatePath(partitionedEvents.ContentEvents) - - c.printChangeDetected("") - c.changeDetector.PrepareNew() - if err := c.rebuildSites(dynamicEvents); err != nil { - c.handleBuildErr(err, "Rebuild failed") - } - - if doLiveReload { - if len(partitionedEvents.ContentEvents) == 0 && len(partitionedEvents.AssetEvents) > 0 { - changed := c.changeDetector.changed() - if c.changeDetector != nil && len(changed) == 0 { - // Nothing has changed. - return - } else if len(changed) == 1 { - pathToRefresh := c.firstPathSpec().RelURL(helpers.ToSlashTrimLeading(changed[0]), false) - livereload.RefreshPath(pathToRefresh) - } else { - livereload.ForceRefresh() - } - } - - if len(partitionedEvents.ContentEvents) > 0 { - - navigate := c.Cfg.GetBool("navigateToChanged") - // We have fetched the same page above, but it may have - // changed. - var p page.Page - - if navigate { - if onePageName != "" { - p = c.hugo().GetContentPage(onePageName) - } - } - - if p != nil { - livereload.NavigateToPathForPort(p.RelPermalink(), p.Site().ServerPort()) - } else { - livereload.ForceRefresh() - } - } - } - } -} - -// 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 -} - -func partitionDynamicEvents(sourceFs *filesystems.SourceFilesystems, events []fsnotify.Event) (de dynamicEvents) { - for _, e := range events { - if sourceFs.IsAsset(e.Name) { - de.AssetEvents = append(de.AssetEvents, e) - } else { - de.ContentEvents = append(de.ContentEvents, e) - } - } - return - -} - -func pickOneWriteOrCreatePath(events []fsnotify.Event) string { - name := "" - - // Some editors (for example notepad.exe on Windows) triggers a change - // both for directory and file. So we pick the longest path, which should - // be the file itself. - for _, ev := range events { - if (ev.Op&fsnotify.Write == fsnotify.Write || ev.Op&fsnotify.Create == fsnotify.Create) && len(ev.Name) > len(name) { - name = ev.Name - } - } - - return name -} diff --git a/commands/hugo_test.go b/commands/hugo_test.go deleted file mode 100644 index 6a666ff4a..000000000 --- a/commands/hugo_test.go +++ /dev/null @@ -1,52 +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 commands - -import ( - "os" - "testing" - - qt "github.com/frankban/quicktest" -) - -// Issue #5662 -func TestHugoWithContentDirOverride(t *testing.T) { - c := qt.New(t) - - hugoCmd := newCommandsBuilder().addAll().build() - cmd := hugoCmd.getCommand() - - contentDir := "contentOverride" - - cfgStr := ` - -baseURL = "https://example.org" -title = "Hugo Commands" - -contentDir = "thisdoesnotexist" - -` - dir, err := createSimpleTestSite(t, testSiteConfig{configTOML: cfgStr, contentDir: contentDir}) - c.Assert(err, qt.IsNil) - - defer func() { - os.RemoveAll(dir) - }() - - cmd.SetArgs([]string{"-s=" + dir, "-c=" + contentDir}) - - _, err = cmd.ExecuteC() - c.Assert(err, qt.IsNil) - -} diff --git a/commands/hugo_windows.go b/commands/hugo_windows.go deleted file mode 100644 index 106c0cc71..000000000 --- a/commands/hugo_windows.go +++ /dev/null @@ -1,27 +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 commands - -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 website. - - You need to open cmd.exe and run Hugo from there. - - Visit https://gohugo.io/ for more information.` -} diff --git a/commands/import_jekyll.go b/commands/import_jekyll.go deleted file mode 100644 index e5c39dc34..000000000 --- a/commands/import_jekyll.go +++ /dev/null @@ -1,596 +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 commands - -import ( - "bytes" - "errors" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "regexp" - "strconv" - "strings" - "time" - "unicode" - - "github.com/gohugoio/hugo/common/hugio" - - "github.com/gohugoio/hugo/parser/metadecoders" - - "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" -) - -var _ cmder = (*importCmd)(nil) - -type importCmd struct { - *baseCmd -} - -func newImportCmd() *importCmd { - cc := &importCmd{} - - cc.baseCmd = newBaseCmd(&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, - }) - - 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: cc.importFromJekyll, - } - - importJekyllCmd.Flags().Bool("force", false, "allow import into non-empty target directory") - - cc.cmd.AddCommand(importJekyllCmd) - - return cc - -} - -func (i *importCmd) 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("abort: target path should not be inside the Jekyll root") - } - - forceImport, _ := cmd.Flags().GetBool("force") - - fs := afero.NewOsFs() - jekyllPostDirs, hasAnyPost := i.getJekyllDirInfo(fs, jekyllRoot) - if !hasAnyPost { - return errors.New("abort: jekyll root contains neither posts nor drafts") - } - - site, err := i.createSiteFromJekyll(jekyllRoot, targetDir, jekyllPostDirs, forceImport) - - if err != nil { - return newUserError(err) - } - - jww.FEEDBACK.Println("Importing...") - - fileCount := 0 - callback := func(path string, fi hugofs.FileMetaInfo, 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") - - return nil -} - -func (i *importCmd) 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 := i.retrieveJekyllPostDir(fs, subDir); isPostDir { - postDirs[entry.Name()] = hasAnyPostInDir - if hasAnyPostInDir { - hasAnyPost = true - } - } - } - } - } - return postDirs, hasAnyPost -} - -func (i *importCmd) 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 := i.retrieveJekyllPostDir(fs, subDir); isPostDir { - return isPostDir, hasAnyPost - } - } - } - } - - return false, true -} - -func (i *importCmd) 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 + "\" exists but is not a directory") - } - - isEmpty, _ := helpers.IsEmpty(targetDir, fs) - - if !isEmpty && !force { - return nil, errors.New("target path \"" + targetDir + "\" exists and is not empty") - } - } - - jekyllConfig := i.loadJekyllConfig(fs, jekyllRoot) - - mkdir(targetDir, "layouts") - mkdir(targetDir, "content") - mkdir(targetDir, "archetypes") - mkdir(targetDir, "static") - mkdir(targetDir, "data") - mkdir(targetDir, "themes") - - i.createConfigFromJekyll(fs, targetDir, "yaml", jekyllConfig) - - i.copyJekyllFilesAndFolders(jekyllRoot, filepath.Join(targetDir, "static"), jekyllPostDirs) - - return s, nil -} - -func (i *importCmd) 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 := metadecoders.Default.UnmarshalToMap(b, metadecoders.YAML) - - if err != nil { - return nil - } - - return c -} - -func (i *importCmd) createConfigFromJekyll(fs afero.Fs, inpath string, kind metadecoders.Format, 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, - } - - var buf bytes.Buffer - err = parser.InterfaceToConfig(in, kind, &buf) - if err != nil { - return err - } - - return helpers.WriteToDisk(filepath.Join(inpath, "config."+string(kind)), &buf, fs) -} - -func (i *importCmd) 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 := ioutil.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 { - 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 = hugio.CopyFile(fs, 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 - } - - pf, err := parseContentFile(bytes.NewReader(contentBytes)) - if err != nil { - jww.ERROR.Println("Parse file error:", path) - return err - } - - newmetadata, err := convertJekyllMetaData(pf.frontMatter, postName, postDate, draft) - if err != nil { - jww.ERROR.Println("Convert metadata error:", path) - return err - } - - content := convertJekyllContent(newmetadata, string(pf.content)) - - 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 convertJekyllMetaData(m interface{}, postName string, postDate time.Time, draft bool) (interface{}, error) { - 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 { - 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 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*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}, - {regexp.MustCompile(`{%\s*highlight\s*(.*?)\s*%}`), replaceHighlightTag}, - } - - for _, replace := range replaceListFunc { - content = replace.re.ReplaceAllStringFunc(content, replace.replace) - } - - return content -} - -func 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 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 deleted file mode 100644 index 4ae26b95c..000000000 --- a/commands/import_jekyll_test.go +++ /dev/null @@ -1,133 +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 commands - -import ( - "encoding/json" - "testing" - "time" - - qt "github.com/frankban/quicktest" -) - -func TestParseJekyllFilename(t *testing.T) { - c := qt.New(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) - c.Assert(err, qt.IsNil) - c.Assert(expectResult[i].postDate.Format("2006-01-02"), qt.Equals, postDate.Format("2006-01-02")) - c.Assert(expectResult[i].postName, qt.Equals, postName) - } -} - -func TestConvertJekyllMetadata(t *testing.T) { - c := qt.New(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"}`}, - {map[interface{}]interface{}{}, "testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), true, - `{"date":"2015-10-01T00:00:00Z","draft":true}`}, - {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"}`}, - {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"}`}, - {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"}`}, - } - - for _, data := range testDataList { - result, err := convertJekyllMetaData(data.metadata, data.postName, data.postDate, data.draft) - c.Assert(err, qt.IsNil) - jsonResult, err := json.Marshal(result) - c.Assert(err, qt.IsNil) - c.Assert(string(jsonResult), qt.Equals, data.expect) - } -} - -func TestConvertJekyllContent(t *testing.T) { - c := qt.New(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 >}}"}, - {map[interface{}]interface{}{}, - "{% highlight go linenos hl_lines=\"1 2\" %}\nvar s string\nvar i int\n{% endhighlight %}", - "{{< highlight go \"linenos=table,hl_lines=1 2\" >}}\nvar s string\nvar i 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) - c.Assert(data.expect, qt.Equals, result) - } -} diff --git a/commands/limit_darwin.go b/commands/limit_darwin.go deleted file mode 100644 index 6799f37b1..000000000 --- a/commands/limit_darwin.go +++ /dev/null @@ -1,84 +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 commands - -import ( - "syscall" - - "github.com/spf13/cobra" - jww "github.com/spf13/jwalterweatherman" -) - -var _ cmder = (*limitCmd)(nil) - -type limitCmd struct { - *baseCmd -} - -func newLimitCmd() *limitCmd { - ccmd := &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) - - if rLimit.Cur >= newRlimit { - return nil - } - - jww.FEEDBACK.Println("Attempting to increase limit") - rLimit.Cur = newRlimit - 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 - }, - } - - return &limitCmd{baseCmd: newBaseCmd(ccmd)} -} - -const newRlimit = 10240 - -func tweakLimit() { - var rLimit syscall.Rlimit - err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit) - if err != nil { - jww.WARN.Println("Unable to get rlimit:", err) - return - } - if rLimit.Cur < newRlimit { - rLimit.Cur = newRlimit - err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit) - if err != nil { - // This may not succeed, see https://github.com/golang/go/issues/30401 - jww.INFO.Println("Unable to increase number of open files limit:", err) - } - } -} diff --git a/commands/limit_others.go b/commands/limit_others.go deleted file mode 100644 index 8d3e6ad70..000000000 --- a/commands/limit_others.go +++ /dev/null @@ -1,20 +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. - -// +build !darwin - -package commands - -func tweakLimit() { - // nothing to do -} diff --git a/commands/list.go b/commands/list.go deleted file mode 100644 index f233ce62c..000000000 --- a/commands/list.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 commands - -import ( - "encoding/csv" - "os" - "strconv" - "strings" - "time" - - "github.com/gohugoio/hugo/hugolib" - "github.com/gohugoio/hugo/resources/resource" - "github.com/spf13/cobra" - jww "github.com/spf13/jwalterweatherman" -) - -var _ cmder = (*listCmd)(nil) - -type listCmd struct { - hugoBuilderCommon - *baseCmd -} - -func (lc *listCmd) buildSites(config map[string]interface{}) (*hugolib.HugoSites, error) { - cfgInit := func(c *commandeer) error { - for key, value := range config { - c.Set(key, value) - } - return nil - } - - c, err := initializeConfig(true, false, &lc.hugoBuilderCommon, lc, cfgInit) - if err != nil { - return nil, err - } - - sites, err := hugolib.NewHugoSites(*c.DepsCfg) - - if err != nil { - return nil, newSystemError("Error creating sites", err) - } - - if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil { - return nil, newSystemError("Error Processing Source Content", err) - } - - return sites, nil -} - -func newListCmd() *listCmd { - cc := &listCmd{} - - cc.baseCmd = newBaseCmd(&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, - }) - - cc.cmd.AddCommand( - &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 { - sites, err := cc.buildSites(map[string]interface{}{"buildDrafts": true}) - - if err != nil { - return newSystemError("Error building sites", err) - } - - for _, p := range sites.Pages() { - if p.Draft() { - jww.FEEDBACK.Println(strings.TrimPrefix(p.File().Filename(), sites.WorkingDir+string(os.PathSeparator))) - } - } - - return nil - }, - }, - &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 { - sites, err := cc.buildSites(map[string]interface{}{"buildFuture": true}) - - if err != nil { - return newSystemError("Error building sites", err) - } - - writer := csv.NewWriter(os.Stdout) - defer writer.Flush() - - for _, p := range sites.Pages() { - if resource.IsFuture(p) { - err := writer.Write([]string{ - strings.TrimPrefix(p.File().Filename(), sites.WorkingDir+string(os.PathSeparator)), - p.PublishDate().Format(time.RFC3339), - }) - if err != nil { - return newSystemError("Error writing future posts to stdout", err) - } - } - } - - return nil - }, - }, - &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 { - sites, err := cc.buildSites(map[string]interface{}{"buildExpired": true}) - - if err != nil { - return newSystemError("Error building sites", err) - } - - writer := csv.NewWriter(os.Stdout) - defer writer.Flush() - - for _, p := range sites.Pages() { - if resource.IsExpired(p) { - err := writer.Write([]string{ - strings.TrimPrefix(p.File().Filename(), sites.WorkingDir+string(os.PathSeparator)), - p.ExpiryDate().Format(time.RFC3339), - }) - if err != nil { - return newSystemError("Error writing expired posts to stdout", err) - } - } - } - - return nil - }, - }, - &cobra.Command{ - Use: "all", - Short: "List all posts", - Long: `List all of the posts in your content directory, include drafts, future and expired pages.`, - RunE: func(cmd *cobra.Command, args []string) error { - sites, err := cc.buildSites(map[string]interface{}{ - "buildExpired": true, - "buildDrafts": true, - "buildFuture": true, - }) - - if err != nil { - return newSystemError("Error building sites", err) - } - - writer := csv.NewWriter(os.Stdout) - defer writer.Flush() - - writer.Write([]string{ - "path", - "slug", - "title", - "date", - "expiryDate", - "publishDate", - "draft", - "permalink", - }) - for _, p := range sites.Pages() { - if !p.IsPage() { - continue - } - err := writer.Write([]string{ - strings.TrimPrefix(p.File().Filename(), sites.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(), - }) - if err != nil { - return newSystemError("Error writing posts to stdout", err) - } - } - - return nil - }, - }, - ) - - cc.cmd.PersistentFlags().StringVarP(&cc.source, "source", "s", "", "filesystem path to read files relative from") - cc.cmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{}) - - return cc -} diff --git a/commands/list_test.go b/commands/list_test.go deleted file mode 100644 index bfc280679..000000000 --- a/commands/list_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package commands - -import ( - "bytes" - "encoding/csv" - "io" - "os" - "path/filepath" - "strings" - "testing" - - qt "github.com/frankban/quicktest" - "github.com/spf13/cobra" -) - -func captureStdout(f func() (*cobra.Command, error)) (string, error) { - old := os.Stdout - r, w, _ := os.Pipe() - os.Stdout = w - - _, err := f() - - if err != nil { - return "", err - } - - w.Close() - os.Stdout = old - - var buf bytes.Buffer - io.Copy(&buf, r) - return buf.String(), nil -} - -func TestListAll(t *testing.T) { - c := qt.New(t) - dir, err := createSimpleTestSite(t, testSiteConfig{}) - - c.Assert(err, qt.IsNil) - - hugoCmd := newCommandsBuilder().addAll().build() - cmd := hugoCmd.getCommand() - - defer func() { - os.RemoveAll(dir) - }() - - cmd.SetArgs([]string{"-s=" + dir, "list", "all"}) - - out, err := captureStdout(cmd.ExecuteC) - c.Assert(err, qt.IsNil) - - r := csv.NewReader(strings.NewReader(out)) - - header, err := r.Read() - - c.Assert(err, qt.IsNil) - c.Assert(header, qt.DeepEquals, []string{ - "path", "slug", "title", - "date", "expiryDate", "publishDate", - "draft", "permalink", - }) - - record, err := r.Read() - - c.Assert(err, qt.IsNil) - c.Assert(record, qt.DeepEquals, []string{ - filepath.Join("content", "p1.md"), "", "P1", - "0001-01-01T00:00:00Z", "0001-01-01T00:00:00Z", "0001-01-01T00:00:00Z", - "false", "https://example.org/p1/", - }) -} diff --git a/commands/mod.go b/commands/mod.go deleted file mode 100644 index 5fbd93ecb..000000000 --- a/commands/mod.go +++ /dev/null @@ -1,188 +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 commands - -import ( - "os" - - "github.com/gohugoio/hugo/modules" - "github.com/spf13/cobra" -) - -var _ cmder = (*modCmd)(nil) - -type modCmd struct { - *baseBuilderCmd -} - -func (b *commandsBuilder) newModCmd() *modCmd { - c := &modCmd{} - - const commonUsage = ` -Note that Hugo will always start out by resolving the components defined in the site -configuration, provided by a _vendor directory (if no --ignoreVendor flag provided), -Go Modules, or a folder inside the themes directory, in that order. - -See https://gohugo.io/hugo-modules/ for more information. - -` - - cmd := &cobra.Command{ - Use: "mod", - Short: "Various Hugo Modules helpers.", - 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". - -` + commonUsage, - - RunE: nil, - } - - cmd.AddCommand( - &cobra.Command{ - Use: "get", - DisableFlagParsing: true, - 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 module dependencies: - - hugo mod get -u - -Run "go help get" for more information. All flags available for "go get" is also relevant here. -` + commonUsage, - RunE: func(cmd *cobra.Command, args []string) error { - return c.withModsClient(false, func(c *modules.Client) 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" { - return cmd.Help() - } - return c.Get(args...) - }) - }, - }, - &cobra.Command{ - Use: "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. -`, - RunE: func(cmd *cobra.Command, args []string) error { - return c.withModsClient(true, func(c *modules.Client) error { - return c.Graph(os.Stdout) - }) - }, - }, - &cobra.Command{ - Use: "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. -`, - RunE: func(cmd *cobra.Command, args []string) error { - var path string - if len(args) >= 1 { - path = args[0] - } - return c.withModsClient(false, func(c *modules.Client) error { - return c.Init(path) - }) - }, - }, - &cobra.Command{ - Use: "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. -`, - RunE: func(cmd *cobra.Command, args []string) error { - return c.withModsClient(true, func(c *modules.Client) error { - return c.Vendor() - }) - }, - }, - &cobra.Command{ - Use: "tidy", - Short: "Remove unused entries in go.mod and go.sum.", - RunE: func(cmd *cobra.Command, args []string) error { - return c.withModsClient(true, func(c *modules.Client) error { - return c.Tidy() - }) - }, - }, - &cobra.Command{ - Use: "clean", - Short: "Delete the entire Hugo Module cache.", - Long: `Delete the entire Hugo Module cache. - -Note that after you run this command, all of your dependencies will be re-downloaded next time you run "hugo". - -Also note that if you configure a positive maxAge for the "modules" file cache, it will also be cleaned as part of "hugo --gc". - -`, - RunE: func(cmd *cobra.Command, args []string) error { - com, err := c.initConfig(true) - if err != nil { - return err - } - - _, err = com.hugo().FileCaches.ModulesCache().Prune(true) - return err - - }, - }, - ) - - c.baseBuilderCmd = b.newBuilderCmd(cmd) - - return c - -} - -func (c *modCmd) withModsClient(failOnMissingConfig bool, f func(*modules.Client) error) error { - com, err := c.initConfig(failOnMissingConfig) - if err != nil { - return err - } - - return f(com.hugo().ModulesClient) -} - -func (c *modCmd) initConfig(failOnNoConfig bool) (*commandeer, error) { - com, err := initializeConfig(failOnNoConfig, false, &c.hugoBuilderCommon, c, nil) - if err != nil { - return nil, err - } - return com, nil -} diff --git a/commands/new.go b/commands/new.go deleted file mode 100644 index 4fc0d4ed4..000000000 --- a/commands/new.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 commands - -import ( - "bytes" - "os" - "path/filepath" - "strings" - - "github.com/gohugoio/hugo/create" - "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/hugolib" - "github.com/spf13/afero" - "github.com/spf13/cobra" - jww "github.com/spf13/jwalterweatherman" -) - -var _ cmder = (*newCmd)(nil) - -type newCmd struct { - contentEditor string - contentType string - - *baseBuilderCmd -} - -func (b *commandsBuilder) newNewCmd() *newCmd { - cmd := &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. - -Ensure you run this within the root directory of your site.`, - } - - cc := &newCmd{baseBuilderCmd: b.newBuilderCmd(cmd)} - - cmd.Flags().StringVarP(&cc.contentType, "kind", "k", "", "content type to create") - cmd.Flags().StringVar(&cc.contentEditor, "editor", "", "edit new content with this editor, if provided") - - cmd.AddCommand(newNewSiteCmd().getCommand()) - cmd.AddCommand(newNewThemeCmd().getCommand()) - - cmd.RunE = cc.newContent - - return cc -} - -func (n *newCmd) newContent(cmd *cobra.Command, args []string) error { - cfgInit := func(c *commandeer) error { - if cmd.Flags().Changed("editor") { - c.Set("newContentEditor", n.contentEditor) - } - return nil - } - - c, err := initializeConfig(true, false, &n.hugoBuilderCommon, n, cfgInit) - - if err != nil { - return err - } - - if len(args) < 1 { - return newUserError("path needs to be provided") - } - - createPath := args[0] - - var kind string - - createPath, kind = newContentPathSection(c.hugo(), createPath) - - if n.contentType != "" { - kind = n.contentType - } - - return create.NewContent(c.hugo(), kind, createPath) -} - -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 newContentPathSection(h *hugolib.HugoSites, path string) (string, string) { - // Forward slashes is used in all examples. Convert if needed. - // Issue #1133 - createpath := filepath.FromSlash(path) - - if h != nil { - for _, dir := range h.BaseFs.Content.Dirs { - createpath = strings.TrimPrefix(createpath, dir.Meta().Filename()) - } - } - - var section string - // assume the first directory is the section (kind) - if strings.Contains(createpath[1:], helpers.FilePathSeparator) { - parts := strings.Split(strings.TrimPrefix(createpath, helpers.FilePathSeparator), helpers.FilePathSeparator) - if len(parts) > 0 { - section = parts[0] - } - - } - - return createpath, section -} diff --git a/commands/new_content_test.go b/commands/new_content_test.go deleted file mode 100644 index 36726e37a..000000000 --- a/commands/new_content_test.go +++ /dev/null @@ -1,134 +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 commands - -import ( - "path/filepath" - "testing" - - "github.com/gohugoio/hugo/hugofs" - "github.com/spf13/viper" - - qt "github.com/frankban/quicktest" -) - -// Issue #1133 -func TestNewContentPathSectionWithForwardSlashes(t *testing.T) { - c := qt.New(t) - p, s := newContentPathSection(nil, "/post/new.md") - c.Assert(p, qt.Equals, filepath.FromSlash("/post/new.md")) - c.Assert(s, qt.Equals, "post") -} - -func checkNewSiteInited(fs *hugofs.Fs, basepath string, t *testing.T) { - c := qt.New(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) - c.Assert(err, qt.IsNil) - } -} - -func TestDoNewSite(t *testing.T) { - c := qt.New(t) - n := newNewSiteCmd() - basepath := filepath.Join("base", "blog") - _, fs := newTestCfg() - - c.Assert(n.doNewSite(fs, basepath, false), qt.IsNil) - - checkNewSiteInited(fs, basepath, t) -} - -func TestDoNewSite_noerror_base_exists_but_empty(t *testing.T) { - c := qt.New(t) - basepath := filepath.Join("base", "blog") - _, fs := newTestCfg() - n := newNewSiteCmd() - - c.Assert(fs.Source.MkdirAll(basepath, 0777), qt.IsNil) - - c.Assert(n.doNewSite(fs, basepath, false), qt.IsNil) -} - -func TestDoNewSite_error_base_exists(t *testing.T) { - c := qt.New(t) - basepath := filepath.Join("base", "blog") - _, fs := newTestCfg() - n := newNewSiteCmd() - - c.Assert(fs.Source.MkdirAll(basepath, 0777), qt.IsNil) - _, err := fs.Source.Create(filepath.Join(basepath, "foo")) - c.Assert(err, qt.IsNil) - // Since the directory already exists and isn't empty, expect an error - c.Assert(n.doNewSite(fs, basepath, false), qt.Not(qt.IsNil)) - -} - -func TestDoNewSite_force_empty_dir(t *testing.T) { - c := qt.New(t) - basepath := filepath.Join("base", "blog") - _, fs := newTestCfg() - n := newNewSiteCmd() - - c.Assert(fs.Source.MkdirAll(basepath, 0777), qt.IsNil) - c.Assert(n.doNewSite(fs, basepath, true), qt.IsNil) - - checkNewSiteInited(fs, basepath, t) -} - -func TestDoNewSite_error_force_dir_inside_exists(t *testing.T) { - c := qt.New(t) - basepath := filepath.Join("base", "blog") - _, fs := newTestCfg() - n := newNewSiteCmd() - - contentPath := filepath.Join(basepath, "content") - - c.Assert(fs.Source.MkdirAll(contentPath, 0777), qt.IsNil) - c.Assert(n.doNewSite(fs, basepath, true), qt.Not(qt.IsNil)) -} - -func TestDoNewSite_error_force_config_inside_exists(t *testing.T) { - c := qt.New(t) - basepath := filepath.Join("base", "blog") - _, fs := newTestCfg() - n := newNewSiteCmd() - - configPath := filepath.Join(basepath, "config.toml") - c.Assert(fs.Source.MkdirAll(basepath, 0777), qt.IsNil) - _, err := fs.Source.Create(configPath) - c.Assert(err, qt.IsNil) - - c.Assert(n.doNewSite(fs, basepath, true), qt.Not(qt.IsNil)) -} - -func newTestCfg() (*viper.Viper, *hugofs.Fs) { - - v := viper.New() - fs := hugofs.NewMem(v) - - v.SetFs(fs.Source) - - return v, fs - -} diff --git a/commands/new_site.go b/commands/new_site.go deleted file mode 100644 index 6d1677e22..000000000 --- a/commands/new_site.go +++ /dev/null @@ -1,165 +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 commands - -import ( - "bytes" - "errors" - "path/filepath" - "strings" - - "github.com/gohugoio/hugo/parser/metadecoders" - - _errors "github.com/pkg/errors" - - "github.com/gohugoio/hugo/create" - "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/hugofs" - "github.com/gohugoio/hugo/parser" - "github.com/spf13/cobra" - jww "github.com/spf13/jwalterweatherman" - "github.com/spf13/viper" -) - -var _ cmder = (*newSiteCmd)(nil) - -type newSiteCmd struct { - configFormat string - - *baseCmd -} - -func newNewSiteCmd() *newSiteCmd { - ccmd := &newSiteCmd{} - - cmd := &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: ccmd.newSite, - } - - cmd.Flags().StringVarP(&ccmd.configFormat, "format", "f", "toml", "config & frontmatter format") - cmd.Flags().Bool("force", false, "init inside non-empty directory") - - ccmd.baseCmd = newBaseCmd(cmd) - - return ccmd - -} - -func (n *newSiteCmd) 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"), - } - - 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") - } - - isEmpty, _ := helpers.IsEmpty(basepath, fs.Source) - - switch { - case !isEmpty && !force: - return errors.New(basepath + " already exists and is not empty") - - case !isEmpty && force: - all := append(dirs, filepath.Join(basepath, "config."+n.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 _errors.Wrap(err, "Failed to create dir") - } - } - - createConfig(fs, basepath, n.configFormat) - - // Create a default 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()) - - return nil -} - -// newSite creates a new Hugo site and initializes a structured Hugo directory. -func (n *newSiteCmd) 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 n.doNewSite(hugofs.NewDefault(viper.New()), createpath, forceNew) -} - -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", - } - - var buf bytes.Buffer - err = parser.InterfaceToConfig(in, metadecoders.FormatFromString(kind), &buf) - if err != nil { - return err - } - - return helpers.WriteToDisk(filepath.Join(inpath, "config."+kind), &buf, fs.Source) -} - -func nextStepsText() string { - var nextStepsText bytes.Buffer - - nextStepsText.WriteString(`Just a few more steps and you're ready to go: - -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(`". -3. Start the built-in live server via "hugo server". - -Visit https://gohugo.io/ for quickstart guide and full documentation.`) - - return nextStepsText.String() -} diff --git a/commands/new_theme.go b/commands/new_theme.go deleted file mode 100644 index a0a4e89e3..000000000 --- a/commands/new_theme.go +++ /dev/null @@ -1,179 +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 commands - -import ( - "bytes" - "errors" - "path/filepath" - "strings" - "time" - - "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/hugofs" - "github.com/spf13/cobra" - jww "github.com/spf13/jwalterweatherman" -) - -var _ cmder = (*newThemeCmd)(nil) - -type newThemeCmd struct { - *baseCmd - hugoBuilderCommon -} - -func newNewThemeCmd() *newThemeCmd { - ccmd := &newThemeCmd{baseCmd: newBaseCmd(nil)} - - cmd := &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: ccmd.newTheme, - } - - ccmd.cmd = cmd - - return ccmd -} - -// newTheme creates a new Hugo theme template -func (n *newThemeCmd) newTheme(cmd *cobra.Command, args []string) error { - c, err := initializeConfig(false, false, &n.hugoBuilderCommon, n, nil) - - if err != nil { - return err - } - - if len(args) < 1 { - return newUserError("theme name needs to be provided") - } - - createpath := c.hugo().PathSpec.AbsPathify(filepath.Join(c.Cfg.GetString("themesDir"), args[0])) - jww.FEEDBACK.Println("Creating theme at", createpath) - - cfg := c.DepsCfg - - if x, _ := helpers.Exists(createpath, cfg.Fs.Source); x { - return errors.New(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") - - baseofDefault := []byte(` - - {{- partial "head.html" . -}} - - {{- partial "header.html" . -}} -
- {{- block "main" . }}{{- end }} -
- {{- partial "footer.html" . -}} - - -`) - err = helpers.WriteToDisk(filepath.Join(createpath, "layouts", "_default", "baseof.html"), bytes.NewReader(baseofDefault), cfg.Fs.Source) - if err != nil { - return err - } - - touchFile(cfg.Fs.Source, createpath, "layouts", "partials", "head.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"), bytes.NewReader(by), cfg.Fs.Source) - if err != nil { - return err - } - - n.createThemeMD(cfg.Fs, createpath) - - return nil -} - -func (n *newThemeCmd) 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" -description = "" -homepage = "http://example.com/" -tags = [] -features = [] -min_version = "0.41" - -[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 -} diff --git a/commands/release.go b/commands/release.go deleted file mode 100644 index 4de165f35..000000000 --- a/commands/release.go +++ /dev/null @@ -1,72 +0,0 @@ -// +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. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES 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" - - "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/releaser" - "github.com/spf13/cobra" -) - -var _ cmder = (*releaseCommandeer)(nil) - -type releaseCommandeer struct { - cmd *cobra.Command - - version string - - skipPublish bool - try bool -} - -func createReleaser() cmder { - // 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().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 (c *releaseCommandeer) getCommand() *cobra.Command { - return c.cmd -} - -func (c *releaseCommandeer) flagsToConfig(cfg config.Provider) { - -} - -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.skipPublish, r.try).Run() -} diff --git a/commands/release_noop.go b/commands/release_noop.go deleted file mode 100644 index ccf34b68e..000000000 --- a/commands/release_noop.go +++ /dev/null @@ -1,20 +0,0 @@ -// +build !release - -// 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 commands - -func createReleaser() cmder { - return &nilCommand{} -} diff --git a/commands/server.go b/commands/server.go deleted file mode 100644 index 709181507..000000000 --- a/commands/server.go +++ /dev/null @@ -1,557 +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 commands - -import ( - "bytes" - "fmt" - "net" - "net/http" - "net/url" - "os" - "os/signal" - "path/filepath" - "regexp" - "runtime" - "strconv" - "strings" - "sync" - "syscall" - "time" - - "github.com/pkg/errors" - - "github.com/gohugoio/hugo/livereload" - "github.com/gohugoio/hugo/tpl" - - "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/helpers" - "github.com/spf13/afero" - "github.com/spf13/cobra" - jww "github.com/spf13/jwalterweatherman" -) - -type serverCmd struct { - // Can be used to stop the server. Useful in tests - stop <-chan bool - - disableLiveReload bool - navigateToChanged bool - renderToDisk bool - serverAppend bool - serverInterface string - serverPort int - liveReloadPort int - serverWatch bool - noHTTPCache bool - - disableFastRender bool - disableBrowserError bool - - *baseBuilderCmd -} - -func (b *commandsBuilder) newServerCmd() *serverCmd { - return b.newServerCmdSignaled(nil) -} - -func (b *commandsBuilder) newServerCmdSignaled(stop <-chan bool) *serverCmd { - cc := &serverCmd{stop: stop} - - cc.baseBuilderCmd = b.newBuilderCmd(&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. - -'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.`, - RunE: cc.server, - }) - - cc.cmd.Flags().IntVarP(&cc.serverPort, "port", "p", 1313, "port on which the server will listen") - cc.cmd.Flags().IntVar(&cc.liveReloadPort, "liveReloadPort", -1, "port for live reloading (i.e. 443 in HTTPS proxy situations)") - cc.cmd.Flags().StringVarP(&cc.serverInterface, "bind", "", "127.0.0.1", "interface to which the server will bind") - cc.cmd.Flags().BoolVarP(&cc.serverWatch, "watch", "w", true, "watch filesystem for changes and recreate as needed") - cc.cmd.Flags().BoolVar(&cc.noHTTPCache, "noHTTPCache", false, "prevent HTTP caching") - cc.cmd.Flags().BoolVarP(&cc.serverAppend, "appendPort", "", true, "append port to baseURL") - cc.cmd.Flags().BoolVar(&cc.disableLiveReload, "disableLiveReload", false, "watch without enabling live browser reload on rebuild") - cc.cmd.Flags().BoolVar(&cc.navigateToChanged, "navigateToChanged", false, "navigate to changed content file on live browser reload") - cc.cmd.Flags().BoolVar(&cc.renderToDisk, "renderToDisk", false, "render to Destination path (default is render to memory & serve from there)") - cc.cmd.Flags().BoolVar(&cc.disableFastRender, "disableFastRender", false, "enables full re-renders on changes") - cc.cmd.Flags().BoolVar(&cc.disableBrowserError, "disableBrowserError", false, "do not show build errors in the browser") - - cc.cmd.Flags().String("memstats", "", "log memory usage to this file") - cc.cmd.Flags().String("meminterval", "100ms", "interval to poll memory usage (requires --memstats), valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\".") - - return cc -} - -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 { - return nil, err - } - return noDirFile{f}, nil -} - -func (f noDirFile) Readdir(count int) ([]os.FileInfo, error) { - return nil, nil -} - -var serverPorts []int - -func (sc *serverCmd) server(cmd *cobra.Command, args []string) error { - // If a Destination is provided via flag write to disk - destination, _ := cmd.Flags().GetString("destination") - if destination != "" { - sc.renderToDisk = true - } - - var serverCfgInit sync.Once - - cfgInit := func(c *commandeer) error { - c.Set("renderToMemory", !sc.renderToDisk) - if cmd.Flags().Changed("navigateToChanged") { - c.Set("navigateToChanged", sc.navigateToChanged) - } - if cmd.Flags().Changed("disableLiveReload") { - c.Set("disableLiveReload", sc.disableLiveReload) - } - if cmd.Flags().Changed("disableFastRender") { - c.Set("disableFastRender", sc.disableFastRender) - } - if cmd.Flags().Changed("disableBrowserError") { - c.Set("disableBrowserError", sc.disableBrowserError) - } - if sc.serverWatch { - c.Set("watch", true) - } - - // TODO(bep) yes, we should fix. - if !c.languagesConfigured { - return nil - } - - var err error - - // We can only do this once. - serverCfgInit.Do(func() { - serverPorts = make([]int, 1) - - if c.languages.IsMultihost() { - if !sc.serverAppend { - err = newSystemError("--appendPort=false not supported when in multihost mode") - } - serverPorts = make([]int, len(c.languages)) - } - - currentServerPort := sc.serverPort - - for i := 0; i < len(serverPorts); i++ { - l, err := net.Listen("tcp", net.JoinHostPort(sc.serverInterface, strconv.Itoa(currentServerPort))) - if err == nil { - l.Close() - serverPorts[i] = currentServerPort - } else { - if i == 0 && sc.cmd.Flags().Changed("port") { - // port set explicitly by user -- he/she probably meant it! - err = newSystemErrorF("Server startup failed: %s", err) - } - c.logger.FEEDBACK.Println("port", sc.serverPort, "already in use, attempting to use an available port") - sp, err := helpers.FindAvailablePort() - if err != nil { - err = newSystemError("Unable to find alternative port to use:", err) - } - serverPorts[i] = sp.Port - } - - currentServerPort = serverPorts[i] + 1 - } - }) - - c.serverPorts = serverPorts - - c.Set("port", sc.serverPort) - if sc.liveReloadPort != -1 { - c.Set("liveReloadPort", sc.liveReloadPort) - } else { - c.Set("liveReloadPort", serverPorts[0]) - } - - isMultiHost := c.languages.IsMultihost() - for i, language := range c.languages { - var serverPort int - if isMultiHost { - serverPort = serverPorts[i] - } else { - serverPort = serverPorts[0] - } - - baseURL, err := sc.fixURL(language, sc.baseURL, serverPort) - if err != nil { - return nil - } - if isMultiHost { - language.Set("baseURL", baseURL) - } - if i == 0 { - c.Set("baseURL", baseURL) - } - } - - return err - - } - - if err := memStats(); err != nil { - jww.WARN.Println("memstats error:", err) - } - - c, err := initializeConfig(true, true, &sc.hugoBuilderCommon, sc, cfgInit) - if err != nil { - return err - } - - if err := c.serverBuild(); err != nil { - return err - } - - for _, s := range c.hugo().Sites { - s.RegisterMediaTypes() - } - - // Watch runs its own server as part of the routine - if sc.serverWatch { - - watchDirs, err := c.getDirList() - if err != nil { - return err - } - - watchGroups := helpers.ExtractAndGroupRootPaths(watchDirs) - - for _, group := range watchGroups { - jww.FEEDBACK.Printf("Watching for changes in %s\n", group) - } - watcher, err := c.newWatcher(watchDirs...) - - if err != nil { - return err - } - - defer watcher.Close() - - } - - return c.serve(sc) - -} - -func getRootWatchDirsStr(baseDir string, watchDirs []string) string { - relWatchDirs := make([]string, len(watchDirs)) - for i, dir := range watchDirs { - relWatchDirs[i], _ = helpers.GetRelativePath(dir, baseDir) - } - - return strings.Join(helpers.UniqueStringsSorted(helpers.ExtractRootPaths(relWatchDirs)), ",") -} - -type fileServer struct { - baseURLs []string - roots []string - errorTemplate tpl.Template - c *commandeer - s *serverCmd -} - -func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, error) { - baseURL := f.baseURLs[i] - root := f.roots[i] - port := f.c.serverPorts[i] - - publishDir := f.c.Cfg.GetString("publishDir") - - if root != "" { - publishDir = filepath.Join(publishDir, root) - } - - absPublishDir := f.c.hugo().PathSpec.AbsPathify(publishDir) - - jww.FEEDBACK.Printf("Environment: %q", f.c.hugo().Deps.Site.Hugo().Environment) - - if i == 0 { - if f.s.renderToDisk { - jww.FEEDBACK.Println("Serving pages from " + absPublishDir) - } else { - jww.FEEDBACK.Println("Serving pages from memory") - } - } - - httpFs := afero.NewHttpFs(f.c.destinationFs) - fs := filesOnlyFs{httpFs.Dir(absPublishDir)} - - if i == 0 && f.c.fastRenderMode { - jww.FEEDBACK.Println("Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender") - } - - // We're only interested in the path - u, err := url.Parse(baseURL) - if err != nil { - return nil, "", "", errors.Wrap(err, "Invalid baseURL") - } - - 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 { - w.WriteHeader(500) - var b bytes.Buffer - err := f.errorTemplate.Execute(&b, err) - if err != nil { - f.c.logger.ERROR.Println(err) - } - port = 1313 - if !f.c.paused { - port = f.c.Cfg.GetInt("liveReloadPort") - } - fmt.Fprint(w, injectLiveReloadScript(&b, port)) - - return - } - } - - if f.s.noHTTPCache { - w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0") - w.Header().Set("Pragma", "no-cache") - } - - if f.c.fastRenderMode && f.c.buildErr == nil { - p := r.RequestURI - if strings.HasSuffix(p, "/") || strings.HasSuffix(p, "html") || strings.HasSuffix(p, "htm") { - if !f.c.visitedURLs.Contains(p) { - // If not already on stack, re-render that single page. - if err := f.c.partialReRender(p); err != nil { - f.c.handleBuildErr(err, fmt.Sprintf("Failed to render %q", p)) - if f.c.showErrorInBrowser { - http.Redirect(w, r, p, http.StatusMovedPermanently) - return - } - } - } - - f.c.visitedURLs.Add(p) - - } - } - h.ServeHTTP(w, r) - }) - } - - fileserver := decorate(http.FileServer(fs)) - mu := http.NewServeMux() - - if u.Path == "" || u.Path == "/" { - mu.Handle("/", fileserver) - } else { - mu.Handle(u.Path, http.StripPrefix(u.Path, fileserver)) - } - - endpoint := net.JoinHostPort(f.s.serverInterface, strconv.Itoa(port)) - - return mu, u.String(), endpoint, nil -} - -var logErrorRe = regexp.MustCompile(`(?s)ERROR \d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} `) - -func removeErrorPrefixFromLog(content string) string { - return logErrorRe.ReplaceAllLiteralString(content, "") -} -func (c *commandeer) serve(s *serverCmd) error { - - isMultiHost := c.hugo().IsMultihost() - - var ( - baseURLs []string - roots []string - ) - - if isMultiHost { - for _, s := range c.hugo().Sites { - baseURLs = append(baseURLs, s.BaseURL.String()) - roots = append(roots, s.Language().Lang) - } - } else { - s := c.hugo().Sites[0] - baseURLs = []string{s.BaseURL.String()} - roots = []string{""} - } - - templ, err := c.hugo().TextTmpl.Parse("__default_server_error", buildErrorTemplate) - if err != nil { - return err - } - - srv := &fileServer{ - baseURLs: baseURLs, - roots: roots, - c: c, - s: s, - errorTemplate: templ, - } - - doLiveReload := !c.Cfg.GetBool("disableLiveReload") - - if doLiveReload { - livereload.Initialize() - } - - var sigs = make(chan os.Signal, 1) - signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) - - for i := range baseURLs { - mu, serverURL, endpoint, err := srv.createEndpoint(i) - - if doLiveReload { - mu.HandleFunc("/livereload.js", livereload.ServeJS) - mu.HandleFunc("/livereload", livereload.Handler) - } - jww.FEEDBACK.Printf("Web Server is available at %s (bind address %s)\n", serverURL, s.serverInterface) - go func() { - err = http.ListenAndServe(endpoint, mu) - if err != nil { - c.logger.ERROR.Printf("Error: %s\n", err.Error()) - os.Exit(1) - } - }() - } - - jww.FEEDBACK.Println("Press Ctrl+C to stop") - - if s.stop != nil { - select { - case <-sigs: - case <-s.stop: - } - } else { - <-sigs - } - - return nil -} - -// fixURL massages the baseURL into a form needed for serving -// all pages correctly. -func (sc *serverCmd) fixURL(cfg config.Provider, s string, port int) (string, error) { - useLocalhost := false - if s == "" { - s = cfg.GetString("baseURL") - useLocalhost = true - } - - if !strings.HasSuffix(s, "/") { - s = s + "/" - } - - // do an initial parse of the input string - 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 == "" && s != "/" { - s = "//" + s - - u, err = url.Parse(s) - if err != nil { - return "", err - } - } - - if useLocalhost { - if u.Scheme == "https" { - u.Scheme = "http" - } - u.Host = "localhost" - } - - if sc.serverAppend { - if strings.Contains(u.Host, ":") { - u.Host, _, err = net.SplitHostPort(u.Host) - if err != nil { - return "", errors.Wrap(err, "Failed to split baseURL hostpost") - } - } - u.Host += fmt.Sprintf(":%d", port) - } - - return u.String(), nil -} - -func memStats() error { - b := newCommandsBuilder() - sc := b.newServerCmd().getCommand() - memstats := sc.Flags().Lookup("memstats").Value.String() - if memstats != "" { - interval, err := time.ParseDuration(sc.Flags().Lookup("meminterval").Value.String()) - if err != nil { - interval, _ = time.ParseDuration("100ms") - } - - fileMemStats, err := os.Create(memstats) - if err != nil { - return err - } - - fileMemStats.WriteString("# Time\tHeapSys\tHeapAlloc\tHeapIdle\tHeapReleased\n") - - go func() { - var stats runtime.MemStats - - start := time.Now().UnixNano() - - 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 { - break - } - } - }() - } - return nil -} diff --git a/commands/server_errors.go b/commands/server_errors.go deleted file mode 100644 index 9f13c9d8c..000000000 --- a/commands/server_errors.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 commands - -import ( - "bytes" - "io" - - "github.com/gohugoio/hugo/transform" - "github.com/gohugoio/hugo/transform/livereloadinject" -) - -var buildErrorTemplate = ` - - - - Hugo Server: Error - - - -
- {{ highlight .Error "apl" "noclasses=true,style=monokai" }} - {{ with .File }} - {{ $params := printf "noclasses=true,style=monokai,linenos=table,hl_lines=%d,linenostart=%d" (add .LinesPos 1) (sub .Position.LineNumber .LinesPos) }} - {{ $lexer := .ChromaLexer | default "go-html-template" }} - {{ highlight (delimit .Lines "\n") $lexer $params }} - {{ end }} - {{ with .StackTrace }} - {{ highlight . "apl" "noclasses=true,style=monokai" }} - {{ end }} -

{{ .Version }}

- Reload Page -
- - -` - -func injectLiveReloadScript(src io.Reader, port int) string { - var b bytes.Buffer - chain := transform.Chain{livereloadinject.New(port)} - chain.Apply(&b, src) - - return b.String() -} diff --git a/commands/server_test.go b/commands/server_test.go deleted file mode 100644 index 8bd96c6ec..000000000 --- a/commands/server_test.go +++ /dev/null @@ -1,134 +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 commands - -import ( - "fmt" - "net/http" - "os" - "runtime" - "strings" - "testing" - "time" - - "github.com/gohugoio/hugo/helpers" - - qt "github.com/frankban/quicktest" - "github.com/spf13/viper" -) - -func TestServer(t *testing.T) { - if isWindowsCI() { - // TODO(bep) not sure why server tests have started to fail on the Windows CI server. - t.Skip("Skip server test on appveyor") - } - c := qt.New(t) - dir, err := createSimpleTestSite(t, testSiteConfig{}) - c.Assert(err, qt.IsNil) - - // Let us hope that this port is available on all systems ... - port := 1331 - - defer func() { - os.RemoveAll(dir) - }() - - stop := make(chan bool) - - b := newCommandsBuilder() - scmd := b.newServerCmdSignaled(stop) - - cmd := scmd.getCommand() - cmd.SetArgs([]string{"-s=" + dir, fmt.Sprintf("-p=%d", port)}) - - go func() { - _, err = cmd.ExecuteC() - c.Assert(err, qt.IsNil) - }() - - // There is no way to know exactly when the server is ready for connections. - // We could improve by something like https://golang.org/pkg/net/http/httptest/#Server - // But for now, let us sleep and pray! - time.Sleep(2 * time.Second) - - resp, err := http.Get("http://localhost:1331/") - c.Assert(err, qt.IsNil) - defer resp.Body.Close() - homeContent := helpers.ReaderToString(resp.Body) - - c.Assert(homeContent, qt.Contains, "List: Hugo Commands") - c.Assert(homeContent, qt.Contains, "Environment: development") - - // Stop the server. - stop <- true - -} - -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 _, test := range tests { - t.Run(test.TestName, func(t *testing.T) { - b := newCommandsBuilder() - s := b.newServerCmd() - v := viper.New() - baseURL := test.CLIBaseURL - v.Set("baseURL", test.CfgBaseURL) - s.serverAppend = test.AppendPort - s.serverPort = test.Port - result, err := s.fixURL(v, baseURL, s.serverPort) - if err != nil { - t.Errorf("Unexpected error %s", err) - } - if result != test.Result { - t.Errorf("Expected %q, got %q", test.Result, result) - } - }) - } -} - -func TestRemoveErrorPrefixFromLog(t *testing.T) { - c := qt.New(t) - content := `ERROR 2018/10/07 13:11:12 Error while rendering "home": template: _default/baseof.html:4:3: executing "main" at : error calling partial: template: partials/logo.html:5:84: executing "partials/logo.html" at <$resized.AHeight>: can't evaluate field AHeight in type *resource.Image -ERROR 2018/10/07 13:11:12 Rebuild failed: logged 1 error(s) -` - - withoutError := removeErrorPrefixFromLog(content) - - c.Assert(strings.Contains(withoutError, "ERROR"), qt.Equals, false) - -} - -func isWindowsCI() bool { - return runtime.GOOS == "windows" && os.Getenv("CI") != "" -} diff --git a/commands/static_syncer.go b/commands/static_syncer.go deleted file mode 100644 index 62ef28b2c..000000000 --- a/commands/static_syncer.go +++ /dev/null @@ -1,132 +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 commands - -import ( - "os" - "path/filepath" - - "github.com/gohugoio/hugo/hugolib/filesystems" - - "github.com/fsnotify/fsnotify" - "github.com/gohugoio/hugo/helpers" - "github.com/spf13/fsync" -) - -type staticSyncer struct { - c *commandeer -} - -func newStaticSyncer(c *commandeer) (*staticSyncer, error) { - return &staticSyncer{c: c}, nil -} - -func (s *staticSyncer) isStatic(filename string) bool { - return s.c.hugo().BaseFs.SourceFilesystems.IsStatic(filename) -} - -func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error { - c := s.c - - syncFn := func(sourceFs *filesystems.SourceFilesystem) (uint64, error) { - publishDir := c.hugo().PathSpec.PublishDir - // If root, remove the second '/' - if publishDir == "//" { - publishDir = helpers.FilePathSeparator - } - - if sourceFs.PublishFolder != "" { - publishDir = filepath.Join(publishDir, sourceFs.PublishFolder) - } - - syncer := fsync.NewSyncer() - syncer.NoTimes = c.Cfg.GetBool("noTimes") - syncer.NoChmod = c.Cfg.GetBool("noChmod") - syncer.ChmodFilter = chmodFilter - syncer.SrcFs = sourceFs.Fs - 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 - - relPath := sourceFs.MakePathRelative(fromPath) - - if relPath == "" { - // 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); 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) - } - } - - return 0, nil - } - - _, err := c.doWithPublishDirs(syncFn) - return err - -} diff --git a/commands/version.go b/commands/version.go deleted file mode 100644 index 287950a2d..000000000 --- a/commands/version.go +++ /dev/null @@ -1,44 +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 commands - -import ( - "github.com/gohugoio/hugo/common/hugo" - "github.com/spf13/cobra" - jww "github.com/spf13/jwalterweatherman" -) - -var _ cmder = (*versionCmd)(nil) - -type versionCmd struct { - *baseCmd -} - -func newVersionCmd() *versionCmd { - return &versionCmd{ - newBaseCmd(&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() { - jww.FEEDBACK.Println(hugo.BuildVersionString()) -} diff --git a/common/collections/append.go b/common/collections/append.go deleted file mode 100644 index b56455bc9..000000000 --- a/common/collections/append.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 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 interface{}, from ...interface{}) (interface{}, error) { - tov, toIsNil := indirect(reflect.ValueOf(to)) - - toIsNil = toIsNil || to == nil - var tot reflect.Type - - if !toIsNil { - if tov.Kind() != reflect.Slice { - return nil, fmt.Errorf("expected a slice, got %T", to) - } - - tot = tov.Type().Elem() - toIsNil = tov.Len() == 0 - - if len(from) == 1 { - fromv := reflect.ValueOf(from[0]) - if fromv.Kind() == reflect.Slice { - if toIsNil { - // If we get nil []string, we just return the []string - return from[0], nil - } - - fromt := reflect.TypeOf(from[0]).Elem() - - // If we get []string []string, we append the from slice to to - if tot == fromt { - return reflect.AppendSlice(tov, fromv).Interface(), nil - } 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.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) ([]interface{}, error) { - var tos []interface{} - - for _, slice := range []reflect.Value{slice1, slice2} { - for i := 0; i < slice.Len(); i++ { - tos = append(tos, slice.Index(i).Interface()) - } - } - - return tos, nil -} - -func appendToInterfaceSlice(tov reflect.Value, from ...interface{}) ([]interface{}, error) { - var tos []interface{} - - for i := 0; i < tov.Len(); i++ { - 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 4086570b8..000000000 --- a/common/collections/append_test.go +++ /dev/null @@ -1,75 +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" - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestAppend(t *testing.T) { - t.Parallel() - c := qt.New(t) - - for _, test := range []struct { - start interface{} - addend []interface{} - expected interface{} - }{ - {[]string{"a", "b"}, []interface{}{"c"}, []string{"a", "b", "c"}}, - {[]string{"a", "b"}, []interface{}{"c", "d", "e"}, []string{"a", "b", "c", "d", "e"}}, - {[]string{"a", "b"}, []interface{}{[]string{"c", "d", "e"}}, []string{"a", "b", "c", "d", "e"}}, - {[]string{"a"}, []interface{}{"b", template.HTML("c")}, []interface{}{"a", "b", template.HTML("c")}}, - {nil, []interface{}{"a", "b"}, []string{"a", "b"}}, - {nil, []interface{}{nil}, []interface{}{nil}}, - {[]interface{}{}, []interface{}{[]string{"c", "d", "e"}}, []string{"c", "d", "e"}}, - {tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}}, - []interface{}{&tstSlicer{"c"}}, - tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}, &tstSlicer{"c"}}}, - {&tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}}, - []interface{}{&tstSlicer{"c"}}, - tstSlicers{&tstSlicer{"a"}, - &tstSlicer{"b"}, - &tstSlicer{"c"}}}, - {testSlicerInterfaces{&tstSlicerIn1{"a"}, &tstSlicerIn1{"b"}}, - []interface{}{&tstSlicerIn1{"c"}}, - testSlicerInterfaces{&tstSlicerIn1{"a"}, &tstSlicerIn1{"b"}, &tstSlicerIn1{"c"}}}, - //https://github.com/gohugoio/hugo/issues/5361 - {[]string{"a", "b"}, []interface{}{tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}}}, - []interface{}{"a", "b", &tstSlicer{"a"}, &tstSlicer{"b"}}}, - {[]string{"a", "b"}, []interface{}{&tstSlicer{"a"}}, - []interface{}{"a", "b", &tstSlicer{"a"}}}, - // Errors - {"", []interface{}{[]string{"a", "b"}}, false}, - // No string concatenation. - {"ab", - []interface{}{"c"}, - false}, - } { - - 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) - } - -} diff --git a/common/collections/collections.go b/common/collections/collections.go deleted file mode 100644 index bb47c8acc..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 interface{}, items interface{}) (interface{}, error) -} diff --git a/common/collections/slice.go b/common/collections/slice.go deleted file mode 100644 index 38ca86b08..000000000 --- a/common/collections/slice.go +++ /dev/null @@ -1,66 +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" -) - -// 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 interface{}) (interface{}, error) -} - -// Slice returns a slice of all passed arguments. -func Slice(args ...interface{}) interface{} { - 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() -} diff --git a/common/collections/slice_test.go b/common/collections/slice_test.go deleted file mode 100644 index 3ebfe6d11..000000000 --- a/common/collections/slice_test.go +++ /dev/null @@ -1,124 +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) -var _ Slicer = (*tstSlicerIn1)(nil) -var _ Slicer = (*tstSlicerIn2)(nil) -var _ testSlicerInterface = (*tstSlicerIn1)(nil) -var _ 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 interface{}) (interface{}, error) { - items := in.([]interface{}) - 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 interface{}) (interface{}, error) { - items := in.([]interface{}) - 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 interface{}) (interface{}, error) { - items := in.([]interface{}) - 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 []interface{} - expected interface{} - }{ - {[]interface{}{"a", "b"}, []string{"a", "b"}}, - {[]interface{}{&tstSlicer{"a"}, &tstSlicer{"b"}}, tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}}}, - {[]interface{}{&tstSlicer{"a"}, "b"}, []interface{}{&tstSlicer{"a"}, "b"}}, - {[]interface{}{}, []interface{}{}}, - {[]interface{}{nil}, []interface{}{nil}}, - {[]interface{}{5, "b"}, []interface{}{5, "b"}}, - {[]interface{}{&tstSlicerIn1{"a"}, &tstSlicerIn2{"b"}}, testSlicerInterfaces{&tstSlicerIn1{"a"}, &tstSlicerIn2{"b"}}}, - {[]interface{}{&tstSlicerIn1{"a"}, &tstSlicer{"b"}}, []interface{}{&tstSlicerIn1{"a"}, &tstSlicer{"b"}}}, - } { - errMsg := qt.Commentf("[%d] %v", i, test.args) - - result := Slice(test.args...) - - c.Assert(test.expected, qt.DeepEquals, result, errMsg) - } - -} diff --git a/common/herrors/error_locator.go b/common/herrors/error_locator.go deleted file mode 100644 index 3778a3729..000000000 --- a/common/herrors/error_locator.go +++ /dev/null @@ -1,255 +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 errors contains common Hugo errors and error related utilities. -package herrors - -import ( - "io" - "io/ioutil" - "path/filepath" - "strings" - - "github.com/gohugoio/hugo/common/text" - - "github.com/spf13/afero" -) - -// 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. -type LineMatcherFn func(m LineMatcher) bool - -// SimpleLineMatcher simply matches by line number. -var SimpleLineMatcher = func(m LineMatcher) bool { - return m.Position.LineNumber == m.LineNumber -} - -var _ text.Positioner = ErrorContext{} - -// 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 - - position text.Position - - // The lexer to use for syntax highlighting. - // https://gohugo.io/content-management/syntax-highlighting/#list-of-chroma-highlighting-languages - ChromaLexer string -} - -// Position returns the text position of this error. -func (e ErrorContext) Position() text.Position { - return e.position -} - -var _ causer = (*ErrorWithFileContext)(nil) - -// ErrorWithFileContext is an error with some additional file context related -// to that error. -type ErrorWithFileContext struct { - cause error - ErrorContext -} - -func (e *ErrorWithFileContext) Error() string { - pos := e.Position() - if pos.IsValid() { - return pos.String() + ": " + e.cause.Error() - } - return e.cause.Error() -} - -func (e *ErrorWithFileContext) Cause() error { - return e.cause -} - -// WithFileContextForFile will try to add a file context with lines matching the given matcher. -// If no match could be found, the original error is returned with false as the second return value. -func WithFileContextForFile(e error, realFilename, filename string, fs afero.Fs, matcher LineMatcherFn) (error, bool) { - f, err := fs.Open(filename) - if err != nil { - return e, false - } - defer f.Close() - return WithFileContext(e, realFilename, f, matcher) -} - -// WithFileContextForFile will try to add a file context with lines matching the given matcher. -// If no match could be found, the original error is returned with false as the second return value. -func WithFileContext(e error, realFilename string, r io.Reader, matcher LineMatcherFn) (error, bool) { - if e == nil { - panic("error missing") - } - le := UnwrapFileError(e) - - if le == nil { - var ok bool - if le, ok = ToFileError("", e).(FileError); !ok { - return e, false - } - } - - var errCtx ErrorContext - - posle := le.Position() - - if posle.Offset != -1 { - errCtx = locateError(r, le, func(m LineMatcher) bool { - 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 matcher(m) - }) - } else { - errCtx = locateError(r, le, matcher) - } - - pos := &errCtx.position - - if pos.LineNumber == -1 { - return e, false - } - - pos.Filename = realFilename - - if le.Type() != "" { - errCtx.ChromaLexer = chromaLexerFromType(le.Type()) - } else { - errCtx.ChromaLexer = chromaLexerFromFilename(realFilename) - } - - return &ErrorWithFileContext{cause: e, ErrorContext: errCtx}, true -} - -// UnwrapErrorWithFileContext tries to unwrap an ErrorWithFileContext from err. -// It returns nil if this is not possible. -func UnwrapErrorWithFileContext(err error) *ErrorWithFileContext { - for err != nil { - switch v := err.(type) { - case *ErrorWithFileContext: - return v - case causer: - err = v.Cause() - default: - return nil - } - } - return nil -} - -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") - } - - errCtx := ErrorContext{position: text.Position{LineNumber: -1, ColumnNumber: 1, Offset: -1}, LinesPos: -1} - - b, err := ioutil.ReadAll(r) - if err != nil { - return errCtx - } - - pos := &errCtx.position - lepos := le.Position() - - lines := strings.Split(string(b), "\n") - - if lepos.ColumnNumber >= 0 { - pos.ColumnNumber = lepos.ColumnNumber - } - - 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, - } - if errCtx.LinesPos == -1 && matches(m) { - pos.LineNumber = lineNo - break - } - - posBytes += len(line) - } - - if pos.LineNumber != -1 { - low := pos.LineNumber - 3 - if low < 0 { - low = 0 - } - - if pos.LineNumber > 2 { - errCtx.LinesPos = 2 - } else { - errCtx.LinesPos = pos.LineNumber - 1 - } - - high := pos.LineNumber + 2 - if high > len(lines) { - high = len(lines) - } - - errCtx.Lines = lines[low:high] - - } - - return errCtx -} diff --git a/common/herrors/error_locator_test.go b/common/herrors/error_locator_test.go deleted file mode 100644 index cce94166f..000000000 --- a/common/herrors/error_locator_test.go +++ /dev/null @@ -1,129 +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 errors 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) bool { - return strings.Contains(m.Line, "THEONE") - } - - lines := `LINE 1 -LINE 2 -LINE 3 -LINE 4 -This is THEONE -LINE 6 -LINE 7 -LINE 8 -` - - location := locateErrorInString(lines, lineMatcher) - c.Assert(location.Lines, qt.DeepEquals, []string{"LINE 3", "LINE 4", "This is THEONE", "LINE 6", "LINE 7"}) - - pos := location.Position() - c.Assert(pos.LineNumber, qt.Equals, 5) - c.Assert(location.LinesPos, qt.Equals, 2) - - c.Assert(locateErrorInString(`This is THEONE`, lineMatcher).Lines, qt.DeepEquals, []string{"This is THEONE"}) - - location = locateErrorInString(`L1 -This is THEONE -L2 -`, lineMatcher) - c.Assert(location.Position().LineNumber, qt.Equals, 2) - c.Assert(location.LinesPos, qt.Equals, 1) - c.Assert(location.Lines, qt.DeepEquals, []string{"L1", "This is THEONE", "L2", ""}) - - location = locateErrorInString(`This is THEONE -L2 -`, lineMatcher) - c.Assert(location.LinesPos, qt.Equals, 0) - c.Assert(location.Lines, qt.DeepEquals, []string{"This is THEONE", "L2", ""}) - - location = locateErrorInString(`L1 -This THEONE -`, lineMatcher) - c.Assert(location.Lines, qt.DeepEquals, []string{"L1", "This THEONE", ""}) - c.Assert(location.LinesPos, qt.Equals, 1) - - location = locateErrorInString(`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) - c.Assert(location.Position().LineNumber, qt.Equals, -1) - c.Assert(location.LinesPos, qt.Equals, -1) - c.Assert(len(location.Lines), qt.Equals, 0) - - lineMatcher = func(m LineMatcher) bool { - return m.LineNumber == 6 - } - - location = locateErrorInString(`A -B -C -D -E -F -G -H -I -J`, lineMatcher) - - c.Assert(location.Lines, qt.DeepEquals, []string{"D", "E", "F", "G", "H"}) - c.Assert(location.Position().LineNumber, qt.Equals, 6) - c.Assert(location.LinesPos, qt.Equals, 2) - - // Test match EOF - lineMatcher = func(m LineMatcher) bool { - return m.LineNumber == 4 - } - - location = locateErrorInString(`A -B -C -`, lineMatcher) - - c.Assert(location.Lines, qt.DeepEquals, []string{"B", "C", ""}) - c.Assert(location.Position().LineNumber, qt.Equals, 4) - c.Assert(location.LinesPos, qt.Equals, 2) - - offsetMatcher := func(m LineMatcher) bool { - return m.Offset == 1 - } - - location = locateErrorInString(`A -B -C -D -E`, offsetMatcher) - - c.Assert(location.Lines, qt.DeepEquals, []string{"A", "B", "C", "D"}) - c.Assert(location.Position().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 ff8eab116..000000000 --- a/common/herrors/errors.go +++ /dev/null @@ -1,66 +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 contains common Hugo errors and error related utilities. -package herrors - -import ( - "errors" - "fmt" - "io" - "os" - "runtime/debug" - - _errors "github.com/pkg/errors" -) - -// As defined in https://godoc.org/github.com/pkg/errors -type causer interface { - Cause() error -} - -type stackTracer interface { - StackTrace() _errors.StackTrace -} - -// PrintStackTrace prints the error's stack trace to stdoud. -func PrintStackTrace(err error) { - FprintStackTrace(os.Stdout, err) -} - -// FprintStackTrace prints the error's stack trace to w. -func FprintStackTrace(w io.Writer, err error) { - if err, ok := err.(stackTracer); ok { - for _, f := range err.StackTrace() { - fmt.Fprintf(w, "%+s:%d\n", f, f) - } - } -} - -// 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 ...interface{}) { - if r := recover(); r != nil { - fmt.Println("ERR:", r) - args = append(args, "stacktrace from panic: \n"+string(debug.Stack()), "\n") - fmt.Println(args...) - } - -} - -// ErrFeatureNotAvailable denotes that a feature is unavailable. -// -// We will, at least to begin with, make some Hugo features (SCSS with libsass) optional, -// and this error is used to signal those situations. -var ErrFeatureNotAvailable = errors.New("this feature is not available in your current Hugo version, see https://goo.gl/YMrWcn for more information") diff --git a/common/herrors/file_error.go b/common/herrors/file_error.go deleted file mode 100644 index 5af84adf5..000000000 --- a/common/herrors/file_error.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 -// limitatio ns under the License. - -package herrors - -import ( - "encoding/json" - - "github.com/gohugoio/hugo/common/text" - - "github.com/pkg/errors" -) - -var ( - _ causer = (*fileError)(nil) -) - -// FileError represents an error when handling a file: Parsing a config file, -// execute a template etc. -type FileError interface { - error - - text.Positioner - - // A string identifying the type of file, e.g. JSON, TOML, markdown etc. - Type() string -} - -var _ FileError = (*fileError)(nil) - -type fileError struct { - position text.Position - - fileType string - - cause error -} - -// Position returns the text position of this error. -func (e fileError) Position() text.Position { - return e.position -} - -func (e *fileError) Type() string { - return e.fileType -} - -func (e *fileError) Error() string { - if e.cause == nil { - return "" - } - return e.cause.Error() -} - -func (f *fileError) Cause() error { - return f.cause -} - -// NewFileError creates a new FileError. -func NewFileError(fileType string, offset, lineNumber, columnNumber int, err error) FileError { - pos := text.Position{Offset: offset, LineNumber: lineNumber, ColumnNumber: columnNumber} - return &fileError{cause: err, fileType: fileType, position: 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 - case causer: - err = v.Cause() - default: - return nil - } - } - return nil -} - -// ToFileErrorWithOffset will return a new FileError with a line number -// with the given offset from the original. -func ToFileErrorWithOffset(fe FileError, offset int) FileError { - pos := fe.Position() - return ToFileErrorWithLineNumber(fe, pos.LineNumber+offset) -} - -// ToFileErrorWithOffset will return a new FileError with the given line number. -func ToFileErrorWithLineNumber(fe FileError, lineNumber int) FileError { - pos := fe.Position() - pos.LineNumber = lineNumber - return &fileError{cause: fe, fileType: fe.Type(), position: pos} -} - -// ToFileError will convert the given error to an error supporting -// the FileError interface. -func ToFileError(fileType string, err error) FileError { - for _, handle := range lineNumberExtractors { - lno, col := handle(err) - offset, typ := extractOffsetAndType(err) - if fileType == "" { - fileType = typ - } - - if lno > 0 || offset != -1 { - return NewFileError(fileType, offset, lno, col, err) - } - } - // Fall back to the pointing to line number 1. - return NewFileError(fileType, -1, 1, 1, err) -} - -func extractOffsetAndType(e error) (int, string) { - e = errors.Cause(e) - switch v := e.(type) { - case *json.UnmarshalTypeError: - return int(v.Offset), "json" - case *json.SyntaxError: - return int(v.Offset), "json" - default: - return -1, "" - } -} diff --git a/common/herrors/file_error_test.go b/common/herrors/file_error_test.go deleted file mode 100644 index b1b5c5a02..000000000 --- a/common/herrors/file_error_test.go +++ /dev/null @@ -1,56 +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 ( - "testing" - - "github.com/pkg/errors" - - qt "github.com/frankban/quicktest" -) - -func TestToLineNumberError(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("error in front matter: Near line 32 (last key parsed 'title')"), 0, 32, 1}, - {errors.New(`failed to load translations: (6, 7): was expecting token =, but got "g" instead`), 0, 6, 7}, - } { - - got := ToFileError("template", test.in) - - errMsg := qt.Commentf("[%d][%T]", i, got) - le, ok := got.(FileError) - c.Assert(ok, qt.Equals, true) - - c.Assert(ok, qt.Equals, true, errMsg) - pos := le.Position() - c.Assert(pos.LineNumber, qt.Equals, test.lineNumber, errMsg) - c.Assert(pos.ColumnNumber, qt.Equals, test.columnNumber, errMsg) - c.Assert(errors.Cause(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 93969b967..000000000 --- a/common/herrors/line_number_extractors.go +++ /dev/null @@ -1,66 +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 -// limitatio ns under the License. - -package herrors - -import ( - "regexp" - "strconv" -) - -var lineNumberExtractors = []lineNumberExtractor{ - // Template/shortcode parse errors - newLineNumberErrHandlerFromRegexp(".*:(\\d+):(\\d*):"), - newLineNumberErrHandlerFromRegexp(".*:(\\d+):"), - - // TOML parse errors - newLineNumberErrHandlerFromRegexp(".*Near line (\\d+)(\\s.*)"), - - // 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 -1, col - } -} diff --git a/common/hreflect/helpers.go b/common/hreflect/helpers.go deleted file mode 100644 index db7b208b5..000000000 --- a/common/hreflect/helpers.go +++ /dev/null @@ -1,91 +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 hreflect contains reflect helpers. -package hreflect - -import ( - "reflect" - - "github.com/gohugoio/hugo/common/types" -) - -// IsTruthful returns whether in represents a truthful value. -// See IsTruthfulValue -func IsTruthful(in interface{}) bool { - switch v := in.(type) { - case reflect.Value: - return IsTruthfulValue(v) - default: - return IsTruthfulValue(reflect.ValueOf(in)) - } - -} - -var zeroType = reflect.TypeOf((*types.Zeroer)(nil)).Elem() - -// IsTruthfulValue returns whether the given value has a meaningful truth value. -// This is based on template.IsTrue in Go's stdlib, but also considers -// IsZero and any interface value will be unwrapped before it's considered -// for truthfulness. -// -// Based on: -// https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L306 -func IsTruthfulValue(val reflect.Value) (truth bool) { - val = indirectInterface(val) - - if !val.IsValid() { - // Something like var x interface{}, never set. It's a form of nil. - return - } - - if val.Type().Implements(zeroType) { - return !val.Interface().(types.Zeroer).IsZero() - } - - switch val.Kind() { - case reflect.Array, reflect.Map, reflect.Slice, reflect.String: - truth = val.Len() > 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 -} - -// 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() -} diff --git a/common/hreflect/helpers_test.go b/common/hreflect/helpers_test.go deleted file mode 100644 index 480ccb27a..000000000 --- a/common/hreflect/helpers_test.go +++ /dev/null @@ -1,42 +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 ( - "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 BenchmarkIsTruthFul(b *testing.B) { - v := reflect.ValueOf("Hugo") - - b.ResetTimer() - for i := 0; i < b.N; i++ { - if !IsTruthfulValue(v) { - b.Fatal("not truthful") - } - } -} diff --git a/common/hugio/copy.go b/common/hugio/copy.go deleted file mode 100644 index 2b756cb44..000000000 --- a/common/hugio/copy.go +++ /dev/null @@ -1,90 +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 ( - "io" - "io/ioutil" - "os" - "path/filepath" - - "github.com/pkg/errors" - - "github.com/spf13/afero" -) - -// CopyFile copies a file. -func CopyFile(fs afero.Fs, from, to string) error { - sf, err := os.Open(from) - if err != nil { - return err - } - defer sf.Close() - df, err := os.Create(to) - if err != nil { - return err - } - defer df.Close() - _, err = io.Copy(df, sf) - if err == nil { - si, err := os.Stat(from) - if err != nil { - err = os.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 := os.Stat(from) - if err != nil { - return err - } - - if !fi.IsDir() { - return errors.Errorf("%q is not a directory", from) - } - - err = fs.MkdirAll(to, 0777) // before umask - if err != nil { - return err - } - - entries, _ := ioutil.ReadDir(from) - 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/readers.go b/common/hugio/readers.go deleted file mode 100644 index 8c901dd24..000000000 --- a/common/hugio/readers.go +++ /dev/null @@ -1,54 +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 ( - "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 -} - -// ReadSeekerNoOpCloser implements ReadSeekCloser by doing nothing in Close. -// TODO(bep) rename this and simila to ReadSeekerNopCloser, naming used in stdlib, which kind of makes sense. -type ReadSeekerNoOpCloser struct { - ReadSeeker -} - -// Close does nothing. -func (r ReadSeekerNoOpCloser) Close() error { - return nil -} - -// NewReadSeekerNoOpCloser creates a new ReadSeekerNoOpCloser with the given ReadSeeker. -func NewReadSeekerNoOpCloser(r ReadSeeker) ReadSeekerNoOpCloser { - return ReadSeekerNoOpCloser{r} -} - -// NewReadSeekerNoOpCloserFromString uses strings.NewReader to create a new ReadSeekerNoOpCloser -// from the given string. -func NewReadSeekerNoOpCloserFromString(content string) ReadSeekerNoOpCloser { - return ReadSeekerNoOpCloser{strings.NewReader(content)} -} diff --git a/common/hugio/writers.go b/common/hugio/writers.go deleted file mode 100644 index 82c4dca52..000000000 --- a/common/hugio/writers.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 hugio - -import ( - "io" - "io/ioutil" -) - -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(); err != 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, - ioutil.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, - ioutil.NopCloser(nil), - } -} diff --git a/common/hugo/hugo.go b/common/hugo/hugo.go deleted file mode 100644 index 62d923bf0..000000000 --- a/common/hugo/hugo.go +++ /dev/null @@ -1,67 +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" - "html/template" -) - -const ( - EnvironmentDevelopment = "development" - EnvironmentProduction = "production" -) - -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 -) - -// Info contains information about the current Hugo environment -type Info 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 returns the current version as a comparable version string. -func (i Info) Version() VersionString { - return CurrentVersion.Version() -} - -// Generator a Hugo meta generator HTML tag. -func (i Info) Generator() template.HTML { - return template.HTML(fmt.Sprintf(``, CurrentVersion.String())) -} - -// NewInfo creates a new Hugo Info object. -func NewInfo(environment string) Info { - if environment == "" { - environment = EnvironmentProduction - } - return Info{ - CommitHash: commitHash, - BuildDate: buildDate, - Environment: environment, - } -} diff --git a/common/hugo/hugo_test.go b/common/hugo/hugo_test.go deleted file mode 100644 index 5be575b62..000000000 --- a/common/hugo/hugo_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 hugo - -import ( - "fmt" - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestHugoInfo(t *testing.T) { - c := qt.New(t) - - hugoInfo := NewInfo("") - - c.Assert(hugoInfo.Version(), qt.Equals, CurrentVersion.Version()) - c.Assert(fmt.Sprintf("%T", VersionString("")), qt.Equals, fmt.Sprintf("%T", hugoInfo.Version())) - c.Assert(hugoInfo.CommitHash, qt.Equals, commitHash) - c.Assert(hugoInfo.BuildDate, qt.Equals, buildDate) - c.Assert(hugoInfo.Environment, qt.Equals, "production") - c.Assert(string(hugoInfo.Generator()), qt.Contains, fmt.Sprintf("Hugo %s", hugoInfo.Version())) - -} diff --git a/common/hugo/vars_extended.go b/common/hugo/vars_extended.go deleted file mode 100644 index bb96bade6..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. - -// +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 fae18df14..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. - -// +build !extended - -package hugo - -var IsExtended = false diff --git a/common/hugo/version.go b/common/hugo/version.go deleted file mode 100644 index 848393f97..000000000 --- a/common/hugo/version.go +++ /dev/null @@ -1,251 +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" - "strconv" - - "runtime" - "strings" - - "github.com/gohugoio/hugo/compare" - "github.com/spf13/cast" -) - -// Version represents the Hugo build version. -type Version struct { - // Major and minor version. - Number float32 - - // 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.Number, v.PatchLevel, v.Suffix) -} - -// Version returns the Hugo version. -func (v Version) Version() VersionString { - return VersionString(v.String()) -} - -// 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 interface{}) int { - v := MustParseVersion(h.String()) - return compareVersionsWithSuffix(v.Number, v.PatchLevel, v.Suffix, other) -} - -// Eq implements the compare.Eqer interface. -func (h VersionString) Eq(other interface{}) 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) - } - } - - v, p := parseVersion(s) - - vv.Number = v - vv.PatchLevel = p - - 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{Number: v.Number + 0.01} -} - -// Prev returns the previous Hugo release version. -func (v Version) Prev() Version { - return Version{Number: v.Number - 0.01} -} - -// 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 { - return Version{Number: v.Number - 0.01, PatchLevel: level} -} - -// BuildVersionString creates a version string. This is what you see when -// running "hugo version". -func BuildVersionString() string { - program := "Hugo Static Site Generator" - - version := "v" + CurrentVersion.String() - if commitHash != "" { - version += "-" + strings.ToUpper(commitHash) - } - if IsExtended { - version += "/extended" - } - - osArch := runtime.GOOS + "/" + runtime.GOARCH - - date := buildDate - if date == "" { - date = "unknown" - } - - return fmt.Sprintf("%s %s %s BuildDate: %s", program, version, osArch, date) - -} - -func version(version float32, patchVersion int, suffix string) string { - if patchVersion > 0 || version > 0.53 { - return fmt.Sprintf("%.2f.%d%s", version, patchVersion, suffix) - } - return fmt.Sprintf("%.2f%s", version, 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 interface{}) int { - return compareVersionsWithSuffix(CurrentVersion.Number, CurrentVersion.PatchLevel, CurrentVersion.Suffix, version) -} - -func compareVersions(inVersion float32, inPatchVersion int, in interface{}) int { - return compareVersionsWithSuffix(inVersion, inPatchVersion, "", in) -} - -func compareVersionsWithSuffix(inVersion float32, inPatchVersion int, suffix string, in interface{}) int { - var c int - switch d := in.(type) { - case float64: - c = compareFloatVersions(inVersion, float32(d)) - case float32: - c = compareFloatVersions(inVersion, d) - case int: - c = compareFloatVersions(inVersion, float32(d)) - case int32: - c = compareFloatVersions(inVersion, float32(d)) - case int64: - c = compareFloatVersions(inVersion, float32(d)) - default: - s, err := cast.ToStringE(in) - if err != nil { - return -1 - } - - v, err := ParseVersion(s) - if err != nil { - return -1 - } - - if v.Number == inVersion && v.PatchLevel == inPatchVersion { - return strings.Compare(suffix, v.Suffix) - } - - if v.Number < inVersion || (v.Number == inVersion && v.PatchLevel < inPatchVersion) { - return -1 - } - - return 1 - } - - if c == 0 && suffix != "" { - return 1 - } - - return c -} - -func parseVersion(s string) (float32, int) { - var ( - v float32 - p int - ) - - if strings.Count(s, ".") == 2 { - li := strings.LastIndex(s, ".") - p = cast.ToInt(s[li+1:]) - s = s[:li] - } - - v = float32(cast.ToFloat64(s)) - - return v, p -} - -func compareFloatVersions(version float32, v float32) int { - if v == version { - return 0 - } - if v < version { - return -1 - } - return 1 -} - -func GoMinorVersion() int { - return goMinorVersion(runtime.Version()) -} - -func goMinorVersion(version string) int { - if strings.HasPrefix(version, "devel") { - return 9999 // magic - } - i, _ := strconv.Atoi(strings.Split(version, ".")[1]) - return i - -} diff --git a/common/hugo/version_current.go b/common/hugo/version_current.go deleted file mode 100644 index 8e023e264..000000000 --- a/common/hugo/version_current.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 hugo - -// CurrentVersion represents the current build version. -// This should be the only one. -var CurrentVersion = Version{ - Number: 0.59, - PatchLevel: 0, - Suffix: "-DEV", -} diff --git a/common/hugo/version_test.go b/common/hugo/version_test.go deleted file mode 100644 index e0cd0e6e8..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{Number: 0.21, PatchLevel: 0, 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{Number: 0.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{Number: 0.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(0.20, 0, 0.20), qt.Equals, 0) - c.Assert(compareVersions(0.20, 0, float32(0.20)), qt.Equals, 0) - c.Assert(compareVersions(0.20, 0, float64(0.20)), qt.Equals, 0) - c.Assert(compareVersions(0.19, 1, 0.20), qt.Equals, 1) - c.Assert(compareVersions(0.19, 3, "0.20.2"), qt.Equals, 1) - c.Assert(compareVersions(0.19, 1, 0.01), qt.Equals, -1) - c.Assert(compareVersions(0, 1, 3), qt.Equals, 1) - c.Assert(compareVersions(0, 1, int32(3)), qt.Equals, 1) - c.Assert(compareVersions(0, 1, int64(3)), qt.Equals, 1) - c.Assert(compareVersions(0.20, 0, "0.20"), qt.Equals, 0) - c.Assert(compareVersions(0.20, 1, "0.20.1"), qt.Equals, 0) - c.Assert(compareVersions(0.20, 1, "0.20"), qt.Equals, -1) - c.Assert(compareVersions(0.20, 0, "0.20.1"), qt.Equals, 1) - c.Assert(compareVersions(0.20, 1, "0.20.2"), qt.Equals, 1) - c.Assert(compareVersions(0.21, 1, "0.22.1"), qt.Equals, 1) - c.Assert(compareVersions(0.22, 0, "0.22-DEV"), qt.Equals, -1) - c.Assert(compareVersions(0.22, 0, "0.22.1-DEV"), qt.Equals, 1) - c.Assert(compareVersionsWithSuffix(0.22, 0, "-DEV", "0.22"), qt.Equals, 1) - c.Assert(compareVersionsWithSuffix(0.22, 1, "-DEV", "0.22"), qt.Equals, -1) - c.Assert(compareVersionsWithSuffix(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() >= 11, qt.Equals, true) -} diff --git a/common/loggers/loggers.go b/common/loggers/loggers.go deleted file mode 100644 index 082fd1487..000000000 --- a/common/loggers/loggers.go +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright 2018 The Hugo Authors. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package loggers - -import ( - "bytes" - "io" - "io/ioutil" - "log" - "os" - "regexp" - - "github.com/gohugoio/hugo/common/terminal" - - jww "github.com/spf13/jwalterweatherman" -) - -var ( - // Counts ERROR logs to the global jww logger. - GlobalErrorCounter *jww.Counter -) - -func init() { - GlobalErrorCounter = &jww.Counter{} - jww.SetLogListeners(jww.LogCounter(GlobalErrorCounter, jww.LevelError)) -} - -// Logger wraps a *loggers.Logger and some other related logging state. -type Logger struct { - *jww.Notepad - ErrorCounter *jww.Counter - WarnCounter *jww.Counter - - // This is only set in server mode. - errors *bytes.Buffer -} - -func (l *Logger) Errors() string { - if l.errors == nil { - return "" - } - return ansiColorRe.ReplaceAllString(l.errors.String(), "") -} - -// Reset resets the logger's internal state. -func (l *Logger) Reset() { - l.ErrorCounter.Reset() - if l.errors != nil { - l.errors.Reset() - } -} - -// NewLogger creates a new Logger for the given thresholds -func NewLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer, saveErrors bool) *Logger { - return newLogger(stdoutThreshold, logThreshold, outHandle, logHandle, saveErrors) -} - -// NewDebugLogger is a convenience function to create a debug logger. -func NewDebugLogger() *Logger { - return newBasicLogger(jww.LevelDebug) -} - -// NewWarningLogger is a convenience function to create a warning logger. -func NewWarningLogger() *Logger { - return newBasicLogger(jww.LevelWarn) -} - -// NewErrorLogger is a convenience function to create an error logger. -func NewErrorLogger() *Logger { - return newBasicLogger(jww.LevelError) -} - -var ( - ansiColorRe = regexp.MustCompile("(?s)\\033\\[\\d*(;\\d*)*m") - errorRe = regexp.MustCompile("^(ERROR|FATAL|WARN)") -) - -type ansiCleaner struct { - w io.Writer -} - -func (a ansiCleaner) Write(p []byte) (n int, err error) { - return a.w.Write(ansiColorRe.ReplaceAll(p, []byte(""))) -} - -type labelColorizer struct { - w io.Writer -} - -func (a labelColorizer) Write(p []byte) (n int, err error) { - replaced := errorRe.ReplaceAllStringFunc(string(p), func(m string) string { - switch m { - case "ERROR", "FATAL": - return terminal.Error(m) - case "WARN": - return terminal.Warning(m) - default: - return m - } - }) - // io.MultiWriter will abort if we return a bigger write count than input - // bytes, so we lie a little. - _, err = a.w.Write([]byte(replaced)) - return len(p), err - -} - -// InitGlobalLogger initializes the global logger, used in some rare cases. -func InitGlobalLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer) { - outHandle, logHandle = getLogWriters(outHandle, logHandle) - - jww.SetStdoutOutput(outHandle) - jww.SetLogOutput(logHandle) - jww.SetLogThreshold(logThreshold) - jww.SetStdoutThreshold(stdoutThreshold) - -} - -func getLogWriters(outHandle, logHandle io.Writer) (io.Writer, io.Writer) { - isTerm := terminal.IsTerminal(os.Stdout) - if logHandle != ioutil.Discard && isTerm { - // Remove any Ansi coloring from log output - logHandle = ansiCleaner{w: logHandle} - } - - if isTerm { - outHandle = labelColorizer{w: outHandle} - } - - return outHandle, logHandle - -} - -func newLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer, saveErrors bool) *Logger { - errorCounter := &jww.Counter{} - warnCounter := &jww.Counter{} - outHandle, logHandle = getLogWriters(outHandle, logHandle) - - listeners := []jww.LogListener{jww.LogCounter(errorCounter, jww.LevelError), jww.LogCounter(warnCounter, jww.LevelWarn)} - var errorBuff *bytes.Buffer - if saveErrors { - errorBuff = new(bytes.Buffer) - errorCapture := func(t jww.Threshold) io.Writer { - if t != jww.LevelError { - // Only interested in ERROR - return nil - } - - return errorBuff - } - - listeners = append(listeners, errorCapture) - } - - return &Logger{ - Notepad: jww.NewNotepad(stdoutThreshold, logThreshold, outHandle, logHandle, "", log.Ldate|log.Ltime, listeners...), - ErrorCounter: errorCounter, - WarnCounter: warnCounter, - errors: errorBuff, - } -} - -func newBasicLogger(t jww.Threshold) *Logger { - return newLogger(t, jww.LevelError, os.Stdout, ioutil.Discard, false) -} diff --git a/common/loggers/loggers_test.go b/common/loggers/loggers_test.go deleted file mode 100644 index f572ba170..000000000 --- a/common/loggers/loggers_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 loggers - -import ( - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestLogger(t *testing.T) { - c := qt.New(t) - l := NewWarningLogger() - - l.ERROR.Println("One error") - l.ERROR.Println("Two error") - l.WARN.Println("A warning") - - c.Assert(l.ErrorCounter.Count(), qt.Equals, uint64(2)) - -} diff --git a/common/maps/maps.go b/common/maps/maps.go deleted file mode 100644 index e0d4f964d..000000000 --- a/common/maps/maps.go +++ /dev/null @@ -1,116 +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 ( - "strings" - - "github.com/gobwas/glob" - - "github.com/spf13/cast" -) - -// ToLower makes all the keys in the given map lower cased and will do so -// recursively. -// Notes: -// * This will modify the map given. -// * Any nested map[interface{}]interface{} will be converted to map[string]interface{}. -func ToLower(m map[string]interface{}) { - for k, v := range m { - switch v.(type) { - case map[interface{}]interface{}: - v = cast.ToStringMap(v) - ToLower(v.(map[string]interface{})) - case map[string]interface{}: - ToLower(v.(map[string]interface{})) - } - - lKey := strings.ToLower(k) - if k != lKey { - delete(m, k) - m[lKey] = 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]interface{}) { - r.renamePath("", m) -} - -func (KeyRenamer) keyPath(k1, k2 string) string { - k1, k2 = strings.ToLower(k1), strings.ToLower(k2) - if k1 == "" { - return k2 - } else { - return k1 + "/" + k2 - } -} - -func (r KeyRenamer) renamePath(parentKeyPath string, m map[string]interface{}) { - for key, val := range m { - keyPath := r.keyPath(parentKeyPath, key) - switch val.(type) { - case map[interface{}]interface{}: - val = cast.ToStringMap(val) - r.renamePath(keyPath, val.(map[string]interface{})) - case map[string]interface{}: - r.renamePath(keyPath, val.(map[string]interface{})) - } - - newKey := r.getNewKey(keyPath) - - if newKey != "" { - delete(m, key) - m[newKey] = val - } - } -} diff --git a/common/maps/maps_get.go b/common/maps/maps_get.go deleted file mode 100644 index 9289991ae..000000000 --- a/common/maps/maps_get.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 maps - -import ( - "github.com/spf13/cast" -) - -// GetString tries to get a value with key from map m and convert it to a string. -// It will return an empty string if not found or if it cannot be convertd to a string. -func GetString(m map[string]interface{}, key string) string { - if m == nil { - return "" - } - v, found := m[key] - if !found { - return "" - } - return cast.ToString(v) -} diff --git a/common/maps/maps_test.go b/common/maps/maps_test.go deleted file mode 100644 index 8b0aa5eb9..000000000 --- a/common/maps/maps_test.go +++ /dev/null @@ -1,123 +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" - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestToLower(t *testing.T) { - - tests := []struct { - input map[string]interface{} - expected map[string]interface{} - }{ - { - map[string]interface{}{ - "abC": 32, - }, - map[string]interface{}{ - "abc": 32, - }, - }, - { - map[string]interface{}{ - "abC": 32, - "deF": map[interface{}]interface{}{ - 23: "A value", - 24: map[string]interface{}{ - "AbCDe": "A value", - "eFgHi": "Another value", - }, - }, - "gHi": map[string]interface{}{ - "J": 25, - }, - }, - map[string]interface{}{ - "abc": 32, - "def": map[string]interface{}{ - "23": "A value", - "24": map[string]interface{}{ - "abcde": "A value", - "efghi": "Another value", - }, - }, - "ghi": map[string]interface{}{ - "j": 25, - }, - }, - }, - } - - for i, test := range tests { - // ToLower modifies input. - ToLower(test.input) - if !reflect.DeepEqual(test.expected, test.input) { - t.Errorf("[%d] Expected\n%#v, got\n%#v\n", i, test.expected, test.input) - } - } -} - -func TestRenameKeys(t *testing.T) { - c := qt.New(t) - - m := map[string]interface{}{ - "a": 32, - "ren1": "m1", - "ren2": "m1_2", - "sub": map[string]interface{}{ - "subsub": map[string]interface{}{ - "REN1": "m2", - "ren2": "m2_2", - }, - }, - "no": map[string]interface{}{ - "ren1": "m2", - "ren2": "m2_2", - }, - } - - expected := map[string]interface{}{ - "a": 32, - "new1": "m1", - "new2": "m1_2", - "sub": map[string]interface{}{ - "subsub": map[string]interface{}{ - "new1": "m2", - "ren2": "m2_2", - }, - }, - "no": map[string]interface{}{ - "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) - } - -} diff --git a/common/maps/params.go b/common/maps/params.go deleted file mode 100644 index 2d62ad752..000000000 --- a/common/maps/params.go +++ /dev/null @@ -1,94 +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 ( - "strings" - - "github.com/spf13/cast" -) - -// 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 ...map[string]interface{}) (interface{}, error) { - keyStr = strings.ToLower(keyStr) - - lookupFn := func(key string) interface{} { - for _, m := range candidates { - if v, ok := m[key]; ok { - return v - } - } - - return nil - } - - v, _, _, err := GetNestedParamFn(keyStr, separator, lookupFn) - return v, err -} - -func GetNestedParamFn(keyStr, separator string, lookupFn func(key string) interface{}) (interface{}, string, map[string]interface{}, error) { - result, _ := traverseDirectParams(keyStr, lookupFn) - if result != nil { - return result, keyStr, nil, nil - } - - keySegments := strings.Split(keyStr, separator) - if len(keySegments) == 1 { - return nil, keyStr, nil, nil - } - - return traverseNestedParams(keySegments, lookupFn) -} - -func traverseDirectParams(keyStr string, lookupFn func(key string) interface{}) (interface{}, error) { - return lookupFn(keyStr), nil -} - -func traverseNestedParams(keySegments []string, lookupFn func(key string) interface{}) (interface{}, string, map[string]interface{}, error) { - firstKey, rest := keySegments[0], keySegments[1:] - result := lookupFn(firstKey) - if result == nil || len(rest) == 0 { - return result, firstKey, nil, nil - } - - switch m := result.(type) { - case map[string]interface{}: - v, key, owner := traverseParams(rest, m) - return v, key, owner, nil - default: - return nil, "", nil, nil - } -} - -func traverseParams(keys []string, m map[string]interface{}) (interface{}, string, map[string]interface{}) { - // Shift first element off. - firstKey, rest := keys[0], keys[1:] - result := m[firstKey] - - // No point in continuing here. - if result == nil { - return result, "", nil - } - - if len(rest) == 0 { - // That was the last key. - return result, firstKey, m - } - - // That was not the last key. - return traverseParams(rest, cast.ToStringMap(result)) -} diff --git a/common/maps/params_test.go b/common/maps/params_test.go deleted file mode 100644 index 6477de6f4..000000000 --- a/common/maps/params_test.go +++ /dev/null @@ -1,51 +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]interface{}{ - "string": "value", - "first": 1, - "with_underscore": 2, - "nested": map[string]interface{}{ - "color": "blue", - "nestednested": map[string]interface{}{ - "color": "green", - }, - }, - } - - c := qt.New(t) - - must := func(keyStr, separator string, candidates ...map[string]interface{}) interface{} { - 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) - -} diff --git a/common/maps/scratch.go b/common/maps/scratch.go deleted file mode 100644 index 4acd10c6c..000000000 --- a/common/maps/scratch.go +++ /dev/null @@ -1,153 +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" -) - -// Scratch is a writable context used for stateful operations in Page/Node rendering. -type Scratch struct { - values map[string]interface{} - mu sync.RWMutex -} - -// Scratcher provides a scratching service. -type Scratcher interface { - Scratch() *Scratch -} - -type scratcher struct { - s *Scratch -} - -func (s scratcher) Scratch() *Scratch { - return s.s -} - -// NewScratcher creates a new Scratcher. -func NewScratcher() Scratcher { - return scratcher{s: NewScratch()} -} - -// Add will, for single values, add (using the + operator) the addend to the existing addend (if found). -// Supports numeric values and strings. -// -// 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.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 interface{}) 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) 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 -} - -// NewScratch returns a new instance Scratch. -func NewScratch() *Scratch { - return &Scratch{values: make(map[string]interface{})} -} diff --git a/common/maps/scratch_test.go b/common/maps/scratch_test.go deleted file mode 100644 index c2c436e40..000000000 --- a/common/maps/scratch_test.go +++ /dev/null @@ -1,207 +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) - - 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", []interface{}{}) - - _, 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, []interface{}{"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 := 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() - 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, []interface{}{0: "Abc (updated)", 1: "Def", 2: "Lux", 3: "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 cd06379aa..000000000 --- a/common/math/math.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 math - -import ( - "errors" - "reflect" -) - -// DoArithmetic performs arithmetic operations (+,-,*,/) using reflection to -// determine the type of the two terms. -func DoArithmetic(a, b interface{}, op rune) (interface{}, error) { - av := reflect.ValueOf(a) - bv := reflect.ValueOf(b) - var ai, bi int64 - var af, bf float64 - var au, bu uint64 - 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: - bi = bv.Int() - case reflect.Float32, reflect.Float64: - af = float64(ai) // may overflow - ai = 0 - bf = bv.Float() - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - bu = bv.Uint() - if ai >= 0 { - au = uint64(ai) - ai = 0 - } else { - bi = int64(bu) // may overflow - bu = 0 - } - default: - return nil, errors.New("can't apply the operator to the values") - } - case reflect.Float32, reflect.Float64: - 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 { - bu = uint64(bi) - bi = 0 - } else { - ai = int64(au) // may overflow - au = 0 - } - case reflect.Float32, reflect.Float64: - af = float64(au) // may overflow - au = 0 - bf = bv.Float() - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - 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 ai != 0 || bi != 0 { - return ai + bi, nil - } else if af != 0 || bf != 0 { - return af + bf, nil - } else if au != 0 || bu != 0 { - return au + bu, nil - } - return 0, nil - case '-': - if ai != 0 || bi != 0 { - return ai - bi, nil - } else if af != 0 || bf != 0 { - return af - bf, nil - } else if au != 0 || bu != 0 { - return au - bu, nil - } - return 0, nil - case '*': - if ai != 0 || bi != 0 { - return ai * bi, nil - } else if af != 0 || bf != 0 { - return af * bf, nil - } else if au != 0 || bu != 0 { - return au * bu, nil - } - return 0, nil - case '/': - if bi != 0 { - return ai / bi, nil - } else if bf != 0 { - return af / bf, nil - } else if 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 a11701862..000000000 --- a/common/math/math_test.go +++ /dev/null @@ -1,106 +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 interface{} - b interface{} - op rune - expect interface{} - }{ - {3, 2, '+', int64(5)}, - {3, 2, '-', int64(1)}, - {3, 2, '*', int64(6)}, - {3, 2, '/', int64(1)}, - {3.0, 2, '+', float64(5)}, - {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)}, - {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(3), uint(2), '-', uint64(1)}, - {uint(3), uint(2), '*', uint64(6)}, - {uint(3), uint(2), '/', uint64(1)}, - {uint(3), 2, '+', uint64(5)}, - {uint(3), 2, '-', uint64(1)}, - {uint(3), 2, '*', uint64(6)}, - {uint(3), 2, '/', uint64(1)}, - {3, uint(2), '+', uint64(5)}, - {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(3), 2.0, '-', float64(1)}, - {uint(3), 2.0, '*', float64(6)}, - {uint(3), 2.0, '/', float64(1.5)}, - {3.0, uint(2), '+', float64(5)}, - {3.0, uint(2), '-', float64(1)}, - {3.0, uint(2), '*', float64(6)}, - {3.0, uint(2), '/', float64(1.5)}, - {0, 0, '+', 0}, - {0, 0, '-', 0}, - {0, 0, '*', 0}, - {"foo", "bar", '+', "foobar"}, - {3, 0, '/', false}, - {3.0, 0, '/', false}, - {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/terminal/colors.go b/common/terminal/colors.go deleted file mode 100644 index 334b82fae..000000000 --- a/common/terminal/colors.go +++ /dev/null @@ -1,70 +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 terminal contains helper for the terminal, such as coloring output. -package terminal - -import ( - "fmt" - "os" - "runtime" - "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" -) - -// IsTerminal return true if the file descriptor is terminal and the TERM -// environment variable isn't a dumb one. -func IsTerminal(f *os.File) bool { - if runtime.GOOS == "windows" { - return false - } - - fd := f.Fd() - return os.Getenv("TERM") != "dumb" && (isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd)) -} - -// 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 0c43c5ae7..000000000 --- a/common/text/position.go +++ /dev/null @@ -1,99 +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() 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\"" - } - - var 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([]interface{}, 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.IsTerminal(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 ba4824344..000000000 --- a/common/text/position_test.go +++ /dev/null @@ -1,33 +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/types/evictingqueue.go b/common/types/evictingqueue.go deleted file mode 100644 index 884762426..000000000 --- a/common/types/evictingqueue.go +++ /dev/null @@ -1,96 +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 ( - "sync" -) - -// EvictingStringQueue is a queue which automatically evicts elements from the head of -// the queue when attempting to add new elements onto the queue and it is full. -// This queue orders elements LIFO (last-in-first-out). It throws away duplicates. -// Note: This queue currently does not contain any remove (poll etc.) methods. -type EvictingStringQueue struct { - size int - vals []string - set map[string]bool - mu sync.Mutex -} - -// NewEvictingStringQueue creates a new queue with the given size. -func NewEvictingStringQueue(size int) *EvictingStringQueue { - return &EvictingStringQueue{size: size, set: make(map[string]bool)} -} - -// Add adds a new string to the tail of the queue if it's not already there. -func (q *EvictingStringQueue) Add(v string) { - q.mu.Lock() - if q.set[v] { - q.mu.Unlock() - return - } - - if len(q.set) == q.size { - // Full - delete(q.set, q.vals[0]) - q.vals = append(q.vals[:0], q.vals[1:]...) - } - q.set[v] = true - q.vals = append(q.vals, v) - q.mu.Unlock() -} - -// Contains returns whether the queue contains v. -func (q *EvictingStringQueue) Contains(v string) bool { - q.mu.Lock() - defer q.mu.Unlock() - return q.set[v] -} - -// Peek looks at the last element added to the queue. -func (q *EvictingStringQueue) Peek() string { - q.mu.Lock() - l := len(q.vals) - if l == 0 { - q.mu.Unlock() - return "" - } - 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 *EvictingStringQueue) PeekAll() []string { - q.mu.Lock() - vals := make([]string, 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 *EvictingStringQueue) PeekAllSet() map[string]bool { - all := q.PeekAll() - set := make(map[string]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 7489ba88d..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 := NewEvictingStringQueue(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 := NewEvictingStringQueue(3) - - for j := 0; j < 100; j++ { - 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/types.go b/common/types/types.go deleted file mode 100644 index f03031439..000000000 --- a/common/types/types.go +++ /dev/null @@ -1,80 +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" - - "github.com/spf13/cast" -) - -// 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 interface{} - Values []interface{} -} - -// 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([]interface{}, len(values)) - for i := 0; i < len(values); i++ { - 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 interface{}) 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() -} diff --git a/common/types/types_test.go b/common/types/types_test.go deleted file mode 100644 index 7c5cba659..000000000 --- a/common/types/types_test.go +++ /dev/null @@ -1,29 +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, []interface{}{"a1", "a2"}) -} diff --git a/common/urls/ref.go b/common/urls/ref.go deleted file mode 100644 index 71b00b71d..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]interface{}) (string, error) - RelRef(args map[string]interface{}) (string, error) -} diff --git a/compare/compare.go b/compare/compare.go deleted file mode 100644 index 537a66676..000000000 --- a/compare/compare.go +++ /dev/null @@ -1,35 +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(other interface{}) bool -} - -// ProbablyEqer is an equal check that may return false positives, but never -// a false negative. -type ProbablyEqer interface { - ProbablyEq(other interface{}) 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 interface{}) int -} 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 db286c2c5..000000000 --- a/compare/compare_strings_test.go +++ /dev/null @@ -1,65 +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"}) - -} diff --git a/config.toml b/config.toml new file mode 100644 index 000000000..6f66955ba --- /dev/null +++ b/config.toml @@ -0,0 +1,118 @@ +baseURL = "https://gohugo.io/" +paginate = 100 +defaultContentLanguage = "en" +enableEmoji = true +# Set the unicode character used for the "return" link in page footnotes. +footnotereturnlinkcontents = "↩" +languageCode = "en-us" +title = "Hugo" + + +googleAnalytics = "UA-7131036-4" + +pluralizeListTitles = false + +# We do redirects via Netlify's _redirects file, generated by Hugo (see "outputs" below). +disableAliases = true + +# Highlighting config (Pygments) +# It is (currently) not in use, but you can do ```go in a content file if you want to. +pygmentsCodeFences = true + +pygmentsOptions = "" +# Use the Chroma stylesheet +pygmentsUseClasses = true +pygmentsUseClassic = false + +# See https://help.farbox.com/pygments.html +pygmentsStyle = "trac" + +[module] + [module.hugoVersion] + min = "0.56.0" + [[module.imports]] + path = "github.com/gohugoio/gohugoioTheme" + +[outputs] +home = [ "HTML", "RSS", "REDIR", "HEADERS" ] +section = [ "HTML", "RSS"] + +[mediaTypes] +[mediaTypes."text/netlify"] +delimiter = "" + +[outputFormats] +[outputFormats.REDIR] +mediatype = "text/netlify" +baseName = "_redirects" +isPlainText = true +notAlternative = true +[outputFormats.HEADERS] +mediatype = "text/netlify" +baseName = "_headers" +isPlainText = true +notAlternative = true + +[related] + +threshold = 80 +includeNewer = true +toLower = false + +[[related.indices]] +name = "keywords" +weight = 100 +[[related.indices]] +name = "date" +weight = 10 +pattern = "2006" + +[social] +twitter = "GoHugoIO" + +#CUSTOM PARAMS +[params] + description = "The world’s fastest framework for building websites" + ## Used for views in rendered HTML (i.e., rather than using the .Hugo variable) + release = "0.55.0-DEV" + ## Setting this to true will add a "noindex" to *EVERY* page on the site + removefromexternalsearch = false + ## Gh repo for site footer (include trailing slash) + ghrepo = "https://github.com/gohugoio/hugoDocs/" + ## GH Repo for filing a new issue + github_repo = "https://github.com/gohugoio/hugo/issues/new" + ### Edit content repo (set to automatically enter "edit" mode; this is good for "improve this page" links) + ghdocsrepo = "https://github.com/gohugoio/hugoDocs/tree/master/docs" + ## Gitter URL + gitter = "https://gitter.im/spf13/hugo" + ## Discuss Forum URL + forum = "https://discourse.gohugo.io/" + ## Google Tag Manager + gtmid = "" + + # First one is picked as the Twitter card image if not set on page. + images = ["images/gohugoio-card.png"] + + flex_box_interior_classes = "flex-auto w-100 w-40-l mr3 mb3 bg-white ba b--moon-gray nested-copy-line-height" + + #sidebar_direction = "sidebar_left" + +# MARKDOWN +## Configuration for BlackFriday markdown parser: https://github.com/russross/blackfriday +[blackfriday] + plainIDAnchors = true + # See https://github.com/gohugoio/hugo/issues/2424 + hrefTargetBlank = false + angledQuotes = false + latexDashes = true + +[imaging] +# See https://github.com/disintegration/imaging +# CatmullRom is a sharp bicubic filter which should fit the docs site well with its many screenshots. +# Note that you can also set this per image processing. +resampleFilter = "CatmullRom" + +# Default JPEG quality setting. Default is 75. +quality = 75 + +anchor = "smart" diff --git a/config/_default/config.toml b/config/_default/config.toml new file mode 100644 index 000000000..9f908c9fd --- /dev/null +++ b/config/_default/config.toml @@ -0,0 +1,103 @@ +baseURL = "https://gohugo.io/" +paginate = 100 +defaultContentLanguage = "en" +enableEmoji = true +# Set the unicode character used for the "return" link in page footnotes. +footnotereturnlinkcontents = "↩" +languageCode = "en-us" +title = "Hugo" + +googleAnalytics = "UA-7131036-4" + +pluralizeListTitles = false + +# We do redirects via Netlify's _redirects file, generated by Hugo (see "outputs" below). +disableAliases = true + +# Highlighting config (Pygments) +# It is (currently) not in use, but you can do ```go in a content file if you want to. +pygmentsCodeFences = true + +pygmentsOptions = "" +# Use the Chroma stylesheet +pygmentsUseClasses = true +pygmentsUseClassic = false + +# See https://help.farbox.com/pygments.html +pygmentsStyle = "trac" + +[outputs] +home = [ "HTML", "RSS", "REDIR", "HEADERS" ] +section = [ "HTML", "RSS"] + +[mediaTypes] +[mediaTypes."text/netlify"] +delimiter = "" + +[outputFormats] +[outputFormats.REDIR] +mediatype = "text/netlify" +baseName = "_redirects" +isPlainText = true +notAlternative = true +[outputFormats.HEADERS] +mediatype = "text/netlify" +baseName = "_headers" +isPlainText = true +notAlternative = true + +[caches] +[caches.getjson] +dir = ":cacheDir/:project" +maxAge = -1 +[caches.getcsv] +dir = ":cacheDir/:project" +maxAge = -1 +[caches.images] +dir = ":resourceDir/_gen" +maxAge = -1 +[caches.assets] +dir = ":resourceDir/_gen" +maxAge = -1 + + +[related] + +threshold = 80 +includeNewer = true +toLower = false + +[[related.indices]] +name = "keywords" +weight = 100 +[[related.indices]] +name = "date" +weight = 10 +pattern = "2006" + +[social] +twitter = "GoHugoIO" + + +# MARKDOWN +## Configuration for BlackFriday markdown parser: https://github.com/russross/blackfriday +[blackfriday] +plainIDAnchors = true +# See https://github.com/gohugoio/hugo/issues/2424 +hrefTargetBlank = false +angledQuotes = false +latexDashes = true + +[imaging] +# See https://github.com/disintegration/imaging +# CatmullRom is a sharp bicubic filter which should fit the docs site well with its many screenshots. +# Note that you can also set this per image processing. +resampleFilter = "CatmullRom" + +# Default JPEG quality setting. Default is 75. +quality = 75 + +anchor = "smart" + +[taxonomies] +category = "categories" diff --git a/config/_default/languages.toml b/config/_default/languages.toml new file mode 100644 index 000000000..c9914d84d --- /dev/null +++ b/config/_default/languages.toml @@ -0,0 +1,10 @@ + + [en] + contentDir = "content/en" + languageName = "English" + weight = 1 + + [zh] + contentDir = "content/zh" + languageName = "中文" + weight = 2 diff --git a/config/_default/menus/menus.en.toml b/config/_default/menus/menus.en.toml new file mode 100644 index 000000000..a31218a4f --- /dev/null +++ b/config/_default/menus/menus.en.toml @@ -0,0 +1,154 @@ +[[docs]] + name = "About Hugo" + weight = 1 + identifier = "about" + url = "/about/" + +[[docs]] + name = "Getting Started" + weight = 5 + identifier = "getting-started" + url = "/getting-started/" + +[[docs]] + name = "Hugo Modules" + weight = 15 + identifier = "modules" + post = "break" + url = "/hugo-modules/" + +# Core Menus + +[[docs]] + name = "Content Management" + weight = 20 + identifier = "content-management" + post = "expanded" + url = "/content-management/" + +[[docs]] + name = "Templates" + weight = 25 + identifier = "templates" + + url = "/templates/" + +[[docs]] + name = "Functions" + weight = 30 + identifier = "functions" + url = "/functions/" + +[[docs]] + name = "Variables" + weight = 35 + identifier = "variables" + url = "/variables/" +[[docs]] + name = "Hugo Pipes" + weight = 36 + identifier = "pipes" + url = "/hugo-pipes/" +[[docs]] + name = "CLI" + weight = 40 + post = "break" + identifier = "commands" + url = "/commands/" + + + +# LOW LEVEL ITEMS + + +[[docs]] + name = "Troubleshooting" + weight = 60 + identifier = "troubleshooting" + url = "/troubleshooting/" + +[[docs]] + name = "Tools" + weight = 70 + identifier = "tools" + url = "/tools/" + +[[docs]] + name = "Hosting & Deployment" + weight = 80 + identifier = "hosting-and-deployment" + url = "/hosting-and-deployment/" + +[[docs]] + name = "Contribute" + weight = 100 + post = "break" + identifier = "contribute" + url = "/contribute/" + +#[[docs]] +# name = "Tags" +# weight = 120 +# identifier = "tags" +# url = "/tags/" + + +# [[docs]] +# name = "Categories" +# weight = 140 +# identifier = "categories" +# url = "/categories/" + +######## QUICKLINKS + + [[quicklinks]] + name = "Fundamentals" + weight = 1 + identifier = "fundamentals" + url = "/tags/fundamentals/" + + + + +######## GLOBAL ITEMS TO BE SHARED WITH THE HUGO SITES + +[[global]] + name = "News" + weight = 1 + identifier = "news" + url = "/news/" + + [[global]] + name = "Docs" + weight = 5 + identifier = "docs" + url = "/documentation/" + + [[global]] + name = "Themes" + weight = 10 + identifier = "themes" + url = "https://themes.gohugo.io/" + + [[global]] + name = "Showcase" + weight = 20 + identifier = "showcase" + url = "/showcase/" + + # Anything with a weight > 100 gets an external icon + [[global]] + name = "Community" + weight = 150 + icon = true + identifier = "community" + post = "external" + url = "https://discourse.gohugo.io/" + + + [[global]] + name = "GitHub" + weight = 200 + identifier = "github" + post = "external" + url = "https://github.com/gohugoio/hugo" \ No newline at end of file diff --git a/config/_default/menus/menus.zh.toml b/config/_default/menus/menus.zh.toml new file mode 100644 index 000000000..2f68be67b --- /dev/null +++ b/config/_default/menus/menus.zh.toml @@ -0,0 +1,121 @@ + +# Chinese menus + +[[docs]] + name = "关于 Hugo" + weight = 1 + identifier = "about" + url = "/zh/about/" + +[[docs]] + name = "入门" + weight = 5 + identifier = "getting-started" + url = "/zh/getting-started/" + +[[docs]] + name = "主题" + weight = 15 + identifier = "themes" + post = "break" + url = "/zh/themes/" + +# Core languages.zh.menus + +[[docs]] + name = "内容管理" + weight = 20 + identifier = "content-management" + post = "expanded" + url = "/zh/content-management/" + +[[docs]] + name = "模板" + weight = 25 + identifier = "templates" + url = "/zh/templates/" + +[[docs]] + name = "函数" + weight = 30 + identifier = "functions" + url = "/zh/functions/" + +[[docs]] + name = "变量" + weight = 35 + identifier = "variables" + url = "/zh/variables/" + +[[docs]] + name = "CLI" + weight = 40 + post = "break" + identifier = "commands" + url = "/commands/" + +# LOW LEVEL ITEMS +[[docs]] + name = "故障排除" + weight = 60 + identifier = "troubleshooting" + url = "/zh/troubleshooting/" + +[[docs]] + name = "工具" + weight = 70 + identifier = "tools" + url = "/zh/tools/" + +[[docs]] + name = "托管与部署" + weight = 80 + identifier = "hosting-and-deployment" + url = "/zh/hosting-and-deployment/" + +[[docs]] + name = "贡献" + weight = 100 + post = "break" + identifier = "contribute" + url = "/zh/contribute/" + +[[global]] + name = "新闻" + weight = 1 + identifier = "news" + url = "/zh/news/" + +[[global]] + name = "文档" + weight = 5 + identifier = "docs" + url = "/zh/documentation/" + +[[global]] + name = "主题" + weight = 10 + identifier = "themes" + url = "https://themes.gohugo.io/" + +[[global]] + name = "作品展示" + weight = 20 + identifier = "showcase" + url = "/zh/showcase/" + +# Anything with a weight > 100 gets an external icon +[[global]] + name = "社区" + weight = 150 + icon = true + identifier = "community" + post = "external" + url = "https://discourse.gohugo.io/" + +[[global]] + name = "GitHub" + weight = 200 + identifier = "github" + post = "external" + url = "https://github.com/gohugoio/hugo" diff --git a/config/_default/params.toml b/config/_default/params.toml new file mode 100644 index 000000000..6ddf97e56 --- /dev/null +++ b/config/_default/params.toml @@ -0,0 +1,25 @@ + +description = "The world’s fastest framework for building websites" +## Used for views in rendered HTML (i.e., rather than using the .Hugo variable) +release = "0.52" +## Setting this to true will add a "noindex" to *EVERY* page on the site.. +removefromexternalsearch = false +## Gh repo for site footer (include trailing slash) +ghrepo = "https://github.com/gohugoio/hugoDocs/" +## GH Repo for filing a new issue +github_repo = "https://github.com/gohugoio/hugo/issues/new" +### Edit content repo (set to automatically enter "edit" mode; this is good for "improve this page" links) +ghdocsrepo = "https://github.com/gohugoio/hugoDocs/tree/master/docs" +## Gitter URL +gitter = "https://gitter.im/spf13/hugo" +## Discuss Forum URL +forum = "https://discourse.gohugo.io/" +## Google Tag Manager +gtmid = "" + +# First one is picked as the Twitter card image if not set on page. +images = ["images/gohugoio-card.png"] + +flex_box_interior_classes = "flex-auto w-100 w-40-l mr3 mb3 bg-white ba b--moon-gray nested-copy-line-height" + +#sidebar_direction = "sidebar_left" \ No newline at end of file diff --git a/config/configLoader.go b/config/configLoader.go deleted file mode 100644 index 2e37a5b35..000000000 --- a/config/configLoader.go +++ /dev/null @@ -1,125 +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 ( - "path/filepath" - "strings" - - "github.com/gohugoio/hugo/common/maps" - "github.com/gohugoio/hugo/parser/metadecoders" - "github.com/spf13/afero" - "github.com/spf13/viper" -) - -var ( - ValidConfigFileExtensions = []string{"toml", "yaml", "yml", "json"} - validConfigFileExtensionsMap map[string]bool = make(map[string]bool) -) - -func init() { - 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] -} - -// FromConfigString creates a config from the given YAML, JSON or TOML config. This is useful in tests. -func FromConfigString(config, configType string) (Provider, error) { - v := newViper() - m, err := readConfig(metadecoders.FormatFromString(configType), []byte(config)) - if err != nil { - return nil, err - } - - v.MergeConfigMap(m) - - return v, 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 { - return nil, err - } - - v := newViper() - - err = v.MergeConfigMap(m) - if err != nil { - return nil, err - } - - return v, 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]interface{}, error) { - return loadConfigFromFile(fs, filename) -} - -func readConfig(format metadecoders.Format, data []byte) (map[string]interface{}, 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]interface{}, error) { - m, err := metadecoders.Default.UnmarshalFileToMap(fs, filename) - if err != nil { - return nil, err - } - RenameKeys(m) - return m, 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]interface{}) { - keyAliases.Rename(m) -} - -func newViper() *viper.Viper { - v := viper.New() - - return v -} 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 deleted file mode 100644 index 187fb7b10..000000000 --- a/config/configProvider.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 config - -import ( - "github.com/spf13/cast" -) - -// Provider provides the configuration settings for Hugo. -type Provider interface { - GetString(key string) string - GetInt(key string) int - GetBool(key string) bool - GetStringMap(key string) map[string]interface{} - GetStringMapString(key string) map[string]string - GetStringSlice(key string) []string - 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 toStringSlicePreserveString(sd) -} - -func toStringSlicePreserveString(v interface{}) []string { - if sds, ok := v.(string); ok { - return []string{sds} - } - return cast.ToStringSlice(v) -} - -// SetBaseTestDefaults provides some common config defaults used in tests. -func SetBaseTestDefaults(cfg Provider) { - cfg.Set("resourceDir", "resources") - cfg.Set("contentDir", "content") - cfg.Set("dataDir", "data") - cfg.Set("i18nDir", "i18n") - cfg.Set("layoutDir", "layouts") - cfg.Set("assetDir", "assets") - cfg.Set("archetypeDir", "archetypes") - cfg.Set("publishDir", "public") -} diff --git a/config/configProvider_test.go b/config/configProvider_test.go deleted file mode 100644 index d9fff56b6..000000000 --- a/config/configProvider_test.go +++ /dev/null @@ -1,36 +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" - "github.com/spf13/viper" -) - -func TestGetStringSlicePreserveString(t *testing.T) { - c := qt.New(t) - cfg := viper.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/development/params.toml b/config/development/params.toml new file mode 100644 index 000000000..4cd7314ab --- /dev/null +++ b/config/development/params.toml @@ -0,0 +1 @@ +# Params for development (server mode) diff --git a/config/env.go b/config/env.go deleted file mode 100644 index f482cd247..000000000 --- a/config/env.go +++ /dev/null @@ -1,57 +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" -) - -// 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() -} - -// 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) { - parts := strings.Split(v, "=") - return parts[0], parts[1] -} - -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/privacy/privacyConfig.go b/config/privacy/privacyConfig.go deleted file mode 100644 index ea34563eb..000000000 --- a/config/privacy/privacyConfig.go +++ /dev/null @@ -1,110 +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 - Vimeo Vimeo - YouTube YouTube -} - -// 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 disable the use of Cookies and use Session Storage to Store the GA Client ID. - UseSessionStorage bool - - // Enabling this will make the GA templates respect the - // "Do Not Track" HTTP header. See https://www.paulfurley.com/google-analytics-dnt/. - RespectDoNotTrack bool - - // Enabling this will make it so the users' IP addresses are anonymized within Google Analytics. - AnonymizeIP bool -} - -// Instagram holds the privacy configuration settings related to the Instagram shortcode. -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 settingsrelated to the Twitter shortcode. -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 settingsrelated to the Vimeo shortcode. -type Vimeo struct { - Service `mapstructure:",squash"` - - // 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 settingsrelated 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 -} - -// 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 d798721e1..000000000 --- a/config/privacy/privacyConfig_test.go +++ /dev/null @@ -1,101 +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" - "github.com/spf13/viper" -) - -func TestDecodeConfigFromTOML(t *testing.T) { - c := qt.New(t) - - tomlConfig := ` - -someOtherValue = "foo" - -[privacy] -[privacy.disqus] -disable = true -[privacy.googleAnalytics] -disable = true -respectDoNotTrack = true -anonymizeIP = true -useSessionStorage = true -[privacy.instagram] -disable = true -simple = true -[privacy.twitter] -disable = true -enableDNT = true -simple = true -[privacy.vimeo] -disable = 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.GoogleAnalytics.AnonymizeIP, - pc.GoogleAnalytics.UseSessionStorage, pc.Instagram.Disable, - pc.Instagram.Simple, pc.Twitter.Disable, pc.Twitter.EnableDNT, - pc.Twitter.Simple, pc.Vimeo.Disable, pc.Vimeo.Simple, - pc.YouTube.PrivacyEnhanced, pc.YouTube.Disable, - } - - 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(viper.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/production/config.toml b/config/production/config.toml new file mode 100644 index 000000000..961f04d35 --- /dev/null +++ b/config/production/config.toml @@ -0,0 +1,6 @@ +# Config for production + +# This is turned off in development as it is relatively slow. +# This is needed to get accurate lastMod and Git commit info +# on the docs pages. +enableGitInfo = true \ No newline at end of file diff --git a/config/production/params.toml b/config/production/params.toml new file mode 100644 index 000000000..d0071fe65 --- /dev/null +++ b/config/production/params.toml @@ -0,0 +1,2 @@ +# Params for production + diff --git a/config/services/servicesConfig.go b/config/services/servicesConfig.go deleted file mode 100644 index 559848f5c..000000000 --- a/config/services/servicesConfig.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 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 - 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 -} - -// Twitter holds the functional configuration settings related to the Twitter shortcodes. -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 -} - -// 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) - } - - return -} diff --git a/config/services/servicesConfig_test.go b/config/services/servicesConfig_test.go deleted file mode 100644 index ed3038159..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" - "github.com/spf13/viper" -) - -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 -` - 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 := viper.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/sitemap.go b/config/sitemap.go deleted file mode 100644 index 4031b7ec1..000000000 --- a/config/sitemap.go +++ /dev/null @@ -1,44 +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 ( - "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 DecodeSitemap(prototype Sitemap, input map[string]interface{}) Sitemap { - - for key, value := range input { - switch key { - case "changefreq": - prototype.ChangeFreq = cast.ToString(value) - case "priority": - prototype.Priority = cast.ToFloat64(value) - case "filename": - prototype.Filename = cast.ToString(value) - default: - jww.WARN.Printf("Unknown Sitemap field: %s\n", key) - } - } - - return prototype -} diff --git a/content/en/_index.md b/content/en/_index.md new file mode 100644 index 000000000..bfebd8b2f --- /dev/null +++ b/content/en/_index.md @@ -0,0 +1,49 @@ +--- +title: "The world’s fastest framework for building websites" +date: 2017-03-02T12:00:00-05:00 +features: + - heading: Blistering Speed + image_path: /images/icon-fast.svg + tagline: What's modern about waiting for your site to build? + copy: Hugo is the fastest tool of its kind. At <1 ms per page, the average site builds in less than a second. + + - heading: Robust Content Management + image_path: /images/icon-content-management.svg + tagline: Flexibility rules. Hugo is a content strategist's dream. + copy: Hugo supports unlimited content types, taxonomies, menus, dynamic API-driven content, and more, all without plugins. + + - heading: Shortcodes + image_path: /images/icon-shortcodes.svg + tagline: Hugo's shortcodes are Markdown's hidden superpower. + copy: We love the beautiful simplicity of markdown’s syntax, but there are times when we want more flexibility. Hugo shortcodes allow for both beauty and flexibility. + + - heading: Built-in Templates + image_path: /images/icon-built-in-templates.svg + tagline: Hugo has common patterns to get your work done quickly. + copy: Hugo ships with pre-made templates to make quick work of SEO, commenting, analytics and other functions. One line of code, and you're done. + + - heading: Multilingual and i18n + image_path: /images/icon-multilingual2.svg + tagline: Polyglot baked in. + copy: Hugo provides full i18n support for multi-language sites with the same straightforward development experience Hugo users love in single-language sites. + + - heading: Custom Outputs + image_path: /images/icon-custom-outputs.svg + tagline: HTML not enough? + copy: Hugo allows you to output your content in multiple formats, including JSON or AMP, and makes it easy to create your own. +sections: + - heading: "300+ Themes" + cta: Check out the Hugo themes. + link: https://themes.gohugo.io/ + color_classes: bg-accent-color white + image: /images/homepage-screenshot-hugo-themes.jpg + copy: "Hugo provides a robust theming system that is easy to implement but capable of producing even the most complicated websites." + - heading: "Capable Templating" + cta: Get Started. + link: templates/ + color_classes: bg-primary-color-light black + image: /images/home-page-templating-example.png + copy: "Hugo's Go-based templating provides just the right amount of logic to build anything from the simple to complex. If you prefer Jade/Pug-like syntax, you can also use Amber, Ace, or any combination of the three." +--- + +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/content/en/about/_index.md b/content/en/about/_index.md new file mode 100644 index 000000000..8ed441b61 --- /dev/null +++ b/content/en/about/_index.md @@ -0,0 +1,20 @@ +--- +title: About Hugo +linktitle: Overview +description: Hugo's features, roadmap, license, and motivation. +date: 2017-02-01 +publishdate: 2017-02-01 +lastmod: 2017-02-01 +categories: [] +keywords: [] +menu: + docs: + parent: "about" + weight: 1 +weight: 1 +draft: false +aliases: [/about-hugo/,/docs/] +toc: false +--- + +Hugo is not your average static site generator. diff --git a/content/en/about/benefits.md b/content/en/about/benefits.md new file mode 100644 index 000000000..16d43e48f --- /dev/null +++ b/content/en/about/benefits.md @@ -0,0 +1,43 @@ +--- +title: The Benefits of Static Site Generators +linktitle: The Benefits of Static +description: Improved performance, security and ease of use are just a few of the reasons static site generators are so appealing. +date: 2017-02-01 +publishdate: 2017-02-01 +lastmod: 2017-02-01 +keywords: [ssg,static,performance,security] +menu: + docs: + parent: "about" + weight: 30 +weight: 30 +sections_weight: 30 +draft: false +aliases: [] +toc: false +--- + +The purpose of website generators is to render content into HTML files. Most are "dynamic site generators." That means the HTTP server---i.e., the program that sends files to the browser to be viewed---runs the generator to create a new HTML file every time an end user requests a page. + +Over time, dynamic site generators were programmed to cache their HTML files to prevent unnecessary delays in delivering pages to end users. A cached page is a static version of a web page. + +Hugo takes caching a step further and all HTML files are rendered on your computer. You can review the files locally before copying 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*. + +This has many benefits. The most noticeable is performance. HTTP servers are *very* good at sending files---so good, in fact, that you can effectively serve the same number of pages with a fraction of the memory and CPU needed for a dynamic site. + +## More on Static Site Generators + +* ["An Introduction to Static Site Generators", David Walsh][] +* ["Hugo vs. Wordpress page load speed comparison: Hugo leaves WordPress in its dust", GettingThingsTech][hugovwordpress] +* ["Static Site Generators", O'Reilly][] +* [StaticGen: Top Open-Source Static Site Generators (GitHub Stars)][] +* ["Top 10 Static Website Generators", Netlify blog][] +* ["The Resurgence of Static", dotCMS][dotcms] + + +["An Introduction to Static Site Generators", David Walsh]: https://davidwalsh.name/introduction-static-site-generators +["Static Site Generators", O'Reilly]: https://www.oreilly.com/web-platform/free/files/static-site-generators.pdf +["Top 10 Static Website Generators", Netlify blog]: https://www.netlify.com/blog/2016/05/02/top-ten-static-website-generators/ +[hugovwordpress]: https://gettingthingstech.com/hugo-vs.-wordpress-page-load-speed-comparison-hugo-leaves-wordpress-in-its-dust/ +[StaticGen: Top Open-Source Static Site Generators (GitHub Stars)]: https://www.staticgen.com/ +[dotcms]: https://dotcms.com/blog/post/the-resurgence-of-static diff --git a/content/en/about/features.md b/content/en/about/features.md new file mode 100644 index 000000000..cadcc6b0f --- /dev/null +++ b/content/en/about/features.md @@ -0,0 +1,87 @@ +--- +title: Hugo Features +linktitle: Hugo Features +description: Hugo boasts blistering speed, robust content management, and a powerful templating language making it a great fit for all kinds of static websites. +date: 2017-02-01 +publishdate: 2017-02-01 +lastmod: 2017-02-01 +menu: + docs: + parent: "about" + weight: 20 +weight: 20 +sections_weight: 20 +draft: false +toc: true +--- + +## General + +* [Extremely fast][] build times (< 1 ms per page) +* Completely cross platform, with [easy installation][install] on macOS, Linux, Windows, and more +* Renders changes on the fly with [LiveReload][] as you develop +* [Powerful theming][] +* [Host your site anywhere][hostanywhere] + +## Organization + +* Straightforward [organization for your projects][], including website sections +* Customizable [URLs][] +* Support for configurable [taxonomies][], including categories and tags +* [Sort content][] as you desire through powerful template [functions][] +* Automatic [table of contents][] generation +* [Dynamic menu][] creation +* [Pretty URLs][] support +* [Permalink][] pattern support +* Redirects via [aliases][] + +## Content + +* Native Markdown and Emacs Org-Mode support, as well as other languages via *external helpers* (see [supported formats][]) +* TOML, YAML, and JSON metadata support in [front matter][] +* Customizable [homepage][] +* Multiple [content types][] +* Automatic and user defined [content summaries][] +* [Shortcodes][] to enable rich content inside of Markdown +* ["Minutes to Read"][pagevars] functionality +* ["Wordcount"][pagevars] functionality + +## Additional Features + +* Integrated [Disqus][] comment support +* Integrated [Google Analytics][] support +* Automatic [RSS][] creation +* Support for [Go][], [Amber], and [Ace][] HTML templates +* [Syntax highlighting][] powered by [Chroma][] (partly compatible with Pygments) + + +[Ace]: /templates/alternatives/ +[aliases]: /content-management/urls/#aliases +[Amber]: https://github.com/eknkc/amber +[Chroma]: https://github.com/alecthomas/chroma +[content summaries]: /content-management/summaries/ +[content types]: /content-management/types/ +[Disqus]: https://disqus.com/ +[Dynamic menu]: /templates/menus/ +[Extremely fast]: https://github.com/bep/hugo-benchmark +[front matter]: /content-management/front-matter/ +[functions]: /functions/ +[Go]: https://golang.org/pkg/html/template/ +[Google Analytics]: https://google-analytics.com/ +[homepage]: /templates/homepage/ +[hostanywhere]: /hosting-and-deployment/ +[install]: /getting-started/installing/ +[LiveReload]: /getting-started/usage/ +[organization for your projects]: /getting-started/directory-structure/ +[pagevars]: /variables/page/ +[Permalink]: /content-management/urls/#permalinks +[Powerful theming]: /themes/ +[Pretty URLs]: /content-management/urls/ +[RSS]: /templates/rss/ +[Shortcodes]: /content-management/shortcodes/ +[sort content]: /templates/ +[supported formats]: /content-management/formats/ +[Syntax highlighting]: /tools/syntax-highlighting/ +[table of contents]: /content-management/toc/ +[taxonomies]: /content-management/taxonomies/ +[URLs]: /content-management/urls/ diff --git a/content/en/about/hugo-and-gdpr.md b/content/en/about/hugo-and-gdpr.md new file mode 100644 index 000000000..e193e1838 --- /dev/null +++ b/content/en/about/hugo-and-gdpr.md @@ -0,0 +1,135 @@ + + +--- +title: Hugo and the General Data Protection Regulation (GDPR) +linktitle: Hugo and GDPR +description: About how to configure your Hugo site to meet the new regulations. +date: 2018-05-25 +layout: single +keywords: ["GDPR", "Privacy", "Data Protection"] +menu: + docs: + parent: "about" + weight: 5 +weight: 5 +sections_weight: 5 +draft: false +aliases: [/privacy/,/gdpr/] +toc: true +--- + + + General Data Protection Regulation ([GDPR](https://en.wikipedia.org/wiki/General_Data_Protection_Regulation)) is a regulation in EU law on data protection and privacy for all individuals within the European Union and the European Economic Area. It became enforceable on 25 May 2018. + + **Hugo is a static site generator. By using Hugo you are already standing on very solid ground. Static HTML files on disk are much easier to reason about compared to server and database driven web sites.** + + But even static websites can integrate with external services, so from version `0.41`, Hugo provides a **Privacy Config** that covers the relevant built-in templates. + + Note that: + + * These settings have their defaults setting set to _off_, i.e. how it worked before Hugo `0.41`. You must do your own evaluation of your site and apply the appropriate settings. + * These settings work with the [internal templates](/templates/internal/). Some theme may contain custom templates for embedding services like Google Analytics. In that case these options have no effect. + * We will continue this work and improve this further in future Hugo versions. + +## All Privacy Settings + +Below are all privacy settings and their default value. These settings need to be put in your site config (e.g. `config.toml`). + + {{< code-toggle file="config">}} +[privacy] +[privacy.disqus] +disable = false +[privacy.googleAnalytics] +disable = false +respectDoNotTrack = false +anonymizeIP = false +useSessionStorage = false +[privacy.instagram] +disable = false +simple = false +[privacy.twitter] +disable = false +enableDNT = false +simple = false +[privacy.vimeo] +disable = false +simple = false +[privacy.youtube] +disable = false +privacyEnhanced = false +{{< /code-toggle >}} + + +## Disable All Services + +An example Privacy Config that disables all the relevant services in Hugo. With this configuration, the other settings will not matter. + + {{< code-toggle file="config">}} +[privacy] +[privacy.disqus] +disable = true +[privacy.googleAnalytics] +disable = true +[privacy.instagram] +disable = true +[privacy.twitter] +disable = true +[privacy.vimeo] +disable = true +[privacy.youtube] +disable = true +{{< /code-toggle >}} + +## The Privacy Settings Explained + +### GoogleAnalytics + +anonymizeIP +: Enabling this will make it so the users' IP addresses are anonymized within Google Analytics. + +respectDoNotTrack +: Enabling this will make the GA templates respect the "Do Not Track" HTTP header. + +useSessionStorage +: Enabling this will disable the use of Cookies and use Session Storage to Store the GA Client ID. + +### Instagram + +simple +: If simple mode is enabled, a static and no-JS version of the Instagram image card will be built. Note that this only supports image cards and the image itself will be fetched from Instagram's servers. + +**Note:** If you use the _simple mode_ for Instagram and a site styled with Bootstrap 4, you may want to disable the inlines styles provided by Hugo: + + {{< code-toggle file="config">}} +[services] +[services.instagram] +disableInlineCSS = true +{{< /code-toggle >}} + +### Twitter + +enableDNT +: Enabling this for the twitter/tweet shortcode, the tweet and its embedded page on your site are not used for purposes that include personalized suggestions and personalized ads. + +simple +: If simple mode is enabled, a static and no-JS version of a tweet will be built. + + +**Note:** If you use the _simple mode_ for Twitter, you may want to disable the inlines styles provided by Hugo: + + {{< code-toggle file="config">}} +[services] +[services.twitter] +disableInlineCSS = true +{{< /code-toggle >}} + +### YouTube + +privacyEnhanced +: When you turn on privacy-enhanced mode, YouTube won’t store information about visitors on your website unless the user plays the embedded video. + +### Vimeo + +simple +: If simple mode is enabled, the video thumbnail is fetched from Vimeo's servers and it is overlayed with a play button. If the user clicks to play the video, it will open in a new tab directly on Vimeo's website. + diff --git a/content/en/about/license.md b/content/en/about/license.md new file mode 100644 index 000000000..ae74b6047 --- /dev/null +++ b/content/en/about/license.md @@ -0,0 +1,165 @@ +--- +title: Apache License +linktitle: License +description: Hugo v0.15 and later are released under the Apache 2.0 license. +date: 2016-02-01 +publishdate: 2016-02-01 +lastmod: 2016-03-02 +categories: ["about hugo"] +keywords: ["License","apache"] +menu: + docs: + parent: "about" + weight: 60 +weight: 60 +sections_weight: 60 +aliases: [/meta/license] +toc: true +--- + +{{% note %}} +Hugo v0.15 and later are released under the Apache 2.0 license. +Earlier versions of Hugo were released under the [Simple Public License](https://opensource.org/licenses/Simple-2.0). +{{% /note %}} + +_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. + +{{< code file="apache-notice.txt" download="apache-notice.txt" >}} +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 + + 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. +{{< /code >}} diff --git a/content/en/about/what-is-hugo.md b/content/en/about/what-is-hugo.md new file mode 100644 index 000000000..257c7e82d --- /dev/null +++ b/content/en/about/what-is-hugo.md @@ -0,0 +1,65 @@ +--- +title: What is Hugo +linktitle: What is Hugo +description: Hugo is a fast and modern static site generator written in Go, and designed to make website creation fun again. +date: 2017-02-01 +publishdate: 2017-02-01 +lastmod: 2017-02-01 +layout: single +menu: + docs: + parent: "about" + weight: 10 +weight: 10 +sections_weight: 10 +draft: false +aliases: [/overview/introduction/,/about/why-i-built-hugo/] +toc: true +--- + +Hugo is a general-purpose website framework. Technically speaking, Hugo is a [static site generator][]. Unlike systems that dynamically build a page with each visitor request, Hugo builds pages when you create or update your content. Since websites are viewed far more often than they are edited, Hugo is designed to provide an optimal viewing experience for your website's end users and an ideal writing experience for website authors. + +Websites built with Hugo are extremely fast and secure. Hugo sites can be hosted anywhere, including [Netlify][], [Heroku][], [GoDaddy][], [DreamHost][], [GitHub Pages][], [GitLab Pages][], [Surge][], [Aerobatic][], [Firebase][], [Google Cloud Storage][], [Amazon S3][], [Rackspace][], [Azure][], and [CloudFront][] and work well with CDNs. Hugo sites run without the need for a database or dependencies on expensive runtimes like Ruby, Python, or PHP. + +We think of Hugo as the ideal website creation tool with nearly instant build times, able to rebuild whenever a change is made. + +## 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. + +## 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, a company site, a portfolio site, documentation, a single landing page, or a website with thousands of pages. + + + +[@spf13]: https://twitter.com/@spf13 +[Aerobatic]: https://www.aerobatic.com/ +[Amazon S3]: https://aws.amazon.com/s3/ +[Azure]: https://docs.microsoft.com/en-us/azure/storage/blobs/storage-blob-static-website +[CloudFront]: https://aws.amazon.com/cloudfront/ "Amazon CloudFront" +[DreamHost]: https://www.dreamhost.com/ +[Firebase]: https://firebase.google.com/docs/hosting/ "Firebase static hosting" +[GitHub Pages]: https://pages.github.com/ +[GitLab Pages]: https://about.gitlab.com/features/pages/ +[Go language]: https://golang.org/ +[GoDaddy]: https://www.godaddy.com/ "Godaddy.com Hosting" +[Google Cloud Storage]: https://cloud.google.com/storage/ +[Heroku]: https://www.heroku.com/ +[Jekyll]: https://jekyllrb.com/ +[Middleman]: https://middlemanapp.com/ +[Nanoc]: https://nanoc.ws/ +[Netlify]: https://netlify.com +[Rackspace]: https://www.rackspace.com/cloud/files +[Surge]: https://surge.sh +[contributing to it]: https://github.com/gohugoio/hugo +[rackspace]: https://www.rackspace.com/cloud/files +[static site generator]: /about/benefits/ diff --git a/content/en/commands/hugo.md b/content/en/commands/hugo.md new file mode 100644 index 000000000..a802271bd --- /dev/null +++ b/content/en/commands/hugo.md @@ -0,0 +1,82 @@ +--- +date: 2019-07-31 +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 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. Defaults: $TMPDIR/hugo_cache/ + --cleanDestinationDir remove files from destination not found in static directories + --config string config file (default is path/config.yaml|json|toml) + --configDir string config dir (default "config") + -c, --contentDir string filesystem path to content directory + --debug debug output + -d, --destination string filesystem path to write files to + --disableKinds strings disable different kind of pages (home, RSS etc.) + --enableGitInfo add Git revision, date and author 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 + --i18n-warnings print missing translations + --ignoreCache ignores the cache directory + --ignoreVendor ignores any _vendor directory + -l, --layoutDir string filesystem path to layout directory + --log enable Logging + --logFile string log File path (if set, logging enabled automatically) + --minify minify any supported output format (HTML, XML etc.) + --noChmod don't sync permission mode of files + --noTimes don't sync modification time of files + --path-warnings print warnings on duplicate target paths etc. + --quiet build in quiet mode + --renderToMemory render to memory (only useful for benchmark testing) + -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) + -v, --verbose verbose output + --verboseLog verbose logging + -w, --watch watch filesystem for changes and recreate as needed +``` + +### SEE ALSO + +* [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 deploy](/commands/hugo_deploy/) - Deploy your site to a Cloud provider. +* [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 mod](/commands/hugo_mod/) - Various Hugo Modules helpers. +* [hugo new](/commands/hugo_new/) - Create new content for your site +* [hugo server](/commands/hugo_server/) - A high performance webserver +* [hugo version](/commands/hugo_version/) - Print the version number of Hugo + +###### Auto generated by spf13/cobra on 31-Jul-2019 diff --git a/content/en/commands/hugo_check.md b/content/en/commands/hugo_check.md new file mode 100644 index 000000000..fc727476a --- /dev/null +++ b/content/en/commands/hugo_check.md @@ -0,0 +1,43 @@ +--- +date: 2019-07-31 +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) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + -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 31-Jul-2019 diff --git a/content/en/commands/hugo_check_ulimit.md b/content/en/commands/hugo_check_ulimit.md new file mode 100644 index 000000000..478c9a850 --- /dev/null +++ b/content/en/commands/hugo_check_ulimit.md @@ -0,0 +1,47 @@ +--- +date: 2019-07-31 +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) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO + +* [hugo check](/commands/hugo_check/) - Contains some verification checks + +###### Auto generated by spf13/cobra on 31-Jul-2019 diff --git a/content/en/commands/hugo_config.md b/content/en/commands/hugo_config.md new file mode 100644 index 000000000..6e1094903 --- /dev/null +++ b/content/en/commands/hugo_config.md @@ -0,0 +1,47 @@ +--- +date: 2019-07-31 +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) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO + +* [hugo](/commands/hugo/) - hugo builds your site +* [hugo config mounts](/commands/hugo_config_mounts/) - Print the configured file mounts + +###### Auto generated by spf13/cobra on 31-Jul-2019 diff --git a/content/en/commands/hugo_config_mounts.md b/content/en/commands/hugo_config_mounts.md new file mode 100644 index 000000000..98cd6f9eb --- /dev/null +++ b/content/en/commands/hugo_config_mounts.md @@ -0,0 +1,46 @@ +--- +date: 2019-07-31 +title: "hugo config mounts" +slug: hugo_config_mounts +url: /commands/hugo_config_mounts/ +--- +## hugo config mounts + +Print the configured file mounts + +### Synopsis + +Print the configured file mounts + +``` +hugo config mounts [flags] +``` + +### Options + +``` + -h, --help help for mounts +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO + +* [hugo config](/commands/hugo_config/) - Print the site configuration + +###### Auto generated by spf13/cobra on 31-Jul-2019 diff --git a/content/en/commands/hugo_convert.md b/content/en/commands/hugo_convert.md new file mode 100644 index 000000000..1e85b72f1 --- /dev/null +++ b/content/en/commands/hugo_convert.md @@ -0,0 +1,49 @@ +--- +date: 2019-07-31 +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 + --unsafe enable less safe operations, please backup first +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + -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 31-Jul-2019 diff --git a/content/en/commands/hugo_convert_toJSON.md b/content/en/commands/hugo_convert_toJSON.md new file mode 100644 index 000000000..484d942e6 --- /dev/null +++ b/content/en/commands/hugo_convert_toJSON.md @@ -0,0 +1,49 @@ +--- +date: 2019-07-31 +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) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + --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 31-Jul-2019 diff --git a/content/en/commands/hugo_convert_toTOML.md b/content/en/commands/hugo_convert_toTOML.md new file mode 100644 index 000000000..53f25cf66 --- /dev/null +++ b/content/en/commands/hugo_convert_toTOML.md @@ -0,0 +1,49 @@ +--- +date: 2019-07-31 +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) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + --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 31-Jul-2019 diff --git a/content/en/commands/hugo_convert_toYAML.md b/content/en/commands/hugo_convert_toYAML.md new file mode 100644 index 000000000..4173883d8 --- /dev/null +++ b/content/en/commands/hugo_convert_toYAML.md @@ -0,0 +1,49 @@ +--- +date: 2019-07-31 +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) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + --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 31-Jul-2019 diff --git a/content/en/commands/hugo_deploy.md b/content/en/commands/hugo_deploy.md new file mode 100644 index 000000000..054a16d42 --- /dev/null +++ b/content/en/commands/hugo_deploy.md @@ -0,0 +1,56 @@ +--- +date: 2019-07-31 +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] +``` + +### 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 via the cloudFrontDistributionID 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 +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO + +* [hugo](/commands/hugo/) - hugo builds your site + +###### Auto generated by spf13/cobra on 31-Jul-2019 diff --git a/content/en/commands/hugo_env.md b/content/en/commands/hugo_env.md new file mode 100644 index 000000000..5d134d693 --- /dev/null +++ b/content/en/commands/hugo_env.md @@ -0,0 +1,46 @@ +--- +date: 2019-07-31 +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) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO + +* [hugo](/commands/hugo/) - hugo builds your site + +###### Auto generated by spf13/cobra on 31-Jul-2019 diff --git a/content/en/commands/hugo_gen.md b/content/en/commands/hugo_gen.md new file mode 100644 index 000000000..e195882de --- /dev/null +++ b/content/en/commands/hugo_gen.md @@ -0,0 +1,46 @@ +--- +date: 2019-07-31 +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) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + -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 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 + +###### Auto generated by spf13/cobra on 31-Jul-2019 diff --git a/content/en/commands/hugo_gen_autocomplete.md b/content/en/commands/hugo_gen_autocomplete.md new file mode 100644 index 000000000..0c4657a87 --- /dev/null +++ b/content/en/commands/hugo_gen_autocomplete.md @@ -0,0 +1,64 @@ +--- +date: 2019-07-31 +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) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + -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 31-Jul-2019 diff --git a/content/en/commands/hugo_gen_chromastyles.md b/content/en/commands/hugo_gen_chromastyles.md new file mode 100644 index 000000000..c893a7f33 --- /dev/null +++ b/content/en/commands/hugo_gen_chromastyles.md @@ -0,0 +1,51 @@ +--- +date: 2019-07-31 +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 pygmentsUseClasses is enabled in config. + +See https://help.farbox.com/pygments.html for preview of available styles + +``` +hugo gen chromastyles [flags] +``` + +### Options + +``` + -h, --help help for chromastyles + --highlightStyle string style used for highlighting lines (see https://github.com/alecthomas/chroma) (default "bg:#ffffcc") + --linesStyle string style used for line numbers (see https://github.com/alecthomas/chroma) + --style string highlighter style (see https://help.farbox.com/pygments.html) (default "friendly") +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + -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 31-Jul-2019 diff --git a/content/en/commands/hugo_gen_doc.md b/content/en/commands/hugo_gen_doc.md new file mode 100644 index 000000000..cda495090 --- /dev/null +++ b/content/en/commands/hugo_gen_doc.md @@ -0,0 +1,53 @@ +--- +date: 2019-07-31 +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] +``` + +### 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) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + -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 31-Jul-2019 diff --git a/content/en/commands/hugo_gen_man.md b/content/en/commands/hugo_gen_man.md new file mode 100644 index 000000000..a6f7b7534 --- /dev/null +++ b/content/en/commands/hugo_gen_man.md @@ -0,0 +1,49 @@ +--- +date: 2019-07-31 +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) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + -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 31-Jul-2019 diff --git a/content/en/commands/hugo_import.md b/content/en/commands/hugo_import.md new file mode 100644 index 000000000..67b91e66c --- /dev/null +++ b/content/en/commands/hugo_import.md @@ -0,0 +1,45 @@ +--- +date: 2019-07-31 +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) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + -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 31-Jul-2019 diff --git a/content/en/commands/hugo_import_jekyll.md b/content/en/commands/hugo_import_jekyll.md new file mode 100644 index 000000000..e126c2107 --- /dev/null +++ b/content/en/commands/hugo_import_jekyll.md @@ -0,0 +1,49 @@ +--- +date: 2019-07-31 +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) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + -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 31-Jul-2019 diff --git a/content/en/commands/hugo_list.md b/content/en/commands/hugo_list.md new file mode 100644 index 000000000..16bd1933e --- /dev/null +++ b/content/en/commands/hugo_list.md @@ -0,0 +1,48 @@ +--- +date: 2019-07-31 +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 +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO + +* [hugo](/commands/hugo/) - hugo builds your site +* [hugo list all](/commands/hugo_list_all/) - List all posts +* [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 31-Jul-2019 diff --git a/content/en/commands/hugo_list_all.md b/content/en/commands/hugo_list_all.md new file mode 100644 index 000000000..6a820ee18 --- /dev/null +++ b/content/en/commands/hugo_list_all.md @@ -0,0 +1,46 @@ +--- +date: 2019-07-31 +title: "hugo list all" +slug: hugo_list_all +url: /commands/hugo_list_all/ +--- +## hugo list all + +List all posts + +### Synopsis + +List all of the posts in your content directory, include drafts, future and expired pages. + +``` +hugo list all [flags] +``` + +### Options + +``` + -h, --help help for all +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + -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 31-Jul-2019 diff --git a/content/en/commands/hugo_list_drafts.md b/content/en/commands/hugo_list_drafts.md new file mode 100644 index 000000000..2dff18040 --- /dev/null +++ b/content/en/commands/hugo_list_drafts.md @@ -0,0 +1,46 @@ +--- +date: 2019-07-31 +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) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + -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 31-Jul-2019 diff --git a/content/en/commands/hugo_list_expired.md b/content/en/commands/hugo_list_expired.md new file mode 100644 index 000000000..1703d9277 --- /dev/null +++ b/content/en/commands/hugo_list_expired.md @@ -0,0 +1,46 @@ +--- +date: 2019-07-31 +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) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + -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 31-Jul-2019 diff --git a/content/en/commands/hugo_list_future.md b/content/en/commands/hugo_list_future.md new file mode 100644 index 000000000..bb4621484 --- /dev/null +++ b/content/en/commands/hugo_list_future.md @@ -0,0 +1,46 @@ +--- +date: 2019-07-31 +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) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + -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 31-Jul-2019 diff --git a/content/en/commands/hugo_mod.md b/content/en/commands/hugo_mod.md new file mode 100644 index 000000000..f34cba2f3 --- /dev/null +++ b/content/en/commands/hugo_mod.md @@ -0,0 +1,83 @@ +--- +date: 2019-07-31 +title: "hugo mod" +slug: hugo_mod +url: /commands/hugo_mod/ +--- +## hugo mod + +Various Hugo Modules helpers. + +### 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 --ignoreVendor flag provided), +Go Modules, or a folder inside the themes directory, in that order. + +See https://gohugo.io/hugo-modules/ for more information. + + + +### 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. Defaults: $TMPDIR/hugo_cache/ + --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 + --disableKinds strings disable different kind of pages (home, RSS etc.) + --enableGitInfo add Git revision, date and author 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 mod + --i18n-warnings print missing translations + --ignoreCache ignores the cache directory + -l, --layoutDir string filesystem path to layout directory + --minify minify any supported output format (HTML, XML etc.) + --noChmod don't sync permission mode of files + --noTimes don't sync modification time of files + --path-warnings print warnings on duplicate target paths etc. + --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/) + --trace file write trace to file (not useful in general) +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO + +* [hugo](/commands/hugo/) - hugo builds your site +* [hugo mod clean](/commands/hugo_mod_clean/) - Delete the entire Hugo Module cache. +* [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 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. + +###### Auto generated by spf13/cobra on 31-Jul-2019 diff --git a/content/en/commands/hugo_mod_clean.md b/content/en/commands/hugo_mod_clean.md new file mode 100644 index 000000000..ef21ddfcd --- /dev/null +++ b/content/en/commands/hugo_mod_clean.md @@ -0,0 +1,52 @@ +--- +date: 2019-07-31 +title: "hugo mod clean" +slug: hugo_mod_clean +url: /commands/hugo_mod_clean/ +--- +## hugo mod clean + +Delete the entire Hugo Module cache. + +### Synopsis + +Delete the entire Hugo Module cache. + +Note that after you run this command, all of your dependencies will be re-downloaded next time you run "hugo". + +Also note that if you configure a positive maxAge for the "modules" file cache, it will also be cleaned as part of "hugo --gc". + + + +``` +hugo mod clean [flags] +``` + +### Options + +``` + -h, --help help for clean +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO + +* [hugo mod](/commands/hugo_mod/) - Various Hugo Modules helpers. + +###### Auto generated by spf13/cobra on 31-Jul-2019 diff --git a/content/en/commands/hugo_mod_get.md b/content/en/commands/hugo_mod_get.md new file mode 100644 index 000000000..678bcded5 --- /dev/null +++ b/content/en/commands/hugo_mod_get.md @@ -0,0 +1,71 @@ +--- +date: 2019-07-31 +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 module dependencies: + + hugo mod get -u + +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 --ignoreVendor 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] +``` + +### Options + +``` + -h, --help help for get +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO + +* [hugo mod](/commands/hugo_mod/) - Various Hugo Modules helpers. + +###### Auto generated by spf13/cobra on 31-Jul-2019 diff --git a/content/en/commands/hugo_mod_graph.md b/content/en/commands/hugo_mod_graph.md new file mode 100644 index 000000000..397283180 --- /dev/null +++ b/content/en/commands/hugo_mod_graph.md @@ -0,0 +1,48 @@ +--- +date: 2019-07-31 +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] +``` + +### Options + +``` + -h, --help help for graph +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO + +* [hugo mod](/commands/hugo_mod/) - Various Hugo Modules helpers. + +###### Auto generated by spf13/cobra on 31-Jul-2019 diff --git a/content/en/commands/hugo_mod_init.md b/content/en/commands/hugo_mod_init.md new file mode 100644 index 000000000..53c5412f1 --- /dev/null +++ b/content/en/commands/hugo_mod_init.md @@ -0,0 +1,53 @@ +--- +date: 2019-07-31 +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] +``` + +### Options + +``` + -h, --help help for init +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO + +* [hugo mod](/commands/hugo_mod/) - Various Hugo Modules helpers. + +###### Auto generated by spf13/cobra on 31-Jul-2019 diff --git a/content/en/commands/hugo_mod_tidy.md b/content/en/commands/hugo_mod_tidy.md new file mode 100644 index 000000000..68e7ac2d1 --- /dev/null +++ b/content/en/commands/hugo_mod_tidy.md @@ -0,0 +1,46 @@ +--- +date: 2019-07-31 +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. + +### Synopsis + +Remove unused entries in go.mod and go.sum. + +``` +hugo mod tidy [flags] +``` + +### Options + +``` + -h, --help help for tidy +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO + +* [hugo mod](/commands/hugo_mod/) - Various Hugo Modules helpers. + +###### Auto generated by spf13/cobra on 31-Jul-2019 diff --git a/content/en/commands/hugo_mod_vendor.md b/content/en/commands/hugo_mod_vendor.md new file mode 100644 index 000000000..f3beb1253 --- /dev/null +++ b/content/en/commands/hugo_mod_vendor.md @@ -0,0 +1,49 @@ +--- +date: 2019-07-31 +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] +``` + +### Options + +``` + -h, --help help for vendor +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO + +* [hugo mod](/commands/hugo_mod/) - Various Hugo Modules helpers. + +###### Auto generated by spf13/cobra on 31-Jul-2019 diff --git a/content/en/commands/hugo_new.md b/content/en/commands/hugo_new.md new file mode 100644 index 000000000..4587f4475 --- /dev/null +++ b/content/en/commands/hugo_new.md @@ -0,0 +1,80 @@ +--- +date: 2019-07-31 +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. + +Ensure you run this within the root directory of your site. + +``` +hugo new [path] [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. Defaults: $TMPDIR/hugo_cache/ + --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 + --disableKinds strings disable different kind of pages (home, RSS etc.) + --editor string edit new content with this editor, if provided + --enableGitInfo add Git revision, date and author 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 new + --i18n-warnings print missing translations + --ignoreCache ignores the cache directory + -k, --kind string content type to create + -l, --layoutDir string filesystem path to layout directory + --minify minify any supported output format (HTML, XML etc.) + --noChmod don't sync permission mode of files + --noTimes don't sync modification time of files + --path-warnings print warnings on duplicate target paths etc. + --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/) + --trace file write trace to file (not useful in general) +``` + +### Options inherited from parent commands + +``` + --config string config file (default is path/config.yaml|json|toml) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + -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 31-Jul-2019 diff --git a/content/en/commands/hugo_new_site.md b/content/en/commands/hugo_new_site.md new file mode 100644 index 000000000..29ab9e8ef --- /dev/null +++ b/content/en/commands/hugo_new_site.md @@ -0,0 +1,50 @@ +--- +date: 2019-07-31 +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) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + -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 31-Jul-2019 diff --git a/content/en/commands/hugo_new_theme.md b/content/en/commands/hugo_new_theme.md new file mode 100644 index 000000000..8a97aad97 --- /dev/null +++ b/content/en/commands/hugo_new_theme.md @@ -0,0 +1,49 @@ +--- +date: 2019-07-31 +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) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + -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 31-Jul-2019 diff --git a/content/en/commands/hugo_server.md b/content/en/commands/hugo_server.md new file mode 100644 index 000000000..7d64bdc33 --- /dev/null +++ b/content/en/commands/hugo_server.md @@ -0,0 +1,93 @@ +--- +date: 2019-07-31 +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. 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. Defaults: $TMPDIR/hugo_cache/ + --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 + --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 and author 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 + --i18n-warnings print missing translations + --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) + --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 + --minify minify any supported output format (HTML, XML etc.) + --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 + --path-warnings print warnings on duplicate target paths etc. + -p, --port int port on which the server will listen (default 1313) + --renderToDisk render to Destination path (default is render to memory & serve from there) + --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/) + --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 + +``` + --config string config file (default is path/config.yaml|json|toml) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO + +* [hugo](/commands/hugo/) - hugo builds your site + +###### Auto generated by spf13/cobra on 31-Jul-2019 diff --git a/content/en/commands/hugo_version.md b/content/en/commands/hugo_version.md new file mode 100644 index 000000000..c0bc9359b --- /dev/null +++ b/content/en/commands/hugo_version.md @@ -0,0 +1,46 @@ +--- +date: 2019-07-31 +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) + --configDir string config dir (default "config") + --debug debug output + -e, --environment string build environment + --ignoreVendor ignores any _vendor directory + --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 + --themesDir string filesystem path to themes directory + -v, --verbose verbose output + --verboseLog verbose logging +``` + +### SEE ALSO + +* [hugo](/commands/hugo/) - hugo builds your site + +###### Auto generated by spf13/cobra on 31-Jul-2019 diff --git a/content/en/content-management/_index.md b/content/en/content-management/_index.md new file mode 100644 index 000000000..28f2ecf82 --- /dev/null +++ b/content/en/content-management/_index.md @@ -0,0 +1,20 @@ +--- +title: Content Management +linktitle: Content Management Overview +description: Hugo makes managing large static sites easy with support for archetypes, content types, menus, cross references, summaries, and more. +date: 2017-02-01 +publishdate: 2017-02-01 +lastmod: 2017-02-01 +menu: + docs: + parent: "content-management" + weight: 1 +keywords: [source, organization] +categories: [content management] +weight: 01 #rem +draft: false +aliases: [/content/,/content/organization] +toc: false +--- + +A static site generator needs to extend beyond front matter and a couple of templates to be both scalable and *manageable*. Hugo was designed with not only developers in mind, but also content managers and authors. diff --git a/content/en/content-management/archetypes.md b/content/en/content-management/archetypes.md new file mode 100644 index 000000000..354ef0fef --- /dev/null +++ b/content/en/content-management/archetypes.md @@ -0,0 +1,97 @@ +--- +title: Archetypes +linktitle: Archetypes +description: Archetypes are templates used when creating new content. +date: 2017-02-01 +publishdate: 2017-02-01 +keywords: [archetypes,generators,metadata,front matter] +categories: ["content management"] +menu: + docs: + parent: "content-management" + weight: 70 + quicklinks: +weight: 70 #rem +draft: false +aliases: [/content/archetypes/] +toc: true +--- + +## What are Archetypes? + +**Archetypes** are content template files in the [archetypes directory][] of your project that contain preconfigured [front matter][] and possibly also a content disposition for your website's [content types][]. These will be used when you run `hugo new`. + + +The `hugo new` uses the `content-section` to find the most suitable archetype template in your project. If your project does not contain any archetype files, it will also look in the theme. + +{{< code file="archetype-example.sh" >}} +hugo new posts/my-first-post.md +{{< /code >}} + +The above will create a new content file in `content/posts/my-first-post.md` using the first archetype file found of these: + +1. `archetypes/posts.md` +2. `archetypes/default.md` +3. `themes/my-theme/archetypes/posts.md` +4. `themes/my-theme/archetypes/default.md` + +The last two list items are only applicable if you use a theme and it uses the `my-theme` theme name as an example. + +## Create a New Archetype Template + +A fictional example for the section `newsletter` and the archetype file `archetypes/newsletter.md`. Create a new file in `archetypes/newsletter.md` and open it in a text editor. + +{{< code file="archetypes/newsletter.md" >}} +--- +title: "{{ replace .Name "-" " " | title }}" +date: {{ .Date }} +draft: true +--- + +**Insert Lead paragraph here.** + +## New Cool Posts + +{{ range first 10 ( where .Site.RegularPages "Type" "cool" ) }} +* {{ .Title }} +{{ end }} +{{< /code >}} + +When you create a new newsletter with: + +```bash +hugo new newsletter/the-latest-cool.stuff.md +``` + +It will create a new newsletter type of content file based on the archetype template. + +**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. + +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. + + +## Directory based archetypes + +Since Hugo `0.49` you can use complete directories as archetype templates. Given this archetype directory: + +```bash +archetypes +├── default.md +└── post-bundle + ├── bio.md + ├── images + │ └── featured.jpg + └── index.md +``` + +```bash +hugo new --kind post-bundle posts/my-post +``` + +Will create a new folder in `/content/posts/my-post` with the same set of files as in the `post-bundle` archetypes folder. All content files (`index.md` etc.) can contain template logic, and will receive the correct `.Site` for the content's language. + + + +[archetypes directory]: /getting-started/directory-structure/ +[content types]: /content-management/types/ +[front matter]: /content-management/front-matter/ diff --git a/content/en/content-management/authors.md b/content/en/content-management/authors.md new file mode 100644 index 000000000..4cec5281a --- /dev/null +++ b/content/en/content-management/authors.md @@ -0,0 +1,184 @@ +--- +title: Authors +linktitle: Authors +description: +date: 2016-08-22 +publishdate: 2017-03-12 +lastmod: 2017-03-12 +keywords: [authors] +categories: ["content management"] +menu: + docs: + parent: "content-management" + weight: 55 +weight: 55 #rem +draft: true +aliases: [/content/archetypes/] +toc: true +comments: Before this page is published, need to also update both site- and page-level variables documentation. +--- + + + +Larger sites often have multiple content authors. Hugo provides standardized author profiles to organize relationships between content and content creators for sites operating under a distributed authorship model. + +## Author Profiles + +You can create a profile containing metadata for each author on your website. These profiles have to be saved under `data/_authors/`. The filename of the profile will later be used as an identifier. This way Hugo can associate content with one or multiple authors. An author's profile can be defined in the JSON, YAML, or TOML format. + +### Example: Author Profile + +Let's suppose Alice Allison is a blogger. A simple unique identifier would be `alice`. Now, we have to create a file called `alice.toml` in the `data/_authors/` directory. The following example is the standardized template written in TOML: + +{{< code file="data/_authors/alice.toml" >}} +givenName = "Alice" # or firstName as alias +familyName = "Allison" # or lastName as alias +displayName = "Alice Allison" +thumbnail = "static/authors/alice-thumb.jpg" +image = "static/authors/alice-full.jpg" +shortBio = "My name is Alice and I'm a blogger." +bio = "My name is Alice and I'm a blogger... some other stuff" +email = "alice.allison@email.com" +weight = 10 + +[social] + facebook = "alice.allison" + twitter = "alice" + website = "www.example.com" + +[params] + random = "whatever you want" +{{< /code >}} + +All variables are optional but it's advised to fill all important ones (e.g. names and biography) because themes can vary in their usage. + +You can store files for the `thumbnail` and `image` attributes in the `static` folder. Then add the path to the photos relative to `static`; e.g., `/static/path/to/thumbnail.jpg`. + +`weight` allows you to define the order of an author in an `.Authors` list and can be accessed on list or via the `.Site.Authors` variable. + +The `social` section contains all the links to the social network accounts of an author. Hugo is able to generate the account links for the most popular social networks automatically. This way, you only have to enter your username. You can find a list of all supported social networks [here](#linking-social-network-accounts-automatically). All other variables, like `website` in the example above remain untouched. + +The `params` section can contain arbitrary data much like the same-named section in the config file. What it contains is up to you. + +## Associate Content Through Identifiers + +Earlier it was mentioned that content can be associated with an author through their corresponding identifier. In our case, blogger Alice has the identifier `alice`. In the front matter of a content file, you can create a list of identifiers and assign it to the `authors` variable. Here are examples for `alice` using YAML and TOML, respectively. + +``` +--- +title: Why Hugo is so Awesome +date: 2016-08-22T14:27:502:00 +authors: ["alice"] +--- + +Nothing to read here. Move along... +``` + +``` ++++ +title = Why Hugo is so Awesome +date = "2016-08-22T14:27:502:00" +authors: ["alice"] ++++ + +Nothing to read here. Move along... +``` + +Future authors who might work on this blog post can append their identifiers to the `authors` array in the front matter as well. + +## Work with Templates + +After a successful setup it's time to give some credit to the authors by showing them on the website. Within the templates Hugo provides a list of the author's profiles if they are listed in the `authors` variable within the front matter. + +The list is accessible via the `.Authors` template variable. Printing all authors of a the blog post is straight forward: + +``` +{{ range .Authors }} + {{ .DisplayName }} +{{ end }} +=> Alice Allison +``` + +Even if there are co-authors you may only want to show the main author. For this case you can use the `.Author` template variable **(note the singular form)**. The template variable contains the profile of the author that is first listed with his identifier in the front matter. + +{{% note %}} +You can find a list of all template variables to access the profile information in [Author Variables](/variables/authors/). +{{% /note %}} + +### Link Social Network Accounts + +As aforementioned, Hugo is able to generate links to profiles of the most popular social networks. The following social networks with their corrersponding identifiers are supported: `github`, `facebook`, `twitter`, `pinterest`, `instagram`, `youtube` and `linkedin`. + +This is can be done with the `.Social.URL` function. Its only parameter is the name of the social network as they are defined in the profile (e.g. `facebook`, `twitter`). Custom variables like `website` remain as they are. + +Most articles feature a small section with information about the author at the end. Let's create one containing the author's name, a thumbnail, a (summarized) biography and links to all social networks: + +{{< code file="layouts/partials/author-info.html" download="author-info.html" >}} +{{ with .Author }} +

{{ .DisplayName }}

+ {{ .DisplayName }} +

{{ .ShortBio }}

+
    + {{ range $network, $username := .Social }} +
  • {{ $network }}
  • + {{ end }} +
+{{ end }} +{{< /code >}} + +## Who Published What? + +That question can be answered with a list of all authors and another list containing all articles that they each have written. Now we have to translate this idea into templates. The [taxonomy][] feature allows us to logically group content based on information that they have in common; e.g. a tag or a category. Well, many articles share the same author, so this should sound familiar, right? + +In order to let Hugo know that we want to group content based on their author, we have to create a new taxonomy called `author` (the name corresponds to the variable in the front matter). Here is the snippet in a `config.yaml` and `config.toml`, respectively: + +``` +taxonomies: + author: authors +``` + +``` +[taxonomies] + author = "authors" +``` + + +### List All Authors + +In the next step we can create a template to list all authors of your website. Later, the list can be accessed at `www.example.com/authors/`. Create a new template in the `layouts/taxonomy/` directory called `authors.term.html`. This template will be exclusively used for this taxonomy. + +{{< code file="layouts/taxonomy/author.term.html" download="author.term.html" >}} + +{{< /code >}} + +`.Data.Terms` contains the identifiers of all authors and we can range over it to create a list with all author names. The `$profile` variable gives us access to the profile of the current author. This allows you to generate a nice info box with a thumbnail, a biography and social media links, like at the [end of a blog post](#linking-social-network-accounts-automatically). + +### List Each Author's Publications + +Last but not least, we have to create the second list that contains all publications of an author. Each list will be shown in its own page and can be accessed at `www.example.com/authors/`. Replace `` with a valid author identifier like `alice`. + +The layout for this page can be defined in the template `layouts/taxonomy/author.html`. + +{{< code file="layouts/taxonomy/author.html" download="author.html" >}} +{{ range .Pages }} +

{{ .Title }}

+ written by {{ .Author.DisplayName }} + {{ .Summary }} +{{ end }} +{{< /code >}} + +The example above generates a simple list of all posts written by a single author. Inside the loop you've access to the complete set of [page variables][pagevars]. Therefore, you can add additional information about the current posts like the publishing date or the tags. + +With a lot of content this list can quickly become very long. Consider to use the [pagination][] feature. It splits the list into smaller chunks and spreads them over multiple pages. + +[pagevars]: /variables/page/ +[pagination]: /templates/pagination/ diff --git a/content/en/content-management/comments.md b/content/en/content-management/comments.md new file mode 100644 index 000000000..47b3ccf16 --- /dev/null +++ b/content/en/content-management/comments.md @@ -0,0 +1,88 @@ +--- +title: Comments +linktitle: 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. +date: 2017-02-01 +publishdate: 2017-02-01 +lastmod: 2017-03-09 +keywords: [sections,content,organization] +categories: [project organization, fundamentals] +menu: + docs: + parent: "content-management" + weight: 140 +weight: 140 #rem +draft: false +aliases: [/extras/comments/] +toc: true +--- + +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 copy="false" >}} +disqusShortname = "yourdiscussshortname" +{{}} + +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](https://gohugo.io/templates/internal/#disqus) available, to render it add the following code where you want comments to appear: + +``` +{{ template "_internal/disqus.html" . }} +``` + +## Comments Alternatives + +There are a few alternatives to commenting on static sites for those who do not want to use Disqus: + +* [Static Man](https://staticman.net/) +* [Talkyard](https://www.talkyard.io/blog-comments) (Open source, & serverless hosting) +* [txtpen](https://txtpen.github.io/hn/) +* [IntenseDebate](https://intensedebate.com/) +* [Graph Comment][] +* [Muut](https://muut.com/) +* [isso](https://posativ.org/isso/) (Self-hosted, Python) + * [Tutorial on Implementing Isso with Hugo][issotutorial] +* [Utterances](https://utteranc.es/) (Open source, Github comments widget built on Github issues) +* [Remark](https://github.com/umputun/remark) (Open source, Golang, Easy to run docker) +* [Commento](https://commento.io/) (Open Source, available as a service, local install, or docker image) +* [JustComments](https://just-comments.com) (Open Source, available as a service, can be self-hosted) + + + + + + +[configuration]: /getting-started/configuration/ +[disquspartial]: /templates/partials/#disqus +[disqussetup]: https://disqus.com/profile/signup/ +[forum]: https://discourse.gohugo.io +[front matter]: /content-management/front-matter/ +[Graph Comment]: https://graphcomment.com/ +[kaijuissue]: https://github.com/spf13/kaiju/issues/new +[issotutorial]: https://stiobhart.net/2017-02-24-isso-comments/ +[partials]: /templates/partials/ +[MongoDB]: https://www.mongodb.com/ +[tweet]: https://twitter.com/spf13 diff --git a/content/en/content-management/cross-references.md b/content/en/content-management/cross-references.md new file mode 100644 index 000000000..21a73b861 --- /dev/null +++ b/content/en/content-management/cross-references.md @@ -0,0 +1,100 @@ +--- +title: Links and Cross References +description: Shortcodes for creating links to documents. +date: 2017-02-01 +publishdate: 2017-02-01 +lastmod: 2017-03-31 +categories: [content management] +keywords: ["cross references","references", "anchors", "urls"] +menu: + docs: + parent: "content-management" + weight: 100 +weight: 100 #rem +aliases: [/extras/crossreferences/] +toc: true +--- + + +The `ref` and `relref` shortcode resolves the absolute or relative permalink given a path to a document. + +## Use `ref` and `relref` + +```go-html-template +{{}} +{{}} +{{}} +{{}} +{{}} +{{}} +{{}} +{{}} +``` + +The single parameter to `ref` is a string with a content `documentname` (e.g., `about.md`) with or without an appended in-document `anchor` (`#who`) without spaces. Hugo is flexible in how we search for documents, so the file suffix may be omitted. + +**Paths without a leading `/` will first be tried resolved relative to the current page.** + +You will get an error if your document could not be uniquely resolved. The error behaviour can be configured, see below. + +### Link to another language version + +Link to another language version of a document, you need to use this syntax: + +```go-html-template +{{}} +``` + +### Get another Output Format + +To link to a given Output Format of a document, you can use this syntax: + +```go-html-template +{{}} +``` + +### Anchors + +When an `anchor` is provided by itself, the current page’s unique identifier will be appended; when an `anchor` is provided appended to `documentname`, the found page's unique identifier will be appended: + +```go-html-template +{{}} => #anchors:9decaf7 +``` + +The above examples render as follows for this very page as well as a reference to the "Content" heading in the Hugo docs features pageyoursite + +```go-html-template +{{}} => #who:9decaf7 +{{}} => /blog/post/#who:badcafe +``` + +More information about document unique identifiers and headings can be found [below]({{< ref "#hugo-heading-anchors" >}}). + +## 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.) + +``` +{{}} +/content-management/cross-references/#hugo-heading-anchors:77cd9ea530577debf4ce0f28c8dca242 +``` + +## Ref and RelRef Configuration + +The behaviour can, since Hugo 0.45, be configured in `config.toml`: + +refLinksErrorLevel ("ERROR") +: When using `ref` or `relref` to resolve page links and a link cannot resolved, it will be logged with this log level. Valid values are `ERROR` (default) or `WARNING`. Any `ERROR` will fail the build (`exit -1`). + +refLinksNotFoundURL +: URL to be used as a placeholder when a page reference cannot be found in `ref` or `relref`. Is used as-is. + + +[lists]: /templates/lists/ +[output formats]: /templates/output-formats/ +[shortcode]: /content-management/shortcodes/ +[bfext]: /content-management/formats/#blackfriday-extensions diff --git a/content/en/content-management/formats.md b/content/en/content-management/formats.md new file mode 100644 index 000000000..a0ed992f2 --- /dev/null +++ b/content/en/content-management/formats.md @@ -0,0 +1,248 @@ +--- +title: Supported Content Formats +linktitle: Supported Content Formats +description: Both HTML and Markdown are supported content formats. +date: 2017-01-10 +publishdate: 2017-01-10 +lastmod: 2017-04-06 +categories: [content management] +keywords: [markdown,asciidoc,mmark,pandoc,content format] +menu: + docs: + parent: "content-management" + weight: 20 +weight: 20 #rem +draft: false +aliases: [/content/markdown-extras/,/content/supported-formats/,/doc/supported-formats/,/tutorials/mathjax/] +toc: true +--- + +**Markdown is the main content format** and comes in two flavours: The excellent [Blackfriday project][blackfriday] (name your files `*.md` or set `markup = "markdown"` in front matter) or its fork [Mmark][mmark] (name your files `*.mmark` or set `markup = "mmark"` in front matter), both very fast markdown engines written in Go. + +For Emacs users, [go-org](https://github.com/niklasfasching/go-org) provides built-in native support for Org-mode (name your files `*.org` or set `markup = "org"` in front matter) + +But in many situations, plain HTML is what you want. Just name your files with `.html` or `.htm` extension inside your content folder. Note that if you want your HTML files to have a layout, they need front matter. It can be empty, but it has to be there: + +```html +--- +title: "This is a content file in HTML" +--- + +
+

Hello, Hugo!

+
+``` + +{{% note "Deeply Nested Lists" %}} +Before you begin writing your content in markdown, Blackfriday has a known issue [(#329)](https://github.com/russross/blackfriday/issues/329) with handling deeply nested lists. Luckily, there is an easy workaround. Use 4-spaces (i.e., tab) rather than 2-space indentations. +{{% /note %}} + +## Configure BlackFriday Markdown Rendering + +You can configure multiple aspects of Blackfriday as shown in the following list. See the docs on [Configuration][config] for the full list of explicit directions you can give to Hugo when rendering your site. + +{{< readfile file="/content/en/readfiles/bfconfig.md" markdown="true" >}} + +## Extend Markdown + +Hugo provides some convenient methods for extending markdown. + +### Task Lists + +Hugo supports [GitHub-styled task lists (i.e., TODO lists)][gfmtasks] for the Blackfriday markdown renderer. If you do not want to use this feature, you can disable it in your configuration. + +#### Example Task List Input + +{{< code file="content/my-to-do-list.md" >}} +- [ ] a task list item +- [ ] list syntax required +- [ ] incomplete +- [x] completed +{{< /code >}} + +#### Example Task List Output + +The preceding markdown produces the following HTML in your rendered website: + +``` +
    +
  • a task list item
  • +
  • list syntax required
  • +
  • incomplete
  • +
  • completed
  • +
+``` + +#### Example Task List Display + +The following shows how the example task list will look to the end users of your website. Note that visual styling of lists is up to you. This list has been styled according to [the Hugo Docs stylesheet][hugocss]. + +- [ ] a task list item +- [ ] list syntax required +- [ ] incomplete +- [x] completed + +### Emojis + +To add emojis directly to content, set `enableEmoji` to `true` in your [site configuration][config]. To use emojis in templates or shortcodes, see [`emojify` function][]. + +For a full list of emojis, see the [Emoji cheat sheet][emojis]. + +### Shortcodes + +If you write in Markdown and find yourself frequently embedding your content with raw HTML, Hugo provides built-in shortcodes functionality. This is one of the most powerful features in Hugo and allows you to create your own Markdown extensions very quickly. + +See [Shortcodes][sc] for usage, particularly for the built-in shortcodes that ship with Hugo, and [Shortcode Templating][sct] to learn how to build your own. + +### Code Blocks + +Hugo supports GitHub-flavored markdown's use of triple back ticks, as well as provides a special [`highlight` shortcode][hlsc], and syntax highlights those code blocks natively using *Chroma*. Users also have an option to use *Pygments* instead. See the [Syntax Highlighting][hl] section for details. + +## Mmark + +Mmark is a [fork of BlackFriday][mmark] and markdown superset that is well suited for writing [IETF documentation][ietf]. You can see examples of the syntax in the [Mmark GitHub repository][mmark] or the full syntax on [Miek Gieben's website][]. + +### Use Mmark + +As Hugo ships with Mmark, using the syntax is as easy as changing the extension of your content files from `.md` to `.mmark`. + +In the event that you want to only use Mmark in specific files, you can also define the Mmark syntax in your content's front matter: + +``` +--- +title: My Post +date: 2017-04-01 +markup: mmark +--- +``` + +{{% warning %}} +Thare are some features not available in Mmark; one example being that shortcodes are not translated when used in an included `.mmark` file ([#3131](https://github.com/gohugoio/hugo/issues/3137)), and `EXTENSION_ABBREVIATION` ([#1970](https://github.com/gohugoio/hugo/issues/1970)) and the aforementioned GFM todo lists ([#2270](https://github.com/gohugoio/hugo/issues/2270)) are not fully supported. Contributions are welcome. +{{% /warning %}} + +## MathJax with Hugo + +[MathJax](https://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. + +### Enable 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](https://docs.mathjax.org/en/latest/web/configuration.html) section of the MathJax documentation for additional methods of including MathJax), but the easiest way is to use the secure MathJax CDN by include a ` +{{< /code >}} + +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](https://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 + +{{% note %}} +The following issues with Markdown assume you are using `.md` for content and BlackFriday for parsing. Using [Mmark](#mmark) as your content format will obviate the need for the following workarounds. + +When using Mmark with MathJax, use `displayMath: [['$$','$$'], ['\\[','\\]']]`. See the [Mmark `README.md`](https://github.com/miekg/mmark/wiki/Syntax#math-blocks) for more information. In addition to MathJax, Mmark has been shown to work well with [KaTeX](https://github.com/Khan/KaTeX). See this [related blog post from a Hugo user](http://nosubstance.me/post/a-great-toolset-for-static-blogging/). +{{% /note %}} + +After enabling MathJax, any math entered between proper markers (see the [MathJax documentation][mathjaxdocs]) 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). + +{{< code file="mathjax-markdown-solution.html" >}} + +{{< /code >}} + +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 file="mathjax-style.css" >}} +code.has-jax { + font: inherit; + font-size: 100%; + background: inherit; + border: inherit; + color: #515151; +} +{{< /code >}} + +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! + +## Additional Formats Through External Helpers + +Hugo has a new concept called _external helpers_. It means that you can write your content using [Asciidoc][ascii], [reStructuredText][rest], or [pandoc]. If you have files with associated extensions, Hugo will call external commands to generate the content. ([See the Hugo source code for external helpers][helperssource].) + +For example, for Asciidoc files, Hugo will try to call the `asciidoctor` or `asciidoc` command. This means that you will have to install the associated tool on your machine to be able to use these formats. ([See the Asciidoctor docs for installation instructions](https://asciidoctor.org/docs/install-toolchain/)). + +To use these formats, just use the standard extension and the front matter exactly as you would do with natively supported `.md` files. + +Hugo passes reasonable default arguments to these external helpers by default: + +- `asciidoc`: `--no-header-footer --safe -` +- `asciidoctor`: `--no-header-footer --safe --trace -` +- `rst2html`: `--leave-comments --initial-header-level=2` +- `pandoc`: `--mathjax` + +{{% warning "Performance of External Helpers" %}} +Because additional formats are external commands generation performance will rely heavily on the performance of the external tool you are using. As this feature is still in its infancy, feedback is welcome. +{{% /warning %}} + +## Learn Markdown + +Markdown syntax is simple enough to learn in a single sitting. The following are excellent resources to get you up and running: + +* [Daring Fireball: Markdown, John Gruber (Creator of Markdown)][fireball] +* [Markdown Cheatsheet, Adam Pritchard][mdcheatsheet] +* [Markdown Tutorial (Interactive), Garen Torikian][mdtutorial] +* [The Markdown Guide, Matt Cone][mdguide] + +[`emojify` function]: /functions/emojify/ +[ascii]: https://asciidoctor.org/ +[bfconfig]: /getting-started/configuration/#configuring-blackfriday-rendering +[blackfriday]: https://github.com/russross/blackfriday +[mmark]: https://github.com/miekg/mmark +[config]: /getting-started/configuration/ +[developer tools]: /tools/ +[emojis]: https://www.webpagefx.com/tools/emoji-cheat-sheet/ +[fireball]: https://daringfireball.net/projects/markdown/ +[gfmtasks]: https://guides.github.com/features/mastering-markdown/#syntax +[helperssource]: https://github.com/gohugoio/hugo/blob/77c60a3440806067109347d04eb5368b65ea0fe8/helpers/general.go#L65 +[hl]: /content-management/syntax-highlighting/ +[hlsc]: /content-management/shortcodes/#highlight +[hugocss]: /css/style.css +[ietf]: https://tools.ietf.org/html/ +[mathjaxdocs]: https://docs.mathjax.org/en/latest/ +[mdcheatsheet]: https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet +[mdguide]: https://www.markdownguide.org/ +[mdtutorial]: https://www.markdowntutorial.com/ +[Miek Gieben's website]: https://miek.nl/2016/march/05/mmark-syntax-document/ +[mmark]: https://github.com/mmarkdown/mmark +[org]: https://orgmode.org/ +[pandoc]: https://www.pandoc.org/ +[Pygments]: http://pygments.org/ +[rest]: http://docutils.sourceforge.net/rst.html +[sc]: /content-management/shortcodes/ +[sct]: /templates/shortcode-templates/ diff --git a/content/en/content-management/front-matter.md b/content/en/content-management/front-matter.md new file mode 100644 index 000000000..0e3baf437 --- /dev/null +++ b/content/en/content-management/front-matter.md @@ -0,0 +1,210 @@ +--- +title: Front Matter +linktitle: +description: Hugo allows you to add front matter in yaml, toml, or json to your content files. +date: 2017-01-09 +publishdate: 2017-01-09 +lastmod: 2017-02-24 +categories: [content management] +keywords: ["front matter", "yaml", "toml", "json", "metadata", "archetypes"] +menu: + docs: + parent: "content-management" + weight: 30 +weight: 30 #rem +draft: false +aliases: [/content/front-matter/] +toc: true +--- + +**Front matter** allows you to keep metadata attached to an instance of a [content type][]---i.e., embedded inside a content file---and is one of the many features that gives Hugo its strength. + +{{< youtube Yh2xKRJGff4 >}} + +## Front Matter Formats + +Hugo supports four formats for front matter, each with their own identifying tokens. + +TOML +: identified by opening and closing `+++`. + +YAML +: identified by opening and closing `---`. + +JSON +: a single JSON object surrounded by '`{`' and '`}`', followed by a new line. + +ORG +: a group of Org mode keywords in the format '`#+KEY: VALUE`'. Any line that does not start with `#+` ends the front matter section. + Keyword values can be either strings (`#+KEY: VALUE`) or a whitespace separated list of strings (`#+KEY[]: VALUE_1 VALUE_2`). + +### Example + +{{< code-toggle >}} +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" +{{< /code-toggle >}} + +## Front Matter Variables + +### Predefined + +There are a few predefined variables that Hugo is aware of. See [Page Variables][pagevars] for how to call many of these predefined variables in your templates. + +aliases +: an array of one or more aliases (e.g., old published paths of renamed content) that will be created in the output directory structure . See [Aliases][aliases] for details. + +audio +: an array of paths to audio files related to the page; used by the `opengraph` [internal template](/templates/internal) to populate `og:audio`. + +cascade +: a map of Front Matter keys whose values are passed down to the page's descendents unless overwritten by self or a closer ancestor's cascade. See [Front Matter Cascade](#front-matter-cascade) for details. + +date +: the datetime assigned to this page. This is usually fetched from the `date` field in front matter, but this behaviour is configurable. + +description +: the description for the content. + +draft +: if `true`, the content will not be rendered unless the `--buildDrafts` flag is passed to the `hugo` command. + +expiryDate +: the datetime at which the content should no longer be published by Hugo; expired content will not be rendered unless the `--buildExpired` flag is passed to the `hugo` command. + +headless +: if `true`, sets a leaf bundle to be [headless][headless-bundle]. + +images +: an array of paths to images related to the page; used by [internal templates](/templates/internal) such as `_internal/twitter_cards.html`. + +isCJKLanguage +: if `true`, Hugo will explicitly treat the content as a CJK language; both `.Summary` and `.WordCount` work properly in CJK languages. + +keywords +: the meta keywords for the content. + +layout +: the layout Hugo should select from the [lookup order][lookup] when rendering the content. If a `type` is not specified in the front matter, Hugo will look for the layout of the same name in the layout directory that corresponds with a content's section. See ["Defining a Content Type"][definetype] + +lastmod +: the datetime at which the content was last modified. + +linkTitle +: used for creating links to content; if set, Hugo defaults to using the `linktitle` before the `title`. Hugo can also [order lists of content by `linktitle`][bylinktitle]. + +markup +: **experimental**; specify `"rst"` for reStructuredText (requires`rst2html`) or `"md"` (default) for Markdown. + +outputs +: allows you to specify output formats specific to the content. See [output formats][outputs]. + +publishDate +: if in the future, content will not be rendered unless the `--buildFuture` flag is passed to `hugo`. + +resources +: used for configuring page bundle resources. See [Page Resources][page-resources]. + +series +: an array of series this page belongs to, as a subset of the `series` [taxonomy](/content-management/taxonomies/); used by the `opengraph` [internal template](/templates/internal) to populate `og:see_also`. + +slug +: appears as the tail of the output URL. A value specified in front matter will override the segment of the URL based on the filename. + +summary +: text used when providing a summary of the article in the `.Summary` page variable; details available in the [content-summaries](/content-management/summaries/) section. + +title +: the title for the content. + +type +: the type of the content; this value will be automatically derived from the directory (i.e., the [section][]) if not specified in front matter. + +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. + +videos +: an array of paths to videos related to the page; used by the `opengraph` [internal template](/templates/internal) to populate `og:video`. + +weight +: used for [ordering your content in lists][ordering]. Lower weight gets higher precedence. So content with lower weight will come first. + +\ +: field name of the *plural* form of the index. See `tags` and `categories` in the above front matter examples. _Note that the plural form of user-defined taxonomies cannot be the same as any of the predefined front matter variables._ + +{{% note "Hugo's Default URL Destinations" %}} +If neither `slug` nor `url` is present and [permalinks are not configured otherwise in your site `config` file](/content-management/urls/#permalinks), Hugo will use the filename of your content to create the output URL. See [Content Organization](/content-management/organization) for an explanation of paths in Hugo and [URL Management](/content-management/urls/) for ways to customize Hugo's default behaviors. +{{% /note %}} + +### User-Defined + +You can add fields to your front matter arbitrarily to meet your needs. These user-defined key-values are placed into a single `.Params` variable for use in your templates. + +The following fields can be accessed via `.Params.include_toc` and `.Params.show_comments`, respectively. The [Variables][] section provides more information on using Hugo's page- and site-level variables in your templates. + +{{< code-toggle copy="false" >}} +include_toc: true +show_comments: false +{{}} + +## Front Matter Cascade + +Any node or section can pass down to descendents a set of Front Matter values as long as defined underneath the reserved `cascade` Front Matter key. + +### Example +```yaml +# content/blog/_index.md +title: Blog +cascade: + banner: images/typewriter.jpg +``` + +With the above example the Blog section page and its descendents will return `images/typewriter.jpg` when `.Params.banner` is invoked unless: + +- Said descendent has its own `banner` value set +- Or a closer ancestor node has its own `cascade.banner` value set. + +## Order Content Through Front Matter + +You can assign content-specific `weight` in the front matter of your content. These values are especially useful for [ordering][ordering] in list views. You can use `weight` for ordering of content and the convention of [`_weight`][taxweight] for ordering content within a taxonomy. See [Ordering and Grouping Hugo Lists][lists] to see how `weight` can be used to organize your content in list views. + +## Override Global Markdown Configuration + +It's possible to set some options for Markdown rendering in a content's front matter as an override to the [BlackFriday rendering options set in your project configuration][config]. + +## Front Matter Format Specs + +* [TOML Spec][toml] +* [YAML Spec][yaml] +* [JSON Spec][json] + +[variables]: /variables/ +[aliases]: /content-management/urls/#aliases +[archetype]: /content-management/archetypes/ +[bylinktitle]: /templates/lists/#by-link-title +[config]: /getting-started/configuration/ "Hugo documentation for site configuration" +[content type]: /content-management/types/ +[contentorg]: /content-management/organization/ +[definetype]: /content-management/types/#defining-a-content-type "Learn how to specify a type and a layout in a content's front matter" +[headless-bundle]: /content-management/page-bundles/#headless-bundle +[json]: https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf "Specification for JSON, JavaScript Object Notation" +[lists]: /templates/lists/#ordering-content "See how to order content in list pages; for example, templates that look to specific _index.md for content and front matter." +[lookup]: /templates/lookup-order/ "Hugo traverses your templates in a specific order when rendering content to allow for DRYer templating." +[ordering]: /templates/lists/ "Hugo provides multiple ways to sort and order your content in list templates" +[outputs]: /templates/output-formats/ "With the release of v22, you can output your content to any text format using Hugo's familiar templating" +[page-resources]: /content-management/page-resources/ +[pagevars]: /variables/page/ +[section]: /content-management/sections/ +[taxweight]: /content-management/taxonomies/ +[toml]: https://github.com/toml-lang/toml "Specification for TOML, Tom's Obvious Minimal Language" +[urls]: /content-management/urls/ +[variables]: /variables/ +[yaml]: https://yaml.org/spec/ "Specification for YAML, YAML Ain't Markup Language" diff --git a/content/en/content-management/image-processing/index.md b/content/en/content-management/image-processing/index.md new file mode 100644 index 000000000..63911b159 --- /dev/null +++ b/content/en/content-management/image-processing/index.md @@ -0,0 +1,292 @@ +--- +title: "Image Processing" +description: "Image Page resources can be resized and cropped." +date: 2018-01-24T13:10:00-05:00 +linktitle: "Image Processing" +categories: ["content management"] +keywords: [resources,images] +weight: 4004 +draft: false +toc: true +menu: + docs: + parent: "content-management" + weight: 32 +--- + +## The Image Page Resource + +The `image` is a [Page Resource]({{< relref "/content-management/page-resources" >}}), and the processing methods listed below does not work on images inside your `/static` folder. + +To get all images in a [Page Bundle]({{< relref "/content-management/organization#page-bundles" >}}): + +```go-html-template +{{ with .Resources.ByType "image" }} +{{ end }} + +``` + +## Image Processing Methods + + +The `image` resource implements the methods `Resize`, `Fit` and `Fill`, each returning the transformed image using the specified dimensions and processing options. The `image` resource also, since Hugo 0.58, implements the method `Exif` and `Filter`. + +### Resize + +Resizes the image to the specified width and height. + +```go +// Resize to a width of 600px and preserve ratio +{{ $image := $resource.Resize "600x" }} + +// Resize to a height of 400px and preserve ratio +{{ $image := $resource.Resize "x400" }} + +// Resize to a width 600px and a height of 400px +{{ $image := $resource.Resize "600x400" }} +``` + +### Fit +Scale down the image to fit the given dimensions while maintaining aspect ratio. Both height and width are required. + +```go +{{ $image := $resource.Fit "600x400" }} +``` + +### Fill +Resize and crop the image to match the given dimensions. Both height and width are required. + +```go +{{ $image := $resource.Fill "600x400" }} +``` + +### Filter + +Apply one or more filters to your image. See [Image Filters](/functions/images/#image-filters) for a full list. + +```go-html-template +{{ $img = $img.Filter (images.GaussianBlur 6) (images.Pixelate 8) }} +``` + +The above can also be written in a more functional style using pipes: + +```go-html-template +{{ $img = $img | images.Filter (images.GaussianBlur 6) (images.Pixelate 8) }} +``` + +The filters will be applied in the given order. + +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) }} +{{ $img1 = $img1.Filter $filters }} +{{ $img2 = $img2.Filter $filters }} +``` + +### Exif + +Provides an [Exif](https://en.wikipedia.org/wiki/Exif) object with metadata about the image. + +Note that this is only suported for JPG and TIFF images, so it's recommended to wrap the access with a `with`, e.g.: + +```go-html-template +{{ with $img.Exif }} +Date: {{ .Date }} +Lat/Long: {{ .Lat}}/{{ .Long }} +Tags: +{{ range $k, $v := .Tags }} +TAG: {{ $k }}: {{ $v }} +{{ end }} +``` + +#### Exif fields + +Data +: "photo taken" date/time + +Lat +: "photo taken where", GPS latitude + +Long +: "photo taken where", GPS longitude + +See [Image Processing Config](#image-processing-config) for how to configure what gets included in Exif. + + + + + +## Image Processing Options + +In addition to the dimensions (e.g. `600x400`), Hugo supports a set of additional image options. + +### Background Color + +The background color to fill into the transparency layer. This is mostly useful when converting to a format that does not support transparency, e.g. `JPEG`. + +You can set the background color to use with a 3 or 6 digit hex code starting with `#`. + +```go +{{ $image.Resize "600x jpg #b31280" }} +``` + +For color codes, see https://www.google.com/search?q=color+picker + +### JPEG Quality +Only relevant for JPEG images, values 1 to 100 inclusive, higher is better. Default is 75. + +```go +{{ $image.Resize "600x q50" }} +``` + +### Rotate +Rotates an image by the given angle counter-clockwise. The rotation will be performed first to get the dimensions correct. The main use of this is to be able to manually correct for [EXIF orientation](https://github.com/golang/go/issues/4341) of JPEG images. + +```go +{{ $image.Resize "600x r90" }} +``` + +### Anchor +Only relevant for the `Fill` method. This is useful for thumbnail generation where the main motive is located in, say, the left corner. +Valid are `Center`, `TopLeft`, `Top`, `TopRight`, `Left`, `Right`, `BottomLeft`, `Bottom`, `BottomRight`. + +```go +{{ $image.Fill "300x200 BottomLeft" }} +``` + +### Resample Filter +Filter used in resizing. Default is `Box`, a simple and fast resampling filter appropriate for downscaling. + +Examples are: `Box`, `NearestNeighbor`, `Linear`, `Gaussian`. + +See https://github.com/disintegration/imaging for more. If you want to trade quality for faster processing, this may be a option to test. + +```go +{{ $image.Resize "600x400 Gaussian" }} +``` + +### Target Format + +By default the images is encoded in the source format, but you can set the target format as an option. + +Valid values are `jpg`, `png`, `tif`, `bmp`, and `gif`. + +```go +{{ $image.Resize "600x jpg" }} +``` + +## Image Processing Examples + +_The photo of the sunset used in the examples below is Copyright [Bjørn Erik Pedersen](https://commons.wikimedia.org/wiki/User:Bep) (Creative Commons Attribution-Share Alike 4.0 International license)_ + + +{{< imgproc sunset Resize "300x" />}} + +{{< imgproc sunset Fill "90x120 left" />}} + +{{< imgproc sunset Fill "90x120 right" />}} + +{{< imgproc sunset Fit "90x90" />}} + +{{< imgproc sunset Resize "300x q10" />}} + + +This is the shortcode used in the examples above: + + +{{< code file="layouts/shortcodes/imgproc.html" >}} +{{< readfile file="layouts/shortcodes/imgproc.html" >}} +{{< /code >}} + +And it is used like this: + +```go-html-template +{{}} +``` + + +{{% note %}} +**Tip:** Note the self-closing shortcode syntax above. The `imgproc` shortcode can be called both with and without **inner content**. +{{% /note %}} + +## Image Processing Config + +You can configure an `imaging` section in `config.toml` with default image processing options: + +```toml +[imaging] +# Default resample filter used for resizing. Default is Box, +# a simple and fast averaging filter appropriate for downscaling. +# See https://github.com/disintegration/imaging +resampleFilter = "box" + +# Default JPEG quality setting. Default is 75. +quality = 75 + +# Anchor used when cropping pictures. +# Default is "smart" which does Smart Cropping, using https://github.com/muesli/smartcrop +# Smart Cropping is content aware and tries to find the best crop for each image. +# Valid values are Smart, Center, TopLeft, Top, TopRight, Left, Right, BottomLeft, Bottom, BottomRight +anchor = "smart" + +# Default background color. +# Hugo will preserve transparency for target formats that supports it, +# but will fall back to this color for JPEG. +# Expects a standard HEX color string with 3 or 6 digits. +# See https://www.google.com/search?q=color+picker +bgColor = "#ffffff" + +[imaging.exif] + # Regexp matching the fields you want to Exclude from the (massive) set of Exif info +# available. As we cache this info to disk, this is for performance and +# disk space reasons more than anything. +# If you want it all, put ".*" in this config setting. +# Note that if neither this or ExcludeFields is set, Hugo will return a small +# default set. +includeFields = "" + +# Regexp matching the Exif fields you want to exclude. This may be easier to use +# than IncludeFields above, depending on what you want. +excludeFields = "" + +# Hugo extracts the "photo taken" date/time into .Date by default. +# Set this to true to turn it off. +disableDate = false + +# Hugo extracts the "photo taken where" (GPS latitude and longitude) into +# .Long and .Lat. Set this to true to turn it off. +disableLatLong = false + + +``` + +## Smart Cropping of Images + +By default, Hugo will use the [Smartcrop](https://github.com/muesli/smartcrop), a library created by [muesli](https://github.com/muesli), when cropping images with `.Fill`. You can set the anchor point manually, but in most cases the smart option will make a good choice. And we will work with the library author to improve this in the future. + +An example using the sunset image from above: + + +{{< imgproc sunset Fill "200x200 smart" />}} + + +## Image Processing Performance Consideration + +Processed images are stored below `/resources` (can be set with `resourceDir` config setting). This folder is deliberately placed in the project, as it is recommended to check these into source control as part of the project. These images are not "Hugo fast" to generate, but once generated they can be reused. + +If you change your image settings (e.g. size), remove or rename images etc., you will end up with unused images taking up space and cluttering your project. + +To clean up, run: + +```bash +hugo --gc +``` + + +{{% note %}} +**GC** is short for **Garbage Collection**. +{{% /note %}} + + + diff --git a/hugolib/assets/images/sunset.jpg b/content/en/content-management/image-processing/sunset.jpg similarity index 100% rename from hugolib/assets/images/sunset.jpg rename to content/en/content-management/image-processing/sunset.jpg diff --git a/content/en/content-management/menus.md b/content/en/content-management/menus.md new file mode 100644 index 000000000..9ac6f8bff --- /dev/null +++ b/content/en/content-management/menus.md @@ -0,0 +1,123 @@ +--- +title: Menus +linktitle: Menus +description: Hugo has a simple yet powerful menu system. +date: 2017-02-01 +publishdate: 2017-02-01 +lastmod: 2017-03-31 +categories: [content management] +keywords: [menus] +draft: false +menu: + docs: + parent: "content-management" + weight: 120 +weight: 120 #rem +aliases: [/extras/menus/] +toc: true +--- + +{{% note "Lazy Blogger"%}} +If all you want is a simple menu for your sections, see the ["Section Menu for Lazy Bloggers" in Menu Templates](/templates/menu-templates/#section-menu-for-lazy-bloggers). +{{% /note %}} + +You can do this: + +* Place content in one or many menus +* Handle nested menus with unlimited depth +* Create menu entries without being attached to any content +* Distinguish active element (and active branch) + +## What is a Menu in Hugo? + +A **menu** is a named array of menu entries accessible by name via the [`.Site.Menus` site variable][sitevars]. For example, you can access your site's `main` menu via `.Site.Menus.main`. + +{{% note "Menus on Multilingual Sites" %}} +If you make use of the [multilingual feature](/content-management/multilingual/), you can define language-independent menus. +{{% /note %}} + +See the [Menu Entry Properties][me-props] for all the variables and functions related to a menu entry. + +## Add content to menus + +Hugo allows you to add content to a menu via the content's [front matter](/content-management/front-matter/). + +### Simple + +If all you need to do is add an entry to a menu, the simple form works well. + +#### A Single Menu + +``` +--- +menu: "main" +--- +``` + +#### Multiple Menus + +``` +--- +menu: ["main", "footer"] +--- +``` + +#### Advanced + + +``` +--- +menu: + docs: + parent: 'extras' + weight: 20 +--- +``` + +## Add Non-content Entries to a Menu + +You can also add entries to menus that aren’t attached to a piece of content. This takes place in your Hugo project's [`config` file][config]. + +Here’s an example snippet pulled from a configuration file: + +{{< code-toggle file="config" >}} +[[menu.main]] + name = "about hugo" + pre = "" + weight = -110 + identifier = "about" + url = "/about/" +[[menu.main]] + name = "getting started" + pre = "" + post = "New!" + weight = -100 + url = "/getting-started/" +{{< /code-toggle >}} + +{{% note %}} +The URLs must be relative to the context root. If the `baseURL` is `https://example.com/mysite/`, then the URLs in the menu must not include the context root `mysite`. Using an absolute URL will override the baseURL. If the value used for `URL` in the above example is `https://subdomain.example.com/`, the output will be `https://subdomain.example.com`. +{{% /note %}} + +## Nesting + +All nesting of content is done via the `parent` field. + +The parent of an entry should be the identifier of another entry. The identifier should be unique (within a menu). + +The following order is used to determine an Identifier: + +`.Name > .LinkTitle > .Title` + +This means that `.Title` will be used unless `.LinkTitle` is present, etc. In practice, `.Name` and `.Identifier` are only used to structure relationships and therefore never displayed. + +In this example, the top level of the menu is defined in your [site `config` file][config]. All content entries are attached to one of these entries via the `.Parent` field. + +## Render Menus + +See [Menu Templates](/templates/menu-templates/) for information on how to render your site menus within your templates. + +[config]: /getting-started/configuration/ +[multilingual]: /content-management/multilingual/ +[sitevars]: /variables/ +[me-props]: /variables/menus/ diff --git a/content/en/content-management/multilingual.md b/content/en/content-management/multilingual.md new file mode 100644 index 000000000..d3f71676a --- /dev/null +++ b/content/en/content-management/multilingual.md @@ -0,0 +1,460 @@ +--- +title: Multilingual Mode +linktitle: Multilingual and i18n +description: Hugo supports the creation of websites with multiple languages side by side. +date: 2017-01-10 +publishdate: 2017-01-10 +lastmod: 2017-01-10 +categories: [content management] +keywords: [multilingual,i18n, internationalization] +menu: + docs: + parent: "content-management" + weight: 150 +weight: 150 #rem +draft: false +aliases: [/content/multilingual/,/tutorials/create-a-multilingual-site/] +toc: true +--- + +You should define the available languages in a `languages` section in your site configuration. + +> Also See [Hugo Multilingual Part 1: Content translation](https://regisphilibert.com/blog/2018/08/hugo-multilingual-part-1-managing-content-translation/) + +## Configure Languages + +The following is an example of a site configuration for a multilingual Hugo project: + +{{< code-toggle file="config" >}} +DefaultContentLanguage = "en" +copyright = "Everything is mine" + +[params] +[params.navigation] +help = "Help" + +[languages] +[languages.en] +title = "My blog" +weight = 1 +[languages.en.params] +linkedin = "https://linkedin.com/whoever" + +[languages.fr] +title = "Mon blogue" +weight = 2 +[languages.fr.params] +linkedin = "https://linkedin.com/fr/whoever" +[languages.fr.params.navigation] +help = "Aide" +{{< /code-toggle >}} + +Anything not defined in a `languages` block will fall back to the global value for that key (e.g., `copyright` for the English `en` language). This also works for `params`, as demonstrated with `help` above: You will get the value `Aide` in French and `Help` in all the languages without this parameter set. + +With the configuration above, all content, sitemap, RSS feeds, paginations, +and taxonomy pages will be rendered below `/` in English (your default content language) and then below `/fr` in French. + +When working with front matter `Params` in [single page templates][singles], omit the `params` in the key for the translation. + +`defaultContentLanguage` sets the project's default language. If not set, the default language will be `en`. + +If the default language needs to be rendererd below its own language code (`/en`) like the others, set `defaultContentLanguageInSubdir: true`. + +Only the obvious non-global options can be overridden per language. Examples of global options are `baseURL`, `buildDrafts`, etc. + +### Disable a Language + +You can disable one or more languages. This can be useful when working on a new translation. + +```toml +disableLanguages = ["fr", "ja"] +``` + +Note that you cannot disable the default content language. + +We kept this as a standalone setting to make it easier to set via [OS environment](/getting-started/configuration/#configure-with-environment-variables): + +```bash +HUGO_DISABLELANGUAGES="fr ja" hugo +``` +If you have already a list of disabled languages in `config.toml`, you can enable them in development like this: + +```bash +HUGO_DISABLELANGUAGES=" " hugo server +``` + + +### Configure Multilingual Multihost + +From **Hugo 0.31** we support multiple languages in a multihost configuration. See [this issue](https://github.com/gohugoio/hugo/issues/4027) for details. + +This means that you can now configure a `baseURL` per `language`: + + +> If a `baseURL` is set on the `language` level, then all languages must have one and they must all be different. + +Example: + +{{< code-toggle file="config" >}} +[languages] +[languages.fr] +baseURL = "https://example.fr" +languageName = "Français" +weight = 1 +title = "En Français" + +[languages.en] +baseURL = "https://example.com" +languageName = "English" +weight = 2 +title = "In English" +{{}} + +With the above, the two sites will be generated into `public` with their own root: + +```bash +public +├── en +└── fr +``` + +**All URLs (i.e `.Permalink` etc.) will be generated from that root. So the English home page above will have its `.Permalink` set to `https://example.com/`.** + +When you run `hugo server` we will start multiple HTTP servers. You will typlically see something like this in the console: + +```bash +Web Server is available at 127.0.0.1:1313 (bind address 127.0.0.1) +Web Server is available at 127.0.0.1:1314 (bind address 127.0.0.1) +Press Ctrl+C to stop +``` + +Live reload and `--navigateToChanged` between the servers work as expected. + +### Taxonomies and Blackfriday + +Taxonomies and [Blackfriday configuration][config] can also be set per language: + + +{{< code-toggle file="config" >}} +[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" +{{}} + +## Translate Your Content + +There are two ways to manage your content translations. Both ensure each page is assigned a language and is linked to its counterpart translations. + +### Translation by filename + +Considering the following example: + +1. `/content/about.en.md` +2. `/content/about.fr.md` + +The first file is assigned the English language and is linked to the second. +The second file is assigned the French language and is linked to the first. + +Their language is __assigned__ according to the language code added as a __suffix to the filename__. + +By having the same **path and base filename**, 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` param. + +{{< code-toggle file="config" >}} + +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` +2. `/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` +2. `/content/om.nn.md` +3. `/content/presentation/a-propos.fr.md` + +```yaml +# set in all three pages +translationKey: "about" +``` + +By setting the `translationKey` front matter param to `about` in all three pages, they will be __linked__ as translated pages. + + +### Localizing permalinks + +Because paths and filenames are used to handle linking, all translated pages will share the same URL (apart from the language subdirectory). + +To localize the URLs, the [`slug`]({{< ref "/content-management/organization/index.md#slug" >}}) or [`url`]({{< ref "/content-management/organization/index.md#url" >}}) front matter param can be set in any of the non-default language file. + +For example, a French translation (`content/about.fr.md`) can have its own localized slug. + +{{< code-toggle >}} +Title: A Propos +slug: "a-propos" +{{< /code-toggle >}} + + +At render, Hugo will build both `/about/` and `/fr/a-propos/` while maintaining their translation linking. + +{{% note %}} +If using `url`, remember to include the language part as well: `/fr/compagnie/a-propos/`. +{{%/ note %}} + +### 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 filename (`image.jpg`, `image.fr.jpg`) and by directory (`english/about/header.jpg`, `french/about/header.jpg`). +{{%/ note %}} + +## Reference the Translated Content + +To create a list of links to translated content, use a template similar to the following: + +{{< code file="layouts/partials/i18nlist.html" >}} +{{ if .IsTranslated }} +

{{ i18n "translations" }}

+ +{{ end }} +{{< /code >}} + +The above can be put in a `partial` (i.e., inside `layouts/partials/`) and included in any template, whether a [single content page][contenttemplate] or the [homepage][]. It will not print anything if there are no translations for a given page. + +The above 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: + + +{{< code file="layouts/partials/allLanguages.html" >}} + +{{< /code >}} + +## Translation of Strings + +Hugo uses [go-i18n][] to support string translations. [See the project's source repository][go-i18n-source] to find tools that will help you manage your translation workflows. + +Translations are collected from the `themes//i18n/` folder (built into the theme), as well as translations present in `i18n/` at the root of your project. In the `i18n`, the translations will be merged and take precedence over what is in the theme folder. Language files should be named according to [RFC 5646][] with names such as `en-US.toml`, `fr.toml`, etc. + +{{% note %}} +From **Hugo 0.31** you no longer need to use a valid language code. It can be anything. + +See https://github.com/gohugoio/hugo/issues/3564 + +{{% /note %}} + +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 to read" +other = "{{.Count}} minutes to read" +``` +And then in the template: + +``` +{{ i18n "readingTime" .ReadingTime }} +``` + +## Customize Dates + +At the time of this writing, Go does not yet have support for internationalized locales for dates, 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: + +~~~yaml +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. + +## Menus + +You can define your menus for each language independently. Creating multilingual menus works just like [creating regular menus][menus], except they're defined in language-specific blocks in the configuration file: + +``` +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 in the current language. Note that `absLangURL` below will link to the correct locale of your website. Without it, menu entries in all languages would link to the English version, since it's the default content language that resides in the root directory. + +``` +
    + {{- $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 website, it can be handy to have a visual indicator of missing translations. The [`enableMissingTranslationPlaceholders` configuration option][config] will flag all untranslated strings with the placeholder `[i18n] identifier`, where `identifier` is the id of the missing translation. + +{{% note %}} +Hugo will generate your website with these missing translation placeholders. It might not be suitable for production environments. +{{% /note %}} + +For merging of content from other languages (i.e. missing content translations), see [lang.Merge](/functions/lang.merge/). + +To track down missing translation strings, run Hugo with the `--i18n-warnings` flag: + +``` + hugo --i18n-warnings | 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` template function][rellangurl] or the [`absLangURL` template function][abslangurl] **OR** be prefixed with `{{ .LanguagePrefix }}` + +If there is more than one language defined, the `LanguagePrefix` variable will equal `/en` (or whatever your `CurrentLanguage` is). If not enabled, it will be an empty string (and is therefore harmless for single-language Hugo websites). + +[abslangurl]: /functions/abslangurl +[config]: /getting-started/configuration/ +[contenttemplate]: /templates/single-page-templates/ +[go-i18n-source]: https://github.com/nicksnyder/go-i18n +[go-i18n]: https://github.com/nicksnyder/go-i18n +[homepage]: /templates/homepage/ +[i18func]: /functions/i18n/ +[menus]: /content-management/menus/ +[rellangurl]: /functions/rellangurl +[RFC 5646]: https://tools.ietf.org/html/rfc5646 +[singles]: /templates/single-page-templates/ diff --git a/content/en/content-management/organization/1-featured-content-bundles.png b/content/en/content-management/organization/1-featured-content-bundles.png new file mode 100644 index 000000000..1706a29d6 Binary files /dev/null and b/content/en/content-management/organization/1-featured-content-bundles.png differ diff --git a/content/en/content-management/organization/index.md b/content/en/content-management/organization/index.md new file mode 100644 index 000000000..9ed2dbff3 --- /dev/null +++ b/content/en/content-management/organization/index.md @@ -0,0 +1,240 @@ +--- +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. +date: 2017-02-01 +publishdate: 2017-02-01 +lastmod: 2017-02-01 +categories: [content management,fundamentals] +keywords: [sections,content,organization,bundle,resources] +menu: + docs: + parent: "content-management" + weight: 10 +weight: 10 #rem +draft: false +aliases: [/content/sections/] +toc: true +--- + +## 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]({{< relref "/content-management/page-resources" >}}) and [Image Processing]({{< relref "/content-management/image-processing" >}}) to get the full picture. + +{{% imgproc 1-featured Resize "300x" %}} +The illustration shows 3 bundles. Note that the home page bundle cannot contain other content pages, but other files (images etc.) are fine. +{{% /imgproc %}} + + +{{% note %}} +The bundle documentation is **work in progress**. We will publish more comprehensive docs about this soon. +{{% /note %}} + + +# 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 just work: + +``` +. +└── content + └── about + | └── index.md // <- https://example.com/about/ + ├── posts + | ├── firstpost.md // <- https://example.com/posts/firstpost/ + | ├── happy + | | └── ness.md // <- https://example.com/posts/happy/ness/ + | └── secondpost.md // <- https://example.com/posts/secondpost/ + └── quote + ├── first.md // <- https://example.com/quote/first/ + └── second.md // <- https://example.com/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.com"` 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 your [list templates][lists]. These templates include those for [section templates][], [taxonomy templates][], [taxonomy terms templates][], and your [homepage template][]. + +{{% note %}} +**Tip:** You can get a reference to the content and metadata in `_index.md` using the [`.Site.GetPage` function](/functions/getpage/). +{{% /note %}} + +You can keep one `_index.md` for your homepage and one in each of your content sections, taxonomies, and taxonomy terms. The following shows typical placement of an `_index.md` that would contain content and front matter for a `posts` section list page on a Hugo website: + + +``` +. url +. ⊢--^-⊣ +. path slug +. ⊢--^-⊣⊢---^---⊣ +. filepath +. ⊢------^------⊣ +content/posts/_index.md +``` + +At build, this will output to the following destination with the associated values: + +``` + + url ("/posts/") + ⊢-^-⊣ + baseurl section ("posts") +⊢--------^---------⊣⊢-^-⊣ + permalink +⊢----------^-------------⊣ +https://example.com/posts/index.html +``` + +The [sections][] can be nested as deeply as you need. The important part to understand is, that to make the section tree fully navigational, at least the lower-most section needs a content file. (i.e. `_index.md`). + + +### Single Pages in Sections + +Single content files in each of your sections are going to be rendered as [single page templates][singles]. Here is an example of a single `post` within `posts`: + + +``` + path ("posts/my-first-hugo-post.md") +. ⊢-----------^------------⊣ +. section slug +. ⊢-^-⊣⊢--------^----------⊣ +content/posts/my-first-hugo-post.md +``` + +At the time Hugo builds your site, the content will be output to the following destination: + +``` + + url ("/posts/my-first-hugo-post/") + ⊢------------^----------⊣ + baseurl section slug +⊢--------^--------⊣⊢-^--⊣⊢-------^---------⊣ + permalink +⊢--------------------^---------------------⊣ +https://example.com/posts/my-first-hugo-post/index.html +``` + + +## Paths Explained + +The following concepts will provide more insight into the relationship between your project's organization and the default behaviors of Hugo when building the output website. + +### `section` + +A default content type is determined by a piece of content's section. `section` is determined by the location within the project's `content` directory. `section` *cannot* be specified or overridden in front matter. + +### `slug` + +A content's `slug` is either `name.extension` or `name/`. The value for `slug` is determined by + +* the name of the content file (e.g., `lollapalooza.md`) OR +* front matter overrides + +### `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 relative URL for the piece of content. The `url` + +* is based on the content's location within the directory structure OR +* is defined in front matter and *overrides all the above* + +## Override Destination Paths via Front Matter + +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. + +There are times where you may need more control over your content. In these cases, there are fields that can be specified in the front matter to determine the destination of a specific piece of content. + +The following items are defined in this order for a specific reason: items explained further down in the list will override earlier items, and not all of these items can be defined in front matter: + +### `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 (e.g., `content/posts/my-post.md` becomes `example.com/posts/my-post/`). + +### `slug` + +When defined in the front matter, the `slug` can take the place of the filename for the destination. + +{{< code file="content/posts/old-post.md" >}} +--- +title: New Post +slug: "new-post" +--- +{{< /code >}} + +This will render to the following destination according to Hugo's default behavior: + +``` +example.com/posts/new-post/ +``` + +### `section` + +`section` is determined by a content's location on disk and *cannot* be specified in the front matter. See [sections][] for more information. + +### `type` + +A content's `type` is also determined by its location on disk but, unlike `section`, it *can* be specified in the front matter. See [types][]. This can come in especially handy when you want a piece of content to render using a different layout. In the following example, you can create a layout at `layouts/new/mylayout.html` that Hugo will use to render this piece of content, even in the midst of many other posts. + +{{< code file="content/posts/my-post.md" >}} +--- +title: My Post +type: new +layout: mylayout +--- +{{< /code >}} + + + + + +### `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 `/`). `url` will be used exactly as it provided in the front matter and will ignore the `--uglyURLs` setting in your site configuration: + +{{< code file="content/posts/old-url.md" >}} +--- +title: Old URL +url: /blog/new-url/ +--- +{{< /code >}} + +Assuming your `baseURL` is [configured][config] to `https://example.com`, the addition of `url` to the front matter will make `old-url.md` render to the following destination: + +``` +https://example.com/blog/new-url/ +``` + +You can see more information on how to control output paths in [URL Management][urls]. + +[config]: /getting-started/configuration/ +[formats]: /content-management/formats/ +[front matter]: /content-management/front-matter/ +[getpage]: /functions/getpage/ +[homepage template]: /templates/homepage/ +[homepage]: /templates/homepage/ +[lists]: /templates/lists/ +[pretty]: /content-management/urls/#pretty-urls +[section templates]: /templates/section-templates/ +[sections]: /content-management/sections/ +[singles]: /templates/single-page-templates/ +[taxonomy templates]: /templates/taxonomy-templates/ +[taxonomy terms templates]: /templates/taxonomy-templates/ +[types]: /content-management/types/ +[urls]: /content-management/urls/ diff --git a/content/en/content-management/page-bundles.md b/content/en/content-management/page-bundles.md new file mode 100644 index 000000000..0d665759c --- /dev/null +++ b/content/en/content-management/page-bundles.md @@ -0,0 +1,185 @@ +--- +title : "Page Bundles" +description : "Content organization using Page Bundles" +date : 2018-01-24T13:09:00-05:00 +lastmod : 2018-01-28T22:26:40-05:00 +linktitle : "Page Bundles" +keywords : ["page", "bundle", "leaf", "branch"] +categories : ["content management"] +toc : true +menu : + docs: + identifier : "page-bundles" + parent : "content-management" + weight : 11 +--- + +Page Bundles are a way to group [Page Resources](/content-management/page-resources/). + +A Page Bundle can be one of: + +- Leaf Bundle (leaf means it has no children) +- Branch Bundle (home page, section, taxonomy terms, taxonomy list) + +| | Leaf Bundle | Branch Bundle | +|-------------------------------------|----------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Usage | Collection of content and attachments for single pages | Collection of attachments for section pages (home page, section, taxonomy terms, taxonomy list) | +| Index file name | `index.md` [^fn:1] | `_index.md` [^fn:1] | +| Allowed Resources | Page and non-page (like images, pdf, etc.) types | Only non-page (like images, pdf, etc.) types | +| Where can the Resources live? | At any directory level within the leaf bundle directory. | Only in the directory level **of** the branch bundle directory i.e. the directory containing the `_index.md` ([ref](https://discourse.gohugo.io/t/question-about-content-folder-structure/11822/4?u=kaushalmodi)). | +| Layout type | `single` | `list` | +| Nesting | Does not allow nesting of more bundles under it | Allows nesting of leaf or branch bundles under it | +| Example | `content/posts/my-post/index.md` | `content/posts/_index.md` | +| Content from non-index page files... | Accessed only as page resources | Accessed only as regular pages | + + +## Leaf Bundles {#leaf-bundles} + +A _Leaf Bundle_ is a directory at any hierarchy within the `content/` +directory, that contains an **`index.md`** file. + +### Examples of Leaf Bundle organization {#examples-of-leaf-bundle-organization} + +```text +content/ +├── about +│ ├── index.md +├── posts +│ ├── my-post +│ │ ├── content1.md +│ │ ├── content2.md +│ │ ├── image1.jpg +│ │ ├── image2.png +│ │ └── index.md +│ └── my-other-post +│    └── index.md +│ +└── another-section + ├── .. +    └── not-a-leaf-bundle + ├── .. +    └── another-leaf-bundle +    └── index.md +``` + +In the above example `content/` directory, there are four leaf +bundles: + +about +: This leaf bundle is at the root level (directly under + `content` directory) and has only the `index.md`. + +my-post +: This leaf bundle has the `index.md`, two other content + Markdown files and two image files. + +my-other-post +: This leaf bundle has only the `index.md`. + +another-leaf-bundle +: This leaf bundle is nested under couple of + directories. This bundle also has only the `index.md`. + +{{% note %}} +The hierarchy depth at which a leaf bundle is created does not matter, +as long as it is not inside another **leaf** bundle. +{{% /note %}} + + +### Headless Bundle {#headless-bundle} + +A headless bundle is a bundle that is configured to not get published +anywhere: + +- It will have no `Permalink` and no rendered HTML in `public/`. +- It will not be part of `.Site.RegularPages`, etc. + +But you can get it by `.Site.GetPage`. Here is an example: + +```go-html-template +{{ $headless := .Site.GetPage "/some-headless-bundle" }} +{{ $reusablePages := $headless.Resources.Match "author*" }} +

Authors

+{{ range $reusablePages }} +

{{ .Title }}

+ {{ .Content }} +{{ end }} +``` + +_In this example, we are assuming the `some-headless-bundle` to be a headless + bundle containing one or more **page** resources whose `.Name` matches + `"author*"`._ + +Explanation of the above example: + +1. Get the `some-headless-bundle` Page "object". +2. Collect a *slice* of resources in this *Page Bundle* that matches + `"author*"` using `.Resources.Match`. +3. Loop through that *slice* of nested pages, and output their `.Title` and + `.Content`. + +--- + +A leaf bundle can be made headless by adding below in the Front Matter +(in the `index.md`): + +```toml +headless = true +``` + +{{% note %}} +Only leaf bundles can be made headless. +{{% /note %}} + +There are many use cases of such headless page bundles: + +- Shared media galleries +- Reusable page content "snippets" + + +## Branch Bundles {#branch-bundles} + +A _Branch Bundle_ is any directory at any hierarchy within the +`content/` directory, that contains at least an **`_index.md`** file. + +This `_index.md` can also be directly under the `content/` directory. + +{{% note %}} +Here `md` (markdown) is used just as an example. You can use any file +type as a content resource as long as it is a content type recognized by Hugo. +{{% /note %}} + + +### Examples of Branch Bundle organization {#examples-of-branch-bundle-organization} + +```text +content/ +├── branch-bundle-1 +│   ├── branch-content1.md +│   ├── branch-content2.md +│   ├── image1.jpg +│   ├── image2.png +│   └── _index.md +└── branch-bundle-2 + ├── _index.md + └── a-leaf-bundle + └── index.md +``` + +In the above example `content/` directory, there are two branch +bundles (and a leaf bundle): + +`branch-bundle-1` +: This branch bundle has the `_index.md`, two + other content Markdown files and two image files. + +`branch-bundle-2` +: This branch bundle has the `_index.md` and a + nested leaf bundle. + +{{% note %}} +The hierarchy depth at which a branch bundle is created does not +matter. +{{% /note %}} + +[^fn:1]: The `.md` extension is just an example. The extension can be `.html`, `.json` or any of any valid MIME type. diff --git a/content/en/content-management/page-resources.md b/content/en/content-management/page-resources.md new file mode 100644 index 000000000..dcd19e42f --- /dev/null +++ b/content/en/content-management/page-resources.md @@ -0,0 +1,165 @@ +--- +title : "Page Resources" +description : "Page Resources -- images, other pages, documents etc. -- have page-relative URLs and their own metadata." +date: 2018-01-24 +categories: ["content management"] +keywords: [bundle,content,resources] +weight: 4003 +draft: false +toc: true +linktitle: "Page Resources" +menu: + docs: + parent: "content-management" + weight: 31 +--- + +## Properties + +ResourceType +: The main type of the resource. For example, a file of MIME type `image/jpg` has the ResourceType `image`. + +Name +: Default value is the filename (relative to the owning page). Can be set in front matter. + +Title +: Default value is the same as `.Name`. Can be set in front matter. + +Permalink +: The absolute URL to the resource. Resources of type `page` will have no value. + +RelPermalink +: The relative URL to the resource. Resources of type `page` will have no value. + +Content +: The content of the resource itself. For most resources, this returns a string with the contents of the file. This can be used to inline some resources, such as `` or ``. + +MediaType +: The MIME type of the resource, such as `image/jpg`. + +MediaType.MainType +: The main type of the resource's MIME type. For example, a file of MIME type `application/pdf` has for MainType `application`. + +MediaType.SubType +: The subtype of the resource's MIME type. For example, a file of MIME type `application/pdf` has for SubType `pdf`. Note that this is not the same as the file extension - PowerPoint files have a subtype of `vnd.mspowerpoint`. + +MediaType.Suffixes +: A slice of possible suffixes for the resource's MIME type. + +## Methods +ByType +: Returns the page resources of the given type. + +```go +{{ .Resources.ByType "image" }} +``` +Match +: Returns all the page resources (as a slice) whose `Name` matches the given Glob pattern ([examples](https://github.com/gobwas/glob/blob/master/readme.md)). The matching is case-insensitive. + +```go +{{ .Resources.Match "images/*" }} +``` + +GetMatch +: Same as `Match` but will return the first match. + +### Pattern Matching +```go +// Using Match/GetMatch to find this images/sunset.jpg ? +.Resources.Match "images/sun*" ✅ +.Resources.Match "**/Sunset.jpg" ✅ +.Resources.Match "images/*.jpg" ✅ +.Resources.Match "**.jpg" ✅ +.Resources.Match "*" 🚫 +.Resources.Match "sunset.jpg" 🚫 +.Resources.Match "*sunset.jpg" 🚫 + +``` + +## Page Resources Metadata + +Page Resources' metadata is managed from their page's front matter with an array/table parameter named `resources`. You can batch assign values using a [wildcards](http://tldp.org/LDP/GNU-Linux-Tools-Summary/html/x11655.htm). + +{{% note %}} +Resources of type `page` get `Title` etc. from their own front matter. +{{% /note %}} + +name +: Sets the value returned in `Name`. + +{{% warning %}} +The methods `Match` and `GetMatch` use `Name` to match the resources. +{{%/ warning %}} + +title +: Sets the value returned in `Title` + +params +: A map of custom key/values. + + +### Resources metadata example + +{{< code-toggle copy="false">}} +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. + +{{% warning %}} +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. For example, in the above example, `.Params.icon` is already 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. +{{%/ warning %}} + +### 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 copy="false">}} +[[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"` | diff --git a/content/en/content-management/related.md b/content/en/content-management/related.md new file mode 100644 index 000000000..640cb04c0 --- /dev/null +++ b/content/en/content-management/related.md @@ -0,0 +1,135 @@ +--- +title: Related Content +description: List related content in "See Also" sections. +date: 2017-09-05 +categories: [content management] +keywords: [content] +menu: + docs: + parent: "content-management" + weight: 40 +weight: 30 +draft: false +aliases: [/content/related/,/related/] +toc: true +--- + + +Hugo uses a set of factors to identify a page's related content based on Front Matter parameters. This can be tuned to the desired set of indices and parameters or left to Hugo's default [Related Content configuration](#configure-related-content). + +## List Related Content + + +To list up to 5 related pages (which share the same _date_ or _keyword_ parameters) is as simple as including something similar to this partial in your single page template: + +{{< code file="layouts/partials/related.html" >}} +{{ $related := .Site.RegularPages.Related . | first 5 }} +{{ with $related }} +

See Also

+ +{{ end }} +{{< /code >}} + +### Methods + +Here is the list of "Related" methods available on a page collection such `.RegularPages`. + +#### .Related PAGE +Returns a collection of pages related the given one. + +``` +{{ $related := .Site.RegularPages.Related . }} +``` + +#### .RelatedIndices PAGE INDICE1 [INDICE2 ...] +Returns a collection of pages related to a given one restricted to a list of indices. + +``` +{{ $related := .Site.RegularPages.RelatedIndices . "tags" "date" }} +``` + +#### .RelatedTo KEYVALS [KEYVALS2 ...] +Returns a collection of pages related together by a set of indices and their match. + +In order to build those set and pass them as argument, one must use the `keyVals` function where the first argument would be the `indice` and the consective ones its potential `matches`. + +``` +{{ $related := .Site.RegularPages.RelatedTo ( keyVals "tags" "hugo" "rocks") ( keyVals "date" .Date ) }} +``` + +{{% note %}} +Read [this blog article](https://regisphilibert.com/blog/2018/04/hugo-optmized-relashionships-with-related-content/) for a great explanation of more advanced usage of this feature. +{{% /note %}} + +## Configure Related Content +Hugo provides a sensible default configuration of Related Content, but you can fine-tune this in your configuration, on the global or language level if needed. + +### Default configuration + +Without any `related` configuration set on the project, Hugo's Related Content methods will use the following. + +```yaml +related: + threshold: 80 + includeNewer: false + toLower: false + indices: + - name: keywords + weight: 100 + - name: date + weight: 10 +``` + +Custom configuration should be set using the same syntax. + +{{% note %}} +If you add a `related` config section, you need to add a complete configuration. It is not possible to just set, say, `includeNewer` and use the rest from the Hugo defaults. +{{% /note %}} + +### Top Level Config Options + +threshold +: A value between 0-100. Lower value will give more, but maybe not so relevant, matches. + +includeNewer +: Set to true to include **pages newer than the current page** in the related content listing. This will mean that the output for older posts may change as new related content gets added. + +toLower +: Set to true to lower case keywords in both the indexes and the queries. This may give more accurate results at a slight performance penalty. Note that this can also be set per index. + +### Config Options per Index + +name +: The index name. This value maps directly to a page param. Hugo supports string values (`author` in the example) and lists (`tags`, `keywords` etc.) and time and date objects. + +weight +: An integer weight that indicates _how important_ this parameter is relative to the other parameters. It can be 0, which has the effect of turning this index off, or even negative. Test with different values to see what fits your content best. + +pattern +: This is currently only relevant for dates. When listing related content, we may want to list content that is also close in time. Setting "2006" (default value for date indexes) as the pattern for a date index will add weight to pages published in the same year. For busier blogs, "200601" (year and month) may be a better default. + +toLower +: See above. + +## Performance Considerations + +**Fast is Hugo's middle name** and we would not have released this feature had it not been blistering fast. + +This feature has been in the back log and requested by many for a long time. The development got this recent kick start from this Twitter thread: + +{{< tweet 898398437527363585 >}} + +Scott S. Lowe removed the "Related Content" section built using the `intersect` template function on tags, and the build time dropped from 30 seconds to less than 2 seconds on his 1700 content page sized blog. + +He should now be able to add an improved version of that "Related Content" section without giving up the fast live-reloads. But it's worth noting that: + +* If you don't use any of the `Related` methods, you will not use the Relate Content feature, and performance will be the same as before. +* Calling `.RegularPages.Related` etc. will create one inverted index, also sometimes named posting list, that will be reused for any lookups in that same page collection. Doing that in addition to, as an example, calling `.Pages.Related` will work as expected, but will create one additional inverted index. This should still be very fast, but worth having in mind, especially for bigger sites. + +{{% note %}} +We currently do not index **Page content**. We thought we would release something that will make most people happy before we start solving [Sherlock's last case](https://github.com/joearms/sherlock). +{{% /note %}} diff --git a/content/en/content-management/sections.md b/content/en/content-management/sections.md new file mode 100644 index 000000000..79ae201d4 --- /dev/null +++ b/content/en/content-management/sections.md @@ -0,0 +1,98 @@ +--- +title: Content Sections +linktitle: Sections +description: "Hugo generates a **section tree** that matches your content." +date: 2017-02-01 +publishdate: 2017-02-01 +lastmod: 2017-02-01 +categories: [content management] +keywords: [lists,sections,content types,organization] +menu: + docs: + parent: "content-management" + weight: 50 +weight: 50 #rem +draft: false +aliases: [/content/sections/] +toc: true +--- + +A **Section** is a collection of pages that gets defined based on the +organization structure under the `content/` directory. + +By default, all the **first-level** directories under `content/` form their own +sections (**root sections**). + +If a user needs to define a section `foo` at a deeper level, they need to create +a directory named `foo` with an `_index.md` file (see [Branch Bundles][branch bundles] +for more information). + + +{{% note %}} +A **section** cannot be defined or overridden by a front matter parameter -- it +is strictly derived from the content organization structure. +{{% /note %}} + +## Nested Sections + +The sections can be nested as deeply as you need. + +```bash +content +└── blog <-- Section, because first-level dir under content/ + ├── funny-cats + │   ├── mypost.md + │   └── kittens <-- Section, because contains _index.md + │   └── _index.md + └── tech <-- Section, because contains _index.md + └── _index.md +``` + +**The important part to understand is, that to make the section tree fully navigational, at least the lower-most section needs a content file. (e.g. `_index.md`).** + +{{% note %}} +When we talk about a **section** in correlation with template selection, it is +currently always the *root section* only (`/blog/funny-cats/mypost/ => blog`). + +If you need a specific template for a sub-section, you need to adjust either the `type` or `layout` in front matter. +{{% /note %}} + +## Example: Breadcrumb Navigation + +With the available [section variables and methods](#section-page-variables-and-methods) you can build powerful navigation. One common example would be a partial to show Breadcrumb navigation: + +{{< code file="layouts/partials/breadcrumb.html" download="breadcrumb.html" >}} + +{{ define "breadcrumbnav" }} +{{ if .p1.Parent }} +{{ template "breadcrumbnav" (dict "p1" .p1.Parent "p2" .p2 ) }} +{{ else if not .p1.IsHome }} +{{ template "breadcrumbnav" (dict "p1" .p1.Site.Home "p2" .p2 ) }} +{{ end }} + + {{ .p1.Title }} + +{{ end }} +{{< /code >}} + +## Section Page Variables and Methods + +Also see [Page Variables](/variables/page/). + +{{< readfile file="/content/en/readfiles/sectionvars.md" markdown="true" >}} + +## Content Section Lists + +Hugo will automatically create pages for each *root section* that list all of the content in that section. See the documentation on [section templates][] for details on customizing the way these pages are rendered. + +## Content *Section* vs Content *Type* + +By default, everything created within a section will use the [content `type`][content type] that matches the *root section* name. For example, Hugo will assume that `posts/post-1.md` has a `posts` content `type`. If you are using an [archetype][] for your `posts` section, Hugo will generate front matter according to what it finds in `archetypes/posts.md`. + +[archetype]: /content-management/archetypes/ +[content type]: /content-management/types/ +[directory structure]: /getting-started/directory-structure/ +[section templates]: /templates/section-templates/ +[branch bundles]: /content-management/page-bundles/#branch-bundles diff --git a/content/en/content-management/shortcodes.md b/content/en/content-management/shortcodes.md new file mode 100644 index 000000000..94966343a --- /dev/null +++ b/content/en/content-management/shortcodes.md @@ -0,0 +1,422 @@ +--- +title: Shortcodes +linktitle: +description: Shortcodes are simple snippets inside your content files calling built-in or custom templates. +godocref: +date: 2017-02-01 +publishdate: 2017-02-01 +lastmod: 2017-03-31 +menu: + docs: + parent: "content-management" + weight: 35 +weight: 35 #rem +categories: [content management] +keywords: [markdown,content,shortcodes] +draft: false +aliases: [/extras/shortcodes/] +testparam: "Hugo Rocks!" +toc: true +--- + +## What a Shortcode is + +Hugo loves Markdown because of its simple content format, but there are times when Markdown falls short. Often, content authors are forced to add raw HTML (e.g., video ``) to Markdown content. We think this contradicts the beautiful simplicity of Markdown's syntax. + +Hugo created **shortcodes** to circumvent these limitations. + +A shortcode is a simple snippet inside a content file that Hugo will render using a predefined template. Note that shortcodes will not work in template files. If you need the type of drop-in functionality that shortcodes provide but in a template, you most likely want a [partial template][partials] instead. + +In addition to cleaner Markdown, shortcodes can be updated any time to reflect new classes, techniques, or standards. At the point of site generation, Hugo shortcodes will easily merge in your changes. You avoid a possibly complicated search and replace operation. + +## Use Shortcodes + +{{< youtube 2xkNJL4gJ9E >}} + +In your content files, a shortcode can be called by calling `{{%/* shortcodename parameters */%}}`. Shortcode parameters are space delimited, and parameters with internal spaces can be quoted. + +The first word in the shortcode declaration is always the name of the shortcode. Parameters follow the name. Depending upon how the shortcode is defined, the parameters may be named, positional, or both, although you can't mix parameter types in a single call. The format for named parameters models that of HTML with the format `name="value"`. + +Some shortcodes use or require closing shortcodes. Again like HTML, the opening and closing shortcodes match (name only) with the closing declaration, which is prepended with a slash. + +Here are two examples of paired shortcodes: + +``` +{{%/* mdshortcode */%}}Stuff to `process` in the *center*.{{%/* /mdshortcode */%}} +``` + +``` +{{}} A bunch of code here {{}} +``` + +The examples above use two different delimiters, the difference being the `%` character in the first and the `<>` characters in the second. + +### Shortcodes with Markdown + +In Hugo `0.55` we changed how the `%` delimiter works. Shortcodes using the `%` as the outer-most delimiter will now be fully rendered when sent to the content renderer (e.g. Blackfriday for Markdown), meaning they can be part of the generated table of contents, footnotes, etc. + +If you want the old behavior, you can put the following line in the start of your shortcode template: + +``` +{{ $_hugo_config := `{ "version": 1 }` }} +``` + + +### Shortcodes Without Markdown + +The `<` character indicates that the shortcode's inner content does *not* need further rendering. Often shortcodes without markdown include internal HTML: + +``` +{{}}

Hello World!

{{}} +``` + +### Nested Shortcodes + +You can call shortcodes within other shortcodes by creating your own templates that leverage the `.Parent` variable. `.Parent` allows you to check the context in which the shortcode is being called. See [Shortcode templates][sctemps]. + +## Use Hugo's Built-in Shortcodes + +Hugo ships with a set of predefined shortcodes that represent very common usage. These shortcodes are provided for author convenience and to keep your markdown content clean. + +### `figure` + +`figure` is an extension of the image syntax in markdown, which does not provide a shorthand for the more semantic [HTML5 `
` element][figureelement]. + +The `figure` shortcode can use the following named parameters: + +src +: URL of the image to be displayed. + +link +: If the image needs to be hyperlinked, URL of the destination. + +target +: Optional `target` attribute for the URL if `link` parameter is set. + +rel +: Optional `rel` attribute for the URL if `link` parameter is set. + +alt +: Alternate text for the image if the image cannot be displayed. + +title +: Image title. + +caption +: Image caption. + +class +: `class` attribute of the HTML `figure` tag. + +height +: `height` attribute of the image. + +width +: `width` attribute of the image. + +attr +: Image attribution text. + +attrlink +: If the attribution text needs to be hyperlinked, URL of the destination. + +#### Example `figure` Input + +{{< code file="figure-input-example.md" >}} +{{}} +{{< /code >}} + +#### Example `figure` Output + +{{< output file="figure-output-example.html" >}} +
+ +
+

Steve Francia

+
+
+{{< /output >}} + +### `gist` + +Bloggers often want to include GitHub gists when writing posts. Let's suppose we want to use the [gist at the following url][examplegist]: + +``` +https://gist.github.com/spf13/7896402 +``` + +We can embed the gist in our content via username and gist ID pulled from the URL: + +``` +{{}} +``` + +#### Example `gist` Input + +If the gist contains several files and you want to quote just one of them, you can pass the filename (quoted) as an optional third argument: + +{{< code file="gist-input.md" >}} +{{}} +{{< /code >}} + +#### Example `gist` Output + +{{< output file="gist-output.html" >}} +{{< gist spf13 7896402 >}} +{{< /output >}} + +#### Example `gist` Display + +To demonstrate the remarkably efficiency of Hugo's shortcode feature, we have embedded the `spf13` `gist` example in this page. The following simulates the experience for visitors to your website. Naturally, the final display will be contingent on your stylesheets and surrounding markup. + +{{< gist spf13 7896402 >}} + +### `highlight` + +This shortcode will convert the source code provided into syntax-highlighted HTML. Read more on [highlighting](/tools/syntax-highlighting/). `highlight` takes exactly one required `language` parameter and requires a closing shortcode. + +#### Example `highlight` Input + +{{< code file="content/tutorials/learn-html.md" >}} +{{}} +
+
+

{{ .Title }}

+ {{ range .Pages }} + {{ .Render "summary"}} + {{ end }} +
+
+{{}} +{{< /code >}} + +#### Example `highlight` Output + +The `highlight` shortcode example above would produce the following HTML when the site is rendered: + +{{< output file="tutorials/learn-html/index.html" >}} +<section id="main"> + <div> + <h1 id="title">{{ .Title }}</h1> + {{ range .Pages }} + {{ .Render "summary"}} + {{ end }} + </div> +</section> +{{< /output >}} + +{{% note "More on Syntax Highlighting" %}} +To see even more options for adding syntax-highlighted code blocks to your website, see [Syntax Highlighting in Developer Tools](/tools/syntax-highlighting/). +{{% /note %}} + +### `instagram` + +If you'd like to embed a photo from [Instagram][], you only need the photo's ID. You can discern an Instagram photo ID from the URL: + +``` +https://www.instagram.com/p/BWNjjyYFxVx/ +``` + +#### Example `instagram` Input + +{{< code file="instagram-input.md" >}} +{{}} +{{< /code >}} + +You also have the option to hide the caption: + +{{< code file="instagram-input-hide-caption.md" >}} +{{}} +{{< /code >}} + +#### Example `instagram` Output + +By adding the preceding `hidecaption` example, the following HTML will be added to your rendered website's markup: + +{{< output file="instagram-hide-caption-output.html" >}} +{{< instagram BWNjjyYFxVx hidecaption >}} +{{< /output >}} + +#### Example `instagram` Display + +Using the preceding `instagram` with `hidecaption` example above, the following simulates the displayed experience for visitors to your website. Naturally, the final display will be contingent on your stylesheets and surrounding markup. + +{{< instagram BWNjjyYFxVx hidecaption >}} + + +### `param` + +Gets a value from the current `Page's` params set in front matter, with a fall back to the site param value. It will log an `ERROR` if the param with the given key could not be found in either. + +```bash +{{}} +``` + +Since `testparam` is a param defined in front matter of this page with the value `Hugo Rocks!`, the above will print: + +{{< param testparam >}} + +To access deeply nested params, use "dot syntax", e.g: + +```bash +{{}} +``` + +### `ref` and `relref` + +These shortcodes will look up the pages by their relative path (e.g., `blog/post.md`) or their logical name (`post.md`) and return the permalink (`ref`) or relative permalink (`relref`) for the found page. + +`ref` and `relref` also make it possible to make fragmentary links that work for the header links generated by Hugo. + +{{% note "More on Cross References" %}} +Read a more extensive description of `ref` and `relref` in the [cross references](/content-management/cross-references/) documentation. +{{% /note %}} + +`ref` and `relref` take exactly one required parameter of _reference_, quoted and in position `0`. + +#### Example `ref` and `relref` Input + +``` +[Neat]({{}}) +[Who]({{}}) +``` + +#### Example `ref` and `relref` Output + +Assuming that standard Hugo pretty URLs are turned on. + +``` +Neat +Who +``` + +### `tweet` + +You want to include a single tweet into your blog post? Everything you need is the URL of the tweet: + +``` +https://twitter.com/spf13/status/877500564405444608 +``` + +#### Example `tweet` Input + +Pass the tweet's ID from the URL as a parameter to the `tweet` shortcode: + +{{< code file="example-tweet-input.md" >}} +{{}} +{{< /code >}} + +#### Example `tweet` Output + +Using the preceding `tweet` example, the following HTML will be added to your rendered website's markup: + +{{< output file="example-tweet-output.html" >}} +{{< tweet 877500564405444608 >}} +{{< /output >}} + +#### Example `tweet` Display + +Using the preceding `tweet` example, the following simulates the displayed experience for visitors to your website. Naturally, the final display will be contingent on your stylesheets and surrounding markup. + +{{< tweet 877500564405444608 >}} + +### `vimeo` + +Adding a video from [Vimeo][] is equivalent to the YouTube shortcode above. + +``` +https://vimeo.com/channels/staffpicks/146022717 +``` + +#### Example `vimeo` Input + +Extract the ID from the video's URL and pass it to the `vimeo` shortcode: + +{{< code file="example-vimeo-input.md" >}} +{{}} +{{< /code >}} + +#### Example `vimeo` Output + +Using the preceding `vimeo` example, the following HTML will be added to your rendered website's markup: + +{{< output file="example-vimeo-output.html" >}} +{{< vimeo 146022717 >}} +{{< /output >}} + +{{% tip %}} +If you want to further customize the visual styling of the YouTube or Vimeo output, add a `class` named parameter when calling the shortcode. The new `class` will be added to the `
` that wraps the ` +
+{{< /code >}} + +{{< code file="youtube-embed.html" copy="false" >}} +
+ +
+{{< /code >}} + +### Single Named Example: `image` + +Let's say you want to create your own `img` shortcode rather than use Hugo's built-in [`figure` shortcode][figure]. Your goal is to be able to call the shortcode as follows in your content files: + +{{< code file="content-image.md" >}} +{{}} +{{< /code >}} + +You have created the shortcode at `/layouts/shortcodes/img.html`, which loads the following shortcode template: + +{{< code file="/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 }} +
+ +{{< /code >}} + +Would be rendered as: + +{{< code file="img-output.html" copy="false" >}} +
+ +
+

Steve Francia

+
+
+{{< /code >}} + +### Single Flexible Example: `vimeo` + +``` +{{}} +{{}} +``` + +Would load the template found at `/layouts/shortcodes/vimeo.html`: + +{{< code file="/layouts/shortcodes/vimeo.html" >}} +{{ if .IsNamedParams }} +
+ +
+{{ else }} +
+ +
+{{ end }} +{{< /code >}} + +Would be rendered as: + +{{< code file="vimeo-iframes.html" copy="false" >}} +
+ +
+
+ +
+{{< /code >}} + +### Paired Example: `highlight` + +The following is taken from `highlight`, which is a [built-in shortcode][] that ships with Hugo. + +{{< code file="highlight-example.md" >}} +{{}} + + This HTML + +{{}} +{{< /code >}} + +The template for the `highlight` shortcode uses the following code, which is already included in Hugo: + +``` +{{ .Get 0 | highlight .Inner }} +``` + +The rendered output of the HTML example code block will be as follows: + +{{< code file="syntax-highlighted.html" copy="false" >}} +
<html>
+    <body> This HTML </body>
+</html>
+
+{{< /code >}} + +{{% note %}} +The preceding shortcode makes use of a Hugo-specific template function called `highlight`, which uses [Pygments](http://pygments.org) to add syntax highlighting to the example HTML code block. See the [developer tools page on syntax highlighting](/tools/syntax-highlighting/) for more information. +{{% /note %}} + +### Nested Shortcode: Image Gallery + +Hugo's [`.Parent` shortcode variable][parent] returns a boolean value depending on whether the shortcode in question is called within the context of a *parent* shortcode. This provides an inheritance model for common shortcode parameters. + +The following example is contrived but demonstrates the concept. Assume you have a `gallery` shortcode that expects one named `class` parameter: + +{{< code file="layouts/shortcodes/gallery.html" >}} +
+ {{.Inner}} +
+{{< /code >}} + +You also have an `img` shortcode with a single named `src` parameter that you want to call inside of `gallery` and other shortcodes, so that the parent defines the context of each `img`: + +{{< code file="layouts/shortcodes/img.html" >}} +{{- $src := .Get "src" -}} +{{- with .Parent -}} + +{{- else -}} + +{{- end }} +{{< /code >}} + +You can then call your shortcode in your content as follows: + +``` +{{}} + {{}} + {{}} +{{}} +{{}} +``` + +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`: + +``` + + +``` + + +## Error Handling in Shortcodes + +Use the [errorf](/functions/errorf) template func and [.Position](/variables/shortcodes/) variable to get useful error messages in shortcodes: + +```bash +{{ with .Get "name" }} +{{ else }} +{{ errorf "missing value for param 'name': %s" .Position }} +{{ end }} +``` + +When the above fails, you will see an `ERROR` log similar to the below: + +```bash +ERROR 2018/11/07 10:05:55 missing value for param name: "/Users/bep/dev/go/gohugoio/hugo/docs/content/en/variables/shortcodes.md:32:1" +``` + +## More Shortcode Examples + +More shortcode examples can be found in the [shortcodes directory for spf13.com][spfscs] and the [shortcodes directory for the Hugo docs][docsshortcodes]. + + +## Inline Shortcodes + +Since Hugo 0.52, you can implement your shortcodes inline -- e.g. where you use them in the content file. This can be useful for scripting that you only need in one place. + +This feature is disabled by default, but can be enabled in your site config: + +{{< code-toggle file="config">}} +enableInlineShortcodes = true +{{< /code-toggle >}} + +It is disabled by default for security reasons. The security model used by Hugo's template handling assumes that template authors are trusted, but that the content files are not, so the templates are injection-safe from malformed input data. But in most situations you have full control over the content, too, and then `enableInlineShortcodes = true` would be considered safe. But it's something to be aware of: It allows ad-hoc [Go Text templates](https://golang.org/pkg/text/template/) to be executed from the content files. + +And once enabled, you can do this in your content files: + + ```go-text-template + {{}}{{ now }}{{}} + ``` + +The above will print the current date and time. + + Note that an inline shortcode's inner content is parsed and executed as a Go text template with the same context as a regular shortcode template. + +This means that the current page can be accessed via `.Page.Title` etc. This also means that there are no concept of "nested inline shortcodes". + +The same inline shortcode can be reused later in the same content file, with different params if needed, using the self-closing syntax: + + ```go-text-template +{{}} +``` + + +[basic content files]: /content-management/formats/ "See how Hugo leverages markdown--and other supported formats--to create content for your website." +[built-in shortcode]: /content-management/shortcodes/ +[config]: /getting-started/configuration/ "Learn more about Hugo's built-in configuration variables as well as how to us your site's configuration file to include global key-values that can be used throughout your rendered website." +[Content Management: Shortcodes]: /content-management/shortcodes/#using-hugo-s-built-in-shortcodes "Check this section if you are not familiar with the definition of what a shortcode is or if you are unfamiliar with how to use Hugo's built-in shortcodes in your content files." +[source organization]: /getting-started/directory-structure/#directory-structure-explained "Learn how Hugo scaffolds new sites and what it expects to find in each of your directories." +[docsshortcodes]: https://github.com/gohugoio/hugo/tree/master/docs/layouts/shortcodes "See the shortcode source directory for the documentation site you're currently reading." +[figure]: /content-management/shortcodes/#figure +[hugosc]: /content-management/shortcodes/#using-hugo-s-built-in-shortcodes +[lookup order]: /templates/lookup-order/ "See the order in which Hugo traverses your template files to decide where and how to render your content at build time" +[pagevars]: /variables/page/ "See which variables you can leverage in your templating for page vs list templates." +[parent]: /variables/shortcodes/ +[shortcodesvars]: /variables/shortcodes/ "Certain variables are specific to shortcodes, although most .Page variables can be accessed within your shortcode template." +[spfscs]: https://github.com/spf13/spf13.com/tree/master/layouts/shortcodes "See more examples of shortcodes by visiting the shortcode directory of the source for spf13.com, the blog of Hugo's creator, Steve Francia." +[templates]: /templates/ "The templates section of the Hugo docs." +[vimeoexample]: #single-flexible-example-vimeo +[youtubeshortcode]: /content-management/shortcodes/#youtube "See how to use Hugo's built-in YouTube shortcode." diff --git a/content/en/templates/single-page-templates.md b/content/en/templates/single-page-templates.md new file mode 100644 index 000000000..55e267d38 --- /dev/null +++ b/content/en/templates/single-page-templates.md @@ -0,0 +1,92 @@ +--- +title: Single Page Templates +linktitle: +description: The primary view of content in Hugo is the single view. Hugo will render every Markdown file provided with a corresponding single template. +date: 2017-02-01 +publishdate: 2017-02-01 +lastmod: 2017-04-06 +categories: [templates] +keywords: [page,templates] +menu: + docs: + parent: "templates" + weight: 60 +weight: 60 +sections_weight: 60 +draft: false +aliases: [/layout/content/] +toc: true +--- + +## Single Page Template Lookup Order + +See [Template Lookup](/templates/lookup-order/). + +## Example Single Page Templates + +Content pages are of the type `page` and will therefore have all the [page variables][pagevars] and [site variables][] available to use in their templates. + +### `posts/single.html` + +This single page template makes use of Hugo [base templates][], the [`.Format` function][] for dates, the [`.WordCount` page variable][pagevars], and ranges through the single content's specific [taxonomies][pagetaxonomy]. [`with`][] is also used to check whether the taxonomies are set in the front matter. + +{{< code file="layouts/posts/single.html" download="single.html" >}} +{{ define "main" }} +
+

{{ .Title }}

+
+
+ {{ .Content }} +
+
+
+ +{{ end }} +{{< /code >}} + +To easily generate new instances of a content type (e.g., new `.md` files in a section like `project/`) with preconfigured front matter, use [content archetypes][archetypes]. + +[archetypes]: /content-management/archetypes/ +[base templates]: /templates/base/ +[config]: /getting-started/configuration/ +[content type]: /content-management/types/ +[directory structure]: /getting-started/directory-structure/ +[dry]: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself +[`.Format` function]: /functions/format/ +[front matter]: /content-management/front-matter/ +[pagetaxonomy]: /templates/taxonomy-templates/#displaying-a-single-piece-of-content-s-taxonomies +[pagevars]: /variables/page/ +[partials]: /templates/partials/ +[section]: /content-management/sections/ +[site variables]: /variables/site/ +[spf13]: https://spf13.com/ +[`with`]: /functions/with/ diff --git a/content/en/templates/sitemap-template.md b/content/en/templates/sitemap-template.md new file mode 100644 index 000000000..9afc0d389 --- /dev/null +++ b/content/en/templates/sitemap-template.md @@ -0,0 +1,106 @@ +--- +title: Sitemap Template +# linktitle: Sitemap +description: Hugo ships with a built-in template file observing the v0.9 of the Sitemap Protocol, but you can override this template if needed. +date: 2017-02-01 +publishdate: 2017-02-01 +lastmod: 2017-02-01 +categories: [templates] +keywords: [sitemap, xml, templates] +menu: + docs: + parent: "templates" + weight: 160 +weight: 160 +sections_weight: 160 +draft: false +aliases: [/layout/sitemap/,/templates/sitemap/] +toc: false +--- + +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 therefore has all the [page variables][pagevars] 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 `sitemap.xml` template that ships with Hugo. + +## Sitemap Templates + +Hugo has built-on Sitemap templates, but you can provide your own if needed, in either `layouts/sitemap.xml` or `layouts/_default/sitemap.xml`. + +For multilingual sites, we also create a Sitemap index. You can provide a custom layout for that in either `layouts/sitemapindex.xml` or `layouts/_default/sitemapindex.xml`. + +## Hugo’s sitemap.xml + +This template respects the version 0.9 of the [Sitemap Protocol](http://www.sitemaps.org/protocol.html). + +```xml +{{ printf "" | safeHTML }} + + {{ 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 }} + +``` + +## Hugo's sitemapindex.xml + +This is used to create a Sitemap index in multilingual mode: + +```xml +{{ printf "" | safeHTML }} + + {{ range . }} + + {{ .SitemapAbsURL }} + {{ if not .LastChange.IsZero }} + {{ .LastChange.Format "2006-01-02T15:04:05-07:00" | safeHTML }} + {{ end }} + + {{ end }} + +``` + +## Configure `sitemap.xml` + +Defaults for ``, `` and `filename` values can be set in the site's config file, e.g.: + +{{< code-toggle file="config" >}} +[sitemap] + changefreq = "monthly" + priority = 0.5 + filename = "sitemap.xml" +{{}} + +The same fields can be specified in an individual content file's front matter in order to override the value assigned to that piece of content at render time. + + + +[pagevars]: /variables/page/ diff --git a/content/en/templates/taxonomy-templates.md b/content/en/templates/taxonomy-templates.md new file mode 100644 index 000000000..b82a5175c --- /dev/null +++ b/content/en/templates/taxonomy-templates.md @@ -0,0 +1,375 @@ +--- +title: Taxonomy Templates +# linktitle: +description: Taxonomy templating includes taxonomy list pages, taxonomy terms pages, and using taxonomies in your single page templates. +date: 2017-02-01 +publishdate: 2017-02-01 +lastmod: 2017-02-01 +categories: [templates] +keywords: [taxonomies,metadata,front matter,terms,templates] +menu: + docs: + parent: "templates" + weight: 50 +weight: 50 +sections_weight: 50 +draft: false +aliases: [/taxonomies/displaying/,/templates/terms/,/indexes/displaying/,/taxonomies/templates/,/indexes/ordering/, /templates/taxonomies/, /templates/taxonomy/] +toc: true +--- + + + +Hugo includes support for user-defined groupings of content called **taxonomies**. Taxonomies are classifications that demonstrate logical relationships between content. See [Taxonomies under Content Management](/content-management/taxonomies) if you are unfamiliar with how Hugo leverages this powerful feature. + +Hugo provides multiple ways to use taxonomies throughout your project templates: + +* Order the way content associated with a taxonomy term is displayed in a [taxonomy list template](#taxonomy-list-template) +* Order the way the terms for a taxonomy are displayed in a [taxonomy terms template](#taxonomy-terms-template) +* List a single content's taxonomy terms within a [single page template][] + +## Taxonomy List Templates + +Taxonomy list page templates are lists and therefore have all the variables and methods available to [list pages][lists]. + +### Taxonomy List Template Lookup Order + +See [Template Lookup](/templates/lookup-order/). + +## Taxonomy Terms Template + +### Taxonomy Terms Templates Lookup Order + +See [Template Lookup](/templates/lookup-order/). + +### 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. + +.Reverse +: Returns an OrderedTaxonomy (slice) in reverse order. Must be used with an OrderedTaxonomy. + +### 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][renderlists] 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][renderlists]. + +## Displaying custom metadata in Taxonomy Terms Templates + +If you need to display custom metadata for each taxonomy term, you will need to create a page for that term at `/content///_index.md` and add your metadata in its front matter, [as explained in the taxonomies documentation](/content-management/taxonomies/#add-custom-meta-data-to-a-taxonomy-term). Based on the Actors taxonomy example shown there, within your taxonomy terms template, you may access your custom fields by iterating through the variable `.Pages` as such: + +```go-html-template +
    + {{ range .Pages }} +
  • + {{ .Title }} + {{ .Params.wikipedia }} +
  • + {{ end }} +
+``` + + + +## Order Taxonomies + +Taxonomies can be ordered by either alphabetical key or by the number of content pieces assigned to that key. + +### Order Alphabetically Example + +In Hugo 0.55 and later you can do: + +```go-html-template +
    + {{ range .Data.Terms.Alphabetical }} +
  • {{ .Page.Title }} {{ .Count }}
  • + {{ end }} +
+``` + +Before that you would have to do something like: + +```go-html-template +
    + {{ $type := .Type }} + {{ range $key, $value := .Data.Terms.Alphabetical }} + {{ $name := .Name }} + {{ $count := .Count }} + {{ with $.Site.GetPage (printf "/%s/%s" $type $name) }} +
  • {{ $name }} {{ $count }}
  • + {{ end }} + {{ end }} +
+``` + + + + +## Order 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 the same as that used for [section and list pages]() 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. + +### Assign Weight + +Content can be assigned weight for each taxonomy that it's assigned to. + +``` ++++ +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. + + + +There are two different templates that the use of taxonomies will require you to provide. + +Both templates are covered in detail in the templates section. + +A [list template](/templates/list/) is any template that will be used to render multiple pieces of content in a single html page. This template will be used to generate all the automatically created taxonomy pages. + +A [taxonomy terms template](/templates/terms/) is a template used to +generate the list of terms for a given template. + + + +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) + +## Display a Single Piece of Content's Taxonomies + +Within your content templates, you may wish to display the taxonomies 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 (i.e., `.Params.`). + +### Example: List Tags in a Single Page Template + +```go-html-template +{{ $taxo := "tags" }} +
    + {{ range .Param $taxo }} + {{ $name := . }} + {{ with $.Site.GetPage (printf "/%s/%s" $taxo ($name | urlize)) }} +
  • {{ $name }}
  • + {{ end }} + {{ end }} +
+``` + +If you want to list taxonomies inline, you will have to take care of optional plural endings 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 taxonomies, use the following: + +### Example: Comma-delimit Tags in a Single Page Template + +```go-html-template +{{ $taxo := "directors" }} +{{ with .Param $taxo }} + Director{{ if gt (len .) 1 }}s{{ end }}: + {{ range $index, $director := . }} + {{- if gt $index 0 }}, {{ end -}} + {{ with $.Site.GetPage (printf "/%s/%s" $taxo $director) -}} + {{ $director }} + {{- end -}} + {{- end -}} +{{ end }} +``` + +Alternatively, you may use the [delimit template function][delimit] as a shortcut if the taxonomies should just be listed with a separator. See {{< gh 2143 >}} on GitHub for discussion. + +## 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 taxonomy. This is also a quick and dirty method for showing related content: + +### Example: Showing Content in Same Series + +```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. + +### Example: Grouping "Featured" Content + +```go-html-template + +``` + +## Render a Site's Taxonomies + +If you wish to display the list of all keys for your site's taxonomy, you can retrieve them from the [`.Site` variable][sitevars] available on every page. + +This may take the form of a tag cloud, a menu, or simply a list. + +The following example displays all terms in a site's tags taxonomy: + +### Example: List All Site Tags {#example-list-all-site-tags} + +In Hugo 0.55 and later you can simply do: + +```go-html-template + +``` + +Before that you would do something like this: + +{{< todo >}}Clean up rest of the taxonomy examples re Hugo 0.55.{{< /todo >}} + +```go-html-template +
    + {{ range $name, $taxonomy := .Site.Taxonomies.tags }} + {{ with $.Site.GetPage (printf "/tags/%s" $name) }} +
  • {{ $name }}
  • + {{ end }} + {{ end }} +
+``` + +### Example: List All Taxonomies, Terms, and Assigned Content + +This example will list all taxonomies and their terms, as well as all the content assigned to each of the terms. + +{{< code file="layouts/partials/all-taxonomies.html" download="all-taxonomies.html" download="all-taxonomies.html" >}} +
+
    + {{ range $taxonomy_term, $taxonomy := .Site.Taxonomies }} + {{ with $.Site.GetPage (printf "/%s" $taxonomy_term) }} +
  • {{ $taxonomy_term }} +
      + {{ range $key, $value := $taxonomy }} +
    • {{ $key }}
    • + + {{ end }} +
    +
  • + {{ end }} + {{ end }} +
+
+{{< /code >}} + +## `.Site.GetPage` for Taxonomies + +Because taxonomies are lists, the [`.GetPage` function][getpage] can be used to get all the pages associated with a particular taxonomy term using a terse syntax. The following ranges over the full list of tags on your site and links to each of the individual taxonomy pages for each term without having to use the more fragile URL construction of the ["List All Site Tags" example above]({{< relref "#example-list-all-site-tags" >}}): + +{{< code file="links-to-all-tags.html" >}} +{{ $taxo := "tags" }} +
    + {{ with ($.Site.GetPage (printf "/%s" $taxo)) }} + {{ range .Pages }} +
  • {{ .Title}}
  • + {{ end }} + {{ end }} +
+{{< /code >}} + + + + + + +[delimit]: /functions/delimit/ +[getpage]: /functions/getpage/ +[lists]: /templates/lists/ +[renderlists]: /templates/lists/ +[single page template]: /templates/single-page-templates/ +[sitevars]: /variables/site/ diff --git a/content/en/templates/template-debugging.md b/content/en/templates/template-debugging.md new file mode 100644 index 000000000..419581b90 --- /dev/null +++ b/content/en/templates/template-debugging.md @@ -0,0 +1,77 @@ +--- +title: Template Debugging +# linktitle: Template Debugging +description: You can use Go templates' `printf` function to debug your Hugo templates. These snippets provide a quick and easy visualization of the variables available to you in different contexts. +godocref: https://golang.org/pkg/fmt/ +date: 2017-02-01 +publishdate: 2017-02-01 +lastmod: 2017-02-01 +categories: [templates] +keywords: [debugging,troubleshooting] +menu: + docs: + parent: "templates" + weight: 180 +weight: 180 +sections_weight: 180 +draft: false +aliases: [] +toc: false +--- + +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](https://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 +(`.`, aka ["the dot"][tempintro]). + + +``` +{{ printf "%#v" . }} +``` + + +When developing a [homepage][], what does one of the pages you're looping through look like? + +``` +{{ range .Pages }} + {{/* The context, ".", is now each one of the pages as it goes through the loop */}} + {{ printf "%#v" . }} +{{ end }} +``` + +## Why Am I Showing No Defined Variables? + +Check that you are passing variables in the `partial` function: + +``` +{{ partial "header" }} +``` + +This example will render the header partial, but the header partial will not have access to any contextual variables. You need to pass variables explicitly. For example, note the addition of ["the dot"][tempintro]. + +``` +{{ partial "header" . }} +``` + +The dot (`.`) is considered fundamental to understanding Hugo templating. For more information, see [Introduction to Hugo Templating][tempintro]. + +[homepage]: /templates/homepage/ +[tempintro]: /templates/introduction/ diff --git a/content/en/templates/views.md b/content/en/templates/views.md new file mode 100644 index 000000000..87f66afe0 --- /dev/null +++ b/content/en/templates/views.md @@ -0,0 +1,121 @@ +--- +title: Content View Templates +# linktitle: Content Views +description: Hugo can render alternative views of your content, which is especially useful in list and summary views. +date: 2017-02-01 +publishdate: 2017-02-01 +lastmod: 2017-02-01 +categories: [templates] +keywords: [views] +menu: + docs: + parent: "templates" + weight: 70 +weight: 70 +sections_weight: 70 +draft: false +aliases: [] +toc: true +--- + +These alternative **content views** are especially useful in [list templates][lists]. + +The following are common use cases for content views: + +* You want content of every type to be shown on the homepage but only with limited [summary views][summaries]. +* You only want a bulleted list of your content on a [taxonomy list page][taxonomylists]. 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 content view][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. + +``` + ▾ layouts/ + ▾ posts/ + 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 content view template has not been provided for that type. Content views can also be defined in the `_default` directory and will work the same as list and single templates who eventually trickle down to the `_default` directory as a matter of the lookup order. + + +``` +▾ layouts/ + ▾ _default/ + li.html + single.html + summary.html +``` + +## Which Template Will be Rendered? + +The following is the [lookup order][lookup] for content views: + +1. `/layouts//.html` +2. `/layouts/_default/.html` +3. `/themes//layouts//.html` +4. `/themes//layouts/_default/.html` + +## Example: Content View Inside a List + +The following example demonstrates how to use content views inside of your [list templates][lists]. + +### `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: + +{{< code file="layouts/_default/list.html" download="list.html" >}} +
+
+

{{ .Title }}

+ {{ range .Pages }} + {{ .Render "summary"}} + {{ end }} +
+
+{{< /code >}} + +### `summary.html` + +Hugo will pass the entire page object to the following `summary.html` view template. (See [Page Variables][pagevars] for a complete list.) + +{{< code file="layouts/_default/summary.html" download="summary.html" >}} + +{{< /code >}} + +### `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" }}`). + +{{< code file="layouts/_default/li.html" download="li.html" >}} +
  • + {{ .Title }} +
    {{ .Date.Format "Mon, Jan 2, 2006" }}
    +
  • +{{< /code >}} + +[lists]: /templates/lists/ +[lookup]: /templates/lookup-order/ +[pagevars]: /variables/page/ +[render]: /functions/render/ +[single]: /templates/single-page-templates/ +[spf]: https://spf13.com +[spfsourceli]: https://github.com/spf13/spf13.com/blob/master/layouts/_default/li.html +[spfsourcesection]: https://github.com/spf13/spf13.com/blob/master/layouts/_default/section.html +[spfsourcesummary]: https://github.com/spf13/spf13.com/blob/master/layouts/_default/summary.html +[summaries]: /content-management/summaries/ +[taxonomylists]: /templates/taxonomy-templates/ diff --git a/content/en/tools/_index.md b/content/en/tools/_index.md new file mode 100644 index 000000000..a186ffb06 --- /dev/null +++ b/content/en/tools/_index.md @@ -0,0 +1,24 @@ +--- +title: Developer Tools +linktitle: Developer Tools Overview +description: In addition to Hugo's powerful CLI, there is a large number of community-developed tool chains for Hugo developers. +date: 2016-12-05 +publishdate: 2016-12-05 +lastmod: 2017-02-26 +categories: [developer tools] +keywords: [] +menu: + docs: + parent: "tools" + weight: 01 +weight: 01 +sections_weight: 01 +draft: false +--- + +One of Hugo's greatest strengths is its passionate---and always evolving---developer community. With the exception of the `highlight` shortcode mentioned in [Syntax Highlighting][syntax], the tools and other projects featured in this section are offerings from both commercial services and open-source projects, many of which are developed by Hugo developers just like you. + +[See the popularity of Hugo compared with other static site generators.][staticgen] + +[staticgen]: https://staticgen.com +[syntax]: /tools/syntax-highlighting/ diff --git a/content/en/tools/editors.md b/content/en/tools/editors.md new file mode 100644 index 000000000..f0d82d65d --- /dev/null +++ b/content/en/tools/editors.md @@ -0,0 +1,47 @@ +--- +title: Editor Plug-ins for Hugo +linktitle: Editor Plug-ins +description: The Hugo community uses a wide range of preferred tools and has developed plug-ins for some of the most popular text editors to help automate parts of your workflow. +date: 2017-02-01 +publishdate: 2017-02-01 +lastmod: 2017-02-01 +categories: [developer tools] +keywords: [editor, plug-ins] +menu: + docs: + parent: "tools" + weight: 50 +weight: 50 +sections_weight: 50 +draft: false +aliases: [] +toc: false +--- + +The Hugo community uses a wide range of preferred tools and has developed plug-ins for some of the most popular text editors to help automate parts of 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" when developing with Hugo. The source code can be found [here](https://github.com/akmittal/hugofy-vscode). +* [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). + +## 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 sub-trees 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. + +## 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. +* [language-hugo](https://atom.io/packages/language-hugo). Adds syntax highlighting to Hugo files. + +[formats]: /content-management/formats/ diff --git a/content/en/tools/frontends.md b/content/en/tools/frontends.md new file mode 100644 index 000000000..7f8a92d34 --- /dev/null +++ b/content/en/tools/frontends.md @@ -0,0 +1,34 @@ +--- +title: Frontend Interfaces with Hugo +linktitle: Frontends +description: Do you prefer a graphical user interface over a text editor? Give these frontends a try. +date: 2017-02-01 +publishdate: 2017-02-01 +lastmod: 2017-02-01 +categories: [developer tools] +keywords: [frontend,gui] +menu: + docs: + parent: "tools" + weight: 40 +weight: 40 +sections_weight: 40 +draft: false +aliases: [] +toc: false +--- + +* [enwrite](https://github.com/zzamboni/enwrite). Enwrite enables 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! +* [Lipi](https://github.com/SohanChy/Lipi). Lipi is a native GUI frontend written in Java to manage your Hugo websites. +* [Netlify CMS](https://netlifycms.org). Netlify 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/Netlify CMS starter](https://github.com/netlify-templates/one-click-hugo-cms) is available to get new projects running quickly. +* [Hokus CMS](https://github.com/julianoappelklein/hokus). Hokus CMS is an open source, multiplatform, easy to use, desktop application for Hugo. Build from simple to complex user interfaces for Hugo websites by choosing from a dozen ready-to-use components — all for free, with no vendor lock-in. + + +## Commercial Services + +* [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. +* [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. +* [Forestry.io](https://forestry.io/). Forestry is a git-backed CMS for Hugo, Gatsby, Jekyll and VuePress websites with support for GitHub, GitLab, Bitbucket and Azure Devops. Forestry provides a nice user interface to edit and model content for non technical editors. It supports S3, Cloudinary and Netlify Large Media integrations for storing media. Every time an update is made via the CMS, Forestry will commit changes back to your repo and vice-versa. +* [Netlify.com](https://www.netlify.com). Netlify builds, deploys, and hosts your static website or app (Hugo, Jekyll, etc). Netlify offers a drag-and-drop interface and automatic deployments 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, and custom domains. Developers have complete control over the source code and can manage it with GitHub or Bitbucket's deceptively simple workflow. diff --git a/content/en/tools/migrations.md b/content/en/tools/migrations.md new file mode 100644 index 000000000..9a48d1df0 --- /dev/null +++ b/content/en/tools/migrations.md @@ -0,0 +1,85 @@ +--- +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. +date: 2017-02-01 +publishdate: 2017-02-01 +lastmod: 2017-02-01 +keywords: [migrations,jekyll,wordpress,drupal,ghost,contentful] +menu: + docs: + parent: "tools" + weight: 10 +weight: 10 +sections_weight: 10 +draft: false +aliases: [/developer-tools/migrations/,/developer-tools/migrated/] +toc: true +--- + +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. +{{% /note %}} + +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 use the new [Jekyll import command](/commands/hugo_import_jekyll/). + +- [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.) +- [exitwp-for-hugo](https://github.com/wooni005/exitwp-for-hugo) - A python script which works with the xml export from Wordpress and converts Wordpress pages and posts to Markdown and YAML for hugo. +- [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. + +## 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. + +## 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. +- [blogger-to-hugo](https://bitbucket.org/petraszd/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 + +- [contentful2hugo](https://github.com/ArnoNuyts/contentful2hugo) - 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) tramsform to hugo sites easily. diff --git a/content/en/tools/other.md b/content/en/tools/other.md new file mode 100644 index 000000000..3afd7b96b --- /dev/null +++ b/content/en/tools/other.md @@ -0,0 +1,29 @@ +--- +title: Other Hugo 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. +date: 2017-02-01 +publishdate: 2017-02-01 +lastmod: 2017-02-01 +categories: [developer tools] +keywords: [frontend,gui] +menu: + docs: + parent: "tools" + weight: 70 +weight: 70 +sections_weight: 70 +draft: false +aliases: [] +toc: false +--- + +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 that 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. +* [Hugo SFTP Upload](https://github.com/thomasmey/HugoSftpUpload) Syncs the local build of your Hugo website with your remote webserver via SFTP. +* [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. +* [JAMStack Themes](https://jamstackthemes.dev/ssg/hugo/). JAMStack themes is 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). diff --git a/content/en/tools/search.md b/content/en/tools/search.md new file mode 100644 index 000000000..2a6c0296a --- /dev/null +++ b/content/en/tools/search.md @@ -0,0 +1,33 @@ +--- +title: Search for your Hugo Website +linktitle: Search +description: See some of the open-source and commercial search options for your newly created Hugo website. +date: 2017-02-01 +publishdate: 2017-02-01 +lastmod: 2017-02-26 +categories: [developer tools] +keywords: [search,tools] +menu: + docs: + parent: "tools" + weight: 60 +weight: 60 +sections_weight: 60 +draft: false +aliases: [] +toc: true +--- + +A static website with a dynamic search function? Yes. As alternatives to embeddable scripts from Google or other search engines, you can provide your visitors a custom search by indexing your content files directly. + +* [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](http://lunrjs.com/) to serve the search results. +* [hugo-elasticsearch](https://www.npmjs.com/package/hugo-elasticsearch). Generate [Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html) indexes for Hugo static sites by parsing front matter. Hugo-Elasticsearch will generate a newline delimited JSON (NDJSON) file that can be bulk uploaded into Elasticsearch using any one of the available [clients](https://www.elastic.co/guide/en/elasticsearch/client/index.html). +* [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. +* [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](http://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. + +## Commercial Search Services + +* [Algolia](https://www.algolia.com/)'s Search API 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) 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://docs.bonsai.io/docs/hugo). diff --git a/content/en/tools/starter-kits.md b/content/en/tools/starter-kits.md new file mode 100644 index 000000000..e30de33d9 --- /dev/null +++ b/content/en/tools/starter-kits.md @@ -0,0 +1,43 @@ +--- +title: Starter Kits +linktitle: Starter Kits +description: A list of community-developed projects designed to help you get up and running with Hugo. +date: 2017-02-22 +publishdate: 2017-02-01 +lastmod: 2018-08-11 +keywords: [starters,assets,pipeline] +menu: + docs: + parent: "tools" + weight: 30 +weight: 30 +sections_weight: 30 +draft: false +aliases: [/developer-tools/migrations/,/developer-tools/migrated/] +toc: false +--- + +Know of a Hugo-related starter kit that isn't mentioned here? [Please add it to the list.][addkit] + +{{% note "Starter Kits are Not Maintained by the Hugo Team"%}} +The following starter kits are developed by active members of the Hugo community. If you find yourself having issues with any of the projects, it's best to file an issue directly with the project's maintainer(s). +{{% /note %}} + +* [Hugo Wrapper][hugow]. Hugo Wrapper is a POSIX-style shell script which acts as a wrapper to download and run Hugo binary for your platform. It can be executed in variety of [Operating Systems][hugow-test] and [Command Shells][hugow-test]. +* [Victor Hugo][]. Victor Hugo is a Hugo boilerplate for creating truly epic websites using Webpack as an asset pipeline. Victor Hugo uses post-css and Babel for CSS and JavaScript, respectively, and is actively maintained. +* [GOHUGO AMP][]. GoHugo AMP is a starter theme that aims to make it easy to adopt [Google's AMP Project][amp]. The starter kit comes with 40+ shortcodes and partials plus automatic structured data. The project also includes a [separate site with extensive documentation][gohugodocs]. +* [Blaupause][]. Blaupause is a developer-friendly Hugo starter kit based on Gulp tasks. It comes ES6-ready with several helpers for SVG and fonts and basic structure for HTML, SCSS, and JavaScript. +* [hugulp][]. hugulp is a tool to optimize the assets of a Hugo website. The main idea is to recreate the famous Ruby on Rails Asset Pipeline, which minifies, concatenates and fingerprints the assets used in your website. +* [Atlas][]. Atlas is a Hugo boilerplate designed to speed up development with support for Netlify, Hugo Pipes, SCSS & more. It's actively maintained and contributions are always welcome. + + +[addkit]: https://github.com/gohugoio/hugo/edit/master/docs/content/en/tools/starter-kits.md +[amp]: https://www.ampproject.org/ +[Blaupause]: https://github.com/fspoettel/blaupause +[GOHUGO AMP]: https://github.com/wildhaber/gohugo-amp +[gohugodocs]: https://gohugo-amp.gohugohq.com/ +[hugow]: https://github.com/khos2ow/hugo-wrapper +[hugow-test]: https://github.com/khos2ow/hugo-wrapper#tested-on +[hugulp]: https://github.com/jbrodriguez/hugulp +[Victor Hugo]: https://github.com/netlify/victor-hugo +[Atlas]: https://github.com/indigotree/atlas diff --git a/content/en/troubleshooting/_index.md b/content/en/troubleshooting/_index.md new file mode 100644 index 000000000..3b0e93725 --- /dev/null +++ b/content/en/troubleshooting/_index.md @@ -0,0 +1,26 @@ +--- +title: Troubleshoot +linktitle: Troubleshoot +description: Frequently asked questions and known issues pulled from the Hugo Discuss forum. +date: 2017-02-01 +publishdate: 2017-02-01 +lastmod: 2017-02-01 +menu: + docs: + parent: "troubleshooting" + weight: 1 +weight: 1 +draft: false +hidesectioncontents: false +slug: +aliases: [/troubleshooting/faqs/,/faqs/] +toc: false +notesforauthors: +--- + +The Troubleshooting section includes known issues, recent workarounds, and FAQs pulled from the [Hugo Discussion Forum][forum]. + + + + +[forum]: https://discourse.gohugo.io diff --git a/content/en/troubleshooting/build-performance.md b/content/en/troubleshooting/build-performance.md new file mode 100644 index 000000000..e0700f381 --- /dev/null +++ b/content/en/troubleshooting/build-performance.md @@ -0,0 +1,101 @@ +--- +title: Build Performance +linktitle: Build Performance +description: An overview of features used for diagnosing and improving performance issues in site builds. +date: 2017-03-12 +publishdate: 2017-03-12 +lastmod: 2017-03-12 +keywords: [performance, build] +categories: [troubleshooting] +menu: + docs: + parent: "troubleshooting" +weight: 3 +slug: +aliases: [] +toc: true +--- + +{{% note %}} +The example site used below is from https://github.com/gohugoio/hugo/tree/master/examples/blog +{{% /note %}} + +## Template Metrics + +Hugo is a very fast static site generator, but it is possible to write +inefficient templates. Hugo's *template metrics* feature is extremely helpful +in pinpointing which templates are executed most often and how long those +executions take **in terms of CPU time**. + +| Metric Name | Description | +|---------------------|-------------| +| cumulative duration | The cumulative time spent executing a given template. | +| average duration | The average time spent executing a given template. | +| maximum duration | The maximum time a single execution took for a given template. | +| count | The number of times a template was executed. | +| template | The template name. | + +``` +▶ hugo --templateMetrics +Started building sites ... + +Built site for language en: +0 draft content +0 future content +0 expired content +2 regular pages created +22 other pages created +0 non-page files copied +0 paginator pages created +4 tags created +3 categories created +total in 18 ms + +Template Metrics: + + cumulative average maximum + duration duration duration count template + ---------- -------- -------- ----- -------- + 6.419663ms 583.605µs 994.374µs 11 _internal/_default/rss.xml + 4.718511ms 1.572837ms 3.880742ms 3 indexes/category.html + 4.642666ms 2.321333ms 3.282842ms 2 posts/single.html + 4.364445ms 396.767µs 2.451372ms 11 partials/header.html + 2.346069ms 586.517µs 903.343µs 4 indexes/tag.html + 2.330919ms 211.901µs 2.281342ms 11 partials/header.includes.html + 1.238976ms 103.248µs 446.084µs 12 posts/li.html + 972.16µs 972.16µs 972.16µs 1 _internal/_default/sitemap.xml + 953.597µs 953.597µs 953.597µs 1 index.html + 822.263µs 822.263µs 822.263µs 1 indexes/post.html + 567.498µs 51.59µs 112.205µs 11 partials/navbar.html + 348.22µs 31.656µs 88.249µs 11 partials/meta.html + 346.782µs 173.391µs 276.176µs 2 posts/summary.html + 235.184µs 21.38µs 124.383µs 11 partials/footer.copyright.html + 132.003µs 12µs 117.999µs 11 partials/menu.html + 72.547µs 6.595µs 63.764µs 11 partials/footer.html +``` + +{{% note %}} +**A Note About Parallelism** + +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. +{{% /note %}} + + +## Cached Partials + +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`][partialCached] template function provides +caching capabilities for `partial` templates. + +{{% tip %}} +Note that you can create cached variants of each `partial` by passing additional +parameters to `partialCached` beyond the initial context. See the +`partialCached` documentation for more details. +{{% /tip %}} + + +[partialCached]:{{< ref "/functions/partialCached.md" >}} diff --git a/content/en/troubleshooting/faq.md b/content/en/troubleshooting/faq.md new file mode 100644 index 000000000..46f3e8cd5 --- /dev/null +++ b/content/en/troubleshooting/faq.md @@ -0,0 +1,56 @@ +--- +title: Frequently Asked Questions +linktitle: FAQ +description: Solutions to some common Hugo problems. +date: 2018-02-10 +categories: [troubleshooting] +menu: + docs: + parent: "troubleshooting" +keywords: [faqs] +weight: 2 +toc: true +aliases: [/faq/] +--- + +{{% note %}} +**Note:** The answers/solutions presented below are short, and may not be enough to solve your problem. Visit [Hugo Discourse](https://discourse.gohugo.io/) and use the search. It that does not help, start a new topic and ask your questions. +{{% /note %}} + +## I can't see my content! + +Is your markdown file [in draft mode](https://gohugo.io/content-management/front-matter/#front-matter-variables)? When testing, run `hugo server` with the `-D` or `--buildDrafts` [switch](https://gohugo.io/getting-started/usage/#draft-future-and-expired-content). + +## Can I set configuration variables via OS environment? + +Yes you can! See [Configure with Environment Variables](/getting-started/configuration/#configure-with-environment-variables). + +## How do I schedule posts? + +1. Set `publishDate` in the page [Front Matter](/content-management/front-matter/) to a date in the future. +2. Build and publish at intervals. + +How to automate the "publish at intervals" part depends on your situation: + +* If you deploy from your own PC/server, you can automate with [Cron](https://en.wikipedia.org/wiki/Cron) or similar. +* If your site is hosted on a service similar to [Netlify](https://www.netlify.com/) you can use a service such as [ifttt](https://ifttt.com/date_and_time) to schedule the updates. + +Also see this Twitter thread: + +{{< tweet 962380712027590657 >}} + +## Can I use the latest Hugo version on Netlify? + +Yes you can! Read [this](/hosting-and-deployment/hosting-on-netlify/#configure-hugo-version-in-netlify). + +## I get "TOCSS ... this feature is not available in your current Hugo version" + +If you process `SCSS` or `SASS` to `CSS` in your Hugo project, you need the Hugo `extended` version, or else you may see this error message: + +```bash +error: failed to transform resource: TOCSS: failed to transform "scss/main.scss" (text/x-scss): this feature is not available in your current Hugo version +``` + +We release two set of binaries for technical reasons. The extended version is not what you get by default for some installation methods. On the [release page](https://github.com/gohugoio/hugo/releases), look for archives with `extended` in the name. To build `hugo-extended`, use `go install --tags extended` + +To confirm, run `hugo version` and look for the word `extended`. diff --git a/content/en/variables/_index.md b/content/en/variables/_index.md new file mode 100644 index 000000000..382ee25d4 --- /dev/null +++ b/content/en/variables/_index.md @@ -0,0 +1,23 @@ +--- +title: Variables and Params +linktitle: Variables Overview +description: Page-, file-, taxonomy-, and site-level variables and parameters available in templates. +date: 2017-02-01 +publishdate: 2017-02-01 +lastmod: 2017-02-01 +categories: [variables and params] +keywords: [variables,params,values,globals] +draft: false +menu: + docs: + parent: "variables" + weight: 1 +weight: 01 #rem +sections_weight: 01 +aliases: [/templates/variables/] +toc: false +--- + +Hugo's templates are context aware and make a large number of values available to you as you're creating views for your website. + +[Go templates]: /templates/introduction/ "Understand context in Go templates by learning the language's fundamental templating functions." diff --git a/content/en/variables/files.md b/content/en/variables/files.md new file mode 100644 index 000000000..eb5d404de --- /dev/null +++ b/content/en/variables/files.md @@ -0,0 +1,52 @@ +--- +title: File Variables +linktitle: +description: "You can access filesystem-related data for a content file in the `.File` variable." +date: 2017-02-01 +publishdate: 2017-02-01 +lastmod: 2017-02-01 +categories: [variables and params] +keywords: [files] +draft: false +menu: + docs: + parent: "variables" + weight: 40 +weight: 40 +sections_weight: 40 +aliases: [/variables/file-variables/] +toc: false +--- + +{{% note "Rendering Local Files" %}} +For information on creating shortcodes and templates that tap into Hugo's file-related feature set, see [Local File Templates](/templates/files/). +{{% /note %}} + +The `.File` object contains the following fields: + +.File.Path +: the original relative path of the page, relative to the content dir (e.g., `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.ContentBaseName +: is a either TranslationBaseName or name of containing folder if file is a leaf bundle. + +.File.BaseFileName +: the filename without extension (e.g., `foo.en`) + + +.File.Ext +: the file extension of the content file (e.g., `md`); this can also be called using `.File.Extension` as well. Note that it is *only* the extension without `.`. + +.File.Lang +: the language associated with the given file if Hugo's [Multilingual features][multilingual] are 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/`). Note that the path separator (`\` or `/`) could be dependent on the operating system. + +[Multilingual]: /content-management/multilingual/ diff --git a/content/en/variables/git.md b/content/en/variables/git.md new file mode 100644 index 000000000..59ee9ac88 --- /dev/null +++ b/content/en/variables/git.md @@ -0,0 +1,58 @@ +--- +title: Git Info Variables +linktitle: Git Variables +description: Get the last Git revision information for every content file. +date: 2017-03-12 +publishdate: 2017-03-12 +lastmod: 2017-03-12 +categories: [variables and params] +keywords: [git] +draft: false +menu: + docs: + parent: "variables" + weight: 70 +weight: 70 +sections_weight: 70 +aliases: [/extras/gitinfo/] +toc: false +wip: false +--- + +{{% note "`.GitInfo` Performance Considerations" %}} +Hugo's Git integrations should be fairly performant but *can* increase your build time. This will depend on the size of your Git history. +{{% /note %}} + +## `.GitInfo` Prerequisites + +1. The Hugo site must be in a Git-enabled directory. +2. The Git executable must be installed and in your system `PATH`. +3. The `.GitInfo` feature must be enabled in your Hugo project by passing `--enableGitInfo` flag on the command line or by setting `enableGitInfo` to `true` in your [site's configuration file][configuration]. + +## The `.GitInfo` Object + +The `GitInfo` object contains the following fields: + +.AbbreviatedHash +: the abbreviated commit hash (e.g., `866cbcc`) + +.AuthorName +: the author's name, respecting `.mailmap` + +.AuthorEmail +: the author's email address, respecting `.mailmap` + +.AuthorDate +: the author date + +.Hash +: the commit hash (e.g., `866cbccdab588b9908887ffd3b4f2667e94090c3`) + +.Subject +: commit message subject (e.g., `tpl: Add custom index function`) + +## `.Lastmod` + +If the `.GitInfo` feature is enabled, `.Lastmod` (on `Page`) is fetched from Git i.e. `.GitInfo.AuthorDate`. This behaviour can be changed by adding your own [front matter configuration for dates](/getting-started/configuration/#configure-front-matter). + +[configuration]: /getting-started/configuration/ diff --git a/content/en/variables/hugo.md b/content/en/variables/hugo.md new file mode 100644 index 000000000..0de5bd794 --- /dev/null +++ b/content/en/variables/hugo.md @@ -0,0 +1,49 @@ +--- +title: Hugo-specific Variables +linktitle: Hugo Variables +description: The `.Hugo` variable provides easy access to Hugo-related data. +date: 2017-03-12 +publishdate: 2017-03-12 +lastmod: 2017-03-12 +categories: [variables and params] +keywords: [hugo,generator] +draft: false +menu: + docs: + parent: "variables" + weight: 60 +weight: 60 +sections_weight: 60 +aliases: [] +toc: false +wip: false +--- + +{{% warning "Deprecated" %}} +Page's `.Hugo` is deprecated and will be removed in a future release. Use the global `hugo` function. +For example: `hugo.Generator`. +{{% /warning %}} + +It contains the following fields: + +.Hugo.Generator +: `` tag for the version of Hugo that generated the site. `.Hugo.Generator` 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.Environment +: the current running environment as defined through the `--environment` cli tag. + +.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`
    + + + +{{% note "Use the Hugo Generator Tag" %}} +We highly recommend using `.Hugo.Generator` in your website's ``. `.Hugo.Generator` is included by default in all themes hosted on [themes.gohugo.io](https://themes.gohugo.io). The generator tag allows the Hugo team to track the usage and popularity of Hugo. +{{% /note %}} + diff --git a/content/en/variables/menus.md b/content/en/variables/menus.md new file mode 100644 index 000000000..6717fecbb --- /dev/null +++ b/content/en/variables/menus.md @@ -0,0 +1,124 @@ +--- +title: Menu Entry Properties +linktitle: Menu Entry Properties +description: A menu entry in a menu-template has specific variables and functions to make menu management easier. +date: 2017-03-12 +publishdate: 2017-03-12 +lastmod: 2017-03-12 +categories: [variables and params] +keywords: [menus] +draft: false +menu: + docs: + title: "variables defined by a menu entry" + parent: "variables" + weight: 50 +weight: 50 +sections_weight: 50 +aliases: [/variables/menu/] +toc: false +--- + +A **menu entry** has the following properties available that can be used in a +[menu template][menu-template]. + +## Menu Entry Variables + +.Menu +: _string_
    +Name of the **menu** that contains this **menu entry**. + +.URL +: _string_
    +URL that the menu entry points to. The `url` key, if set for the menu entry, +sets this value. If that key is not set, and if the menu entry is set in a page +front-matter, this value defaults to the page's `.RelPermalink`. + +.Page +: _\*Page_
    +Reference to the [page object][page-object] associated with the menu entry. This +will be non-nil if the menu entry is set via a page's front-matter and not via +the site config. + +.Name +: _string_
    +Name of the menu entry. The `name` key, if set for the menu entry, sets +this value. If that key is not set, and if the menu entry is set in a page +front-matter, this value defaults to the page's `.LinkTitle`. + +.Identifier +: _string_
    +Value of the `identifier` key if set for the menu entry. This value must be +unique for each menu entry. **It is necessary to set a unique identifier +manually if two or more menu entries have the same `.Name`.** + +.Pre +: _template.HTML_
    +Value of the `pre` key if set for the menu entry. This value typically contains +a string representing HTML. + +.Post +: _template.HTML_
    +Value of the `post` key if set for the menu entry. This value typically contains +a string representing HTML. + +.Weight +: _int_
    +Value of the `weight` key if set for the menu entry. If that key is not set, +and if the menu entry is set in a page front-matter, this value defaults to the +page's `.Weight`. + +.Parent +: _string_
    +Name (or Identifier if present) of this menu entry's parent **menu entry**. The +`parent` key, if set for the menu entry, sets this value. If this key is set, +this menu entry nests under that parent entry, else it nests directly under the +`.Menu`. + +.Children +: _Menu_
    +This value is auto-populated by Hugo. It is a collection of children menu +entries, if any, under the current menu entry. + +## Menu Entry Functions + +Menus also have the following functions available: + +.HasChildren +: _boolean_
    +Returns `true` if `.Children` is non-nil. + +.KeyName +: _string_
    +Returns the `.Identifier` if present, else returns the `.Name`. + +.IsEqual +: _boolean_
    +Returns `true` if the two compared menu entries represent the same menu entry. + +.IsSameResource +: _boolean_
    +Returns `true` if the two compared menu entries have the same `.URL`. + +.Title +: _string_
    +Link title, meant to be used in the `title` attribute of a menu entry's +``-tags. Returns the menu entry's `title` key if set. Else, if the menu +entry was created through a page's front-matter, it returns the page's +`.LinkTitle`. Else, it just returns an empty string. + +## Other Menu-related Functions + +Additionally, here are some relevant methods available to menus on a page: + +.IsMenuCurrent +: _(menu string, menuEntry *MenuEntry ) boolean_
    +See [`.IsMenuCurrent` method](/functions/ismenucurrent/). + +.HasMenuCurrent +: _(menu string, menuEntry *MenuEntry) boolean_
    +See [`.HasMenuCurrent` method](/functions/hasmenucurrent/). + + +[menu-template]: /templates/menu-templates/ +[page-object]: /variables/page/ diff --git a/content/en/variables/page.md b/content/en/variables/page.md new file mode 100644 index 000000000..c426ca47c --- /dev/null +++ b/content/en/variables/page.md @@ -0,0 +1,304 @@ +--- +title: Page Variables +linktitle: +description: Page-level variables are defined in a content file's front matter, derived from the content's file location, or extracted from the content body itself. +date: 2017-02-01 +publishdate: 2017-02-01 +lastmod: 2017-02-01 +categories: [variables and params] +keywords: [pages] +draft: false +menu: + docs: + title: "variables defined by a page" + parent: "variables" + weight: 20 +weight: 20 +sections_weight: 20 +aliases: [] +toc: true +--- + +The following is a list of page-level variables. Many of these will be defined in the front matter, derived from file location, or extracted from the content itself. + +{{% note "`.Scratch`" %}} +See [`.Scratch`](/functions/scratch/) for page-scoped, writable variables. +{{% /note %}} + +## Page Variables + +.AlternativeOutputFormats +: contains all alternative formats for a given page; this variable is especially useful `link rel` list in your site's ``. (See [Output Formats](/templates/output-formats/).) + +.Aliases +: aliases of this page + +.Content +: the content itself, defined below the front matter. + +.Data +: the data specific to this type of page. + +.Date +: the date associated with the page; `.Date` pulls from the `date` field in a content's front matter. See also `.ExpiryDate`, `.PublishDate`, and `.Lastmod`. + +.Description +: the description for the page. + +.Dir +: the path of the folder containing this content file. The path is relative to the `content` folder. + +.Draft +: a boolean, `true` if the content is marked as a draft in the front matter. + +.ExpiryDate +: the date on which the content is scheduled to expire; `.ExpiryDate` pulls from the `expirydate` field in a content's front matter. See also `.PublishDate`, `.Date`, and `.Lastmod`. + +.File +: filesystem-related data for this content file. See also [File Variables][]. + +.FuzzyWordCount +: the approximate number of words in the content. + +.Hugo +: see [Hugo Variables](/variables/hugo/). + +.IsHome +: `true` in the context of the [homepage](/templates/homepage/). + +.IsNode +: always `false` for regular content pages. + +.IsPage +: always `true` for regular content pages. + +.IsTranslated +: `true` if there are translations to display. + +.Keywords +: the meta keywords for the content. + +.Kind +: the page's *kind*. Possible return values are `page`, `home`, `section`, `taxonomy`, or `taxonomyTerm`. Note that there are also `RSS`, `sitemap`, `robotsTXT`, and `404` kinds, but these are only available during the rendering of each of these respective page's kind and therefore *not* available in any of the `Pages` collections. + +.Language +: a language object that points to the language's definition in the site `config`. `.Language.Lang` gives you the language code. + +.Lastmod +: the date the content was last modified. `.Lastmod` pulls from the `lastmod` field in a content's front matter. + + - If `lastmod` is not set, and `.GitInfo` feature is disabled, the front matter `date` field will be used. + - If `lastmod` is not set, and `.GitInfo` feature is enabled, `.GitInfo.AuthorDate` will be used instead. + +See also `.ExpiryDate`, `.Date`, `.PublishDate`, and [`.GitInfo`][gitinfo]. + +.LinkTitle +: access when creating links to the content. If set, Hugo will use the `linktitle` from the front matter before `title`. + +.Next +: Points up to the next [regular page](/variables/site/#site-pages) (sorted by Hugo's [default sort](/templates/lists#default-weight-date-linktitle-filepath)). Example: `{{with .Next}}{{.Permalink}}{{end}}`. Calling `.Next` from the first page returns `nil`. + +.NextInSection +: Points up to the next [regular page](/variables/site/#site-pages) below the same top level section (e.g. in `/blog`)). Pages are sorted by Hugo's [default sort](/templates/lists#default-weight-date-linktitle-filepath). Example: `{{with .NextInSection}}{{.Permalink}}{{end}}`. Calling `.NextInSection` from the first page returns `nil`. + +.OutputFormats +: contains all formats, including the current format, for a given page. Can be combined the with [`.Get` function](/functions/get/) to grab a specific format. (See [Output Formats](/templates/output-formats/).) + +.Pages +: a collection of associated pages. This value will be `nil` within + the context of regular content pages. See [`.Pages`](#pages). + +.Permalink +: the Permanent link for this page; see [Permalinks](/content-management/urls/) + +.Plain +: the Page content stripped of HTML tags and presented as a string. + +.PlainWords +: the Page content stripped of HTML as a `[]string` using Go's [`strings.Fields`](https://golang.org/pkg/strings/#Fields) to split `.Plain` into a slice. + +.Prev +: Points down to the previous [regular page](/variables/site/#site-pages) (sorted by Hugo's [default sort](/templates/lists#default-weight-date-linktitle-filepath)). Example: `{{if .PrevPage}}{{.PrevPage.Permalink}}{{end}}`. Calling `.Prev` from the last page returns `nil`. + +.PrevInSection +: Points down to the previous [regular page](/variables/site/#site-pages) below the same top level section (e.g. `/blog`). Pages are sorted by Hugo's [default sort](/templates/lists#default-weight-date-linktitle-filepath). Example: `{{if .PrevInSection}}{{.PrevInSection.Permalink}}{{end}}`. Calling `.PrevInSection` from the last page returns `nil`. + +.PublishDate +: the date on which the content was or will be published; `.Publishdate` pulls from the `publishdate` field in a content's front matter. See also `.ExpiryDate`, `.Date`, and `.Lastmod`. + +.RSSLink (deprecated) +: link to the page's RSS feed. This is deprecated. You should instead do something like this: `{{ with .OutputFormats.Get "RSS" }}{{ .RelPermalink }}{{ end }}`. + +.RawContent +: raw markdown content without the front matter. Useful with [remarkjs.com]( +http://remarkjs.com) + +.ReadingTime +: the estimated time, in minutes, it takes to read the content. + +.Resources +: resources such as images and CSS that are associated with this page + +.Ref +: returns the permalink for a given reference (e.g., `.Ref "sample.md"`). `.Ref` does *not* handle in-page fragments correctly. See [Cross References](/content-management/cross-references/). + +.RelPermalink +: the relative permanent link for this page. + +.RelRef +: returns the relative permalink for a given reference (e.g., `RelRef +"sample.md"`). `.RelRef` does *not* handle in-page fragments correctly. See [Cross References](/content-management/cross-references/). + +.Site +: see [Site Variables](/variables/site/). + +.Sites +: returns all sites (languages). A typical use case would be to link back to the main language: `
    ...`. + +.Sites.First +: returns the site for the first language. If this is not a multilingual setup, it will return itself. + +.Summary +: a generated summary of the content for easily showing a snippet in a summary view. The breakpoint can be set manually by inserting <!--more--> at the appropriate place in the content page, or the summary can be written independent of the page text. See [Content Summaries](/content-management/summaries/) for more details. + +.TableOfContents +: the rendered [table of contents](/content-management/toc/) for the page. + +.Title +: the title for this page. + +.Translations +: a list of translated versions of the current page. See [Multilingual Mode](/content-management/multilingual/) for more information. + +.TranslationKey +: the key used to map language translations of the current page. See [Multilingual Mode](/content-management/multilingual/) for more information. + +.Truncated +: a boolean, `true` if the `.Summary` is truncated. Useful for showing a "Read more..." link only when necessary. See [Summaries](/content-management/summaries/) for more information. + +.Type +: the [content type](/content-management/types/) of the content (e.g., `posts`). + +.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. + +## Section Variables and Methods + +Also see [Sections](/content-management/sections/). + +{{< readfile file="/content/en/readfiles/sectionvars.md" markdown="true" >}} + +## The `.Pages` Variable {#pages} + +`.Pages` is an alias to `.Data.Pages`. It is conventional to use the +aliased form `.Pages`. + +### `.Pages` compared to `.Site.Pages` + +{{< readfile file="/content/en/readfiles/pages-vs-site-pages.md" markdown="true" >}} + +## Page-level Params + +Any other value defined in the front matter in a content file, including taxonomies, will be made available as part of the `.Params` variable. + +``` +--- +title: My First Post +date: 2017-02-20T15:26:23-06:00 +categories: [one] +tags: [two,three,four] +``` + +With the above front matter, the `tags` and `categories` taxonomies are accessible via the following: + +* `.Params.tags` +* `.Params.categories` + +{{% note "Casing of Params" %}} +Page-level `.Params` are *only* accessible in lowercase. +{{% /note %}} + +The `.Params` variable is particularly useful for the introduction of user-defined front matter fields in content files. For example, a Hugo website on book reviews could have the following front matter in `/content/review/book01.md`: + +``` +--- +... +affiliatelink: "http://www.my-book-link.here" +recommendedby: "My Mother" +... +--- +``` + +These fields would then be accessible to the `/themes/yourtheme/layouts/review/single.html` template through `.Params.affiliatelink` and `.Params.recommendedby`, respectively. + +Two common situations where this type of front matter field could be introduced is as a value of a certain attribute like `href=""` or by itself to be displayed as text to the website's visitors. + +{{< code file="/themes/yourtheme/layouts/review/single.html" >}} +

    Buy this book

    +

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

    +{{< /code >}} + +This template would render as follows, assuming you've set [`uglyURLs`](/content-management/urls/) to `false` in your [site `config`](/getting-started/configuration/): + +{{< output file="yourbaseurl/review/book01/index.html" >}} +

    Buy this book

    +

    It was recommended by my Mother.

    +{{< /output >}} + +{{% note %}} +See [Archetypes](/content-management/archetypes/) for consistency of `Params` across pieces of content. +{{% /note %}} + +### The `.Param` Method + +In Hugo, you can declare params in individual pages and globally for your entire website. A common use case is to have a general value for the site param 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 according to it's definition in a page parameter (i.e. in the content's front matter) or a site parameter (i.e., in your `config`). + +### Access Nested Fields in Front Matter + +When front matter contains nested fields like the following: + +``` +--- +author: + given_name: John + family_name: Feminella + display_name: John Feminella +--- +``` +`.Param` can access these fields by concatenating the field names together with a dot: + +``` +{{ $.Param "author.display_name" }} +``` + +If your front matter contains a top-level key that is ambiguous with a nested key, as in the following case: + +``` +--- +favorites.flavor: vanilla +favorites: + flavor: chocolate +--- +``` + +The top-level key will be preferred. Therefore, the following method, when applied to the previous example, will print `vanilla` and not `chocolate`: + +``` +{{ $.Param "favorites.flavor" }} +=> vanilla +``` + +[gitinfo]: /variables/git/ +[File Variables]: /variables/files/ diff --git a/content/en/variables/shortcodes.md b/content/en/variables/shortcodes.md new file mode 100644 index 000000000..7462deec7 --- /dev/null +++ b/content/en/variables/shortcodes.md @@ -0,0 +1,48 @@ +--- +title: Shortcode Variables +linktitle: Shortcode Variables +description: Shortcodes can access page variables and also have their own specific built-in variables. +date: 2017-03-12 +publishdate: 2017-03-12 +lastmod: 2017-03-12 +categories: [variables and params] +keywords: [shortcodes] +draft: false +menu: + docs: + parent: "variables" + weight: 20 +weight: 20 +sections_weight: 20 +aliases: [] +toc: false +--- + +[Shortcodes][shortcodes] have access to parameters delimited in the shortcode declaration via [`.Get`][getfunction], page- and site-level variables, and also the following shortcode-specific fields: + +.Name +: Shortcode name. + +.Ordinal +: 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. + +.Parent +: provides access to the parent shortcode context in nested shortcodes. This can be very useful for inheritance of common shortcode parameters from the root. + +.Position +: Contains [filename and position](https://godoc.org/github.com/gohugoio/hugo/common/text#Position) for the shortcode in a page. Note that this can be relatively expensive to calculate, and is meant for error reporting. See [Error Handling in Shortcodes](/templates/shortcode-templates/#error-handling-in-shortcodes). + + + + +.IsNamedParams +: boolean that returns `true` when the shortcode in question uses [named rather than positional parameters][shortcodes] + +.Inner +: represents the content between the opening and closing shortcode tags when a [closing shortcode][markdownshortcode] is used + +[getfunction]: /functions/get/ +[markdownshortcode]: /content-management/shortcodes/#shortcodes-with-markdown +[shortcodes]: /templates/shortcode-templates/ + + diff --git a/content/en/variables/site.md b/content/en/variables/site.md new file mode 100644 index 000000000..7d663b96b --- /dev/null +++ b/content/en/variables/site.md @@ -0,0 +1,131 @@ +--- +title: Site Variables +linktitle: Site Variables +description: Many, but not all, site-wide variables are defined in your site's configuration. However, Hugo provides a number of built-in variables for convenient access to global values in your templates. +date: 2017-02-01 +publishdate: 2017-02-01 +lastmod: 2017-02-01 +categories: [variables and params] +keywords: [global,site] +draft: false +menu: + docs: + parent: "variables" + weight: 10 +weight: 10 +sections_weight: 10 +aliases: [/variables/site-variables/] +toc: true +--- + +The following is a list of site-level (aka "global") variables. Many of these variables are defined in your site's [configuration file][config], whereas others are built into Hugo's core for convenient usage in your templates. + +## Site Variables List + +.Site.AllPages +: array of all pages, regardless of their translation. + +.Site.Author +: a map of the authors as defined in the site configuration. + +.Site.BaseURL +: the base URL for the site as defined in the site configuration. + +.Site.BuildDrafts +: a boolean (default: `false`) to indicate whether to build drafts as defined in the site configuration. + +.Site.Copyright +: a string representing the copyright of your website as defined in the site configuration. + +.Site.Data +: custom data, see [Data Templates](/templates/data-templates/). + +.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.Home +: reference to the homepage's [page object](https://gohugo.io/variables/page/) + +.Site.IsMultiLingual +: whether there are more than one language in this site. See [Multilingual](/content-management/multilingual/) for more information. + +.Site.IsServer +: a boolean to indicate if the site is being served with Hugo's built-in server. See [`hugo server`](/commands/hugo_server/) for more information. + +.Site.Language.Lang +: the language code of the current locale (e.g., `en`). + +.Site.Language.LanguageName +: the full language name (e.g. `English`). + +.Site.Language.Weight +: the weight that defines the order in the `.Site.Languages` list. + +.Site.Language +: indicates the language currently being used to render the website. This object's attributes are set in site configurations' language definition. + +.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.LanguagePrefix +: this can be used to prefix URLs to point to the correct language. It will even work when only one defined language. See also the functions [absLangURL](/functions/abslangurl/) and [relLangURL](/functions/rellangurl). + +.Site.Languages +: an ordered list (ordered by defined weight) of languages. + +.Site.LastChange +: a string representing the date/time of the most recent change to your site. This string is based on the [`date` variable in the front matter](/content-management/front-matter) of your content pages. + +.Site.Menus +: all of the menus in the site. + +.Site.Pages +: array of all content ordered by Date with the newest first. This array contains only the pages in the current language. See [`.Site.Pages`](#site-pages). + +.Site.RegularPages +: a shortcut to the *regular* page collection. `.Site.RegularPages` is equivalent to `where .Site.Pages "Kind" "page"`. See [`.Site.Pages`](#site-pages). + +.Site.Sections +: top-level directories of the site. + +.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.Title +: a string representing the title of the site. + +## The `.Site.Params` Variable + +`.Site.Params` is a container holding the values from the `params` section of your site configuration. + +### Example: `.Site.Params` + +The following `config.[yaml|toml|json]` defines a site-wide param for `description`: + +{{< code-toggle file="config" >}} +baseURL = "https://yoursite.example.com/" + +[params] + description = "Tesla's Awesome Hugo Site" + author = "Nikola Tesla" +{{}} + +You can use `.Site.Params` in a [partial template](/templates/partials/) to call the default site description: + +{{< code file="layouts/partials/head.html" >}} + +{{< /code >}} + +## The `.Site.Pages` Variable {#site-pages} + +### `.Site.Pages` compared to `.Pages` + +{{< readfile file="/content/en/readfiles/pages-vs-site-pages.md" markdown="true" >}} + + + + +[config]: /getting-started/configuration/ diff --git a/content/en/variables/sitemap.md b/content/en/variables/sitemap.md new file mode 100644 index 000000000..dd926f2b3 --- /dev/null +++ b/content/en/variables/sitemap.md @@ -0,0 +1,32 @@ +--- +title: Sitemap Variables +linktitle: Sitemap Variables +description: +date: 2017-03-12 +publishdate: 2017-03-12 +lastmod: 2017-03-12 +categories: [variables and params] +keywords: [sitemap] +draft: false +menu: + docs: + parent: "variables" + weight: 80 +weight: 80 +sections_weight: 80 +aliases: [] +toc: false +--- + +A sitemap is a `Page` and therefore has all the [page variables][pagevars] available to use sitemap templates. They also have the following sitemap-specific variables available to them: + +.Sitemap.ChangeFreq +: the page change frequency + +.Sitemap.Priority +: the priority of the page + +.Sitemap.Filename +: the sitemap filename + +[pagevars]: /variables/page/ \ No newline at end of file diff --git a/content/en/variables/taxonomy.md b/content/en/variables/taxonomy.md new file mode 100644 index 000000000..5bcdffee5 --- /dev/null +++ b/content/en/variables/taxonomy.md @@ -0,0 +1,84 @@ +--- +title: Taxonomy Variables +linktitle: +description: Taxonomy pages are of type `Page` and have all page-, site-, and list-level variables available to them. However, taxonomy terms templates have additional variables available to their templates. +date: 2017-02-01 +publishdate: 2017-02-01 +lastmod: 2017-02-01 +categories: [variables and params] +keywords: [taxonomies,terms] +draft: false +menu: + docs: + parent: "variables" + weight: 30 +weight: 30 +sections_weight: 30 +aliases: [] +toc: true +--- + +## Taxonomy Terms Page Variables + +[Taxonomy terms pages][taxonomytemplates] are of the type `Page` and have the following additional variables. + +For example, the following fields would be available in `layouts/_defaults/terms.html`, depending on how you organize your [taxonomy templates][taxonomytemplates]: + +.Data.Singular +: The singular name of the taxonomy (e.g., `tags => tag`) + +.Data.Plural +: The plural name of the taxonomy (e.g., `tags => tags`) + +.Data.Pages +: The list of pages in the taxonomy + +.Data.Terms +: The taxonomy itself + +.Data.Terms.Alphabetical +: The taxonomy terms alphabetized + +.Data.Terms.ByCount +: The Terms ordered by popularity + +Note that `.Data.Terms.Alphabetical` and `.Data.Terms.ByCount` can also be reversed: + +* `.Data.Terms.Alphabetical.Reverse` +* `.Data.Terms.ByCount.Reverse` + +## Use `.Site.Taxonomies` Outside of Taxonomy Templates + +The `.Site.Taxonomies` variable holds all the taxonomies defined site-wide. `.Site.Taxonomies` is a map of the taxonomy name to a list of its values (e.g., `"tags" -> ["tag1", "tag2", "tag3"]`). Each value, though, is not a string but rather a *Taxonomy variable*. + +## The `.Taxonomy` Variable + +The `.Taxonomy` variable, available, for example, as `.Site.Taxonomies.tags`, contains the list of tags (values) and, for each tag, their corresponding content pages. + +### Example Usage of `.Site.Taxonomies` + +The following [partial template][partials] will list all your site's taxonomies, each of their keys, and all the content assigned to each of the keys. For more examples of how to order and render your taxonomies, see [Taxonomy Templates][taxonomytemplates]. + +{{< code file="all-taxonomies-keys-and-pages.html" download="all-taxonomies-keys-and-pages.html" >}} +
    +
      + {{ range $taxonomyname, $taxonomy := .Site.Taxonomies }} +
    • {{ $taxonomyname }} +
        + {{ range $key, $value := $taxonomy }} +
      • {{ $key }}
      • + + {{ end }} +
      +
    • + {{ end }} +
    +
    +{{< /code >}} + +[partials]: /templates/partials/ +[taxonomytemplates]: /templates/taxonomy-templates/ diff --git a/content/zh/_index.md b/content/zh/_index.md new file mode 100644 index 000000000..78f9ef15f --- /dev/null +++ b/content/zh/_index.md @@ -0,0 +1,49 @@ +--- +title: "世界上最快的网站构建框架 The world’s fastest framework for building websites" +date: 2017-03-02T12:00:00-05:00 +features: + - heading: Blistering Speed + image_path: /images/icon-fast.svg + tagline: What's modern about waiting for your site to build? + copy: Hugo is the fastest tool of its kind. At <1 ms per page, the average site builds in less than a second. + + - heading: Robust Content Management + image_path: /images/icon-content-management.svg + tagline: Flexibility rules. Hugo is a content strategist's dream. + copy: Hugo supports unlimited content types, taxonomies, menus, dynamic API-driven content, and more, all without plugins. + + - heading: Shortcodes + image_path: /images/icon-shortcodes.svg + tagline: Hugo's shortcodes are Markdown's hidden superpower. + copy: We love the beautiful simplicity of markdown’s syntax, but there are times when we want more flexibility. Hugo shortcodes allow for both beauty and flexibility. + + - heading: Built-in Templates + image_path: /images/icon-built-in-templates.svg + tagline: Hugo has common patterns to get your work done quickly. + copy: Hugo ships with pre-made templates to make quick work of SEO, commenting, analytics and other functions. One line of code, and you're done. + + - heading: Multilingual and i18n + image_path: /images/icon-multilingual2.svg + tagline: Polyglot baked in. + copy: Hugo provides full i18n support for multi-language sites with the same straightforward development experience Hugo users love in single-language sites. + + - heading: Custom Outputs + image_path: /images/icon-custom-outputs.svg + tagline: HTML not enough? + copy: Hugo allows you to output your content in multiple formats, including JSON or AMP, and makes it easy to create your own. +sections: + - heading: "100s of Themes" + cta: Check out the Hugo's themes. + link: http://themes.gohugo.io/ + color_classes: bg-accent-color white + image: /images/homepage-screenshot-hugo-themes.jpg + copy: "Hugo provides a robust theming system that is easy to implement but capable of producing even the most complicated websites." + - heading: "Capable Templating" + cta: Get Started. + link: templates/ + color_classes: bg-primary-color-light black + image: /images/home-page-templating-example.png + copy: "Hugo's Go-based templating provides just the right amount of logic to build anything from the simple to complex. If you prefer Jade/Pug-like syntax, you can also use Amber, Ace, or any combination of the three." +--- + +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/content/zh/about/_index.md b/content/zh/about/_index.md new file mode 100644 index 000000000..bf19807d9 --- /dev/null +++ b/content/zh/about/_index.md @@ -0,0 +1,20 @@ +--- +title: 关于 Hugo +linktitle: 概览 +description: Hugo 的特色、规划、许可和动力。 +date: 2018-04-26 +publishdate: 2018-04-26 +lastmod: 2018-04-26 +categories: [] +keywords: [] +menu: + docs: + parent: "about" + weight: 1 +weight: 1 +draft: false +aliases: [/about-hugo/,/docs/] +toc: false +--- + +Hugo 不是一般的静态网站生成器。 diff --git a/content/zh/content-management/_index.md b/content/zh/content-management/_index.md new file mode 100644 index 000000000..8c088dc57 --- /dev/null +++ b/content/zh/content-management/_index.md @@ -0,0 +1,21 @@ +--- +title: 内容管理 +linktitle: 内容管理概览 +description: Hugo 可以管理大型的静态网站,支持骨架、内容类型、菜单、引用、概要等等。 +date: 2018-04-23 +publishdate: 2018-04-23 +lastmod: 2018-04-23 +menu: + docs: + parent: "content-management" + weight: 1 +keywords: [source, organization] +categories: [content management] +weight: 01 #rem +draft: false +aliases: [/content/,/content/organization] +toc: false +isCJKLanguage: true +--- + +一个实用的静态网站生成器,需要超越“文件头” (front matter) 和模板的等基本功能,才能兼备可伸缩性和可管理性,满足用户所需。Hugo 不仅是给开发者设计的,也同样适用于内容管理员和写作人员。 diff --git a/content/zh/documentation.md b/content/zh/documentation.md new file mode 100644 index 000000000..1639bbcd2 --- /dev/null +++ b/content/zh/documentation.md @@ -0,0 +1,21 @@ +--- +title: Hugo 说明文档 +linktitle: Hugo +description: Hugo is the world's fastest static website engine. It's written in Go (aka Golang) and developed by bep, spf13 and friends. +date: 2017-02-01 +publishdate: 2017-02-01 +menu: + main: + parent: "section name" + weight: 01 +weight: 01 #rem +draft: false +slug: +aliases: [] +toc: false +layout: documentation-home +isCJKLanguage: true +--- +Hugo 号称**世界上最快的静态网站引擎**。它是以 Go (即 Golang) 编程语言所写成,并由 [bep](https://github.com/bep)、[spf13](https://github.com/spf13) 和[朋友们](https://github.com/gohugoio/hugo/graphs/contributors) 共同开发。 + +下面是我们说明文档中最常用和实用的章节: diff --git a/content/zh/news/_index.md b/content/zh/news/_index.md new file mode 100644 index 000000000..286d32e19 --- /dev/null +++ b/content/zh/news/_index.md @@ -0,0 +1,4 @@ +--- +title: "Hugo 新闻" +aliases: [/release-notes/] +--- diff --git a/content/zh/templates/_index.md b/content/zh/templates/_index.md new file mode 100644 index 000000000..3cd8df436 --- /dev/null +++ b/content/zh/templates/_index.md @@ -0,0 +1,19 @@ +--- +title: 模板 Templates +linktitle: 模板概览 +description: Go templating, template types and lookup order, shortcodes, and data. +date: 2017-02-01 +publishdate: 2017-02-01 +lastmod: 2017-02-01 +menu: + docs: + parent: "templates" + weight: 01 +weight: 01 #rem +categories: [templates] +keywords: [] +draft: false +aliases: [/templates/overview/,/templates/content] +toc: false +notesforauthors: +--- diff --git a/content/zh/templates/base.md b/content/zh/templates/base.md new file mode 100644 index 000000000..689a54408 --- /dev/null +++ b/content/zh/templates/base.md @@ -0,0 +1,132 @@ +--- +title: Base 模板 and Blocks +linktitle: +description: The base and block constructs allow you to define the outer shell of your master templates (i.e., the chrome of the page). +godocref: https://golang.org/pkg/text/template/#example_Template_block +date: 2017-02-01 +publishdate: 2018-08-11 +lastmod: 2017-02-01 +categories: [templates,fundamentals] +keywords: [blocks,base] +menu: + docs: + parent: "templates" + weight: 20 +weight: 20 +sections_weight: 20 +draft: false +aliases: [/templates/blocks/,/templates/base-templates-and-blocks/] +toc: true +--- + +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 [lookup order][lookup] for base templates is as follows: + +1. `/layouts/section/-baseof.html` +2. `/themes//layouts/section/-baseof.html` +3. `/layouts//baseof.html` +4. `/themes//layouts//baseof.html` +5. `/layouts/section/baseof.html` +6. `/themes//layouts/section/baseof.html` +7. `/layouts/_default/-baseof.html` +8. `/themes//layouts/_default/-baseof.html` +9. `/layouts/_default/baseof.html` +10. `/themes//layouts/_default/baseof.html` + +Variables are denoted by capitalized text set within `<>`. Note that Hugo's default behavior is for `type` to inherit from `section` unless otherwise specified. + +### Example Base Template Lookup Order + +As an example, let's assume your site is using a theme called "mytheme" when rendering the section list for a `posts` section. Hugo picks `layout/section/posts.html` as the template for [rendering the section][]. The `{{define}}` block in this template tells Hugo that the template is an extension of a base template. + +Here is the lookup order for the `posts` base template: + +1. `/layouts/section/posts-baseof.html` +2. `/themes/mytheme/layouts/section/posts-baseof.html` +3. `/layouts/posts/baseof.html` +4. `/themes/mytheme/layouts/posts/baseof.html` +5. `/layouts/section/baseof.html` +6. `/themes/mytheme/layouts/section/baseof.html` +7. `/layouts/_default/posts-baseof.html` +8. `/themes/mytheme/layouts/_default/posts-baseof.html` +9. `/layouts/_default/baseof.html` +10. `/themes/mytheme/layouts/_default/baseof.html` + +## 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. + +{{< code file="layouts/_default/baseof.html" download="baseof.html" >}} + + + + + {{ block "title" . }} + <!-- Blocks may include default content. --> + {{ .Site.Title }} + {{ end }} + + + + {{ block "main" . }} + + {{ end }} + {{ block "footer" . }} + + {{ end }} + + +{{< /code >}} + +## Override the Base Template + +From the above base template, you can define a [default list template][hugolists]. The default list template will inherit all of the code defined above and can then implement its own `"main"` block from: + +{{< code file="layouts/_default/list.html" download="list.html" >}} +{{ define "main" }} +

    Posts

    + {{ range .Pages }} +
    +

    {{ .Title }}

    + {{ .Content }} +
    + {{ end }} +{{ end }} +{{< /code >}} + +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 %}} +Code that you put outside the block definitions *can* break your layout. This even includes HTML comments. For example: + +``` + +{{ define "main" }} +...your code here +{{ end }} +``` +[See this thread from the Hugo discussion forums.](https://discourse.gohugo.io/t/baseof-html-block-templates-and-list-types-results-in-empty-pages/5612/6) +{{% /warning %}} + +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 page template][singletemplate]: + +{{< code file="layouts/_default/single.html" download="single.html" >}} +{{ define "title" }} + + {{ .Title }} – {{ .Site.Title }} +{{ end }} +{{ define "main" }} +

    {{ .Title }}

    + {{ .Content }} +{{ end }} +{{< /code >}} + +[hugolists]: /templates/lists +[lookup]: /templates/lookup-order/ +[rendering the section]: /templates/section-templates/ +[singletemplate]: /templates/single-page-templates/ diff --git a/create/content.go b/create/content.go deleted file mode 100644 index 0e05adf93..000000000 --- a/create/content.go +++ /dev/null @@ -1,349 +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 create provides functions to create new content. -package create - -import ( - "bytes" - - "github.com/pkg/errors" - - "io" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/gohugoio/hugo/hugofs/files" - - "github.com/gohugoio/hugo/hugofs" - - "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/hugolib" - "github.com/spf13/afero" - jww "github.com/spf13/jwalterweatherman" -) - -// NewContent creates a new content file in the content directory based upon the -// given kind, which is used to lookup an archetype. -func NewContent( - sites *hugolib.HugoSites, kind, targetPath string) error { - targetPath = filepath.Clean(targetPath) - ext := helpers.Ext(targetPath) - ps := sites.PathSpec - archetypeFs := ps.BaseFs.SourceFilesystems.Archetypes.Fs - sourceFs := ps.Fs.Source - - jww.INFO.Printf("attempting to create %q of %q of ext %q", targetPath, kind, ext) - - archetypeFilename, isDir := findArchetype(ps, kind, ext) - contentPath, s := resolveContentPath(sites, sourceFs, targetPath) - - if isDir { - - langFs, err := hugofs.NewLanguageFs(sites.LanguageSet(), archetypeFs) - if err != nil { - return err - } - - cm, err := mapArcheTypeDir(ps, langFs, archetypeFilename) - if err != nil { - return err - } - - if cm.siteUsed { - if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil { - return err - } - } - - name := filepath.Base(targetPath) - return newContentFromDir(archetypeFilename, sites, sourceFs, cm, name, contentPath) - } - - // Building the sites can be expensive, so only do it if really needed. - siteUsed := false - - if archetypeFilename != "" { - - var err error - siteUsed, err = usesSiteVar(archetypeFs, archetypeFilename) - if err != nil { - return err - } - } - - if siteUsed { - if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil { - return err - } - } - - content, err := executeArcheTypeAsTemplate(s, "", kind, targetPath, archetypeFilename) - if err != nil { - return err - } - - 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 -} - -func targetSite(sites *hugolib.HugoSites, fi hugofs.FileMetaInfo) *hugolib.Site { - for _, s := range sites.Sites { - if fi.Meta().Lang() == s.Language().Lang { - return s - } - } - return sites.Sites[0] -} - -func newContentFromDir( - archetypeDir string, - sites *hugolib.HugoSites, - targetFs afero.Fs, - cm archetypeMap, name, targetPath string) error { - - for _, f := range cm.otherFiles { - meta := f.Meta() - filename := meta.Path() - // Just copy the file to destination. - in, err := meta.Open() - if err != nil { - return errors.Wrap(err, "failed to open non-content file") - } - - targetFilename := filepath.Join(targetPath, strings.TrimPrefix(filename, archetypeDir)) - - targetDir := filepath.Dir(targetFilename) - if err := targetFs.MkdirAll(targetDir, 0777); err != nil && !os.IsExist(err) { - return errors.Wrapf(err, "failed to create target directory for %s:", targetDir) - } - - out, err := targetFs.Create(targetFilename) - if err != nil { - return err - } - - _, err = io.Copy(out, in) - if err != nil { - return err - } - - in.Close() - out.Close() - } - - for _, f := range cm.contentFiles { - filename := f.Meta().Path() - s := targetSite(sites, f) - targetFilename := filepath.Join(targetPath, strings.TrimPrefix(filename, archetypeDir)) - - content, err := executeArcheTypeAsTemplate(s, name, archetypeDir, targetFilename, filename) - if err != nil { - return errors.Wrap(err, "failed to execute archetype template") - } - - if err := helpers.SafeWriteToDisk(targetFilename, bytes.NewReader(content), targetFs); err != nil { - return errors.Wrap(err, "failed to save results") - } - } - - jww.FEEDBACK.Println(targetPath, "created") - - return 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 -} - -func mapArcheTypeDir( - ps *helpers.PathSpec, - fs afero.Fs, - archetypeDir string) (archetypeMap, error) { - - var m archetypeMap - - walkFn := func(path string, fi hugofs.FileMetaInfo, err error) error { - - if err != nil { - return err - } - - if fi.IsDir() { - return nil - } - - fil := fi.(hugofs.FileMetaInfo) - - if files.IsContentFile(path) { - m.contentFiles = append(m.contentFiles, fil) - if !m.siteUsed { - m.siteUsed, err = usesSiteVar(fs, path) - if err != nil { - return err - } - } - return nil - } - - m.otherFiles = append(m.otherFiles, fil) - - return nil - } - - walkCfg := hugofs.WalkwayConfig{ - WalkFn: walkFn, - Fs: fs, - Root: archetypeDir, - } - - w := hugofs.NewWalkway(walkCfg) - - if err := w.Walk(); err != nil { - return m, errors.Wrapf(err, "failed to walk archetype dir %q", archetypeDir) - } - - return m, nil -} - -func usesSiteVar(fs afero.Fs, filename string) (bool, error) { - f, err := fs.Open(filename) - if err != nil { - return false, errors.Wrap(err, "failed to open archetype file") - } - defer f.Close() - return helpers.ReaderContains(f, []byte(".Site")), nil -} - -// Resolve the target content path. -func resolveContentPath(sites *hugolib.HugoSites, fs afero.Fs, targetPath string) (string, *hugolib.Site) { - targetDir := filepath.Dir(targetPath) - first := sites.Sites[0] - - var ( - s *hugolib.Site - siteContentDir string - ) - - // Try the filename: my-post.en.md - for _, ss := range sites.Sites { - if strings.Contains(targetPath, "."+ss.Language().Lang+".") { - s = ss - break - } - } - - var dirLang string - - for _, dir := range sites.BaseFs.Content.Dirs { - meta := dir.Meta() - contentDir := meta.Filename() - - if !strings.HasSuffix(contentDir, helpers.FilePathSeparator) { - contentDir += helpers.FilePathSeparator - } - - if strings.HasPrefix(targetPath, contentDir) { - siteContentDir = contentDir - dirLang = meta.Lang() - break - } - } - - if s == nil && dirLang != "" { - for _, ss := range sites.Sites { - if ss.Lang() == dirLang { - s = ss - break - } - } - } - - if s == nil { - s = first - } - - if targetDir != "" && targetDir != "." { - exists, _ := helpers.Exists(targetDir, fs) - - if exists { - return targetPath, s - } - } - - if siteContentDir == "" { - - } - - if siteContentDir != "" { - pp := filepath.Join(siteContentDir, strings.TrimPrefix(targetPath, siteContentDir)) - return s.PathSpec.AbsPathify(pp), s - } else { - var contentDir string - for _, dir := range sites.BaseFs.Content.Dirs { - contentDir = dir.Meta().Filename() - if dir.Meta().Lang() == s.Lang() { - break - } - } - return s.PathSpec.AbsPathify(filepath.Join(contentDir, targetPath)), s - } - -} - -// FindArchetype takes a given kind/archetype of content and returns the path -// to the archetype in the archetype filesystem, blank if none found. -func findArchetype(ps *helpers.PathSpec, kind, ext string) (outpath string, isDir bool) { - fs := ps.BaseFs.Archetypes.Fs - - var pathsToCheck []string - - if kind != "" { - pathsToCheck = append(pathsToCheck, kind+ext) - } - pathsToCheck = append(pathsToCheck, "default"+ext, "default") - - for _, p := range pathsToCheck { - fi, err := fs.Stat(p) - if err == nil { - return p, fi.IsDir() - } - } - - return "", false -} diff --git a/create/content_template_handler.go b/create/content_template_handler.go deleted file mode 100644 index 1576fabdb..000000000 --- a/create/content_template_handler.go +++ /dev/null @@ -1,149 +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 create - -import ( - "bytes" - "fmt" - "path/filepath" - "strings" - "time" - - "github.com/pkg/errors" - - "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.SiteInfo - - // Name will in most cases be the same as TranslationBaseName, e.g. "my-post". - // But if that value is "index" (bundles), the Name is instead the owning folder. - // This is the value you in most cases would want to use to construct the title in your - // archetype template. - Name string - - // The target content file. Note that the .Content will be empty, as that - // has not been created yet. - source.File -} - -const ( - // ArchetypeTemplateTemplate is used as initial template when adding an archetype template. - ArchetypeTemplateTemplate = `--- -title: "{{ replace .Name "-" " " | 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, name, kind, targetPath, archetypeFilename string) ([]byte, error) { - - var ( - archetypeContent []byte - archetypeTemplate []byte - err error - ) - - f, err := s.SourceSpec.NewFileInfoFrom(targetPath, targetPath) - if err != nil { - return nil, err - } - - if name == "" { - name = f.TranslationBaseName() - - if name == "index" || name == "_index" { - // Page bundles; the directory name will hopefully have a better name. - dir := strings.TrimSuffix(f.Dir(), helpers.FilePathSeparator) - _, name = filepath.Split(dir) - } - } - - data := ArchetypeFileData{ - Type: kind, - Date: time.Now().Format(time.RFC3339), - Name: name, - File: f, - Site: &s.Info, - } - - if archetypeFilename == "" { - // TODO(bep) archetype revive the issue about wrong tpl funcs arg order - archetypeTemplate = []byte(ArchetypeTemplateTemplate) - } else { - archetypeTemplate, err = afero.ReadFile(s.BaseFs.Archetypes.Fs, archetypeFilename) - if err != nil { - return nil, fmt.Errorf("failed to read archetype file %s", 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, errors.Wrapf(err, "Failed to parse archetype file %q:", archetypeFilename) - } - - templ, _ := templateHandler.Lookup(templateName) - - var buff bytes.Buffer - if err := templ.Execute(&buff, data); err != nil { - return nil, errors.Wrapf(err, "Failed to process archetype file %q:", archetypeFilename) - } - - archetypeContent = []byte(archetypeShortcodeReplacementsPost.Replace(buff.String())) - - return archetypeContent, nil - -} diff --git a/create/content_test.go b/create/content_test.go deleted file mode 100644 index f43d3a5f4..000000000 --- a/create/content_test.go +++ /dev/null @@ -1,285 +0,0 @@ -// 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 create_test - -import ( - "os" - "path/filepath" - "strings" - "testing" - - "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" -) - -func TestNewContent(t *testing.T) { - - cases := []struct { - kind string - path string - expected []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 - {"lang", "post/lang-1.md", []string{`Site Lang: en|Name: Lang 1|i18n: Hugo Rocks!`}}, - {"lang", "post/lang-2.en.md", []string{`Site Lang: en|Name: Lang 2|i18n: Hugo Rocks!`}}, - {"lang", "content/post/lang-3.nn.md", []string{`Site Lang: nn|Name: Lang 3|i18n: Hugo Rokkar!`}}, - {"lang", "content_nn/post/lang-4.md", []string{`Site Lang: nn|Name: Lang 4|i18n: Hugo Rokkar!`}}, - {"lang", "content_nn/post/lang-5.en.md", []string{`Site Lang: en|Name: Lang 5|i18n: Hugo Rocks!`}}, - {"lang", "post/my-bundle/index.md", []string{`Site Lang: en|Name: My Bundle|i18n: Hugo Rocks!`}}, - {"lang", "post/my-bundle/index.en.md", []string{`Site Lang: en|Name: My Bundle|i18n: Hugo Rocks!`}}, - {"lang", "content/post/my-bundle/index.nn.md", []string{`Site Lang: nn|Name: My Bundle|i18n: Hugo Rokkar!`}}, - {"shortcodes", "shortcodes/go.md", []string{ - `title = "GO"`, - "{{< myshortcode >}}", - "{{% myshortcode %}}", - "{{}}\n{{%/* comment */%}}"}}, // shortcodes - } - - for i, cas := range cases { - cas := cas - t.Run(fmt.Sprintf("%s-%d", cas.kind, i), func(t *testing.T) { - t.Parallel() - c := qt.New(t) - mm := afero.NewMemMapFs() - c.Assert(initFs(mm), qt.IsNil) - cfg, fs := newTestCfg(c, mm) - h, err := hugolib.NewHugoSites(deps.DepsCfg{Cfg: cfg, Fs: fs}) - c.Assert(err, qt.IsNil) - - c.Assert(create.NewContent(h, cas.kind, cas.path), qt.IsNil) - - fname := filepath.FromSlash(cas.path) - if !strings.HasPrefix(fname, "content") { - fname = filepath.Join("content", fname) - } - content := readFileFromFs(t, fs.Source, fname) - for _, v := range cas.expected { - found := strings.Contains(content, v) - if !found { - t.Fatalf("[%d] %q missing from output:\n%q", i, v, content) - } - } - }) - - } -} - -func TestNewContentFromDir(t *testing.T) { - mm := afero.NewMemMapFs() - c := qt.New(t) - - archetypeDir := filepath.Join("archetypes", "my-bundle") - c.Assert(mm.MkdirAll(archetypeDir, 0755), qt.IsNil) - - archetypeThemeDir := filepath.Join("themes", "mytheme", "archetypes", "my-theme-bundle") - c.Assert(mm.MkdirAll(archetypeThemeDir, 0755), qt.IsNil) - - contentFile := ` -File: %s -Site Lang: {{ .Site.Language.Lang }} -Name: {{ replace .Name "-" " " | title }} -i18n: {{ T "hugo" }} -` - - c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "index.md"), []byte(fmt.Sprintf(contentFile, "index.md")), 0755), qt.IsNil) - c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "index.nn.md"), []byte(fmt.Sprintf(contentFile, "index.nn.md")), 0755), qt.IsNil) - - c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "pages", "bio.md"), []byte(fmt.Sprintf(contentFile, "bio.md")), 0755), qt.IsNil) - c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "resources", "hugo1.json"), []byte(`hugo1: {{ printf "no template handling in here" }}`), 0755), qt.IsNil) - c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "resources", "hugo2.xml"), []byte(`hugo2: {{ printf "no template handling in here" }}`), 0755), qt.IsNil) - - c.Assert(afero.WriteFile(mm, filepath.Join(archetypeThemeDir, "index.md"), []byte(fmt.Sprintf(contentFile, "index.md")), 0755), qt.IsNil) - c.Assert(afero.WriteFile(mm, filepath.Join(archetypeThemeDir, "resources", "hugo1.json"), []byte(`hugo1: {{ printf "no template handling in here" }}`), 0755), qt.IsNil) - - c.Assert(initFs(mm), qt.IsNil) - cfg, fs := newTestCfg(c, mm) - - h, err := hugolib.NewHugoSites(deps.DepsCfg{Cfg: cfg, 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"), qt.IsNil) - - cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/resources/hugo1.json")), `hugo1: {{ printf "no template handling in here" }}`) - cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/resources/hugo2.xml")), `hugo2: {{ printf "no template handling in here" }}`) - - // Content files should get the correct site context. - // TODO(bep) archetype check i18n - cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/index.md")), `File: index.md`, `Site Lang: en`, `Name: My Post`, `i18n: Hugo Rocks!`) - cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/index.nn.md")), `File: index.nn.md`, `Site Lang: nn`, `Name: My Post`, `i18n: Hugo Rokkar!`) - - cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/pages/bio.md")), `File: bio.md`, `Site Lang: en`, `Name: My Post`) - - c.Assert(create.NewContent(h, "my-theme-bundle", "post/my-theme-post"), qt.IsNil) - cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-theme-post/index.md")), `File: index.md`, `Site Lang: en`, `Name: My Theme Post`, `i18n: Hugo Rocks!`) - cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-theme-post/resources/hugo1.json")), `hugo1: {{ printf "no template handling in here" }}`) - -} - -func initFs(fs afero.Fs) error { - perm := os.FileMode(0755) - var err error - - // create directories - dirs := []string{ - "archetypes", - "content", - filepath.Join("themes", "sample", "archetypes"), - } - for _, dir := range dirs { - err = fs.Mkdir(dir, perm) - if err != nil && !os.IsExist(err) { - return err - } - } - - // create files - for _, v := range []struct { - path string - content string - }{ - { - path: filepath.Join("archetypes", "post.md"), - content: "+++\ndate = \"2015-01-12T19:20:04-07:00\"\ntitle = \"Post Arch title\"\ntest = \"test1\"\n+++\n", - }, - { - path: filepath.Join("archetypes", "post.org"), - content: "#+title: {{ .BaseFileName | upper }}", - }, - { - path: filepath.Join("archetypes", "product.md"), - content: `+++ -title = "{{ .BaseFileName | upper }}" -+++`, - }, - { - 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"), - content: `+++ -title = "{{ .BaseFileName | upper }}" -+++ - -{{< myshortcode >}} - -Some text. - -{{% myshortcode %}} -{{}} -{{%/* comment */%}} - - -`, - }, - } { - f, err := fs.Create(v.path) - if err != nil { - return err - } - defer f.Close() - - _, err = f.Write([]byte(v.content)) - if err != nil { - return err - } - } - - return nil -} - -func cContains(c *qt.C, v interface{}, 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.T, fs afero.Fs, filename string) string { - t.Helper() - filename = filepath.FromSlash(filename) - b, err := afero.ReadFile(fs, filename) - if err != nil { - // Print some debug info - 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) - } - - return nil - }) - t.Fatalf("Failed to read file: %s", err) - } - return string(b) -} - -func newTestCfg(c *qt.C, mm afero.Fs) (*viper.Viper, *hugofs.Fs) { - - cfg := ` - -theme = "mytheme" -[languages] -[languages.en] -weight = 1 -languageName = "English" -[languages.nn] -weight = 2 -languageName = "Nynorsk" -contentDir = "content_nn" - -` - if mm == nil { - mm = afero.NewMemMapFs() - } - - mm.MkdirAll(filepath.FromSlash("content_nn"), 0777) - - mm.MkdirAll(filepath.FromSlash("themes/mytheme"), 0777) - - c.Assert(afero.WriteFile(mm, filepath.Join("i18n", "en.toml"), []byte(`[hugo] -other = "Hugo Rocks!"`), 0755), qt.IsNil) - c.Assert(afero.WriteFile(mm, filepath.Join("i18n", "nn.toml"), []byte(`[hugo] -other = "Hugo Rokkar!"`), 0755), qt.IsNil) - - c.Assert(afero.WriteFile(mm, "config.toml", []byte(cfg), 0755), qt.IsNil) - - v, _, err := hugolib.LoadConfig(hugolib.ConfigSourceDescriptor{Fs: mm, Filename: "config.toml"}) - c.Assert(err, qt.IsNil) - - return v, hugofs.NewFrom(mm, v) - -} diff --git a/data/articles.toml b/data/articles.toml new file mode 100644 index 000000000..109810803 --- /dev/null +++ b/data/articles.toml @@ -0,0 +1,731 @@ +[[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 Friggin' 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/data/docs.json b/data/docs.json new file mode 100644 index 000000000..1c09a788e --- /dev/null +++ b/data/docs.json @@ -0,0 +1,4441 @@ +{ + "chroma": { + "lexers": [ + { + "Name": "ABNF", + "Aliases": [ + "abnf" + ] + }, + { + "Name": "ANTLR", + "Aliases": [ + "antlr" + ] + }, + { + "Name": "APL", + "Aliases": [ + "apl" + ] + }, + { + "Name": "ActionScript", + "Aliases": [ + "actionscript", + "as" + ] + }, + { + "Name": "ActionScript 3", + "Aliases": [ + "actionscript3", + "as", + "as3" + ] + }, + { + "Name": "Ada", + "Aliases": [ + "ada", + "ada2005", + "ada95", + "adb", + "ads" + ] + }, + { + "Name": "Angular2", + "Aliases": [ + "ng2" + ] + }, + { + "Name": "ApacheConf", + "Aliases": [ + "aconf", + "apache", + "apacheconf", + "conf", + "htaccess" + ] + }, + { + "Name": "AppleScript", + "Aliases": [ + "applescript" + ] + }, + { + "Name": "Arduino", + "Aliases": [ + "arduino", + "ino" + ] + }, + { + "Name": "Awk", + "Aliases": [ + "awk", + "gawk", + "mawk", + "nawk" + ] + }, + { + "Name": "BNF", + "Aliases": [ + "bnf" + ] + }, + { + "Name": "Ballerina", + "Aliases": [ + "bal", + "ballerina" + ] + }, + { + "Name": "Base Makefile", + "Aliases": [ + "*", + "bsdmake", + "mak", + "make", + "makefile", + "mf", + "mk" + ] + }, + { + "Name": "Bash", + "Aliases": [ + "bash", + "bash_*", + "bashrc", + "ebuild", + "eclass", + "exheres-0", + "exlib", + "ksh", + "sh", + "shell", + "zsh", + "zshrc" + ] + }, + { + "Name": "Batchfile", + "Aliases": [ + "bat", + "batch", + "cmd", + "dosbatch", + "winbatch" + ] + }, + { + "Name": "BlitzBasic", + "Aliases": [ + "b3d", + "bb", + "blitzbasic", + "bplus", + "decls" + ] + }, + { + "Name": "Brainfuck", + "Aliases": [ + "b", + "bf", + "brainfuck" + ] + }, + { + "Name": "C", + "Aliases": [ + "c", + "h", + "idc" + ] + }, + { + "Name": "C#", + "Aliases": [ + "c#", + "cs", + "csharp" + ] + }, + { + "Name": "C++", + "Aliases": [ + "C", + "CPP", + "H", + "c++", + "cc", + "cp", + "cpp", + "cxx", + "h++", + "hh", + "hpp", + "hxx" + ] + }, + { + "Name": "CFEngine3", + "Aliases": [ + "cf", + "cf3", + "cfengine3" + ] + }, + { + "Name": "CMake", + "Aliases": [ + "cmake", + "txt" + ] + }, + { + "Name": "COBOL", + "Aliases": [ + "COB", + "CPY", + "cob", + "cobol", + "cpy" + ] + }, + { + "Name": "CSS", + "Aliases": [ + "css" + ] + }, + { + "Name": "Cap'n Proto", + "Aliases": [ + "capnp" + ] + }, + { + "Name": "Cassandra CQL", + "Aliases": [ + "cassandra", + "cql" + ] + }, + { + "Name": "Ceylon", + "Aliases": [ + "ceylon" + ] + }, + { + "Name": "ChaiScript", + "Aliases": [ + "chai", + "chaiscript" + ] + }, + { + "Name": "Cheetah", + "Aliases": [ + "cheetah", + "spitfire", + "spt", + "tmpl" + ] + }, + { + "Name": "Clojure", + "Aliases": [ + "clj", + "clojure" + ] + }, + { + "Name": "CoffeeScript", + "Aliases": [ + "coffee", + "coffee-script", + "coffeescript" + ] + }, + { + "Name": "Common Lisp", + "Aliases": [ + "cl", + "common-lisp", + "lisp" + ] + }, + { + "Name": "Coq", + "Aliases": [ + "coq", + "v" + ] + }, + { + "Name": "Crystal", + "Aliases": [ + "cr", + "crystal" + ] + }, + { + "Name": "Cython", + "Aliases": [ + "cython", + "pxd", + "pxi", + "pyrex", + "pyx" + ] + }, + { + "Name": "D", + "Aliases": [ + "d", + "di" + ] + }, + { + "Name": "DTD", + "Aliases": [ + "dtd" + ] + }, + { + "Name": "Dart", + "Aliases": [ + "dart" + ] + }, + { + "Name": "Diff", + "Aliases": [ + "diff", + "patch", + "udiff" + ] + }, + { + "Name": "Django/Jinja", + "Aliases": [ + "django", + "jinja" + ] + }, + { + "Name": "Docker", + "Aliases": [ + "docker", + "dockerfile" + ] + }, + { + "Name": "EBNF", + "Aliases": [ + "ebnf" + ] + }, + { + "Name": "Elixir", + "Aliases": [ + "elixir", + "ex", + "exs" + ] + }, + { + "Name": "Elm", + "Aliases": [ + "elm" + ] + }, + { + "Name": "EmacsLisp", + "Aliases": [ + "el", + "elisp", + "emacs", + "emacs-lisp" + ] + }, + { + "Name": "Erlang", + "Aliases": [ + "erl", + "erlang", + "es", + "escript", + "hrl" + ] + }, + { + "Name": "FSharp", + "Aliases": [ + "fs", + "fsharp", + "fsi" + ] + }, + { + "Name": "Factor", + "Aliases": [ + "factor" + ] + }, + { + "Name": "Fish", + "Aliases": [ + "fish", + "fishshell", + "load" + ] + }, + { + "Name": "Forth", + "Aliases": [ + "forth", + "frt", + "fs" + ] + }, + { + "Name": "Fortran", + "Aliases": [ + "F03", + "F90", + "f03", + "f90", + "fortran" + ] + }, + { + "Name": "GAS", + "Aliases": [ + "S", + "asm", + "gas", + "s" + ] + }, + { + "Name": "GDScript", + "Aliases": [ + "gd", + "gdscript" + ] + }, + { + "Name": "GLSL", + "Aliases": [ + "frag", + "geo", + "glsl", + "vert" + ] + }, + { + "Name": "Genshi", + "Aliases": [ + "genshi", + "kid", + "xml+genshi", + "xml+kid" + ] + }, + { + "Name": "Genshi HTML", + "Aliases": [ + "html+genshi", + "html+kid" + ] + }, + { + "Name": "Genshi Text", + "Aliases": [ + "genshitext" + ] + }, + { + "Name": "Gnuplot", + "Aliases": [ + "gnuplot", + "plot", + "plt" + ] + }, + { + "Name": "Go", + "Aliases": [ + "go", + "golang" + ] + }, + { + "Name": "Go HTML Template", + "Aliases": [ + "go-html-template" + ] + }, + { + "Name": "Go Text Template", + "Aliases": [ + "go-text-template" + ] + }, + { + "Name": "GraphQL", + "Aliases": [ + "gql", + "graphql", + "graphqls" + ] + }, + { + "Name": "Groovy", + "Aliases": [ + "gradle", + "groovy" + ] + }, + { + "Name": "HCL", + "Aliases": [ + "hcl" + ] + }, + { + "Name": "HTML", + "Aliases": [ + "htm", + "html", + "xhtml", + "xslt" + ] + }, + { + "Name": "HTTP", + "Aliases": [ + "http" + ] + }, + { + "Name": "Handlebars", + "Aliases": [ + "handlebars" + ] + }, + { + "Name": "Haskell", + "Aliases": [ + "haskell", + "hs" + ] + }, + { + "Name": "Haxe", + "Aliases": [ + "haxe", + "hx", + "hxsl" + ] + }, + { + "Name": "Hexdump", + "Aliases": [ + "hexdump" + ] + }, + { + "Name": "Hy", + "Aliases": [ + "hy", + "hylang" + ] + }, + { + "Name": "INI", + "Aliases": [ + "cfg", + "dosini", + "gitconfig", + "inf", + "ini" + ] + }, + { + "Name": "Idris", + "Aliases": [ + "idr", + "idris" + ] + }, + { + "Name": "Io", + "Aliases": [ + "io" + ] + }, + { + "Name": "JSON", + "Aliases": [ + "json" + ] + }, + { + "Name": "Java", + "Aliases": [ + "java" + ] + }, + { + "Name": "JavaScript", + "Aliases": [ + "javascript", + "js", + "jsm" + ] + }, + { + "Name": "Julia", + "Aliases": [ + "jl", + "julia" + ] + }, + { + "Name": "Jungle", + "Aliases": [ + "jungle" + ] + }, + { + "Name": "Kotlin", + "Aliases": [ + "kotlin", + "kt" + ] + }, + { + "Name": "LLVM", + "Aliases": [ + "ll", + "llvm" + ] + }, + { + "Name": "Lighttpd configuration file", + "Aliases": [ + "lighttpd", + "lighty" + ] + }, + { + "Name": "Lua", + "Aliases": [ + "lua", + "wlua" + ] + }, + { + "Name": "Mako", + "Aliases": [ + "mako", + "mao" + ] + }, + { + "Name": "Mason", + "Aliases": [ + "m", + "mason", + "mc", + "mhtml", + "mi" + ] + }, + { + "Name": "Mathematica", + "Aliases": [ + "cdf", + "ma", + "mathematica", + "mma", + "nb", + "nbp" + ] + }, + { + "Name": "Matlab", + "Aliases": [ + "m", + "matlab" + ] + }, + { + "Name": "MiniZinc", + "Aliases": [ + "MZN", + "dzn", + "fzn", + "minizinc", + "mzn" + ] + }, + { + "Name": "Modula-2", + "Aliases": [ + "def", + "m2", + "mod", + "modula2" + ] + }, + { + "Name": "MonkeyC", + "Aliases": [ + "mc", + "monkeyc" + ] + }, + { + "Name": "MorrowindScript", + "Aliases": [ + "morrowind", + "mwscript" + ] + }, + { + "Name": "MySQL", + "Aliases": [ + "mysql", + "sql" + ] + }, + { + "Name": "Myghty", + "Aliases": [ + "myghty", + "myt" + ] + }, + { + "Name": "NASM", + "Aliases": [ + "ASM", + "asm", + "nasm" + ] + }, + { + "Name": "Newspeak", + "Aliases": [ + "newspeak", + "ns2" + ] + }, + { + "Name": "Nginx configuration file", + "Aliases": [ + "conf", + "nginx" + ] + }, + { + "Name": "Nim", + "Aliases": [ + "nim", + "nimrod" + ] + }, + { + "Name": "Nix", + "Aliases": [ + "nix", + "nixos" + ] + }, + { + "Name": "OCaml", + "Aliases": [ + "ml", + "mli", + "mll", + "mly", + "ocaml" + ] + }, + { + "Name": "Objective-C", + "Aliases": [ + "h", + "m", + "obj-c", + "objc", + "objective-c", + "objectivec" + ] + }, + { + "Name": "Octave", + "Aliases": [ + "m", + "octave" + ] + }, + { + "Name": "OpenSCAD", + "Aliases": [ + "openscad", + "scad" + ] + }, + { + "Name": "Org Mode", + "Aliases": [ + "org", + "orgmode" + ] + }, + { + "Name": "PHP", + "Aliases": [ + "inc", + "php", + "php3", + "php4", + "php5", + "php[345]" + ] + }, + { + "Name": "PL/pgSQL", + "Aliases": [ + "plpgsql" + ] + }, + { + "Name": "POVRay", + "Aliases": [ + "inc", + "pov" + ] + }, + { + "Name": "PacmanConf", + "Aliases": [ + "conf", + "pacmanconf" + ] + }, + { + "Name": "Perl", + "Aliases": [ + "perl", + "pl", + "pm", + "t" + ] + }, + { + "Name": "Pig", + "Aliases": [ + "pig" + ] + }, + { + "Name": "PkgConfig", + "Aliases": [ + "pc", + "pkgconfig" + ] + }, + { + "Name": "PostScript", + "Aliases": [ + "eps", + "postscr", + "postscript", + "ps" + ] + }, + { + "Name": "PostgreSQL SQL dialect", + "Aliases": [ + "postgres", + "postgresql" + ] + }, + { + "Name": "PowerShell", + "Aliases": [ + "posh", + "powershell", + "ps1", + "psm1" + ] + }, + { + "Name": "Prolog", + "Aliases": [ + "ecl", + "pl", + "pro", + "prolog" + ] + }, + { + "Name": "Protocol Buffer", + "Aliases": [ + "proto", + "protobuf" + ] + }, + { + "Name": "Puppet", + "Aliases": [ + "pp", + "puppet" + ] + }, + { + "Name": "Python", + "Aliases": [ + "py", + "python", + "pyw", + "sage", + "sc", + "tac" + ] + }, + { + "Name": "Python 3", + "Aliases": [ + "py3", + "python3" + ] + }, + { + "Name": "QBasic", + "Aliases": [ + "BAS", + "bas", + "basic", + "qbasic" + ] + }, + { + "Name": "R", + "Aliases": [ + "R", + "Renviron", + "Rhistory", + "Rprofile", + "S", + "r", + "s", + "splus" + ] + }, + { + "Name": "Racket", + "Aliases": [ + "racket", + "rkt", + "rktd", + "rktl" + ] + }, + { + "Name": "Ragel", + "Aliases": [ + "ragel" + ] + }, + { + "Name": "Rexx", + "Aliases": [ + "arexx", + "rex", + "rexx", + "rx" + ] + }, + { + "Name": "Ruby", + "Aliases": [ + "duby", + "gemspec", + "rake", + "rb", + "rbw", + "rbx", + "ruby" + ] + }, + { + "Name": "Rust", + "Aliases": [ + "in", + "rs", + "rust" + ] + }, + { + "Name": "SCSS", + "Aliases": [ + "scss" + ] + }, + { + "Name": "SPARQL", + "Aliases": [ + "rq", + "sparql" + ] + }, + { + "Name": "SQL", + "Aliases": [ + "sql" + ] + }, + { + "Name": "SYSTEMD", + "Aliases": [ + "service", + "systemd" + ] + }, + { + "Name": "Sass", + "Aliases": [ + "sass" + ] + }, + { + "Name": "Scala", + "Aliases": [ + "scala" + ] + }, + { + "Name": "Scheme", + "Aliases": [ + "scheme", + "scm", + "ss" + ] + }, + { + "Name": "Scilab", + "Aliases": [ + "sce", + "sci", + "scilab", + "tst" + ] + }, + { + "Name": "Smalltalk", + "Aliases": [ + "smalltalk", + "squeak", + "st" + ] + }, + { + "Name": "Smarty", + "Aliases": [ + "smarty", + "tpl" + ] + }, + { + "Name": "Snobol", + "Aliases": [ + "snobol" + ] + }, + { + "Name": "Solidity", + "Aliases": [ + "sol", + "solidity" + ] + }, + { + "Name": "SquidConf", + "Aliases": [ + "conf", + "squid", + "squid.conf", + "squidconf" + ] + }, + { + "Name": "Swift", + "Aliases": [ + "swift" + ] + }, + { + "Name": "TASM", + "Aliases": [ + "ASM", + "asm", + "tasm" + ] + }, + { + "Name": "TOML", + "Aliases": [ + "toml" + ] + }, + { + "Name": "Tcl", + "Aliases": [ + "rvt", + "tcl" + ] + }, + { + "Name": "Tcsh", + "Aliases": [ + "csh", + "tcsh" + ] + }, + { + "Name": "TeX", + "Aliases": [ + "aux", + "latex", + "tex", + "toc" + ] + }, + { + "Name": "Termcap", + "Aliases": [ + "src", + "termcap" + ] + }, + { + "Name": "Terminfo", + "Aliases": [ + "src", + "terminfo" + ] + }, + { + "Name": "Terraform", + "Aliases": [ + "terraform", + "tf" + ] + }, + { + "Name": "Thrift", + "Aliases": [ + "thrift" + ] + }, + { + "Name": "TradingView", + "Aliases": [ + "tradingview", + "tv" + ] + }, + { + "Name": "Transact-SQL", + "Aliases": [ + "t-sql", + "tsql" + ] + }, + { + "Name": "Turing", + "Aliases": [ + "tu", + "turing" + ] + }, + { + "Name": "Turtle", + "Aliases": [ + "ttl", + "turtle" + ] + }, + { + "Name": "Twig", + "Aliases": [ + "twig" + ] + }, + { + "Name": "TypeScript", + "Aliases": [ + "ts", + "tsx", + "typescript" + ] + }, + { + "Name": "TypoScript", + "Aliases": [ + "ts", + "txt", + "typoscript" + ] + }, + { + "Name": "TypoScriptCssData", + "Aliases": [ + "typoscriptcssdata" + ] + }, + { + "Name": "TypoScriptHtmlData", + "Aliases": [ + "typoscripthtmldata" + ] + }, + { + "Name": "VB.net", + "Aliases": [ + "bas", + "vb", + "vb.net", + "vbnet" + ] + }, + { + "Name": "VHDL", + "Aliases": [ + "vhd", + "vhdl" + ] + }, + { + "Name": "VimL", + "Aliases": [ + "exrc", + "gvimrc", + "vim", + "vimrc" + ] + }, + { + "Name": "WDTE", + "Aliases": [ + "wdte" + ] + }, + { + "Name": "XML", + "Aliases": [ + "rss", + "svg", + "wsdl", + "wsf", + "xml", + "xsd", + "xsl", + "xslt" + ] + }, + { + "Name": "Xorg", + "Aliases": [ + "conf", + "xorg.conf" + ] + }, + { + "Name": "YAML", + "Aliases": [ + "yaml", + "yml" + ] + }, + { + "Name": "cfstatement", + "Aliases": [ + "cfs" + ] + }, + { + "Name": "markdown", + "Aliases": [ + "markdown", + "md", + "mkd" + ] + }, + { + "Name": "plaintext", + "Aliases": [ + "no-highlight", + "plain", + "text", + "txt" + ] + }, + { + "Name": "reStructuredText", + "Aliases": [ + "rest", + "restructuredtext", + "rst" + ] + }, + { + "Name": "react", + "Aliases": [ + "jsx", + "react" + ] + }, + { + "Name": "reg", + "Aliases": [ + "reg", + "registry" + ] + }, + { + "Name": "systemverilog", + "Aliases": [ + "sv", + "svh", + "systemverilog" + ] + }, + { + "Name": "verilog", + "Aliases": [ + "v", + "verilog" + ] + }, + { + "Name": "vue", + "Aliases": [ + "vue", + "vuejs" + ] + } + ] + }, + "media": { + "types": [ + { + "type": "application/javascript", + "string": "application/javascript", + "mainType": "application", + "subType": "javascript", + "delimiter": ".", + "suffixes": [ + "js" + ] + }, + { + "type": "application/json", + "string": "application/json", + "mainType": "application", + "subType": "json", + "delimiter": ".", + "suffixes": [ + "json" + ] + }, + { + "type": "application/octet-stream", + "string": "application/octet-stream", + "mainType": "application", + "subType": "octet-stream", + "delimiter": "", + "suffixes": null + }, + { + "type": "application/rss+xml", + "string": "application/rss+xml", + "mainType": "application", + "subType": "rss", + "delimiter": ".", + "suffixes": [ + "xml" + ] + }, + { + "type": "application/toml", + "string": "application/toml", + "mainType": "application", + "subType": "toml", + "delimiter": ".", + "suffixes": [ + "toml" + ] + }, + { + "type": "application/xml", + "string": "application/xml", + "mainType": "application", + "subType": "xml", + "delimiter": ".", + "suffixes": [ + "xml" + ] + }, + { + "type": "application/yaml", + "string": "application/yaml", + "mainType": "application", + "subType": "yaml", + "delimiter": ".", + "suffixes": [ + "yaml", + "yml" + ] + }, + { + "type": "image/jpg", + "string": "image/jpg", + "mainType": "image", + "subType": "jpg", + "delimiter": ".", + "suffixes": [ + "jpg", + "jpeg" + ] + }, + { + "type": "image/png", + "string": "image/png", + "mainType": "image", + "subType": "png", + "delimiter": ".", + "suffixes": [ + "png" + ] + }, + { + "type": "image/svg+xml", + "string": "image/svg+xml", + "mainType": "image", + "subType": "svg", + "delimiter": ".", + "suffixes": [ + "svg" + ] + }, + { + "type": "text/calendar", + "string": "text/calendar", + "mainType": "text", + "subType": "calendar", + "delimiter": ".", + "suffixes": [ + "ics" + ] + }, + { + "type": "text/css", + "string": "text/css", + "mainType": "text", + "subType": "css", + "delimiter": ".", + "suffixes": [ + "css" + ] + }, + { + "type": "text/csv", + "string": "text/csv", + "mainType": "text", + "subType": "csv", + "delimiter": ".", + "suffixes": [ + "csv" + ] + }, + { + "type": "text/html", + "string": "text/html", + "mainType": "text", + "subType": "html", + "delimiter": ".", + "suffixes": [ + "html" + ] + }, + { + "type": "text/plain", + "string": "text/plain", + "mainType": "text", + "subType": "plain", + "delimiter": ".", + "suffixes": [ + "txt" + ] + }, + { + "type": "text/x-sass", + "string": "text/x-sass", + "mainType": "text", + "subType": "x-sass", + "delimiter": ".", + "suffixes": [ + "sass" + ] + }, + { + "type": "text/x-scss", + "string": "text/x-scss", + "mainType": "text", + "subType": "x-scss", + "delimiter": ".", + "suffixes": [ + "scss" + ] + } + ] + }, + "output": { + "formats": [ + { + "MediaType": "text/html", + "name": "HTML", + "mediaType": { + "type": "text/html", + "string": "text/html", + "mainType": "text", + "subType": "html", + "delimiter": ".", + "suffixes": [ + "html" + ] + }, + "path": "", + "baseName": "index", + "rel": "canonical", + "protocol": "", + "isPlainText": false, + "isHTML": true, + "noUgly": false, + "notAlternative": false, + "permalinkable": true, + "weight": 10 + }, + { + "MediaType": "text/html", + "name": "AMP", + "mediaType": { + "type": "text/html", + "string": "text/html", + "mainType": "text", + "subType": "html", + "delimiter": ".", + "suffixes": [ + "html" + ] + }, + "path": "amp", + "baseName": "index", + "rel": "amphtml", + "protocol": "", + "isPlainText": false, + "isHTML": true, + "noUgly": false, + "notAlternative": false, + "permalinkable": true, + "weight": 0 + }, + { + "MediaType": "text/css", + "name": "CSS", + "mediaType": { + "type": "text/css", + "string": "text/css", + "mainType": "text", + "subType": "css", + "delimiter": ".", + "suffixes": [ + "css" + ] + }, + "path": "", + "baseName": "styles", + "rel": "stylesheet", + "protocol": "", + "isPlainText": true, + "isHTML": false, + "noUgly": false, + "notAlternative": true, + "permalinkable": false, + "weight": 0 + }, + { + "MediaType": "text/csv", + "name": "CSV", + "mediaType": { + "type": "text/csv", + "string": "text/csv", + "mainType": "text", + "subType": "csv", + "delimiter": ".", + "suffixes": [ + "csv" + ] + }, + "path": "", + "baseName": "index", + "rel": "alternate", + "protocol": "", + "isPlainText": true, + "isHTML": false, + "noUgly": false, + "notAlternative": false, + "permalinkable": false, + "weight": 0 + }, + { + "MediaType": "text/calendar", + "name": "Calendar", + "mediaType": { + "type": "text/calendar", + "string": "text/calendar", + "mainType": "text", + "subType": "calendar", + "delimiter": ".", + "suffixes": [ + "ics" + ] + }, + "path": "", + "baseName": "index", + "rel": "alternate", + "protocol": "webcal://", + "isPlainText": true, + "isHTML": false, + "noUgly": false, + "notAlternative": false, + "permalinkable": false, + "weight": 0 + }, + { + "MediaType": "application/json", + "name": "JSON", + "mediaType": { + "type": "application/json", + "string": "application/json", + "mainType": "application", + "subType": "json", + "delimiter": ".", + "suffixes": [ + "json" + ] + }, + "path": "", + "baseName": "index", + "rel": "alternate", + "protocol": "", + "isPlainText": true, + "isHTML": false, + "noUgly": false, + "notAlternative": false, + "permalinkable": false, + "weight": 0 + }, + { + "MediaType": "text/plain", + "name": "ROBOTS", + "mediaType": { + "type": "text/plain", + "string": "text/plain", + "mainType": "text", + "subType": "plain", + "delimiter": ".", + "suffixes": [ + "txt" + ] + }, + "path": "", + "baseName": "robots", + "rel": "alternate", + "protocol": "", + "isPlainText": true, + "isHTML": false, + "noUgly": false, + "notAlternative": false, + "permalinkable": false, + "weight": 0 + }, + { + "MediaType": "application/rss+xml", + "name": "RSS", + "mediaType": { + "type": "application/rss+xml", + "string": "application/rss+xml", + "mainType": "application", + "subType": "rss", + "delimiter": ".", + "suffixes": [ + "xml" + ] + }, + "path": "", + "baseName": "index", + "rel": "alternate", + "protocol": "", + "isPlainText": false, + "isHTML": false, + "noUgly": true, + "notAlternative": false, + "permalinkable": false, + "weight": 0 + }, + { + "MediaType": "application/xml", + "name": "Sitemap", + "mediaType": { + "type": "application/xml", + "string": "application/xml", + "mainType": "application", + "subType": "xml", + "delimiter": ".", + "suffixes": [ + "xml" + ] + }, + "path": "", + "baseName": "sitemap", + "rel": "sitemap", + "protocol": "", + "isPlainText": false, + "isHTML": false, + "noUgly": true, + "notAlternative": false, + "permalinkable": false, + "weight": 0 + } + ], + "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": "Single page in \"posts\" section with layout set", + "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": "AMP single page", + "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, 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": "Home page with type set", + "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": "Home page with layout set", + "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": "RSS section 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 in categories", + "Kind": "taxonomy", + "OutputFormat": "RSS", + "Suffix": "xml", + "Template Lookup Order": [ + "layouts/categories/category.rss.xml", + "layouts/categories/taxonomy.rss.xml", + "layouts/categories/rss.xml", + "layouts/categories/list.rss.xml", + "layouts/categories/category.xml", + "layouts/categories/taxonomy.xml", + "layouts/categories/list.xml", + "layouts/taxonomy/category.rss.xml", + "layouts/taxonomy/taxonomy.rss.xml", + "layouts/taxonomy/rss.xml", + "layouts/taxonomy/list.rss.xml", + "layouts/taxonomy/category.xml", + "layouts/taxonomy/taxonomy.xml", + "layouts/taxonomy/list.xml", + "layouts/category/category.rss.xml", + "layouts/category/taxonomy.rss.xml", + "layouts/category/rss.xml", + "layouts/category/list.rss.xml", + "layouts/category/category.xml", + "layouts/category/taxonomy.xml", + "layouts/category/list.xml", + "layouts/_default/category.rss.xml", + "layouts/_default/taxonomy.rss.xml", + "layouts/_default/rss.xml", + "layouts/_default/list.rss.xml", + "layouts/_default/category.xml", + "layouts/_default/taxonomy.xml", + "layouts/_default/list.xml", + "layouts/_internal/_default/rss.xml" + ] + }, + { + "Example": "Taxonomy terms in categories", + "Kind": "taxonomyTerm", + "OutputFormat": "RSS", + "Suffix": "xml", + "Template Lookup Order": [ + "layouts/categories/category.terms.rss.xml", + "layouts/categories/terms.rss.xml", + "layouts/categories/rss.xml", + "layouts/categories/list.rss.xml", + "layouts/categories/category.terms.xml", + "layouts/categories/terms.xml", + "layouts/categories/list.xml", + "layouts/taxonomy/category.terms.rss.xml", + "layouts/taxonomy/terms.rss.xml", + "layouts/taxonomy/rss.xml", + "layouts/taxonomy/list.rss.xml", + "layouts/taxonomy/category.terms.xml", + "layouts/taxonomy/terms.xml", + "layouts/taxonomy/list.xml", + "layouts/category/category.terms.rss.xml", + "layouts/category/terms.rss.xml", + "layouts/category/rss.xml", + "layouts/category/list.rss.xml", + "layouts/category/category.terms.xml", + "layouts/category/terms.xml", + "layouts/category/list.xml", + "layouts/_default/category.terms.rss.xml", + "layouts/_default/terms.rss.xml", + "layouts/_default/rss.xml", + "layouts/_default/list.rss.xml", + "layouts/_default/category.terms.xml", + "layouts/_default/terms.xml", + "layouts/_default/list.xml", + "layouts/_internal/_default/rss.xml" + ] + }, + { + "Example": "Section list for \"posts\" section", + "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\" section 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\" section 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": "Taxonomy list in categories", + "Kind": "taxonomy", + "OutputFormat": "HTML", + "Suffix": "html", + "Template Lookup Order": [ + "layouts/categories/category.html.html", + "layouts/categories/taxonomy.html.html", + "layouts/categories/list.html.html", + "layouts/categories/category.html", + "layouts/categories/taxonomy.html", + "layouts/categories/list.html", + "layouts/taxonomy/category.html.html", + "layouts/taxonomy/taxonomy.html.html", + "layouts/taxonomy/list.html.html", + "layouts/taxonomy/category.html", + "layouts/taxonomy/taxonomy.html", + "layouts/taxonomy/list.html", + "layouts/category/category.html.html", + "layouts/category/taxonomy.html.html", + "layouts/category/list.html.html", + "layouts/category/category.html", + "layouts/category/taxonomy.html", + "layouts/category/list.html", + "layouts/_default/category.html.html", + "layouts/_default/taxonomy.html.html", + "layouts/_default/list.html.html", + "layouts/_default/category.html", + "layouts/_default/taxonomy.html", + "layouts/_default/list.html" + ] + }, + { + "Example": "Taxonomy term in categories", + "Kind": "taxonomyTerm", + "OutputFormat": "HTML", + "Suffix": "html", + "Template Lookup Order": [ + "layouts/categories/category.terms.html.html", + "layouts/categories/terms.html.html", + "layouts/categories/list.html.html", + "layouts/categories/category.terms.html", + "layouts/categories/terms.html", + "layouts/categories/list.html", + "layouts/taxonomy/category.terms.html.html", + "layouts/taxonomy/terms.html.html", + "layouts/taxonomy/list.html.html", + "layouts/taxonomy/category.terms.html", + "layouts/taxonomy/terms.html", + "layouts/taxonomy/list.html", + "layouts/category/category.terms.html.html", + "layouts/category/terms.html.html", + "layouts/category/list.html.html", + "layouts/category/category.terms.html", + "layouts/category/terms.html", + "layouts/category/list.html", + "layouts/_default/category.terms.html.html", + "layouts/_default/terms.html.html", + "layouts/_default/list.html.html", + "layouts/_default/category.terms.html", + "layouts/_default/terms.html", + "layouts/_default/list.html" + ] + } + ] + }, + "tpl": { + "funcs": { + "cast": { + "ToFloat": { + "Description": "ToFloat converts the given value to a float.", + "Args": [ + "v" + ], + "Aliases": [ + "float" + ], + "Examples": [ + [ + "{{ \"1234\" | float | printf \"%T\" }}", + "float64" + ] + ] + }, + "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": { + "And": { + "Description": "And computes the Boolean AND of its arguments, returning\nthe first false argument it encounters, or the last argument.", + "Args": [ + "arg0", + "args" + ], + "Aliases": [ + "and" + ], + "Examples": [] + }, + "Conditional": { + "Description": "Conditional can be used as a ternary operator.\nIt returns a if condition, else b.", + "Args": [ + "condition", + "a", + "b" + ], + "Aliases": [ + "cond" + ], + "Examples": [ + [ + "{{ cond (eq (add 2 2) 4) \"2+2 is 4\" \"what?\" | safeHTML }}", + "2+2 is 4" + ] + ] + }, + "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": [ + [ + "{{ if ge .Hugo.Version \"0.36\" }}Reasonable new Hugo version!{{ end }}", + "Reasonable new Hugo version!" + ] + ] + }, + "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": [] + }, + "Not": { + "Description": "Not returns the Boolean negation of its argument.", + "Args": [ + "arg" + ], + "Aliases": [ + "not" + ], + "Examples": [] + }, + "Or": { + "Description": "Or computes the Boolean OR of its arguments, returning\nthe first true argument it encounters, or the last argument.", + "Args": [ + "arg0", + "args" + ], + "Aliases": [ + "or" + ], + "Examples": [] + } + }, + "collections": { + "After": { + "Description": "After returns all the items after the first N in a rangeable list.", + "Args": [ + "index", + "seq" + ], + "Aliases": [ + "after" + ], + "Examples": [] + }, + "Append": { + "Description": "Append appends the arguments up to the last one to the slice in the last argument.\nThis construct allows template constructs like this:\n {{ $pages = $pages | append $p2 $p1 }}\nNote that with 2 arguments where both are slices of the same type,\nthe first slice will be appended to the second:\n {{ $pages = $pages | append .Site.RegularPages }}", + "Args": [ + "args" + ], + "Aliases": [ + "append" + ], + "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": [] + }, + "Complement": { + "Description": "Complement gives the elements in the last element of seqs that are not in\nany of the others.\nAll elements of seqs 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 {{ $c := .Pages | complement $last4 }}", + "Args": [ + "seqs" + ], + "Aliases": [ + "complement" + ], + "Examples": [ + [ + "{{ slice \"a\" \"b\" \"c\" \"d\" \"e\" \"f\" | complement (slice \"b\" \"c\") (slice \"d\" \"e\") }}", + "[a f]" + ] + ] + }, + "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": [] + }, + "Group": { + "Description": "Group groups a set of elements by the given key.\nThis is currently only supported for Pages.", + "Args": [ + "key", + "items" + ], + "Aliases": [ + "group" + ], + "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": [] + }, + "KeyVals": { + "Description": "KeyVals creates a key and values wrapper.", + "Args": [ + "key", + "vals" + ], + "Aliases": [ + "keyVals" + ], + "Examples": [ + [ + "{{ keyVals \"key\" \"a\" \"b\" }}", + "key: [a b]" + ] + ] + }, + "Last": { + "Description": "Last returns the last N items in a rangeable list.", + "Args": [ + "limit", + "seq" + ], + "Aliases": [ + "last" + ], + "Examples": [] + }, + "Merge": { + "Description": "Merge creates a copy of dst and merges src into it.\nCurrently only maps supported. Key handling is case insensitive.", + "Args": [ + "src", + "dst" + ], + "Aliases": [ + "merge" + ], + "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!]" + ] + ] + }, + "NewScratch": { + "Description": "NewScratch creates a new Scratch which can be used to store values in a\nthread safe way.", + "Args": null, + "Aliases": [ + "newScratch" + ], + "Examples": [ + [ + "{{ $scratch := newScratch }}{{ $scratch.Add \"b\" 2 }}{{ $scratch.Add \"b\" 2 }}{{ $scratch.Get \"b\" }}", + "4" + ] + ] + }, + "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": [] + }, + "SymDiff": { + "Description": "SymDiff returns the symmetric difference of s1 and s2.\nArguments must be either a slice or an array of comparable types.", + "Args": [ + "s2", + "s1" + ], + "Aliases": [ + "symdiff" + ], + "Examples": [ + [ + "{{ slice 1 2 3 | symdiff (slice 3 4) }}", + "[1 2 4]" + ] + ] + }, + "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": [ + "seq" + ], + "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": { + "Errorf": { + "Description": "Errorf formats according to a format specifier and returns the string as a\nvalue that satisfies error.", + "Args": [ + "format", + "a" + ], + "Aliases": [ + "errorf" + ], + "Examples": [ + [ + "{{ errorf \"%s.\" \"failed\" }}", + "failed." + ] + ] + }, + "Print": { + "Description": "Print returns string representation of the passed arguments.", + "Args": [ + "a" + ], + "Aliases": [ + "print" + ], + "Examples": [ + [ + "{{ print \"works!\" }}", + "works!" + ] + ] + }, + "Printf": { + "Description": "Printf returns a formatted string representation of the passed arguments.", + "Args": [ + "format", + "a" + ], + "Aliases": [ + "printf" + ], + "Examples": [ + [ + "{{ printf \"%s!\" \"works\" }}", + "works!" + ] + ] + }, + "Println": { + "Description": "Println returns string representation of the passed arguments ending with a newline.", + "Args": [ + "a" + ], + "Aliases": [ + "println" + ], + "Examples": [ + [ + "{{ println \"works!\" }}", + "works!\n" + ] + ] + } + }, + "hugo": { + "Generator": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "Version": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + } + }, + "images": { + "Brightness": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "ColorBalance": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "Colorize": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "Config": { + "Description": "Config returns the image.Config for the specified path relative to the\nworking directory.", + "Args": [ + "path" + ], + "Aliases": [ + "imageConfig" + ], + "Examples": [] + }, + "Contrast": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "Filter": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "Gamma": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "GaussianBlur": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "Grayscale": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "Hue": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "Invert": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "Pixelate": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "Saturation": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "Sepia": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "Sigmoid": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "UnsharpMask": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + } + }, + "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": { + "Merge": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "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 returns a translated string for id.", + "Args": [ + "id", + "args" + ], + "Aliases": [ + "i18n", + "T" + ], + "Examples": [] + } + }, + "math": { + "Add": { + "Description": "Add adds two numbers.", + "Args": [ + "a", + "b" + ], + "Aliases": [ + "add" + ], + "Examples": [ + [ + "{{add 1 2}}", + "3" + ] + ] + }, + "Ceil": { + "Description": "Ceil returns the least integer value greater than or equal to x.", + "Args": [ + "x" + ], + "Aliases": null, + "Examples": [ + [ + "{{math.Ceil 2.1}}", + "3" + ] + ] + }, + "Div": { + "Description": "Div divides two numbers.", + "Args": [ + "a", + "b" + ], + "Aliases": [ + "div" + ], + "Examples": [ + [ + "{{div 6 3}}", + "2" + ] + ] + }, + "Floor": { + "Description": "Floor returns the greatest integer value less than or equal to x.", + "Args": [ + "x" + ], + "Aliases": null, + "Examples": [ + [ + "{{math.Floor 1.9}}", + "1" + ] + ] + }, + "Log": { + "Description": "Log returns the natural logarithm of a number.", + "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": "Mul multiplies two numbers.", + "Args": [ + "a", + "b" + ], + "Aliases": [ + "mul" + ], + "Examples": [ + [ + "{{mul 2 3}}", + "6" + ] + ] + }, + "Round": { + "Description": "Round returns the nearest integer, rounding half away from zero.", + "Args": [ + "x" + ], + "Aliases": null, + "Examples": [ + [ + "{{math.Round 1.5}}", + "2" + ] + ] + }, + "Sub": { + "Description": "Sub subtracts two numbers.", + "Args": [ + "a", + "b" + ], + "Aliases": [ + "sub" + ], + "Examples": [ + [ + "{{sub 3 2}}", + "1" + ] + ] + } + }, + "os": { + "FileExists": { + "Description": "FileExists checks whether a file exists under the given path.", + "Args": [ + "i" + ], + "Aliases": [ + "fileExists" + ], + "Examples": [ + [ + "{{ fileExists \"foo.txt\" }}", + "false" + ] + ] + }, + "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 \"files\") }}{{ .Name }}{{ end }}", + "README.txt" + ] + ] + }, + "ReadFile": { + "Description": "ReadFile reads the file named by filename relative to the configured WorkingDir.\nIt returns the contents as a string.\nThere is an upper size limit set at 1 megabytes.", + "Args": [ + "i" + ], + "Aliases": [ + "readFile" + ], + "Examples": [ + [ + "{{ readFile \"files/README.txt\" }}", + "Hugo Rocks!" + ] + ] + }, + "Stat": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + } + }, + "partials": { + "Include": { + "Description": "Include executes the named partial.\nIf the partial contains a return statement, that value will be returned.\nElse, the rendered output will be returned:\nA string if 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" + ] + ] + }, + "IncludeCached": { + "Description": "IncludeCached executes and caches partial templates. An optional variant\nstring parameter (a string slice actually, but be only use a variadic\nargument to make it optional) can be passed so that a given partial can have\nmultiple uses. The cache is created with name+variant as the key.", + "Args": [ + "name", + "context", + "variant" + ], + "Aliases": [ + "partialCached" + ], + "Examples": [] + } + }, + "path": { + "Base": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "Dir": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "Ext": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "Join": { + "Description": "Join joins any number of path elements into a single path, adding a\nseparating slash if necessary. All the input\npath elements are passed into filepath.ToSlash converting any Windows slashes\nto forward slashes.\nThe result is Cleaned; in particular,\nall empty strings are ignored.", + "Args": [ + "elements" + ], + "Aliases": null, + "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": { + "Description": "Split splits path immediately following the final slash,\nseparating it into a directory and file name component.\nIf there is no slash in path, Split returns an empty dir and\nfile set to path.\nThe input path is passed into filepath.ToSlash converting any Windows slashes\nto forward slashes.\nThe returned values have the property that path = dir+file.", + "Args": [ + "path" + ], + "Aliases": null, + "Examples": [ + [ + "{{ \"/my/path/filename.txt\" | path.Split }}", + "/my/path/|filename.txt" + ], + [ + "{{ \"/my/path/filename.txt\" | path.Split }}", + "/my/path/|filename.txt" + ] + ] + } + }, + "reflect": { + "IsMap": { + "Description": "IsMap reports whether v is a map.", + "Args": [ + "v" + ], + "Aliases": null, + "Examples": [ + [ + "{{ if reflect.IsMap (dict \"a\" 1) }}Map{{ end }}", + "Map" + ] + ] + }, + "IsSlice": { + "Description": "IsSlice reports whether v is a slice.", + "Args": [ + "v" + ], + "Aliases": null, + "Examples": [ + [ + "{{ if reflect.IsSlice (slice 1 2 3) }}Slice{{ end }}", + "Slice" + ] + ] + } + }, + "resources": { + "Concat": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "ExecuteAsTemplate": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "Fingerprint": { + "Description": "Fingerprint transforms the given Resource with a MD5 hash of the content in\nthe RelPermalink and Permalink.", + "Args": [ + "args" + ], + "Aliases": [ + "fingerprint" + ], + "Examples": [] + }, + "FromString": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "Get": { + "Description": "Get locates the filename given in Hugo's assets filesystem\nand creates a Resource object that can be used for further transformations.", + "Args": [ + "filename" + ], + "Aliases": null, + "Examples": [] + }, + "GetMatch": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "Match": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "Minify": { + "Description": "Minify minifies the given Resource using the MediaType to pick the correct\nminifier.", + "Args": [ + "r" + ], + "Aliases": [ + "minify" + ], + "Examples": [] + }, + "PostCSS": { + "Description": "PostCSS processes the given Resource with PostCSS", + "Args": [ + "args" + ], + "Aliases": [ + "postCSS" + ], + "Examples": [] + }, + "ToCSS": { + "Description": "ToCSS converts the given Resource to CSS. You can optional provide an Options\nobject or a target path (string) as first argument.", + "Args": [ + "args" + ], + "Aliases": [ + "toCSS" + ], + "Examples": [] + } + }, + "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" + ] + ] + } + }, + "site": { + "BaseURL": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "Data": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "Hugo": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "IsServer": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "Language": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "LastChange": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "Menus": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "Pages": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "Params": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "RegularPages": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "ServerPort": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "Sites": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "Taxonomies": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "Title": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + } + }, + "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\" | safeHTML }}", + "\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]" + ] + ] + }, + "FirstUpper": { + "Description": "FirstUpper returns a string with the first character as upper case.", + "Args": [ + "s" + ], + "Aliases": null, + "Examples": [ + [ + "{{ \"hugo rocks!\" | strings.FirstUpper }}", + "Hugo rocks!" + ] + ] + }, + "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 + }, + "Repeat": { + "Description": "Repeat returns a new string consisting of count copies of the string s.", + "Args": [ + "n", + "s" + ], + "Aliases": null, + "Examples": [ + [ + "{{ \"yo\" | strings.Repeat 4 }}", + "yoyoyoyo" + ] + ] + }, + "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": [] + }, + "RuneCount": { + "Description": "RuneCount returns the number of runes in s.", + "Args": [ + "s" + ], + "Aliases": null, + "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" + ], + [ + "{{title \"somewhere over the rainbow\"}}", + "Somewhere Over the Rainbow" + ] + ] + }, + "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" + ] + ] + }, + "TrimLeft": { + "Description": "TrimLeft returns a slice of the string s with all leading characters\ncontained in cutset removed.", + "Args": [ + "cutset", + "s" + ], + "Aliases": null, + "Examples": [ + [ + "{{ \"aabbaa\" | strings.TrimLeft \"a\" }}", + "bbaa" + ] + ] + }, + "TrimPrefix": { + "Description": "TrimPrefix returns s without the provided leading prefix string. If s doesn't\nstart with prefix, s is returned unchanged.", + "Args": [ + "prefix", + "s" + ], + "Aliases": null, + "Examples": [ + [ + "{{ \"aabbaa\" | strings.TrimPrefix \"a\" }}", + "abbaa" + ], + [ + "{{ \"aabbaa\" | strings.TrimPrefix \"aa\" }}", + "bbaa" + ] + ] + }, + "TrimRight": { + "Description": "TrimRight returns a slice of the string s with all trailing characters\ncontained in cutset removed.", + "Args": [ + "cutset", + "s" + ], + "Aliases": null, + "Examples": [ + [ + "{{ \"aabbaa\" | strings.TrimRight \"a\" }}", + "aabb" + ] + ] + }, + "TrimSuffix": { + "Description": "TrimSuffix returns s without the provided trailing suffix string. If s\ndoesn't end with suffix, s is returned unchanged.", + "Args": [ + "suffix", + "s" + ], + "Aliases": null, + "Examples": [ + [ + "{{ \"aabbaa\" | strings.TrimSuffix \"a\" }}", + "aabba" + ], + [ + "{{ \"aabbaa\" | strings.TrimSuffix \"aa\" }}", + "aabb" + ] + ] + }, + "Truncate": { + "Description": "Truncate truncates a given string to the specified length.", + "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" + ] + ] + } + }, + "templates": { + "Exists": { + "Description": "Exists returns whether the template with the given name exists.\nNote that this is the Unix-styled relative path including filename suffix,\ne.g. partials/header.html", + "Args": [ + "name" + ], + "Aliases": null, + "Examples": [ + [ + "{{ if (templates.Exists \"partials/header.html\") }}Yes!{{ end }}", + "Yes!" + ], + [ + "{{ if not (templates.Exists \"partials/doesnotexist.html\") }}No!{{ end }}", + "No!" + ] + ] + } + }, + "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" + ] + ] + }, + "Duration": { + "Description": "Duration converts the given number to a time.Duration.\nUnit is one of nanosecond/ns, microsecond/us/µs, millisecond/ms, second/s, minute/m or hour/h.", + "Args": [ + "unit", + "number" + ], + "Aliases": [ + "duration" + ], + "Examples": [ + [ + "{{ mul 60 60 | duration \"second\" }}", + "1h0m0s" + ] + ] + }, + "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": [] + }, + "ParseDuration": { + "Description": "ParseDuration parses a duration string.\nA duration string is a possibly signed sequence of\ndecimal numbers, each with optional fraction and a unit suffix,\nsuch as \"300ms\", \"-1.5h\" or \"2h45m\".\nValid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\".\nSee https://golang.org/pkg/time/#ParseDuration", + "Args": [ + "in" + ], + "Aliases": null, + "Examples": [ + [ + "{{ \"1h12m10s\" | time.ParseDuration }}", + "1h12m10s" + ] + ] + } + }, + "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!" + ] + ] + }, + "Remarshal": { + "Description": "Remarshal is used in the Hugo documentation to convert configuration\nexamples from YAML to JSON, TOML (and possibly the other way around).\nThe is primarily a helper for the Hugo docs site.\nIt is not a general purpose YAML to TOML converter etc., and may\nchange without notice if it serves a purpose in the docs.\nFormat is one of json, yaml or toml.", + "Args": [ + "format", + "data" + ], + "Aliases": null, + "Examples": [ + [ + "{{ \"title = \\\"Hello World\\\"\" | transform.Remarshal \"json\" | safeHTML }}", + "{\n \"title\": \"Hello World\"\n}\n" + ] + ] + }, + "Unmarshal": { + "Description": "Unmarshal unmarshals the data given, which can be either a string\nor a Resource. Supported formats are JSON, TOML, YAML, and CSV.\nYou can optionally provide an options map as the first argument.", + "Args": [ + "args" + ], + "Aliases": [ + "unmarshal" + ], + "Examples": [ + [ + "{{ \"hello = \\\"Hello World\\\"\" | transform.Unmarshal }}", + "map[hello:Hello World]" + ], + [ + "{{ \"hello = \\\"Hello World\\\"\" | resources.FromString \"data/greetings.toml\" | transform.Unmarshal }}", + "map[hello:Hello World]" + ] + ] + } + }, + "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": [] + }, + "Anchorize": { + "Description": "Anchorize creates sanitized anchor names that are compatible with Blackfriday.", + "Args": [ + "a" + ], + "Aliases": [ + "anchorize" + ], + "Examples": [ + [ + "{{ \"This is a title\" | anchorize }}", + "this-is-a-title" + ] + ] + }, + "Parse": { + "Description": "", + "Args": null, + "Aliases": null, + "Examples": null + }, + "Ref": { + "Description": "Ref returns the absolute URL path to a given content item.", + "Args": [ + "in", + "args" + ], + "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", + "args" + ], + "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": "URLize returns the given argument formatted as URL.", + "Args": [ + "a" + ], + "Aliases": [ + "urlize" + ], + "Examples": [] + } + } + } + } +} diff --git a/data/homepagetweets.toml b/data/homepagetweets.toml new file mode 100644 index 000000000..01f8c2fc2 --- /dev/null +++ b/data/homepagetweets.toml @@ -0,0 +1,279 @@ +[[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://twitter.com/jscarto/status/1039648827815485440" +date = 2018-09-12T00:00:00Z + +[[tweet]] +name = "Jens Munch" +twitter_handle = "@jensamunch" +quote = "Hugo is really, really incredible... Now does resizing/resampling of images as well! Crazy that something so fast can be a static site generator... Amazing open-source project." +link = "https://twitter.com/jensamunch/status/948533063537086464" +date = 2018-01-03T04:00:00Z + +[[tweet]] +name = "carriecoxwell" +twitter_handle = "@carriecoxwell" +quote = "Having a lot of fun with @GoHugoIO! It's exactly what I didn't even know I wanted." +link = "https://twitter.com/carriecoxwell/status/948402470144872448" +date = 2018-01-03T03:00:00Z + +[[tweet]] +name = "STOQE" +twitter_handle = "@STOQE" +quote = "I fear @GoHugoIO v0.22 might be so fast it creates a code vortex that time-warps me back to a time I used Wordpress. #gasp" +link = "https://twitter.com/STOQE/status/874184881701494784" +date = 2017-06-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://twitter.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://twitter.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://twitter.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://twitter.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://twitter.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://twitter.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://twitter.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://twitter.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://twitter.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://twitter.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://twitter.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://twitter.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://twitter.com/dcaunt/statuses/406466996277374976" +date = 2013-11-29T00:00:00Z + +[[tweet]] +name = "David Gay" +twitter_handle = "@oddshocks" +quote = "Hugo is super-rad." +link = "https://twitter.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://twitter.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://twitter.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://twitter.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://twitter.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://twitter.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://twitter.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://twitter.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://twitter.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://twitter.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://twitter.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://twitter.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://twitter.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://twitter.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://twitter.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://twitter.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://twitter.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://twitter.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://twitter.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://twitter.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://twitter.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://twitter.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://twitter.com/workhtml/status/563064361301053440" +date = 2015-02-04T00:00:00Z diff --git a/data/titles.toml b/data/titles.toml new file mode 100644 index 000000000..2348c8561 --- /dev/null +++ b/data/titles.toml @@ -0,0 +1,2 @@ +[Showcase] +title = "Site Showcase" diff --git a/deploy/cloudfront.go b/deploy/cloudfront.go deleted file mode 100644 index dbdf9baf4..000000000 --- a/deploy/cloudfront.go +++ /dev/null @@ -1,51 +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 deploy - -import ( - "context" - "time" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/cloudfront" -) - -// InvalidateCloudFront invalidates the CloudFront cache for distributionID. -// It uses the default AWS credentials from the environment. -func InvalidateCloudFront(ctx context.Context, distributionID string) error { - // SharedConfigEnable enables loading "shared config (~/.aws/config) and - // shared credentials (~/.aws/credentials) files". - // See https://docs.aws.amazon.com/sdk-for-go/api/aws/session/ for more - // details. - // This is the same codepath used by Go CDK when creating an s3 URL. - // TODO: Update this to a Go CDK helper once available - // (https://github.com/google/go-cloud/issues/2003). - sess, err := session.NewSessionWithOptions(session.Options{SharedConfigState: session.SharedConfigEnable}) - if err != nil { - return err - } - req := &cloudfront.CreateInvalidationInput{ - DistributionId: aws.String(distributionID), - InvalidationBatch: &cloudfront.InvalidationBatch{ - CallerReference: aws.String(time.Now().Format("20060102150405")), - Paths: &cloudfront.Paths{ - Items: []*string{aws.String("/*")}, - Quantity: aws.Int64(1), - }, - }, - } - _, err = cloudfront.New(sess).CreateInvalidationWithContext(ctx, req) - return err -} diff --git a/deploy/deploy.go b/deploy/deploy.go deleted file mode 100644 index 1d911f29b..000000000 --- a/deploy/deploy.go +++ /dev/null @@ -1,654 +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 deploy - -import ( - "bytes" - "compress/gzip" - "context" - "crypto/md5" - "fmt" - "io" - "io/ioutil" - "mime" - "os" - "path/filepath" - "regexp" - "runtime" - "sort" - "strings" - "sync" - - "github.com/dustin/go-humanize" - "github.com/gohugoio/hugo/config" - "github.com/pkg/errors" - "github.com/spf13/afero" - jww "github.com/spf13/jwalterweatherman" - "golang.org/x/text/unicode/norm" - - "gocloud.dev/blob" - _ "gocloud.dev/blob/fileblob" // import - _ "gocloud.dev/blob/gcsblob" // import - _ "gocloud.dev/blob/s3blob" // import -) - -// Deployer supports deploying the site to target cloud providers. -type Deployer struct { - localFs afero.Fs - bucket *blob.Bucket - - target *target // the target to deploy to - matchers []*matcher // matchers to apply to uploaded files - ordering []*regexp.Regexp // orders uploads - quiet bool // true reduces STDOUT - confirm bool // true enables confirmation before making changes - dryRun bool // true skips conformations and prints changes instead of applying them - force bool // true forces upload of all files - invalidateCDN bool // true enables invalidate CDN cache (if possible) - maxDeletes int // caps the # of files to delete; -1 to disable - - // For tests... - summary deploySummary // summary of latest Deploy results -} - -type deploySummary struct { - NumLocal, NumRemote, NumUploads, NumDeletes int -} - -// New constructs a new *Deployer. -func New(cfg config.Provider, localFs afero.Fs) (*Deployer, error) { - targetName := cfg.GetString("target") - - // Load the [deployment] section of the config. - dcfg, err := decodeConfig(cfg) - if err != nil { - return nil, err - } - - if len(dcfg.Targets) == 0 { - return nil, errors.New("no deployment targets found") - } - - // Find the target to deploy to. - var tgt *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, - matchers: dcfg.Matchers, - ordering: dcfg.ordering, - quiet: cfg.GetBool("quiet"), - confirm: cfg.GetBool("confirm"), - dryRun: cfg.GetBool("dryRun"), - force: cfg.GetBool("force"), - invalidateCDN: cfg.GetBool("invalidateCDN"), - maxDeletes: cfg.GetInt("maxDeletes"), - }, nil -} - -func (d *Deployer) openBucket(ctx context.Context) (*blob.Bucket, error) { - if d.bucket != nil { - return d.bucket, nil - } - jww.FEEDBACK.Printf("Deploying to target %q (%s)\n", d.target.Name, d.target.URL) - return blob.OpenBucket(ctx, d.target.URL) -} - -// Deploy deploys the site to a target. -func (d *Deployer) Deploy(ctx context.Context) error { - bucket, err := d.openBucket(ctx) - if err != nil { - return err - } - - // Load local files from the source directory. - local, err := walkLocal(d.localFs, d.matchers) - if err != nil { - return err - } - jww.INFO.Printf("Found %d local files.\n", len(local)) - d.summary.NumLocal = len(local) - - // Load remote files from the target. - remote, err := walkRemote(ctx, bucket) - if err != nil { - return err - } - jww.INFO.Printf("Found %d remote files.\n", len(remote)) - d.summary.NumRemote = len(remote) - - // Diff local vs remote to see what changes need to be applied. - uploads, deletes := findDiffs(local, remote, d.force) - d.summary.NumUploads = len(uploads) - d.summary.NumDeletes = len(deletes) - if len(uploads)+len(deletes) == 0 { - if !d.quiet { - jww.FEEDBACK.Println("No changes required.") - } - return nil - } - if !d.quiet { - jww.FEEDBACK.Println(summarizeChanges(uploads, deletes)) - } - - // Ask for confirmation before proceeding. - if d.confirm && !d.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.ordering, uploads) - - // Apply the changes in parallel, using an inverted worker - // pool (https://www.youtube.com/watch?v=5zXAHh5tJqQ&t=26m58s). - // sem prevents more than nParallel concurrent goroutines. - const nParallel = 10 - 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.dryRun { - if !d.quiet { - jww.FEEDBACK.Printf("[DRY RUN] Would upload: %v\n", upload) - } - continue - } - - sem <- struct{}{} - go func(upload *fileToUpload) { - if err := 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.maxDeletes != -1 && len(deletes) > d.maxDeletes { - jww.WARN.Printf("Skipping %d deletes because it is more than --maxDeletes (%d). If this is expected, set --maxDeletes to a larger number, or -1 to disable this check.\n", len(deletes), d.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.dryRun { - if !d.quiet { - jww.FEEDBACK.Printf("[DRY RUN] Would delete %s\n", del) - } - continue - } - sem <- struct{}{} - go func(del string) { - jww.INFO.Printf("Deleting %s...\n", del) - if err := bucket.Delete(ctx, del); err != nil { - 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 { - jww.FEEDBACK.Printf("Encountered %d errors.\n", len(errs)) - } - return errs[0] - } - if !d.quiet { - jww.FEEDBACK.Println("Success!") - } - - if d.invalidateCDN { - if d.target.CloudFrontDistributionID != "" { - jww.FEEDBACK.Println("Invalidating CloudFront CDN...") - if err := InvalidateCloudFront(ctx, d.target.CloudFrontDistributionID); err != nil { - jww.FEEDBACK.Printf("Failed to invalidate CloudFront CDN: %v\n", err) - return err - } - } - if d.target.GoogleCloudCDNOrigin != "" { - jww.FEEDBACK.Println("Invalidating Google Cloud CDN...") - if err := InvalidateGoogleCloudCDN(ctx, d.target.GoogleCloudCDNOrigin); err != nil { - jww.FEEDBACK.Printf("Failed to invalidate Google Cloud CDN: %v\n", err) - return err - } - } - jww.FEEDBACK.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 doSingleUpload(ctx context.Context, bucket *blob.Bucket, upload *fileToUpload) error { - jww.INFO.Printf("Uploading %v...\n", upload) - opts := &blob.WriterOptions{ - CacheControl: upload.Local.CacheControl(), - ContentEncoding: upload.Local.ContentEncoding(), - ContentType: upload.Local.ContentType(), - } - 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 *matcher - md5 []byte // cache - gzipped bytes.Buffer // cached of gzipped contents if gzipping -} - -// newLocalFile initializes a *localFile. -func newLocalFile(fs afero.Fs, nativePath, slashpath string, m *matcher) (*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, - } - 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 ioutil.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 - } - // TODO: Hugo has a MediaType and a MediaTypes list and also a concept - // of custom MIME types. - // Use 1) The matcher 2) Hugo's MIME types 3) TypeByExtension. - return mime.TypeByExtension(filepath.Ext(lf.NativePath)) -} - -// 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 -} - -// walkLocal walks the source directory and returns a flat list of files, -// using localFile.SlashPath as the map keys. -func walkLocal(fs afero.Fs, matchers []*matcher) (map[string]*localFile, error) { - retval := map[string]*localFile{} - 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(), ".") { - return filepath.SkipDir - } - return nil - } - - // .DS_Store is an internal MacOS attribute file; skip it. - if info.Name() == ".DS_Store" { - return nil - } - - // When a file system is HFS+, its filepath is in NFD form. - if runtime.GOOS == "darwin" { - path = norm.NFC.String(path) - } - - // Find the first matching matcher (if any). - slashpath := filepath.ToSlash(path) - var m *matcher - for _, cur := range matchers { - if cur.Matches(slashpath) { - m = cur - break - } - } - lf, err := newLocalFile(fs, path, slashpath, m) - if err != nil { - return err - } - retval[lf.SlashPath] = lf - return nil - }) - if err != nil { - return nil, err - } - return retval, nil -} - -// walkRemote walks the target bucket and returns a flat list. -func walkRemote(ctx context.Context, bucket *blob.Bucket) (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 - } - // If the remote didn't give us an MD5, 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 { - 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() - } - } - 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 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 - } else { - // Nope! Leave uploaded = false. - } - found[path] = true - } else { - // The file doesn't exist in remote. - upload = true - reason = reasonNotFound - } - if upload { - jww.DEBUG.Printf("%s needs to be uploaded: %v\n", path, reason) - uploads = append(uploads, &fileToUpload{lf, reason}) - } else { - jww.DEBUG.Printf("%s exists at target and does not need to be uploaded", path) - } - } - - // 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/deployConfig.go b/deploy/deployConfig.go deleted file mode 100644 index 3bc51294d..000000000 --- a/deploy/deployConfig.go +++ /dev/null @@ -1,105 +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 deploy - -import ( - "fmt" - "regexp" - - "github.com/gohugoio/hugo/config" - "github.com/mitchellh/mapstructure" -) - -const deploymentConfigKey = "deployment" - -// deployConfig is the complete configuration for deployment. -type deployConfig struct { - Targets []*target - Matchers []*matcher - Order []string - - ordering []*regexp.Regexp // 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 -} - -// 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 -} - -func (m *matcher) Matches(path string) bool { - return m.re.MatchString(path) -} - -// decode creates a config from a given Hugo configuration. -func decodeConfig(cfg config.Provider) (deployConfig, error) { - var dcfg deployConfig - if !cfg.IsSet(deploymentConfigKey) { - return dcfg, nil - } - if err := mapstructure.WeakDecode(cfg.GetStringMap(deploymentConfigKey), &dcfg); err != nil { - return dcfg, err - } - var err error - for _, m := range dcfg.Matchers { - 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_test.go b/deploy/deployConfig_test.go deleted file mode 100644 index f4aaa5eaf..000000000 --- a/deploy/deployConfig_test.go +++ /dev/null @@ -1,156 +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 deploy - -import ( - "fmt" - "testing" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/config" - "github.com/spf13/viper" -) - -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" - -# All uppercase. -[[deployment.targets]] -NAME = "name1" -URL = "url1" -CLOUDFRONTDISTRIBUTIONID = "cdn1" - -# Camelcase. -[[deployment.targets]] -name = "name2" -url = "url2" -cloudFrontDistributionID = "cdn2" - -# 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) - 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)) - } - - // 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(viper.New()) - c.Assert(err, qt.IsNil) - c.Assert(len(dcfg.Targets), qt.Equals, 0) - c.Assert(len(dcfg.Matchers), qt.Equals, 0) -} diff --git a/deploy/deploy_azure.go b/deploy/deploy_azure.go deleted file mode 100644 index 6251429ff..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. - -// +build !solaris - -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 ed20daef4..000000000 --- a/deploy/deploy_test.go +++ /dev/null @@ -1,810 +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 deploy - -import ( - "bytes" - "compress/gzip" - "context" - "crypto/md5" - "fmt" - "io" - "io/ioutil" - "os" - "path" - "path/filepath" - "regexp" - "sort" - "testing" - - "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: &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 - } - gotUpdates, gotDeletes := 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 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 *matcher - 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: &matcher{CacheControl: "max-age=630720000"}, - WantContent: contentBytes, - WantSize: contentLen, - WantMD5: contentMD5[:], - WantCacheControl: "max-age=630720000", - }, - { - Description: "ContentEncoding from matcher", - Path: "foo.txt", - Matcher: &matcher{ContentEncoding: "foobar"}, - WantContent: contentBytes, - WantSize: contentLen, - WantMD5: contentMD5[:], - WantContentEncoding: "foobar", - }, - { - Description: "ContentType from matcher", - Path: "foo.txt", - Matcher: &matcher{ContentType: "foo/bar"}, - WantContent: contentBytes, - WantSize: contentLen, - WantMD5: contentMD5[:], - WantContentType: "foo/bar", - }, - { - Description: "gzipped content", - Path: "foo.txt", - Matcher: &matcher{Gzip: true}, - WantContent: gzBytes, - WantSize: gzLen, - WantMD5: gzMD5[:], - WantContentEncoding: "gzip", - }, - } - - 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) - } - lf, err := newLocalFile(fs, tc.Path, filepath.ToSlash(tc.Path), tc.Matcher) - 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 := ioutil.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 = ioutil.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() ([]*fsTest, func(), error) { - tmpfsdir, err := ioutil.TempDir("", "fs") - if err != nil { - return nil, nil, err - } - tmpbucketdir, err := ioutil.TempDir("", "bucket") - if err != nil { - return nil, nil, err - } - - memfs := afero.NewMemMapFs() - membucket := memblob.OpenBucket(nil) - - filefs := afero.NewBasePathFs(afero.NewOsFs(), tmpfsdir) - filebucket, err := fileblob.OpenBucket(tmpbucketdir, nil) - if err != nil { - return nil, nil, err - } - - tests := []*fsTest{ - {"mem", memfs, membucket}, - {"file", filefs, filebucket}, - } - cleanup := func() { - membucket.Close() - filebucket.Close() - os.RemoveAll(tmpfsdir) - os.RemoveAll(tmpbucketdir) - } - return tests, cleanup, nil -} - -// TestEndToEndSync verifies that basic adds, updates, and deletes are working -// correctly. -func TestEndToEndSync(t *testing.T) { - ctx := context.Background() - tests, cleanup, err := initFsTests() - if err != nil { - t.Fatal(err) - } - defer cleanup() - 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, - maxDeletes: -1, - bucket: test.bucket, - } - - // 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, cleanup, err := initFsTests() - if err != nil { - t.Fatal(err) - } - defer cleanup() - 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, - maxDeletes: -1, - bucket: test.bucket, - } - - // 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.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.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.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.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) - } - }) - } -} - -// 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, cleanup, err := initFsTests() - if err != nil { - t.Fatal(err) - } - defer cleanup() - 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, - matchers: []*matcher{{Pattern: ".*", Gzip: true, re: regexp.MustCompile(".*")}}, - } - - // 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, cleanup, err := initFsTests() - if err != nil { - t.Fatal(err) - } - defer cleanup() - 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, - matchers: []*matcher{{Pattern: "^subdir/aaa$", Force: true, re: regexp.MustCompile("^subdir/aaa$")}}, - } - - // 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.matchers = []*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 -} diff --git a/deploy/google.go b/deploy/google.go deleted file mode 100644 index be3ce52f0..000000000 --- a/deploy/google.go +++ /dev/null @@ -1,37 +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 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 deleted file mode 100644 index aaed900e5..000000000 --- a/deps/deps.go +++ /dev/null @@ -1,354 +0,0 @@ -package deps - -import ( - "sync" - "time" - - "github.com/pkg/errors" - - "github.com/gohugoio/hugo/cache/filecache" - "github.com/gohugoio/hugo/common/loggers" - "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/hugofs" - "github.com/gohugoio/hugo/langs" - "github.com/gohugoio/hugo/media" - "github.com/gohugoio/hugo/resources/page" - - "github.com/gohugoio/hugo/metrics" - "github.com/gohugoio/hugo/output" - "github.com/gohugoio/hugo/resources" - "github.com/gohugoio/hugo/source" - "github.com/gohugoio/hugo/tpl" - jww "github.com/spf13/jwalterweatherman" -) - -// Deps holds dependencies used by many. -// There will be normally only one instance of deps in play -// at a given time, i.e. one per Site built. -type Deps struct { - - // The logger to use. - Log *loggers.Logger `json:"-"` - - // Used to log errors that may repeat itself many times. - DistinctErrorLog *helpers.DistinctLogger - - // Used to log warnings that may repeat itself many times. - DistinctWarningLog *helpers.DistinctLogger - - // The templates to use. This will usually implement the full tpl.TemplateHandler. - Tmpl tpl.TemplateFinder `json:"-"` - - // We use this to parse and execute ad-hoc text templates. - TextTmpl tpl.TemplateParseFinder `json:"-"` - - // The file systems to use. - Fs *hugofs.Fs `json:"-"` - - // The PathSpec to use - *helpers.PathSpec `json:"-"` - - // 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 - Cfg config.Provider `json:"-"` - - // The file cache to use. - FileCaches filecache.Caches - - // The translation func to use - Translate func(translationID string, args ...interface{}) string `json:"-"` - - // The language in use. TODO(bep) consolidate with site - Language *langs.Language - - // The site building. - Site page.Site - - // All the output formats available for the current site. - OutputFormatsConfig output.Formats - - templateProvider ResourceProvider - WithTemplate func(templ tpl.TemplateHandler) error `json:"-"` - - translationProvider ResourceProvider - - Metrics metrics.Provider - - // Timeout is configurable in site config. - Timeout time.Duration - - // BuildStartListeners will be notified before a build starts. - BuildStartListeners *Listeners - - *globalErrHandler -} - -type globalErrHandler struct { - // Channel for some "hard to get to" build errors - buildErrors chan error -} - -// SendErr 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.buildErrors <- err: - default: - } - return - } - - jww.ERROR.Println(err) -} - -func (e *globalErrHandler) StartErrorCollector() chan error { - e.buildErrors = make(chan error, 10) - return e.buildErrors -} - -// Listeners represents an event listener. -type Listeners struct { - sync.Mutex - - // A list of funcs to be notified about an event. - listeners []func() -} - -// Add adds a function to a Listeners instance. -func (b *Listeners) Add(f func()) { - if b == nil { - return - } - b.Lock() - defer b.Unlock() - b.listeners = append(b.listeners, f) -} - -// Notify executes all listener functions. -func (b *Listeners) Notify() { - b.Lock() - defer b.Unlock() - for _, notify := range b.listeners { - notify() - } -} - -// ResourceProvider is used to create and refresh, and clone resources needed. -type ResourceProvider interface { - Update(deps *Deps) error - Clone(deps *Deps) error -} - -// TemplateHandler returns the used tpl.TemplateFinder as tpl.TemplateHandler. -func (d *Deps) TemplateHandler() tpl.TemplateHandler { - return d.Tmpl.(tpl.TemplateHandler) -} - -// LoadResources loads translations and templates. -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 errors.Wrap(err, "loading translations") - } - - if err := d.templateProvider.Update(d); err != nil { - return errors.Wrap(err, "loading templates") - } - - return nil -} - -// New initializes a Dep struct. -// Defaults are set for nil values, -// but TemplateProvider, TranslationProvider and Language are always required. -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 = loggers.NewErrorLogger() - } - - if fs == nil { - // Default to the production file system. - fs = hugofs.NewDefault(cfg.Language) - } - - if cfg.MediaTypes == nil { - cfg.MediaTypes = media.DefaultTypes - } - - if cfg.OutputFormats == nil { - cfg.OutputFormats = output.DefaultFormats - } - - ps, err := helpers.NewPathSpec(fs, cfg.Language, logger) - - if err != nil { - return nil, errors.Wrap(err, "create PathSpec") - } - - fileCaches, err := filecache.NewCaches(ps) - if err != nil { - return nil, errors.WithMessage(err, "failed to create file caches from configuration") - } - - resourceSpec, err := resources.NewSpec(ps, fileCaches, logger, cfg.OutputFormats, cfg.MediaTypes) - if err != nil { - return nil, err - } - - contentSpec, err := helpers.NewContentSpec(cfg.Language) - if err != nil { - return nil, err - } - - sp := source.NewSourceSpec(ps, fs.Source) - - timeoutms := cfg.Language.GetInt("timeout") - if timeoutms <= 0 { - timeoutms = 3000 - } - - distinctErrorLogger := helpers.NewDistinctLogger(logger.ERROR) - distinctWarnLogger := helpers.NewDistinctLogger(logger.WARN) - - d := &Deps{ - Fs: fs, - Log: logger, - DistinctErrorLog: distinctErrorLogger, - DistinctWarningLog: distinctWarnLogger, - templateProvider: cfg.TemplateProvider, - translationProvider: cfg.TranslationProvider, - WithTemplate: cfg.WithTemplate, - PathSpec: ps, - ContentSpec: contentSpec, - SourceSpec: sp, - ResourceSpec: resourceSpec, - Cfg: cfg.Language, - Language: cfg.Language, - Site: cfg.Site, - FileCaches: fileCaches, - BuildStartListeners: &Listeners{}, - Timeout: time.Duration(timeoutms) * time.Millisecond, - globalErrHandler: &globalErrHandler{}, - } - - if cfg.Cfg.GetBool("templateMetrics") { - d.Metrics = metrics.NewProvider(cfg.Cfg.GetBool("templateMetricsHints")) - } - - return d, nil -} - -// ForLanguage creates a copy of the Deps with the language dependent -// parts switched out. -func (d Deps) ForLanguage(cfg DepsCfg, onCreated func(d *Deps) error) (*Deps, error) { - l := cfg.Language - var err error - - d.PathSpec, err = helpers.NewPathSpecWithBaseBaseFsProvided(d.Fs, l, d.Log, d.BaseFs) - if err != nil { - return nil, err - } - - d.ContentSpec, err = helpers.NewContentSpec(l) - if err != nil { - return nil, err - } - - d.Site = cfg.Site - - // The resource cache is global so reuse. - // TODO(bep) clean up these inits. - resourceCache := d.ResourceSpec.ResourceCache - d.ResourceSpec, err = resources.NewSpec(d.PathSpec, d.ResourceSpec.FileCaches, d.Log, cfg.OutputFormats, cfg.MediaTypes) - if err != nil { - return nil, err - } - d.ResourceSpec.ResourceCache = resourceCache - - d.Cfg = l - d.Language = l - - if onCreated != nil { - if err = onCreated(&d); err != nil { - return nil, err - } - } - - if err := d.translationProvider.Clone(&d); err != nil { - return nil, err - } - - if err := d.templateProvider.Clone(&d); err != nil { - return nil, err - } - - d.BuildStartListeners = &Listeners{} - - 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. - Logger *loggers.Logger - - // The file systems to use - Fs *hugofs.Fs - - // The language to use. - Language *langs.Language - - // The Site in use - Site page.Site - - // The configuration to use. - Cfg config.Provider - - // The media types configured. - MediaTypes media.Types - - // The output formats configured. - OutputFormats output.Formats - - // Template handling. - TemplateProvider ResourceProvider - WithTemplate func(templ tpl.TemplateHandler) error - - // i18n handling. - TranslationProvider ResourceProvider - - // Whether we are in running (server) mode - Running bool -} diff --git a/docshelper/docs.go b/docshelper/docs.go deleted file mode 100644 index 94cb70dec..000000000 --- a/docshelper/docs.go +++ /dev/null @@ -1,37 +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 docshelper provides some helpers for the Hugo documentation, and -// is of limited interest for the general Hugo user. -package docshelper - -import ( - "encoding/json" -) - -// DocProviders contains all DocProviders added to the system. -var DocProviders = make(map[string]DocProvider) - -// AddDocProvider adds or updates the DocProvider for a given name. -func AddDocProvider(name string, provider DocProvider) { - DocProviders[name] = provider -} - -// DocProvider is used to save arbitrary JSON data -// used for the generation of the documentation. -type DocProvider func() map[string]interface{} - -// MarshalJSON returns a JSON representation of the DocProvider. -func (d DocProvider) MarshalJSON() ([]byte, error) { - return json.MarshalIndent(d(), "", " ") -} diff --git a/examples/blog/.gitignore b/examples/blog/.gitignore deleted file mode 100644 index 958340e12..000000000 --- a/examples/blog/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -# Hugo default output directory -/public - -## OS Files -# Windows -Thumbs.db -ehthumbs.db -Desktop.ini -$RECYCLE.BIN/ - -# OSX -.DS_Store diff --git a/examples/blog/README.md b/examples/blog/README.md deleted file mode 100644 index 4c68edac8..000000000 --- a/examples/blog/README.md +++ /dev/null @@ -1,42 +0,0 @@ -Hugo Example Blog -================= - -This repository provides a fully-working example of a [Hugo](https://github.com/gohugoio/hugo)-powered blog. Many -Hugo-specific features are used as a way to see them in action, and hopefully ease the learning curve for creating your -very own site with Hugo. - -Features --------- - -- Recent Posts at main index -- Indexes for `tags` and `categories` -- Post information block, with links for all `tags` and `categories` post belongs to -- [Bootstrap 3](https://getbootstrap.com/) ready - - Currently using the [Yeti](https://bootswatch.com/yeti/) theme from https://bootswatch.com/ - -Common things that should be added in the near future *(pull requests are welcome!)*: - -- Disqus integration -- More content types to demonstrate different layout methods - - About Me - - Contact - -Getting Started ---------------- - -To get started, you should simply fork or clone this repository! That's definitely an important first step. - -[Install Hugo](https://gohugo.io/getting-started/installing) in a way that best suits your environment and comfort level. - -Edit `config.toml` and change the default properties to suit your own information. This is not required to run the -example, but this is the global configuration file and you're going to need to use it eventually. Start here! - -In a command prompt or terminal, navigate to the path that contains your `config.toml` file and run `hugo`. That's it! -You should now have a `public` directory with a complete blog! Open `public/index.html` in your browser and bask. - -If that wasn't amazing enough, from the same terminal, run `hugo server`. This will watch your directories for changes -and rebuild the site immediately, *and* it will make these changes available at http://localhost:1313/ so you can view -your finished site in your browser. Go on, try it. This is one of the best ways to preview your site while working on it. - -To further learn Hugo and learn more, read through the Hugo [documentation](https://gohugo.io/getting-started/) -or browse around the files in this repository. Have fun! diff --git a/examples/blog/config.toml b/examples/blog/config.toml deleted file mode 100644 index b402f2c7f..000000000 --- a/examples/blog/config.toml +++ /dev/null @@ -1,4 +0,0 @@ -baseURL = "http://blog.hugoexample.com/" -languageCode = "en-us" -title = "Hugo Example Blog" -canonifyURLs = true diff --git a/examples/blog/content/post/another-post.md b/examples/blog/content/post/another-post.md deleted file mode 100644 index 057c2d27b..000000000 --- a/examples/blog/content/post/another-post.md +++ /dev/null @@ -1,57 +0,0 @@ -+++ -title = "Another Hugo Post" -description = "Nothing special, but one post is boring." -date = "2014-09-02" -categories = [ "example", "configuration" ] -tags = [ - "example", - "hugo", - "toml" -] -+++ - -TOML, YAML, JSON --- Oh my! -------------------------- - -One of the nifty Hugo features we should cover: flexible configuration and front matter formats! This entry has front -matter in `toml`, unlike the last one which used `yaml`, and `json` is also available if that's your preference. - - - -The `toml` front matter used on this entry: - -``` -+++ -title = "Another Hugo Post" -description = "Nothing special, but one post is boring." -date = "2014-09-02" -categories = [ "example", "configuration" ] -tags = [ - "example", - "hugo", - "toml" -] -+++ -``` - -This flexibility also extends to your site's global configuration file. You're free to use any format you prefer::simply -name the file `config.yaml`, `config.toml` or `config.json`, and go on your merry way. - -JSON Example ------------- - -How would this entry's front matter look in `json`? That's easy enough to demonstrate: - -``` -{ - "title": "Another Hugo Post", - "description": "Nothing special, but one post is boring.", - "date": "2014-09-02", - "categories": [ "example", "configuration" ], - "tags": [ - "example", - "hugo", - "toml" - ], -} -``` diff --git a/examples/blog/content/post/hello-hugo.md b/examples/blog/content/post/hello-hugo.md deleted file mode 100644 index f58886ee8..000000000 --- a/examples/blog/content/post/hello-hugo.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -title: "Hello Hugo!" -description: "Saying 'Hello' from Hugo" -date: "2014-09-01" -categories: - - "example" - - "hello" -tags: - - "example" - - "hugo" - - "blog" ---- - -Hello from Hugo! If you're reading this in your browser, good job! The file `content/post/hello-hugo.md` has been -converted into a complete HTML document by Hugo. Isn't that pretty nifty? - -A Section ---------- - -Here's a simple titled section where you can place whatever information you want. - -You can use inline HTML if you want, but really there's not much that Markdown can't do. - -Showing off with Markdown -------------------------- - -A full cheat sheet can be found [here](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) -or through [Google](https://google.com/). - -There are some *easy* examples for styling, though. I can't **emphasize** that enough. -Creating [links](https://google.com/) or `inline code` blocks are very straightforward. - -``` -There are some *easy* examples for styling, though. I can't **emphasize** that enough. -Creating [links](https://google.com/) or `inline code` blocks are very straightforward. -``` - -Front Matter for Fun --------------------- - -This is the meta data for this post. It is located at the top of the `content/post/hello-hugo.md` markdown file. - -``` ---- -title: "Hello Hugo!" -description: "Saying 'Hello' from Hugo" -date: "2014-09-01" -categories: - - "example" - - "hello" -tags: - - "example" - - "hugo" - - "blog" ---- -``` - -This section, called 'Front Matter', is what tells Hugo about the content in this file: the `title` of the item, the -`description`, and the `date` it was posted. In our example, we've added two custom bits of data too. The `categories` and -`tags` sections are used in this example for indexing/grouping content. You will learn more about what that means by -examining the code in this example and through reading the Hugo [documentation](http://gohugo.io/overview/introduction). diff --git a/examples/blog/layouts/_default/single.html b/examples/blog/layouts/_default/single.html deleted file mode 100644 index 13a53f666..000000000 --- a/examples/blog/layouts/_default/single.html +++ /dev/null @@ -1,21 +0,0 @@ -{{ partial "header.html" . }} - -{{ partial "navbar.html" . }} -
    -
    -
    -
    -

    {{ .Title }}
    {{ .Description }}

    -
    - {{ .Content }} -
    -
    - - -
    - {{ partial "menu.html" . }} -
    -
    -{{ partial "footer.copyright.html" . }} -
    -{{ partial "footer.html" . }} diff --git a/examples/blog/layouts/index.html b/examples/blog/layouts/index.html deleted file mode 100644 index a69100409..000000000 --- a/examples/blog/layouts/index.html +++ /dev/null @@ -1,19 +0,0 @@ -{{ partial "header.html" . }} - -{{ partial "navbar.html" . }} -
    -
    -
    - {{ range first 10 .Data.Pages }} - {{ .Render "summary" }} - {{ end }} -
    - - -
    - {{ partial "menu.html" . }} -
    -
    -{{ partial "footer.copyright.html" . }} -
    -{{ partial "footer.html" . }} diff --git a/examples/blog/layouts/indexes/category.html b/examples/blog/layouts/indexes/category.html deleted file mode 100644 index 653d81964..000000000 --- a/examples/blog/layouts/indexes/category.html +++ /dev/null @@ -1,24 +0,0 @@ -{{ partial "header.html" . }} - -{{ partial "navbar.html" . }} -
    -
    -
    -
    - Items in category {{ .Title | lower }} -
      - {{ range .Data.Pages }} - {{ .Render "li" }} - {{ end}} -
    -
    -
    - - -
    - {{ partial "menu.html" . }} -
    -
    -{{ partial "footer.copyright.html" . }} -
    -{{ partial "footer.html" . }} diff --git a/examples/blog/layouts/indexes/post.html b/examples/blog/layouts/indexes/post.html deleted file mode 100644 index b3a835ccd..000000000 --- a/examples/blog/layouts/indexes/post.html +++ /dev/null @@ -1,24 +0,0 @@ -{{ partial "header.html" . }} - -{{ partial "navbar.html" . }} -
    -
    -
    -
    - Blog Post Archive -
      - {{ range .Data.Pages }} - {{ .Render "li" }} - {{ end}} -
    -
    -
    - - -
    - {{ partial "menu.html" . }} -
    -
    -{{ partial "footer.copyright.html" . }} -
    -{{ partial "footer.html" . }} diff --git a/examples/blog/layouts/indexes/tag.html b/examples/blog/layouts/indexes/tag.html deleted file mode 100644 index f59b76715..000000000 --- a/examples/blog/layouts/indexes/tag.html +++ /dev/null @@ -1,24 +0,0 @@ -{{ partial "header.html" . }} - -{{ partial "navbar.html" . }} -
    -
    -
    -
    - Items with tag {{ .Title | lower }} -
      - {{ range .Data.Pages }} - {{ .Render "li" }} - {{ end}} -
    -
    -
    - - -
    - {{ partial "menu.html" . }} -
    -
    -{{ partial "footer.copyright.html" . }} -
    -{{ partial "footer.html" . }} diff --git a/examples/blog/layouts/partials/footer.copyright.html b/examples/blog/layouts/partials/footer.copyright.html deleted file mode 100644 index 7f7b5fc7b..000000000 --- a/examples/blog/layouts/partials/footer.copyright.html +++ /dev/null @@ -1,9 +0,0 @@ -
    -
    -
    -
    -

    © Enthusiastic Hugo User {{ now.Format "2006" }} · - Built with Hugo

    -
    -
    -
    diff --git a/examples/blog/layouts/partials/footer.html b/examples/blog/layouts/partials/footer.html deleted file mode 100644 index 8945fa4ed..000000000 --- a/examples/blog/layouts/partials/footer.html +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/examples/blog/layouts/partials/header.html b/examples/blog/layouts/partials/header.html deleted file mode 100644 index 5863e59ec..000000000 --- a/examples/blog/layouts/partials/header.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - {{ partial "meta.html" . }} - - {{ .Title }} - {{ .Site.BaseURL }} - - {{ partial "header.includes.html" . }} - {{ with .OutputFormats.Get "RSS" -}} - {{ printf "" .Permalink .MediaType .Title | safeHTML }} - {{- end }} - diff --git a/examples/blog/layouts/partials/header.includes.html b/examples/blog/layouts/partials/header.includes.html deleted file mode 100644 index 767e3eee1..000000000 --- a/examples/blog/layouts/partials/header.includes.html +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/examples/blog/layouts/partials/menu.html b/examples/blog/layouts/partials/menu.html deleted file mode 100644 index 61ce0c6b5..000000000 --- a/examples/blog/layouts/partials/menu.html +++ /dev/null @@ -1,15 +0,0 @@ -
    -
    -

    Connect. Socialize.

    -
    -
    - - - -
    - - Hey, listen!
    - You should modify the layouts/partials/menu.html template and include your own profile links. -
    -
    -
    diff --git a/examples/blog/layouts/partials/meta.html b/examples/blog/layouts/partials/meta.html deleted file mode 100644 index 95fd2a711..000000000 --- a/examples/blog/layouts/partials/meta.html +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/examples/blog/layouts/partials/navbar.html b/examples/blog/layouts/partials/navbar.html deleted file mode 100644 index b15c24630..000000000 --- a/examples/blog/layouts/partials/navbar.html +++ /dev/null @@ -1,22 +0,0 @@ - diff --git a/examples/blog/layouts/post/li.html b/examples/blog/layouts/post/li.html deleted file mode 100644 index d57be9c80..000000000 --- a/examples/blog/layouts/post/li.html +++ /dev/null @@ -1,4 +0,0 @@ -
  • -
    {{ .Title}}
    - posted on {{ .Date.Format "January 2, 2006" }}
    -
  • \ No newline at end of file diff --git a/examples/blog/layouts/post/single.html b/examples/blog/layouts/post/single.html deleted file mode 100644 index 4b792ebe2..000000000 --- a/examples/blog/layouts/post/single.html +++ /dev/null @@ -1,35 +0,0 @@ -{{ partial "header.html" . }} - -{{ partial "navbar.html" . }} -
    -
    -
    -
    -

    {{ .Title }}
    {{ .Description }}

    -
    - {{ .Content }} -
    -
    - - -
    -
    -

    {{ .Date.Format "January 2, 2006" }}
    - {{ .WordCount }} words

    -
    - Categories -
      - {{ range .Params.categories }} -
    • {{ . }}
    • - {{ end }} -
    -
    - Tags
    - {{ range .Params.tags }}{{ . }} {{ end }} -
    - {{ partial "menu.html" . }} -
    -
    -{{ partial "footer.copyright.html" . }} -
    -{{ partial "footer.html" . }} diff --git a/examples/blog/layouts/post/summary.html b/examples/blog/layouts/post/summary.html deleted file mode 100644 index f70b6827a..000000000 --- a/examples/blog/layouts/post/summary.html +++ /dev/null @@ -1,9 +0,0 @@ -
    -

    - {{ .Title }} Posted on {{ .Date.Format "Jan 2, 2006" }}
    - {{ .Description }} -

    -
    -

    {{ .Summary }}

    - Read More -
    \ No newline at end of file diff --git a/examples/blog/static/css/bootstrap.min.css b/examples/blog/static/css/bootstrap.min.css deleted file mode 100644 index 70829d0d3..000000000 --- a/examples/blog/static/css/bootstrap.min.css +++ /dev/null @@ -1,11 +0,0 @@ -@import url("https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,700italic,400,300,700");/*! - * bootswatch v3.3.6 - * Homepage: http://bootswatch.com - * Copyright 2012-2015 Thomas Park - * Licensed under MIT - * Based on Bootstrap -*//*! - * 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;-ms-text-size-adjust:100%;-webkit-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}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,*:before,*:after{background:transparent !important;color:#000 !important;-webkit-box-shadow:none !important;box-shadow:none !important;text-shadow:none !important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}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}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000 !important}.label{border:1px solid #000}.table{border-collapse:collapse !important}.table td,.table th{background-color:#fff !important}.table-bordered th,.table-bordered td{border:1px solid #ddd !important}}@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')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-euro:before,.glyphicon-eur:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-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:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#222222;background-color:#ffffff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#008cba;text-decoration:none}a:hover,a:focus{color:#008cba;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}.img-responsive,.thumbnail>img,.thumbnail a>img,.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:0}.img-thumbnail{padding:4px;line-height:1.4;background-color:#ffffff;border:1px solid #dddddd;border-radius:0;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:21px;margin-bottom:21px;border:0;border-top:1px solid #dddddd}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role="button"]{cursor:pointer}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:normal;line-height:1;color:#999999}h1,.h1,h2,.h2,h3,.h3{margin-top:21px;margin-bottom:10.5px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10.5px;margin-bottom:10.5px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:39px}h2,.h2{font-size:32px}h3,.h3{font-size:26px}h4,.h4{font-size:19px}h5,.h5{font-size:15px}h6,.h6{font-size:13px}p{margin:0 0 10.5px}.lead{margin-bottom:21px;font-size:17px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:22.5px}}small,.small{font-size:80%}mark,.mark{background-color:#fcf8e3;padding:.2em}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#999999}.text-primary{color:#008cba}a.text-primary:hover,a.text-primary:focus{color:#006687}.text-success{color:#43ac6a}a.text-success:hover,a.text-success:focus{color:#358753}.text-info{color:#5bc0de}a.text-info:hover,a.text-info:focus{color:#31b0d5}.text-warning{color:#e99002}a.text-warning:hover,a.text-warning:focus{color:#b67102}.text-danger{color:#f04124}a.text-danger:hover,a.text-danger:focus{color:#d32a0e}.bg-primary{color:#fff;background-color:#008cba}a.bg-primary:hover,a.bg-primary:focus{background-color:#006687}.bg-success{background-color:#dff0d8}a.bg-success:hover,a.bg-success:focus{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:hover,a.bg-info:focus{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover,a.bg-warning:focus{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:hover,a.bg-danger:focus{background-color:#e4b9b9}.page-header{padding-bottom:9.5px;margin:42px 0 21px;border-bottom:1px solid #dddddd}ul,ol{margin-top:0;margin-bottom:10.5px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}dl{margin-top:0;margin-bottom:21px}dt,dd{line-height:1.4}dt{font-weight:bold}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999999}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10.5px 21px;margin:0 0 21px;font-size:18.75px;border-left:5px solid #dddddd}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.4;color:#6f6f6f}blockquote footer:before,blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #dddddd;border-left:0;text-align:right}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:''}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}address{margin-bottom:21px;font-style:normal;line-height:1.4}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:0}kbd{padding:2px 4px;font-size:90%;color:#ffffff;background-color:#333333;border-radius:0;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.25)}kbd kbd{padding:0;font-size:100%;font-weight:bold;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:10px;margin:0 0 10.5px;font-size:14px;line-height:1.4;word-break:break-all;word-wrap:break-word;color:#333333;background-color:#f5f5f5;border:1px solid #cccccc;border-radius:0}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.row{margin-left:-15px;margin-right:-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-left:15px;padding-right: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:#999999;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:21px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.4;vertical-align:top;border-top:1px solid #dddddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #dddddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #dddddd}.table .table{background-color:#ffffff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #dddddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #dddddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*="col-"]{position:static;float:none;display:table-column}table td[class*="col-"],table th[class*="col-"]{position:static;float:none;display:table-cell}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#dff0d8}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr.success:hover>th{background-color:#d0e9c6}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#d9edf7}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr.info:hover>th{background-color:#c4e3f3}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr.warning:hover>th{background-color:#faf2cc}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#f2dede}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr.danger:hover>th{background-color:#ebcccc}.table-responsive{overflow-x:auto;min-height:0.01%}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15.75px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #dddddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0;min-width:0}legend{display:block;width:100%;padding:0;margin-bottom:21px;font-size:22.5px;line-height:inherit;color:#333333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:bold}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type="file"]{display:block}input[type="range"]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:9px;font-size:15px;line-height:1.4;color:#6f6f6f}.form-control{display:block;width:100%;height:39px;padding:8px 12px;font-size:15px;line-height:1.4;color:#6f6f6f;background-color:#ffffff;background-image:none;border:1px solid #cccccc;border-radius:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6)}.form-control::-moz-placeholder{color:#999999;opacity:1}.form-control:-ms-input-placeholder{color:#999999}.form-control::-webkit-input-placeholder{color:#999999}.form-control::-ms-expand{border:0;background-color:transparent}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eeeeee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type="search"]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type="date"].form-control,input[type="time"].form-control,input[type="datetime-local"].form-control,input[type="month"].form-control{line-height:39px}input[type="date"].input-sm,input[type="time"].input-sm,input[type="datetime-local"].input-sm,input[type="month"].input-sm,.input-group-sm input[type="date"],.input-group-sm input[type="time"],.input-group-sm input[type="datetime-local"],.input-group-sm input[type="month"]{line-height:36px}input[type="date"].input-lg,input[type="time"].input-lg,input[type="datetime-local"].input-lg,input[type="month"].input-lg,.input-group-lg input[type="date"],.input-group-lg input[type="time"],.input-group-lg input[type="datetime-local"],.input-group-lg input[type="month"]{line-height:60px}}.form-group{margin-bottom:15px}.radio,.checkbox{position:relative;display:block;margin-top:10px;margin-bottom:10px}.radio label,.checkbox label{min-height:21px;padding-left:20px;margin-bottom:0;font-weight:normal;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{position:absolute;margin-left:-20px;margin-top:4px \9}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:normal;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"].disabled,input[type="checkbox"].disabled,fieldset[disabled] input[type="radio"],fieldset[disabled] input[type="checkbox"]{cursor:not-allowed}.radio-inline.disabled,.checkbox-inline.disabled,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.radio.disabled label,.checkbox.disabled label,fieldset[disabled] .radio label,fieldset[disabled] .checkbox label{cursor:not-allowed}.form-control-static{padding-top:9px;padding-bottom:9px;margin-bottom:0;min-height:36px}.form-control-static.input-lg,.form-control-static.input-sm{padding-left:0;padding-right:0}.input-sm{height:36px;padding:8px 12px;font-size:12px;line-height:1.5;border-radius:0}select.input-sm{height:36px;line-height:36px}textarea.input-sm,select[multiple].input-sm{height:auto}.form-group-sm .form-control{height:36px;padding:8px 12px;font-size:12px;line-height:1.5;border-radius:0}.form-group-sm select.form-control{height:36px;line-height:36px}.form-group-sm textarea.form-control,.form-group-sm select[multiple].form-control{height:auto}.form-group-sm .form-control-static{height:36px;min-height:33px;padding:9px 12px;font-size:12px;line-height:1.5}.input-lg{height:60px;padding:16px 20px;font-size:19px;line-height:1.3333333;border-radius:0}select.input-lg{height:60px;line-height:60px}textarea.input-lg,select[multiple].input-lg{height:auto}.form-group-lg .form-control{height:60px;padding:16px 20px;font-size:19px;line-height:1.3333333;border-radius:0}.form-group-lg select.form-control{height:60px;line-height:60px}.form-group-lg textarea.form-control,.form-group-lg select[multiple].form-control{height:auto}.form-group-lg .form-control-static{height:60px;min-height:40px;padding:17px 20px;font-size:19px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:48.75px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:39px;height:39px;line-height:39px;text-align:center;pointer-events:none}.input-lg+.form-control-feedback,.input-group-lg+.form-control-feedback,.form-group-lg .form-control+.form-control-feedback{width:60px;height:60px;line-height:60px}.input-sm+.form-control-feedback,.input-group-sm+.form-control-feedback,.form-group-sm .form-control+.form-control-feedback{width:36px;height:36px;line-height:36px}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline,.has-success.radio label,.has-success.checkbox label,.has-success.radio-inline label,.has-success.checkbox-inline label{color:#43ac6a}.has-success .form-control{border-color:#43ac6a;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-success .form-control:focus{border-color:#358753;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #85d0a1;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #85d0a1}.has-success .input-group-addon{color:#43ac6a;border-color:#43ac6a;background-color:#dff0d8}.has-success .form-control-feedback{color:#43ac6a}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline,.has-warning.radio label,.has-warning.checkbox label,.has-warning.radio-inline label,.has-warning.checkbox-inline label{color:#e99002}.has-warning .form-control{border-color:#e99002;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-warning .form-control:focus{border-color:#b67102;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #febc53;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #febc53}.has-warning .input-group-addon{color:#e99002;border-color:#e99002;background-color:#fcf8e3}.has-warning .form-control-feedback{color:#e99002}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline,.has-error.radio label,.has-error.checkbox label,.has-error.radio-inline label,.has-error.checkbox-inline label{color:#f04124}.has-error .form-control{border-color:#f04124;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-error .form-control:focus{border-color:#d32a0e;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #f79483;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #f79483}.has-error .input-group-addon{color:#f04124;border-color:#f04124;background-color:#f2dede}.has-error .form-control-feedback{color:#f04124}.has-feedback label~.form-control-feedback{top:26px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#626262}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn,.form-inline .input-group .form-control{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .radio label,.form-inline .checkbox label{padding-left:0}.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:0;margin-bottom:0;padding-top:9px}.form-horizontal .radio,.form-horizontal .checkbox{min-height:30px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}@media (min-width:768px){.form-horizontal .control-label{text-align:right;margin-bottom:0;padding-top:9px}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:17px;font-size:19px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:9px;font-size:12px}}.btn{display:inline-block;margin-bottom:0;font-weight:normal;text-align:center;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;padding:8px 12px;font-size:15px;line-height:1.4;border-radius:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn.active.focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus,.btn.focus{color:#333333;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;opacity:0.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333333;background-color:#e7e7e7;border-color:#cccccc}.btn-default:focus,.btn-default.focus{color:#333333;background-color:#cecece;border-color:#8c8c8c}.btn-default:hover{color:#333333;background-color:#cecece;border-color:#adadad}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{color:#333333;background-color:#cecece;border-color:#adadad}.btn-default:active:hover,.btn-default.active:hover,.open>.dropdown-toggle.btn-default:hover,.btn-default:active:focus,.btn-default.active:focus,.open>.dropdown-toggle.btn-default:focus,.btn-default:active.focus,.btn-default.active.focus,.open>.dropdown-toggle.btn-default.focus{color:#333333;background-color:#bcbcbc;border-color:#8c8c8c}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled.focus,.btn-default[disabled].focus,fieldset[disabled] .btn-default.focus{background-color:#e7e7e7;border-color:#cccccc}.btn-default .badge{color:#e7e7e7;background-color:#333333}.btn-primary{color:#ffffff;background-color:#008cba;border-color:#0079a1}.btn-primary:focus,.btn-primary.focus{color:#ffffff;background-color:#006687;border-color:#001921}.btn-primary:hover{color:#ffffff;background-color:#006687;border-color:#004b63}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#ffffff;background-color:#006687;border-color:#004b63}.btn-primary:active:hover,.btn-primary.active:hover,.open>.dropdown-toggle.btn-primary:hover,.btn-primary:active:focus,.btn-primary.active:focus,.open>.dropdown-toggle.btn-primary:focus,.btn-primary:active.focus,.btn-primary.active.focus,.open>.dropdown-toggle.btn-primary.focus{color:#ffffff;background-color:#004b63;border-color:#001921}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus{background-color:#008cba;border-color:#0079a1}.btn-primary .badge{color:#008cba;background-color:#ffffff}.btn-success{color:#ffffff;background-color:#43ac6a;border-color:#3c9a5f}.btn-success:focus,.btn-success.focus{color:#ffffff;background-color:#358753;border-color:#183e26}.btn-success:hover{color:#ffffff;background-color:#358753;border-color:#2b6e44}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{color:#ffffff;background-color:#358753;border-color:#2b6e44}.btn-success:active:hover,.btn-success.active:hover,.open>.dropdown-toggle.btn-success:hover,.btn-success:active:focus,.btn-success.active:focus,.open>.dropdown-toggle.btn-success:focus,.btn-success:active.focus,.btn-success.active.focus,.open>.dropdown-toggle.btn-success.focus{color:#ffffff;background-color:#2b6e44;border-color:#183e26}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled.focus,.btn-success[disabled].focus,fieldset[disabled] .btn-success.focus{background-color:#43ac6a;border-color:#3c9a5f}.btn-success .badge{color:#43ac6a;background-color:#ffffff}.btn-info{color:#ffffff;background-color:#5bc0de;border-color:#46b8da}.btn-info:focus,.btn-info.focus{color:#ffffff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#ffffff;background-color:#31b0d5;border-color:#269abc}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{color:#ffffff;background-color:#31b0d5;border-color:#269abc}.btn-info:active:hover,.btn-info.active:hover,.open>.dropdown-toggle.btn-info:hover,.btn-info:active:focus,.btn-info.active:focus,.open>.dropdown-toggle.btn-info:focus,.btn-info:active.focus,.btn-info.active.focus,.open>.dropdown-toggle.btn-info.focus{color:#ffffff;background-color:#269abc;border-color:#1b6d85}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled.focus,.btn-info[disabled].focus,fieldset[disabled] .btn-info.focus{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#ffffff}.btn-warning{color:#ffffff;background-color:#e99002;border-color:#d08002}.btn-warning:focus,.btn-warning.focus{color:#ffffff;background-color:#b67102;border-color:#513201}.btn-warning:hover{color:#ffffff;background-color:#b67102;border-color:#935b01}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{color:#ffffff;background-color:#b67102;border-color:#935b01}.btn-warning:active:hover,.btn-warning.active:hover,.open>.dropdown-toggle.btn-warning:hover,.btn-warning:active:focus,.btn-warning.active:focus,.open>.dropdown-toggle.btn-warning:focus,.btn-warning:active.focus,.btn-warning.active.focus,.open>.dropdown-toggle.btn-warning.focus{color:#ffffff;background-color:#935b01;border-color:#513201}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled.focus,.btn-warning[disabled].focus,fieldset[disabled] .btn-warning.focus{background-color:#e99002;border-color:#d08002}.btn-warning .badge{color:#e99002;background-color:#ffffff}.btn-danger{color:#ffffff;background-color:#f04124;border-color:#ea2f10}.btn-danger:focus,.btn-danger.focus{color:#ffffff;background-color:#d32a0e;border-color:#731708}.btn-danger:hover{color:#ffffff;background-color:#d32a0e;border-color:#b1240c}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{color:#ffffff;background-color:#d32a0e;border-color:#b1240c}.btn-danger:active:hover,.btn-danger.active:hover,.open>.dropdown-toggle.btn-danger:hover,.btn-danger:active:focus,.btn-danger.active:focus,.open>.dropdown-toggle.btn-danger:focus,.btn-danger:active.focus,.btn-danger.active.focus,.open>.dropdown-toggle.btn-danger.focus{color:#ffffff;background-color:#b1240c;border-color:#731708}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled.focus,.btn-danger[disabled].focus,fieldset[disabled] .btn-danger.focus{background-color:#f04124;border-color:#ea2f10}.btn-danger .badge{color:#f04124;background-color:#ffffff}.btn-link{color:#008cba;font-weight:normal;border-radius:0}.btn-link,.btn-link:active,.btn-link.active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#008cba;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#999999;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:16px 20px;font-size:19px;line-height:1.3333333;border-radius:0}.btn-sm,.btn-group-sm>.btn{padding:8px 12px;font-size:12px;line-height:1.5;border-radius:0}.btn-xs,.btn-group-xs>.btn{padding:4px 6px;font-size:12px;line-height:1.5;border-radius:0}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity 0.15s linear;-o-transition:opacity 0.15s linear;transition:opacity 0.15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-property:height, visibility;-o-transition-property:height, visibility;transition-property:height, visibility;-webkit-transition-duration:0.35s;-o-transition-duration:0.35s;transition-duration:0.35s;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid \9;border-right:4px solid transparent;border-left:4px solid transparent}.dropup,.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;font-size:15px;text-align:left;background-color:#ffffff;border:1px solid #cccccc;border:1px solid rgba(0,0,0,0.15);border-radius:0;-webkit-box-shadow:0 6px 12px rgba(0,0,0,0.175);box-shadow:0 6px 12px rgba(0,0,0,0.175);-webkit-background-clip:padding-box;background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9.5px 0;overflow:hidden;background-color:rgba(0,0,0,0.2)}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:1.4;color:#555555;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{text-decoration:none;color:#262626;background-color:#eeeeee}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#ffffff;text-decoration:none;outline:0;background-color:#008cba}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);cursor:not-allowed}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{left:auto;right:0}.dropdown-menu-left{left:0;right:auto}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.4;color:#999999;white-space:nowrap}.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px dashed;border-bottom:4px solid \9;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{left:auto;right:0}.navbar-right .dropdown-menu-left{left:0;right:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:0;border-top-left-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-right-radius:0;border-top-left-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle="buttons"]>.btn input[type="radio"],[data-toggle="buttons"]>.btn-group>.btn input[type="radio"],[data-toggle="buttons"]>.btn input[type="checkbox"],[data-toggle="buttons"]>.btn-group>.btn input[type="checkbox"]{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*="col-"]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:60px;padding:16px 20px;font-size:19px;line-height:1.3333333;border-radius:0}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:60px;line-height:60px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:36px;padding:8px 12px;font-size:12px;line-height:1.5;border-radius:0}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:36px;line-height:36px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:8px 12px;font-size:15px;font-weight:normal;line-height:1;color:#6f6f6f;text-align:center;background-color:#eeeeee;border:1px solid #cccccc;border-radius:0}.input-group-addon.input-sm{padding:8px 12px;font-size:12px;border-radius:0}.input-group-addon.input-lg{padding:16px 20px;font-size:19px;border-radius:0}.input-group-addon input[type="radio"],.input-group-addon input[type="checkbox"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{margin-bottom:0;padding-left:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eeeeee}.nav>li.disabled>a{color:#999999}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#999999;text-decoration:none;background-color:transparent;cursor:not-allowed}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eeeeee;border-color:#008cba}.nav .nav-divider{height:1px;margin:9.5px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #dddddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.4;border:1px solid transparent;border-radius:0 0 0 0}.nav-tabs>li>a:hover{border-color:#eeeeee #eeeeee #dddddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#6f6f6f;background-color:#ffffff;border:1px solid #dddddd;border-bottom-color:transparent;cursor:default}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #dddddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #dddddd;border-radius:0 0 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#ffffff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:0}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#ffffff;background-color:#008cba}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #dddddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #dddddd;border-radius:0 0 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#ffffff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:45px;margin-bottom:21px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:0}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{overflow-x:visible;padding-right:15px;padding-left:15px;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block !important;height:auto !important;padding-bottom:0;overflow:visible !important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-left:0;padding-right:0}}.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:200px}}.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:12px 15px;font-size:19px;line-height:21px;height:45px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;margin-right:15px;padding:9px 10px;margin-top:5.5px;margin-bottom:5.5px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:0}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:6px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:21px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:21px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:12px;padding-bottom:12px}}.navbar-form{margin-left:-15px;margin-right:-15px;padding:10px 15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);margin-top:3px;margin-bottom:3px}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn,.navbar-form .input-group .form-control{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .radio label,.navbar-form .checkbox label{padding-left:0}.navbar-form .radio input[type="radio"],.navbar-form .checkbox input[type="checkbox"]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;border:0;margin-left:0;margin-right:0;padding-top:0;padding-bottom:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-right-radius:0;border-top-left-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:3px;margin-bottom:3px}.navbar-btn.btn-sm{margin-top:4.5px;margin-bottom:4.5px}.navbar-btn.btn-xs{margin-top:11.5px;margin-bottom:11.5px}.navbar-text{margin-top:12px;margin-bottom:12px}@media (min-width:768px){.navbar-text{float:left;margin-left:15px;margin-right:15px}}@media (min-width:768px){.navbar-left{float:left !important}.navbar-right{float:right !important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#333333;border-color:#222222}.navbar-default .navbar-brand{color:#ffffff}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#ffffff;background-color:transparent}.navbar-default .navbar-text{color:#ffffff}.navbar-default .navbar-nav>li>a{color:#ffffff}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#ffffff;background-color:#272727}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#ffffff;background-color:#272727}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#cccccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:transparent}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:transparent}.navbar-default .navbar-toggle .icon-bar{background-color:#ffffff}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#222222}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{background-color:#272727;color:#ffffff}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#ffffff}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#ffffff;background-color:#272727}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#ffffff;background-color:#272727}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#cccccc;background-color:transparent}}.navbar-default .navbar-link{color:#ffffff}.navbar-default .navbar-link:hover{color:#ffffff}.navbar-default .btn-link{color:#ffffff}.navbar-default .btn-link:hover,.navbar-default .btn-link:focus{color:#ffffff}.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:hover,.navbar-default .btn-link[disabled]:focus,fieldset[disabled] .navbar-default .btn-link:focus{color:#cccccc}.navbar-inverse{background-color:#008cba;border-color:#006687}.navbar-inverse .navbar-brand{color:#ffffff}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#ffffff;background-color:transparent}.navbar-inverse .navbar-text{color:#ffffff}.navbar-inverse .navbar-nav>li>a{color:#ffffff}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#ffffff;background-color:#006687}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#ffffff;background-color:#006687}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:transparent}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:transparent}.navbar-inverse .navbar-toggle .icon-bar{background-color:#ffffff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#007196}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{background-color:#006687;color:#ffffff}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#006687}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#006687}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#ffffff}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#ffffff;background-color:#006687}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#ffffff;background-color:#006687}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444444;background-color:transparent}}.navbar-inverse .navbar-link{color:#ffffff}.navbar-inverse .navbar-link:hover{color:#ffffff}.navbar-inverse .btn-link{color:#ffffff}.navbar-inverse .btn-link:hover,.navbar-inverse .btn-link:focus{color:#ffffff}.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:hover,.navbar-inverse .btn-link[disabled]:focus,fieldset[disabled] .navbar-inverse .btn-link:focus{color:#444444}.breadcrumb{padding:8px 15px;margin-bottom:21px;list-style:none;background-color:#f5f5f5;border-radius:0}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{content:"/\00a0";padding:0 5px;color:#999999}.breadcrumb>.active{color:#333333}.pagination{display:inline-block;padding-left:0;margin:21px 0;border-radius:0}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:8px 12px;line-height:1.4;text-decoration:none;color:#008cba;background-color:transparent;border:1px solid transparent;margin-left:-1px}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:0;border-top-left-radius:0}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:0;border-top-right-radius:0}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{z-index:2;color:#008cba;background-color:#eeeeee;border-color:transparent}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:3;color:#ffffff;background-color:#008cba;border-color:transparent;cursor:default}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#999999;background-color:#ffffff;border-color:transparent;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:16px 20px;font-size:19px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:0;border-top-left-radius:0}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:0;border-top-right-radius:0}.pagination-sm>li>a,.pagination-sm>li>span{padding:8px 12px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:0;border-top-left-radius:0}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:0;border-top-right-radius:0}.pager{padding-left:0;margin:21px 0;list-style:none;text-align:center}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:transparent;border:1px solid transparent;border-radius:3px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eeeeee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999999;background-color:transparent;cursor:not-allowed}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:bold;line-height:1;color:#ffffff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:hover,a.label:focus{color:#ffffff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#999999}.label-default[href]:hover,.label-default[href]:focus{background-color:#808080}.label-primary{background-color:#008cba}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#006687}.label-success{background-color:#43ac6a}.label-success[href]:hover,.label-success[href]:focus{background-color:#358753}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#e99002}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#b67102}.label-danger{background-color:#f04124}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#d32a0e}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:bold;color:#ffffff;line-height:1;vertical-align:middle;white-space:nowrap;text-align:center;background-color:#008cba;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge,.btn-group-xs>.btn .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#ffffff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#008cba;background-color:#ffffff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#fafafa}.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:23px;font-weight:200}.jumbotron>hr{border-top-color:#e1e1e1}.container .jumbotron,.container-fluid .jumbotron{border-radius:0;padding-left:15px;padding-right:15px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-left:60px;padding-right:60px}.jumbotron h1,.jumbotron .h1{font-size:68px}}.thumbnail{display:block;padding:4px;margin-bottom:21px;line-height:1.4;background-color:#ffffff;border:1px solid #dddddd;border-radius:0;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail>img,.thumbnail a>img{margin-left:auto;margin-right:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#008cba}.thumbnail .caption{padding:9px;color:#222222}.alert{padding:15px;margin-bottom:21px;border:1px solid transparent;border-radius:0}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{background-color:#43ac6a;border-color:#3c9a5f;color:#ffffff}.alert-success hr{border-top-color:#358753}.alert-success .alert-link{color:#e6e6e6}.alert-info{background-color:#5bc0de;border-color:#3db5d8;color:#ffffff}.alert-info hr{border-top-color:#2aabd2}.alert-info .alert-link{color:#e6e6e6}.alert-warning{background-color:#e99002;border-color:#d08002;color:#ffffff}.alert-warning hr{border-top-color:#b67102}.alert-warning .alert-link{color:#e6e6e6}.alert-danger{background-color:#f04124;border-color:#ea2f10;color:#ffffff}.alert-danger hr{border-top-color:#d32a0e}.alert-danger .alert-link{color:#e6e6e6}@-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}}.progress{overflow:hidden;height:21px;margin-bottom:21px;background-color:#f5f5f5;border-radius:0;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress-bar{float:left;width:0%;height:100%;font-size:12px;line-height:21px;color:#ffffff;text-align:center;background-color:#008cba;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-transition:width 0.6s ease;-o-transition:width 0.6s ease;transition:width 0.6s ease}.progress-striped .progress-bar,.progress-bar-striped{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress.active .progress-bar,.progress-bar.active{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#43ac6a}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-warning{background-color:#e99002}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-danger{background-color:#f04124}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{zoom:1;overflow:hidden}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-left,.media-right,.media-body{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{margin-bottom:20px;padding-left:0}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#ffffff;border:1px solid #dddddd}.list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}a.list-group-item,button.list-group-item{color:#555555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333333}a.list-group-item:hover,button.list-group-item:hover,a.list-group-item:focus,button.list-group-item:focus{text-decoration:none;color:#555555;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:hover,.list-group-item.disabled:focus{background-color:#eeeeee;color:#999999;cursor:not-allowed}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text{color:#999999}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{z-index:2;color:#ffffff;background-color:#008cba;border-color:#008cba}.list-group-item.active .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>.small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:hover .list-group-item-text,.list-group-item.active:focus .list-group-item-text{color:#87e1ff}.list-group-item-success{color:#43ac6a;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#43ac6a}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,button.list-group-item-success:hover,a.list-group-item-success:focus,button.list-group-item-success:focus{color:#43ac6a;background-color:#d0e9c6}a.list-group-item-success.active,button.list-group-item-success.active,a.list-group-item-success.active:hover,button.list-group-item-success.active:hover,a.list-group-item-success.active:focus,button.list-group-item-success.active:focus{color:#fff;background-color:#43ac6a;border-color:#43ac6a}.list-group-item-info{color:#5bc0de;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#5bc0de}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,button.list-group-item-info:hover,a.list-group-item-info:focus,button.list-group-item-info:focus{color:#5bc0de;background-color:#c4e3f3}a.list-group-item-info.active,button.list-group-item-info.active,a.list-group-item-info.active:hover,button.list-group-item-info.active:hover,a.list-group-item-info.active:focus,button.list-group-item-info.active:focus{color:#fff;background-color:#5bc0de;border-color:#5bc0de}.list-group-item-warning{color:#e99002;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#e99002}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,button.list-group-item-warning:hover,a.list-group-item-warning:focus,button.list-group-item-warning:focus{color:#e99002;background-color:#faf2cc}a.list-group-item-warning.active,button.list-group-item-warning.active,a.list-group-item-warning.active:hover,button.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus,button.list-group-item-warning.active:focus{color:#fff;background-color:#e99002;border-color:#e99002}.list-group-item-danger{color:#f04124;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#f04124}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,button.list-group-item-danger:hover,a.list-group-item-danger:focus,button.list-group-item-danger:focus{color:#f04124;background-color:#ebcccc}a.list-group-item-danger.active,button.list-group-item-danger.active,a.list-group-item-danger.active:hover,button.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus,button.list-group-item-danger.active:focus{color:#fff;background-color:#f04124;border-color:#f04124}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:21px;background-color:#ffffff;border:1px solid transparent;border-radius:0;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:-1;border-top-left-radius:-1}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:17px;color:inherit}.panel-title>a,.panel-title>small,.panel-title>.small,.panel-title>small>a,.panel-title>.small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #dddddd;border-bottom-right-radius:-1;border-bottom-left-radius:-1}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-right-radius:-1;border-top-left-radius:-1}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:-1;border-bottom-left-radius:-1}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.table,.panel>.table-responsive>.table,.panel>.panel-collapse>.table{margin-bottom:0}.panel>.table caption,.panel>.table-responsive>.table caption,.panel>.panel-collapse>.table caption{padding-left:15px;padding-right:15px}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-right-radius:-1;border-top-left-radius:-1}.panel>.table:first-child>thead:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child{border-top-left-radius:-1;border-top-right-radius:-1}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:-1}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:-1}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:-1;border-bottom-left-radius:-1}.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-left-radius:-1;border-bottom-right-radius:-1}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:-1}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:-1}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #dddddd}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{border:0;margin-bottom:0}.panel-group{margin-bottom:21px}.panel-group .panel{margin-bottom:0;border-radius:0}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.panel-body,.panel-group .panel-heading+.panel-collapse>.list-group{border-top:1px solid #dddddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #dddddd}.panel-default{border-color:#dddddd}.panel-default>.panel-heading{color:#333333;background-color:#f5f5f5;border-color:#dddddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#dddddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#dddddd}.panel-primary{border-color:#008cba}.panel-primary>.panel-heading{color:#ffffff;background-color:#008cba;border-color:#008cba}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#008cba}.panel-primary>.panel-heading .badge{color:#008cba;background-color:#ffffff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#008cba}.panel-success{border-color:#3c9a5f}.panel-success>.panel-heading{color:#ffffff;background-color:#43ac6a;border-color:#3c9a5f}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#3c9a5f}.panel-success>.panel-heading .badge{color:#43ac6a;background-color:#ffffff}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#3c9a5f}.panel-info{border-color:#3db5d8}.panel-info>.panel-heading{color:#ffffff;background-color:#5bc0de;border-color:#3db5d8}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#3db5d8}.panel-info>.panel-heading .badge{color:#5bc0de;background-color:#ffffff}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#3db5d8}.panel-warning{border-color:#d08002}.panel-warning>.panel-heading{color:#ffffff;background-color:#e99002;border-color:#d08002}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d08002}.panel-warning>.panel-heading .badge{color:#e99002;background-color:#ffffff}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d08002}.panel-danger{border-color:#ea2f10}.panel-danger>.panel-heading{color:#ffffff;background-color:#f04124;border-color:#ea2f10}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ea2f10}.panel-danger>.panel-heading .badge{color:#f04124;background-color:#ffffff}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ea2f10}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object,.embed-responsive video{position:absolute;top:0;left:0;bottom:0;height:100%;width:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#fafafa;border:1px solid #e8e8e8;border-radius:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-lg{padding:24px;border-radius:0}.well-sm{padding:9px;border-radius:0}.close{float:right;font-size:22.5px;font-weight:bold;line-height:1;color:#ffffff;text-shadow:0 1px 0 #ffffff;opacity:0.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#ffffff;text-decoration:none;cursor:pointer;opacity:0.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{display:none;overflow:hidden;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0, -25%);-ms-transform:translate(0, -25%);-o-transform:translate(0, -25%);transform:translate(0, -25%);-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#ffffff;border:1px solid #999999;border:1px solid rgba(0,0,0,0.2);border-radius:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,0.5);box-shadow:0 3px 9px rgba(0,0,0,0.5);-webkit-background-clip:padding-box;background-clip:padding-box;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:0.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.4}.modal-body{position:relative;padding:20px}.modal-footer{padding:20px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,0.5);box-shadow:0 5px 15px rgba(0,0,0,0.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:normal;letter-spacing:normal;line-break:auto;line-height:1.4;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;word-wrap:normal;font-size:12px;opacity:0;filter:alpha(opacity=0)}.tooltip.in{opacity:0.9;filter:alpha(opacity=90)}.tooltip.top{margin-top:-3px;padding:5px 0}.tooltip.right{margin-left:3px;padding:0 5px}.tooltip.bottom{margin-top:3px;padding:5px 0}.tooltip.left{margin-left:-3px;padding:0 5px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#ffffff;text-align:center;background-color:#333333;border-radius:0}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#333333}.tooltip.top-left .tooltip-arrow{bottom:0;right:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#333333}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#333333}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#333333}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#333333}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#333333}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#333333}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#333333}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:normal;letter-spacing:normal;line-break:auto;line-height:1.4;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;word-wrap:normal;font-size:15px;background-color:#333333;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #333333;border:1px solid transparent;border-radius:0;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{margin:0;padding:8px 14px;font-size:15px;background-color:#333333;border-bottom:1px solid #262626;border-radius:-1 -1 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{border-width:10px;content:""}.popover.top>.arrow{left:50%;margin-left:-11px;border-bottom-width:0;border-top-color:#000000;border-top-color:rgba(0,0,0,0.05);bottom:-11px}.popover.top>.arrow:after{content:" ";bottom:1px;margin-left:-10px;border-bottom-width:0;border-top-color:#333333}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-left-width:0;border-right-color:#000000;border-right-color:rgba(0,0,0,0.05)}.popover.right>.arrow:after{content:" ";left:1px;bottom:-10px;border-left-width:0;border-right-color:#333333}.popover.bottom>.arrow{left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#000000;border-bottom-color:rgba(0,0,0,0.05);top:-11px}.popover.bottom>.arrow:after{content:" ";top:1px;margin-left:-10px;border-top-width:0;border-bottom-color:#333333}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#000000;border-left-color:rgba(0,0,0,0.05)}.popover.left>.arrow:after{content:" ";right:1px;border-right-width:0;border-left-color:#333333;bottom:-10px}.carousel{position:relative}.carousel-inner{position:relative;overflow:hidden;width:100%}.carousel-inner>.item{display:none;position:relative;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.next,.carousel-inner>.item.active.right{-webkit-transform:translate3d(100%, 0, 0);transform:translate3d(100%, 0, 0);left:0}.carousel-inner>.item.prev,.carousel-inner>.item.active.left{-webkit-transform:translate3d(-100%, 0, 0);transform:translate3d(-100%, 0, 0);left:0}.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right,.carousel-inner>.item.active{-webkit-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0);left:0}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;left:0;bottom:0;width:15%;opacity:0.5;filter:alpha(opacity=50);font-size:20px;color:#ffffff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6);background-color:rgba(0,0,0,0)}.carousel-control.left{background-image:-webkit-linear-gradient(left, rgba(0,0,0,0.5) 0, rgba(0,0,0,0.0001) 100%);background-image:-o-linear-gradient(left, rgba(0,0,0,0.5) 0, rgba(0,0,0,0.0001) 100%);background-image:-webkit-gradient(linear, left top, right top, from(rgba(0,0,0,0.5)), to(rgba(0,0,0,0.0001)));background-image:linear-gradient(to right, rgba(0,0,0,0.5) 0, rgba(0,0,0,0.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1)}.carousel-control.right{left:auto;right:0;background-image:-webkit-linear-gradient(left, rgba(0,0,0,0.0001) 0, rgba(0,0,0,0.5) 100%);background-image:-o-linear-gradient(left, rgba(0,0,0,0.0001) 0, rgba(0,0,0,0.5) 100%);background-image:-webkit-gradient(linear, left top, right top, from(rgba(0,0,0,0.0001)), to(rgba(0,0,0,0.5)));background-image:linear-gradient(to right, rgba(0,0,0,0.0001) 0, rgba(0,0,0,0.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1)}.carousel-control:hover,.carousel-control:focus{outline:0;color:#ffffff;text-decoration:none;opacity:0.9;filter:alpha(opacity=90)}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;margin-top:-10px;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%;margin-left:-10px}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%;margin-right:-10px}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;line-height:1;font-family:serif}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;margin-left:-30%;padding-left:0;list-style:none;text-align:center}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;border:1px solid #ffffff;border-radius:10px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0)}.carousel-indicators .active{margin:0;width:12px;height:12px;background-color:#ffffff}.carousel-caption{position:absolute;left:15%;right:15%;bottom:20px;z-index:10;padding-top:20px;padding-bottom:20px;color:#ffffff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{left:20%;right:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after,.dl-horizontal dd:before,.dl-horizontal dd:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.nav:before,.nav:after,.navbar:before,.navbar:after,.navbar-header:before,.navbar-header:after,.navbar-collapse:before,.navbar-collapse:after,.pager:before,.pager:after,.panel-body:before,.panel-body:after,.modal-header:before,.modal-header:after,.modal-footer:before,.modal-footer:after{content:" ";display:table}.clearfix:after,.dl-horizontal dd:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after,.modal-header:after,.modal-footer:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right !important}.pull-left{float:left !important}.hide{display:none !important}.show{display:block !important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none !important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none !important}.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block{display:none !important}@media (max-width:767px){.visible-xs{display:block !important}table.visible-xs{display:table !important}tr.visible-xs{display:table-row !important}th.visible-xs,td.visible-xs{display:table-cell !important}}@media (max-width:767px){.visible-xs-block{display:block !important}}@media (max-width:767px){.visible-xs-inline{display:inline !important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block !important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block !important}table.visible-sm{display:table !important}tr.visible-sm{display:table-row !important}th.visible-sm,td.visible-sm{display:table-cell !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline !important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block !important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block !important}table.visible-md{display:table !important}tr.visible-md{display:table-row !important}th.visible-md,td.visible-md{display:table-cell !important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block !important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline !important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block !important}}@media (min-width:1200px){.visible-lg{display:block !important}table.visible-lg{display:table !important}tr.visible-lg{display:table-row !important}th.visible-lg,td.visible-lg{display:table-cell !important}}@media (min-width:1200px){.visible-lg-block{display:block !important}}@media (min-width:1200px){.visible-lg-inline{display:inline !important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block !important}}@media (max-width:767px){.hidden-xs{display:none !important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none !important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none !important}}@media (min-width:1200px){.hidden-lg{display:none !important}}.visible-print{display:none !important}@media print{.visible-print{display:block !important}table.visible-print{display:table !important}tr.visible-print{display:table-row !important}th.visible-print,td.visible-print{display:table-cell !important}}.visible-print-block{display:none !important}@media print{.visible-print-block{display:block !important}}.visible-print-inline{display:none !important}@media print{.visible-print-inline{display:inline !important}}.visible-print-inline-block{display:none !important}@media print{.visible-print-inline-block{display:inline-block !important}}@media print{.hidden-print{display:none !important}}.navbar{border:none;font-size:13px;font-weight:300}.navbar .navbar-toggle:hover .icon-bar{background-color:#b3b3b3}.navbar-collapse{border-top-color:rgba(0,0,0,0.2);-webkit-box-shadow:none;box-shadow:none}.navbar .btn{padding-top:6px;padding-bottom:6px}.navbar-form{margin-top:7px;margin-bottom:5px}.navbar-form .form-control{height:auto;padding:4px 6px}.navbar .dropdown-menu{border:none}.navbar .dropdown-menu>li>a,.navbar .dropdown-menu>li>a:focus{background-color:transparent;font-size:13px;font-weight:300}.navbar .dropdown-header{color:rgba(255,255,255,0.5)}.navbar-default .dropdown-menu{background-color:#333333}.navbar-default .dropdown-menu>li>a,.navbar-default .dropdown-menu>li>a:focus{color:#ffffff}.navbar-default .dropdown-menu>li>a:hover,.navbar-default .dropdown-menu>.active>a,.navbar-default .dropdown-menu>.active>a:hover{background-color:#272727}.navbar-inverse .dropdown-menu{background-color:#008cba}.navbar-inverse .dropdown-menu>li>a,.navbar-inverse .dropdown-menu>li>a:focus{color:#ffffff}.navbar-inverse .dropdown-menu>li>a:hover,.navbar-inverse .dropdown-menu>.active>a,.navbar-inverse .dropdown-menu>.active>a:hover{background-color:#006687}.btn{padding:8px 12px}.btn-lg{padding:16px 20px}.btn-sm{padding:8px 12px}.btn-xs{padding:4px 6px}.btn-group .btn~.dropdown-toggle{padding-left:16px;padding-right:16px}.btn-group .dropdown-menu{border-top-width:0}.btn-group.dropup .dropdown-menu{border-top-width:1px;border-bottom-width:0;margin-bottom:0}.btn-group .dropdown-toggle.btn-default~.dropdown-menu{background-color:#e7e7e7;border-color:#cccccc}.btn-group .dropdown-toggle.btn-default~.dropdown-menu>li>a{color:#333333}.btn-group .dropdown-toggle.btn-default~.dropdown-menu>li>a:hover{background-color:#d3d3d3}.btn-group .dropdown-toggle.btn-primary~.dropdown-menu{background-color:#008cba;border-color:#0079a1}.btn-group .dropdown-toggle.btn-primary~.dropdown-menu>li>a{color:#ffffff}.btn-group .dropdown-toggle.btn-primary~.dropdown-menu>li>a:hover{background-color:#006d91}.btn-group .dropdown-toggle.btn-success~.dropdown-menu{background-color:#43ac6a;border-color:#3c9a5f}.btn-group .dropdown-toggle.btn-success~.dropdown-menu>li>a{color:#ffffff}.btn-group .dropdown-toggle.btn-success~.dropdown-menu>li>a:hover{background-color:#388f58}.btn-group .dropdown-toggle.btn-info~.dropdown-menu{background-color:#5bc0de;border-color:#46b8da}.btn-group .dropdown-toggle.btn-info~.dropdown-menu>li>a{color:#ffffff}.btn-group .dropdown-toggle.btn-info~.dropdown-menu>li>a:hover{background-color:#39b3d7}.btn-group .dropdown-toggle.btn-warning~.dropdown-menu{background-color:#e99002;border-color:#d08002}.btn-group .dropdown-toggle.btn-warning~.dropdown-menu>li>a{color:#ffffff}.btn-group .dropdown-toggle.btn-warning~.dropdown-menu>li>a:hover{background-color:#c17702}.btn-group .dropdown-toggle.btn-danger~.dropdown-menu{background-color:#f04124;border-color:#ea2f10}.btn-group .dropdown-toggle.btn-danger~.dropdown-menu>li>a{color:#ffffff}.btn-group .dropdown-toggle.btn-danger~.dropdown-menu>li>a:hover{background-color:#dc2c0f}.lead{color:#6f6f6f}cite{font-style:italic}blockquote{border-left-width:1px;color:#6f6f6f}blockquote.pull-right{border-right-width:1px}blockquote small{font-size:12px;font-weight:300}table{font-size:12px}label,.control-label,.help-block,.checkbox,.radio{font-size:12px;font-weight:normal}input[type="radio"],input[type="checkbox"]{margin-top:1px}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{border-color:transparent}.nav-tabs>li>a{background-color:#e7e7e7;color:#222222}.nav-tabs .caret{border-top-color:#222222;border-bottom-color:#222222}.nav-pills{font-weight:300}.breadcrumb{border:1px solid #dddddd;border-radius:3px;font-size:10px;font-weight:300;text-transform:uppercase}.pagination{font-size:12px;font-weight:300;color:#999999}.pagination>li>a,.pagination>li>span{margin-left:4px;color:#999999}.pagination>.active>a,.pagination>.active>span{color:#fff}.pagination>li>a,.pagination>li:first-child>a,.pagination>li:last-child>a,.pagination>li>span,.pagination>li:first-child>span,.pagination>li:last-child>span{border-radius:3px}.pagination-lg>li>a,.pagination-lg>li>span{padding-left:22px;padding-right:22px}.pagination-sm>li>a,.pagination-sm>li>span{padding:0 5px}.pager{font-size:12px;font-weight:300;color:#999999}.list-group{font-size:12px;font-weight:300}.close{opacity:0.4;text-decoration:none;text-shadow:none}.close:hover,.close:focus{opacity:1}.alert{font-size:12px;font-weight:300}.alert .alert-link{font-weight:normal;color:#fff;text-decoration:underline}.label{padding-left:1em;padding-right:1em;border-radius:0;font-weight:300}.label-default{background-color:#e7e7e7;color:#333333}.badge{font-weight:300}.progress{height:22px;padding:2px;background-color:#f6f6f6;border:1px solid #ccc;-webkit-box-shadow:none;box-shadow:none}.dropdown-menu{padding:0;margin-top:0;font-size:12px}.dropdown-menu>li>a{padding:12px 15px}.dropdown-header{padding-left:15px;padding-right:15px;font-size:9px;text-transform:uppercase}.popover{color:#fff;font-size:12px;font-weight:300}.panel-heading,.panel-footer{border-top-right-radius:0;border-top-left-radius:0}.panel-default .close{color:#222222}.modal .close{color:#222222} \ No newline at end of file diff --git a/examples/blog/static/css/custom.css b/examples/blog/static/css/custom.css deleted file mode 100644 index a9bb3c03b..000000000 --- a/examples/blog/static/css/custom.css +++ /dev/null @@ -1,7 +0,0 @@ -body { - margin-top: 75px; /* 100px is double the height of the navbar - I made it a big larger for some more space - keep it at 50px at least if you want to use the fixed top nav */ -} - -footer { - margin: 50px 0; -} \ No newline at end of file diff --git a/examples/blog/static/css/font-awesome.css b/examples/blog/static/css/font-awesome.css deleted file mode 100644 index b2a5fe2f2..000000000 --- a/examples/blog/static/css/font-awesome.css +++ /dev/null @@ -1,2086 +0,0 @@ -/*! - * Font Awesome 4.5.0 by @davegandy - http://fontawesome.io - @fontawesome - * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */ -/* FONT PATH - * -------------------------- */ -@font-face { - font-family: 'FontAwesome'; - src: url('../fonts/fontawesome-webfont.eot?v=4.5.0'); - src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.5.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff2?v=4.5.0') format('woff2'), url('../fonts/fontawesome-webfont.woff?v=4.5.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.5.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.5.0#fontawesomeregular') format('svg'); - font-weight: normal; - font-style: normal; -} -.fa { - display: inline-block; - font: normal normal normal 14px/1 FontAwesome; - font-size: inherit; - text-rendering: auto; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} -/* makes the font 33% larger relative to the icon container */ -.fa-lg { - font-size: 1.33333333em; - line-height: 0.75em; - vertical-align: -15%; -} -.fa-2x { - font-size: 2em; -} -.fa-3x { - font-size: 3em; -} -.fa-4x { - font-size: 4em; -} -.fa-5x { - font-size: 5em; -} -.fa-fw { - width: 1.28571429em; - text-align: center; -} -.fa-ul { - padding-left: 0; - margin-left: 2.14285714em; - list-style-type: none; -} -.fa-ul > li { - position: relative; -} -.fa-li { - position: absolute; - left: -2.14285714em; - width: 2.14285714em; - top: 0.14285714em; - text-align: center; -} -.fa-li.fa-lg { - left: -1.85714286em; -} -.fa-border { - padding: .2em .25em .15em; - border: solid 0.08em #eeeeee; - 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; -} -/* Deprecated as of 4.4.0 */ -.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: #ffffff; -} -/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen - readers do not read off random characters that represent icons */ -.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/examples/blog/static/fonts/FontAwesome.otf b/examples/blog/static/fonts/FontAwesome.otf deleted file mode 100644 index 3ed7f8b48..000000000 Binary files a/examples/blog/static/fonts/FontAwesome.otf and /dev/null differ diff --git a/examples/blog/static/fonts/fontawesome-webfont.eot b/examples/blog/static/fonts/fontawesome-webfont.eot deleted file mode 100644 index 9b6afaedc..000000000 Binary files a/examples/blog/static/fonts/fontawesome-webfont.eot and /dev/null differ diff --git a/examples/blog/static/fonts/fontawesome-webfont.svg b/examples/blog/static/fonts/fontawesome-webfont.svg deleted file mode 100644 index d05688e9e..000000000 --- a/examples/blog/static/fonts/fontawesome-webfont.svg +++ /dev/null @@ -1,655 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/examples/blog/static/fonts/fontawesome-webfont.ttf b/examples/blog/static/fonts/fontawesome-webfont.ttf deleted file mode 100644 index 26dea7951..000000000 Binary files a/examples/blog/static/fonts/fontawesome-webfont.ttf and /dev/null differ diff --git a/examples/blog/static/fonts/fontawesome-webfont.woff b/examples/blog/static/fonts/fontawesome-webfont.woff deleted file mode 100644 index dc35ce3c2..000000000 Binary files a/examples/blog/static/fonts/fontawesome-webfont.woff and /dev/null differ diff --git a/examples/blog/static/fonts/fontawesome-webfont.woff2 b/examples/blog/static/fonts/fontawesome-webfont.woff2 deleted file mode 100644 index 500e51725..000000000 Binary files a/examples/blog/static/fonts/fontawesome-webfont.woff2 and /dev/null differ diff --git a/examples/blog/static/fonts/glyphicons-halflings-regular.eot b/examples/blog/static/fonts/glyphicons-halflings-regular.eot deleted file mode 100644 index b93a4953f..000000000 Binary files a/examples/blog/static/fonts/glyphicons-halflings-regular.eot and /dev/null differ diff --git a/examples/blog/static/fonts/glyphicons-halflings-regular.svg b/examples/blog/static/fonts/glyphicons-halflings-regular.svg deleted file mode 100644 index 94fb5490a..000000000 --- a/examples/blog/static/fonts/glyphicons-halflings-regular.svg +++ /dev/null @@ -1,288 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/examples/blog/static/fonts/glyphicons-halflings-regular.ttf b/examples/blog/static/fonts/glyphicons-halflings-regular.ttf deleted file mode 100644 index 1413fc609..000000000 Binary files a/examples/blog/static/fonts/glyphicons-halflings-regular.ttf and /dev/null differ diff --git a/examples/blog/static/fonts/glyphicons-halflings-regular.woff b/examples/blog/static/fonts/glyphicons-halflings-regular.woff deleted file mode 100644 index 9e612858f..000000000 Binary files a/examples/blog/static/fonts/glyphicons-halflings-regular.woff and /dev/null differ diff --git a/examples/blog/static/fonts/glyphicons-halflings-regular.woff2 b/examples/blog/static/fonts/glyphicons-halflings-regular.woff2 deleted file mode 100644 index 64539b54c..000000000 Binary files a/examples/blog/static/fonts/glyphicons-halflings-regular.woff2 and /dev/null differ diff --git a/examples/blog/static/js/bootstrap.js b/examples/blog/static/js/bootstrap.js deleted file mode 100644 index 01fbbcbaa..000000000 --- a/examples/blog/static/js/bootstrap.js +++ /dev/null @@ -1,2363 +0,0 @@ -/*! - * Bootstrap v3.3.6 (http://getbootstrap.com) - * Copyright 2011-2015 Twitter, Inc. - * Licensed under the MIT license - */ - -if (typeof jQuery === 'undefined') { - throw new Error('Bootstrap\'s JavaScript requires jQuery') -} - -+function ($) { - 'use strict'; - var version = $.fn.jquery.split(' ')[0].split('.') - if ((version[0] < 2 && version[1] < 9) || (version[0] == 1 && version[1] == 9 && version[2] < 1) || (version[0] > 2)) { - throw new Error('Bootstrap\'s JavaScript requires jQuery version 1.9.1 or higher, but lower than version 3') - } -}(jQuery); - -/* ======================================================================== - * Bootstrap: transition.js v3.3.6 - * http://getbootstrap.com/javascript/#transitions - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/) - // ============================================================ - - function transitionEnd() { - var el = document.createElement('bootstrap') - - var transEndEventNames = { - WebkitTransition : 'webkitTransitionEnd', - MozTransition : 'transitionend', - OTransition : 'oTransitionEnd otransitionend', - transition : 'transitionend' - } - - for (var name in transEndEventNames) { - if (el.style[name] !== undefined) { - return { end: transEndEventNames[name] } - } - } - - return false // explicit for ie8 ( ._.) - } - - // http://blog.alexmaccaw.com/css-transitions - $.fn.emulateTransitionEnd = function (duration) { - var called = false - var $el = this - $(this).one('bsTransitionEnd', function () { called = true }) - var callback = function () { if (!called) $($el).trigger($.support.transition.end) } - setTimeout(callback, duration) - return this - } - - $(function () { - $.support.transition = transitionEnd() - - if (!$.support.transition) return - - $.event.special.bsTransitionEnd = { - bindType: $.support.transition.end, - delegateType: $.support.transition.end, - handle: function (e) { - if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments) - } - } - }) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: alert.js v3.3.6 - * http://getbootstrap.com/javascript/#alerts - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // ALERT CLASS DEFINITION - // ====================== - - var dismiss = '[data-dismiss="alert"]' - var Alert = function (el) { - $(el).on('click', dismiss, this.close) - } - - Alert.VERSION = '3.3.6' - - Alert.TRANSITION_DURATION = 150 - - Alert.prototype.close = function (e) { - var $this = $(this) - var selector = $this.attr('data-target') - - if (!selector) { - selector = $this.attr('href') - selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 - } - - var $parent = $(selector) - - if (e) e.preventDefault() - - if (!$parent.length) { - $parent = $this.closest('.alert') - } - - $parent.trigger(e = $.Event('close.bs.alert')) - - if (e.isDefaultPrevented()) return - - $parent.removeClass('in') - - function removeElement() { - // detach from parent, fire event then clean up data - $parent.detach().trigger('closed.bs.alert').remove() - } - - $.support.transition && $parent.hasClass('fade') ? - $parent - .one('bsTransitionEnd', removeElement) - .emulateTransitionEnd(Alert.TRANSITION_DURATION) : - removeElement() - } - - - // ALERT PLUGIN DEFINITION - // ======================= - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.alert') - - if (!data) $this.data('bs.alert', (data = new Alert(this))) - if (typeof option == 'string') data[option].call($this) - }) - } - - var old = $.fn.alert - - $.fn.alert = Plugin - $.fn.alert.Constructor = Alert - - - // ALERT NO CONFLICT - // ================= - - $.fn.alert.noConflict = function () { - $.fn.alert = old - return this - } - - - // ALERT DATA-API - // ============== - - $(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: button.js v3.3.6 - * http://getbootstrap.com/javascript/#buttons - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // BUTTON PUBLIC CLASS DEFINITION - // ============================== - - var Button = function (element, options) { - this.$element = $(element) - this.options = $.extend({}, Button.DEFAULTS, options) - this.isLoading = false - } - - Button.VERSION = '3.3.6' - - Button.DEFAULTS = { - loadingText: 'loading...' - } - - Button.prototype.setState = function (state) { - var d = 'disabled' - var $el = this.$element - var val = $el.is('input') ? 'val' : 'html' - var data = $el.data() - - state += 'Text' - - if (data.resetText == null) $el.data('resetText', $el[val]()) - - // push to event loop to allow forms to submit - setTimeout($.proxy(function () { - $el[val](data[state] == null ? this.options[state] : data[state]) - - if (state == 'loadingText') { - this.isLoading = true - $el.addClass(d).attr(d, d) - } else if (this.isLoading) { - this.isLoading = false - $el.removeClass(d).removeAttr(d) - } - }, this), 0) - } - - Button.prototype.toggle = function () { - var changed = true - var $parent = this.$element.closest('[data-toggle="buttons"]') - - if ($parent.length) { - var $input = this.$element.find('input') - if ($input.prop('type') == 'radio') { - if ($input.prop('checked')) changed = false - $parent.find('.active').removeClass('active') - this.$element.addClass('active') - } else if ($input.prop('type') == 'checkbox') { - if (($input.prop('checked')) !== this.$element.hasClass('active')) changed = false - this.$element.toggleClass('active') - } - $input.prop('checked', this.$element.hasClass('active')) - if (changed) $input.trigger('change') - } else { - this.$element.attr('aria-pressed', !this.$element.hasClass('active')) - this.$element.toggleClass('active') - } - } - - - // BUTTON PLUGIN DEFINITION - // ======================== - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.button') - var options = typeof option == 'object' && option - - if (!data) $this.data('bs.button', (data = new Button(this, options))) - - if (option == 'toggle') data.toggle() - else if (option) data.setState(option) - }) - } - - var old = $.fn.button - - $.fn.button = Plugin - $.fn.button.Constructor = Button - - - // BUTTON NO CONFLICT - // ================== - - $.fn.button.noConflict = function () { - $.fn.button = old - return this - } - - - // BUTTON DATA-API - // =============== - - $(document) - .on('click.bs.button.data-api', '[data-toggle^="button"]', function (e) { - var $btn = $(e.target) - if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') - Plugin.call($btn, 'toggle') - if (!($(e.target).is('input[type="radio"]') || $(e.target).is('input[type="checkbox"]'))) e.preventDefault() - }) - .on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^="button"]', function (e) { - $(e.target).closest('.btn').toggleClass('focus', /^focus(in)?$/.test(e.type)) - }) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: carousel.js v3.3.6 - * http://getbootstrap.com/javascript/#carousel - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // CAROUSEL CLASS DEFINITION - // ========================= - - var Carousel = function (element, options) { - this.$element = $(element) - this.$indicators = this.$element.find('.carousel-indicators') - this.options = options - this.paused = null - this.sliding = null - this.interval = null - this.$active = null - this.$items = null - - this.options.keyboard && this.$element.on('keydown.bs.carousel', $.proxy(this.keydown, this)) - - this.options.pause == 'hover' && !('ontouchstart' in document.documentElement) && this.$element - .on('mouseenter.bs.carousel', $.proxy(this.pause, this)) - .on('mouseleave.bs.carousel', $.proxy(this.cycle, this)) - } - - Carousel.VERSION = '3.3.6' - - Carousel.TRANSITION_DURATION = 600 - - Carousel.DEFAULTS = { - interval: 5000, - pause: 'hover', - wrap: true, - keyboard: true - } - - Carousel.prototype.keydown = function (e) { - if (/input|textarea/i.test(e.target.tagName)) return - switch (e.which) { - case 37: this.prev(); break - case 39: this.next(); break - default: return - } - - e.preventDefault() - } - - Carousel.prototype.cycle = function (e) { - e || (this.paused = false) - - this.interval && clearInterval(this.interval) - - this.options.interval - && !this.paused - && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) - - return this - } - - Carousel.prototype.getItemIndex = function (item) { - this.$items = item.parent().children('.item') - return this.$items.index(item || this.$active) - } - - Carousel.prototype.getItemForDirection = function (direction, active) { - var activeIndex = this.getItemIndex(active) - var willWrap = (direction == 'prev' && activeIndex === 0) - || (direction == 'next' && activeIndex == (this.$items.length - 1)) - if (willWrap && !this.options.wrap) return active - var delta = direction == 'prev' ? -1 : 1 - var itemIndex = (activeIndex + delta) % this.$items.length - return this.$items.eq(itemIndex) - } - - Carousel.prototype.to = function (pos) { - var that = this - var activeIndex = this.getItemIndex(this.$active = this.$element.find('.item.active')) - - if (pos > (this.$items.length - 1) || pos < 0) return - - if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) // yes, "slid" - if (activeIndex == pos) return this.pause().cycle() - - return this.slide(pos > activeIndex ? 'next' : 'prev', this.$items.eq(pos)) - } - - Carousel.prototype.pause = function (e) { - e || (this.paused = true) - - if (this.$element.find('.next, .prev').length && $.support.transition) { - this.$element.trigger($.support.transition.end) - this.cycle(true) - } - - this.interval = clearInterval(this.interval) - - return this - } - - Carousel.prototype.next = function () { - if (this.sliding) return - return this.slide('next') - } - - Carousel.prototype.prev = function () { - if (this.sliding) return - return this.slide('prev') - } - - Carousel.prototype.slide = function (type, next) { - var $active = this.$element.find('.item.active') - var $next = next || this.getItemForDirection(type, $active) - var isCycling = this.interval - var direction = type == 'next' ? 'left' : 'right' - var that = this - - if ($next.hasClass('active')) return (this.sliding = false) - - var relatedTarget = $next[0] - var slideEvent = $.Event('slide.bs.carousel', { - relatedTarget: relatedTarget, - direction: direction - }) - this.$element.trigger(slideEvent) - if (slideEvent.isDefaultPrevented()) return - - this.sliding = true - - isCycling && this.pause() - - if (this.$indicators.length) { - this.$indicators.find('.active').removeClass('active') - var $nextIndicator = $(this.$indicators.children()[this.getItemIndex($next)]) - $nextIndicator && $nextIndicator.addClass('active') - } - - var slidEvent = $.Event('slid.bs.carousel', { relatedTarget: relatedTarget, direction: direction }) // yes, "slid" - if ($.support.transition && this.$element.hasClass('slide')) { - $next.addClass(type) - $next[0].offsetWidth // force reflow - $active.addClass(direction) - $next.addClass(direction) - $active - .one('bsTransitionEnd', function () { - $next.removeClass([type, direction].join(' ')).addClass('active') - $active.removeClass(['active', direction].join(' ')) - that.sliding = false - setTimeout(function () { - that.$element.trigger(slidEvent) - }, 0) - }) - .emulateTransitionEnd(Carousel.TRANSITION_DURATION) - } else { - $active.removeClass('active') - $next.addClass('active') - this.sliding = false - this.$element.trigger(slidEvent) - } - - isCycling && this.cycle() - - return this - } - - - // CAROUSEL PLUGIN DEFINITION - // ========================== - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.carousel') - var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option) - var action = typeof option == 'string' ? option : options.slide - - if (!data) $this.data('bs.carousel', (data = new Carousel(this, options))) - if (typeof option == 'number') data.to(option) - else if (action) data[action]() - else if (options.interval) data.pause().cycle() - }) - } - - var old = $.fn.carousel - - $.fn.carousel = Plugin - $.fn.carousel.Constructor = Carousel - - - // CAROUSEL NO CONFLICT - // ==================== - - $.fn.carousel.noConflict = function () { - $.fn.carousel = old - return this - } - - - // CAROUSEL DATA-API - // ================= - - var clickHandler = function (e) { - var href - var $this = $(this) - var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7 - if (!$target.hasClass('carousel')) return - var options = $.extend({}, $target.data(), $this.data()) - var slideIndex = $this.attr('data-slide-to') - if (slideIndex) options.interval = false - - Plugin.call($target, options) - - if (slideIndex) { - $target.data('bs.carousel').to(slideIndex) - } - - e.preventDefault() - } - - $(document) - .on('click.bs.carousel.data-api', '[data-slide]', clickHandler) - .on('click.bs.carousel.data-api', '[data-slide-to]', clickHandler) - - $(window).on('load', function () { - $('[data-ride="carousel"]').each(function () { - var $carousel = $(this) - Plugin.call($carousel, $carousel.data()) - }) - }) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: collapse.js v3.3.6 - * http://getbootstrap.com/javascript/#collapse - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // COLLAPSE PUBLIC CLASS DEFINITION - // ================================ - - var Collapse = function (element, options) { - this.$element = $(element) - this.options = $.extend({}, Collapse.DEFAULTS, options) - this.$trigger = $('[data-toggle="collapse"][href="#' + element.id + '"],' + - '[data-toggle="collapse"][data-target="#' + element.id + '"]') - this.transitioning = null - - if (this.options.parent) { - this.$parent = this.getParent() - } else { - this.addAriaAndCollapsedClass(this.$element, this.$trigger) - } - - if (this.options.toggle) this.toggle() - } - - Collapse.VERSION = '3.3.6' - - Collapse.TRANSITION_DURATION = 350 - - Collapse.DEFAULTS = { - toggle: true - } - - Collapse.prototype.dimension = function () { - var hasWidth = this.$element.hasClass('width') - return hasWidth ? 'width' : 'height' - } - - Collapse.prototype.show = function () { - if (this.transitioning || this.$element.hasClass('in')) return - - var activesData - var actives = this.$parent && this.$parent.children('.panel').children('.in, .collapsing') - - if (actives && actives.length) { - activesData = actives.data('bs.collapse') - if (activesData && activesData.transitioning) return - } - - var startEvent = $.Event('show.bs.collapse') - this.$element.trigger(startEvent) - if (startEvent.isDefaultPrevented()) return - - if (actives && actives.length) { - Plugin.call(actives, 'hide') - activesData || actives.data('bs.collapse', null) - } - - var dimension = this.dimension() - - this.$element - .removeClass('collapse') - .addClass('collapsing')[dimension](0) - .attr('aria-expanded', true) - - this.$trigger - .removeClass('collapsed') - .attr('aria-expanded', true) - - this.transitioning = 1 - - var complete = function () { - this.$element - .removeClass('collapsing') - .addClass('collapse in')[dimension]('') - this.transitioning = 0 - this.$element - .trigger('shown.bs.collapse') - } - - if (!$.support.transition) return complete.call(this) - - var scrollSize = $.camelCase(['scroll', dimension].join('-')) - - this.$element - .one('bsTransitionEnd', $.proxy(complete, this)) - .emulateTransitionEnd(Collapse.TRANSITION_DURATION)[dimension](this.$element[0][scrollSize]) - } - - Collapse.prototype.hide = function () { - if (this.transitioning || !this.$element.hasClass('in')) return - - var startEvent = $.Event('hide.bs.collapse') - this.$element.trigger(startEvent) - if (startEvent.isDefaultPrevented()) return - - var dimension = this.dimension() - - this.$element[dimension](this.$element[dimension]())[0].offsetHeight - - this.$element - .addClass('collapsing') - .removeClass('collapse in') - .attr('aria-expanded', false) - - this.$trigger - .addClass('collapsed') - .attr('aria-expanded', false) - - this.transitioning = 1 - - var complete = function () { - this.transitioning = 0 - this.$element - .removeClass('collapsing') - .addClass('collapse') - .trigger('hidden.bs.collapse') - } - - if (!$.support.transition) return complete.call(this) - - this.$element - [dimension](0) - .one('bsTransitionEnd', $.proxy(complete, this)) - .emulateTransitionEnd(Collapse.TRANSITION_DURATION) - } - - Collapse.prototype.toggle = function () { - this[this.$element.hasClass('in') ? 'hide' : 'show']() - } - - Collapse.prototype.getParent = function () { - return $(this.options.parent) - .find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]') - .each($.proxy(function (i, element) { - var $element = $(element) - this.addAriaAndCollapsedClass(getTargetFromTrigger($element), $element) - }, this)) - .end() - } - - Collapse.prototype.addAriaAndCollapsedClass = function ($element, $trigger) { - var isOpen = $element.hasClass('in') - - $element.attr('aria-expanded', isOpen) - $trigger - .toggleClass('collapsed', !isOpen) - .attr('aria-expanded', isOpen) - } - - function getTargetFromTrigger($trigger) { - var href - var target = $trigger.attr('data-target') - || (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 - - return $(target) - } - - - // COLLAPSE PLUGIN DEFINITION - // ========================== - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.collapse') - var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option) - - if (!data && options.toggle && /show|hide/.test(option)) options.toggle = false - if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - var old = $.fn.collapse - - $.fn.collapse = Plugin - $.fn.collapse.Constructor = Collapse - - - // COLLAPSE NO CONFLICT - // ==================== - - $.fn.collapse.noConflict = function () { - $.fn.collapse = old - return this - } - - - // COLLAPSE DATA-API - // ================= - - $(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function (e) { - var $this = $(this) - - if (!$this.attr('data-target')) e.preventDefault() - - var $target = getTargetFromTrigger($this) - var data = $target.data('bs.collapse') - var option = data ? 'toggle' : $this.data() - - Plugin.call($target, option) - }) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: dropdown.js v3.3.6 - * http://getbootstrap.com/javascript/#dropdowns - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // DROPDOWN CLASS DEFINITION - // ========================= - - var backdrop = '.dropdown-backdrop' - var toggle = '[data-toggle="dropdown"]' - var Dropdown = function (element) { - $(element).on('click.bs.dropdown', this.toggle) - } - - Dropdown.VERSION = '3.3.6' - - function getParent($this) { - var selector = $this.attr('data-target') - - if (!selector) { - selector = $this.attr('href') - selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 - } - - var $parent = selector && $(selector) - - return $parent && $parent.length ? $parent : $this.parent() - } - - function clearMenus(e) { - if (e && e.which === 3) return - $(backdrop).remove() - $(toggle).each(function () { - var $this = $(this) - var $parent = getParent($this) - var relatedTarget = { relatedTarget: this } - - if (!$parent.hasClass('open')) return - - if (e && e.type == 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)) return - - $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget)) - - if (e.isDefaultPrevented()) return - - $this.attr('aria-expanded', 'false') - $parent.removeClass('open').trigger($.Event('hidden.bs.dropdown', relatedTarget)) - }) - } - - Dropdown.prototype.toggle = function (e) { - var $this = $(this) - - if ($this.is('.disabled, :disabled')) return - - var $parent = getParent($this) - var isActive = $parent.hasClass('open') - - clearMenus() - - if (!isActive) { - if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { - // if mobile we use a backdrop because click events don't delegate - $(document.createElement('div')) - .addClass('dropdown-backdrop') - .insertAfter($(this)) - .on('click', clearMenus) - } - - var relatedTarget = { relatedTarget: this } - $parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget)) - - if (e.isDefaultPrevented()) return - - $this - .trigger('focus') - .attr('aria-expanded', 'true') - - $parent - .toggleClass('open') - .trigger($.Event('shown.bs.dropdown', relatedTarget)) - } - - return false - } - - Dropdown.prototype.keydown = function (e) { - if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return - - var $this = $(this) - - e.preventDefault() - e.stopPropagation() - - if ($this.is('.disabled, :disabled')) return - - var $parent = getParent($this) - var isActive = $parent.hasClass('open') - - if (!isActive && e.which != 27 || isActive && e.which == 27) { - if (e.which == 27) $parent.find(toggle).trigger('focus') - return $this.trigger('click') - } - - var desc = ' li:not(.disabled):visible a' - var $items = $parent.find('.dropdown-menu' + desc) - - if (!$items.length) return - - var index = $items.index(e.target) - - if (e.which == 38 && index > 0) index-- // up - if (e.which == 40 && index < $items.length - 1) index++ // down - if (!~index) index = 0 - - $items.eq(index).trigger('focus') - } - - - // DROPDOWN PLUGIN DEFINITION - // ========================== - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.dropdown') - - if (!data) $this.data('bs.dropdown', (data = new Dropdown(this))) - if (typeof option == 'string') data[option].call($this) - }) - } - - var old = $.fn.dropdown - - $.fn.dropdown = Plugin - $.fn.dropdown.Constructor = Dropdown - - - // DROPDOWN NO CONFLICT - // ==================== - - $.fn.dropdown.noConflict = function () { - $.fn.dropdown = old - return this - } - - - // APPLY TO STANDARD DROPDOWN ELEMENTS - // =================================== - - $(document) - .on('click.bs.dropdown.data-api', clearMenus) - .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) - .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle) - .on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown) - .on('keydown.bs.dropdown.data-api', '.dropdown-menu', Dropdown.prototype.keydown) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: modal.js v3.3.6 - * http://getbootstrap.com/javascript/#modals - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // MODAL CLASS DEFINITION - // ====================== - - var Modal = function (element, options) { - this.options = options - this.$body = $(document.body) - this.$element = $(element) - this.$dialog = this.$element.find('.modal-dialog') - this.$backdrop = null - this.isShown = null - this.originalBodyPad = null - this.scrollbarWidth = 0 - this.ignoreBackdropClick = false - - if (this.options.remote) { - this.$element - .find('.modal-content') - .load(this.options.remote, $.proxy(function () { - this.$element.trigger('loaded.bs.modal') - }, this)) - } - } - - Modal.VERSION = '3.3.6' - - Modal.TRANSITION_DURATION = 300 - Modal.BACKDROP_TRANSITION_DURATION = 150 - - Modal.DEFAULTS = { - backdrop: true, - keyboard: true, - show: true - } - - Modal.prototype.toggle = function (_relatedTarget) { - return this.isShown ? this.hide() : this.show(_relatedTarget) - } - - Modal.prototype.show = function (_relatedTarget) { - var that = this - var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget }) - - this.$element.trigger(e) - - if (this.isShown || e.isDefaultPrevented()) return - - this.isShown = true - - this.checkScrollbar() - this.setScrollbar() - this.$body.addClass('modal-open') - - this.escape() - this.resize() - - this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this)) - - this.$dialog.on('mousedown.dismiss.bs.modal', function () { - that.$element.one('mouseup.dismiss.bs.modal', function (e) { - if ($(e.target).is(that.$element)) that.ignoreBackdropClick = true - }) - }) - - this.backdrop(function () { - var transition = $.support.transition && that.$element.hasClass('fade') - - if (!that.$element.parent().length) { - that.$element.appendTo(that.$body) // don't move modals dom position - } - - that.$element - .show() - .scrollTop(0) - - that.adjustDialog() - - if (transition) { - that.$element[0].offsetWidth // force reflow - } - - that.$element.addClass('in') - - that.enforceFocus() - - var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget }) - - transition ? - that.$dialog // wait for modal to slide in - .one('bsTransitionEnd', function () { - that.$element.trigger('focus').trigger(e) - }) - .emulateTransitionEnd(Modal.TRANSITION_DURATION) : - that.$element.trigger('focus').trigger(e) - }) - } - - Modal.prototype.hide = function (e) { - if (e) e.preventDefault() - - e = $.Event('hide.bs.modal') - - this.$element.trigger(e) - - if (!this.isShown || e.isDefaultPrevented()) return - - this.isShown = false - - this.escape() - this.resize() - - $(document).off('focusin.bs.modal') - - this.$element - .removeClass('in') - .off('click.dismiss.bs.modal') - .off('mouseup.dismiss.bs.modal') - - this.$dialog.off('mousedown.dismiss.bs.modal') - - $.support.transition && this.$element.hasClass('fade') ? - this.$element - .one('bsTransitionEnd', $.proxy(this.hideModal, this)) - .emulateTransitionEnd(Modal.TRANSITION_DURATION) : - this.hideModal() - } - - Modal.prototype.enforceFocus = function () { - $(document) - .off('focusin.bs.modal') // guard against infinite focus loop - .on('focusin.bs.modal', $.proxy(function (e) { - if (this.$element[0] !== e.target && !this.$element.has(e.target).length) { - this.$element.trigger('focus') - } - }, this)) - } - - Modal.prototype.escape = function () { - if (this.isShown && this.options.keyboard) { - this.$element.on('keydown.dismiss.bs.modal', $.proxy(function (e) { - e.which == 27 && this.hide() - }, this)) - } else if (!this.isShown) { - this.$element.off('keydown.dismiss.bs.modal') - } - } - - Modal.prototype.resize = function () { - if (this.isShown) { - $(window).on('resize.bs.modal', $.proxy(this.handleUpdate, this)) - } else { - $(window).off('resize.bs.modal') - } - } - - Modal.prototype.hideModal = function () { - var that = this - this.$element.hide() - this.backdrop(function () { - that.$body.removeClass('modal-open') - that.resetAdjustments() - that.resetScrollbar() - that.$element.trigger('hidden.bs.modal') - }) - } - - Modal.prototype.removeBackdrop = function () { - this.$backdrop && this.$backdrop.remove() - this.$backdrop = null - } - - Modal.prototype.backdrop = function (callback) { - var that = this - var animate = this.$element.hasClass('fade') ? 'fade' : '' - - if (this.isShown && this.options.backdrop) { - var doAnimate = $.support.transition && animate - - this.$backdrop = $(document.createElement('div')) - .addClass('modal-backdrop ' + animate) - .appendTo(this.$body) - - this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) { - if (this.ignoreBackdropClick) { - this.ignoreBackdropClick = false - return - } - if (e.target !== e.currentTarget) return - this.options.backdrop == 'static' - ? this.$element[0].focus() - : this.hide() - }, this)) - - if (doAnimate) this.$backdrop[0].offsetWidth // force reflow - - this.$backdrop.addClass('in') - - if (!callback) return - - doAnimate ? - this.$backdrop - .one('bsTransitionEnd', callback) - .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : - callback() - - } else if (!this.isShown && this.$backdrop) { - this.$backdrop.removeClass('in') - - var callbackRemove = function () { - that.removeBackdrop() - callback && callback() - } - $.support.transition && this.$element.hasClass('fade') ? - this.$backdrop - .one('bsTransitionEnd', callbackRemove) - .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : - callbackRemove() - - } else if (callback) { - callback() - } - } - - // these following methods are used to handle overflowing modals - - Modal.prototype.handleUpdate = function () { - this.adjustDialog() - } - - Modal.prototype.adjustDialog = function () { - var modalIsOverflowing = this.$element[0].scrollHeight > document.documentElement.clientHeight - - this.$element.css({ - paddingLeft: !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '', - paddingRight: this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : '' - }) - } - - Modal.prototype.resetAdjustments = function () { - this.$element.css({ - paddingLeft: '', - paddingRight: '' - }) - } - - Modal.prototype.checkScrollbar = function () { - var fullWindowWidth = window.innerWidth - if (!fullWindowWidth) { // workaround for missing window.innerWidth in IE8 - var documentElementRect = document.documentElement.getBoundingClientRect() - fullWindowWidth = documentElementRect.right - Math.abs(documentElementRect.left) - } - this.bodyIsOverflowing = document.body.clientWidth < fullWindowWidth - this.scrollbarWidth = this.measureScrollbar() - } - - Modal.prototype.setScrollbar = function () { - var bodyPad = parseInt((this.$body.css('padding-right') || 0), 10) - this.originalBodyPad = document.body.style.paddingRight || '' - if (this.bodyIsOverflowing) this.$body.css('padding-right', bodyPad + this.scrollbarWidth) - } - - Modal.prototype.resetScrollbar = function () { - this.$body.css('padding-right', this.originalBodyPad) - } - - Modal.prototype.measureScrollbar = function () { // thx walsh - var scrollDiv = document.createElement('div') - scrollDiv.className = 'modal-scrollbar-measure' - this.$body.append(scrollDiv) - var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth - this.$body[0].removeChild(scrollDiv) - return scrollbarWidth - } - - - // MODAL PLUGIN DEFINITION - // ======================= - - function Plugin(option, _relatedTarget) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.modal') - var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option) - - if (!data) $this.data('bs.modal', (data = new Modal(this, options))) - if (typeof option == 'string') data[option](_relatedTarget) - else if (options.show) data.show(_relatedTarget) - }) - } - - var old = $.fn.modal - - $.fn.modal = Plugin - $.fn.modal.Constructor = Modal - - - // MODAL NO CONFLICT - // ================= - - $.fn.modal.noConflict = function () { - $.fn.modal = old - return this - } - - - // MODAL DATA-API - // ============== - - $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) { - var $this = $(this) - var href = $this.attr('href') - var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) // strip for ie7 - var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data()) - - if ($this.is('a')) e.preventDefault() - - $target.one('show.bs.modal', function (showEvent) { - if (showEvent.isDefaultPrevented()) return // only register focus restorer if modal will actually get shown - $target.one('hidden.bs.modal', function () { - $this.is(':visible') && $this.trigger('focus') - }) - }) - Plugin.call($target, option, this) - }) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: tooltip.js v3.3.6 - * http://getbootstrap.com/javascript/#tooltip - * Inspired by the original jQuery.tipsy by Jason Frame - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // TOOLTIP PUBLIC CLASS DEFINITION - // =============================== - - var Tooltip = function (element, options) { - this.type = null - this.options = null - this.enabled = null - this.timeout = null - this.hoverState = null - this.$element = null - this.inState = null - - this.init('tooltip', element, options) - } - - Tooltip.VERSION = '3.3.6' - - Tooltip.TRANSITION_DURATION = 150 - - Tooltip.DEFAULTS = { - animation: true, - placement: 'top', - selector: false, - template: '', - trigger: 'hover focus', - title: '', - delay: 0, - html: false, - container: false, - viewport: { - selector: 'body', - padding: 0 - } - } - - Tooltip.prototype.init = function (type, element, options) { - this.enabled = true - this.type = type - this.$element = $(element) - this.options = this.getOptions(options) - this.$viewport = this.options.viewport && $($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : (this.options.viewport.selector || this.options.viewport)) - this.inState = { click: false, hover: false, focus: false } - - if (this.$element[0] instanceof document.constructor && !this.options.selector) { - throw new Error('`selector` option must be specified when initializing ' + this.type + ' on the window.document object!') - } - - var triggers = this.options.trigger.split(' ') - - for (var i = triggers.length; i--;) { - var trigger = triggers[i] - - if (trigger == 'click') { - this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) - } else if (trigger != 'manual') { - var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin' - var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout' - - this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) - this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) - } - } - - this.options.selector ? - (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : - this.fixTitle() - } - - Tooltip.prototype.getDefaults = function () { - return Tooltip.DEFAULTS - } - - Tooltip.prototype.getOptions = function (options) { - options = $.extend({}, this.getDefaults(), this.$element.data(), options) - - if (options.delay && typeof options.delay == 'number') { - options.delay = { - show: options.delay, - hide: options.delay - } - } - - return options - } - - Tooltip.prototype.getDelegateOptions = function () { - var options = {} - var defaults = this.getDefaults() - - this._options && $.each(this._options, function (key, value) { - if (defaults[key] != value) options[key] = value - }) - - return options - } - - Tooltip.prototype.enter = function (obj) { - var self = obj instanceof this.constructor ? - obj : $(obj.currentTarget).data('bs.' + this.type) - - if (!self) { - self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) - $(obj.currentTarget).data('bs.' + this.type, self) - } - - if (obj instanceof $.Event) { - self.inState[obj.type == 'focusin' ? 'focus' : 'hover'] = true - } - - if (self.tip().hasClass('in') || self.hoverState == 'in') { - self.hoverState = 'in' - return - } - - clearTimeout(self.timeout) - - self.hoverState = 'in' - - if (!self.options.delay || !self.options.delay.show) return self.show() - - self.timeout = setTimeout(function () { - if (self.hoverState == 'in') self.show() - }, self.options.delay.show) - } - - Tooltip.prototype.isInStateTrue = function () { - for (var key in this.inState) { - if (this.inState[key]) return true - } - - return false - } - - Tooltip.prototype.leave = function (obj) { - var self = obj instanceof this.constructor ? - obj : $(obj.currentTarget).data('bs.' + this.type) - - if (!self) { - self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) - $(obj.currentTarget).data('bs.' + this.type, self) - } - - if (obj instanceof $.Event) { - self.inState[obj.type == 'focusout' ? 'focus' : 'hover'] = false - } - - if (self.isInStateTrue()) return - - clearTimeout(self.timeout) - - self.hoverState = 'out' - - if (!self.options.delay || !self.options.delay.hide) return self.hide() - - self.timeout = setTimeout(function () { - if (self.hoverState == 'out') self.hide() - }, self.options.delay.hide) - } - - Tooltip.prototype.show = function () { - var e = $.Event('show.bs.' + this.type) - - if (this.hasContent() && this.enabled) { - this.$element.trigger(e) - - var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0]) - if (e.isDefaultPrevented() || !inDom) return - var that = this - - var $tip = this.tip() - - var tipId = this.getUID(this.type) - - this.setContent() - $tip.attr('id', tipId) - this.$element.attr('aria-describedby', tipId) - - if (this.options.animation) $tip.addClass('fade') - - var placement = typeof this.options.placement == 'function' ? - this.options.placement.call(this, $tip[0], this.$element[0]) : - this.options.placement - - var autoToken = /\s?auto?\s?/i - var autoPlace = autoToken.test(placement) - if (autoPlace) placement = placement.replace(autoToken, '') || 'top' - - $tip - .detach() - .css({ top: 0, left: 0, display: 'block' }) - .addClass(placement) - .data('bs.' + this.type, this) - - this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element) - this.$element.trigger('inserted.bs.' + this.type) - - var pos = this.getPosition() - var actualWidth = $tip[0].offsetWidth - var actualHeight = $tip[0].offsetHeight - - if (autoPlace) { - var orgPlacement = placement - var viewportDim = this.getPosition(this.$viewport) - - placement = placement == 'bottom' && pos.bottom + actualHeight > viewportDim.bottom ? 'top' : - placement == 'top' && pos.top - actualHeight < viewportDim.top ? 'bottom' : - placement == 'right' && pos.right + actualWidth > viewportDim.width ? 'left' : - placement == 'left' && pos.left - actualWidth < viewportDim.left ? 'right' : - placement - - $tip - .removeClass(orgPlacement) - .addClass(placement) - } - - var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight) - - this.applyPlacement(calculatedOffset, placement) - - var complete = function () { - var prevHoverState = that.hoverState - that.$element.trigger('shown.bs.' + that.type) - that.hoverState = null - - if (prevHoverState == 'out') that.leave(that) - } - - $.support.transition && this.$tip.hasClass('fade') ? - $tip - .one('bsTransitionEnd', complete) - .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : - complete() - } - } - - Tooltip.prototype.applyPlacement = function (offset, placement) { - var $tip = this.tip() - var width = $tip[0].offsetWidth - var height = $tip[0].offsetHeight - - // manually read margins because getBoundingClientRect includes difference - var marginTop = parseInt($tip.css('margin-top'), 10) - var marginLeft = parseInt($tip.css('margin-left'), 10) - - // we must check for NaN for ie 8/9 - if (isNaN(marginTop)) marginTop = 0 - if (isNaN(marginLeft)) marginLeft = 0 - - offset.top += marginTop - offset.left += marginLeft - - // $.fn.offset doesn't round pixel values - // so we use setOffset directly with our own function B-0 - $.offset.setOffset($tip[0], $.extend({ - using: function (props) { - $tip.css({ - top: Math.round(props.top), - left: Math.round(props.left) - }) - } - }, offset), 0) - - $tip.addClass('in') - - // check to see if placing tip in new offset caused the tip to resize itself - var actualWidth = $tip[0].offsetWidth - var actualHeight = $tip[0].offsetHeight - - if (placement == 'top' && actualHeight != height) { - offset.top = offset.top + height - actualHeight - } - - var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight) - - if (delta.left) offset.left += delta.left - else offset.top += delta.top - - var isVertical = /top|bottom/.test(placement) - var arrowDelta = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight - var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight' - - $tip.offset(offset) - this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], isVertical) - } - - Tooltip.prototype.replaceArrow = function (delta, dimension, isVertical) { - this.arrow() - .css(isVertical ? 'left' : 'top', 50 * (1 - delta / dimension) + '%') - .css(isVertical ? 'top' : 'left', '') - } - - Tooltip.prototype.setContent = function () { - var $tip = this.tip() - var title = this.getTitle() - - $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) - $tip.removeClass('fade in top bottom left right') - } - - Tooltip.prototype.hide = function (callback) { - var that = this - var $tip = $(this.$tip) - var e = $.Event('hide.bs.' + this.type) - - function complete() { - if (that.hoverState != 'in') $tip.detach() - that.$element - .removeAttr('aria-describedby') - .trigger('hidden.bs.' + that.type) - callback && callback() - } - - this.$element.trigger(e) - - if (e.isDefaultPrevented()) return - - $tip.removeClass('in') - - $.support.transition && $tip.hasClass('fade') ? - $tip - .one('bsTransitionEnd', complete) - .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : - complete() - - this.hoverState = null - - return this - } - - Tooltip.prototype.fixTitle = function () { - var $e = this.$element - if ($e.attr('title') || typeof $e.attr('data-original-title') != 'string') { - $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') - } - } - - Tooltip.prototype.hasContent = function () { - return this.getTitle() - } - - Tooltip.prototype.getPosition = function ($element) { - $element = $element || this.$element - - var el = $element[0] - var isBody = el.tagName == 'BODY' - - var elRect = el.getBoundingClientRect() - if (elRect.width == null) { - // width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093 - elRect = $.extend({}, elRect, { width: elRect.right - elRect.left, height: elRect.bottom - elRect.top }) - } - var elOffset = isBody ? { top: 0, left: 0 } : $element.offset() - var scroll = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() } - var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null - - return $.extend({}, elRect, scroll, outerDims, elOffset) - } - - Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) { - return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } : - placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } : - placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } : - /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width } - - } - - Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) { - var delta = { top: 0, left: 0 } - if (!this.$viewport) return delta - - var viewportPadding = this.options.viewport && this.options.viewport.padding || 0 - var viewportDimensions = this.getPosition(this.$viewport) - - if (/right|left/.test(placement)) { - var topEdgeOffset = pos.top - viewportPadding - viewportDimensions.scroll - var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight - if (topEdgeOffset < viewportDimensions.top) { // top overflow - delta.top = viewportDimensions.top - topEdgeOffset - } else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow - delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset - } - } else { - var leftEdgeOffset = pos.left - viewportPadding - var rightEdgeOffset = pos.left + viewportPadding + actualWidth - if (leftEdgeOffset < viewportDimensions.left) { // left overflow - delta.left = viewportDimensions.left - leftEdgeOffset - } else if (rightEdgeOffset > viewportDimensions.right) { // right overflow - delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset - } - } - - return delta - } - - Tooltip.prototype.getTitle = function () { - var title - var $e = this.$element - var o = this.options - - title = $e.attr('data-original-title') - || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) - - return title - } - - Tooltip.prototype.getUID = function (prefix) { - do prefix += ~~(Math.random() * 1000000) - while (document.getElementById(prefix)) - return prefix - } - - Tooltip.prototype.tip = function () { - if (!this.$tip) { - this.$tip = $(this.options.template) - if (this.$tip.length != 1) { - throw new Error(this.type + ' `template` option must consist of exactly 1 top-level element!') - } - } - return this.$tip - } - - Tooltip.prototype.arrow = function () { - return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow')) - } - - Tooltip.prototype.enable = function () { - this.enabled = true - } - - Tooltip.prototype.disable = function () { - this.enabled = false - } - - Tooltip.prototype.toggleEnabled = function () { - this.enabled = !this.enabled - } - - Tooltip.prototype.toggle = function (e) { - var self = this - if (e) { - self = $(e.currentTarget).data('bs.' + this.type) - if (!self) { - self = new this.constructor(e.currentTarget, this.getDelegateOptions()) - $(e.currentTarget).data('bs.' + this.type, self) - } - } - - if (e) { - self.inState.click = !self.inState.click - if (self.isInStateTrue()) self.enter(self) - else self.leave(self) - } else { - self.tip().hasClass('in') ? self.leave(self) : self.enter(self) - } - } - - Tooltip.prototype.destroy = function () { - var that = this - clearTimeout(this.timeout) - this.hide(function () { - that.$element.off('.' + that.type).removeData('bs.' + that.type) - if (that.$tip) { - that.$tip.detach() - } - that.$tip = null - that.$arrow = null - that.$viewport = null - }) - } - - - // TOOLTIP PLUGIN DEFINITION - // ========================= - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.tooltip') - var options = typeof option == 'object' && option - - if (!data && /destroy|hide/.test(option)) return - if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - var old = $.fn.tooltip - - $.fn.tooltip = Plugin - $.fn.tooltip.Constructor = Tooltip - - - // TOOLTIP NO CONFLICT - // =================== - - $.fn.tooltip.noConflict = function () { - $.fn.tooltip = old - return this - } - -}(jQuery); - -/* ======================================================================== - * Bootstrap: popover.js v3.3.6 - * http://getbootstrap.com/javascript/#popovers - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // POPOVER PUBLIC CLASS DEFINITION - // =============================== - - var Popover = function (element, options) { - this.init('popover', element, options) - } - - if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js') - - Popover.VERSION = '3.3.6' - - Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, { - placement: 'right', - trigger: 'click', - content: '', - template: '' - }) - - - // NOTE: POPOVER EXTENDS tooltip.js - // ================================ - - Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype) - - Popover.prototype.constructor = Popover - - Popover.prototype.getDefaults = function () { - return Popover.DEFAULTS - } - - Popover.prototype.setContent = function () { - var $tip = this.tip() - var title = this.getTitle() - var content = this.getContent() - - $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title) - $tip.find('.popover-content').children().detach().end()[ // we use append for html objects to maintain js events - this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text' - ](content) - - $tip.removeClass('fade top bottom left right in') - - // IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do - // this manually by checking the contents. - if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide() - } - - Popover.prototype.hasContent = function () { - return this.getTitle() || this.getContent() - } - - Popover.prototype.getContent = function () { - var $e = this.$element - var o = this.options - - return $e.attr('data-content') - || (typeof o.content == 'function' ? - o.content.call($e[0]) : - o.content) - } - - Popover.prototype.arrow = function () { - return (this.$arrow = this.$arrow || this.tip().find('.arrow')) - } - - - // POPOVER PLUGIN DEFINITION - // ========================= - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.popover') - var options = typeof option == 'object' && option - - if (!data && /destroy|hide/.test(option)) return - if (!data) $this.data('bs.popover', (data = new Popover(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - var old = $.fn.popover - - $.fn.popover = Plugin - $.fn.popover.Constructor = Popover - - - // POPOVER NO CONFLICT - // =================== - - $.fn.popover.noConflict = function () { - $.fn.popover = old - return this - } - -}(jQuery); - -/* ======================================================================== - * Bootstrap: scrollspy.js v3.3.6 - * http://getbootstrap.com/javascript/#scrollspy - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // SCROLLSPY CLASS DEFINITION - // ========================== - - function ScrollSpy(element, options) { - this.$body = $(document.body) - this.$scrollElement = $(element).is(document.body) ? $(window) : $(element) - this.options = $.extend({}, ScrollSpy.DEFAULTS, options) - this.selector = (this.options.target || '') + ' .nav li > a' - this.offsets = [] - this.targets = [] - this.activeTarget = null - this.scrollHeight = 0 - - this.$scrollElement.on('scroll.bs.scrollspy', $.proxy(this.process, this)) - this.refresh() - this.process() - } - - ScrollSpy.VERSION = '3.3.6' - - ScrollSpy.DEFAULTS = { - offset: 10 - } - - ScrollSpy.prototype.getScrollHeight = function () { - return this.$scrollElement[0].scrollHeight || Math.max(this.$body[0].scrollHeight, document.documentElement.scrollHeight) - } - - ScrollSpy.prototype.refresh = function () { - var that = this - var offsetMethod = 'offset' - var offsetBase = 0 - - this.offsets = [] - this.targets = [] - this.scrollHeight = this.getScrollHeight() - - if (!$.isWindow(this.$scrollElement[0])) { - offsetMethod = 'position' - offsetBase = this.$scrollElement.scrollTop() - } - - this.$body - .find(this.selector) - .map(function () { - var $el = $(this) - var href = $el.data('target') || $el.attr('href') - var $href = /^#./.test(href) && $(href) - - return ($href - && $href.length - && $href.is(':visible') - && [[$href[offsetMethod]().top + offsetBase, href]]) || null - }) - .sort(function (a, b) { return a[0] - b[0] }) - .each(function () { - that.offsets.push(this[0]) - that.targets.push(this[1]) - }) - } - - ScrollSpy.prototype.process = function () { - var scrollTop = this.$scrollElement.scrollTop() + this.options.offset - var scrollHeight = this.getScrollHeight() - var maxScroll = this.options.offset + scrollHeight - this.$scrollElement.height() - var offsets = this.offsets - var targets = this.targets - var activeTarget = this.activeTarget - var i - - if (this.scrollHeight != scrollHeight) { - this.refresh() - } - - if (scrollTop >= maxScroll) { - return activeTarget != (i = targets[targets.length - 1]) && this.activate(i) - } - - if (activeTarget && scrollTop < offsets[0]) { - this.activeTarget = null - return this.clear() - } - - for (i = offsets.length; i--;) { - activeTarget != targets[i] - && scrollTop >= offsets[i] - && (offsets[i + 1] === undefined || scrollTop < offsets[i + 1]) - && this.activate(targets[i]) - } - } - - ScrollSpy.prototype.activate = function (target) { - this.activeTarget = target - - this.clear() - - var selector = this.selector + - '[data-target="' + target + '"],' + - this.selector + '[href="' + target + '"]' - - var active = $(selector) - .parents('li') - .addClass('active') - - if (active.parent('.dropdown-menu').length) { - active = active - .closest('li.dropdown') - .addClass('active') - } - - active.trigger('activate.bs.scrollspy') - } - - ScrollSpy.prototype.clear = function () { - $(this.selector) - .parentsUntil(this.options.target, '.active') - .removeClass('active') - } - - - // SCROLLSPY PLUGIN DEFINITION - // =========================== - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.scrollspy') - var options = typeof option == 'object' && option - - if (!data) $this.data('bs.scrollspy', (data = new ScrollSpy(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - var old = $.fn.scrollspy - - $.fn.scrollspy = Plugin - $.fn.scrollspy.Constructor = ScrollSpy - - - // SCROLLSPY NO CONFLICT - // ===================== - - $.fn.scrollspy.noConflict = function () { - $.fn.scrollspy = old - return this - } - - - // SCROLLSPY DATA-API - // ================== - - $(window).on('load.bs.scrollspy.data-api', function () { - $('[data-spy="scroll"]').each(function () { - var $spy = $(this) - Plugin.call($spy, $spy.data()) - }) - }) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: tab.js v3.3.6 - * http://getbootstrap.com/javascript/#tabs - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // TAB CLASS DEFINITION - // ==================== - - var Tab = function (element) { - // jscs:disable requireDollarBeforejQueryAssignment - this.element = $(element) - // jscs:enable requireDollarBeforejQueryAssignment - } - - Tab.VERSION = '3.3.6' - - Tab.TRANSITION_DURATION = 150 - - Tab.prototype.show = function () { - var $this = this.element - var $ul = $this.closest('ul:not(.dropdown-menu)') - var selector = $this.data('target') - - if (!selector) { - selector = $this.attr('href') - selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 - } - - if ($this.parent('li').hasClass('active')) return - - var $previous = $ul.find('.active:last a') - var hideEvent = $.Event('hide.bs.tab', { - relatedTarget: $this[0] - }) - var showEvent = $.Event('show.bs.tab', { - relatedTarget: $previous[0] - }) - - $previous.trigger(hideEvent) - $this.trigger(showEvent) - - if (showEvent.isDefaultPrevented() || hideEvent.isDefaultPrevented()) return - - var $target = $(selector) - - this.activate($this.closest('li'), $ul) - this.activate($target, $target.parent(), function () { - $previous.trigger({ - type: 'hidden.bs.tab', - relatedTarget: $this[0] - }) - $this.trigger({ - type: 'shown.bs.tab', - relatedTarget: $previous[0] - }) - }) - } - - Tab.prototype.activate = function (element, container, callback) { - var $active = container.find('> .active') - var transition = callback - && $.support.transition - && ($active.length && $active.hasClass('fade') || !!container.find('> .fade').length) - - function next() { - $active - .removeClass('active') - .find('> .dropdown-menu > .active') - .removeClass('active') - .end() - .find('[data-toggle="tab"]') - .attr('aria-expanded', false) - - element - .addClass('active') - .find('[data-toggle="tab"]') - .attr('aria-expanded', true) - - if (transition) { - element[0].offsetWidth // reflow for transition - element.addClass('in') - } else { - element.removeClass('fade') - } - - if (element.parent('.dropdown-menu').length) { - element - .closest('li.dropdown') - .addClass('active') - .end() - .find('[data-toggle="tab"]') - .attr('aria-expanded', true) - } - - callback && callback() - } - - $active.length && transition ? - $active - .one('bsTransitionEnd', next) - .emulateTransitionEnd(Tab.TRANSITION_DURATION) : - next() - - $active.removeClass('in') - } - - - // TAB PLUGIN DEFINITION - // ===================== - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.tab') - - if (!data) $this.data('bs.tab', (data = new Tab(this))) - if (typeof option == 'string') data[option]() - }) - } - - var old = $.fn.tab - - $.fn.tab = Plugin - $.fn.tab.Constructor = Tab - - - // TAB NO CONFLICT - // =============== - - $.fn.tab.noConflict = function () { - $.fn.tab = old - return this - } - - - // TAB DATA-API - // ============ - - var clickHandler = function (e) { - e.preventDefault() - Plugin.call($(this), 'show') - } - - $(document) - .on('click.bs.tab.data-api', '[data-toggle="tab"]', clickHandler) - .on('click.bs.tab.data-api', '[data-toggle="pill"]', clickHandler) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: affix.js v3.3.6 - * http://getbootstrap.com/javascript/#affix - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // AFFIX CLASS DEFINITION - // ====================== - - var Affix = function (element, options) { - this.options = $.extend({}, Affix.DEFAULTS, options) - - this.$target = $(this.options.target) - .on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this)) - .on('click.bs.affix.data-api', $.proxy(this.checkPositionWithEventLoop, this)) - - this.$element = $(element) - this.affixed = null - this.unpin = null - this.pinnedOffset = null - - this.checkPosition() - } - - Affix.VERSION = '3.3.6' - - Affix.RESET = 'affix affix-top affix-bottom' - - Affix.DEFAULTS = { - offset: 0, - target: window - } - - Affix.prototype.getState = function (scrollHeight, height, offsetTop, offsetBottom) { - var scrollTop = this.$target.scrollTop() - var position = this.$element.offset() - var targetHeight = this.$target.height() - - if (offsetTop != null && this.affixed == 'top') return scrollTop < offsetTop ? 'top' : false - - if (this.affixed == 'bottom') { - if (offsetTop != null) return (scrollTop + this.unpin <= position.top) ? false : 'bottom' - return (scrollTop + targetHeight <= scrollHeight - offsetBottom) ? false : 'bottom' - } - - var initializing = this.affixed == null - var colliderTop = initializing ? scrollTop : position.top - var colliderHeight = initializing ? targetHeight : height - - if (offsetTop != null && scrollTop <= offsetTop) return 'top' - if (offsetBottom != null && (colliderTop + colliderHeight >= scrollHeight - offsetBottom)) return 'bottom' - - return false - } - - Affix.prototype.getPinnedOffset = function () { - if (this.pinnedOffset) return this.pinnedOffset - this.$element.removeClass(Affix.RESET).addClass('affix') - var scrollTop = this.$target.scrollTop() - var position = this.$element.offset() - return (this.pinnedOffset = position.top - scrollTop) - } - - Affix.prototype.checkPositionWithEventLoop = function () { - setTimeout($.proxy(this.checkPosition, this), 1) - } - - Affix.prototype.checkPosition = function () { - if (!this.$element.is(':visible')) return - - var height = this.$element.height() - var offset = this.options.offset - var offsetTop = offset.top - var offsetBottom = offset.bottom - var scrollHeight = Math.max($(document).height(), $(document.body).height()) - - if (typeof offset != 'object') offsetBottom = offsetTop = offset - if (typeof offsetTop == 'function') offsetTop = offset.top(this.$element) - if (typeof offsetBottom == 'function') offsetBottom = offset.bottom(this.$element) - - var affix = this.getState(scrollHeight, height, offsetTop, offsetBottom) - - if (this.affixed != affix) { - if (this.unpin != null) this.$element.css('top', '') - - var affixType = 'affix' + (affix ? '-' + affix : '') - var e = $.Event(affixType + '.bs.affix') - - this.$element.trigger(e) - - if (e.isDefaultPrevented()) return - - this.affixed = affix - this.unpin = affix == 'bottom' ? this.getPinnedOffset() : null - - this.$element - .removeClass(Affix.RESET) - .addClass(affixType) - .trigger(affixType.replace('affix', 'affixed') + '.bs.affix') - } - - if (affix == 'bottom') { - this.$element.offset({ - top: scrollHeight - height - offsetBottom - }) - } - } - - - // AFFIX PLUGIN DEFINITION - // ======================= - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.affix') - var options = typeof option == 'object' && option - - if (!data) $this.data('bs.affix', (data = new Affix(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - var old = $.fn.affix - - $.fn.affix = Plugin - $.fn.affix.Constructor = Affix - - - // AFFIX NO CONFLICT - // ================= - - $.fn.affix.noConflict = function () { - $.fn.affix = old - return this - } - - - // AFFIX DATA-API - // ============== - - $(window).on('load', function () { - $('[data-spy="affix"]').each(function () { - var $spy = $(this) - var data = $spy.data() - - data.offset = data.offset || {} - - if (data.offsetBottom != null) data.offset.bottom = data.offsetBottom - if (data.offsetTop != null) data.offset.top = data.offsetTop - - Plugin.call($spy, data) - }) - }) - -}(jQuery); diff --git a/examples/blog/static/js/jquery-1.11.3.min.js b/examples/blog/static/js/jquery-1.11.3.min.js deleted file mode 100644 index 0f60b7bd0..000000000 --- a/examples/blog/static/js/jquery-1.11.3.min.js +++ /dev/null @@ -1,5 +0,0 @@ -/*! jQuery v1.11.3 | (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="1.11.3",m=function(a,b){return new m.fn.init(a,b)},n=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,o=/^-ms-/,p=/-([\da-z])/gi,q=function(a,b){return b.toUpperCase()};m.fn=m.prototype={jquery:l,constructor:m,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=m.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return m.each(this,a,b)},map:function(a){return this.pushStack(m.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},m.extend=m.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||m.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(m.isPlainObject(c)||(b=m.isArray(c)))?(b?(b=!1,f=a&&m.isArray(a)?a:[]):f=a&&m.isPlainObject(a)?a:{},g[d]=m.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},m.extend({expando:"jQuery"+(l+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===m.type(a)},isArray:Array.isArray||function(a){return"array"===m.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){return!m.isArray(a)&&a-parseFloat(a)+1>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==m.type(a)||a.nodeType||m.isWindow(a))return!1;try{if(a.constructor&&!j.call(a,"constructor")&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(k.ownLast)for(b in a)return j.call(a,b);for(b in a);return void 0===b||j.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(b){b&&m.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(o,"ms-").replace(p,q)},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=r(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(n,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(r(Object(a))?m.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(g)return g.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[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=r(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&&(f=a[b],b=a,a=f),m.isFunction(a)?(c=d.call(arguments,2),e=function(){return a.apply(b||this,c.concat(d.call(arguments)))},e.guid=a.guid=a.guid||m.guid++,e):void 0},now:function(){return+new Date},support:k}),m.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function r(a){var b="length"in a&&a.length,c=m.type(a);return"function"===c||m.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var s=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);m.find=s,m.expr=s.selectors,m.expr[":"]=m.expr.pseudos,m.unique=s.uniqueSort,m.text=s.getText,m.isXMLDoc=s.isXML,m.contains=s.contains;var t=m.expr.match.needsContext,u=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,v=/^.[^:#\[\.,]*$/;function w(a,b,c){if(m.isFunction(b))return m.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return m.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(v.test(b))return m.filter(b,a,c);b=m.filter(b,a)}return m.grep(a,function(a){return m.inArray(a,b)>=0!==c})}m.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?m.find.matchesSelector(d,a)?[d]:[]:m.find.matches(a,m.grep(b,function(a){return 1===a.nodeType}))},m.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(m(a).filter(function(){for(b=0;e>b;b++)if(m.contains(d[b],this))return!0}));for(b=0;e>b;b++)m.find(a,d[b],c);return c=this.pushStack(e>1?m.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(w(this,a||[],!1))},not:function(a){return this.pushStack(w(this,a||[],!0))},is:function(a){return!!w(this,"string"==typeof a&&t.test(a)?m(a):a||[],!1).length}});var x,y=a.document,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=m.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||x).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof m?b[0]:b,m.merge(this,m.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:y,!0)),u.test(c[1])&&m.isPlainObject(b))for(c in b)m.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}if(d=y.getElementById(c[2]),d&&d.parentNode){if(d.id!==c[2])return x.find(a);this.length=1,this[0]=d}return this.context=y,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):m.isFunction(a)?"undefined"!=typeof x.ready?x.ready(a):a(m):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),m.makeArray(a,this))};A.prototype=m.fn,x=m(y);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};m.extend({dir:function(a,b,c){var d=[],e=a[b];while(e&&9!==e.nodeType&&(void 0===c||1!==e.nodeType||!m(e).is(c)))1===e.nodeType&&d.push(e),e=e[b];return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),m.fn.extend({has:function(a){var b,c=m(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(m.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=t.test(a)||"string"!=typeof a?m(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&&m.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?m.unique(f):f)},index:function(a){return a?"string"==typeof a?m.inArray(this[0],m(a)):m.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(m.unique(m.merge(this.get(),m(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}m.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return m.dir(a,"parentNode")},parentsUntil:function(a,b,c){return m.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return m.dir(a,"nextSibling")},prevAll:function(a){return m.dir(a,"previousSibling")},nextUntil:function(a,b,c){return m.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return m.dir(a,"previousSibling",c)},siblings:function(a){return m.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return m.sibling(a.firstChild)},contents:function(a){return m.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:m.merge([],a.childNodes)}},function(a,b){m.fn[a]=function(c,d){var e=m.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=m.filter(d,e)),this.length>1&&(C[a]||(e=m.unique(e)),B.test(a)&&(e=e.reverse())),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return m.each(a.match(E)||[],function(a,c){b[c]=!0}),b}m.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):m.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(c=a.memory&&l,d=!0,f=g||0,g=0,e=h.length,b=!0;h&&e>f;f++)if(h[f].apply(l[0],l[1])===!1&&a.stopOnFalse){c=!1;break}b=!1,h&&(i?i.length&&j(i.shift()):c?h=[]:k.disable())},k={add:function(){if(h){var d=h.length;!function f(b){m.each(b,function(b,c){var d=m.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&f(c)})}(arguments),b?e=h.length:c&&(g=d,j(c))}return this},remove:function(){return h&&m.each(arguments,function(a,c){var d;while((d=m.inArray(c,h,d))>-1)h.splice(d,1),b&&(e>=d&&e--,f>=d&&f--)}),this},has:function(a){return a?m.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],e=0,this},disable:function(){return h=i=c=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,c||k.disable(),this},locked:function(){return!i},fireWith:function(a,c){return!h||d&&!i||(c=c||[],c=[a,c.slice?c.slice():c],b?i.push(c):j(c)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!d}};return k},m.extend({Deferred:function(a){var b=[["resolve","done",m.Callbacks("once memory"),"resolved"],["reject","fail",m.Callbacks("once memory"),"rejected"],["notify","progress",m.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return m.Deferred(function(c){m.each(b,function(b,f){var g=m.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&m.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?m.extend(a,d):d}},e={};return d.pipe=d.then,m.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&&m.isFunction(a.promise)?e:0,g=1===f?a:m.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]&&m.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;m.fn.ready=function(a){return m.ready.promise().done(a),this},m.extend({isReady:!1,readyWait:1,holdReady:function(a){a?m.readyWait++:m.ready(!0)},ready:function(a){if(a===!0?!--m.readyWait:!m.isReady){if(!y.body)return setTimeout(m.ready);m.isReady=!0,a!==!0&&--m.readyWait>0||(H.resolveWith(y,[m]),m.fn.triggerHandler&&(m(y).triggerHandler("ready"),m(y).off("ready")))}}});function I(){y.addEventListener?(y.removeEventListener("DOMContentLoaded",J,!1),a.removeEventListener("load",J,!1)):(y.detachEvent("onreadystatechange",J),a.detachEvent("onload",J))}function J(){(y.addEventListener||"load"===event.type||"complete"===y.readyState)&&(I(),m.ready())}m.ready.promise=function(b){if(!H)if(H=m.Deferred(),"complete"===y.readyState)setTimeout(m.ready);else if(y.addEventListener)y.addEventListener("DOMContentLoaded",J,!1),a.addEventListener("load",J,!1);else{y.attachEvent("onreadystatechange",J),a.attachEvent("onload",J);var c=!1;try{c=null==a.frameElement&&y.documentElement}catch(d){}c&&c.doScroll&&!function e(){if(!m.isReady){try{c.doScroll("left")}catch(a){return setTimeout(e,50)}I(),m.ready()}}()}return H.promise(b)};var K="undefined",L;for(L in m(k))break;k.ownLast="0"!==L,k.inlineBlockNeedsLayout=!1,m(function(){var a,b,c,d;c=y.getElementsByTagName("body")[0],c&&c.style&&(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),typeof b.style.zoom!==K&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",k.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(d))}),function(){var a=y.createElement("div");if(null==k.deleteExpando){k.deleteExpando=!0;try{delete a.test}catch(b){k.deleteExpando=!1}}a=null}(),m.acceptData=function(a){var b=m.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b};var M=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,N=/([A-Z])/g;function O(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(N,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:M.test(c)?m.parseJSON(c):c}catch(e){}m.data(a,b,c)}else c=void 0}return c}function P(a){var b;for(b in a)if(("data"!==b||!m.isEmptyObject(a[b]))&&"toJSON"!==b)return!1; - -return!0}function Q(a,b,d,e){if(m.acceptData(a)){var f,g,h=m.expando,i=a.nodeType,j=i?m.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||m.guid++:h),j[k]||(j[k]=i?{}:{toJSON:m.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=m.extend(j[k],b):j[k].data=m.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[m.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[m.camelCase(b)])):f=g,f}}function R(a,b,c){if(m.acceptData(a)){var d,e,f=a.nodeType,g=f?m.cache:a,h=f?a[m.expando]:m.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){m.isArray(b)?b=b.concat(m.map(b,m.camelCase)):b in d?b=[b]:(b=m.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!P(d):!m.isEmptyObject(d))return}(c||(delete g[h].data,P(g[h])))&&(f?m.cleanData([a],!0):k.deleteExpando||g!=g.window?delete g[h]:g[h]=null)}}}m.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?m.cache[a[m.expando]]:a[m.expando],!!a&&!P(a)},data:function(a,b,c){return Q(a,b,c)},removeData:function(a,b){return R(a,b)},_data:function(a,b,c){return Q(a,b,c,!0)},_removeData:function(a,b){return R(a,b,!0)}}),m.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.data(f),1===f.nodeType&&!m._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=m.camelCase(d.slice(5)),O(f,d,e[d])));m._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){m.data(this,a)}):arguments.length>1?this.each(function(){m.data(this,a,b)}):f?O(f,a,m.data(f,a)):void 0},removeData:function(a){return this.each(function(){m.removeData(this,a)})}}),m.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=m._data(a,b),c&&(!d||m.isArray(c)?d=m._data(a,b,m.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=m.queue(a,b),d=c.length,e=c.shift(),f=m._queueHooks(a,b),g=function(){m.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 m._data(a,c)||m._data(a,c,{empty:m.Callbacks("once memory").add(function(){m._removeData(a,b+"queue"),m._removeData(a,c)})})}}),m.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;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},W=/^(?:checkbox|radio)$/i;!function(){var a=y.createElement("input"),b=y.createElement("div"),c=y.createDocumentFragment();if(b.innerHTML="
    a",k.leadingWhitespace=3===b.firstChild.nodeType,k.tbody=!b.getElementsByTagName("tbody").length,k.htmlSerialize=!!b.getElementsByTagName("link").length,k.html5Clone="<:nav>"!==y.createElement("nav").cloneNode(!0).outerHTML,a.type="checkbox",a.checked=!0,c.appendChild(a),k.appendChecked=a.checked,b.innerHTML="",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue,c.appendChild(b),b.innerHTML="",k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,k.noCloneEvent=!0,b.attachEvent&&(b.attachEvent("onclick",function(){k.noCloneEvent=!1}),b.cloneNode(!0).click()),null==k.deleteExpando){k.deleteExpando=!0;try{delete b.test}catch(d){k.deleteExpando=!1}}}(),function(){var b,c,d=y.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(k[b+"Bubbles"]=c in a)||(d.setAttribute(c,"t"),k[b+"Bubbles"]=d.attributes[c].expando===!1);d=null}();var X=/^(?:input|select|textarea)$/i,Y=/^key/,Z=/^(?:mouse|pointer|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=/^([^.]*)(?:\.(.+)|)$/;function aa(){return!0}function ba(){return!1}function ca(){try{return y.activeElement}catch(a){}}m.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=m.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return typeof m===K||a&&m.event.triggered===a.type?void 0:m.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(E)||[""],h=b.length;while(h--)f=_.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=m.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=m.event.special[o]||{},l=m.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&m.expr.match.needsContext.test(e),namespace:p.join(".")},i),(n=g[o])||(n=g[o]=[],n.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?n.splice(n.delegateCount++,0,l):n.push(l),m.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m.hasData(a)&&m._data(a);if(r&&(k=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=_.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=m.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,n=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=n.length;while(f--)g=n[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(n.splice(f,1),g.selector&&n.delegateCount--,l.remove&&l.remove.call(a,g));i&&!n.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||m.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)m.event.remove(a,o+b[j],c,d,!0);m.isEmptyObject(k)&&(delete r.handle,m._removeData(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,l,n,o=[d||y],p=j.call(b,"type")?b.type:b,q=j.call(b,"namespace")?b.namespace.split("."):[];if(h=l=d=d||y,3!==d.nodeType&&8!==d.nodeType&&!$.test(p+m.event.triggered)&&(p.indexOf(".")>=0&&(q=p.split("."),p=q.shift(),q.sort()),g=p.indexOf(":")<0&&"on"+p,b=b[m.expando]?b:new m.Event(p,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=q.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:m.makeArray(c,[b]),k=m.event.special[p]||{},e||!k.trigger||k.trigger.apply(d,c)!==!1)){if(!e&&!k.noBubble&&!m.isWindow(d)){for(i=k.delegateType||p,$.test(i+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),l=h;l===(d.ownerDocument||y)&&o.push(l.defaultView||l.parentWindow||a)}n=0;while((h=o[n++])&&!b.isPropagationStopped())b.type=n>1?i:k.bindType||p,f=(m._data(h,"events")||{})[b.type]&&m._data(h,"handle"),f&&f.apply(h,c),f=g&&h[g],f&&f.apply&&m.acceptData(h)&&(b.result=f.apply(h,c),b.result===!1&&b.preventDefault());if(b.type=p,!e&&!b.isDefaultPrevented()&&(!k._default||k._default.apply(o.pop(),c)===!1)&&m.acceptData(d)&&g&&d[p]&&!m.isWindow(d)){l=d[g],l&&(d[g]=null),m.event.triggered=p;try{d[p]()}catch(r){}m.event.triggered=void 0,l&&(d[g]=l)}return b.result}},dispatch:function(a){a=m.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(m._data(this,"events")||{})[a.type]||[],k=m.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=m.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,g=0;while((e=f.handlers[g++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(e.namespace))&&(a.handleObj=e,a.data=e.data,c=((m.event.special[e.origType]||{}).handle||e.handler).apply(f.elem,i),void 0!==c&&(a.result=c)===!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(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(e=[],f=0;h>f;f++)d=b[f],c=d.selector+" ",void 0===e[c]&&(e[c]=d.needsContext?m(c,this).index(i)>=0:m.find(c,this,null,[i]).length),e[c]&&e.push(d);e.length&&g.push({elem:i,handlers:e})}return h]","i"),ha=/^\s+/,ia=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,ja=/<([\w:]+)/,ka=/\s*$/g,ra={option:[1,""],legend:[1,"
    ","
    "],area:[1,"",""],param:[1,"",""],thead:[1,"","
    "],tr:[2,"","
    "],col:[2,"","
    "],td:[3,"","
    "],_default:k.htmlSerialize?[0,"",""]:[1,"X
    ","
    "]},sa=da(y),ta=sa.appendChild(y.createElement("div"));ra.optgroup=ra.option,ra.tbody=ra.tfoot=ra.colgroup=ra.caption=ra.thead,ra.th=ra.td;function ua(a,b){var c,d,e=0,f=typeof a.getElementsByTagName!==K?a.getElementsByTagName(b||"*"):typeof a.querySelectorAll!==K?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||m.nodeName(d,b)?f.push(d):m.merge(f,ua(d,b));return void 0===b||b&&m.nodeName(a,b)?m.merge([a],f):f}function va(a){W.test(a.type)&&(a.defaultChecked=a.checked)}function wa(a,b){return m.nodeName(a,"table")&&m.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function xa(a){return a.type=(null!==m.find.attr(a,"type"))+"/"+a.type,a}function ya(a){var b=pa.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function za(a,b){for(var c,d=0;null!=(c=a[d]);d++)m._data(c,"globalEval",!b||m._data(b[d],"globalEval"))}function Aa(a,b){if(1===b.nodeType&&m.hasData(a)){var c,d,e,f=m._data(a),g=m._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)m.event.add(b,c,h[c][d])}g.data&&(g.data=m.extend({},g.data))}}function Ba(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!k.noCloneEvent&&b[m.expando]){e=m._data(b);for(d in e.events)m.removeEvent(b,d,e.handle);b.removeAttribute(m.expando)}"script"===c&&b.text!==a.text?(xa(b).text=a.text,ya(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),k.html5Clone&&a.innerHTML&&!m.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&W.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}}m.extend({clone:function(a,b,c){var d,e,f,g,h,i=m.contains(a.ownerDocument,a);if(k.html5Clone||m.isXMLDoc(a)||!ga.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(ta.innerHTML=a.outerHTML,ta.removeChild(f=ta.firstChild)),!(k.noCloneEvent&&k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||m.isXMLDoc(a)))for(d=ua(f),h=ua(a),g=0;null!=(e=h[g]);++g)d[g]&&Ba(e,d[g]);if(b)if(c)for(h=h||ua(a),d=d||ua(f),g=0;null!=(e=h[g]);g++)Aa(e,d[g]);else Aa(a,f);return d=ua(f,"script"),d.length>0&&za(d,!i&&ua(a,"script")),d=h=e=null,f},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,l,n=a.length,o=da(b),p=[],q=0;n>q;q++)if(f=a[q],f||0===f)if("object"===m.type(f))m.merge(p,f.nodeType?[f]:f);else if(la.test(f)){h=h||o.appendChild(b.createElement("div")),i=(ja.exec(f)||["",""])[1].toLowerCase(),l=ra[i]||ra._default,h.innerHTML=l[1]+f.replace(ia,"<$1>")+l[2],e=l[0];while(e--)h=h.lastChild;if(!k.leadingWhitespace&&ha.test(f)&&p.push(b.createTextNode(ha.exec(f)[0])),!k.tbody){f="table"!==i||ka.test(f)?""!==l[1]||ka.test(f)?0:h:h.firstChild,e=f&&f.childNodes.length;while(e--)m.nodeName(j=f.childNodes[e],"tbody")&&!j.childNodes.length&&f.removeChild(j)}m.merge(p,h.childNodes),h.textContent="";while(h.firstChild)h.removeChild(h.firstChild);h=o.lastChild}else p.push(b.createTextNode(f));h&&o.removeChild(h),k.appendChecked||m.grep(ua(p,"input"),va),q=0;while(f=p[q++])if((!d||-1===m.inArray(f,d))&&(g=m.contains(f.ownerDocument,f),h=ua(o.appendChild(f),"script"),g&&za(h),c)){e=0;while(f=h[e++])oa.test(f.type||"")&&c.push(f)}return h=null,o},cleanData:function(a,b){for(var d,e,f,g,h=0,i=m.expando,j=m.cache,l=k.deleteExpando,n=m.event.special;null!=(d=a[h]);h++)if((b||m.acceptData(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)n[e]?m.event.remove(d,e):m.removeEvent(d,e,g.handle);j[f]&&(delete j[f],l?delete d[i]:typeof d.removeAttribute!==K?d.removeAttribute(i):d[i]=null,c.push(f))}}}),m.fn.extend({text:function(a){return V(this,function(a){return void 0===a?m.text(this):this.empty().append((this[0]&&this[0].ownerDocument||y).createTextNode(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=wa(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=wa(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?m.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||m.cleanData(ua(c)),c.parentNode&&(b&&m.contains(c.ownerDocument,c)&&za(ua(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&m.cleanData(ua(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&m.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return m.clone(this,a,b)})},html:function(a){return V(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(fa,""):void 0;if(!("string"!=typeof a||ma.test(a)||!k.htmlSerialize&&ga.test(a)||!k.leadingWhitespace&&ha.test(a)||ra[(ja.exec(a)||["",""])[1].toLowerCase()])){a=a.replace(ia,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(m.cleanData(ua(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,m.cleanData(ua(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,n=this,o=l-1,p=a[0],q=m.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&na.test(p))return this.each(function(c){var d=n.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(i=m.buildFragment(a,this[0].ownerDocument,!1,this),c=i.firstChild,1===i.childNodes.length&&(i=c),c)){for(g=m.map(ua(i,"script"),xa),f=g.length;l>j;j++)d=i,j!==o&&(d=m.clone(d,!0,!0),f&&m.merge(g,ua(d,"script"))),b.call(this[j],d,j);if(f)for(h=g[g.length-1].ownerDocument,m.map(g,ya),j=0;f>j;j++)d=g[j],oa.test(d.type||"")&&!m._data(d,"globalEval")&&m.contains(h,d)&&(d.src?m._evalUrl&&m._evalUrl(d.src):m.globalEval((d.text||d.textContent||d.innerHTML||"").replace(qa,"")));i=c=null}return this}}),m.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){m.fn[a]=function(a){for(var c,d=0,e=[],g=m(a),h=g.length-1;h>=d;d++)c=d===h?this:this.clone(!0),m(g[d])[b](c),f.apply(e,c.get());return this.pushStack(e)}});var Ca,Da={};function Ea(b,c){var d,e=m(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:m.css(e[0],"display");return e.detach(),f}function Fa(a){var b=y,c=Da[a];return c||(c=Ea(a,b),"none"!==c&&c||(Ca=(Ca||m(".*?\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 = newTestHelper(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 vimeo title - { - `{{< vimeo 146022717 video my-title >}}`, - "(?s)\n
    .*?.*?
    \n", - }, - // set class (using named params) - { - `{{< vimeo id="146022717" class="video" >}}`, - "(?s)^
    .*?.*?
    ", - }, - // set vimeo title (using named params) - { - `{{< vimeo id="146022717" class="video" title="my vimeo video" >}}`, - "(?s)^
    .*?.*?
    ", - }, - } { - var ( - cfg, fs = newTestCfg() - th = newTestHelper(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 = newTestHelper(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 { - privacy map[string]interface{} - in, resp, expected string - }{ - { - map[string]interface{}{ - "twitter": map[string]interface{}{ - "simple": true, - }, - }, - `{{< 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"}`, - `.twitter-tweet a`, - }, - { - map[string]interface{}{ - "twitter": map[string]interface{}{ - "simple": false, - }, - }, - `{{< 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 ...interface{}) 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 = newTestHelper(cfg, fs, t) - ) - - cfg.Set("privacy", this.privacy) - - 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 = newTestHelper(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 a2debc253..000000000 --- a/hugolib/embedded_templates_test.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 hugolib - -import ( - "testing" - - qt "github.com/frankban/quicktest" -) - -// Just some simple test of the embedded templates to avoid -// https://github.com/gohugoio/hugo/issues/4757 and similar. -// TODO(bep) fix me https://github.com/gohugoio/hugo/issues/5926 -func _TestEmbeddedTemplates(t *testing.T) { - t.Parallel() - - c := qt.New(t) - c.Assert(true, qt.Equals, true) - - home := []string{"index.html", ` -GA: -{{ template "_internal/google_analytics.html" . }} - -GA async: - -{{ template "_internal/google_analytics_async.html" . }} - -Disqus: - -{{ template "_internal/disqus.html" . }} - -`} - - b := newTestSitesBuilder(t) - b.WithSimpleConfigFile().WithTemplatesAdded(home...) - - b.Build(BuildCfg{}) - - // Gheck GA regular and async - b.AssertFileContent("public/index.html", - "'anonymizeIp', true", - "'script','https://www.google-analytics.com/analytics.js','ga');\n\tga('create', 'ga_id', 'auto')", - " -` - - simplePageWithSummaryDelimiterSameLine = `--- -title: Simple ---- -Summary Same Line - -Some more text -` - - simplePageWithAllCJKRunes = `--- -title: Simple ---- - - -€ € € € € -你好 -도형이 -カテゴリー - - -` - - simplePageWithMainEnglishWithCJKRunes = `--- -title: Simple ---- - - -In Chinese, 好 means good. In Chinese, 好 means good. -In Chinese, 好 means good. In Chinese, 好 means good. -In Chinese, 好 means good. In Chinese, 好 means good. -In Chinese, 好 means good. In Chinese, 好 means good. -In Chinese, 好 means good. In Chinese, 好 means good. -In Chinese, 好 means good. In Chinese, 好 means good. -In Chinese, 好 means good. In Chinese, 好 means good. -More then 70 words. - - -` - simplePageWithMainEnglishWithCJKRunesSummary = "In Chinese, 好 means good. In Chinese, 好 means good. " + - "In Chinese, 好 means good. In Chinese, 好 means good. " + - "In Chinese, 好 means good. In Chinese, 好 means good. " + - "In Chinese, 好 means good. In Chinese, 好 means good. " + - "In Chinese, 好 means good. In Chinese, 好 means good. " + - "In Chinese, 好 means good. In Chinese, 好 means good. " + - "In Chinese, 好 means good. In Chinese, 好 means good." - - simplePageWithIsCJKLanguageFalse = `--- -title: Simple -isCJKLanguage: false ---- - -In Chinese, 好的啊 means good. In Chinese, 好的呀 means good. -In Chinese, 好的啊 means good. In Chinese, 好的呀 means good. -In Chinese, 好的啊 means good. In Chinese, 好的呀 means good. -In Chinese, 好的啊 means good. In Chinese, 好的呀 means good. -In Chinese, 好的啊 means good. In Chinese, 好的呀 means good. -In Chinese, 好的啊 means good. In Chinese, 好的呀 means good. -In Chinese, 好的啊 means good. In Chinese, 好的呀呀 means good enough. -More then 70 words. - - -` - simplePageWithIsCJKLanguageFalseSummary = "In Chinese, 好的啊 means good. In Chinese, 好的呀 means good. " + - "In Chinese, 好的啊 means good. In Chinese, 好的呀 means good. " + - "In Chinese, 好的啊 means good. In Chinese, 好的呀 means good. " + - "In Chinese, 好的啊 means good. In Chinese, 好的呀 means good. " + - "In Chinese, 好的啊 means good. In Chinese, 好的呀 means good. " + - "In Chinese, 好的啊 means good. In Chinese, 好的呀 means good. " + - "In Chinese, 好的啊 means good. In Chinese, 好的呀呀 means good enough." - - simplePageWithLongContent = `--- -title: Simple ---- - -Lorem ipsum dolor sit amet, consectetur adipisicing 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. Lorem ipsum dolor sit -amet, consectetur adipisicing 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. Lorem ipsum dolor sit amet, -consectetur adipisicing 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. Lorem ipsum dolor sit amet, consectetur -adipisicing 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. Lorem ipsum dolor sit amet, consectetur adipisicing 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. Lorem -ipsum dolor sit amet, consectetur adipisicing 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. Lorem ipsum dolor sit -amet, consectetur adipisicing 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.` - - pageWithToC = `--- -title: TOC ---- -For some moments the old man did not reply. He stood with bowed head, buried in deep thought. But at last he spoke. - -## AA - -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. - -### AAA - -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 - -### 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 = `--- -title: Simple -url: simple/url/ ---- -Simple Page With URL` - - simplePageWithSlug = `--- -title: Simple -slug: simple-slug ---- -Simple Page With Slug` - - simplePageWithDate = `--- -title: Simple -date: '2013-10-15T06:16:13' ---- -Simple Page With Date` - - UTF8Page = `--- -title: ラーメン ---- -UTF8 Page` - - UTF8PageWithURL = `--- -title: ラーメン -url: ラーメン/url/ ---- -UTF8 Page With URL` - - UTF8PageWithSlug = `--- -title: ラーメン -slug: ラーメン-slug ---- -UTF8 Page With Slug` - - UTF8PageWithDate = `--- -title: ラーメン -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) - } -} - -func checkPageContent(t *testing.T, page page.Page, expected string, msg ...interface{}) { - t.Helper() - a := normalizeContent(expected) - b := normalizeContent(content(page)) - if a != b { - t.Fatalf("Page content is:\n%q\nExpected:\n%q (%q)", b, a, msg) - } -} - -func normalizeContent(c string) string { - norm := c - norm = strings.Replace(norm, "\n", " ", -1) - norm = strings.Replace(norm, " ", " ", -1) - norm = strings.Replace(norm, " ", " ", -1) - norm = strings.Replace(norm, " ", " ", -1) - norm = strings.Replace(norm, "p> ", "p>", -1) - norm = strings.Replace(norm, "> <", "> <", -1) - return strings.TrimSpace(norm) -} - -func checkPageTOC(t *testing.T, page 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 ...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) { - 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 normalizeExpected(ext, str string) string { - str = normalizeContent(str) - switch ext { - default: - return str - case "html": - return strings.Trim(helpers.StripHTML(str), " ") - case "ad": - paragraphs := strings.Split(str, "

    ") - expected := "" - for _, para := range paragraphs { - if para == "" { - continue - } - expected += fmt.Sprintf("
    \n%s

    \n", para) - } - - return expected - case "rst": - return fmt.Sprintf("
    \n\n\n%s
    ", str) - } -} - -func testAllMarkdownEnginesForPages(t *testing.T, - assertFunc func(t *testing.T, ext string, pages page.Pages), settings map[string]interface{}, pageSources ...string) { - - engines := []struct { - ext string - shouldExecute func() bool - }{ - {"md", func() bool { return true }}, - {"mmark", func() bool { return true }}, - {"ad", func() bool { return helpers.HasAsciidoc() }}, - {"rst", func() bool { return helpers.HasRst() }}, - } - - for _, e := range engines { - if !e.shouldExecute() { - continue - } - - cfg, fs := newTestCfg(func(cfg config.Provider) error { - for k, v := range settings { - cfg.Set(k, v) - } - return nil - - }) - - contentDir := "content" - - if s := cfg.GetString("contentDir"); s != "" { - contentDir = s - } - - var fileSourcePairs []string - - for i, source := range pageSources { - fileSourcePairs = append(fileSourcePairs, fmt.Sprintf("p%d.%s", i, e.ext), source) - } - - for i := 0; i < len(fileSourcePairs); i += 2 { - writeSource(t, fs, filepath.Join(contentDir, fileSourcePairs[i]), fileSourcePairs[i+1]) - } - - // Add a content page for the home page - homePath := fmt.Sprintf("_index.%s", e.ext) - writeSource(t, fs, filepath.Join(contentDir, homePath), homePage) - - b := newTestSitesBuilderFromDepsCfg(t, deps.DepsCfg{Fs: fs, Cfg: cfg}).WithNothingAdded() - b.Build(BuildCfg{SkipRender: true}) - - s := b.H.Sites[0] - - b.Assert(len(s.RegularPages()), qt.Equals, len(pageSources)) - - assertFunc(t, e.ext, s.RegularPages()) - - home, err := s.Info.Home() - b.Assert(err, qt.IsNil) - b.Assert(home, qt.Not(qt.IsNil)) - b.Assert(home.File().Path(), qt.Equals, homePath) - b.Assert(content(home), qt.Contains, "Home Page Content") - - } - -} - -// Issue #1076 -func TestPageWithDelimiterForMarkdownThatCrossesBorder(t *testing.T) { - t.Parallel() - cfg, fs := newTestCfg() - c := qt.New(t) - - writeSource(t, fs, filepath.Join("content", "simple.md"), simplePageWithSummaryDelimiterAndMarkdownThatCrossesBorder) - - s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) - - c.Assert(len(s.RegularPages()), qt.Equals, 1) - - p := s.RegularPages()[0] - - if p.Summary() != template.HTML( - "

    The best static site generator.1

    ") { - t.Fatalf("Got summary:\n%q", p.Summary()) - } - - cnt := content(p) - if cnt != "

    The best static site generator.1

    \n\n
    \n\n
    \n\n
      \n
    1. Many people say so.\n [return]
    2. \n
    \n
    " { - t.Fatalf("Got content:\n%q", cnt) - } -} - -func TestPageDatesAllKinds(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.Assert(t.Year(), qt.Equals, 2017) - } - - checkDated := func(d resource.Dated, msg string) { - checkDate(d.Date(), "date: "+msg) - checkDate(d.Lastmod(), "lastmod: "+msg) - } - for _, p := range s.Pages() { - checkDated(p, p.Kind()) - } - checkDate(s.Info.LastChange(), "site") - -} - -func TestPageDatesSections(t *testing.T) { - t.Parallel() - - b := newTestSitesBuilder(t) - b.WithSimpleConfigFile().WithContent("no-index/page.md", ` ---- -title: Page -date: 2017-01-15 ---- -`) - b.WithSimpleConfigFile().WithContent("with-index-no-date/_index.md", `--- -title: No Date ---- - -`) - - // https://github.com/gohugoio/hugo/issues/5854 - b.WithSimpleConfigFile().WithContent("with-index-date/_index.md", `--- -title: Date -date: 2018-01-15 ---- - -`) - - b.CreateSites().Build(BuildCfg{}) - - b.Assert(len(b.H.Sites), qt.Equals, 1) - s := b.H.Sites[0] - - b.Assert(s.getPage("/").Date().Year(), qt.Equals, 2017) - b.Assert(s.getPage("/no-index").Date().Year(), qt.Equals, 2017) - b.Assert(s.getPage("/with-index-no-date").Date().IsZero(), qt.Equals, true) - b.Assert(s.getPage("/with-index-date").Date().Year(), qt.Equals, 2018) -} - -func TestCreateNewPage(t *testing.T) { - t.Parallel() - c := qt.New(t) - assertFunc := func(t *testing.T, ext string, pages page.Pages) { - p := pages[0] - - // issue #2290: Path is relative to the content dir and will continue to be so. - c.Assert(p.File().Path(), qt.Equals, fmt.Sprintf("p0.%s", ext)) - c.Assert(p.IsHome(), qt.Equals, false) - checkPageTitle(t, p, "Simple") - checkPageContent(t, p, normalizeExpected(ext, "

    Simple Page

    \n")) - checkPageSummary(t, p, "Simple Page") - checkPageType(t, p, "page") - } - - settings := map[string]interface{}{ - "contentDir": "mycontent", - } - - testAllMarkdownEnginesForPages(t, assertFunc, settings, simplePage) -} - -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, "

    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

    \n\n

    Additional text.

    \n\n

    Further text.

    \n"), ext) - checkPageSummary(t, p, normalizeExpected(ext, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Additional text."), ext) - } - 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) { - p := pages[0] - checkPageTitle(t, p, "Simple") - checkPageContent(t, p, normalizeExpected(ext, "

    Summary Next Line

    \n\n

    Some more text

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

    Summary Next Line

    "), ext) - checkPageType(t, p, "page") - } - - testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithSummaryDelimiter) -} - -func TestPageWithSummaryParameter(t *testing.T) { - t.Parallel() - assertFunc := func(t *testing.T, ext string, pages page.Pages) { - p := pages[0] - checkPageTitle(t, p, "SimpleWithSummaryParameter") - checkPageContent(t, p, normalizeExpected(ext, "

    Some text.

    \n\n

    Some more text.

    \n"), ext) - // Summary is not Asciidoctor- or RST-compatible so don't test them - if ext != "ad" && ext != "rst" { - checkPageSummary(t, p, normalizeExpected(ext, "Page with summary parameter and a link"), ext) - } - checkPageType(t, p, "page") - } - - testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithSummaryParameter) -} - -// 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) - } - - 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) - } - - } - - fields := []string{"date", "publishdate", "pubdate", "published"} - pageContents := make([]string, len(fields)) - for i, field := range fields { - pageContents[i] = datePage(field, i+1) - } - - testAllMarkdownEnginesForPages(t, assertFunc, nil, pageContents...) -} - -// Issue #2601 -func TestPageRawContent(t *testing.T) { - t.Parallel() - cfg, fs := newTestCfg() - c := qt.New(t) - - writeSource(t, fs, filepath.Join("content", "raw.md"), `--- -title: Raw ---- -**Raw**`) - - writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), `{{ .RawContent }}`) - - s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) - - c.Assert(len(s.RegularPages()), qt.Equals, 1) - p := s.RegularPages()[0] - - c.Assert("**Raw**", qt.Equals, p.RawContent()) - -} - -func TestPageWithShortCodeInSummary(t *testing.T) { - t.Parallel() - assertFunc := func(t *testing.T, ext string, pages page.Pages) { - p := pages[0] - checkPageTitle(t, p, "Simple") - checkPageContent(t, p, normalizeExpected(ext, "

    Summary Next Line.

    . More text here.

    Some more text

    ")) - checkPageSummary(t, p, "Summary Next Line. . More text here. Some more text") - checkPageType(t, p, "page") - } - - testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithShortcodeInSummary) -} - -func TestPageWithEmbeddedScriptTag(t *testing.T) { - t.Parallel() - assertFunc := func(t *testing.T, ext string, pages page.Pages) { - p := pages[0] - if ext == "ad" || ext == "rst" { - // TOD(bep) - return - } - checkPageContent(t, p, "\n", ext) - } - - testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithEmbeddedScript) -} - -func TestPageWithAdditionalExtension(t *testing.T) { - t.Parallel() - cfg, fs := newTestCfg() - c := qt.New(t) - - writeSource(t, fs, filepath.Join("content", "simple.md"), simplePageWithAdditionalExtension) - - s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) - - c.Assert(len(s.RegularPages()), qt.Equals, 1) - - p := s.RegularPages()[0] - - checkPageContent(t, p, "

    first line.
    \nsecond line.

    \n\n

    fourth line.

    \n") -} - -func TestTableOfContents(t *testing.T) { - - cfg, fs := newTestCfg() - c := qt.New(t) - - writeSource(t, fs, filepath.Join("content", "tocpage.md"), pageWithToC) - - s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) - - c.Assert(len(s.RegularPages()), qt.Equals, 1) - - p := s.RegularPages()[0] - - checkPageContent(t, p, "\n\n

    For some moments the old man did not reply. He stood with bowed head, buried in deep thought. But at last he spoke.

    \n\n

    AA

    \n\n

    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.

    \n\n

    AAA

    \n\n

    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

    \n\n

    BBB

    \n\n

    “You’re a great Granser,” he cried delightedly, “always making believe them little marks mean something.”

    \n") - checkPageTOC(t, p, "") -} - -func TestPageWithMoreTag(t *testing.T) { - t.Parallel() - assertFunc := func(t *testing.T, ext string, pages page.Pages) { - p := pages[0] - checkPageTitle(t, p, "Simple") - checkPageContent(t, p, normalizeExpected(ext, "

    Summary Same Line

    \n\n

    Some more text

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

    Summary Same Line

    ")) - checkPageType(t, p, "page") - - } - - testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithSummaryDelimiterSameLine) -} - -// #2973 -func TestSummaryWithHTMLTagsOnNextLine(t *testing.T) { - - assertFunc := func(t *testing.T, ext string, pages page.Pages) { - c := qt.New(t) - p := pages[0] - s := string(p.Summary()) - c.Assert(s, qt.Contains, "Happy new year everyone!") - c.Assert(s, qt.Not(qt.Contains), "User interface") - } - - testAllMarkdownEnginesForPages(t, assertFunc, nil, `--- -title: Simple ---- -Happy new year everyone! - -Here is the last report for commits in the year 2016. It covers hrev50718-hrev50829. - - - -

    User interface

    - -`) -} - -func TestPageWithDate(t *testing.T) { - t.Parallel() - cfg, fs := newTestCfg() - c := qt.New(t) - - writeSource(t, fs, filepath.Join("content", "simple.md"), simplePageRFC3339Date) - - s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) - - c.Assert(len(s.RegularPages()), qt.Equals, 1) - - p := s.RegularPages()[0] - d, _ := time.Parse(time.RFC3339, "2013-05-17T16:59:30Z") - - checkPageDate(t, p, d) -} - -func TestPageWithLastmodFromGitInfo(t *testing.T) { - c := qt.New(t) - - // We need to use the OS fs for this. - cfg := viper.New() - fs := hugofs.NewFrom(hugofs.Os, cfg) - fs.Destination = &afero.MemMapFs{} - - wd, err := os.Getwd() - c.Assert(err, qt.IsNil) - - cfg.Set("frontmatter", map[string]interface{}{ - "lastmod": []string{":git", "lastmod"}, - }) - cfg.Set("defaultContentLanguage", "en") - - langConfig := map[string]interface{}{ - "en": map[string]interface{}{ - "weight": 1, - "languageName": "English", - "contentDir": "content", - }, - "nn": map[string]interface{}{ - "weight": 2, - "languageName": "Nynorsk", - "contentDir": "content_nn", - }, - } - - cfg.Set("languages", langConfig) - cfg.Set("enableGitInfo", true) - - cfg.Set("workingDir", filepath.Join(wd, "testsite")) - - b := newTestSitesBuilderFromDepsCfg(t, deps.DepsCfg{Fs: fs, Cfg: cfg}).WithNothingAdded() - - b.Build(BuildCfg{SkipRender: true}) - h := b.H - - c.Assert(len(h.Sites), qt.Equals, 2) - - enSite := h.Sites[0] - c.Assert(len(enSite.RegularPages()), qt.Equals, 1) - - // 2018-03-11 is the Git author date for testsite/content/first-post.md - c.Assert(enSite.RegularPages()[0].Lastmod().Format("2006-01-02"), qt.Equals, "2018-03-11") - - nnSite := h.Sites[1] - c.Assert(len(nnSite.RegularPages()), qt.Equals, 1) - - // 2018-08-11 is the Git author date for testsite/content_nn/first-post.md - c.Assert(nnSite.RegularPages()[0].Lastmod().Format("2006-01-02"), qt.Equals, "2018-08-11") - -} - -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]interface{}{ - "date": []string{dateHandler, "date"}, - }) - - 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, Cfg: cfg}).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) { - p := pages[0] - if p.WordCount() != 8 { - t.Fatalf("[%s] incorrect word count. expected %v, got %v", ext, 8, p.WordCount()) - } - } - - testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithAllCJKRunes) -} - -func TestWordCountWithAllCJKRunesHasCJKLanguage(t *testing.T) { - t.Parallel() - settings := map[string]interface{}{"hasCJKLanguage": true} - - assertFunc := func(t *testing.T, ext string, pages page.Pages) { - p := pages[0] - if p.WordCount() != 15 { - t.Fatalf("[%s] incorrect word count, expected %v, got %v", ext, 15, p.WordCount()) - } - } - testAllMarkdownEnginesForPages(t, assertFunc, settings, simplePageWithAllCJKRunes) -} - -func TestWordCountWithMainEnglishWithCJKRunes(t *testing.T) { - t.Parallel() - settings := map[string]interface{}{"hasCJKLanguage": true} - - assertFunc := func(t *testing.T, ext string, pages page.Pages) { - p := pages[0] - if p.WordCount() != 74 { - t.Fatalf("[%s] incorrect word count, expected %v, got %v", ext, 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()) - } - } - - testAllMarkdownEnginesForPages(t, assertFunc, settings, simplePageWithMainEnglishWithCJKRunes) -} - -func TestWordCountWithIsCJKLanguageFalse(t *testing.T) { - t.Parallel() - settings := map[string]interface{}{ - "hasCJKLanguage": true, - } - - assertFunc := func(t *testing.T, ext string, pages page.Pages) { - p := pages[0] - 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) { - p := pages[0] - if p.WordCount() != 483 { - t.Fatalf("[%s] incorrect word count. expected %v, got %v", ext, 483, p.WordCount()) - } - - if p.FuzzyWordCount() != 500 { - t.Fatalf("[%s] incorrect word count. expected %v, got %v", ext, 500, p.FuzzyWordCount()) - } - - if p.ReadingTime() != 3 { - t.Fatalf("[%s] incorrect min read. expected %v, got %v", ext, 3, p.ReadingTime()) - } - - } - - testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithLongContent) -} - -func TestPagePaths(t *testing.T) { - t.Parallel() - c := qt.New(t) - - siteParmalinksSetting := map[string]string{ - "post": ":year/:month/:day/:title/", - } - - tests := []struct { - content string - path string - hasPermalink bool - expected string - }{ - {simplePage, "post/x.md", false, "post/x.html"}, - {simplePageWithURL, "post/x.md", false, "simple/url/index.html"}, - {simplePageWithSlug, "post/x.md", false, "post/simple-slug.html"}, - {simplePageWithDate, "post/x.md", true, "2013/10/15/simple/index.html"}, - {UTF8Page, "post/x.md", false, "post/x.html"}, - {UTF8PageWithURL, "post/x.md", false, "ラーメン/url/index.html"}, - {UTF8PageWithSlug, "post/x.md", false, "post/ラーメン-slug.html"}, - {UTF8PageWithDate, "post/x.md", true, "2013/10/15/ラーメン/index.html"}, - } - - for _, test := range tests { - cfg, fs := newTestCfg() - - if test.hasPermalink { - cfg.Set("permalinks", siteParmalinksSetting) - } - - writeSource(t, fs, filepath.Join("content", filepath.FromSlash(test.path)), test.content) - - s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) - c.Assert(len(s.RegularPages()), qt.Equals, 1) - - } -} - -func TestTranslationKey(t *testing.T) { - t.Parallel() - c := qt.New(t) - cfg, fs := newTestCfg() - - writeSource(t, fs, filepath.Join("content", filepath.FromSlash("sect/simple.no.md")), "---\ntitle: \"A1\"\ntranslationKey: \"k1\"\n---\nContent\n") - writeSource(t, fs, filepath.Join("content", filepath.FromSlash("sect/simple.en.md")), "---\ntitle: \"A2\"\n---\nContent\n") - - s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) - - c.Assert(len(s.RegularPages()), qt.Equals, 2) - - home, _ := s.Info.Home() - c.Assert(home, qt.Not(qt.IsNil)) - c.Assert(home.TranslationKey(), qt.Equals, "home") - c.Assert(s.RegularPages()[0].TranslationKey(), qt.Equals, "page/k1") - p2 := s.RegularPages()[1] - - c.Assert(p2.TranslationKey(), qt.Equals, "page/sect/simple") - -} - -func TestChompBOM(t *testing.T) { - t.Parallel() - c := qt.New(t) - const utf8BOM = "\xef\xbb\xbf" - - cfg, fs := newTestCfg() - - writeSource(t, fs, filepath.Join("content", "simple.md"), utf8BOM+simplePage) - - s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) - - c.Assert(len(s.RegularPages()), qt.Equals, 1) - - p := s.RegularPages()[0] - - checkPageTitle(t, p, "Simple") -} - -func TestPageWithEmoji(t *testing.T) { - for _, enableEmoji := range []bool{true, false} { - v := viper.New() - v.Set("enableEmoji", enableEmoji) - - b := newTestSitesBuilder(t).WithViper(v) - - b.WithContent("page-emoji.md", `--- -title: "Hugo Smile" ---- -This is a :smile:. - - -Another :smile: This is :not: :an: :emoji:. - -O :christmas_tree: - -Write me an :e-mail: or :email:? - -Too many colons: :: ::: :::: :?: :!: :.: - -If you dislike this video, you can hit that :-1: button :stuck_out_tongue_winking_eye:, -but if you like it, hit :+1: and get subscribed! -`) - - b.CreateSites().Build(BuildCfg{}) - - if enableEmoji { - b.AssertFileContent("public/page-emoji/index.html", - "This is a 😄", - "Another 😄", - "This is :not: :an: :emoji:.", - "O 🎄", - "Write me an 📧 or ✉️?", - "Too many colons: :: ::: :::: :?: :!: :.:", - "you can hit that 👎 button 😜,", - "hit 👍 and get subscribed!", - ) - } else { - b.AssertFileContent("public/page-emoji/index.html", - "This is a :smile:", - "Another :smile:", - "This is :not: :an: :emoji:.", - "O :christmas_tree:", - "Write me an :e-mail: or :email:?", - "Too many colons: :: ::: :::: :?: :!: :.:", - "you can hit that :-1: button :stuck_out_tongue_winking_eye:,", - "hit :+1: and get subscribed!", - ) - } - - } - -} - -func TestPageHTMLContent(t *testing.T) { - b := newTestSitesBuilder(t) - b.WithSimpleConfigFile() - - frontmatter := `--- -title: "HTML Content" ---- -` - b.WithContent("regular.html", frontmatter+`

    Hugo

    `) - b.WithContent("noblackfridayforyou.html", frontmatter+`**Hugo!**`) - b.WithContent("manualsummary.html", frontmatter+` -

    This is summary

    - -

    This is the main content.

    `) - - b.Build(BuildCfg{}) - - b.AssertFileContent( - "public/regular/index.html", - "Single: HTML Content|Hello|en|RelPermalink: /regular/|", - "Summary: Hugo|Truncated: false") - - b.AssertFileContent( - "public/noblackfridayforyou/index.html", - "Permalink: http://example.com/noblackfridayforyou/|**Hugo!**|", - ) - - // https://github.com/gohugoio/hugo/issues/5723 - b.AssertFileContent( - "public/manualsummary/index.html", - "Single: HTML Content|Hello|en|RelPermalink: /manualsummary/|", - "Summary: \n

    This is summary

    \n|Truncated: true", - "|

    This is the main content.

    |", - ) - -} - -// https://github.com/gohugoio/hugo/issues/5381 -func TestPageManualSummary(t *testing.T) { - b := newTestSitesBuilder(t) - b.WithSimpleConfigFile() - - b.WithContent("page-md-shortcode.md", `--- -title: "Hugo" ---- -This is a {{< sc >}}. - -Content. -`) - - // https://github.com/gohugoio/hugo/issues/5464 - b.WithContent("page-md-only-shortcode.md", `--- -title: "Hugo" ---- -{{< sc >}} - -{{< sc >}} -`) - - b.WithContent("page-md-shortcode-same-line.md", `--- -title: "Hugo" ---- -This is a {{< sc >}}Same line. -`) - - b.WithContent("page-md-shortcode-same-line-after.md", `--- -title: "Hugo" ---- -Summary{{< 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:

    This is a a shortcode.

    :END", - "CONTENT:

    This is a a shortcode.

    \n\n

    Content.

    \n", - ) - - b.AssertFileContent("public/page-md-shortcode-same-line/index.html", - "SUMMARY:

    This is a a shortcode

    :END", - "CONTENT:

    This is a a shortcode

    \n\n

    Same line.

    \n", - ) - - b.AssertFileContent("public/page-md-shortcode-same-line-after/index.html", - "SUMMARY:

    Summary

    :END", - "CONTENT:

    Summary

    \n\na shortcode", - ) - - b.AssertFileContent("public/page-org-shortcode/index.html", - "SUMMARY:

    \nThis is a a shortcode.\n

    :END", - "CONTENT:

    \nThis is a a shortcode.\n

    \n

    \nContent.\t\n

    \n", - ) - b.AssertFileContent("public/page-org-variant1/index.html", - "SUMMARY:

    \nSummary.\n

    :END", - "CONTENT:

    \nSummary.\n

    \n

    \nContent.\t\n

    \n", - ) - - b.AssertFileContent("public/page-md-only-shortcode/index.html", - "SUMMARY:a shortcode:END", - "CONTENT:a shortcode\n\na shortcode\n", - ) -} - -// https://github.com/gohugoio/hugo/issues/5478 -func TestPageWithCommentedOutFrontMatter(t *testing.T) { - b := newTestSitesBuilder(t) - b.WithSimpleConfigFile() - - b.WithContent("page.md", ` -This is the content. -`) - - b.WithTemplatesAdded("layouts/_default/single.html", ` -Title: {{ .Title }} -Content:{{ .Content }} -`) - - b.CreateSites().Build(BuildCfg{}) - - b.AssertFileContent("public/page/index.html", - "Title: hello", - "Content:

    This is the content.

    ", - ) - -} - -// https://github.com/gohugoio/hugo/issues/5781 -func TestPageWithZeroFile(t *testing.T) { - newTestSitesBuilder(t).WithLogger(loggers.NewWarningLogger()).WithSimpleConfigFile(). - WithTemplatesAdded("index.html", "{{ .File.Filename }}{{ with .File }}{{ .Dir }}{{ end }}").Build(BuildCfg{}) -} - -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||") -} - -func TestShouldBuild(t *testing.T) { - 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{} - - var 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}, - - // 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}, - - // buildDrafts and draft - {true, true, false, true, past, future, false}, - {true, true, true, true, past, future, true}, - {true, true, true, true, past, future, true}, - } - - for _, ps := range publishSettings { - s := shouldBuild(ps.buildFuture, ps.buildExpired, ps.buildDrafts, ps.draft, - ps.publishDate, ps.expiryDate) - if s != ps.out { - t.Errorf("AssertShouldBuild unexpected output with params: %+v", ps) - } - } -} - -// "dot" in path: #1885 and #2110 -// disablePathToLower regression: #3374 -func TestPathIssues(t *testing.T) { - for _, disablePathToLower := range []bool{false, true} { - for _, uglyURLs := range []bool{false, true} { - disablePathToLower := disablePathToLower - uglyURLs := uglyURLs - t.Run(fmt.Sprintf("disablePathToLower=%t,uglyURLs=%t", disablePathToLower, uglyURLs), func(t *testing.T) { - t.Parallel() - cfg, fs := newTestCfg() - th := newTestHelper(cfg, fs, t) - c := qt.New(t) - - cfg.Set("permalinks", map[string]string{ - "post": ":section/:title", - }) - - cfg.Set("uglyURLs", uglyURLs) - cfg.Set("disablePathToLower", disablePathToLower) - cfg.Set("paginate", 1) - - 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 }}") - - 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{}) - - c.Assert(len(s.RegularPages()), qt.Equals, 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", `P1|URL: /post.html|Next: /post/page/2.html`) - th.assertFileContent("public/post/page/2.html", `P2|URL: /post/page/2.html|Next: /post/page/3.html`) - } else { - th.assertFileContent("public/post/page/1/index.html", `canonical" href="/post/"/`) - th.assertFileContent("public/post/index.html", `P1|URL: /post/|Next: /post/page/2/`) - th.assertFileContent("public/post/page/2/index.html", `P2|URL: /post/page/2/|Next: /post/page/3/`) - th.assertFileContent("public/tags/.net/index.html", `P1|URL: /tags/.net/|Next: /tags/.net/page/2/`) - - } - - p := s.RegularPages()[0] - if uglyURLs { - c.Assert(p.RelPermalink(), qt.Equals, "/post/test0.dot.html") - } else { - c.Assert(p.RelPermalink(), qt.Equals, "/post/test0.dot/") - } - - }) - } - } -} - -// https://github.com/gohugoio/hugo/issues/4675 -func TestWordCountAndSimilarVsSummary(t *testing.T) { - - t.Parallel() - c := qt.New(t) - - single := []string{"_default/single.html", ` -WordCount: {{ .WordCount }} -FuzzyWordCount: {{ .FuzzyWordCount }} -ReadingTime: {{ .ReadingTime }} -Len Plain: {{ len .Plain }} -Len PlainWords: {{ len .PlainWords }} -Truncated: {{ .Truncated }} -Len Summary: {{ len .Summary }} -Len Content: {{ len .Content }} - -SUMMARY:{{ .Summary }}:{{ len .Summary }}:END -`} - - b := newTestSitesBuilder(t) - b.WithSimpleConfigFile().WithTemplatesAdded(single...).WithContent("p1.md", fmt.Sprintf(`--- -title: p1 ---- - -%s - -`, strings.Repeat("word ", 510)), - - "p2.md", fmt.Sprintf(`--- -title: p2 ---- -This is a summary. - - - -%s - -`, strings.Repeat("word ", 310)), - "p3.md", fmt.Sprintf(`--- -title: p3 -isCJKLanguage: true ---- -Summary: In Chinese, 好 means good. - - - -%s - -`, strings.Repeat("好", 200)), - "p4.md", fmt.Sprintf(`--- -title: p4 -isCJKLanguage: false ---- -Summary: In Chinese, 好 means good. - - - -%s - -`, strings.Repeat("好", 200)), - - "p5.md", fmt.Sprintf(`--- -title: p4 -isCJKLanguage: true ---- -Summary: In Chinese, 好 means good. - -%s - -`, strings.Repeat("好", 200)), - "p6.md", fmt.Sprintf(`--- -title: p4 -isCJKLanguage: false ---- -Summary: In Chinese, 好 means good. - -%s - -`, strings.Repeat("好", 200)), - ) - - b.CreateSites().Build(BuildCfg{}) - - c.Assert(len(b.H.Sites), qt.Equals, 1) - c.Assert(len(b.H.Sites[0].RegularPages()), qt.Equals, 6) - - b.AssertFileContent("public/p1/index.html", "WordCount: 510\nFuzzyWordCount: 600\nReadingTime: 3\nLen Plain: 2550\nLen PlainWords: 510\nTruncated: false\nLen Summary: 2549\nLen Content: 2557") - - b.AssertFileContent("public/p2/index.html", "WordCount: 314\nFuzzyWordCount: 400\nReadingTime: 2\nLen Plain: 1569\nLen PlainWords: 314\nTruncated: true\nLen Summary: 25\nLen Content: 1583") - - b.AssertFileContent("public/p3/index.html", "WordCount: 206\nFuzzyWordCount: 300\nReadingTime: 1\nLen Plain: 638\nLen PlainWords: 7\nTruncated: true\nLen Summary: 43\nLen Content: 652") - b.AssertFileContent("public/p4/index.html", "WordCount: 7\nFuzzyWordCount: 100\nReadingTime: 1\nLen Plain: 638\nLen PlainWords: 7\nTruncated: true\nLen Summary: 43\nLen Content: 652") - b.AssertFileContent("public/p5/index.html", "WordCount: 206\nFuzzyWordCount: 300\nReadingTime: 1\nLen Plain: 638\nLen PlainWords: 7\nTruncated: true\nLen Summary: 229\nLen Content: 653") - b.AssertFileContent("public/p6/index.html", "WordCount: 7\nFuzzyWordCount: 100\nReadingTime: 1\nLen Plain: 638\nLen PlainWords: 7\nTruncated: false\nLen Summary: 637\nLen Content: 653") - -} - -func TestScratchSite(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") -} - -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 page string: {{ $withStringParam.Param "author.name" }}| -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") - b.AssertFileContent("public/index.html", "Author page string: |") - b.AssertFileContent("public/index.html", "Author site config: Kurt Vonnegut") - -} diff --git a/hugolib/page_unwrap.go b/hugolib/page_unwrap.go deleted file mode 100644 index eda6636d1..000000000 --- a/hugolib/page_unwrap.go +++ /dev/null @@ -1,50 +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 ( - "github.com/pkg/errors" - - "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 interface{}) (page.Page, error) { - switch v := in.(type) { - case *pageState: - return v, nil - case pageWrapper: - return v.page(), nil - case page.Page: - return v, nil - case nil: - return nil, nil - default: - return nil, errors.Errorf("unwrapPage: %T not supported", in) - } -} - -func mustUnwrapPage(in interface{}) 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 20888166a..000000000 --- a/hugolib/page_unwrap_test.go +++ /dev/null @@ -1,37 +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) -} - -func mustUnwrap(v interface{}) 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 d4d89bd85..000000000 --- a/hugolib/pagebundler_test.go +++ /dev/null @@ -1,1294 +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 ( - "os" - "path" - "regexp" - "strings" - "testing" - - "github.com/gohugoio/hugo/helpers" - - "github.com/gohugoio/hugo/hugofs" - - "github.com/gohugoio/hugo/common/loggers" - "github.com/gohugoio/hugo/resources/page" - - "io" - - "github.com/gohugoio/hugo/htesting" - - "github.com/gohugoio/hugo/media" - - "path/filepath" - - "fmt" - - "github.com/gohugoio/hugo/deps" - "github.com/spf13/viper" - - qt "github.com/frankban/quicktest" -) - -func TestPageBundlerSiteRegular(t *testing.T) { - c := qt.New(t) - baseBaseURL := "https://example.com" - - for _, baseURLPath := range []string{"", "/hugo"} { - for _, canonify := range []bool{false, true} { - for _, ugly := range []bool{false, true} { - baseURLPathId := baseURLPath - if baseURLPathId == "" { - baseURLPathId = "NONE" - } - ugly := ugly - canonify := canonify - c.Run(fmt.Sprintf("ugly=%t,canonify=%t,path=%s", ugly, canonify, baseURLPathId), - func(c *qt.C) { - c.Parallel() - baseURL := baseBaseURL + baseURLPath - relURLBase := baseURLPath - if canonify { - relURLBase = "" - } - fs, cfg := newTestBundleSources(t) - cfg.Set("baseURL", baseURL) - cfg.Set("canonifyURLs", canonify) - - cfg.Set("permalinks", map[string]string{ - "a": ":sections/:filename", - "b": ":year/:slug/", - "c": ":sections/:slug", - "/": ":filename/", - }) - - cfg.Set("outputFormats", map[string]interface{}{ - "CUSTOMO": map[string]interface{}{ - "mediaType": media.HTMLType, - "baseName": "cindex", - "path": "cpath", - "permalinkable": true, - }, - }) - - cfg.Set("outputs", map[string]interface{}{ - "home": []string{"HTML", "CUSTOMO"}, - "page": []string{"HTML", "CUSTOMO"}, - "section": []string{"HTML", "CUSTOMO"}, - }) - - cfg.Set("uglyURLs", ugly) - - b := newTestSitesBuilderFromDepsCfg(t, deps.DepsCfg{Logger: loggers.NewErrorLogger(), Fs: fs, Cfg: cfg}).WithNothingAdded() - - b.Build(BuildCfg{}) - - s := b.H.Sites[0] - - c.Assert(len(s.RegularPages()), qt.Equals, 8) - - singlePage := s.getPage(page.KindPage, "a/1.md") - c.Assert(singlePage.BundleType(), qt.Equals, "") - - c.Assert(singlePage, qt.Not(qt.IsNil)) - c.Assert(s.getPage("page", "a/1"), qt.Equals, singlePage) - c.Assert(s.getPage("page", "1"), qt.Equals, singlePage) - - c.Assert(content(singlePage), qt.Contains, "TheContent") - - relFilename := func(basePath, outBase string) (string, string) { - rel := basePath - if ugly { - rel = strings.TrimSuffix(basePath, "/") + ".html" - } - - var filename string - if !ugly { - filename = path.Join(basePath, outBase) - } else { - filename = rel - } - - rel = fmt.Sprintf("%s%s", relURLBase, rel) - - return rel, filename - } - - // Check both output formats - rel, filename := relFilename("/a/1/", "index.html") - b.AssertFileContent(filepath.Join("/work/public", filename), - "TheContent", - "Single RelPermalink: "+rel, - ) - - rel, filename = relFilename("/cpath/a/1/", "cindex.html") - - b.AssertFileContent(filepath.Join("/work/public", filename), - "TheContent", - "Single RelPermalink: "+rel, - ) - - b.AssertFileContent(filepath.FromSlash("/work/public/images/hugo-logo.png"), "content") - - // This should be just copied to destination. - b.AssertFileContent(filepath.FromSlash("/work/public/assets/pic1.png"), "content") - - leafBundle1 := s.getPage(page.KindPage, "b/my-bundle/index.md") - c.Assert(leafBundle1, qt.Not(qt.IsNil)) - c.Assert(leafBundle1.BundleType(), qt.Equals, "leaf") - c.Assert(leafBundle1.Section(), qt.Equals, "b") - sectionB := s.getPage(page.KindSection, "b") - c.Assert(sectionB, qt.Not(qt.IsNil)) - home, _ := s.Info.Home() - c.Assert(home.BundleType(), qt.Equals, "branch") - - // This is a root bundle and should live in the "home section" - // See https://github.com/gohugoio/hugo/issues/4332 - rootBundle := s.getPage(page.KindPage, "root") - c.Assert(rootBundle, qt.Not(qt.IsNil)) - c.Assert(rootBundle.Parent().IsHome(), qt.Equals, true) - if !ugly { - b.AssertFileContent(filepath.FromSlash("/work/public/root/index.html"), "Single RelPermalink: "+relURLBase+"/root/") - b.AssertFileContent(filepath.FromSlash("/work/public/cpath/root/cindex.html"), "Single RelPermalink: "+relURLBase+"/cpath/root/") - } - - leafBundle2 := s.getPage(page.KindPage, "a/b/index.md") - c.Assert(leafBundle2, qt.Not(qt.IsNil)) - unicodeBundle := s.getPage(page.KindPage, "c/bundle/index.md") - c.Assert(unicodeBundle, qt.Not(qt.IsNil)) - - pageResources := leafBundle1.Resources().ByType(pageResourceType) - c.Assert(len(pageResources), qt.Equals, 2) - firstPage := pageResources[0].(page.Page) - secondPage := pageResources[1].(page.Page) - - c.Assert(firstPage.File().Filename(), qt.Equals, filepath.FromSlash("/work/base/b/my-bundle/1.md")) - c.Assert(content(firstPage), qt.Contains, "TheContent") - c.Assert(len(leafBundle1.Resources()), qt.Equals, 6) - - // Verify shortcode in bundled page - c.Assert(content(secondPage), qt.Contains, filepath.FromSlash("MyShort in b/my-bundle/2.md")) - - // https://github.com/gohugoio/hugo/issues/4582 - c.Assert(firstPage.Parent(), qt.Equals, leafBundle1) - c.Assert(secondPage.Parent(), qt.Equals, leafBundle1) - - c.Assert(pageResources.GetMatch("1*"), qt.Equals, firstPage) - c.Assert(pageResources.GetMatch("2*"), qt.Equals, secondPage) - c.Assert(pageResources.GetMatch("doesnotexist*"), qt.IsNil) - - imageResources := leafBundle1.Resources().ByType("image") - c.Assert(len(imageResources), qt.Equals, 3) - - c.Assert(leafBundle1.OutputFormats().Get("CUSTOMO"), qt.Not(qt.IsNil)) - - relPermalinker := func(s string) string { - return fmt.Sprintf(s, relURLBase) - } - - permalinker := func(s string) string { - return fmt.Sprintf(s, baseURL) - } - - if ugly { - b.AssertFileContent("/work/public/2017/pageslug.html", - relPermalinker("Single RelPermalink: %s/2017/pageslug.html"), - permalinker("Single Permalink: %s/2017/pageslug.html"), - relPermalinker("Sunset RelPermalink: %s/2017/pageslug/sunset1.jpg"), - permalinker("Sunset Permalink: %s/2017/pageslug/sunset1.jpg")) - } else { - b.AssertFileContent("/work/public/2017/pageslug/index.html", - relPermalinker("Sunset RelPermalink: %s/2017/pageslug/sunset1.jpg"), - permalinker("Sunset Permalink: %s/2017/pageslug/sunset1.jpg")) - - b.AssertFileContent("/work/public/cpath/2017/pageslug/cindex.html", - relPermalinker("Single RelPermalink: %s/cpath/2017/pageslug/"), - relPermalinker("Short Sunset RelPermalink: %s/cpath/2017/pageslug/sunset2.jpg"), - relPermalinker("Sunset RelPermalink: %s/cpath/2017/pageslug/sunset1.jpg"), - permalinker("Sunset Permalink: %s/cpath/2017/pageslug/sunset1.jpg"), - ) - } - - b.AssertFileContent(filepath.FromSlash("/work/public/2017/pageslug/c/logo.png"), "content") - b.AssertFileContent(filepath.FromSlash("/work/public/cpath/2017/pageslug/c/logo.png"), "content") - c.Assert(b.CheckExists("/work/public/cpath/cpath/2017/pageslug/c/logo.png"), qt.Equals, false) - - // Custom media type defined in site config. - c.Assert(len(leafBundle1.Resources().ByType("bepsays")), qt.Equals, 1) - - if ugly { - b.AssertFileContent(filepath.FromSlash("/work/public/2017/pageslug.html"), - "TheContent", - relPermalinker("Sunset RelPermalink: %s/2017/pageslug/sunset1.jpg"), - permalinker("Sunset Permalink: %s/2017/pageslug/sunset1.jpg"), - "Thumb Width: 123", - "Thumb Name: my-sunset-1", - relPermalinker("Short Sunset RelPermalink: %s/2017/pageslug/sunset2.jpg"), - "Short Thumb Width: 56", - "1: Image Title: Sunset Galore 1", - "1: Image Params: map[myparam:My Sunny Param]", - relPermalinker("1: Image RelPermalink: %s/2017/pageslug/sunset1.jpg"), - "2: Image Title: Sunset Galore 2", - "2: Image Params: map[myparam:My Sunny Param]", - "1: Image myParam: Lower: My Sunny Param Caps: My Sunny Param", - "0: Page Title: Bundle Galore", - ) - - // https://github.com/gohugoio/hugo/issues/5882 - b.AssertFileContent( - filepath.FromSlash("/work/public/2017/pageslug.html"), "0: Page RelPermalink: |") - - b.AssertFileContent(filepath.FromSlash("/work/public/cpath/2017/pageslug.html"), "TheContent") - - // 은행 - b.AssertFileContent(filepath.FromSlash("/work/public/c/은행/logo-은행.png"), "은행 PNG") - - } else { - b.AssertFileContent(filepath.FromSlash("/work/public/2017/pageslug/index.html"), "TheContent") - b.AssertFileContent(filepath.FromSlash("/work/public/cpath/2017/pageslug/cindex.html"), "TheContent") - b.AssertFileContent(filepath.FromSlash("/work/public/2017/pageslug/index.html"), "Single Title") - b.AssertFileContent(filepath.FromSlash("/work/public/root/index.html"), "Single Title") - - } - - }) - } - } - } - -} - -func TestPageBundlerSiteMultilingual(t *testing.T) { - t.Parallel() - - for _, ugly := range []bool{false, true} { - ugly := ugly - t.Run(fmt.Sprintf("ugly=%t", ugly), - func(t *testing.T) { - t.Parallel() - c := qt.New(t) - fs, cfg := newTestBundleSourcesMultilingual(t) - cfg.Set("uglyURLs", ugly) - - b := newTestSitesBuilderFromDepsCfg(t, deps.DepsCfg{Fs: fs, Cfg: cfg}).WithNothingAdded() - b.Build(BuildCfg{}) - - sites := b.H - - c.Assert(len(sites.Sites), qt.Equals, 2) - - s := sites.Sites[0] - - c.Assert(len(s.RegularPages()), qt.Equals, 8) - c.Assert(len(s.Pages()), qt.Equals, 16) - //dumpPages(s.AllPages()...) - c.Assert(len(s.AllPages()), qt.Equals, 31) - - bundleWithSubPath := s.getPage(page.KindPage, "lb/index") - c.Assert(bundleWithSubPath, qt.Not(qt.IsNil)) - - // See https://github.com/gohugoio/hugo/issues/4312 - // Before that issue: - // A bundle in a/b/index.en.md - // a/b/index.en.md => OK - // a/b/index => OK - // index.en.md => ambigous, but OK. - // With bundles, the file name has little meaning, the folder it lives in does. So this should also work: - // a/b - // and probably also just b (aka "my-bundle") - // These may also be translated, so we also need to test that. - // "bf", "my-bf-bundle", "index.md + nn - bfBundle := s.getPage(page.KindPage, "bf/my-bf-bundle/index") - c.Assert(bfBundle, qt.Not(qt.IsNil)) - c.Assert(bfBundle.Language().Lang, qt.Equals, "en") - c.Assert(s.getPage(page.KindPage, "bf/my-bf-bundle/index.md"), qt.Equals, bfBundle) - c.Assert(s.getPage(page.KindPage, "bf/my-bf-bundle"), qt.Equals, bfBundle) - c.Assert(s.getPage(page.KindPage, "my-bf-bundle"), qt.Equals, bfBundle) - - nnSite := sites.Sites[1] - c.Assert(len(nnSite.RegularPages()), qt.Equals, 7) - - bfBundleNN := nnSite.getPage(page.KindPage, "bf/my-bf-bundle/index") - c.Assert(bfBundleNN, qt.Not(qt.IsNil)) - c.Assert(bfBundleNN.Language().Lang, qt.Equals, "nn") - c.Assert(nnSite.getPage(page.KindPage, "bf/my-bf-bundle/index.nn.md"), qt.Equals, bfBundleNN) - c.Assert(nnSite.getPage(page.KindPage, "bf/my-bf-bundle"), qt.Equals, bfBundleNN) - c.Assert(nnSite.getPage(page.KindPage, "my-bf-bundle"), qt.Equals, bfBundleNN) - - // See https://github.com/gohugoio/hugo/issues/4295 - // Every resource should have its Name prefixed with its base folder. - cBundleResources := bundleWithSubPath.Resources().Match("c/**") - c.Assert(len(cBundleResources), qt.Equals, 4) - bundlePage := bundleWithSubPath.Resources().GetMatch("c/page*") - c.Assert(bundlePage, qt.Not(qt.IsNil)) - - bcBundleNN, _ := nnSite.getPageNew(nil, "bc") - c.Assert(bcBundleNN, qt.Not(qt.IsNil)) - bcBundleEN, _ := s.getPageNew(nil, "bc") - c.Assert(bcBundleNN.Language().Lang, qt.Equals, "nn") - c.Assert(bcBundleEN.Language().Lang, qt.Equals, "en") - c.Assert(len(bcBundleNN.Resources()), qt.Equals, 3) - c.Assert(len(bcBundleEN.Resources()), qt.Equals, 3) - b.AssertFileContent("public/en/bc/data1.json", "data1") - b.AssertFileContent("public/en/bc/data2.json", "data2") - b.AssertFileContent("public/en/bc/logo-bc.png", "logo") - b.AssertFileContent("public/nn/bc/data1.nn.json", "data1.nn") - b.AssertFileContent("public/nn/bc/data2.json", "data2") - b.AssertFileContent("public/nn/bc/logo-bc.png", "logo") - - }) - } -} - -func TestMultilingualDisableDefaultLanguage(t *testing.T) { - t.Parallel() - - c := qt.New(t) - _, cfg := newTestBundleSourcesMultilingual(t) - - cfg.Set("disableLanguages", []string{"en"}) - - err := loadDefaultSettingsFor(cfg) - c.Assert(err, qt.IsNil) - err = loadLanguageSettings(cfg, nil) - c.Assert(err, qt.Not(qt.IsNil)) - c.Assert(err.Error(), qt.Contains, "cannot disable default language") -} - -func TestMultilingualDisableLanguage(t *testing.T) { - t.Parallel() - - c := qt.New(t) - fs, cfg := newTestBundleSourcesMultilingual(t) - cfg.Set("disableLanguages", []string{"nn"}) - - b := newTestSitesBuilderFromDepsCfg(t, deps.DepsCfg{Fs: fs, Cfg: cfg}).WithNothingAdded() - b.Build(BuildCfg{}) - sites := b.H - - c.Assert(len(sites.Sites), qt.Equals, 1) - - s := sites.Sites[0] - - c.Assert(len(s.RegularPages()), qt.Equals, 8) - c.Assert(len(s.Pages()), qt.Equals, 16) - // No nn pages - c.Assert(len(s.AllPages()), qt.Equals, 16) - for _, p := range s.rawAllPages { - c.Assert(p.Language().Lang != "nn", qt.Equals, true) - } - for _, p := range s.AllPages() { - c.Assert(p.Language().Lang != "nn", qt.Equals, true) - } - -} - -func TestPageBundlerSiteWitSymbolicLinksInContent(t *testing.T) { - skipSymlink(t) - - wd, _ := os.Getwd() - defer func() { - os.Chdir(wd) - }() - - c := qt.New(t) - // We need to use the OS fs for this. - cfg := viper.New() - fs := hugofs.NewFrom(hugofs.Os, cfg) - - workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugosym") - c.Assert(err, qt.IsNil) - - contentDirName := "content" - - contentDir := filepath.Join(workDir, contentDirName) - c.Assert(os.MkdirAll(filepath.Join(contentDir, "a"), 0777), qt.IsNil) - - for i := 1; i <= 3; i++ { - c.Assert(os.MkdirAll(filepath.Join(workDir, fmt.Sprintf("symcontent%d", i)), 0777), qt.IsNil) - } - - c.Assert(os.MkdirAll(filepath.Join(workDir, "symcontent2", "a1"), 0777), qt.IsNil) - - // Symlinked sections inside content. - os.Chdir(contentDir) - for i := 1; i <= 3; i++ { - c.Assert(os.Symlink(filepath.FromSlash(fmt.Sprintf(("../symcontent%d"), i)), fmt.Sprintf("symbolic%d", i)), qt.IsNil) - } - - c.Assert(os.Chdir(filepath.Join(contentDir, "a")), qt.IsNil) - - // Create a symlink to one single content file - c.Assert(os.Symlink(filepath.FromSlash("../../symcontent2/a1/page.md"), "page_s.md"), qt.IsNil) - - c.Assert(os.Chdir(filepath.FromSlash("../../symcontent3")), qt.IsNil) - - // Create a circular symlink. Will print some warnings. - c.Assert(os.Symlink(filepath.Join("..", contentDirName), filepath.FromSlash("circus")), qt.IsNil) - - c.Assert(os.Chdir(workDir), qt.IsNil) - - defer clean() - - cfg.Set("workingDir", workDir) - cfg.Set("contentDir", contentDirName) - cfg.Set("baseURL", "https://example.com") - - layout := `{{ .Title }}|{{ .Content }}` - pageContent := `--- -slug: %s -date: 2017-10-09 ---- - -TheContent. -` - - b := newTestSitesBuilderFromDepsCfg(t, deps.DepsCfg{ - Fs: fs, - Cfg: cfg, - }) - - b.WithTemplates( - "_default/single.html", layout, - "_default/list.html", layout, - ) - - b.WithContent( - "a/regular.md", fmt.Sprintf(pageContent, "a1"), - ) - - b.WithSourceFile( - "symcontent1/s1.md", fmt.Sprintf(pageContent, "s1"), - "symcontent1/s2.md", fmt.Sprintf(pageContent, "s2"), - // Regular files inside symlinked folder. - "symcontent1/s1.md", fmt.Sprintf(pageContent, "s1"), - "symcontent1/s2.md", fmt.Sprintf(pageContent, "s2"), - - // A bundle - "symcontent2/a1/index.md", fmt.Sprintf(pageContent, ""), - "symcontent2/a1/page.md", fmt.Sprintf(pageContent, "page"), - "symcontent2/a1/logo.png", "image", - - // Assets - "symcontent3/s1.png", "image", - "symcontent3/s2.png", "image", - ) - - b.Build(BuildCfg{}) - s := b.H.Sites[0] - - c.Assert(len(s.RegularPages()), qt.Equals, 7) - a1Bundle := s.getPage(page.KindPage, "symbolic2/a1/index.md") - c.Assert(a1Bundle, qt.Not(qt.IsNil)) - c.Assert(len(a1Bundle.Resources()), qt.Equals, 2) - c.Assert(len(a1Bundle.Resources().ByType(pageResourceType)), qt.Equals, 1) - - b.AssertFileContent(filepath.FromSlash(workDir+"/public/a/page/index.html"), "TheContent") - b.AssertFileContent(filepath.FromSlash(workDir+"/public/symbolic1/s1/index.html"), "TheContent") - b.AssertFileContent(filepath.FromSlash(workDir+"/public/symbolic2/a1/index.html"), "TheContent") - -} - -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") - - 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, Cfg: cfg}, BuildCfg{}) - - c.Assert(len(s.RegularPages()), qt.Equals, 1) - c.Assert(len(s.headlessPages), qt.Equals, 1) - - regular := s.getPage(page.KindPage, "a/index") - c.Assert(regular.RelPermalink(), qt.Equals, "/s1/") - - headless := s.getPage(page.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) - c.Assert(len(headlessResources.Match("l*")), 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.Cfg, s.Fs, t) - - th.assertFileContent(filepath.FromSlash(workDir+"/public/s1/index.html"), "TheContent") - th.assertFileContent(filepath.FromSlash(workDir+"/public/s1/l1.png"), "PNG") - - th.assertFileNotExist(workDir + "/public/s2/index.html") - // But the bundled resources needs to be published - th.assertFileContent(filepath.FromSlash(workDir+"/public/s2/l1.png"), "PNG") - -} - -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/nn/mybundle/forms.yaml", "forms en") - 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:

    Section content.

    ") - b.AssertFileContent("public/section-not-bundle/single/index.html", "Section Single", "|

    Single content.

    ") - -} - -func newTestBundleSources(t *testing.T) (*hugofs.Fs, *viper.Viper) { - cfg, fs := newTestCfgBasic() - c := qt.New(t) - - workDir := "/work" - cfg.Set("workingDir", workDir) - cfg.Set("contentDir", "base") - cfg.Set("baseURL", "https://example.com") - cfg.Set("mediaTypes", map[string]interface{}{ - "text/bepsays": map[string]interface{}{ - "suffixes": []string{"bep"}, - }, - }) - - pageContent := `--- -title: "Bundle Galore" -slug: pageslug -date: 2017-10-09 ---- - -TheContent. -` - - pageContentShortcode := `--- -title: "Bundle Galore" -slug: pageslug -date: 2017-10-09 ---- - -TheContent. - -{{< myShort >}} -` - - pageWithImageShortcodeAndResourceMetadataContent := `--- -title: "Bundle Galore" -slug: pageslug -date: 2017-10-09 -resources: -- src: "*.jpg" - name: "my-sunset-:counter" - title: "Sunset Galore :counter" - params: - myParam: "My Sunny Param" ---- - -TheContent. - -{{< myShort >}} -` - - pageContentNoSlug := `--- -title: "Bundle Galore #2" -date: 2017-10-09 ---- - -TheContent. -` - - singleLayout := ` -Single Title: {{ .Title }} -Single RelPermalink: {{ .RelPermalink }} -Single Permalink: {{ .Permalink }} -Content: {{ .Content }} -{{ $sunset := .Resources.GetMatch "my-sunset-1*" }} -{{ with $sunset }} -Sunset RelPermalink: {{ .RelPermalink }} -Sunset Permalink: {{ .Permalink }} -{{ $thumb := .Fill "123x123" }} -Thumb Width: {{ $thumb.Width }} -Thumb Name: {{ $thumb.Name }} -Thumb Title: {{ $thumb.Title }} -Thumb RelPermalink: {{ $thumb.RelPermalink }} -{{ end }} -{{ $types := slice "image" "page" }} -{{ range $types }} -{{ $typeTitle := . | title }} -{{ range $i, $e := $.Resources.ByType . }} -{{ $i }}: {{ $typeTitle }} Title: {{ .Title }} -{{ $i }}: {{ $typeTitle }} Name: {{ .Name }} -{{ $i }}: {{ $typeTitle }} RelPermalink: {{ .RelPermalink }}| -{{ $i }}: {{ $typeTitle }} Params: {{ printf "%v" .Params }} -{{ $i }}: {{ $typeTitle }} myParam: Lower: {{ .Params.myparam }} Caps: {{ .Params.MYPARAM }} -{{ end }} -{{ end }} -` - - myShort := ` -MyShort in {{ .Page.File.Path }}: -{{ $sunset := .Page.Resources.GetMatch "my-sunset-2*" }} -{{ with $sunset }} -Short Sunset RelPermalink: {{ .RelPermalink }} -{{ $thumb := .Fill "56x56" }} -Short Thumb Width: {{ $thumb.Width }} -{{ end }} -` - - listLayout := `{{ .Title }}|{{ .Content }}` - - writeSource(t, fs, filepath.Join(workDir, "layouts", "_default", "single.html"), singleLayout) - writeSource(t, fs, filepath.Join(workDir, "layouts", "_default", "list.html"), listLayout) - writeSource(t, fs, filepath.Join(workDir, "layouts", "shortcodes", "myShort.html"), myShort) - - writeSource(t, fs, filepath.Join(workDir, "base", "_index.md"), pageContent) - writeSource(t, fs, filepath.Join(workDir, "base", "_1.md"), pageContent) - writeSource(t, fs, filepath.Join(workDir, "base", "_1.png"), pageContent) - - writeSource(t, fs, filepath.Join(workDir, "base", "images", "hugo-logo.png"), "content") - writeSource(t, fs, filepath.Join(workDir, "base", "a", "2.md"), pageContent) - writeSource(t, fs, filepath.Join(workDir, "base", "a", "1.md"), pageContent) - - writeSource(t, fs, filepath.Join(workDir, "base", "a", "b", "index.md"), pageContentNoSlug) - writeSource(t, fs, filepath.Join(workDir, "base", "a", "b", "ab1.md"), pageContentNoSlug) - - // Mostly plain static assets in a folder with a page in a sub folder thrown in. - writeSource(t, fs, filepath.Join(workDir, "base", "assets", "pic1.png"), "content") - writeSource(t, fs, filepath.Join(workDir, "base", "assets", "pic2.png"), "content") - writeSource(t, fs, filepath.Join(workDir, "base", "assets", "pages", "mypage.md"), pageContent) - - // Bundle - writeSource(t, fs, filepath.Join(workDir, "base", "b", "my-bundle", "index.md"), pageWithImageShortcodeAndResourceMetadataContent) - writeSource(t, fs, filepath.Join(workDir, "base", "b", "my-bundle", "1.md"), pageContent) - writeSource(t, fs, filepath.Join(workDir, "base", "b", "my-bundle", "2.md"), pageContentShortcode) - writeSource(t, fs, filepath.Join(workDir, "base", "b", "my-bundle", "custom-mime.bep"), "bepsays") - writeSource(t, fs, filepath.Join(workDir, "base", "b", "my-bundle", "c", "logo.png"), "content") - - // Bundle with 은행 slug - // See https://github.com/gohugoio/hugo/issues/4241 - writeSource(t, fs, filepath.Join(workDir, "base", "c", "bundle", "index.md"), `--- -title: "은행 은행" -slug: 은행 -date: 2017-10-09 ---- - -Content for 은행. -`) - - // Bundle in root - writeSource(t, fs, filepath.Join(workDir, "base", "root", "index.md"), pageWithImageShortcodeAndResourceMetadataContent) - writeSource(t, fs, filepath.Join(workDir, "base", "root", "1.md"), pageContent) - writeSource(t, fs, filepath.Join(workDir, "base", "root", "c", "logo.png"), "content") - - writeSource(t, fs, filepath.Join(workDir, "base", "c", "bundle", "logo-은행.png"), "은행 PNG") - - // Write a real image into one of the bundle above. - src, err := os.Open("testdata/sunset.jpg") - c.Assert(err, qt.IsNil) - - // We need 2 to test https://github.com/gohugoio/hugo/issues/4202 - out, err := fs.Source.Create(filepath.Join(workDir, "base", "b", "my-bundle", "sunset1.jpg")) - c.Assert(err, qt.IsNil) - out2, err := fs.Source.Create(filepath.Join(workDir, "base", "b", "my-bundle", "sunset2.jpg")) - c.Assert(err, qt.IsNil) - - _, err = io.Copy(out, src) - c.Assert(err, qt.IsNil) - out.Close() - src.Seek(0, 0) - _, err = io.Copy(out2, src) - out2.Close() - src.Close() - c.Assert(err, qt.IsNil) - - return fs, cfg - -} - -func newTestBundleSourcesMultilingual(t *testing.T) (*hugofs.Fs, *viper.Viper) { - cfg, fs := newTestCfgBasic() - - workDir := "/work" - cfg.Set("workingDir", workDir) - cfg.Set("contentDir", "base") - cfg.Set("baseURL", "https://example.com") - cfg.Set("defaultContentLanguage", "en") - - langConfig := map[string]interface{}{ - "en": map[string]interface{}{ - "weight": 1, - "languageName": "English", - }, - "nn": map[string]interface{}{ - "weight": 2, - "languageName": "Nynorsk", - }, - } - - cfg.Set("languages", langConfig) - - pageContent := `--- -slug: pageslug -date: 2017-10-09 ---- - -TheContent. -` - - layout := `{{ .Title }}|{{ .Content }}|Lang: {{ .Site.Language.Lang }}` - - writeSource(t, fs, filepath.Join(workDir, "layouts", "_default", "single.html"), layout) - writeSource(t, fs, filepath.Join(workDir, "layouts", "_default", "list.html"), layout) - - writeSource(t, fs, filepath.Join(workDir, "base", "1s", "mypage.md"), pageContent) - writeSource(t, fs, filepath.Join(workDir, "base", "1s", "mypage.nn.md"), pageContent) - writeSource(t, fs, filepath.Join(workDir, "base", "1s", "mylogo.png"), "content") - - writeSource(t, fs, filepath.Join(workDir, "base", "bb", "_index.md"), pageContent) - writeSource(t, fs, filepath.Join(workDir, "base", "bb", "_index.nn.md"), pageContent) - writeSource(t, fs, filepath.Join(workDir, "base", "bb", "en.md"), pageContent) - writeSource(t, fs, filepath.Join(workDir, "base", "bb", "_1.md"), pageContent) - writeSource(t, fs, filepath.Join(workDir, "base", "bb", "_1.nn.md"), pageContent) - writeSource(t, fs, filepath.Join(workDir, "base", "bb", "a.png"), "content") - writeSource(t, fs, filepath.Join(workDir, "base", "bb", "b.png"), "content") - writeSource(t, fs, filepath.Join(workDir, "base", "bb", "b.nn.png"), "content") - writeSource(t, fs, filepath.Join(workDir, "base", "bb", "c.nn.png"), "content") - writeSource(t, fs, filepath.Join(workDir, "base", "bb", "b", "d.nn.png"), "content") - - writeSource(t, fs, filepath.Join(workDir, "base", "bc", "_index.md"), pageContent) - writeSource(t, fs, filepath.Join(workDir, "base", "bc", "_index.nn.md"), pageContent) - writeSource(t, fs, filepath.Join(workDir, "base", "bc", "page.md"), pageContent) - writeSource(t, fs, filepath.Join(workDir, "base", "bc", "logo-bc.png"), "logo") - writeSource(t, fs, filepath.Join(workDir, "base", "bc", "page.nn.md"), pageContent) - writeSource(t, fs, filepath.Join(workDir, "base", "bc", "data1.json"), "data1") - writeSource(t, fs, filepath.Join(workDir, "base", "bc", "data2.json"), "data2") - writeSource(t, fs, filepath.Join(workDir, "base", "bc", "data1.nn.json"), "data1.nn") - - writeSource(t, fs, filepath.Join(workDir, "base", "bd", "index.md"), pageContent) - writeSource(t, fs, filepath.Join(workDir, "base", "bd", "page.md"), pageContent) - writeSource(t, fs, filepath.Join(workDir, "base", "bd", "page.nn.md"), pageContent) - - writeSource(t, fs, filepath.Join(workDir, "base", "be", "_index.md"), pageContent) - writeSource(t, fs, filepath.Join(workDir, "base", "be", "page.md"), pageContent) - writeSource(t, fs, filepath.Join(workDir, "base", "be", "page.nn.md"), pageContent) - - // Bundle leaf, multilingual - writeSource(t, fs, filepath.Join(workDir, "base", "lb", "index.md"), pageContent) - writeSource(t, fs, filepath.Join(workDir, "base", "lb", "index.nn.md"), pageContent) - writeSource(t, fs, filepath.Join(workDir, "base", "lb", "1.md"), pageContent) - writeSource(t, fs, filepath.Join(workDir, "base", "lb", "2.md"), pageContent) - writeSource(t, fs, filepath.Join(workDir, "base", "lb", "2.nn.md"), pageContent) - writeSource(t, fs, filepath.Join(workDir, "base", "lb", "c", "page.md"), pageContent) - writeSource(t, fs, filepath.Join(workDir, "base", "lb", "c", "logo.png"), "content") - writeSource(t, fs, filepath.Join(workDir, "base", "lb", "c", "logo.nn.png"), "content") - writeSource(t, fs, filepath.Join(workDir, "base", "lb", "c", "one.png"), "content") - writeSource(t, fs, filepath.Join(workDir, "base", "lb", "c", "d", "deep.png"), "content") - - //Translated bundle in some sensible sub path. - writeSource(t, fs, filepath.Join(workDir, "base", "bf", "my-bf-bundle", "index.md"), pageContent) - writeSource(t, fs, filepath.Join(workDir, "base", "bf", "my-bf-bundle", "index.nn.md"), pageContent) - writeSource(t, fs, filepath.Join(workDir, "base", "bf", "my-bf-bundle", "page.md"), pageContent) - - return fs, cfg -} - -// https://github.com/gohugoio/hugo/issues/5858 -func TestBundledResourcesWhenMultipleOutputFormats(t *testing.T) { - t.Parallel() - - b := newTestSitesBuilder(t).Running().WithConfigFile("toml", ` -baseURL = "https://example.org" -[outputs] - # This looks odd, but it triggers the behaviour in #5858 - # The total output formats list gets sorted, so CSS before HTML. - home = [ "CSS" ] - -`) - b.WithContent("mybundle/index.md", ` ---- -title: Page -date: 2017-01-15 ---- -`, - "mybundle/data.json", "MyData", - ) - - b.CreateSites().Build(BuildCfg{}) - - b.AssertFileContent("public/mybundle/data.json", "MyData") - - // Change the bundled JSON file and make sure it gets republished. - b.EditFiles("content/mybundle/data.json", "My changed data") - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/mybundle/data.json", "My changed data") - -} - -// 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) - -} - -func TestBundleMisc(t *testing.T) { - config := ` -baseURL = "https://example.com" -defaultContentLanguage = "en" -defaultContentLanguageInSubdir = true -ignoreFiles = ["README\\.md", "content/en/ignore"] - -[Languages] -[Languages.en] -weight = 99999 -contentDir = "content/en" -[Languages.nn] -weight = 20 -contentDir = "content/nn" -[Languages.sv] -weight = 30 -contentDir = "content/sv" -[Languages.nb] -weight = 40 -contentDir = "content/nb" - -` - - const pageContent = `--- -title: %q ---- -` - createPage := func(s string) string { - return fmt.Sprintf(pageContent, s) - } - - b := newTestSitesBuilder(t).WithConfigFile("toml", config) - b.WithLogger(loggers.NewWarningLogger()) - - b.WithTemplates("_default/list.html", `{{ range .Site.Pages }} -{{ .Kind }}|{{ .Path }}|{{ with .CurrentSection }}CurrentSection: {{ .Path }}{{ end }}|{{ .RelPermalink }}{{ end }} -`) - - b.WithTemplates("_default/single.html", `Single: {{ .Title }}`) - - b.WithContent("en/sect1/sect2/_index.md", createPage("en: Sect 2")) - b.WithContent("en/sect1/sect2/page.md", createPage("en: Page")) - b.WithContent("en/sect1/sect2/data-branch.json", "mydata") - b.WithContent("nn/sect1/sect2/page.md", createPage("nn: Page")) - b.WithContent("nn/sect1/sect2/data-branch.json", "my nn data") - - // En only - b.WithContent("en/enonly/myen.md", createPage("en: Page")) - b.WithContent("en/enonly/myendata.json", "mydata") - - // Leaf - - b.WithContent("nn/b1/index.md", createPage("nn: leaf")) - b.WithContent("en/b1/index.md", createPage("en: leaf")) - b.WithContent("sv/b1/index.md", createPage("sv: leaf")) - b.WithContent("nb/b1/index.md", createPage("nb: leaf")) - - // Should be ignored - b.WithContent("en/ignore/page.md", createPage("en: ignore")) - b.WithContent("en/README.md", createPage("en: ignore")) - - // Both leaf and branch bundle in same dir - b.WithContent("en/b2/index.md", `--- -slug: leaf ---- -`) - b.WithContent("en/b2/_index.md", createPage("en: branch")) - - b.WithContent("en/b1/data1.json", "en: data") - b.WithContent("sv/b1/data1.json", "sv: data") - b.WithContent("sv/b1/data2.json", "sv: data2") - b.WithContent("nb/b1/data2.json", "nb: data2") - - b.WithContent("en/b3/_index.md", createPage("en: branch")) - b.WithContent("en/b3/p1.md", createPage("en: page")) - b.WithContent("en/b3/data1.json", "en: data") - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/en/index.html", - filepath.FromSlash("section|sect1/sect2/_index.md|CurrentSection: sect1/sect2/_index.md"), - "myen.md|CurrentSection: enonly") - - b.AssertFileContentFn("public/en/index.html", func(s string) bool { - // Check ignored files - return !regexp.MustCompile("README|ignore").MatchString(s) - - }) - - b.AssertFileContent("public/nn/index.html", filepath.FromSlash("page|sect1/sect2/page.md|CurrentSection: sect1")) - b.AssertFileContentFn("public/nn/index.html", func(s string) bool { - return !strings.Contains(s, "enonly") - - }) - - // Check order of inherited data file - b.AssertFileContent("public/nb/b1/data1.json", "en: data") // Default content - b.AssertFileContent("public/nn/b1/data2.json", "sv: data") // First match - - b.AssertFileContent("public/en/enonly/myen/index.html", "Single: en: Page") - b.AssertFileContent("public/en/enonly/myendata.json", "mydata") - - c := qt.New(t) - c.Assert(b.CheckExists("public/sv/enonly/myen/index.html"), qt.Equals, false) - - // Both leaf and branch bundle in same dir - // We log a warning about it, but we keep both. - b.AssertFileContent("public/en/b2/index.html", - "/en/b2/leaf/", - filepath.FromSlash("section|sect1/sect2/_index.md|CurrentSection: sect1/sect2/_index.md")) - -} - -// Issue 6136 -func TestPageBundlerPartialTranslations(t *testing.T) { - config := ` -baseURL = "https://example.org" -defaultContentLanguage = "en" -defaultContentLanguageInSubDir = true -disableKinds = ["taxonomyTerm", "taxonomy"] - -[languages] -[languages.nn] -languageName = "Nynorsk" -weight = 2 -title = "Tittel på Nynorsk" - -[languages.en] -title = "Title in English" -languageName = "English" -weight = 1 - -` - - pageContent := func(id string) string { - return fmt.Sprintf(` ---- -title: %q ---- -`, id) - } - - dataContent := func(id string) string { - return id - } - - b := newTestSitesBuilder(t).WithConfigFile("toml", config) - - b.WithContent("blog/sect1/_index.nn.md", pageContent("s1.nn")) - b.WithContent("blog/sect1/data.json", dataContent("s1.data")) - - b.WithContent("blog/sect1/b1/index.nn.md", pageContent("s1.b1.nn")) - b.WithContent("blog/sect1/b1/data.json", dataContent("s1.b1.data")) - - b.WithContent("blog/sect2/_index.md", pageContent("s2")) - b.WithContent("blog/sect2/data.json", dataContent("s2.data")) - - b.WithContent("blog/sect2/b1/index.md", pageContent("s2.b1")) - b.WithContent("blog/sect2/b1/data.json", dataContent("s2.b1.data")) - - b.WithContent("blog/sect2/b2/index.md", pageContent("s2.b2")) - b.WithContent("blog/sect2/b2/bp.md", pageContent("s2.b2.bundlecontent")) - - b.WithContent("blog/sect2/b3/index.md", pageContent("s2.b3")) - b.WithContent("blog/sect2/b3/bp.nn.md", pageContent("s2.b3.bundlecontent.nn")) - - b.WithContent("blog/sect2/b4/index.nn.md", pageContent("s2.b4")) - b.WithContent("blog/sect2/b4/bp.nn.md", pageContent("s2.b4.bundlecontent.nn")) - - b.WithTemplates("index.html", ` -Num Pages: {{ len .Site.Pages }} -{{ range .Site.Pages }} -{{ .Kind }}|{{ .RelPermalink }}|Content: {{ .Title }}|Resources: {{ range .Resources }}R: {{ .Title }}|{{ .Content }}|{{ end -}} -{{ end }} -`) - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/nn/index.html", - "Num Pages: 6", - "page|/nn/blog/sect1/b1/|Content: s1.b1.nn|Resources: R: data.json|s1.b1.data|", - "page|/nn/blog/sect2/b3/|Content: s2.b3|Resources: R: s2.b3.bundlecontent.nn|", - "page|/nn/blog/sect2/b4/|Content: s2.b4|Resources: R: s2.b4.bundlecontent.nn", - ) - - b.AssertFileContent("public/en/index.html", - "Num Pages: 6", - "section|/en/blog/sect2/|Content: s2|Resources: R: data.json|s2.data|", - "page|/en/blog/sect2/b1/|Content: s2.b1|Resources: R: data.json|s2.b1.data|", - "page|/en/blog/sect2/b2/|Content: s2.b2|Resources: R: s2.b2.bundlecontent|", - ) - -} - -// #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.NewWarningLogger()) - - 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", ` - json|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 i := 0; i < 3; i++ { - - 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 := helpers.MD5String(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 := helpers.MD5String(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") - - } -} diff --git a/hugolib/pagecollections.go b/hugolib/pagecollections.go deleted file mode 100644 index 4711d3de3..000000000 --- a/hugolib/pagecollections.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" - "path" - "path/filepath" - "sort" - "strings" - "sync" - "time" - - "github.com/gohugoio/hugo/resources/resource" - - "github.com/pkg/errors" - - "github.com/gohugoio/hugo/cache" - "github.com/gohugoio/hugo/resources/page" -) - -// Used in the page cache to mark more than one hit for a given key. -var ambiguityFlag = &pageState{} - -// PageCollections contains the page collections for a site. -type PageCollections struct { - pagesMap *pagesMap - - // Includes absolute all pages (of all types), including drafts etc. - rawAllPages pageStatePages - - // rawAllPages plus additional pages created during the build process. - workAllPages pageStatePages - - // Includes headless bundles, i.e. bundles that produce no output for its content page. - headlessPages pageStatePages - - // Lazy initialized page collections - pages *lazyPagesFactory - regularPages *lazyPagesFactory - allPages *lazyPagesFactory - allRegularPages *lazyPagesFactory - - // The index for .Site.GetPage etc. - pageIndex *cache.Lazy -} - -// Pages returns all pages. -// This is for the current language only. -func (c *PageCollections) Pages() page.Pages { - return c.pages.get() -} - -// RegularPages returns all the regular pages. -// This is for the current language only. -func (c *PageCollections) RegularPages() page.Pages { - return c.regularPages.get() -} - -// AllPages returns all pages for all languages. -func (c *PageCollections) AllPages() page.Pages { - return c.allPages.get() -} - -// AllPages returns all regular pages for all languages. -func (c *PageCollections) AllRegularPages() page.Pages { - return c.allRegularPages.get() -} - -// Get initializes the index if not already done so, then -// looks up the given page ref, returns nil if no value found. -func (c *PageCollections) getFromCache(ref string) (page.Page, error) { - v, found, err := c.pageIndex.Get(ref) - if err != nil { - return nil, err - } - if !found { - return nil, nil - } - - p := v.(page.Page) - - if p != ambiguityFlag { - return p, nil - } - return nil, fmt.Errorf("page reference %q is ambiguous", ref) -} - -type lazyPagesFactory struct { - pages page.Pages - - init sync.Once - factory page.PagesFactory -} - -func (l *lazyPagesFactory) get() page.Pages { - l.init.Do(func() { - l.pages = l.factory() - }) - return l.pages -} - -func newLazyPagesFactory(factory page.PagesFactory) *lazyPagesFactory { - return &lazyPagesFactory{factory: factory} -} - -func newPageCollections() *PageCollections { - return newPageCollectionsFromPages(nil) -} - -func newPageCollectionsFromPages(pages pageStatePages) *PageCollections { - - c := &PageCollections{rawAllPages: pages} - - c.pages = newLazyPagesFactory(func() page.Pages { - pages := make(page.Pages, len(c.workAllPages)) - for i, p := range c.workAllPages { - pages[i] = p - } - return pages - }) - - c.regularPages = newLazyPagesFactory(func() page.Pages { - return c.findPagesByKindInWorkPages(page.KindPage, c.workAllPages) - }) - - c.pageIndex = cache.NewLazy(func() (map[string]interface{}, error) { - index := make(map[string]interface{}) - - add := func(ref string, p page.Page) { - ref = strings.ToLower(ref) - existing := index[ref] - if existing == nil { - index[ref] = p - } else if existing != ambiguityFlag && existing != p { - index[ref] = ambiguityFlag - } - } - - for _, pageCollection := range []pageStatePages{c.workAllPages, c.headlessPages} { - for _, p := range pageCollection { - if p.IsPage() { - sourceRef := p.sourceRef() - if sourceRef != "" { - // index the canonical ref - // e.g. /section/article.md - add(sourceRef, p) - } - - // Ref/Relref supports this potentially ambiguous lookup. - add(p.File().LogicalName(), p) - - translationBaseName := p.File().TranslationBaseName() - - dir, _ := path.Split(sourceRef) - dir = strings.TrimSuffix(dir, "/") - - if translationBaseName == "index" { - add(dir, p) - add(path.Base(dir), p) - } else { - add(translationBaseName, p) - } - - // We need a way to get to the current language version. - pathWithNoExtensions := path.Join(dir, translationBaseName) - add(pathWithNoExtensions, p) - } else { - // index the canonical, unambiguous ref for any backing file - // e.g. /section/_index.md - sourceRef := p.sourceRef() - if sourceRef != "" { - add(sourceRef, p) - } - - ref := p.SectionsPath() - - // index the canonical, unambiguous virtual ref - // e.g. /section - // (this may already have been indexed above) - add("/"+ref, p) - } - } - } - - return index, nil - }) - - return c -} - -// 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 ambigous. -func (c *PageCollections) getPageOldVersion(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] == page.KindHome { - key = "/" - } else if len(refs) == 1 { - if len(ref) == 2 && refs[0] == page.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] - } - - key = filepath.ToSlash(key) - if !strings.HasPrefix(key, "/") { - key = "/" + key - } - - return c.getPageNew(nil, key) -} - -// Only used in tests. -func (c *PageCollections) getPage(typ string, sections ...string) page.Page { - refs := append([]string{typ}, path.Join(sections...)) - p, _ := c.getPageOldVersion(refs...) - return p -} - -// Case insensitive page lookup. -func (c *PageCollections) getPageNew(context page.Page, ref string) (page.Page, error) { - var anError error - - ref = strings.ToLower(ref) - - // Absolute (content root relative) reference. - if strings.HasPrefix(ref, "/") { - p, err := c.getFromCache(ref) - if err == nil && p != nil { - return p, nil - } - if err != nil { - anError = err - } - - } else if context != nil { - // Try the page-relative path. - ppath := path.Join("/", strings.ToLower(context.SectionsPath()), ref) - p, err := c.getFromCache(ppath) - if err == nil && p != nil { - return p, nil - } - if err != nil { - anError = err - } - } - - if !strings.HasPrefix(ref, "/") { - // Many people will have "post/foo.md" in their content files. - p, err := c.getFromCache("/" + ref) - if err == nil && p != nil { - return p, nil - } - if err != nil { - anError = err - } - } - - // Last try. - ref = strings.TrimPrefix(ref, "/") - p, err := c.getFromCache(ref) - if err != nil { - anError = err - } - - if p == nil && anError != nil { - return nil, wrapErr(errors.Wrap(anError, "failed to resolve ref"), context) - } - - return p, nil -} - -func (*PageCollections) findPagesByKindIn(kind string, inPages page.Pages) page.Pages { - var pages page.Pages - for _, p := range inPages { - if p.Kind() == kind { - pages = append(pages, p) - } - } - return pages -} - -func (c *PageCollections) findPagesByKind(kind string) page.Pages { - return c.findPagesByKindIn(kind, c.Pages()) -} - -func (c *PageCollections) findWorkPagesByKind(kind string) pageStatePages { - var pages pageStatePages - for _, p := range c.workAllPages { - if p.Kind() == kind { - pages = append(pages, p) - } - } - return pages -} - -func (*PageCollections) findPagesByKindInWorkPages(kind string, inPages pageStatePages) page.Pages { - var pages page.Pages - for _, p := range inPages { - if p.Kind() == kind { - pages = append(pages, p) - } - } - return pages -} - -func (c *PageCollections) addPage(page *pageState) { - c.rawAllPages = append(c.rawAllPages, page) -} - -func (c *PageCollections) removePageFilename(filename string) { - if i := c.rawAllPages.findPagePosByFilename(filename); i >= 0 { - c.clearResourceCacheForPage(c.rawAllPages[i]) - c.rawAllPages = append(c.rawAllPages[:i], c.rawAllPages[i+1:]...) - } - -} - -func (c *PageCollections) removePage(page *pageState) { - if i := c.rawAllPages.findPagePos(page); i >= 0 { - c.clearResourceCacheForPage(c.rawAllPages[i]) - c.rawAllPages = append(c.rawAllPages[:i], c.rawAllPages[i+1:]...) - } -} - -func (c *PageCollections) findPagesByShortcode(shortcode string) page.Pages { - var pages page.Pages - for _, p := range c.rawAllPages { - if p.HasShortcode(shortcode) { - pages = append(pages, p) - } - } - return pages -} - -func (c *PageCollections) replacePage(page *pageState) { - // will find existing page that matches filepath and remove it - c.removePage(page) - c.addPage(page) -} - -func (c *PageCollections) clearResourceCacheForPage(page *pageState) { - if len(page.resources) > 0 { - page.s.ResourceSpec.DeleteCacheByPrefix(page.targetPaths().SubResourceBaseTarget) - } -} - -func (c *PageCollections) assemblePagesMap(s *Site) error { - - c.pagesMap = newPagesMap(s) - - rootSections := make(map[string]bool) - - // Add all branch nodes first. - for _, p := range c.rawAllPages { - rootSections[p.Section()] = true - if p.IsPage() { - continue - } - c.pagesMap.addPage(p) - } - - // Create missing home page and the first level sections if no - // _index provided. - s.home = c.pagesMap.getOrCreateHome() - for k := range rootSections { - c.pagesMap.createSectionIfNotExists(k) - } - - // Attach the regular pages to their section. - for _, p := range c.rawAllPages { - if p.IsNode() { - continue - } - c.pagesMap.addPage(p) - } - - return nil -} - -func (c *PageCollections) createWorkAllPages() error { - c.workAllPages = make(pageStatePages, 0, len(c.rawAllPages)) - c.headlessPages = make(pageStatePages, 0) - - var ( - homeDates *resource.Dates - sectionDates *resource.Dates - siteLastmod time.Time - siteLastDate time.Time - - sectionsParamId = "mainSections" - sectionsParamIdLower = strings.ToLower(sectionsParamId) - ) - - mainSections, mainSectionsFound := c.pagesMap.s.Info.Params()[sectionsParamIdLower] - - var ( - bucketsToRemove []string - rootBuckets []*pagesMapBucket - walkErr error - ) - - c.pagesMap.r.Walk(func(s string, v interface{}) bool { - bucket := v.(*pagesMapBucket) - parentBucket := c.pagesMap.parentBucket(s) - - if parentBucket != nil { - - if !mainSectionsFound && strings.Count(s, "/") == 1 && bucket.owner.IsSection() { - // Root section - rootBuckets = append(rootBuckets, bucket) - } - } - - if bucket.owner.IsHome() { - if resource.IsZeroDates(bucket.owner) { - // Calculate dates from the page tree. - homeDates = &bucket.owner.m.Dates - } - } - - sectionDates = nil - if resource.IsZeroDates(bucket.owner) { - sectionDates = &bucket.owner.m.Dates - } - - if parentBucket != nil { - bucket.parent = parentBucket - if bucket.owner.IsSection() { - parentBucket.bucketSections = append(parentBucket.bucketSections, bucket) - } - } - - if bucket.isEmpty() { - if bucket.owner.IsSection() && bucket.owner.File().IsZero() { - // Check for any nested section. - var hasDescendant bool - c.pagesMap.r.WalkPrefix(s, func(ss string, v interface{}) bool { - if s != ss { - hasDescendant = true - return true - } - return false - }) - if !hasDescendant { - // This is an auto-created section with, now, nothing in it. - bucketsToRemove = append(bucketsToRemove, s) - return false - } - } - } - - if !bucket.disabled { - c.workAllPages = append(c.workAllPages, bucket.owner) - } - - if !bucket.view { - for _, p := range bucket.pages { - ps := p.(*pageState) - ps.parent = bucket.owner - if ps.m.headless { - c.headlessPages = append(c.headlessPages, ps) - } else { - c.workAllPages = append(c.workAllPages, ps) - } - - if homeDates != nil { - homeDates.UpdateDateAndLastmodIfAfter(ps) - } - - if sectionDates != nil { - sectionDates.UpdateDateAndLastmodIfAfter(ps) - } - - if p.Lastmod().After(siteLastmod) { - siteLastmod = p.Lastmod() - } - if p.Date().After(siteLastDate) { - siteLastDate = p.Date() - } - } - } - - return false - }) - - if walkErr != nil { - return walkErr - } - - c.pagesMap.s.lastmod = siteLastmod - - if !mainSectionsFound { - - // Calculare main section - var ( - maxRootBucketWeight int - maxRootBucket *pagesMapBucket - ) - - for _, b := range rootBuckets { - weight := len(b.pages) + (len(b.bucketSections) * 5) - if weight >= maxRootBucketWeight { - maxRootBucket = b - maxRootBucketWeight = weight - } - } - - if maxRootBucket != nil { - // Try to make this as backwards compatible as possible. - mainSections = []string{maxRootBucket.owner.Section()} - } - } - - c.pagesMap.s.Info.Params()[sectionsParamId] = mainSections - c.pagesMap.s.Info.Params()[sectionsParamIdLower] = mainSections - - for _, key := range bucketsToRemove { - c.pagesMap.r.Delete(key) - } - - sort.Sort(c.workAllPages) - - return nil -} diff --git a/hugolib/pagecollections_test.go b/hugolib/pagecollections_test.go deleted file mode 100644 index 0f7520e1b..000000000 --- a/hugolib/pagecollections_test.go +++ /dev/null @@ -1,246 +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/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())) - ) - - 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.getPageNew(nil, "/") - if home == nil { - b.Fatal("Home is nil") - } - - p, _ := s.getPageNew(nil, pagePaths[i]) - if p == nil { - b.Fatal("Section is nil") - } - - } -} - -func BenchmarkGetPageRegular(b *testing.B) { - var ( - c = qt.New(b) - 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.getPageNew(nil, pagePaths[i]) - c.Assert(page, qt.Not(qt.IsNil)) - } -} - -type testCase struct { - kind string - context page.Page - path []string - expectedTitle string -} - -func (t *testCase) check(p page.Page, err error, errorMsg string, c *qt.C) { - 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) - ) - - 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, "home page") - writeSource(t, fs, filepath.Join("content", "_index.md"), content) - - content = fmt.Sprintf(pageCollectionsPageTemplate, "about page") - writeSource(t, fs, filepath.Join("content", "about.md"), content) - - content = fmt.Sprintf(pageCollectionsPageTemplate, "section 3") - writeSource(t, fs, filepath.Join("content", "sect3", "_index.md"), content) - - content = fmt.Sprintf(pageCollectionsPageTemplate, "UniqueBase") - writeSource(t, fs, filepath.Join("content", "sect3", "unique.md"), content) - - content = fmt.Sprintf(pageCollectionsPageTemplate, "another sect7") - writeSource(t, fs, filepath.Join("content", "sect3", "sect7", "_index.md"), content) - - content = fmt.Sprintf(pageCollectionsPageTemplate, "deep page") - writeSource(t, fs, filepath.Join("content", "sect3", "subsect", "deep.md"), content) - - s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{SkipRender: true}) - - sec3, err := s.getPageNew(nil, "/sect3") - c.Assert(err, qt.IsNil) - c.Assert(sec3, qt.Not(qt.IsNil)) - - tests := []testCase{ - // legacy content root relative paths - {page.KindHome, nil, []string{}, "home page"}, - {page.KindPage, nil, []string{"about.md"}, "about page"}, - {page.KindSection, nil, []string{"sect3"}, "section 3"}, - {page.KindPage, nil, []string{"sect3/page1.md"}, "Title3_1"}, - {page.KindPage, nil, []string{"sect4/page2.md"}, "Title4_2"}, - {page.KindSection, nil, []string{"sect3/sect7"}, "another sect7"}, - {page.KindPage, nil, []string{"sect3/subsect/deep.md"}, "deep page"}, - {page.KindPage, nil, []string{filepath.FromSlash("sect5/page3.md")}, "Title5_3"}, //test OS-specific path - - // shorthand refs (potentially ambiguous) - {page.KindPage, nil, []string{"unique.md"}, "UniqueBase"}, - {"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"}, ""}, - {page.KindSection, nil, []string{"sect7"}, "Sect7s"}, - - // absolute paths - {page.KindHome, nil, []string{"/"}, "home page"}, - {page.KindPage, nil, []string{"/about.md"}, "about page"}, - {page.KindSection, nil, []string{"/sect3"}, "section 3"}, - {page.KindPage, nil, []string{"/sect3/page1.md"}, "Title3_1"}, - {page.KindPage, nil, []string{"/sect4/page2.md"}, "Title4_2"}, - {page.KindSection, nil, []string{"/sect3/sect7"}, "another sect7"}, - {page.KindPage, nil, []string{"/sect3/subsect/deep.md"}, "deep page"}, - {page.KindPage, nil, []string{filepath.FromSlash("/sect5/page3.md")}, "Title5_3"}, //test OS-specific path - {page.KindPage, nil, []string{"/sect3/unique.md"}, "UniqueBase"}, //next test depends on this page existing - // {"NoPage", nil, []string{"/unique.md"}, ""}, // ISSUE #4969: this is resolving to /sect3/unique.md - {"NoPage", nil, []string{"/missing-page.md"}, ""}, - {"NoPage", nil, []string{"/missing-section"}, ""}, - - // relative paths - {page.KindHome, sec3, []string{".."}, "home page"}, - {page.KindHome, sec3, []string{"../"}, "home page"}, - {page.KindPage, sec3, []string{"../about.md"}, "about page"}, - {page.KindSection, sec3, []string{"."}, "section 3"}, - {page.KindSection, sec3, []string{"./"}, "section 3"}, - {page.KindPage, sec3, []string{"page1.md"}, "Title3_1"}, - {page.KindPage, sec3, []string{"./page1.md"}, "Title3_1"}, - {page.KindPage, sec3, []string{"../sect4/page2.md"}, "Title4_2"}, - {page.KindSection, sec3, []string{"sect7"}, "another sect7"}, - {page.KindSection, sec3, []string{"./sect7"}, "another sect7"}, - {page.KindPage, sec3, []string{"./subsect/deep.md"}, "deep page"}, - {page.KindPage, sec3, []string{"./subsect/../../sect7/page9.md"}, "Title7_9"}, - {page.KindPage, sec3, []string{filepath.FromSlash("../sect5/page3.md")}, "Title5_3"}, //test OS-specific path - {page.KindPage, sec3, []string{"./unique.md"}, "UniqueBase"}, - {"NoPage", sec3, []string{"./sect2"}, ""}, - //{"NoPage", sec3, []string{"sect2"}, ""}, // ISSUE: /sect3 page relative query is resolving to /sect2 - - // absolute paths ignore context - {page.KindHome, sec3, []string{"/"}, "home page"}, - {page.KindPage, sec3, []string{"/about.md"}, "about page"}, - {page.KindPage, sec3, []string{"/sect4/page2.md"}, "Title4_2"}, - {page.KindPage, sec3, []string{"/sect3/subsect/deep.md"}, "deep page"}, //next test depends on this page existing - {"NoPage", sec3, []string{"/subsect/deep.md"}, ""}, - } - - for _, test := range tests { - errorMsg := fmt.Sprintf("Test case %s %v -> %s", test.context, test.path, test.expectedTitle) - - // test legacy public Site.GetPage (which does not support page context relative queries) - if test.context == nil { - args := append([]string{test.kind}, test.path...) - page, err := s.Info.GetPage(args...) - test.check(page, err, errorMsg, c) - } - - // test new internal Site.getPageNew - var ref string - if len(test.path) == 1 { - ref = filepath.ToSlash(test.path[0]) - } else { - ref = path.Join(test.path...) - } - page2, err := s.getPageNew(test.context, ref) - test.check(page2, err, errorMsg, c) - } - -} diff --git a/hugolib/pages_capture.go b/hugolib/pages_capture.go deleted file mode 100644 index 591b8e317..000000000 --- a/hugolib/pages_capture.go +++ /dev/null @@ -1,784 +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" - "os" - pth "path" - "path/filepath" - "strings" - - "github.com/gohugoio/hugo/config" - - "github.com/gohugoio/hugo/hugofs/files" - - "github.com/gohugoio/hugo/resources" - - "github.com/pkg/errors" - "golang.org/x/sync/errgroup" - - "github.com/gohugoio/hugo/common/hugio" - - "github.com/gohugoio/hugo/resources/resource" - - "github.com/gohugoio/hugo/source" - - "github.com/gohugoio/hugo/common/loggers" - "github.com/gohugoio/hugo/hugofs" - "github.com/spf13/afero" -) - -func newPagesCollector( - sp *source.SourceSpec, - logger *loggers.Logger, - contentTracker *contentChangeMap, - proc pagesCollectorProcessorProvider, filenames ...string) *pagesCollector { - - return &pagesCollector{ - fs: sp.SourceFs, - proc: proc, - sp: sp, - logger: logger, - filenames: filenames, - tracker: contentTracker, - } -} - -func newPagesProcessor(h *HugoSites, sp *source.SourceSpec, partialBuild bool) *pagesProcessor { - - return &pagesProcessor{ - h: h, - sp: sp, - partialBuild: partialBuild, - numWorkers: config.GetNumWorkerMultiplier() * 3, - } -} - -type fileinfoBundle struct { - header hugofs.FileMetaInfo - resources []hugofs.FileMetaInfo -} - -func (b *fileinfoBundle) containsResource(name string) bool { - for _, r := range b.resources { - if r.Name() == name { - return true - } - } - - return false - -} - -type pageBundles map[string]*fileinfoBundle - -type pagesCollector struct { - sp *source.SourceSpec - fs afero.Fs - logger *loggers.Logger - - // Ordered list (bundle headers first) used in partial builds. - filenames []string - - // Content files tracker used in partial builds. - tracker *contentChangeMap - - proc pagesCollectorProcessorProvider -} - -type contentDirKey struct { - dirname string - filename string - tp bundleDirType -} - -// Collect. -func (c *pagesCollector) Collect() error { - c.proc.Start(context.Background()) - - var collectErr error - if len(c.filenames) == 0 { - // Collect everything. - collectErr = c.collectDir("", false, nil) - } else { - dirs := make(map[contentDirKey]bool) - for _, filename := range c.filenames { - dir, filename, btype := c.tracker.resolveAndRemove(filename) - dirs[contentDirKey{dir, filename, btype}] = true - } - - for dir := range dirs { - switch dir.tp { - case bundleLeaf, bundleBranch: - collectErr = c.collectDir(dir.dirname, true, nil) - default: - // We always start from a directory. - collectErr = c.collectDir(dir.dirname, true, func(fim hugofs.FileMetaInfo) bool { - return strings.HasSuffix(dir.filename, fim.Meta().Path()) - }) - } - - if collectErr != nil { - break - } - } - - } - - err := c.proc.Wait() - - if collectErr != nil { - return collectErr - } - - return err -} - -func (c *pagesCollector) collectDir(dirname string, partial bool, inFilter func(fim hugofs.FileMetaInfo) bool) error { - fi, err := c.fs.Stat(dirname) - if err != nil { - if os.IsNotExist(err) { - // May have been deleted. - return nil - } - return err - } - - handleDir := func( - btype bundleDirType, - dir hugofs.FileMetaInfo, - path string, - readdir []hugofs.FileMetaInfo) error { - - if btype > bundleNot && c.tracker != nil { - c.tracker.add(path, btype) - } - - if btype == bundleBranch { - if err := c.handleBundleBranch(readdir); err != nil { - return err - } - // A branch bundle is only this directory level, so keep walking. - return nil - } else if btype == bundleLeaf { - if err := c.handleBundleLeaf(dir, path, readdir); err != nil { - return err - } - - return nil - } - - if err := c.handleFiles(readdir...); err != nil { - return err - } - - return nil - - } - - filter := func(fim hugofs.FileMetaInfo) bool { - if fim.Meta().SkipDir() { - return false - } - - if c.sp.IgnoreFile(fim.Meta().Filename()) { - return false - } - - if inFilter != nil { - return inFilter(fim) - } - return true - } - - preHook := func(dir hugofs.FileMetaInfo, path string, readdir []hugofs.FileMetaInfo) ([]hugofs.FileMetaInfo, error) { - var btype bundleDirType - - filtered := readdir[:0] - for _, fi := range readdir { - if filter(fi) { - filtered = append(filtered, fi) - if c.tracker != nil { - // Track symlinks. - c.tracker.addSymbolicLinkMapping(fi) - } - } - } - readdir = filtered - - // We merge language directories, so there can be duplicates, but they - // will be ordered, most important first. - var duplicates []int - seen := make(map[string]bool) - - for i, fi := range readdir { - - if fi.IsDir() { - continue - } - - meta := fi.Meta() - class := meta.Classifier() - translationBase := meta.TranslationBaseNameWithExt() - key := pth.Join(meta.Lang(), translationBase) - - if seen[key] { - duplicates = append(duplicates, i) - continue - } - seen[key] = true - - var thisBtype bundleDirType - - switch class { - case files.ContentClassLeaf: - thisBtype = bundleLeaf - case files.ContentClassBranch: - thisBtype = bundleBranch - } - - // Folders with both index.md and _index.md type of files have - // undefined behaviour and can never work. - // The branch variant will win because of sort order, but log - // a warning about it. - if thisBtype > bundleNot && btype > bundleNot && thisBtype != btype { - c.logger.WARN.Printf("Content directory %q have both index.* and _index.* files, pick one.", dir.Meta().Filename()) - // Reclassify it so it will be handled as a content file inside the - // section, which is in line with the <= 0.55 behaviour. - meta["classifier"] = files.ContentClassContent - } else if thisBtype > bundleNot { - btype = thisBtype - } - - } - - if len(duplicates) > 0 { - for i := len(duplicates) - 1; i >= 0; i-- { - idx := duplicates[i] - readdir = append(readdir[:idx], readdir[idx+1:]...) - } - } - - err := handleDir(btype, dir, path, readdir) - if err != nil { - return nil, err - } - - if btype == bundleLeaf || partial { - return nil, filepath.SkipDir - } - - // Keep walking. - return readdir, nil - - } - - var postHook hugofs.WalkHook - if c.tracker != nil { - postHook = func(dir hugofs.FileMetaInfo, path string, readdir []hugofs.FileMetaInfo) ([]hugofs.FileMetaInfo, error) { - if c.tracker == nil { - // Nothing to do. - return readdir, nil - } - - return readdir, nil - } - } - - wfn := func(path string, info hugofs.FileMetaInfo, err error) error { - if err != nil { - return err - } - - return nil - } - - w := hugofs.NewWalkway(hugofs.WalkwayConfig{ - Fs: c.fs, - Logger: c.logger, - Root: dirname, - Info: fi.(hugofs.FileMetaInfo), - HookPre: preHook, - HookPost: postHook, - WalkFn: wfn}) - - return w.Walk() - -} - -func (c *pagesCollector) isBundleHeader(fi hugofs.FileMetaInfo) bool { - class := fi.Meta().Classifier() - return class == files.ContentClassLeaf || class == files.ContentClassBranch -} - -func (c *pagesCollector) getLang(fi hugofs.FileMetaInfo) string { - lang := fi.Meta().Lang() - if lang != "" { - return lang - } - - return c.sp.DefaultContentLanguage -} - -func (c *pagesCollector) addToBundle(info hugofs.FileMetaInfo, btyp bundleDirType, bundles pageBundles) error { - getBundle := func(lang string) *fileinfoBundle { - return bundles[lang] - } - - cloneBundle := func(lang string) *fileinfoBundle { - // Every bundled content file needs a content file header. - // Use the default content language if found, else just - // pick one. - var ( - source *fileinfoBundle - found bool - ) - - source, found = bundles[c.sp.DefaultContentLanguage] - if !found { - for _, b := range bundles { - source = b - break - } - } - - if source == nil { - panic(fmt.Sprintf("no source found, %d", len(bundles))) - } - - clone := c.cloneFileInfo(source.header) - clone.Meta()["lang"] = lang - - return &fileinfoBundle{ - header: clone, - } - } - - lang := c.getLang(info) - bundle := getBundle(lang) - isBundleHeader := c.isBundleHeader(info) - if bundle != nil && isBundleHeader { - // index.md file inside a bundle, see issue 6208. - info.Meta()["classifier"] = files.ContentClassContent - isBundleHeader = false - } - classifier := info.Meta().Classifier() - isContent := classifier == files.ContentClassContent - if bundle == nil { - if isBundleHeader { - bundle = &fileinfoBundle{header: info} - bundles[lang] = bundle - } else { - if btyp == bundleBranch { - // No special logic for branch bundles. - // Every language needs its own _index.md file. - // Also, we only clone bundle headers for lonsesome, bundled, - // content files. - return c.handleFiles(info) - } - - if isContent { - bundle = cloneBundle(lang) - bundles[lang] = bundle - } - } - } - - if !isBundleHeader && bundle != nil { - bundle.resources = append(bundle.resources, info) - } - - if classifier == files.ContentClassFile { - translations := info.Meta().Translations() - - for lang, b := range bundles { - if !stringSliceContains(lang, translations...) && !b.containsResource(info.Name()) { - - // Clone and add it to the bundle. - clone := c.cloneFileInfo(info) - clone.Meta()["lang"] = lang - b.resources = append(b.resources, clone) - } - } - } - - return nil -} - -func (c *pagesCollector) cloneFileInfo(fi hugofs.FileMetaInfo) hugofs.FileMetaInfo { - cm := hugofs.FileMeta{} - meta := fi.Meta() - if meta == nil { - panic(fmt.Sprintf("not meta: %v", fi.Name())) - } - for k, v := range meta { - cm[k] = v - } - - return hugofs.NewFileMetaInfo(fi, cm) -} - -func (c *pagesCollector) handleBundleBranch(readdir []hugofs.FileMetaInfo) error { - - // Maps bundles to its language. - bundles := pageBundles{} - - for _, fim := range readdir { - - if fim.IsDir() { - continue - } - - meta := fim.Meta() - - switch meta.Classifier() { - case files.ContentClassContent: - if err := c.handleFiles(fim); err != nil { - return err - } - default: - if err := c.addToBundle(fim, bundleBranch, bundles); err != nil { - return err - } - } - - } - - return c.proc.Process(bundles) - -} - -func (c *pagesCollector) handleBundleLeaf(dir hugofs.FileMetaInfo, path string, readdir []hugofs.FileMetaInfo) error { - // Maps bundles to its language. - bundles := pageBundles{} - - walk := func(path string, info hugofs.FileMetaInfo, err error) error { - if err != nil { - return err - } - if info.IsDir() { - return nil - } - - return c.addToBundle(info, bundleLeaf, bundles) - - } - - // Start a new walker from the given path. - w := hugofs.NewWalkway(hugofs.WalkwayConfig{ - Root: path, - Fs: c.fs, - Logger: c.logger, - Info: dir, - DirEntries: readdir, - WalkFn: walk}) - - if err := w.Walk(); err != nil { - return err - } - - return c.proc.Process(bundles) - -} - -func (c *pagesCollector) handleFiles(fis ...hugofs.FileMetaInfo) error { - for _, fi := range fis { - if fi.IsDir() { - continue - } - - if err := c.proc.Process(fi); err != nil { - return err - } - } - return nil -} - -type pagesCollectorProcessorProvider interface { - Process(item interface{}) error - Start(ctx context.Context) context.Context - Wait() error -} - -type pagesProcessor struct { - h *HugoSites - sp *source.SourceSpec - - itemChan chan interface{} - itemGroup *errgroup.Group - - // The output Pages - pagesChan chan *pageState - pagesGroup *errgroup.Group - - numWorkers int - - partialBuild bool -} - -func (proc *pagesProcessor) Process(item interface{}) error { - proc.itemChan <- item - return nil -} - -func (proc *pagesProcessor) Start(ctx context.Context) context.Context { - proc.pagesChan = make(chan *pageState, proc.numWorkers) - proc.pagesGroup, ctx = errgroup.WithContext(ctx) - proc.itemChan = make(chan interface{}, proc.numWorkers) - proc.itemGroup, ctx = errgroup.WithContext(ctx) - - proc.pagesGroup.Go(func() error { - for p := range proc.pagesChan { - s := p.s - p.forceRender = proc.partialBuild - - if p.forceRender { - s.replacePage(p) - } else { - s.addPage(p) - } - } - return nil - }) - - for i := 0; i < proc.numWorkers; i++ { - proc.itemGroup.Go(func() error { - for item := range proc.itemChan { - select { - case <-proc.h.Done(): - return nil - default: - if err := proc.process(item); err != nil { - proc.h.SendError(err) - } - } - } - - return nil - }) - } - - return ctx -} - -func (proc *pagesProcessor) Wait() error { - close(proc.itemChan) - - err := proc.itemGroup.Wait() - - close(proc.pagesChan) - - if err != nil { - return err - } - - return proc.pagesGroup.Wait() -} - -func (proc *pagesProcessor) newPageFromBundle(b *fileinfoBundle) (*pageState, error) { - p, err := proc.newPageFromFi(b.header, nil) - if err != nil { - return nil, err - } - - if len(b.resources) > 0 { - - resources := make(resource.Resources, len(b.resources)) - - for i, rfi := range b.resources { - meta := rfi.Meta() - classifier := meta.Classifier() - var r resource.Resource - switch classifier { - case files.ContentClassContent: - rp, err := proc.newPageFromFi(rfi, p) - if err != nil { - return nil, err - } - rp.m.resourcePath = filepath.ToSlash(strings.TrimPrefix(rp.Path(), p.File().Dir())) - - r = rp - - case files.ContentClassFile: - r, err = proc.newResource(rfi, p) - if err != nil { - return nil, err - } - default: - panic(fmt.Sprintf("invalid classifier: %q", classifier)) - } - - resources[i] = r - - } - - p.addResources(resources...) - } - - return p, nil -} - -func (proc *pagesProcessor) newPageFromFi(fim hugofs.FileMetaInfo, owner *pageState) (*pageState, error) { - fi, err := newFileInfo(proc.sp, fim) - if err != nil { - return nil, err - } - - var s *Site - meta := fim.Meta() - - if owner != nil { - s = owner.s - } else { - lang := meta.Lang() - s = proc.getSite(lang) - } - - r := func() (hugio.ReadSeekCloser, error) { - return meta.Open() - } - - p, err := newPageWithContent(fi, s, owner != nil, r) - if err != nil { - return nil, err - } - p.parent = owner - return p, nil -} - -func (proc *pagesProcessor) newResource(fim hugofs.FileMetaInfo, owner *pageState) (resource.Resource, error) { - - // TODO(bep) consolidate with multihost logic + clean up - outputFormats := owner.m.outputFormats() - seen := make(map[string]bool) - var targetBasePaths []string - // Make sure bundled resources are published to all of the ouptput formats' - // sub paths. - for _, f := range outputFormats { - p := f.Path - if seen[p] { - continue - } - seen[p] = true - targetBasePaths = append(targetBasePaths, p) - - } - - meta := fim.Meta() - r := func() (hugio.ReadSeekCloser, error) { - return meta.Open() - } - - target := strings.TrimPrefix(meta.Path(), owner.File().Dir()) - - return owner.s.ResourceSpec.New( - resources.ResourceSourceDescriptor{ - TargetPaths: owner.getTargetPaths, - OpenReadSeekCloser: r, - FileInfo: fim, - RelTargetFilename: target, - TargetBasePaths: targetBasePaths, - }) -} - -func (proc *pagesProcessor) getSite(lang string) *Site { - if lang == "" { - return proc.h.Sites[0] - } - - for _, s := range proc.h.Sites { - if lang == s.Lang() { - return s - } - } - return proc.h.Sites[0] -} - -func (proc *pagesProcessor) copyFile(fim hugofs.FileMetaInfo) error { - meta := fim.Meta() - s := proc.getSite(meta.Lang()) - f, err := meta.Open() - if err != nil { - return errors.Wrap(err, "copyFile: failed to open") - } - - target := filepath.Join(s.PathSpec.GetTargetLanguageBasePath(), meta.Path()) - - defer f.Close() - - return s.publish(&s.PathSpec.ProcessingStats.Files, target, f) - -} - -func (proc *pagesProcessor) process(item interface{}) error { - send := func(p *pageState, err error) { - if err != nil { - proc.sendError(err) - } else { - proc.pagesChan <- p - } - } - - switch v := item.(type) { - // Page bundles mapped to their language. - case pageBundles: - for _, bundle := range v { - if proc.shouldSkip(bundle.header) { - continue - } - send(proc.newPageFromBundle(bundle)) - } - case hugofs.FileMetaInfo: - if proc.shouldSkip(v) { - return nil - } - meta := v.Meta() - - classifier := meta.Classifier() - switch classifier { - case files.ContentClassContent: - send(proc.newPageFromFi(v, nil)) - case files.ContentClassFile: - proc.sendError(proc.copyFile(v)) - default: - panic(fmt.Sprintf("invalid classifier: %q", classifier)) - } - default: - panic(fmt.Sprintf("unrecognized item type in Process: %T", item)) - } - - return nil -} - -func (proc *pagesProcessor) sendError(err error) { - if err == nil { - return - } - proc.h.SendError(err) -} - -func (proc *pagesProcessor) shouldSkip(fim hugofs.FileMetaInfo) bool { - return proc.sp.DisabledLanguages[fim.Meta().Lang()] -} - -func stringSliceContains(k string, values ...string) bool { - for _, v := range values { - if k == v { - return true - } - } - return false -} diff --git a/hugolib/pages_capture_test.go b/hugolib/pages_capture_test.go deleted file mode 100644 index b5b2bf31c..000000000 --- a/hugolib/pages_capture_test.go +++ /dev/null @@ -1,88 +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" - "path/filepath" - "testing" - - "github.com/pkg/errors" - - "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/source" - - "github.com/gohugoio/hugo/common/loggers" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/hugofs" - "github.com/spf13/afero" -) - -func TestPagesCapture(t *testing.T) { - - cfg, hfs := newTestCfg() - fs := hfs.Source - - c := qt.New(t) - - var writeFile = func(filename string) { - c.Assert(afero.WriteFile(fs, filepath.FromSlash(filename), []byte(fmt.Sprintf("content-%s", filename)), 0755), qt.IsNil) - } - - writeFile("_index.md") - writeFile("logo.png") - writeFile("root.md") - writeFile("blog/index.md") - writeFile("blog/hello.md") - writeFile("blog/images/sunset.png") - writeFile("pages/page1.md") - writeFile("pages/page2.md") - writeFile("pages/page.png") - - ps, err := helpers.NewPathSpec(hugofs.NewFrom(fs, cfg), cfg, loggers.NewErrorLogger()) - c.Assert(err, qt.IsNil) - sourceSpec := source.NewSourceSpec(ps, fs) - - t.Run("Collect", func(t *testing.T) { - c := qt.New(t) - proc := &testPagesCollectorProcessor{} - coll := newPagesCollector(sourceSpec, loggers.NewErrorLogger(), nil, proc) - c.Assert(coll.Collect(), qt.IsNil) - c.Assert(len(proc.items), qt.Equals, 4) - }) - - t.Run("error in Wait", func(t *testing.T) { - c := qt.New(t) - coll := newPagesCollector(sourceSpec, loggers.NewErrorLogger(), nil, - &testPagesCollectorProcessor{waitErr: errors.New("failed")}) - c.Assert(coll.Collect(), qt.Not(qt.IsNil)) - }) -} - -type testPagesCollectorProcessor struct { - items []interface{} - waitErr error -} - -func (proc *testPagesCollectorProcessor) Process(item interface{}) error { - proc.items = append(proc.items, item) - return nil -} -func (proc *testPagesCollectorProcessor) Start(ctx context.Context) context.Context { - return ctx -} - -func (proc *testPagesCollectorProcessor) Wait() error { return proc.waitErr } diff --git a/hugolib/pages_language_merge_test.go b/hugolib/pages_language_merge_test.go deleted file mode 100644 index 7d7181214..000000000 --- a/hugolib/pages_language_merge_test.go +++ /dev/null @@ -1,188 +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 i := 0; i < 2; i++ { - 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().First().Language().Lang, qt.Equals, "en") - - nnBundle := nnSite.getPage("page", "bundle") - enBundle := enSite.getPage("page", "bundle") - - c.Assert(len(enBundle.Resources()), qt.Equals, 6) - c.Assert(len(nnBundle.Resources()), qt.Equals, 2) - - var ri interface{} = 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 }} -{{ if eq .Language.Lang "nn" }}: -{{ $enSite := index .Sites 0 }} -{{ $frSite := index .Sites 1 }} -{{ $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 }} - -`, - "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") -} - -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 := 0; i < 6; i++ { - 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 - - builder := newTestSiteForLanguageMerge(b, count) - 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_map.go b/hugolib/pages_map.go deleted file mode 100644 index c7a74c4c1..000000000 --- a/hugolib/pages_map.go +++ /dev/null @@ -1,456 +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" - "path" - "path/filepath" - "strings" - "sync" - - radix "github.com/armon/go-radix" - "github.com/spf13/cast" - - "github.com/gohugoio/hugo/resources/page" -) - -func newPagesMap(s *Site) *pagesMap { - return &pagesMap{ - r: radix.New(), - s: s, - } -} - -type pagesMap struct { - r *radix.Tree - s *Site -} - -func (m *pagesMap) Get(key string) *pagesMapBucket { - key = m.cleanKey(key) - v, found := m.r.Get(key) - if !found { - return nil - } - - return v.(*pagesMapBucket) -} - -func (m *pagesMap) getKey(p *pageState) string { - if !p.File().IsZero() { - return m.cleanKey(p.File().Dir()) - } - return m.cleanKey(p.SectionsPath()) -} - -func (m *pagesMap) getOrCreateHome() *pageState { - var home *pageState - b, found := m.r.Get("/") - if !found { - home = m.s.newPage(page.KindHome) - m.addBucketFor("/", home, nil) - } else { - home = b.(*pagesMapBucket).owner - } - - return home -} - -func (m *pagesMap) initPageMeta(p *pageState, bucket *pagesMapBucket) error { - var err error - p.metaInit.Do(func() { - if p.metaInitFn != nil { - err = p.metaInitFn(bucket) - } - }) - return err -} - -func (m *pagesMap) initPageMetaFor(prefix string, bucket *pagesMapBucket) error { - parentBucket := m.parentBucket(prefix) - - m.mergeCascades(bucket, parentBucket) - - if err := m.initPageMeta(bucket.owner, bucket); err != nil { - return err - } - - if !bucket.view { - for _, p := range bucket.pages { - ps := p.(*pageState) - if err := m.initPageMeta(ps, bucket); err != nil { - return err - } - - for _, p := range ps.resources.ByType(pageResourceType) { - if err := m.initPageMeta(p.(*pageState), bucket); err != nil { - return err - } - } - } - - // Now that the metadata is initialized (with dates, draft set etc.) - // we can remove the pages that we for some reason should not include - // in this build. - tmp := bucket.pages[:0] - for _, x := range bucket.pages { - if m.s.shouldBuild(x) { - tmp = append(tmp, x) - } - } - bucket.pages = tmp - } - - return nil -} - -func (m *pagesMap) createSectionIfNotExists(section string) { - key := m.cleanKey(section) - _, found := m.r.Get(key) - if !found { - kind := m.s.kindFromSectionPath(section) - p := m.s.newPage(kind, section) - m.addBucketFor(key, p, nil) - } -} - -func (m *pagesMap) addBucket(p *pageState) { - key := m.getKey(p) - - m.addBucketFor(key, p, nil) -} - -func (m *pagesMap) addBucketFor(key string, p *pageState, meta map[string]interface{}) *pagesMapBucket { - var isView bool - switch p.Kind() { - case page.KindTaxonomy, page.KindTaxonomyTerm: - isView = true - } - - disabled := !m.s.isEnabled(p.Kind()) - - bucket := &pagesMapBucket{owner: p, view: isView, meta: meta, disabled: disabled} - p.bucket = bucket - - m.r.Insert(key, bucket) - - return bucket -} - -func (m *pagesMap) addPage(p *pageState) { - if !p.IsPage() { - m.addBucket(p) - return - } - - if !m.s.isEnabled(page.KindPage) { - return - } - - key := m.getKey(p) - - var bucket *pagesMapBucket - - _, v, found := m.r.LongestPrefix(key) - if !found { - panic(fmt.Sprintf("[BUG] bucket with key %q not found", key)) - } - - bucket = v.(*pagesMapBucket) - p.bucket = bucket - - bucket.pages = append(bucket.pages, p) -} - -func (m *pagesMap) assemblePageMeta() error { - var walkErr error - m.r.Walk(func(s string, v interface{}) bool { - bucket := v.(*pagesMapBucket) - - if err := m.initPageMetaFor(s, bucket); err != nil { - walkErr = err - return true - } - return false - }) - - return walkErr -} - -func (m *pagesMap) assembleTaxonomies(s *Site) error { - s.Taxonomies = make(TaxonomyList) - - type bucketKey struct { - plural string - termKey string - } - - // Temporary cache. - taxonomyBuckets := make(map[bucketKey]*pagesMapBucket) - - for singular, plural := range s.siteCfg.taxonomiesConfig { - s.Taxonomies[plural] = make(Taxonomy) - bkey := bucketKey{ - plural: plural, - } - - bucket := m.Get(plural) - - if bucket == nil { - // Create the page and bucket - n := s.newPage(page.KindTaxonomyTerm, plural) - - key := m.cleanKey(plural) - bucket = m.addBucketFor(key, n, nil) - if err := m.initPageMetaFor(key, bucket); err != nil { - return err - } - } - - if bucket.meta == nil { - bucket.meta = map[string]interface{}{ - "singular": singular, - "plural": plural, - } - } - - // Add it to the temporary cache. - taxonomyBuckets[bkey] = bucket - - // Taxonomy entries used in page front matter will be picked up later, - // but there may be some yet to be used. - pluralPrefix := m.cleanKey(plural) + "/" - m.r.WalkPrefix(pluralPrefix, func(k string, v interface{}) bool { - tb := v.(*pagesMapBucket) - termKey := strings.TrimPrefix(k, pluralPrefix) - if tb.meta == nil { - tb.meta = map[string]interface{}{ - "singular": singular, - "plural": plural, - "term": tb.owner.Title(), - "termKey": termKey, - } - } - - bucket.pages = append(bucket.pages, tb.owner) - bkey.termKey = termKey - taxonomyBuckets[bkey] = tb - - return false - }) - - } - - addTaxonomy := func(singular, plural, term string, weight int, p page.Page) error { - bkey := bucketKey{ - plural: plural, - } - - termKey := s.getTaxonomyKey(term) - - b1 := taxonomyBuckets[bkey] - - var b2 *pagesMapBucket - bkey.termKey = termKey - b, found := taxonomyBuckets[bkey] - if found { - b2 = b - } else { - - // Create the page and bucket - n := s.newTaxonomyPage(term, plural, termKey) - meta := map[string]interface{}{ - "singular": singular, - "plural": plural, - "term": term, - "termKey": termKey, - } - - key := m.cleanKey(path.Join(plural, termKey)) - b2 = m.addBucketFor(key, n, meta) - if err := m.initPageMetaFor(key, b2); err != nil { - return err - } - b1.pages = append(b1.pages, b2.owner) - taxonomyBuckets[bkey] = b2 - - } - - w := page.NewWeightedPage(weight, p, b2.owner) - - s.Taxonomies[plural].add(termKey, w) - - b1.owner.m.Dates.UpdateDateAndLastmodIfAfter(p) - b2.owner.m.Dates.UpdateDateAndLastmodIfAfter(p) - - return nil - } - - m.r.Walk(func(k string, v interface{}) bool { - b := v.(*pagesMapBucket) - if b.view { - return false - } - - for singular, plural := range s.siteCfg.taxonomiesConfig { - for _, p := range b.pages { - - vals := getParam(p, plural, false) - - w := getParamToLower(p, plural+"_weight") - weight, err := cast.ToIntE(w) - if err != nil { - m.s.Log.ERROR.Printf("Unable to convert taxonomy weight %#v to int for %q", w, p.Path()) - // weight will equal zero, so let the flow continue - } - - if vals != nil { - if v, ok := vals.([]string); ok { - for _, idx := range v { - if err := addTaxonomy(singular, plural, idx, weight, p); err != nil { - m.s.Log.ERROR.Printf("Failed to add taxonomy %q for %q: %s", plural, p.Path(), err) - } - } - } else if v, ok := vals.(string); ok { - if err := addTaxonomy(singular, plural, v, weight, p); err != nil { - m.s.Log.ERROR.Printf("Failed to add taxonomy %q for %q: %s", plural, p.Path(), err) - } - } else { - m.s.Log.ERROR.Printf("Invalid %s in %q\n", plural, p.Path()) - } - } - - } - } - return false - }) - - for _, plural := range s.siteCfg.taxonomiesConfig { - for k := range s.Taxonomies[plural] { - s.Taxonomies[plural][k].Sort() - } - } - - return nil -} - -func (m *pagesMap) cleanKey(key string) string { - key = filepath.ToSlash(strings.ToLower(key)) - key = strings.Trim(key, "/") - return "/" + key -} - -func (m *pagesMap) mergeCascades(b1, b2 *pagesMapBucket) { - if b1.cascade == nil { - b1.cascade = make(map[string]interface{}) - } - if b2 != nil && b2.cascade != nil { - for k, v := range b2.cascade { - if _, found := b1.cascade[k]; !found { - b1.cascade[k] = v - } - } - } -} - -func (m *pagesMap) parentBucket(prefix string) *pagesMapBucket { - if prefix == "/" { - return nil - } - _, parentv, found := m.r.LongestPrefix(path.Dir(prefix)) - if !found { - panic(fmt.Sprintf("[BUG] parent bucket not found for %q", prefix)) - } - return parentv.(*pagesMapBucket) - -} - -func (m *pagesMap) withEveryPage(f func(p *pageState)) { - m.r.Walk(func(k string, v interface{}) bool { - b := v.(*pagesMapBucket) - f(b.owner) - if !b.view { - for _, p := range b.pages { - f(p.(*pageState)) - } - } - - return false - }) -} - -type pagesMapBucket struct { - // Set if the pages in this bucket is also present in another bucket. - view bool - - // Some additional metatadata attached to this node. - meta map[string]interface{} - - // Cascading front matter. - cascade map[string]interface{} - - owner *pageState // The branch node - - // When disableKinds is enabled for this node. - disabled bool - - // Used to navigate the sections tree - parent *pagesMapBucket - bucketSections []*pagesMapBucket - - pagesInit sync.Once - pages page.Pages - - pagesAndSectionsInit sync.Once - pagesAndSections page.Pages - - sectionsInit sync.Once - sections page.Pages -} - -func (b *pagesMapBucket) isEmpty() bool { - return len(b.pages) == 0 && len(b.bucketSections) == 0 -} - -func (b *pagesMapBucket) getPages() page.Pages { - b.pagesInit.Do(func() { - page.SortByDefault(b.pages) - }) - return b.pages -} - -func (b *pagesMapBucket) getPagesAndSections() page.Pages { - b.pagesAndSectionsInit.Do(func() { - var pas page.Pages - pas = append(pas, b.pages...) - for _, p := range b.bucketSections { - pas = append(pas, p.owner) - } - b.pagesAndSections = pas - page.SortByDefault(b.pagesAndSections) - }) - return b.pagesAndSections -} - -func (b *pagesMapBucket) getSections() page.Pages { - b.sectionsInit.Do(func() { - for _, p := range b.bucketSections { - b.sections = append(b.sections, p.owner) - } - page.SortByDefault(b.sections) - }) - - return b.sections -} diff --git a/hugolib/pages_test.go b/hugolib/pages_test.go deleted file mode 100644 index 6a371b421..000000000 --- a/hugolib/pages_test.go +++ /dev/null @@ -1,83 +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 { - pageTemplate := ` ---- -title: "Page %d" -weight: %d ---- - -` - b := newTestSitesBuilder(t) - - for i := 1; i <= numPages; i++ { - b.WithContent(fmt.Sprintf("page%d.md", i), fmt.Sprintf(pageTemplate, i, rand.Intn(numPages))) - } - - 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{ - Variant{".Next", nil, func(p page.Page, pages page.Pages) { p.Next() }}, - Variant{".Prev", nil, func(p page.Page, pages page.Pages) { p.Prev() }}, - Variant{"Pages.Next", nil, func(p page.Page, pages page.Pages) { pages.Next(p) }}, - Variant{"Pages.Prev", nil, func(p page.Page, pages page.Pages) { pages.Prev(p) }}, - Variant{"Pages.Shuffled.Next", shufflePages, func(p page.Page, pages page.Pages) { pages.Next(p) }}, - Variant{"Pages.Shuffled.Prev", shufflePages, func(p page.Page, pages page.Pages) { pages.Prev(p) }}, - Variant{"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) - } - }) - } - } -} diff --git a/hugolib/paginator_test.go b/hugolib/paginator_test.go deleted file mode 100644 index a97a59d04..000000000 --- a/hugolib/paginator_test.go +++ /dev/null @@ -1,107 +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" - "path/filepath" - "testing" -) - -func TestPaginator(t *testing.T) { - configFile := ` -baseURL = "https://example.com/foo/" -paginate = 3 -paginatepath = "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 := 0; i < 9; i++ { - 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) { - b := newTestSitesBuilder(t).WithSimpleConfigFile() - b.WithTemplatesAdded("index.html", `{{ range (.Paginate (sort .Site.RegularPages ".File.Filename" "desc")).Pages }}|{{ .File.Filename }}{{ end }}`) - b.Build(BuildCfg{}).AssertFileContent("public/index.html", - filepath.FromSlash("|content/sect/doc1.nn.md|content/sect/doc1.nb.md|content/sect/doc1.fr.md|content/sect/doc1.en.md")) -} diff --git a/hugolib/paths/baseURL.go b/hugolib/paths/baseURL.go deleted file mode 100644 index a3c7e9d27..000000000 --- a/hugolib/paths/baseURL.go +++ /dev/null @@ -1,87 +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 ( - "fmt" - "net/url" - "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 - urlStr string -} - -func (b BaseURL) String() string { - if b.urlStr != "" { - return b.urlStr - } - return b.url.String() -} - -func (b BaseURL) Path() string { - return b.url.Path -} - -// 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) (string, 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 "", fmt.Errorf("cannot determine BaseURL for protocol %q", protocol) - } - - return u.String(), nil -} - -// 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) { - var result BaseURL - - base, err := url.Parse(b) - if err != nil { - return result, err - } - - return BaseURL{url: base, urlStr: base.String()}, nil -} diff --git a/hugolib/paths/baseURL_test.go b/hugolib/paths/baseURL_test.go deleted file mode 100644 index 77095bb7d..000000000 --- a/hugolib/paths/baseURL_test.go +++ /dev/null @@ -1,67 +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 ( - "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") - - p, err := b.WithProtocol("webcal://") - c.Assert(err, qt.IsNil) - c.Assert(p, qt.Equals, "webcal://example.com") - - p, err = b.WithProtocol("webcal") - c.Assert(err, qt.IsNil) - c.Assert(p, 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, qt.Equals, "webcal:hugo@rules.com") - - p, err = b.WithProtocol("webcal://") - c.Assert(err, qt.IsNil) - c.Assert(p, 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.HostURL(), qt.Equals, "http://example.com") -} diff --git a/hugolib/paths/paths.go b/hugolib/paths/paths.go deleted file mode 100644 index 97d4f17ba..000000000 --- a/hugolib/paths/paths.go +++ /dev/null @@ -1,281 +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 ( - "fmt" - "path/filepath" - "strings" - - "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/langs" - "github.com/gohugoio/hugo/modules" - "github.com/pkg/errors" - - "github.com/gohugoio/hugo/hugofs" -) - -var FilePathSeparator = string(filepath.Separator) - -type Paths struct { - Fs *hugofs.Fs - Cfg config.Provider - - BaseURL - - // If the baseURL contains a base path, e.g. https://example.com/docs, then "/docs" will be the BasePath. - BasePath string - - // Directories - // TODO(bep) when we have trimmed down mos of the dirs usage outside of this package, make - // these into an interface. - ThemesDir string - WorkingDir string - - // Directories to store Resource related artifacts. - AbsResourcesDir string - - AbsPublishDir string - - // pagination path handling - PaginatePath string - - PublishDir string - - // When in multihost mode, this returns a list of base paths below PublishDir - // for each language. - MultihostTargetBasePaths []string - - DisablePathToLower bool - RemovePathAccents bool - UglyURLs bool - CanonifyURLs bool - - Language *langs.Language - Languages langs.Languages - LanguagesDefaultFirst langs.Languages - - // The PathSpec looks up its config settings in both the current language - // and then in the global Viper config. - // Some settings, the settings listed below, does not make sense to be set - // on per-language-basis. We have no good way of protecting against this - // other than a "white-list". See language.go. - defaultContentLanguageInSubdir bool - DefaultContentLanguage string - multilingual bool - - AllModules modules.Modules - ModulesClient *modules.Client -} - -func New(fs *hugofs.Fs, cfg config.Provider) (*Paths, error) { - baseURLstr := cfg.GetString("baseURL") - baseURL, err := newBaseURLFromString(baseURLstr) - - if err != nil { - return nil, errors.Wrapf(err, "Failed to create baseURL from %q:", baseURLstr) - } - - contentDir := filepath.Clean(cfg.GetString("contentDir")) - workingDir := filepath.Clean(cfg.GetString("workingDir")) - resourceDir := filepath.Clean(cfg.GetString("resourceDir")) - publishDir := filepath.Clean(cfg.GetString("publishDir")) - - if publishDir == "" { - return nil, fmt.Errorf("publishDir not set") - } - - defaultContentLanguage := cfg.GetString("defaultContentLanguage") - - var ( - language *langs.Language - languages langs.Languages - languagesDefaultFirst langs.Languages - ) - - if l, ok := cfg.(*langs.Language); ok { - language = l - - } - - if l, ok := cfg.Get("languagesSorted").(langs.Languages); ok { - languages = l - } - - if l, ok := cfg.Get("languagesSortedDefaultFirst").(langs.Languages); ok { - languagesDefaultFirst = l - } - - // - - if len(languages) == 0 { - // We have some old tests that does not test the entire chain, hence - // they have no languages. So create one so we get the proper filesystem. - languages = langs.Languages{&langs.Language{Lang: "en", Cfg: cfg, ContentDir: contentDir}} - } - - absPublishDir := AbsPathify(workingDir, publishDir) - if !strings.HasSuffix(absPublishDir, FilePathSeparator) { - absPublishDir += FilePathSeparator - } - // If root, remove the second '/' - if absPublishDir == "//" { - absPublishDir = FilePathSeparator - } - absResourcesDir := AbsPathify(workingDir, resourceDir) - if !strings.HasSuffix(absResourcesDir, FilePathSeparator) { - absResourcesDir += FilePathSeparator - } - if absResourcesDir == "//" { - absResourcesDir = FilePathSeparator - } - - var multihostTargetBasePaths []string - if languages.IsMultihost() { - for _, l := range languages { - multihostTargetBasePaths = append(multihostTargetBasePaths, l.Lang) - } - } - - p := &Paths{ - Fs: fs, - Cfg: cfg, - BaseURL: baseURL, - - DisablePathToLower: cfg.GetBool("disablePathToLower"), - RemovePathAccents: cfg.GetBool("removePathAccents"), - UglyURLs: cfg.GetBool("uglyURLs"), - CanonifyURLs: cfg.GetBool("canonifyURLs"), - - ThemesDir: cfg.GetString("themesDir"), - WorkingDir: workingDir, - - AbsResourcesDir: absResourcesDir, - AbsPublishDir: absPublishDir, - - multilingual: cfg.GetBool("multilingual"), - defaultContentLanguageInSubdir: cfg.GetBool("defaultContentLanguageInSubdir"), - DefaultContentLanguage: defaultContentLanguage, - - Language: language, - Languages: languages, - LanguagesDefaultFirst: languagesDefaultFirst, - MultihostTargetBasePaths: multihostTargetBasePaths, - - PaginatePath: cfg.GetString("paginatePath"), - } - - if cfg.IsSet("allModules") { - p.AllModules = cfg.Get("allModules").(modules.Modules) - } - - if cfg.IsSet("modulesClient") { - p.ModulesClient = cfg.Get("modulesClient").(*modules.Client) - } - - // TODO(bep) remove this, eventually - p.PublishDir = absPublishDir - - return p, nil -} - -// GetBasePath returns any path element in baseURL if needed. -func (p *Paths) GetBasePath(isRelativeURL bool) string { - if isRelativeURL && p.CanonifyURLs { - // The baseURL will be prepended later. - return "" - } - return p.BasePath -} - -func (p *Paths) Lang() string { - if p == nil || p.Language == nil { - return "" - } - return p.Language.Lang -} - -func (p *Paths) GetTargetLanguageBasePath() string { - if p.Languages.IsMultihost() { - // In a multihost configuration all assets will be published below the language code. - return p.Lang() - } - return p.GetLanguagePrefix() -} - -func (p *Paths) GetURLLanguageBasePath() string { - if p.Languages.IsMultihost() { - return "" - } - return p.GetLanguagePrefix() -} - -func (p *Paths) GetLanguagePrefix() string { - if !p.multilingual { - return "" - } - - defaultLang := p.DefaultContentLanguage - defaultInSubDir := p.defaultContentLanguageInSubdir - - currentLang := p.Language.Lang - if currentLang == "" || (currentLang == defaultLang && !defaultInSubDir) { - return "" - } - return currentLang -} - -// GetLangSubDir returns the given language's subdir if needed. -func (p *Paths) GetLangSubDir(lang string) string { - if !p.multilingual { - return "" - } - - if p.Languages.IsMultihost() { - return "" - } - - if lang == "" || (lang == p.DefaultContentLanguage && !p.defaultContentLanguageInSubdir) { - return "" - } - - return lang -} - -// 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 AbsPathify(p.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.WorkingDir), FilePathSeparator) - -} - -// AbsPathify creates an absolute path if given a working dir and arelative 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) -} diff --git a/hugolib/paths/paths_test.go b/hugolib/paths/paths_test.go deleted file mode 100644 index 59dbf0e00..000000000 --- a/hugolib/paths/paths_test.go +++ /dev/null @@ -1,51 +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 ( - "testing" - - "github.com/gohugoio/hugo/langs" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/hugofs" - "github.com/spf13/viper" -) - -func TestNewPaths(t *testing.T) { - c := qt.New(t) - - v := viper.New() - fs := hugofs.NewMem(v) - - v.Set("languages", map[string]interface{}{ - "no": map[string]interface{}{}, - "en": map[string]interface{}{}, - }) - v.Set("defaultContentLanguageInSubdir", true) - v.Set("defaultContentLanguage", "no") - v.Set("contentDir", "content") - v.Set("workingDir", "work") - v.Set("resourceDir", "resources") - v.Set("publishDir", "public") - - langs.LoadLanguageSettings(v, nil) - - p, err := New(fs, v) - c.Assert(err, qt.IsNil) - - c.Assert(p.defaultContentLanguageInSubdir, qt.Equals, true) - c.Assert(p.DefaultContentLanguage, qt.Equals, "no") - c.Assert(p.multilingual, qt.Equals, true) -} diff --git a/hugolib/permalinker.go b/hugolib/permalinker.go deleted file mode 100644 index 29dad6ce4..000000000 --- a/hugolib/permalinker.go +++ /dev/null @@ -1,24 +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 - -var ( - _ Permalinker = (*pageState)(nil) -) - -// Permalinker provides permalinks of both the relative and absolute kind. -type Permalinker interface { - Permalink() string - RelPermalink() string -} diff --git a/hugolib/prune_resources.go b/hugolib/prune_resources.go deleted file mode 100644 index bf5a1ef2f..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.FileCaches.Prune() -} diff --git a/hugolib/resource_chain_test.go b/hugolib/resource_chain_test.go deleted file mode 100644 index 10539d1bb..000000000 --- a/hugolib/resource_chain_test.go +++ /dev/null @@ -1,670 +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 ( - "io" - "os" - "path/filepath" - "testing" - - "github.com/gohugoio/hugo/htesting" - - "github.com/spf13/viper" - - qt "github.com/frankban/quicktest" - - "github.com/gohugoio/hugo/hugofs" - - "github.com/gohugoio/hugo/common/loggers" - "github.com/gohugoio/hugo/resources/resource_transformers/tocss/scss" -) - -func TestSCSSWithIncludePaths(t *testing.T) { - if !scss.Supports() { - t.Skip("Skip SCSS") - } - c := qt.New(t) - workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-scss-include") - c.Assert(err, qt.IsNil) - defer clean() - - v := viper.New() - v.Set("workingDir", workDir) - b := newTestSitesBuilder(t).WithLogger(loggers.NewErrorLogger()) - // Need to use OS fs for this. - b.Fs = hugofs.NewDefault(v) - b.WithWorkingDir(workDir) - b.WithViper(v) - - fooDir := filepath.Join(workDir, "node_modules", "foo") - scssDir := filepath.Join(workDir, "assets", "scss") - c.Assert(os.MkdirAll(fooDir, 0777), qt.IsNil) - c.Assert(os.MkdirAll(filepath.Join(workDir, "content", "sect"), 0777), qt.IsNil) - c.Assert(os.MkdirAll(filepath.Join(workDir, "data"), 0777), qt.IsNil) - c.Assert(os.MkdirAll(filepath.Join(workDir, "i18n"), 0777), qt.IsNil) - c.Assert(os.MkdirAll(filepath.Join(workDir, "layouts", "shortcodes"), 0777), qt.IsNil) - c.Assert(os.MkdirAll(filepath.Join(workDir, "layouts", "_default"), 0777), qt.IsNil) - c.Assert(os.MkdirAll(filepath.Join(scssDir), 0777), qt.IsNil) - - b.WithSourceFile(filepath.Join(fooDir, "_moo.scss"), ` -$moolor: #fff; - -moo { - color: $moolor; -} -`) - - b.WithSourceFile(filepath.Join(scssDir, "main.scss"), ` -@import "moo"; - -`) - - b.WithTemplatesAdded("index.html", ` -{{ $cssOpts := (dict "includePaths" (slice "node_modules/foo" ) ) }} -{{ $r := resources.Get "scss/main.scss" | toCSS $cssOpts | minify }} -T1: {{ $r.Content }} -`) - b.Build(BuildCfg{}) - - b.AssertFileContent(filepath.Join(workDir, "public/index.html"), `T1: moo{color:#fff}`) - -} - -func TestSCSSWithThemeOverrides(t *testing.T) { - if !scss.Supports() { - t.Skip("Skip SCSS") - } - c := qt.New(t) - workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-scss-include") - c.Assert(err, qt.IsNil) - defer clean() - - theme := "mytheme" - themesDir := filepath.Join(workDir, "themes") - themeDirs := filepath.Join(themesDir, theme) - v := viper.New() - v.Set("workingDir", workDir) - v.Set("theme", theme) - b := newTestSitesBuilder(t).WithLogger(loggers.NewErrorLogger()) - // Need to use OS fs for this. - b.Fs = hugofs.NewDefault(v) - b.WithWorkingDir(workDir) - b.WithViper(v) - - fooDir := filepath.Join(workDir, "node_modules", "foo") - scssDir := filepath.Join(workDir, "assets", "scss") - scssThemeDir := filepath.Join(themeDirs, "assets", "scss") - c.Assert(os.MkdirAll(fooDir, 0777), qt.IsNil) - c.Assert(os.MkdirAll(filepath.Join(workDir, "content", "sect"), 0777), qt.IsNil) - c.Assert(os.MkdirAll(filepath.Join(workDir, "data"), 0777), qt.IsNil) - c.Assert(os.MkdirAll(filepath.Join(workDir, "i18n"), 0777), qt.IsNil) - c.Assert(os.MkdirAll(filepath.Join(workDir, "layouts", "shortcodes"), 0777), qt.IsNil) - c.Assert(os.MkdirAll(filepath.Join(workDir, "layouts", "_default"), 0777), qt.IsNil) - c.Assert(os.MkdirAll(filepath.Join(scssDir, "components"), 0777), qt.IsNil) - c.Assert(os.MkdirAll(filepath.Join(scssThemeDir, "components"), 0777), qt.IsNil) - - b.WithSourceFile(filepath.Join(scssThemeDir, "components", "_imports.scss"), ` -@import "moo"; -@import "_boo"; -`) - - b.WithSourceFile(filepath.Join(scssThemeDir, "components", "_moo.scss"), ` -$moolor: #fff; - -moo { - color: $moolor; -} -`) - - b.WithSourceFile(filepath.Join(scssThemeDir, "components", "_boo.scss"), ` -$boolor: orange; - -boo { - color: $boolor; -} -`) - - b.WithSourceFile(filepath.Join(scssThemeDir, "main.scss"), ` -@import "components/imports"; - -`) - - b.WithSourceFile(filepath.Join(scssDir, "components", "_moo.scss"), ` -$moolor: #ccc; - -moo { - color: $moolor; -} -`) - - b.WithSourceFile(filepath.Join(scssDir, "components", "_boo.scss"), ` -$boolor: green; - -boo { - color: $boolor; -} -`) - - b.WithTemplatesAdded("index.html", ` -{{ $cssOpts := (dict "includePaths" (slice "node_modules/foo" ) ) }} -{{ $r := resources.Get "scss/main.scss" | toCSS $cssOpts | minify }} -T1: {{ $r.Content }} -`) - b.Build(BuildCfg{}) - - b.AssertFileContent(filepath.Join(workDir, "public/index.html"), `T1: moo{color:#ccc}boo{color:green}`) - -} - -// https://github.com/gohugoio/hugo/issues/6274 -func TestSCSSWithIncludePathsSass(t *testing.T) { - if !scss.Supports() { - t.Skip("Skip SCSS") - } - c := qt.New(t) - workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-scss-includepaths") - c.Assert(err, qt.IsNil) - defer clean() - - v := viper.New() - v.Set("workingDir", workDir) - v.Set("theme", "mytheme") - b := newTestSitesBuilder(t).WithLogger(loggers.NewErrorLogger()) - // Need to use OS fs for this. - b.Fs = hugofs.NewDefault(v) - b.WithWorkingDir(workDir) - b.WithViper(v) - - hulmaDir := filepath.Join(workDir, "node_modules", "hulma") - scssDir := filepath.Join(workDir, "themes/mytheme/assets", "scss") - c.Assert(os.MkdirAll(hulmaDir, 0777), qt.IsNil) - c.Assert(os.MkdirAll(scssDir, 0777), qt.IsNil) - - b.WithSourceFile(filepath.Join(scssDir, "main.scss"), ` -@import "hulma/hulma"; - -`) - - b.WithSourceFile(filepath.Join(hulmaDir, "hulma.sass"), ` -$hulma: #ccc; - -foo - color: $hulma; - -`) - - b.WithTemplatesAdded("index.html", ` - {{ $scssOptions := (dict "targetPath" "css/styles.css" "enableSourceMap" false "includePaths" (slice "node_modules")) }} -{{ $r := resources.Get "scss/main.scss" | toCSS $scssOptions | minify }} -T1: {{ $r.Content }} -`) - b.Build(BuildCfg{}) - - b.AssertFileContent(filepath.Join(workDir, "public/index.html"), `T1: foo{color:#ccc}`) - -} - -func TestResourceChainBasic(t *testing.T) { - t.Parallel() - - b := newTestSitesBuilder(t) - b.WithTemplatesAdded("index.html", ` -{{ $hello := "

    Hello World!

    " | 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 }} - -`) - - fs := b.Fs.Source - - imageDir := filepath.Join("assets", "images") - b.Assert(os.MkdirAll(imageDir, 0777), 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 := 0; i < 2; i++ { - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", - ` -SUNSET: images/sunset.jpg|/images/sunset.a9bf1d944e19c0f382e0d8f51de690f7d0bc8fa97390c4242a86c3e5c0737e71.jpg|900|90587 -FIT: images/sunset.jpg|/images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_200x200_fit_q75_box.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= -`) - - b.AssertFileContent("public/styles.min.a1df58687c3c9cc38bf26532f7b4b2f2c2b0315dcde212376959995c04f11fef.css", "body{background-color:#add8e6}") - b.AssertFileContent("public//styles2.min.1cfc52986836405d37f9998a63fd6dd8608e8c410e5e3db1daaa30f78bc273ba.css", "body{background-color:orange}") - - b.EditFiles("page1.md", ` ---- -title: "Page 1 edit" -summary: "Edited summary" ---- - -Edited content. - -`) - - b.Assert(b.Fs.Destination.Remove("public"), qt.IsNil) - b.H.ResourceSpec.ClearCaches() - - } -} - -func TestResourceChains(t *testing.T) { - t.Parallel() - - c := qt.New(t) - - 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.WithTemplates("home.html", ` -Min CSS: {{ ( resources.Get "css/styles1.css" | minify ).Content }} -Min JS: {{ ( resources.Get "js/script1.js" | resources.Minify ).Content | safeJS }} -Min JSON: {{ ( resources.Get "mydata/json1.json" | resources.Minify ).Content | safeHTML }} -Min XML: {{ ( resources.Get "mydata/xml1.xml" | resources.Minify ).Content | safeHTML }} -Min SVG: {{ ( resources.Get "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 }} - - -`) - }, func(b *sitesBuilder) { - b.AssertFileContent("public/index.html", `Min CSS: h1{font-style:bold}`) - b.AssertFileContent("public/index.html", `Min JS: var x;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 XML: Hugo Rocks!`) - b.AssertFileContent("public/index.html", `Min SVG: `) - b.AssertFileContent("public/index.html", `Min SVG again: `) - b.AssertFileContent("public/index.html", `Min HTML: Cool`) - }}, - - {"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 { - // TODO(bep) eventually remove - return isGo111() - }, 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" ";") }} - -Slogan: {{ $toml.slogan }} -CSV1: {{ $csv1 }} {{ len (index $csv1 0) }} -CSV2: {{ $csv2 }} -`) - }, func(b *sitesBuilder) { - b.AssertFileContent("public/index.html", - `Slogan: Hugo Rocks!`, - `[[Hugo Rocks Hugo is Fast!]] 2`, - `CSV2: [[a b c]]`, - ) - }}, - {"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.NewErrorLogger()) - 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"), ` - - - -`) - - b.WithSourceFile(filepath.Join("assets", "mydata", "xml1.xml"), ` - -Hugo Rocks! - -`) - - b.WithSourceFile(filepath.Join("assets", "mydata", "html1.html"), ` - - -Cool - - -`) - - 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 TestMultiSiteResource(t *testing.T) { - t.Parallel() - c := qt.New(t) - - b := newMultiSiteTestDefaultBuilder(t) - - b.CreateSites().Build(BuildCfg{}) - - // This build is multilingual, but not multihost. There should be only one pipes.txt - b.AssertFileContent("public/fr/index.html", "French Home Page", "String Resource: /blog/text/pipes.txt") - c.Assert(b.CheckExists("public/fr/text/pipes.txt"), qt.Equals, false) - c.Assert(b.CheckExists("public/en/text/pipes.txt"), qt.Equals, false) - b.AssertFileContent("public/en/index.html", "Default Home Page", "String Resource: /blog/text/pipes.txt") - b.AssertFileContent("public/text/pipes.txt", "Hugo Pipes") - -} - -func TestResourcesMatch(t *testing.T) { - t.Parallel() - - b := newTestSitesBuilder(t) - - b.WithContent("page.md", "") - - b.WithSourceFile( - "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 "JSONS: %d" (len $jsons) }} -JSON: {{ $json.RelPermalink }}: {{ $json.Content }} -{{ range $jsons }} -{{- .RelPermalink }}: {{ .Content }} -{{ end }} -`) - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/index.html", - "JSON: /jsons/data1.json: json1 content", - "JSONS: 2", "/jsons/data1.json: json1 content") -} diff --git a/hugolib/robotstxt_test.go b/hugolib/robotstxt_test.go deleted file mode 100644 index e924cb8dc..000000000 --- a/hugolib/robotstxt_test.go +++ /dev/null @@ -1,42 +0,0 @@ -// 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 ( - "testing" - - "github.com/spf13/viper" -) - -const robotTxtTemplate = `User-agent: Googlebot - {{ range .Data.Pages }} - Disallow: {{.RelPermalink}} - {{ end }} -` - -func TestRobotsTXTOutput(t *testing.T) { - t.Parallel() - - cfg := viper.New() - cfg.Set("baseURL", "http://auth/bub/") - cfg.Set("enableRobotsTXT", true) - - b := newTestSitesBuilder(t).WithViper(cfg) - b.WithTemplatesAdded("layouts/robots.txt", robotTxtTemplate) - - b.Build(BuildCfg{}) - - b.AssertFileContent("public/robots.txt", "User-agent: Googlebot") - -} diff --git a/hugolib/rss_test.go b/hugolib/rss_test.go deleted file mode 100644 index 634843e3d..000000000 --- a/hugolib/rss_test.go +++ /dev/null @@ -1,100 +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 ( - "path/filepath" - "strings" - "testing" - - "github.com/gohugoio/hugo/deps" -) - -func TestRSSOutput(t *testing.T) { - t.Parallel() - var ( - cfg, fs = newTestCfg() - th = newTestHelper(cfg, fs, t) - ) - - rssLimit := len(weightedSources) - 1 - - rssURI := "index.xml" - - cfg.Set("baseURL", "http://auth/bub/") - cfg.Set("title", "RSSTest") - cfg.Set("rssLimit", rssLimit) - - for _, src := range weightedSources { - writeSource(t, fs, filepath.Join("content", "sect", src[0]), src[1]) - } - - buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) - - // Home RSS - th.assertFileContent(filepath.Join("public", rssURI), "") - if c != rssLimit { - t.Errorf("incorrect RSS item count: expected %d, got %d", rssLimit, c) - } - - // Encoded summary - th.assertFileContent(filepath.Join("public", rssURI), "{{ range .Pages }}{{ .Content | html }}{{ end }}`) - 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") -} diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go deleted file mode 100644 index d0cdf3950..000000000 --- a/hugolib/shortcode.go +++ /dev/null @@ -1,628 +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" - "fmt" - "strconv" - - "html/template" - "path" - - "github.com/gohugoio/hugo/common/herrors" - "github.com/pkg/errors" - - "reflect" - - "regexp" - "sort" - - "github.com/gohugoio/hugo/parser/pageparser" - "github.com/gohugoio/hugo/resources/page" - - _errors "github.com/pkg/errors" - - "strings" - "sync" - - "github.com/gohugoio/hugo/common/maps" - "github.com/gohugoio/hugo/common/text" - "github.com/gohugoio/hugo/common/urls" - "github.com/gohugoio/hugo/output" - - bp "github.com/gohugoio/hugo/bufferpool" - "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/tpl" -) - -var ( - _ urls.RefLinker = (*ShortcodeWithPage)(nil) - _ pageWrapper = (*ShortcodeWithPage)(nil) - _ text.Positioner = (*ShortcodeWithPage)(nil) -) - -// ShortcodeWithPage is the "." context in a shortcode template. -type ShortcodeWithPage struct { - Params interface{} - Inner template.HTML - 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 - - // pos is the position in bytes in the source file. Used for error logging. - posInit sync.Once - posOffset int - pos text.Position - - scratch *maps.Scratch -} - -// 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 -} - -// Site returns information about the current site. -func (scp *ShortcodeWithPage) Site() page.Site { - 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]interface{}) (string, error) { - return scp.Page.RefFrom(args, scp) -} - -// 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]interface{}) (string, error) { - return scp.Page.RelRefFrom(args, scp) -} - -// Scratch returns a scratch-pad scoped for this shortcode. This can be used -// as a temporary storage for variables, counters etc. -func (scp *ShortcodeWithPage) Scratch() *maps.Scratch { - if scp.scratch == nil { - scp.scratch = maps.NewScratch() - } - return scp.scratch -} - -// Get is a convenience method to look up shortcode parameters by its key. -func (scp *ShortcodeWithPage) Get(key interface{}) interface{} { - if scp.Params == nil { - return nil - } - if reflect.ValueOf(scp.Params).Len() == 0 { - return nil - } - - var x reflect.Value - - 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 - } 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 "" - } - x = reflect.ValueOf(scp.Params).Index(idx) - } - case string: - if reflect.TypeOf(scp.Params).Kind() == reflect.Map { - x = reflect.ValueOf(scp.Params).MapIndex(reflect.ValueOf(key)) - if !x.IsValid() { - 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 - } - } - - return x.Interface() - -} - -func (scp *ShortcodeWithPage) page() page.Page { - return scp.Page -} - -// Note - this value must not contain any markup syntax -const shortcodePlaceholderPrefix = "HAHAHUGOSHORTCODE" - -func createShortcodePlaceholder(id string, ordinal int) string { - return shortcodePlaceholderPrefix + "-" + id + strconv.Itoa(ordinal) + "-HBHB" -} - -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 []interface{} // string or nested shortcode - params interface{} // map or array - ordinal int - err error - - info tpl.Info - - // If set, the rendered shortcode is sent as part of the surrounding content - // to Blackfriday 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 behaviour can be had by starting your shortcode template with: - // {{ $_hugo_config := `{ "version": 1 }`}} - doMarkup bool - - // the placeholder in the source when passed to Blackfriday 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.info.Config.Version == 1 -} - -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 interface{} - switch v := sc.params.(type) { - case map[string]interface{}: - // sort the keys so test assertions won't fail - var keys []string - for k := range v { - keys = append(keys, k) - } - sort.Strings(keys) - var tmp = make(map[string]interface{}) - - for _, k := range keys { - tmp[k] = v[k] - } - params = tmp - - default: - // use it as is - params = sc.params - } - - return fmt.Sprintf("%s(%q, %t){%s}", sc.name, params, sc.doMarkup, sc.inner) -} - -type shortcodeHandler struct { - p *pageState - - s *Site - - // Ordered list of shortcodes for a page. - shortcodes []*shortcode - - // All the shortcode names in this set. - nameSet map[string]bool - - // Configuration - enableInlineShortcodes bool -} - -func newShortcodeHandler(p *pageState, s *Site, placeholderFunc func() string) *shortcodeHandler { - - sh := &shortcodeHandler{ - p: p, - s: s, - enableInlineShortcodes: s.enableInlineShortcodes, - shortcodes: make([]*shortcode, 0, 4), - nameSet: make(map[string]bool), - } - - return sh -} - -const ( - innerNewlineRegexp = "\n" - innerCleanupRegexp = `\A

    (.*)

    \n\z` - innerCleanupExpand = "$1" -) - -func renderShortcode( - level int, - s *Site, - tplVariants tpl.TemplateVariants, - sc *shortcode, - parent *ShortcodeWithPage, - p *pageState) (string, bool, error) { - - var tmpl tpl.Template - - // 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, so we may get some false positives -- we - // should improve on that. - var hasVariants bool - - if sc.isInline { - if !p.s.enableInlineShortcodes { - return "", false, nil - } - templName := path.Join("_inline_shortcode", p.File().Path(), sc.name) - if sc.isClosing { - templStr := sc.innerString() - - var err error - tmpl, err = s.TextTmpl.Parse(templName, templStr) - if err != nil { - fe := herrors.ToFileError("html", err) - l1, l2 := p.posOffset(sc.pos).LineNumber, fe.Position().LineNumber - fe = herrors.ToFileErrorWithLineNumber(fe, l1+l2-1) - return "", false, p.wrapError(fe) - } - - } else { - // Re-use of shortcode defined earlier in the same page. - var found bool - tmpl, found = s.TextTmpl.Lookup(templName) - if !found { - return "", false, _errors.Errorf("no earlier definition of shortcode %q found", sc.name) - } - } - } else { - var found, more bool - tmpl, found, more = s.Tmpl.LookupVariant(sc.name, tplVariants) - if !found { - s.Log.ERROR.Printf("Unable to locate template for shortcode %q in page %q", sc.name, p.File().Path()) - return "", false, nil - } - hasVariants = hasVariants || more - } - - data := &ShortcodeWithPage{Ordinal: sc.ordinal, posOffset: sc.pos, Params: sc.params, Page: newPageForShortcode(p), Parent: parent, Name: sc.name} - if sc.params != nil { - data.IsNamedParams = reflect.TypeOf(sc.params).Kind() == reflect.Map - } - - if len(sc.inner) > 0 { - var inner string - for _, innerData := range sc.inner { - switch innerData := innerData.(type) { - case string: - inner += innerData - case *shortcode: - s, more, err := renderShortcode(level+1, s, tplVariants, innerData, data, p) - if err != nil { - return "", false, err - } - hasVariants = hasVariants || more - inner += s - default: - s.Log.ERROR.Printf("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 "", false, nil - } - } - - // Pre Hugo 0.55 this was the behaviour even for the outer-most - // shortcode. - if sc.doMarkup && (level > 0 || sc.info.Config.Version == 1) { - newInner := s.ContentSpec.RenderBytes(&helpers.RenderingContext{ - Content: []byte(inner), - PageFmt: p.m.markup, - Cfg: p.Language(), - DocumentID: p.File().UniqueID(), - DocumentName: p.File().Path(), - Config: p.getRenderingConfig()}) - - // If the type is “” (unknown) or “markdown”, we assume the markdown - // generation has been performed. Given the input: `a line`, markdown - // specifies the HTML `

    a line

    \n`. When dealing with documents as a - // whole, this is OK. When dealing with an `{{ .Inner }}` block in Hugo, - // this is not so good. This code does two things: - // - // 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

    block and - // the newline. - switch p.m.markup { - case "", "markdown": - if match, _ := regexp.MatchString(innerNewlineRegexp, inner); !match { - cleaner, err := regexp.Compile(innerCleanupRegexp) - - if err == nil { - newInner = cleaner.ReplaceAll(newInner, []byte(innerCleanupExpand)) - } - } - } - - // TODO(bep) we may have plain text inner templates. - data.Inner = template.HTML(newInner) - } else { - data.Inner = template.HTML(inner) - } - - } - - result, err := renderShortcodeWithPage(tmpl, data) - - if err != nil && sc.isInline { - fe := herrors.ToFileError("html", err) - l1, l2 := p.posFromPage(sc.pos).LineNumber, fe.Position().LineNumber - fe = herrors.ToFileErrorWithLineNumber(fe, l1+l2-1) - return "", false, fe - } - - return result, hasVariants, err -} - -func (s *shortcodeHandler) hasShortcodes() bool { - return len(s.shortcodes) > 0 -} - -func (s *shortcodeHandler) renderShortcodesForPage(p *pageState, f output.Format) (map[string]string, bool, error) { - - rendered := make(map[string]string) - - tplVariants := tpl.TemplateVariants{ - Language: p.Language().Lang, - OutputFormat: f, - } - - var hasVariants bool - - for _, v := range s.shortcodes { - s, more, err := renderShortcode(0, s.s, tplVariants, v, nil, p) - if err != nil { - err = p.parseError(_errors.Wrapf(err, "failed to render shortcode %q", v.name), p.source.parsed.Input(), v.pos) - return nil, false, err - } - hasVariants = hasVariants || more - rendered[v.placeholder] = s - - } - - return rendered, hasVariants, nil -} - -var errShortCodeIllegalState = errors.New("Illegal shortcode state") - -func (s *shortcodeHandler) parseError(err error, input []byte, pos int) error { - if s.p != nil { - return s.p.parseError(err, input, pos) - } - return err -} - -// 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, pt *pageparser.Iterator) (*shortcode, error) { - if s == nil { - panic("handler nil") - } - sc := &shortcode{ordinal: ordinal} - - var cnt = 0 - var nestedOrdinal = 0 - var nextLevel = level + 1 - - fail := func(err error, i pageparser.Item) error { - return s.parseError(err, pt.Input(), i.Pos) - } - -Loop: - for { - currItem := pt.Next() - switch { - case currItem.IsLeftShortcodeDelim(): - next := pt.Peek() - if next.IsShortcodeClose() { - continue - } - - if cnt > 0 { - // nested shortcode; append it to inner content - pt.Backup() - nested, err := s.extractShortcode(nestedOrdinal, nextLevel, pt) - nestedOrdinal++ - if nested != nil && nested.name != "" { - s.nameSet[nested.name] = true - } - - if err == nil { - sc.inner = append(sc.inner, nested) - } else { - return sc, err - } - - } else { - sc.doMarkup = currItem.IsShortcodeMarkupDelimiter() - } - - cnt++ - - case currItem.IsRightShortcodeDelim(): - // we trust the template on this: - // if there's no inner, we're done - if !sc.isInline && !sc.info.IsInner { - return sc, nil - } - - case currItem.IsShortcodeClose(): - next := pt.Peek() - if !sc.isInline && !sc.info.IsInner { - if next.IsError() { - // return that error, more specific - continue - } - - return sc, fail(_errors.Errorf("shortcode %q has no .Inner, yet a closing tag was provided", next.Val), next) - } - if next.IsRightShortcodeDelim() { - // self-closing - pt.Consume(1) - } else { - sc.isClosing = true - pt.Consume(2) - } - - return sc, nil - case currItem.IsText(): - sc.inner = append(sc.inner, currItem.ValStr()) - case currItem.IsShortcodeName(): - - sc.name = currItem.ValStr() - - // Check if the template expects inner content. - // We pick the first template for an arbitrary output format - // if more than one. It is "all inner or no inner". - tmpl, found, _ := s.s.Tmpl.LookupVariant(sc.name, tpl.TemplateVariants{}) - if !found { - return nil, _errors.Errorf("template for shortcode %q not found", sc.name) - } - - sc.info = tmpl.(tpl.TemplateInfoProvider).TemplateInfo() - case currItem.IsInlineShortcodeName(): - sc.name = currItem.ValStr() - sc.isInline = true - case currItem.IsShortcodeParam(): - if !pt.IsValueNext() { - continue - } else if pt.Peek().IsShortcodeParamVal() { - // named params - if sc.params == nil { - params := make(map[string]interface{}) - params[currItem.ValStr()] = pt.Next().ValTyped() - sc.params = params - } else { - if params, ok := sc.params.(map[string]interface{}); ok { - params[currItem.ValStr()] = pt.Next().ValTyped() - } else { - return sc, errShortCodeIllegalState - } - - } - } else { - // positional params - if sc.params == nil { - var params []interface{} - params = append(params, currItem.ValTyped()) - sc.params = params - } else { - if params, ok := sc.params.([]interface{}); ok { - params = append(params, currItem.ValTyped()) - sc.params = params - } else { - return sc, errShortCodeIllegalState - } - - } - } - case currItem.IsDone(): - // handled by caller - pt.Backup() - break Loop - - } - } - return sc, nil -} - -// Replace prefixed shortcode tokens with the real content. -// Note: This function will rewrite the input slice. -func replaceShortcodeTokens(source []byte, replacements map[string]string) ([]byte, error) { - - if len(replacements) == 0 { - return source, nil - } - - start := 0 - - pre := []byte(shortcodePlaceholderPrefix) - post := []byte("HBHB") - pStart := []byte("

    ") - pEnd := []byte("

    ") - - k := bytes.Index(source[start:], pre) - - for k != -1 { - j := start + k - postIdx := bytes.Index(source[j:], post) - if postIdx < 0 { - // this should never happen, but let the caller decide to panic or not - return nil, errors.New("illegal state in content; shortcode token missing end delim") - } - - end := j + postIdx + 4 - - newVal := []byte(replacements[string(source[j:end])]) - - // Issue #1148: Check for wrapping p-tags

    - if j >= 3 && bytes.Equal(source[j-3:j], pStart) { - if (k+4) < len(source) && bytes.Equal(source[end:end+4], pEnd) { - j -= 3 - end += 4 - } - } - - // This and other cool slice tricks: https://github.com/golang/go/wiki/SliceTricks - source = append(source[:j], append(newVal, source[end:]...)...) - start = j - k = bytes.Index(source[start:], pre) - - } - - return source, nil -} - -func renderShortcodeWithPage(tmpl tpl.Template, data *ShortcodeWithPage) (string, error) { - buffer := bp.GetBuffer() - defer bp.PutBuffer(buffer) - - err := tmpl.Execute(buffer, data) - if err != nil { - return "", _errors.Wrap(err, "failed to process shortcode") - } - return buffer.String(), nil -} diff --git a/hugolib/shortcode_page.go b/hugolib/shortcode_page.go deleted file mode 100644 index e8a3a37e1..000000000 --- a/hugolib/shortcode_page.go +++ /dev/null @@ -1,56 +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 ( - "html/template" - - "github.com/gohugoio/hugo/resources/page" -) - -var tocShortcodePlaceholder = createShortcodePlaceholder("TOC", 0) - -// 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.ContentProvider - - // We need to replace it after we have rendered it, so provide a - // temporary placeholder. - toc template.HTML - - p *pageState -} - -func newPageForShortcode(p *pageState) page.Page { - return &pageForShortcode{ - PageWithoutContent: p, - ContentProvider: page.NopPage, - toc: template.HTML(tocShortcodePlaceholder), - p: p, - } -} - -func (p *pageForShortcode) page() page.Page { - return p.PageWithoutContent.(page.Page) -} - -func (p *pageForShortcode) TableOfContents() template.HTML { - p.p.enablePlaceholders() - return p.toc -} diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go deleted file mode 100644 index 36f004253..000000000 --- a/hugolib/shortcode_test.go +++ /dev/null @@ -1,1197 +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" - "path/filepath" - - "reflect" - - "github.com/gohugoio/hugo/parser/pageparser" - "github.com/gohugoio/hugo/resources/page" - - "strings" - "testing" - - "github.com/gohugoio/hugo/deps" - "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/tpl" - "github.com/spf13/cast" - - qt "github.com/frankban/quicktest" -) - -func CheckShortCodeMatch(t *testing.T, input, expected string, withTemplate func(templ tpl.TemplateHandler) error) { - t.Helper() - CheckShortCodeMatchAndError(t, input, expected, withTemplate, false) -} - -func CheckShortCodeMatchAndError(t *testing.T, input, expected string, withTemplate func(templ tpl.TemplateHandler) error, expectError bool) { - t.Helper() - cfg, fs := newTestCfg() - c := qt.New(t) - - // Need some front matter, see https://github.com/gohugoio/hugo/issues/2337 - contentFile := `--- -title: "Title" ---- -` + input - - writeSource(t, fs, "content/simple.md", contentFile) - - b := newTestSitesBuilderFromDepsCfg(t, deps.DepsCfg{Fs: fs, Cfg: cfg, WithTemplate: withTemplate}).WithNothingAdded() - err := b.BuildE(BuildCfg{}) - - if err != nil && !expectError { - t.Fatalf("Shortcode rendered error %s.", err) - } - - if err == nil && expectError { - t.Fatalf("No error from shortcode") - } - - h := b.H - c.Assert(len(h.Sites), qt.Equals, 1) - - c.Assert(len(h.Sites[0].RegularPages()), qt.Equals, 1) - - output := strings.TrimSpace(content(h.Sites[0].RegularPages()[0])) - output = strings.TrimPrefix(output, "

    ") - output = strings.TrimSuffix(output, "

    ") - - 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, "{{
    ` 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 deleted file mode 100644 index 2865010f0..000000000 --- a/hugolib/testhelpers_test.go +++ /dev/null @@ -1,985 +0,0 @@ -package hugolib - -import ( - "image/jpeg" - "io" - "path/filepath" - "runtime" - "strconv" - "testing" - "unicode/utf8" - - "github.com/gohugoio/hugo/output" - - "github.com/gohugoio/hugo/parser/metadecoders" - "github.com/google/go-cmp/cmp" - - "github.com/gohugoio/hugo/parser" - "github.com/pkg/errors" - - "bytes" - "fmt" - "regexp" - "strings" - "text/template" - - "github.com/fsnotify/fsnotify" - "github.com/gohugoio/hugo/common/herrors" - "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/tpl" - "github.com/spf13/viper" - - "os" - - "github.com/gohugoio/hugo/resources/resource" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/common/loggers" - "github.com/gohugoio/hugo/hugofs" -) - -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 - environ []string - - Fs *hugofs.Fs - T testing.TB - depsCfg deps.DepsCfg - - *qt.C - - logger *loggers.Logger - - dumper litter.Options - - // Used to test partial rebuilds. - changedFiles []string - - // Aka the Hugo server mode. - running bool - - H *HugoSites - - theme string - - // Default toml - configFormat string - configFileSet bool - viperSet 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 []string - templateFilePairs []string - i18nFilePairs []string - dataFilePairs []string - - // Additional data/content. - // As in "use the base, but add these on top". - contentFilePairsAdded []string - templateFilePairsAdded []string - i18nFilePairsAdded []string - dataFilePairsAdded []string -} - -func newTestSitesBuilder(t testing.TB) *sitesBuilder { - v := viper.New() - fs := hugofs.NewMem(v) - - litterOptions := litter.Options{ - HidePrivateFields: true, - StripPackageNames: true, - Separator: " ", - } - - return &sitesBuilder{T: t, C: qt.New(t), Fs: fs, configFormat: "toml", dumper: litterOptions} -} - -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} - workingDir := d.Cfg.GetString("workingDir") - - b.WithWorkingDir(workingDir) - - return b.WithViper(d.Cfg.(*viper.Viper)) - -} - -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 interface{}, 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 *viper.Viper) *sitesBuilder { - s.T.Helper() - if s.configFileSet { - s.T.Fatal("WithViper: use Viper or config.toml, not both") - } - defer func() { - s.viperSet = true - }() - - // Write to a config file to make sure the tests follow the same code path. - var buff bytes.Buffer - m := v.AllSettings() - 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.viperSet { - s.T.Fatal("WithConfigFile: use Viper 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 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 = "ga_id" - -[privacy] -[privacy.disqus] -disable = false -[privacy.googleAnalytics] -respectDoNotTrack = true -anonymizeIP = true -[privacy.instagram] -simple = true -[privacy.twitter] -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() - config := fmt.Sprintf("baseURL = %q", baseURL) - - config = config + commonConfigSections - return s.WithConfigFile("toml", config) -} - -func (s *sitesBuilder) WithDefaultMultiSiteConfig() *sitesBuilder { - var defaultMultiSiteConfig = ` -baseURL = "http://example.com/blog" - -paginate = 1 -disablePathToLower = true -defaultContentLanguage = "en" -defaultContentLanguageInSubdir = true - -[permalinks] -other = "/somewhere/else/:filename" - -[blackfriday] -angledQuotes = true - -[Taxonomies] -tag = "tags" - -[Languages] -[Languages.en] -weight = 10 -title = "In English" -languageName = "English" -[Languages.en.blackfriday] -angledQuotes = false -[[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" -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" -` + 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) WithContent(filenameContent ...string) *sitesBuilder { - s.contentFilePairs = append(s.contentFilePairs, filenameContent...) - return s -} - -func (s *sitesBuilder) WithContentAdded(filenameContent ...string) *sitesBuilder { - s.contentFilePairsAdded = append(s.contentFilePairsAdded, filenameContent...) - return s -} - -func (s *sitesBuilder) WithTemplates(filenameContent ...string) *sitesBuilder { - s.templateFilePairs = append(s.templateFilePairs, filenameContent...) - return s -} - -func (s *sitesBuilder) WithTemplatesAdded(filenameContent ...string) *sitesBuilder { - s.templateFilePairsAdded = append(s.templateFilePairsAdded, filenameContent...) - return s -} - -func (s *sitesBuilder) WithData(filenameContent ...string) *sitesBuilder { - s.dataFilePairs = append(s.dataFilePairs, filenameContent...) - return s -} - -func (s *sitesBuilder) WithDataAdded(filenameContent ...string) *sitesBuilder { - s.dataFilePairsAdded = append(s.dataFilePairsAdded, filenameContent...) - return s -} - -func (s *sitesBuilder) WithI18n(filenameContent ...string) *sitesBuilder { - s.i18nFilePairs = append(s.i18nFilePairs, filenameContent...) - return s -} - -func (s *sitesBuilder) WithI18nAdded(filenameContent ...string) *sitesBuilder { - s.i18nFilePairsAdded = append(s.i18nFilePairsAdded, filenameContent...) - return s -} - -func (s *sitesBuilder) EditFiles(filenameContent ...string) *sitesBuilder { - var changedFiles []string - for i := 0; i < len(filenameContent); i += 2 { - filename, content := filepath.FromSlash(filenameContent[i]), filenameContent[i+1] - changedFiles = append(changedFiles, filename) - writeSource(s.T, s.Fs, s.absFilename(filename), content) - - } - s.changedFiles = changedFiles - - return s -} - -func (s *sitesBuilder) writeFilePairs(folder string, filenameContent []string) *sitesBuilder { - if len(filenameContent)%2 != 0 { - s.Fatalf("expect filenameContent for %q in pairs (%d)", folder, len(filenameContent)) - } - for i := 0; i < len(filenameContent); i += 2 { - filename, content := filenameContent[i], filenameContent[i+1] - target := folder - // TODO(bep) clean up this magic. - if strings.HasPrefix(filename, folder) { - target = "" - } - - if s.workingDir != "" { - target = filepath.Join(s.workingDir, target) - } - - writeSource(s.T, s.Fs, filepath.Join(target, filename), content) - } - return s -} - -func (s *sitesBuilder) CreateSites() *sitesBuilder { - if err := s.CreateSitesE(); err != nil { - herrors.PrintStackTrace(err) - s.Fatalf("Failed to create sites: %s", err) - } - - return s -} - -func (s *sitesBuilder) LoadConfig() error { - if !s.configFileSet { - s.WithSimpleConfigFile() - } - - cfg, _, err := LoadConfig(ConfigSourceDescriptor{ - WorkingDir: s.workingDir, - Fs: s.Fs.Source, - Logger: s.logger, - Environ: s.environ, - Filename: "config." + s.configFormat}, func(cfg config.Provider) error { - - return nil - }) - - if err != nil { - return err - } - - s.Cfg = cfg - - 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/partials", - "layouts/shortcodes", - "data", - "i18n", - } { - if err := os.MkdirAll(filepath.Join(s.workingDir, dir), 0777); err != nil { - return errors.Wrapf(err, "failed to create %q", dir) - } - } - } - - 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 errors.Wrap(err, "failed to load config") - } - - s.Fs.Destination = hugofs.NewCreateCountingFs(s.Fs.Destination) - - depsCfg := s.depsCfg - depsCfg.Fs = s.Fs - depsCfg.Cfg = s.Cfg - depsCfg.Logger = s.logger - depsCfg.Running = s.running - - sites, err := NewHugoSites(depsCfg) - if err != nil { - return errors.Wrap(err, "failed to create sites") - } - 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 { - if len(s.changedFiles) == 0 { - return nil - } - - events := make([]fsnotify.Event, len(s.changedFiles)) - // TODO(bep) remove? - for i, v := range s.changedFiles { - events[i] = fsnotify.Event{ - Name: v, - Op: fsnotify.Write, - } - } - - 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 { - herrors.PrintStackTrace(err) - 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 }}" - - 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 }}", - "_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 }}", - "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 }}", - "_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", defaultContent) - } - if len(s.templateFilePairs) == 0 { - s.writeFilePairs("layouts", defaultTemplates) - } - if len(s.dataFilePairs) == 0 { - s.writeFilePairs("data", defaultData) - } - if len(s.i18nFilePairs) == 0 { - s.writeFilePairs("i18n", defaultI18n) - } -} - -func (s *sitesBuilder) Fatalf(format string, args ...interface{}) { - 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) - } -} - -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.Fatalf("No match for %q in content for %s\n%s\n%q", match, filename, content, content) - } - } - } -} - -func (s *sitesBuilder) AssertImage(width, height int, filename string) { - filename = filepath.Join(s.workingDir, filename) - f, err := s.Fs.Destination.Open(filename) - s.Assert(err, qt.IsNil) - defer f.Close() - cfg, err := jpeg.DecodeConfig(f) - s.Assert(cfg.Width, qt.Equals, width) - s.Assert(cfg.Height, qt.Equals, height) -} - -func (s *sitesBuilder) AssertNoDuplicateWrites() { - s.Helper() - d := s.Fs.Destination.(hugofs.DuplicatesReporter) - s.Assert(d.ReportDuplicates(), qt.Equals, "") -} - -func (s *sitesBuilder) FileContent(filename string) string { - s.T.Helper() - filename = filepath.FromSlash(filename) - if !strings.HasPrefix(filename, s.workingDir) { - filename = filepath.Join(s.workingDir, filename) - } - return readDestination(s.T, s.Fs, filename) -} - -func (s *sitesBuilder) AssertObject(expected string, object interface{}) { - s.T.Helper() - got := s.dumper.Sdump(object) - expected = strings.TrimSpace(expected) - - if expected != got { - fmt.Println(got) - diff := helpers.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 := readDestination(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 destinationExists(s.Fs, filepath.Clean(filename)) -} - -func newTestHelper(cfg config.Provider, fs *hugofs.Fs, t testing.TB) testHelper { - return testHelper{ - Cfg: cfg, - Fs: fs, - C: qt.New(t), - } -} - -type testHelper struct { - Cfg config.Provider - Fs *hugofs.Fs - *qt.C -} - -func (th testHelper) assertFileContent(filename string, matches ...string) { - th.Helper() - filename = th.replaceDefaultContentLanguageValue(filename) - content := readDestination(th, th.Fs, filename) - for _, match := range matches { - match = th.replaceDefaultContentLanguageValue(match) - th.Assert(strings.Contains(content, match), qt.Equals, true) - } -} - -func (th testHelper) assertFileContentRegexp(filename string, matches ...string) { - filename = th.replaceDefaultContentLanguageValue(filename) - content := readDestination(th, th.Fs, filename) - for _, match := range matches { - match = th.replaceDefaultContentLanguageValue(match) - r := regexp.MustCompile(match) - matches := r.MatchString(content) - if !matches { - fmt.Println(content) - } - th.Assert(matches, qt.Equals, true) - } -} - -func (th testHelper) assertFileNotExist(filename string) { - exists, err := helpers.Exists(filename, th.Fs.Destination) - th.Assert(err, qt.IsNil) - th.Assert(exists, qt.Equals, false) -} - -func (th testHelper) replaceDefaultContentLanguageValue(value string) string { - defaultInSubDir := th.Cfg.GetBool("defaultContentLanguageInSubDir") - replace := th.Cfg.GetString("defaultContentLanguage") + "/" - - if !defaultInSubDir { - value = strings.Replace(value, replace, "", 1) - - } - return value -} - -func loadTestConfig(fs afero.Fs, withConfig ...func(cfg config.Provider) error) (*viper.Viper, error) { - v, _, err := LoadConfig(ConfigSourceDescriptor{Fs: fs}, withConfig...) - return v, err -} - -func newTestCfgBasic() (*viper.Viper, *hugofs.Fs) { - mm := afero.NewMemMapFs() - v := viper.New() - v.Set("defaultContentLanguageInSubdir", true) - - fs := hugofs.NewFrom(hugofs.NewBaseFileDecorator(mm), v) - - return v, fs - -} - -func newTestCfg(withConfig ...func(cfg config.Provider) error) (*viper.Viper, *hugofs.Fs) { - mm := afero.NewMemMapFs() - - v, err := loadTestConfig(mm, func(cfg config.Provider) error { - // Default is false, but true is easier to use as default in tests - cfg.Set("defaultContentLanguageInSubdir", true) - - for _, w := range withConfig { - w(cfg) - } - - return nil - }) - - if err != nil && err != ErrNoConfigFile { - panic(err) - } - - fs := hugofs.NewFrom(hugofs.NewBaseFileDecorator(mm), v) - - return v, fs - -} - -func newTestSitesFromConfig(t testing.TB, afs afero.Fs, tomlConfig string, layoutPathContentPairs ...string) (testHelper, *HugoSites) { - if len(layoutPathContentPairs)%2 != 0 { - 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 := LoadConfigDefault(afs) - c.Assert(err, qt.IsNil) - - fs := hugofs.NewFrom(afs, cfg) - th := newTestHelper(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, Cfg: cfg}) - - c.Assert(err, qt.IsNil) - - return th, h -} - -func createWithTemplateFromNameValues(additionalTemplates ...string) func(templ tpl.TemplateHandler) error { - - 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 - } -} - -// 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 buildSingleSiteExpected(t testing.TB, expectSiteInitEror, expectBuildError bool, depsCfg deps.DepsCfg, buildCfg BuildCfg) *Site { - t.Helper() - b := newTestSitesBuilderFromDepsCfg(t, depsCfg).WithNothingAdded() - - err := b.CreateSitesE() - - if expectSiteInitEror { - b.Assert(err, qt.Not(qt.IsNil)) - return nil - } else { - b.Assert(err, qt.IsNil) - } - - h := b.H - - b.Assert(len(h.Sites), qt.Equals, 1) - - if expectBuildError { - b.Assert(h.Build(buildCfg), qt.Not(qt.IsNil)) - return nil - - } - - b.Assert(h.Build(buildCfg), qt.IsNil) - - return h.Sites[0] -} - -func writeSourcesToSource(t *testing.T, base string, fs *hugofs.Fs, sources ...[2]string) { - for _, src := range sources { - writeSource(t, fs, filepath.Join(base, src[0]), src[1]) - } -} - -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() - if err != nil { - panic(err) - } - - ccs, err := cast.ToStringE(cc) - if err != nil { - panic(err) - } - return ccs -} - -func dumpPages(pages ...page.Page) { - fmt.Println("---------") - for _, p := range pages { - var meta interface{} - if p.File() != nil && p.File().FileInfo() != nil { - meta = p.File().FileInfo().Meta() - } - fmt.Printf("Kind: %s Title: %-10s RelPermalink: %-10s Path: %-10s sections: %s Lang: %s Meta: %v\n", - p.Kind(), p.Title(), p.RelPermalink(), p.Path(), p.SectionsPath(), p.Lang(), meta) - } -} - -func dumpSPages(pages ...*pageState) { - for i, p := range pages { - fmt.Printf("%d: Kind: %s Title: %-10s RelPermalink: %-10s Path: %-10s sections: %s\n", - i+1, - p.Kind(), p.Title(), p.RelPermalink(), p.Path(), p.SectionsPath()) - } -} - -func printStringIndexes(s string) { - lines := strings.Split(s, "\n") - i := 0 - - for _, line := range lines { - - for _, r := range line { - fmt.Printf("%-3s", strconv.Itoa(i)) - i += utf8.RuneLen(r) - } - i++ - fmt.Println() - for _, r := range line { - fmt.Printf("%-3s", string(r)) - } - fmt.Println() - - } -} - -func isCI() bool { - return os.Getenv("CI") != "" -} - -func isGo111() bool { - return strings.Contains(runtime.Version(), "1.11") -} - -// See https://github.com/golang/go/issues/19280 -// Not in use. -var parallelEnabled = true - -func parallel(t *testing.T) { - if parallelEnabled { - t.Parallel() - } -} - -func skipSymlink(t *testing.T) { - if runtime.GOOS == "windows" && os.Getenv("CI") == "" { - t.Skip("skip symlink test on local Windows (needs admin)") - } - -} 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/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 deleted file mode 100644 index 072ce33e5..000000000 --- a/hugolib/translations.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 ( - "github.com/gohugoio/hugo/resources/page" -) - -func pagesToTranslationsMap(sites []*Site) map[string]page.Pages { - out := make(map[string]page.Pages) - - for _, s := range sites { - for _, p := range s.workAllPages { - // TranslationKey is implemented for all page types. - base := p.TranslationKey() - - pageTranslations, found := out[base] - if !found { - pageTranslations = make(page.Pages, 0) - } - - pageTranslations = append(pageTranslations, p) - out[base] = pageTranslations - } - } - - return out -} - -func assignTranslationsToPages(allTranslations map[string]page.Pages, sites []*Site) { - for _, s := range sites { - for _, p := range s.workAllPages { - base := p.TranslationKey() - translations, found := allTranslations[base] - if !found { - continue - } - - p.setTranslations(translations) - } - } -} diff --git a/langs/config.go b/langs/config.go deleted file mode 100644 index 927f3558f..000000000 --- a/langs/config.go +++ /dev/null @@ -1,217 +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 ( - "fmt" - "path/filepath" - "sort" - "strings" - - "github.com/gohugoio/hugo/common/maps" - - "github.com/spf13/cast" - - "github.com/pkg/errors" - - "github.com/gohugoio/hugo/config" -) - -type LanguagesConfig struct { - Languages Languages - Multihost bool - DefaultContentLanguageInSubdir bool -} - -func LoadLanguageSettings(cfg config.Provider, oldLangs Languages) (c LanguagesConfig, err error) { - - defaultLang := cfg.GetString("defaultContentLanguage") - if defaultLang == "" { - defaultLang = "en" - cfg.Set("defaultContentLanguage", defaultLang) - } - - var languages map[string]interface{} - - languagesFromConfig := cfg.GetStringMap("languages") - disableLanguages := cfg.GetStringSlice("disableLanguages") - - if len(disableLanguages) == 0 { - languages = languagesFromConfig - } else { - languages = make(map[string]interface{}) - for k, v := range languagesFromConfig { - for _, disabled := range disableLanguages { - if disabled == defaultLang { - return c, fmt.Errorf("cannot disable default language %q", defaultLang) - } - - if strings.EqualFold(k, disabled) { - v.(map[string]interface{})["disabled"] = true - break - } - } - languages[k] = v - } - } - - var languages2 Languages - - if len(languages) == 0 { - languages2 = append(languages2, NewDefaultLanguage(cfg)) - } else { - languages2, err = toSortedLanguages(cfg, languages) - if err != nil { - return c, errors.Wrap(err, "Failed to parse multilingual config") - } - } - - if oldLangs != nil { - // When in multihost mode, the languages are mapped to a server, so - // some structural language changes will need a restart of the dev server. - // The validation below isn't complete, but should cover the most - // important cases. - var invalid bool - if languages2.IsMultihost() != oldLangs.IsMultihost() { - invalid = true - } else { - if languages2.IsMultihost() && len(languages2) != len(oldLangs) { - invalid = true - } - } - - if invalid { - return c, errors.New("language change needing a server restart detected") - } - - if languages2.IsMultihost() { - // We need to transfer any server baseURL to the new language - for i, ol := range oldLangs { - nl := languages2[i] - nl.Set("baseURL", ol.GetString("baseURL")) - } - } - } - - // The defaultContentLanguage is something the user has to decide, but it needs - // to match a language in the language definition list. - langExists := false - for _, lang := range languages2 { - if lang.Lang == defaultLang { - langExists = true - break - } - } - - if !langExists { - return c, fmt.Errorf("site config value %q for defaultContentLanguage does not match any language definition", defaultLang) - } - - c.Languages = languages2 - c.Multihost = languages2.IsMultihost() - c.DefaultContentLanguageInSubdir = c.Multihost - - sortedDefaultFirst := make(Languages, len(c.Languages)) - for i, v := range c.Languages { - sortedDefaultFirst[i] = v - } - sort.Slice(sortedDefaultFirst, func(i, j int) bool { - li, lj := sortedDefaultFirst[i], sortedDefaultFirst[j] - if li.Lang == defaultLang { - return true - } - - if lj.Lang == defaultLang { - return false - } - - return i < j - }) - - cfg.Set("languagesSorted", c.Languages) - cfg.Set("languagesSortedDefaultFirst", sortedDefaultFirst) - cfg.Set("multilingual", len(languages2) > 1) - - multihost := c.Multihost - - if multihost { - cfg.Set("defaultContentLanguageInSubdir", true) - cfg.Set("multihost", true) - } - - if multihost { - // The baseURL may be provided at the language level. If that is true, - // then every language must have a baseURL. In this case we always render - // to a language sub folder, which is then stripped from all the Permalink URLs etc. - for _, l := range languages2 { - burl := l.GetLocal("baseURL") - if burl == nil { - return c, errors.New("baseURL must be set on all or none of the languages") - } - } - - } - - return c, nil -} - -func toSortedLanguages(cfg config.Provider, l map[string]interface{}) (Languages, error) { - languages := make(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 := 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) - case "contentdir": - language.ContentDir = filepath.Clean(cast.ToString(v)) - case "disabled": - language.Disabled = cast.ToBool(v) - case "params": - m := cast.ToStringMap(v) - // Needed for case insensitive fetching of params values - maps.ToLower(m) - for k, vv := range m { - language.SetParam(k, vv) - } - } - - // Put all into the Params map - language.SetParam(loki, v) - - // Also set it in the configuration map (for baseURL etc.) - language.Set(loki, v) - } - - languages[i] = language - i++ - } - - sort.Sort(languages) - - return languages, nil -} diff --git a/langs/i18n/i18n.go b/langs/i18n/i18n.go deleted file mode 100644 index 5beef8683..000000000 --- a/langs/i18n/i18n.go +++ /dev/null @@ -1,117 +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 ( - "github.com/gohugoio/hugo/common/loggers" - "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/helpers" - - "github.com/nicksnyder/go-i18n/i18n/bundle" - "github.com/nicksnyder/go-i18n/i18n/translation" -) - -var ( - i18nWarningLogger = helpers.NewDistinctFeedbackLogger() -) - -// Translator handles i18n translations. -type Translator struct { - translateFuncs map[string]bundle.TranslateFunc - cfg config.Provider - logger *loggers.Logger -} - -// NewTranslator creates a new Translator for the given language bundle and configuration. -func NewTranslator(b *bundle.Bundle, cfg config.Provider, logger *loggers.Logger) 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.INFO.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.INFO.Println("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(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 { - t.logger.INFO.Printf("No translation bundle found for default language %q", defaultContentLanguage) - } - - translations := bndl.Translations() - - 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 { - t.logger.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(translations, currentLang, translationID) { - 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(translations, defaultContentLanguage, translationID) { - return translated - } - } - return "" - } - } -} - -// If the translation map contains translationID for specified currentLang, -// then the translationID is actually translated. -func isIDTranslated(translations map[string]map[string]translation.Translation, lang, id string) bool { - _, contains := translations[lang][id] - return contains -} diff --git a/langs/i18n/i18n_test.go b/langs/i18n/i18n_test.go deleted file mode 100644 index d9215952a..000000000 --- a/langs/i18n/i18n_test.go +++ /dev/null @@ -1,272 +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 ( - "path/filepath" - "testing" - - "github.com/gohugoio/hugo/modules" - - "github.com/gohugoio/hugo/tpl/tplimpl" - - "github.com/gohugoio/hugo/common/loggers" - "github.com/gohugoio/hugo/langs" - "github.com/gohugoio/hugo/resources/page" - "github.com/spf13/afero" - "github.com/spf13/viper" - - "github.com/gohugoio/hugo/deps" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/hugofs" -) - -var logger = loggers.NewErrorLogger() - -type i18nTest struct { - name string - data map[string][]byte - args interface{} - 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!", - }, - // 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", - }, -} - -func doTestI18nTranslate(t testing.TB, test i18nTest, cfg config.Provider) string { - tp := prepareTranslationProvider(t, test, cfg) - f := tp.t.Func(test.lang) - return f(test.id, test.args) - -} - -func prepareTranslationProvider(t testing.TB, test i18nTest, cfg config.Provider) *TranslationProvider { - c := qt.New(t) - fs := hugofs.NewMem(cfg) - - for file, content := range test.data { - err := afero.WriteFile(fs.Source, filepath.Join("i18n", file), []byte(content), 0755) - c.Assert(err, qt.IsNil) - } - - tp := NewTranslationProvider() - depsCfg := newDepsConfig(tp, cfg, fs) - d, err := deps.New(depsCfg) - c.Assert(err, qt.IsNil) - c.Assert(d.LoadResources(), qt.IsNil) - - return tp -} - -func newDepsConfig(tp *TranslationProvider, cfg config.Provider, fs *hugofs.Fs) deps.DepsCfg { - l := langs.NewLanguage("en", cfg) - l.Set("i18nDir", "i18n") - return deps.DepsCfg{ - Language: l, - Site: page.NewDummyHugoSite(cfg), - Cfg: cfg, - Fs: fs, - Logger: logger, - TemplateProvider: tplimpl.DefaultTemplateProvider, - TranslationProvider: tp, - } -} - -func getConfig() *viper.Viper { - v := viper.New() - v.SetDefault("defaultContentLanguage", "en") - v.Set("contentDir", "content") - v.Set("dataDir", "data") - v.Set("i18nDir", "i18n") - v.Set("layoutDir", "layouts") - v.Set("archetypeDir", "archetypes") - v.Set("assetDir", "assets") - v.Set("resourceDir", "resources") - v.Set("publishDir", "public") - langs.LoadLanguageSettings(v, nil) - mod, err := modules.CreateProjectModule(v) - if err != nil { - panic(err) - } - v.Set("allModules", modules.Modules{mod}) - - return v - -} - -func TestI18nTranslate(t *testing.T) { - c := qt.New(t) - var actual, expected string - v := getConfig() - - // 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) - c.Assert(actual, qt.Equals, expected) - } - } -} - -func BenchmarkI18nTranslate(b *testing.B) { - v := getConfig() - 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(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 4ce9b59fe..000000000 --- a/langs/i18n/translationProvider.go +++ /dev/null @@ -1,135 +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 ( - "errors" - - "github.com/gohugoio/hugo/common/herrors" - - "github.com/gohugoio/hugo/deps" - "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/hugofs" - "github.com/gohugoio/hugo/source" - "github.com/nicksnyder/go-i18n/i18n/bundle" - "github.com/nicksnyder/go-i18n/i18n/language" - _errors "github.com/pkg/errors" -) - -// 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 { - spec := source.NewSourceSpec(d.PathSpec, nil) - - i18nBundle := bundle.New() - - en := language.GetPluralSpec("en") - if en == nil { - return errors.New("the English language has vanished like an old oak table") - } - var newLangs []string - - // The source dirs are ordered so the most important comes first. Since this is a - // last key win situation, we have to reverse the iteration order. - dirs := d.BaseFs.I18n.Dirs - for i := len(dirs) - 1; i >= 0; i-- { - dir := dirs[i] - src := spec.NewFilesystemFromFileMetaInfo(dir) - - files, err := src.Files() - if err != nil { - return err - } - - for _, r := range files { - currentSpec := language.GetPluralSpec(r.BaseFileName()) - if currentSpec == nil { - // This may is a language code not supported by go-i18n, it may be - // Klingon or ... not even a fake language. Make sure it works. - newLangs = append(newLangs, r.BaseFileName()) - } - } - - if len(newLangs) > 0 { - language.RegisterPluralSpec(newLangs, en) - } - - for _, file := range files { - if err := addTranslationFile(i18nBundle, file); err != nil { - return err - } - } - } - - tp.t = NewTranslator(i18nBundle, d.Cfg, d.Log) - - d.Translate = tp.t.Func(d.Language.Lang) - - return nil - -} - -func addTranslationFile(bundle *bundle.Bundle, r source.File) error { - f, err := r.FileInfo().Meta().Open() - if err != nil { - return _errors.Wrapf(err, "failed to open translations file %q:", r.LogicalName()) - } - err = bundle.ParseTranslationFileBytes(r.LogicalName(), helpers.ReaderToBytes(f)) - f.Close() - if err != nil { - return errWithFileContext(_errors.Wrapf(err, "failed to load translations"), r) - } - 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 -} - -func errWithFileContext(inerr error, r source.File) error { - fim, ok := r.FileInfo().(hugofs.FileMetaInfo) - if !ok { - return inerr - } - - meta := fim.Meta() - realFilename := meta.Filename() - f, err := meta.Open() - if err != nil { - return inerr - } - defer f.Close() - - err, _ = herrors.WithFileContext( - inerr, - realFilename, - f, - herrors.SimpleLineMatcher) - - return err - -} diff --git a/langs/language.go b/langs/language.go deleted file mode 100644 index f71b0255b..000000000 --- a/langs/language.go +++ /dev/null @@ -1,242 +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 ( - "sort" - "strings" - - "github.com/gohugoio/hugo/common/maps" - "github.com/gohugoio/hugo/config" - "github.com/spf13/cast" -) - -// These are the settings that should only be looked up in the global Viper -// config and not per language. -// This list may not be complete, but contains only settings that we know -// will be looked up in both. -// This isn't perfect, but it is ultimately the user who shoots him/herself in -// the foot. -// See the pathSpec. -var globalOnlySettings = map[string]bool{ - strings.ToLower("defaultContentLanguageInSubdir"): true, - strings.ToLower("defaultContentLanguage"): true, - strings.ToLower("multilingual"): true, - strings.ToLower("assetDir"): true, - strings.ToLower("resourceDir"): true, -} - -// Language manages specific-language configuration. -type Language struct { - Lang string - LanguageName string - Title string - Weight int - - Disabled bool - - // If set per language, this tells Hugo that all content files without any - // language indicator (e.g. my-page.en.md) is in this language. - // This is usually a path relative to the working dir, but it can be an - // absolute directory reference. It is what we get. - ContentDir string - - Cfg config.Provider - - // These are params declared in the [params] section of the language merged with the - // site's params, the most specific (language) wins on duplicate keys. - params map[string]interface{} - - // These are config values, i.e. the settings declared outside of the [params] section of the language. - // This is the map Hugo looks in when looking for configuration values (baseURL etc.). - // Values in this map can also be fetched from the params map above. - settings map[string]interface{} -} - -func (l *Language) String() string { - return l.Lang -} - -// NewLanguage creates a new language. -func NewLanguage(lang string, cfg config.Provider) *Language { - // Note that language specific params will be overridden later. - // We should improve that, but we need to make a copy: - params := make(map[string]interface{}) - for k, v := range cfg.GetStringMap("params") { - params[k] = v - } - maps.ToLower(params) - - l := &Language{Lang: lang, ContentDir: cfg.GetString("contentDir"), Cfg: cfg, params: params, settings: make(map[string]interface{})} - return l -} - -// NewDefaultLanguage creates the default language for a config.Provider. -// If not otherwise specified the default is "en". -func NewDefaultLanguage(cfg config.Provider) *Language { - defaultLang := cfg.GetString("defaultContentLanguage") - - if defaultLang == "" { - defaultLang = "en" - } - - return NewLanguage(defaultLang, cfg) -} - -// Languages is a sortable list of languages. -type Languages []*Language - -// NewLanguages creates a sorted list of languages. -// NOTE: function is currently unused. -func NewLanguages(l ...*Language) Languages { - languages := make(Languages, len(l)) - for i := 0; i < len(l); i++ { - languages[i] = l[i] - } - sort.Sort(languages) - return languages -} - -func (l Languages) Len() int { return len(l) } -func (l Languages) Less(i, j int) bool { - wi, wj := l[i].Weight, l[j].Weight - - if wi == wj { - return l[i].Lang < l[j].Lang - } - - return wj == 0 || wi < wj - -} - -func (l Languages) Swap(i, j int) { l[i], l[j] = l[j], l[i] } - -// Params retunrs language-specific params merged with the global params. -func (l *Language) Params() map[string]interface{} { - return l.params -} - -func (l Languages) AsSet() map[string]bool { - m := make(map[string]bool) - for _, lang := range l { - m[lang.Lang] = true - } - - return m -} - -func (l Languages) AsOrdinalSet() map[string]int { - m := make(map[string]int) - for i, lang := range l { - m[lang.Lang] = i - } - - return m -} - -// IsMultihost returns whether there are more than one language and at least one of -// the languages has baseURL specificed on the language level. -func (l Languages) IsMultihost() bool { - if len(l) <= 1 { - return false - } - - for _, lang := range l { - if lang.GetLocal("baseURL") != nil { - return true - } - } - return false -} - -// SetParam sets a param with the given key and value. -// SetParam is case-insensitive. -func (l *Language) SetParam(k string, v interface{}) { - l.params[strings.ToLower(k)] = v -} - -// GetBool returns the value associated with the key as a boolean. -func (l *Language) GetBool(key string) bool { return cast.ToBool(l.Get(key)) } - -// GetString returns the value associated with the key as a string. -func (l *Language) GetString(key string) string { return cast.ToString(l.Get(key)) } - -// GetInt returns the value associated with the key as an int. -func (l *Language) GetInt(key string) int { return cast.ToInt(l.Get(key)) } - -// GetStringMap returns the value associated with the key as a map of interfaces. -func (l *Language) GetStringMap(key string) map[string]interface{} { - return cast.ToStringMap(l.Get(key)) -} - -// GetStringMapString returns the value associated with the key as a map of strings. -func (l *Language) GetStringMapString(key string) map[string]string { - return cast.ToStringMapString(l.Get(key)) -} - -// GetStringSlice returns the value associated with the key as a slice of strings. -func (l *Language) GetStringSlice(key string) []string { - return cast.ToStringSlice(l.Get(key)) -} - -// Get returns a value associated with the key relying on specified language. -// Get is case-insensitive for a key. -// -// Get returns an interface. For a specific value use one of the Get____ methods. -func (l *Language) Get(key string) interface{} { - local := l.GetLocal(key) - if local != nil { - return local - } - return l.Cfg.Get(key) -} - -// GetLocal gets a configuration value set on language level. It will -// not fall back to any global value. -// It will return nil if a value with the given key cannot be found. -func (l *Language) GetLocal(key string) interface{} { - if l == nil { - panic("language not set") - } - key = strings.ToLower(key) - if !globalOnlySettings[key] { - if v, ok := l.settings[key]; ok { - return v - } - } - return nil -} - -// Set sets the value for the key in the language's params. -func (l *Language) Set(key string, value interface{}) { - if l == nil { - panic("language not set") - } - key = strings.ToLower(key) - l.settings[key] = value -} - -// IsSet checks whether the key is set in the language or the related config store. -func (l *Language) IsSet(key string) bool { - key = strings.ToLower(key) - - key = strings.ToLower(key) - if !globalOnlySettings[key] { - if _, ok := l.settings[key]; ok { - return true - } - } - return l.Cfg.IsSet(key) - -} diff --git a/langs/language_test.go b/langs/language_test.go deleted file mode 100644 index 97abe77cc..000000000 --- a/langs/language_test.go +++ /dev/null @@ -1,49 +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 ( - "testing" - - qt "github.com/frankban/quicktest" - "github.com/spf13/viper" -) - -func TestGetGlobalOnlySetting(t *testing.T) { - c := qt.New(t) - v := viper.New() - v.Set("defaultContentLanguageInSubdir", true) - v.Set("contentDir", "content") - v.Set("paginatePath", "page") - lang := NewDefaultLanguage(v) - lang.Set("defaultContentLanguageInSubdir", false) - lang.Set("paginatePath", "side") - - c.Assert(lang.GetBool("defaultContentLanguageInSubdir"), qt.Equals, true) - c.Assert(lang.GetString("paginatePath"), qt.Equals, "side") -} - -func TestLanguageParams(t *testing.T) { - c := qt.New(t) - - v := viper.New() - v.Set("p1", "p1cfg") - v.Set("contentDir", "content") - - lang := NewDefaultLanguage(v) - lang.SetParam("p1", "p1p") - - c.Assert(lang.Params()["p1"], qt.Equals, "p1p") - c.Assert(lang.Get("p1"), qt.Equals, "p1cfg") -} diff --git a/tpl/tplimpl/embedded/templates/_default/rss.xml b/layouts/index.rss.xml similarity index 59% rename from tpl/tplimpl/embedded/templates/_default/rss.xml rename to layouts/index.rss.xml index 3e17f48c6..1d3498a1e 100644 --- a/tpl/tplimpl/embedded/templates/_default/rss.xml +++ b/layouts/index.rss.xml @@ -1,33 +1,37 @@ -{{- $pctx := . -}} -{{- if .IsHome -}}{{ $pctx = .Site }}{{- end -}} -{{- $pages := $pctx.RegularPages -}} -{{- $limit := .Site.Config.Services.RSS.Limit -}} -{{- if ge $limit 1 -}} -{{- $pages = $pages | first $limit -}} -{{- end -}} -{{- printf "" | safeHTML }} - {{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }} + {{ .Site.Title }} – {{ .Title }} {{ .Permalink }} - Recent content {{ if ne .Title .Site.Title }}{{ with .Title }}in {{.}} {{ end }}{{ end }}on {{ .Site.Title }} + Recent Hugo news from gohugo.io 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 }} + + {{ "img/hugo.png" | absURL }} + GoHugo.io + {{ .Permalink }} + {{ with .OutputFormats.Get "RSS" }} {{ printf "" .Permalink .MediaType | safeHTML }} {{ end }} - {{ range $pages }} + {{ range first 50 (where .Site.RegularPages "Type" "in" (slice "news" "showcase")) }} - {{ .Title }} + {{ .Section | title }}: {{ .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 }} + + {{ $img := (.Resources.ByType "image").GetMatch "*featured*" }} + {{ with $img }} + {{ $img := .Resize "640x" }} + {{ printf "]]>" $img.Permalink $img.Width $img.Height | safeHTML }} + {{ end }} + {{ .Content | html }} + {{ end }} diff --git a/layouts/maintenance/list.html b/layouts/maintenance/list.html new file mode 100644 index 000000000..50059ad9e --- /dev/null +++ b/layouts/maintenance/list.html @@ -0,0 +1,36 @@ +{{ define "main" }} +
    +
    + +
    + {{ $byLastMod := .Site.RegularPages.ByLastmod }} + {{ $recent := ($byLastMod | last 30).Reverse }} + {{ $leastRecent := $byLastMod | first 10 }} +

    Last Updated

    + {{ partial "maintenance-pages-table" $recent }} +

    Least Recently Updated

    + {{ partial "maintenance-pages-table" $leastRecent }} + + {{/* Don't think this is possible with where directly. Should investigate. */}} + {{ .Scratch.Set "todos" slice }} + {{ range .Site.RegularPages }} + {{ if .HasShortcode "todo" }} + {{ $.Scratch.Add "todos" . }} + {{ end }} + {{ end }} +

    Pages marked with TODO

    + {{ partial "maintenance-pages-table" (.Scratch.Get "todos") }} + +
    +
    +
    +{{ end }} \ No newline at end of file diff --git a/layouts/partials/maintenance-pages-table.html b/layouts/partials/maintenance-pages-table.html new file mode 100644 index 000000000..8538e2104 --- /dev/null +++ b/layouts/partials/maintenance-pages-table.html @@ -0,0 +1,24 @@ + + + + + + + + + + {{ range . }} + + + + + + {{ end }} + +
    LastModLinkGitHub
    {{ .Lastmod.Format "2006-01-02" }} + {{ .Title }} + + + {{ with .GitInfo }}{{ .Subject }}{{ else }}Source{{ end }} + +
    \ No newline at end of file diff --git a/layouts/shortcodes/asciicast.html b/layouts/shortcodes/asciicast.html new file mode 100644 index 000000000..ee23adc2d --- /dev/null +++ b/layouts/shortcodes/asciicast.html @@ -0,0 +1,2 @@ +{{ $id := .Get 0 }} + diff --git a/layouts/shortcodes/chroma-lexers.html b/layouts/shortcodes/chroma-lexers.html new file mode 100644 index 000000000..0df2b868f --- /dev/null +++ b/layouts/shortcodes/chroma-lexers.html @@ -0,0 +1,6 @@ +
    +{{ range .Site.Data.docs.chroma.lexers }} +
    {{ .Name }}
    +
    {{ delimit .Aliases ", " }}
    +{{ end }} +
    \ No newline at end of file diff --git a/layouts/shortcodes/code.html b/layouts/shortcodes/code.html new file mode 100644 index 000000000..eafc02e6b --- /dev/null +++ b/layouts/shortcodes/code.html @@ -0,0 +1,25 @@ +{{ $file := .Get "file" }} +{{ $codeLang := "" }} +{{ $suffix := findRE "(\\.[^.]+)$" $file 1 }} +{{ with $suffix }} +{{ $codeLang = (index . 0 | strings.TrimPrefix ".") }} +{{ end }} +{{ with .Get "codeLang" }}{{ $codeLang = . }}{{ end }} +{{ if eq $codeLang "html"}} +{{ $codeLang = "go-html-template" }} +{{ end }} +
    + {{- with $file -}} +
    {{.}}
    + {{- end -}} + + {{ if ne (.Get "copy") "false" }} + + {{/* Functionality located within filesaver.js The copy here is located in the css with .copy class so it can be replaced with JS on success */}} + {{end}} +
    + {{ if .Get "nocode" }}{{ $.Inner }}{{ else }}{{ with $codeLang }}{{- highlight $.Inner . "" | -}}{{ else }}
    {{- .Inner | string -}}
    {{ end }}{{ end }} +
    + +
    diff --git a/layouts/shortcodes/datatable-filtered.html b/layouts/shortcodes/datatable-filtered.html new file mode 100644 index 000000000..576ddab6f --- /dev/null +++ b/layouts/shortcodes/datatable-filtered.html @@ -0,0 +1,28 @@ +{{ $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 $fields }} + + {{ end }} + + {{ end }} +
    {{ . }}
    {{ $.Scratch.Get . }}
    diff --git a/layouts/shortcodes/datatable.html b/layouts/shortcodes/datatable.html new file mode 100644 index 000000000..4e2814f5a --- /dev/null +++ b/layouts/shortcodes/datatable.html @@ -0,0 +1,22 @@ +{{ $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/layouts/shortcodes/directoryindex.html b/layouts/shortcodes/directoryindex.html new file mode 100644 index 000000000..37e7d3ad1 --- /dev/null +++ b/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/layouts/shortcodes/docfile.html b/layouts/shortcodes/docfile.html new file mode 100644 index 000000000..2f982aae8 --- /dev/null +++ b/layouts/shortcodes/docfile.html @@ -0,0 +1,11 @@ +{{ $file := .Get 0}} +{{ $filepath := $file }} +{{ $syntax := index (split $file ".") 1 }} +{{ $syntaxoverride := eq (len .Params) 2 }} +
    +
    {{$filepath}}
    + +
    {{- readFile $file -}}
    +
    diff --git a/layouts/shortcodes/exfile.html b/layouts/shortcodes/exfile.html new file mode 100644 index 000000000..226782957 --- /dev/null +++ b/layouts/shortcodes/exfile.html @@ -0,0 +1,12 @@ +{{ $file := .Get 0}} +{{ $filepath := replace $file "static/" ""}} +{{ $syntax := index (split $file ".") 1 }} +{{ $syntaxoverride := eq (len .Params) 2 }} +
    +
    {{$filepath}}
    + +
    {{- readFile $file -}}
    + Source +
    diff --git a/layouts/shortcodes/exfm.html b/layouts/shortcodes/exfm.html new file mode 100644 index 000000000..c0429bbe1 --- /dev/null +++ b/layouts/shortcodes/exfm.html @@ -0,0 +1,13 @@ + +{{ $file := .Get 0}} +{{ $filepath := replace $file "static/" ""}} +{{ $syntax := index (split $file ".") 1 }} +{{ $syntaxoverride := eq (len .Params) 2 }} +
    +
    {{$filepath}}
    + +
    {{- readFile $file -}}
    + Source +
    \ No newline at end of file diff --git a/layouts/shortcodes/funcsig.html b/layouts/shortcodes/funcsig.html new file mode 100644 index 000000000..1709c60b0 --- /dev/null +++ b/layouts/shortcodes/funcsig.html @@ -0,0 +1,4 @@ +

    Syntax

    +
    +    {{- .Inner -}}
    +
    \ No newline at end of file diff --git a/layouts/shortcodes/gh.html b/layouts/shortcodes/gh.html new file mode 100644 index 000000000..981f4b838 --- /dev/null +++ b/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/layouts/shortcodes/ghrepo.html b/layouts/shortcodes/ghrepo.html new file mode 100644 index 000000000..e9df40d6a --- /dev/null +++ b/layouts/shortcodes/ghrepo.html @@ -0,0 +1 @@ +GitHub repository \ No newline at end of file diff --git a/layouts/shortcodes/gomodules-info.html b/layouts/shortcodes/gomodules-info.html new file mode 100644 index 000000000..3c9d486ae --- /dev/null +++ b/layouts/shortcodes/gomodules-info.html @@ -0,0 +1,17 @@ +{{ $text := ` +Most of the commands for **Hugo Modules** requires a newer version of Go installed (see https://golang.org/dl/) and the relevant VCS client (e.g. Git, see https://git-scm.com/downloads). If you have an "older" site running on Netlify, you may have to set GO_VERSION to 1.12 in your Environment settings. + +For more information about Go Modules, see: + +* https://github.com/golang/go/wiki/Modules +* https://blog.golang.org/using-go-modules +` }} + + diff --git a/layouts/shortcodes/imgproc.html b/layouts/shortcodes/imgproc.html new file mode 100644 index 000000000..5e02317c6 --- /dev/null +++ b/layouts/shortcodes/imgproc.html @@ -0,0 +1,25 @@ +{{ $original := .Page.Resources.GetMatch (printf "*%s*" (.Get 0)) }} +{{ $command := .Get 1 }} +{{ $options := .Get 2 }} +{{ if eq $command "Fit"}} +{{ .Scratch.Set "image" ($original.Fit $options) }} +{{ else if eq $command "Resize"}} +{{ .Scratch.Set "image" ($original.Resize $options) }} +{{ else if eq $command "Fill"}} +{{ .Scratch.Set "image" ($original.Fill $options) }} +{{ else }} +{{ errorf "Invalid image processing command: Must be one of Fit, Fill or Resize."}} +{{ end }} +{{ $image := .Scratch.Get "image" }} +
    + +
    + + {{ with .Inner }} + {{ . }} + {{ else }} + .{{ $command }} "{{ $options }}" + {{ end }} + +
    +
    \ No newline at end of file diff --git a/layouts/shortcodes/module-mounts-note.html b/layouts/shortcodes/module-mounts-note.html new file mode 100644 index 000000000..654aafef4 --- /dev/null +++ b/layouts/shortcodes/module-mounts-note.html @@ -0,0 +1 @@ +Also see [Module Mounts Config](/hugo-modules/configuration/#module-config-mounts) for an alternative way to configure this directory (from Hugo 0.56). \ No newline at end of file diff --git a/layouts/shortcodes/nohighlight.html b/layouts/shortcodes/nohighlight.html new file mode 100644 index 000000000..238234f17 --- /dev/null +++ b/layouts/shortcodes/nohighlight.html @@ -0,0 +1 @@ +
    {{ .Inner }}
    \ No newline at end of file diff --git a/layouts/shortcodes/note.html b/layouts/shortcodes/note.html new file mode 100644 index 000000000..24d2cd0b2 --- /dev/null +++ b/layouts/shortcodes/note.html @@ -0,0 +1,9 @@ +{{ $_hugo_config := `{ "version": 1 }` }} + diff --git a/layouts/shortcodes/output.html b/layouts/shortcodes/output.html new file mode 100644 index 000000000..e51d284bb --- /dev/null +++ b/layouts/shortcodes/output.html @@ -0,0 +1,8 @@ +{{$file := .Get "file"}} +{{$icon := index (split $file ".") 1 }} +
    +
    {{$file}}
    +
    +
    {{- .Inner | string -}}
    +
    +
    \ No newline at end of file diff --git a/layouts/shortcodes/readfile.html b/layouts/shortcodes/readfile.html new file mode 100644 index 000000000..36400ac55 --- /dev/null +++ b/layouts/shortcodes/readfile.html @@ -0,0 +1,8 @@ +{{$file := .Get "file"}} +{{- if eq (.Get "markdown") "true" -}} +{{- $file | readFile | markdownify -}} +{{- else if (.Get "highlight") -}} +{{- highlight ($file | readFile) (.Get "highlight") "" -}} +{{- else -}} +{{ $file | readFile | safeHTML }} +{{- end -}} \ No newline at end of file diff --git a/layouts/shortcodes/tip.html b/layouts/shortcodes/tip.html new file mode 100644 index 000000000..139e3376b --- /dev/null +++ b/layouts/shortcodes/tip.html @@ -0,0 +1,9 @@ +{{ $_hugo_config := `{ "version": 1 }` }} + diff --git a/layouts/shortcodes/todo.html b/layouts/shortcodes/todo.html new file mode 100644 index 000000000..50a099267 --- /dev/null +++ b/layouts/shortcodes/todo.html @@ -0,0 +1 @@ +{{ if .Inner }}{{ end }} \ No newline at end of file diff --git a/layouts/shortcodes/warning.html b/layouts/shortcodes/warning.html new file mode 100644 index 000000000..c9147be64 --- /dev/null +++ b/layouts/shortcodes/warning.html @@ -0,0 +1,9 @@ +{{ $_hugo_config := `{ "version": 1 }` }} + diff --git a/layouts/shortcodes/yt.html b/layouts/shortcodes/yt.html new file mode 100644 index 000000000..6915cec5f --- /dev/null +++ b/layouts/shortcodes/yt.html @@ -0,0 +1,11 @@ +
    + + {{if (.Get "thumbnail")}} +
    + {{else}} +
    + {{end}} +
    +{{ if (.Get "description") }} +
    {{ .Get "description" | markdownify }}
    +{{ end }} \ No newline at end of file diff --git a/lazy/init.go b/lazy/init.go deleted file mode 100644 index a54fda96a..000000000 --- a/lazy/init.go +++ /dev/null @@ -1,198 +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" - "sync" - "time" - - "github.com/pkg/errors" -) - -// New creates a new empty Init. -func New() *Init { - return &Init{} -} - -// Init holds a graph of lazily initialized dependencies. -type Init struct { - mu sync.Mutex - - prev *Init - children []*Init - - init onceMore - out interface{} - err error - f func() (interface{}, error) -} - -// Add adds a func as a new child dependency. -func (ini *Init) Add(initFn func() (interface{}, error)) *Init { - if ini == nil { - ini = New() - } - return ini.add(false, initFn) -} - -// AddWithTimeout is same as Add, but with a timeout that aborts initialization. -func (ini *Init) AddWithTimeout(timeout time.Duration, f func(ctx context.Context) (interface{}, error)) *Init { - return ini.Add(func() (interface{}, error) { - return ini.withTimeout(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() (interface{}, error)) *Init { - if ini == nil { - ini = New() - } - return ini.add(true, initFn) -} - -// BranchdWithTimeout is same as Branch, but with a timeout. -func (ini *Init) BranchdWithTimeout(timeout time.Duration, f func(ctx context.Context) (interface{}, error)) *Init { - return ini.Branch(func() (interface{}, error) { - return ini.withTimeout(timeout, f) - }) -} - -// Do initializes the entire dependency graph. -func (ini *Init) Do() (interface{}, error) { - if ini == nil { - panic("init is nil") - } - - ini.init.Do(func() { - prev := ini.prev - if prev != nil { - // A branch. Initialize the ancestors. - if prev.shouldInitialize() { - _, err := prev.Do() - 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() - } - - for _, child := range ini.children { - if child.shouldInitialize() { - _, err := child.Do() - 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() - defer mu.Unlock() - for _, d := range ini.children { - d.Reset() - } -} - -func (ini *Init) add(branch bool, initFn func() (interface{}, 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(timeout time.Duration, f func(ctx context.Context) (interface{}, error)) (interface{}, error) { - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - c := make(chan verr, 1) - - go func() { - v, err := f(ctx) - select { - case <-ctx.Done(): - return - default: - c <- verr{v: v, err: err} - } - }() - - select { - case <-ctx.Done(): - return nil, errors.New("timed out initializing value. This is most likely a circular loop in a shortcode") - case ve := <-c: - return ve.v, ve.err - } - -} - -type verr struct { - v interface{} - err error -} diff --git a/lazy/init_test.go b/lazy/init_test.go deleted file mode 100644 index 772081b56..000000000 --- a/lazy/init_test.go +++ /dev/null @@ -1,226 +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() (interface{}, error) { - return func() (interface{}, error) { - result += name + "|" - doWork() - return name, nil - } - } - - f2 := func() func() (interface{}, error) { - return func() (interface{}, 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 - - // Add some concurrency and randomness to verify thread safety and - // init order. - for i := 0; i < 100; i++ { - wg.Add(1) - go func(i int) { - defer wg.Done() - var err error - if rnd.Intn(10) < 5 { - _, err = root.Do() - 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() - c.Assert(err, qt.IsNil) - } else { - _, err = branch1_2_1.Do() - c.Assert(err, qt.IsNil) - } - _, err = branch1_2.Do() - 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) (interface{}, error) { - return nil, nil - }) - - _, err := init.Do() - - c.Assert(err, qt.IsNil) -} - -func TestInitAddWithTimeoutTimeout(t *testing.T) { - c := qt.New(t) - - init := New().AddWithTimeout(100*time.Millisecond, func(ctx context.Context) (interface{}, error) { - time.Sleep(500 * time.Millisecond) - select { - case <-ctx.Done(): - return nil, nil - default: - } - t.Fatal("slept") - return nil, nil - }) - - _, err := init.Do() - - 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) (interface{}, error) { - return nil, errors.New("failed") - }) - - _, err := init.Do() - - 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() (interface{}, error) { - return func() (interface{}, 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 - - for _, v := range inits { - v := v - wg.Add(1) - go func() { - defer wg.Done() - _, err := v.Do() - c.Assert(err, qt.IsNil) - }() - } - - wg.Wait() - - c.Assert(state.V2, qt.Equals, "ABAB") -} diff --git a/lazy/once.go b/lazy/once.go deleted file mode 100644 index c434bfa0b..000000000 --- a/lazy/once.go +++ /dev/null @@ -1,69 +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 deleted file mode 100644 index 4e94e2ee0..000000000 --- a/livereload/connection.go +++ /dev/null @@ -1,66 +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 livereload - -import ( - "bytes" - "sync" - - "github.com/gorilla/websocket" -) - -type connection struct { - // The websocket connection. - ws *websocket.Conn - - // Buffered channel of outbound messages. - send chan []byte - - // There is a potential data race, especially visible with large files. - // This is protected by synchronisation of the send channel's close. - closer sync.Once -} - -func (c *connection) close() { - c.closer.Do(func() { - close(c.send) - }) -} - -func (c *connection) reader() { - for { - _, message, err := c.ws.ReadMessage() - if err != nil { - break - } - if bytes.Contains(message, []byte(`"command":"hello"`)) { - c.send <- []byte(`{ - "command": "hello", - "protocols": [ "http://livereload.com/protocols/official-7" ], - "serverName": "Hugo" - }`) - } - } - c.ws.Close() -} - -func (c *connection) writer() { - for message := range c.send { - err := c.ws.WriteMessage(websocket.TextMessage, message) - if err != nil { - break - } - } - c.ws.Close() -} diff --git a/livereload/hub.go b/livereload/hub.go deleted file mode 100644 index 8ab6083ad..000000000 --- a/livereload/hub.go +++ /dev/null @@ -1,56 +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 livereload - -type hub struct { - // Registered connections. - connections map[*connection]bool - - // Inbound messages from the connections. - broadcast chan []byte - - // Register requests from the connections. - register chan *connection - - // Unregister requests from connections. - unregister chan *connection -} - -var wsHub = hub{ - broadcast: make(chan []byte), - register: make(chan *connection), - unregister: make(chan *connection), - connections: make(map[*connection]bool), -} - -func (h *hub) run() { - for { - select { - case c := <-h.register: - h.connections[c] = true - case c := <-h.unregister: - delete(h.connections, c) - c.close() - case m := <-h.broadcast: - for c := range h.connections { - select { - case c.send <- m: - default: - delete(h.connections, c) - c.close() - } - } - } - } -} diff --git a/livereload/livereload.go b/livereload/livereload.go deleted file mode 100644 index 92214b1f1..000000000 --- a/livereload/livereload.go +++ /dev/null @@ -1,186 +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. -// -// Contains an embedded version of livereload.js -// -// Copyright (c) 2010-2015 Andrey Tarantsov -// -// 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. - -package livereload - -import ( - "fmt" - "net" - "net/http" - "net/url" - "path/filepath" - - "github.com/gorilla/websocket" -) - -// Prefix to signal to LiveReload that we need to navigate to another path. -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 - } - - if u.Host == r.Host { - 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} - -// Handler is a HandlerFunc handling the livereload -// Websocket interaction. -func Handler(w http.ResponseWriter, r *http.Request) { - ws, err := upgrader.Upgrade(w, r, nil) - if err != nil { - return - } - c := &connection{send: make(chan []byte, 256), ws: ws} - wsHub.register <- c - defer func() { wsHub.unregister <- c }() - go c.writer() - c.reader() -} - -// Initialize starts the Websocket Hub handling live reloads. -func Initialize() { - go wsHub.run() -} - -// ForceRefresh tells livereload to force a hard refresh. -func ForceRefresh() { - RefreshPath("/x.js") -} - -// 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) -} - -// 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) -} - -// 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) -} - -// 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", "application/javascript") - w.Write(liveReloadJS()) -} - -func liveReloadJS() []byte { - return []byte(livereloadJS + hugoLiveReloadPlugin) -} - -var ( - // This is a patched version, see https://github.com/livereload/livereload-js/pull/84 - livereloadJS = `!function(){return function e(t,o,n){function r(s,c){if(!o[s]){if(!t[s]){var a="function"==typeof require&&require;if(!c&&a)return a(s,!0);if(i)return i(s,!0);var l=new Error("Cannot find module '"+s+"'");throw l.code="MODULE_NOT_FOUND",l}var h=o[s]={exports:{}};t[s][0].call(h.exports,function(e){return r(t[s][1][e]||e)},h,h.exports,e,t,o,n)}return o[s].exports}for(var i="function"==typeof require&&require,s=0;sh;)if((c=a[h++])!=c)return!0}else for(;l>h;h++)if((e||h in a)&&a[h]===o)return e||h||0;return!e&&-1}}},{"./_to-absolute-index":38,"./_to-iobject":40,"./_to-length":41}],5:[function(e,t,o){var n={}.toString;t.exports=function(e){return n.call(e).slice(8,-1)}},{}],6:[function(e,t,o){var n=t.exports={version:"2.6.5"};"number"==typeof __e&&(__e=n)},{}],7:[function(e,t,o){var n=e("./_a-function");t.exports=function(e,t,o){if(n(e),void 0===t)return e;switch(o){case 1:return function(o){return e.call(t,o)};case 2:return function(o,n){return e.call(t,o,n)};case 3:return function(o,n,r){return e.call(t,o,n,r)}}return function(){return e.apply(t,arguments)}}},{"./_a-function":1}],8:[function(e,t,o){t.exports=function(e){if(null==e)throw TypeError("Can't call method on "+e);return e}},{}],9:[function(e,t,o){t.exports=!e("./_fails")(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},{"./_fails":13}],10:[function(e,t,o){var n=e("./_is-object"),r=e("./_global").document,i=n(r)&&n(r.createElement);t.exports=function(e){return i?r.createElement(e):{}}},{"./_global":15,"./_is-object":21}],11:[function(e,t,o){t.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},{}],12:[function(e,t,o){var n=e("./_global"),r=e("./_core"),i=e("./_hide"),s=e("./_redefine"),c=e("./_ctx"),a=function(e,t,o){var l,h,u,d,f=e&a.F,p=e&a.G,_=e&a.S,m=e&a.P,g=e&a.B,y=p?n:_?n[t]||(n[t]={}):(n[t]||{}).prototype,v=p?r:r[t]||(r[t]={}),w=v.prototype||(v.prototype={});for(l in p&&(o=t),o)u=((h=!f&&y&&void 0!==y[l])?y:o)[l],d=g&&h?c(u,n):m&&"function"==typeof u?c(Function.call,u):u,y&&s(y,l,u,e&a.U),v[l]!=u&&i(v,l,d),m&&w[l]!=u&&(w[l]=u)};n.core=r,a.F=1,a.G=2,a.S=4,a.P=8,a.B=16,a.W=32,a.U=64,a.R=128,t.exports=a},{"./_core":6,"./_ctx":7,"./_global":15,"./_hide":17,"./_redefine":34}],13:[function(e,t,o){t.exports=function(e){try{return!!e()}catch(e){return!0}}},{}],14:[function(e,t,o){t.exports=e("./_shared")("native-function-to-string",Function.toString)},{"./_shared":37}],15:[function(e,t,o){var n=t.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},{}],16:[function(e,t,o){var n={}.hasOwnProperty;t.exports=function(e,t){return n.call(e,t)}},{}],17:[function(e,t,o){var n=e("./_object-dp"),r=e("./_property-desc");t.exports=e("./_descriptors")?function(e,t,o){return n.f(e,t,r(1,o))}:function(e,t,o){return e[t]=o,e}},{"./_descriptors":9,"./_object-dp":28,"./_property-desc":33}],18:[function(e,t,o){var n=e("./_global").document;t.exports=n&&n.documentElement},{"./_global":15}],19:[function(e,t,o){t.exports=!e("./_descriptors")&&!e("./_fails")(function(){return 7!=Object.defineProperty(e("./_dom-create")("div"),"a",{get:function(){return 7}}).a})},{"./_descriptors":9,"./_dom-create":10,"./_fails":13}],20:[function(e,t,o){var n=e("./_cof");t.exports=Object("z").propertyIsEnumerable(0)?Object:function(e){return"String"==n(e)?e.split(""):Object(e)}},{"./_cof":5}],21:[function(e,t,o){t.exports=function(e){return"object"==typeof e?null!==e:"function"==typeof e}},{}],22:[function(e,t,o){"use strict";var n=e("./_object-create"),r=e("./_property-desc"),i=e("./_set-to-string-tag"),s={};e("./_hide")(s,e("./_wks")("iterator"),function(){return this}),t.exports=function(e,t,o){e.prototype=n(s,{next:r(1,o)}),i(e,t+" Iterator")}},{"./_hide":17,"./_object-create":27,"./_property-desc":33,"./_set-to-string-tag":35,"./_wks":45}],23:[function(e,t,o){"use strict";var n=e("./_library"),r=e("./_export"),i=e("./_redefine"),s=e("./_hide"),c=e("./_iterators"),a=e("./_iter-create"),l=e("./_set-to-string-tag"),h=e("./_object-gpo"),u=e("./_wks")("iterator"),d=!([].keys&&"next"in[].keys()),f=function(){return this};t.exports=function(e,t,o,p,_,m,g){a(o,t,p);var y,v,w,b=function(e){if(!d&&e in L)return L[e];switch(e){case"keys":case"values":return function(){return new o(this,e)}}return function(){return new o(this,e)}},S=t+" Iterator",R="values"==_,k=!1,L=e.prototype,x=L[u]||L["@@iterator"]||_&&L[_],j=x||b(_),C=_?R?b("entries"):j:void 0,O="Array"==t&&L.entries||x;if(O&&(w=h(O.call(new e)))!==Object.prototype&&w.next&&(l(w,S,!0),n||"function"==typeof w[u]||s(w,u,f)),R&&x&&"values"!==x.name&&(k=!0,j=function(){return x.call(this)}),n&&!g||!d&&!k&&L[u]||s(L,u,j),c[t]=j,c[S]=f,_)if(y={values:R?j:b("values"),keys:m?j:b("keys"),entries:C},g)for(v in y)v in L||i(L,v,y[v]);else r(r.P+r.F*(d||k),t,y);return y}},{"./_export":12,"./_hide":17,"./_iter-create":22,"./_iterators":25,"./_library":26,"./_object-gpo":30,"./_redefine":34,"./_set-to-string-tag":35,"./_wks":45}],24:[function(e,t,o){t.exports=function(e,t){return{value:t,done:!!e}}},{}],25:[function(e,t,o){t.exports={}},{}],26:[function(e,t,o){t.exports=!1},{}],27:[function(e,t,o){var n=e("./_an-object"),r=e("./_object-dps"),i=e("./_enum-bug-keys"),s=e("./_shared-key")("IE_PROTO"),c=function(){},a=function(){var t,o=e("./_dom-create")("iframe"),n=i.length;for(o.style.display="none",e("./_html").appendChild(o),o.src="javascript:",(t=o.contentWindow.document).open(),t.write(" - -comments powered by Disqus{{end}} -{{- end -}}`}, - {`google_analytics.html`, `{{- $pc := .Site.Config.Privacy.GoogleAnalytics -}} -{{- if not $pc.Disable -}} -{{ with .Site.GoogleAnalytics }} - -{{ end }} -{{- end -}} -{{- define "__ga_js_set_doNotTrack" -}}{{/* This is also used in the async version. */}} -{{- $pc := .Site.Config.Privacy.GoogleAnalytics -}} -{{- if not $pc.RespectDoNotTrack -}} -var doNotTrack = false; -{{- else -}} -var dnt = (navigator.doNotTrack || window.doNotTrack || navigator.msDoNotTrack); -var doNotTrack = (dnt == "1" || dnt == "yes"); -{{- end -}} -{{- end -}}`}, - {`google_analytics_async.html`, `{{- $pc := .Site.Config.Privacy.GoogleAnalytics -}} -{{- if not $pc.Disable -}} -{{ with .Site.GoogleAnalytics }} - - -{{ end }} -{{- end -}} -`}, - {`google_news.html`, `{{ if .IsPage }}{{ with .Params.news_keywords }} - -{{ end }}{{ end }}`}, - {`opengraph.html`, ` - - - -{{ with $.Param "images" }}{{ range first 6 . }} - -{{ end }}{{ end }} - -{{- $iso8601 := "2006-01-02T15:04:05-07:00" -}} -{{- if .IsPage }} -{{- if not .PublishDate.IsZero }} -{{ else if not .Date.IsZero }} -{{ end }} -{{- if not .Lastmod.IsZero }}{{ end }} -{{- else }} -{{- if not .Date.IsZero }} - -{{- end }} -{{- end }}{{/* .IsPage */}} - -{{- 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 }} - {{- range $page := first 6 $series.Pages }} - {{- if ne $page.Permalink $permalink }}{{ end }} - {{- end }} -{{ end }}{{ end }} - -{{- if .IsPage }} -{{- range .Site.Authors }}{{ with .Social.facebook }} -{{ end }}{{ with .Site.Social.facebook }} -{{ end }} - -{{- with .Params.tags }}{{ range first 6 . }} -{{ end }}{{ end }} -{{- end }}{{ end }} - -{{- /* Facebook Page Admin ID for Domain Insights */}} -{{- with .Site.Social.facebook_admin }}{{ end }} -`}, - {`pagination.html`, `{{ $pag := $.Paginator }} -{{ if gt $pag.TotalPages 1 }} -
      - {{ with $pag.First }} -
    • - -
    • - {{ end }} -
    • - -
    • - {{ $ellipsed := false }} - {{ $shouldEllipse := false }} - {{ range $pag.Pagers }} - {{ $right := sub .TotalPages .PageNumber }} - {{ $showNumber := or (le .PageNumber 3) (eq $right 0) }} - {{ $showNumber := or $showNumber (and (gt .PageNumber (sub $pag.PageNumber 2)) (lt .PageNumber (add $pag.PageNumber 2))) }} - {{ if $showNumber }} - {{ $ellipsed = false }} - {{ $shouldEllipse = false }} - {{ else }} - {{ $shouldEllipse = not $ellipsed }} - {{ $ellipsed = true }} - {{ end }} - {{ if $showNumber }} -
    • {{ .PageNumber }}
    • - {{ else if $shouldEllipse }} -
    • - {{ end }} - {{ end }} -
    • - -
    • - {{ with $pag.Last }} -
    • - -
    • - {{ end }} -
    -{{ end }} -`}, - {`schema.html`, ` - - -{{if .IsPage}}{{ $ISO8601 := "2006-01-02T15:04:05-07:00" }}{{ if not .PublishDate.IsZero }} -{{ end }} -{{ if not .Lastmod.IsZero }}{{ end }} - -{{ with .Params.images }}{{ range first 6 . }} - -{{ end }}{{ end }} - - - -{{ end }}`}, - {`shortcodes/__h_simple_assets.html`, `{{ define "__h_simple_css" }}{{/* These template definitions are global. */}} -{{- if not (.Page.Scratch.Get "__h_simple_css") -}} -{{/* Only include once */}} -{{- .Page.Scratch.Set "__h_simple_css" true -}} - -{{- end -}} -{{- end -}} -{{- define "__h_simple_icon_play" -}} - -{{- end -}} -`}, - {`shortcodes/figure.html`, ` - {{- if .Get "link" -}} - - {{- end }} - {{ with .Get - {{- if .Get "link" }}{{ end -}} - {{- if or (or (.Get "title") (.Get "caption")) (.Get "attr") -}} -
    - {{ with (.Get "title") -}} -

    {{ . }}

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

    - {{- .Get "caption" | markdownify -}} - {{- with .Get "attrlink" }} - - {{- end -}} - {{- .Get "attr" | markdownify -}} - {{- if .Get "attrlink" }}{{ end }}

    - {{- end }} -
    - {{- end }} -
    -`}, - {`shortcodes/gist.html`, ` -`}, - {`shortcodes/highlight.html`, `{{ if len .Params | eq 2 }}{{ highlight (trim .Inner "\n\r") (.Get 0) (.Get 1) }}{{ else }}{{ highlight (trim .Inner "\n\r") (.Get 0) "" }}{{ end }}`}, - {`shortcodes/instagram.html`, `{{- $pc := .Page.Site.Config.Privacy.Instagram -}} -{{- if not $pc.Disable -}} -{{- if $pc.Simple -}} -{{ template "_internal/shortcodes/instagram_simple.html" . }} -{{- else -}} -{{ $id := .Get 0 }} -{{ $hideCaption := cond (eq (.Get 1) "hidecaption") "1" "0" }} -{{ with getJSON "https://api.instagram.com/oembed/?url=https://instagram.com/p/" $id "/&hidecaption=" $hideCaption }}{{ .html | safeHTML }}{{ end }} -{{- end -}} -{{- end -}}`}, - {`shortcodes/instagram_simple.html`, `{{- $pc := .Page.Site.Config.Privacy.Instagram -}} -{{- $sc := .Page.Site.Config.Services.Instagram -}} -{{- if not $pc.Disable -}} -{{- $id := .Get 0 -}} -{{- $item := getJSON "https://api.instagram.com/oembed/?url=https://www.instagram.com/p/" $id "/&maxwidth=640&omitscript=true" -}} -{{- $class1 := "__h_instagram" -}} -{{- $class2 := "s_instagram_simple" -}} -{{- $hideCaption := (eq (.Get 1) "hidecaption") -}} -{{ with $item }} -{{- $mediaURL := printf "https://instagram.com/p/%s/" $id | safeURL -}} -{{- if not $sc.DisableInlineCSS -}} -{{ template "__h_simple_instagram_css" $ }} -{{- end -}} -
    - - Instagram Image -
    - {{ if not $hideCaption }}

    {{ $item.author_name }} {{ $item.title}}

    {{ end }} - View More on Instagram -
    -
    -{{ end }} -{{- end -}} - -{{ define "__h_simple_instagram_css" }} -{{ if not (.Page.Scratch.Get "__h_simple_instagram_css") }} -{{/* Only include once */}} -{{ .Page.Scratch.Set "__h_simple_instagram_css" true }} - -{{ end }} -{{ end }}`}, - {`shortcodes/param.html`, `{{- $name := (.Get 0) -}} -{{- with $name -}} -{{- with ($.Page.Param .) }}{{ . }}{{ else }}{{ errorf "Param %q not found: %s" $name $.Position }}{{ end -}} -{{- else }}{{ errorf "Missing param key: %s" $.Position }}{{ end -}}`}, - {`shortcodes/ref.html`, `{{ ref . .Params }}`}, - {`shortcodes/relref.html`, `{{ relref . .Params }}`}, - {`shortcodes/twitter.html`, `{{- $pc := .Page.Site.Config.Privacy.Twitter -}} -{{- if not $pc.Disable -}} -{{- if $pc.Simple -}} -{{ template "_internal/shortcodes/twitter_simple.html" . }} -{{- else -}} -{{- $url := printf "https://api.twitter.com/1/statuses/oembed.json?id=%v&dnt=%t" (index .Params 0) $pc.EnableDNT -}} -{{- $json := getJSON $url -}} -{{ $json.html | safeHTML }} -{{- end -}} -{{- end -}}`}, - {`shortcodes/twitter_simple.html`, `{{- $pc := .Page.Site.Config.Privacy.Twitter -}} -{{- $sc := .Page.Site.Config.Services.Twitter -}} -{{- if not $pc.Disable -}} -{{- $id := .Get 0 -}} -{{- $json := getJSON "https://api.twitter.com/1/statuses/oembed.json?id=" $id "&omit_script=true" -}} -{{- if not $sc.DisableInlineCSS -}} -{{ template "__h_simple_twitter_css" $ }} -{{- end -}} -{{ $json.html | safeHTML }} -{{- end -}} - -{{ define "__h_simple_twitter_css" }} -{{ if not (.Page.Scratch.Get "__h_simple_twitter_css") }} -{{/* Only include once */}} -{{ .Page.Scratch.Set "__h_simple_twitter_css" true }} - -{{ end }} -{{ end }}`}, - {`shortcodes/vimeo.html`, `{{- $pc := .Page.Site.Config.Privacy.Vimeo -}} -{{- if not $pc.Disable -}} -{{- if $pc.Simple -}} -{{ template "_internal/shortcodes/vimeo_simple.html" . }} -{{- else -}} -{{ if .IsNamedParams }}
    - -
    {{ else }} -
    - -
    -{{ end }} -{{- end -}} -{{- end -}}`}, - {`shortcodes/vimeo_simple.html`, `{{ $id := .Get "id" | default (.Get 0) }} -{{- $item := getJSON "https://vimeo.com/api/oembed.json?url=https://vimeo.com/" $id -}} -{{ $class := .Get "class" | default (.Get 1) }} -{{ $hasClass := $class }} -{{ $class := $class | default "__h_video" }} -{{ if not $hasClass }} -{{/* If class is set, assume the user wants to provide his own styles. */}} -{{ template "__h_simple_css" $ }} -{{ end }} -{{ $secondClass := "s_video_simple" }} - -{{- end -}} -`}, - {`shortcodes/youtube.html`, `{{- $pc := .Page.Site.Config.Privacy.YouTube -}} -{{- if not $pc.Disable -}} -{{- $ytHost := cond $pc.PrivacyEnhanced "www.youtube-nocookie.com" "www.youtube.com" -}} -{{- $id := .Get "id" | default (.Get 0) -}} -{{- $class := .Get "class" | default (.Get 1) }} -
    - -
    -{{ end -}} -`}, - {`twitter_cards.html`, `{{- with $.Params.images -}} - - -{{ else -}} -{{- $images := $.Resources.ByType "image" -}} -{{- $featured := $images.GetMatch "*feature*" -}} -{{- $featured := cond (ne $featured nil) $featured ($images.GetMatch "{*cover*,*thumbnail*}") -}} -{{- with $featured -}} - - -{{- else -}} -{{- with $.Site.Params.images -}} - - -{{ else -}} - -{{- end -}} -{{- end -}} -{{- end }} - - -{{ with .Site.Social.twitter -}} - -{{ end -}} -{{ range .Site.Authors }} -{{ with .twitter -}} - -{{ end -}} -{{ end -}}`}, -} diff --git a/tpl/tplimpl/embedded/templates/_default/robots.txt b/tpl/tplimpl/embedded/templates/_default/robots.txt deleted file mode 100644 index 4f9540ba3..000000000 --- a/tpl/tplimpl/embedded/templates/_default/robots.txt +++ /dev/null @@ -1 +0,0 @@ -User-agent: * \ No newline at end of file diff --git a/tpl/tplimpl/embedded/templates/_default/sitemap.xml b/tpl/tplimpl/embedded/templates/_default/sitemap.xml deleted file mode 100644 index cd2cab732..000000000 --- a/tpl/tplimpl/embedded/templates/_default/sitemap.xml +++ /dev/null @@ -1,22 +0,0 @@ -{{ printf "" | safeHTML }} - - {{ 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 }} - \ No newline at end of file diff --git a/tpl/tplimpl/embedded/templates/_default/sitemapindex.xml b/tpl/tplimpl/embedded/templates/_default/sitemapindex.xml deleted file mode 100644 index 62131a987..000000000 --- a/tpl/tplimpl/embedded/templates/_default/sitemapindex.xml +++ /dev/null @@ -1,11 +0,0 @@ -{{ printf "" | safeHTML }} - - {{ range . }} - - {{ .SitemapAbsURL }} - {{ if not .LastChange.IsZero }} - {{ .LastChange.Format "2006-01-02T15:04:05-07:00" | safeHTML }} - {{ end }} - - {{ end }} - diff --git a/tpl/tplimpl/embedded/templates/disqus.html b/tpl/tplimpl/embedded/templates/disqus.html deleted file mode 100644 index fed512ff0..000000000 --- a/tpl/tplimpl/embedded/templates/disqus.html +++ /dev/null @@ -1,23 +0,0 @@ -{{- $pc := .Site.Config.Privacy.Disqus -}} -{{- if not $pc.Disable -}} -{{ if .Site.DisqusShortname }}
    - - -comments powered by Disqus{{end}} -{{- end -}} \ No newline at end of file diff --git a/tpl/tplimpl/embedded/templates/google_analytics.html b/tpl/tplimpl/embedded/templates/google_analytics.html deleted file mode 100644 index 97588113e..000000000 --- a/tpl/tplimpl/embedded/templates/google_analytics.html +++ /dev/null @@ -1,39 +0,0 @@ -{{- $pc := .Site.Config.Privacy.GoogleAnalytics -}} -{{- if not $pc.Disable -}} -{{ with .Site.GoogleAnalytics }} - -{{ end }} -{{- end -}} -{{- define "__ga_js_set_doNotTrack" -}}{{/* This is also used in the async version. */}} -{{- $pc := .Site.Config.Privacy.GoogleAnalytics -}} -{{- if not $pc.RespectDoNotTrack -}} -var doNotTrack = false; -{{- else -}} -var dnt = (navigator.doNotTrack || window.doNotTrack || navigator.msDoNotTrack); -var doNotTrack = (dnt == "1" || dnt == "yes"); -{{- end -}} -{{- end -}} \ No newline at end of file diff --git a/tpl/tplimpl/embedded/templates/google_analytics_async.html b/tpl/tplimpl/embedded/templates/google_analytics_async.html deleted file mode 100644 index 499cb6fe3..000000000 --- a/tpl/tplimpl/embedded/templates/google_analytics_async.html +++ /dev/null @@ -1,28 +0,0 @@ -{{- $pc := .Site.Config.Privacy.GoogleAnalytics -}} -{{- if not $pc.Disable -}} -{{ with .Site.GoogleAnalytics }} - - -{{ end }} -{{- end -}} diff --git a/tpl/tplimpl/embedded/templates/google_news.html b/tpl/tplimpl/embedded/templates/google_news.html deleted file mode 100644 index 9361de16a..000000000 --- a/tpl/tplimpl/embedded/templates/google_news.html +++ /dev/null @@ -1,3 +0,0 @@ -{{ if .IsPage }}{{ with .Params.news_keywords }} - -{{ end }}{{ end }} \ No newline at end of file diff --git a/tpl/tplimpl/embedded/templates/opengraph.html b/tpl/tplimpl/embedded/templates/opengraph.html deleted file mode 100644 index de2d2fddf..000000000 --- a/tpl/tplimpl/embedded/templates/opengraph.html +++ /dev/null @@ -1,49 +0,0 @@ - - - - -{{ with $.Param "images" }}{{ range first 6 . }} - -{{ end }}{{ end }} - -{{- $iso8601 := "2006-01-02T15:04:05-07:00" -}} -{{- if .IsPage }} -{{- if not .PublishDate.IsZero }} -{{ else if not .Date.IsZero }} -{{ end }} -{{- if not .Lastmod.IsZero }}{{ end }} -{{- else }} -{{- if not .Date.IsZero }} - -{{- end }} -{{- end }}{{/* .IsPage */}} - -{{- 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 }} - {{- range $page := first 6 $series.Pages }} - {{- if ne $page.Permalink $permalink }}{{ end }} - {{- end }} -{{ end }}{{ end }} - -{{- if .IsPage }} -{{- range .Site.Authors }}{{ with .Social.facebook }} -{{ end }}{{ with .Site.Social.facebook }} -{{ end }} - -{{- with .Params.tags }}{{ range first 6 . }} -{{ end }}{{ end }} -{{- end }}{{ end }} - -{{- /* Facebook Page Admin ID for Domain Insights */}} -{{- with .Site.Social.facebook_admin }}{{ end }} diff --git a/tpl/tplimpl/embedded/templates/pagination.html b/tpl/tplimpl/embedded/templates/pagination.html deleted file mode 100644 index 896e655fe..000000000 --- a/tpl/tplimpl/embedded/templates/pagination.html +++ /dev/null @@ -1,40 +0,0 @@ -{{ $pag := $.Paginator }} -{{ if gt $pag.TotalPages 1 }} -
      - {{ with $pag.First }} -
    • - -
    • - {{ end }} -
    • - -
    • - {{ $ellipsed := false }} - {{ $shouldEllipse := false }} - {{ range $pag.Pagers }} - {{ $right := sub .TotalPages .PageNumber }} - {{ $showNumber := or (le .PageNumber 3) (eq $right 0) }} - {{ $showNumber := or $showNumber (and (gt .PageNumber (sub $pag.PageNumber 2)) (lt .PageNumber (add $pag.PageNumber 2))) }} - {{ if $showNumber }} - {{ $ellipsed = false }} - {{ $shouldEllipse = false }} - {{ else }} - {{ $shouldEllipse = not $ellipsed }} - {{ $ellipsed = true }} - {{ end }} - {{ if $showNumber }} -
    • {{ .PageNumber }}
    • - {{ else if $shouldEllipse }} -
    • - {{ end }} - {{ end }} -
    • - -
    • - {{ with $pag.Last }} -
    • - -
    • - {{ end }} -
    -{{ end }} diff --git a/tpl/tplimpl/embedded/templates/schema.html b/tpl/tplimpl/embedded/templates/schema.html deleted file mode 100644 index a76dbdd42..000000000 --- a/tpl/tplimpl/embedded/templates/schema.html +++ /dev/null @@ -1,14 +0,0 @@ - - - -{{if .IsPage}}{{ $ISO8601 := "2006-01-02T15:04:05-07:00" }}{{ if not .PublishDate.IsZero }} -{{ end }} -{{ if not .Lastmod.IsZero }}{{ end }} - -{{ with .Params.images }}{{ range first 6 . }} - -{{ end }}{{ end }} - - - -{{ end }} \ No newline at end of file diff --git a/tpl/tplimpl/embedded/templates/shortcodes/__h_simple_assets.html b/tpl/tplimpl/embedded/templates/shortcodes/__h_simple_assets.html deleted file mode 100644 index da1bb82eb..000000000 --- a/tpl/tplimpl/embedded/templates/shortcodes/__h_simple_assets.html +++ /dev/null @@ -1,34 +0,0 @@ -{{ define "__h_simple_css" }}{{/* These template definitions are global. */}} -{{- if not (.Page.Scratch.Get "__h_simple_css") -}} -{{/* Only include once */}} -{{- .Page.Scratch.Set "__h_simple_css" true -}} - -{{- end -}} -{{- end -}} -{{- define "__h_simple_icon_play" -}} - -{{- end -}} diff --git a/tpl/tplimpl/embedded/templates/shortcodes/figure.html b/tpl/tplimpl/embedded/templates/shortcodes/figure.html deleted file mode 100644 index f81bdadfc..000000000 --- a/tpl/tplimpl/embedded/templates/shortcodes/figure.html +++ /dev/null @@ -1,28 +0,0 @@ - - {{- if .Get "link" -}} - - {{- end }} - {{ with .Get - {{- if .Get "link" }}{{ end -}} - {{- if or (or (.Get "title") (.Get "caption")) (.Get "attr") -}} -
    - {{ with (.Get "title") -}} -

    {{ . }}

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

    - {{- .Get "caption" | markdownify -}} - {{- with .Get "attrlink" }} - - {{- end -}} - {{- .Get "attr" | markdownify -}} - {{- if .Get "attrlink" }}{{ end }}

    - {{- end }} -
    - {{- end }} - diff --git a/tpl/tplimpl/embedded/templates/shortcodes/gist.html b/tpl/tplimpl/embedded/templates/shortcodes/gist.html deleted file mode 100644 index 781b26567..000000000 --- a/tpl/tplimpl/embedded/templates/shortcodes/gist.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tpl/tplimpl/embedded/templates/shortcodes/highlight.html b/tpl/tplimpl/embedded/templates/shortcodes/highlight.html deleted file mode 100644 index b063f92ad..000000000 --- a/tpl/tplimpl/embedded/templates/shortcodes/highlight.html +++ /dev/null @@ -1 +0,0 @@ -{{ if len .Params | eq 2 }}{{ highlight (trim .Inner "\n\r") (.Get 0) (.Get 1) }}{{ else }}{{ highlight (trim .Inner "\n\r") (.Get 0) "" }}{{ end }} \ No newline at end of file diff --git a/tpl/tplimpl/embedded/templates/shortcodes/instagram.html b/tpl/tplimpl/embedded/templates/shortcodes/instagram.html deleted file mode 100644 index 67ff2e72c..000000000 --- a/tpl/tplimpl/embedded/templates/shortcodes/instagram.html +++ /dev/null @@ -1,10 +0,0 @@ -{{- $pc := .Page.Site.Config.Privacy.Instagram -}} -{{- if not $pc.Disable -}} -{{- if $pc.Simple -}} -{{ template "_internal/shortcodes/instagram_simple.html" . }} -{{- else -}} -{{ $id := .Get 0 }} -{{ $hideCaption := cond (eq (.Get 1) "hidecaption") "1" "0" }} -{{ with getJSON "https://api.instagram.com/oembed/?url=https://instagram.com/p/" $id "/&hidecaption=" $hideCaption }}{{ .html | safeHTML }}{{ end }} -{{- end -}} -{{- end -}} \ No newline at end of file diff --git a/tpl/tplimpl/embedded/templates/shortcodes/instagram_simple.html b/tpl/tplimpl/embedded/templates/shortcodes/instagram_simple.html deleted file mode 100644 index d816093a6..000000000 --- a/tpl/tplimpl/embedded/templates/shortcodes/instagram_simple.html +++ /dev/null @@ -1,48 +0,0 @@ -{{- $pc := .Page.Site.Config.Privacy.Instagram -}} -{{- $sc := .Page.Site.Config.Services.Instagram -}} -{{- if not $pc.Disable -}} -{{- $id := .Get 0 -}} -{{- $item := getJSON "https://api.instagram.com/oembed/?url=https://www.instagram.com/p/" $id "/&maxwidth=640&omitscript=true" -}} -{{- $class1 := "__h_instagram" -}} -{{- $class2 := "s_instagram_simple" -}} -{{- $hideCaption := (eq (.Get 1) "hidecaption") -}} -{{ with $item }} -{{- $mediaURL := printf "https://instagram.com/p/%s/" $id | safeURL -}} -{{- if not $sc.DisableInlineCSS -}} -{{ template "__h_simple_instagram_css" $ }} -{{- end -}} -
    - - Instagram Image -
    - {{ if not $hideCaption }}

    {{ $item.author_name }} {{ $item.title}}

    {{ end }} - View More on Instagram -
    -
    -{{ end }} -{{- end -}} - -{{ define "__h_simple_instagram_css" }} -{{ if not (.Page.Scratch.Get "__h_simple_instagram_css") }} -{{/* Only include once */}} -{{ .Page.Scratch.Set "__h_simple_instagram_css" true }} - -{{ end }} -{{ end }} \ No newline at end of file diff --git a/tpl/tplimpl/embedded/templates/shortcodes/param.html b/tpl/tplimpl/embedded/templates/shortcodes/param.html deleted file mode 100644 index 74aa3ee7b..000000000 --- a/tpl/tplimpl/embedded/templates/shortcodes/param.html +++ /dev/null @@ -1,4 +0,0 @@ -{{- $name := (.Get 0) -}} -{{- with $name -}} -{{- with ($.Page.Param .) }}{{ . }}{{ else }}{{ errorf "Param %q not found: %s" $name $.Position }}{{ end -}} -{{- else }}{{ errorf "Missing param key: %s" $.Position }}{{ end -}} \ No newline at end of file diff --git a/tpl/tplimpl/embedded/templates/shortcodes/ref.html b/tpl/tplimpl/embedded/templates/shortcodes/ref.html deleted file mode 100644 index cd9c3defc..000000000 --- a/tpl/tplimpl/embedded/templates/shortcodes/ref.html +++ /dev/null @@ -1 +0,0 @@ -{{ ref . .Params }} \ No newline at end of file diff --git a/tpl/tplimpl/embedded/templates/shortcodes/relref.html b/tpl/tplimpl/embedded/templates/shortcodes/relref.html deleted file mode 100644 index 82005bd82..000000000 --- a/tpl/tplimpl/embedded/templates/shortcodes/relref.html +++ /dev/null @@ -1 +0,0 @@ -{{ relref . .Params }} \ No newline at end of file diff --git a/tpl/tplimpl/embedded/templates/shortcodes/twitter.html b/tpl/tplimpl/embedded/templates/shortcodes/twitter.html deleted file mode 100644 index e2c4983d7..000000000 --- a/tpl/tplimpl/embedded/templates/shortcodes/twitter.html +++ /dev/null @@ -1,10 +0,0 @@ -{{- $pc := .Page.Site.Config.Privacy.Twitter -}} -{{- if not $pc.Disable -}} -{{- if $pc.Simple -}} -{{ template "_internal/shortcodes/twitter_simple.html" . }} -{{- else -}} -{{- $url := printf "https://api.twitter.com/1/statuses/oembed.json?id=%v&dnt=%t" (index .Params 0) $pc.EnableDNT -}} -{{- $json := getJSON $url -}} -{{ $json.html | safeHTML }} -{{- end -}} -{{- end -}} \ No newline at end of file diff --git a/tpl/tplimpl/embedded/templates/shortcodes/twitter_simple.html b/tpl/tplimpl/embedded/templates/shortcodes/twitter_simple.html deleted file mode 100644 index 45d594fd9..000000000 --- a/tpl/tplimpl/embedded/templates/shortcodes/twitter_simple.html +++ /dev/null @@ -1,33 +0,0 @@ -{{- $pc := .Page.Site.Config.Privacy.Twitter -}} -{{- $sc := .Page.Site.Config.Services.Twitter -}} -{{- if not $pc.Disable -}} -{{- $id := .Get 0 -}} -{{- $json := getJSON "https://api.twitter.com/1/statuses/oembed.json?id=" $id "&omit_script=true" -}} -{{- if not $sc.DisableInlineCSS -}} -{{ template "__h_simple_twitter_css" $ }} -{{- end -}} -{{ $json.html | safeHTML }} -{{- end -}} - -{{ define "__h_simple_twitter_css" }} -{{ if not (.Page.Scratch.Get "__h_simple_twitter_css") }} -{{/* Only include once */}} -{{ .Page.Scratch.Set "__h_simple_twitter_css" true }} - -{{ end }} -{{ end }} \ No newline at end of file diff --git a/tpl/tplimpl/embedded/templates/shortcodes/vimeo.html b/tpl/tplimpl/embedded/templates/shortcodes/vimeo.html deleted file mode 100644 index 1680c1694..000000000 --- a/tpl/tplimpl/embedded/templates/shortcodes/vimeo.html +++ /dev/null @@ -1,14 +0,0 @@ -{{- $pc := .Page.Site.Config.Privacy.Vimeo -}} -{{- if not $pc.Disable -}} -{{- if $pc.Simple -}} -{{ template "_internal/shortcodes/vimeo_simple.html" . }} -{{- else -}} -{{ if .IsNamedParams }}
    - -
    {{ else }} -
    - -
    -{{ end }} -{{- end -}} -{{- end -}} \ No newline at end of file diff --git a/tpl/tplimpl/embedded/templates/shortcodes/vimeo_simple.html b/tpl/tplimpl/embedded/templates/shortcodes/vimeo_simple.html deleted file mode 100644 index 9a4fb794d..000000000 --- a/tpl/tplimpl/embedded/templates/shortcodes/vimeo_simple.html +++ /dev/null @@ -1,18 +0,0 @@ -{{ $id := .Get "id" | default (.Get 0) }} -{{- $item := getJSON "https://vimeo.com/api/oembed.json?url=https://vimeo.com/" $id -}} -{{ $class := .Get "class" | default (.Get 1) }} -{{ $hasClass := $class }} -{{ $class := $class | default "__h_video" }} -{{ if not $hasClass }} -{{/* If class is set, assume the user wants to provide his own styles. */}} -{{ template "__h_simple_css" $ }} -{{ end }} -{{ $secondClass := "s_video_simple" }} - -{{- end -}} diff --git a/tpl/tplimpl/embedded/templates/shortcodes/youtube.html b/tpl/tplimpl/embedded/templates/shortcodes/youtube.html deleted file mode 100644 index 9e8477659..000000000 --- a/tpl/tplimpl/embedded/templates/shortcodes/youtube.html +++ /dev/null @@ -1,9 +0,0 @@ -{{- $pc := .Page.Site.Config.Privacy.YouTube -}} -{{- if not $pc.Disable -}} -{{- $ytHost := cond $pc.PrivacyEnhanced "www.youtube-nocookie.com" "www.youtube.com" -}} -{{- $id := .Get "id" | default (.Get 0) -}} -{{- $class := .Get "class" | default (.Get 1) }} -
    - -
    -{{ end -}} diff --git a/tpl/tplimpl/embedded/templates/twitter_cards.html b/tpl/tplimpl/embedded/templates/twitter_cards.html deleted file mode 100644 index fc4895b56..000000000 --- a/tpl/tplimpl/embedded/templates/twitter_cards.html +++ /dev/null @@ -1,29 +0,0 @@ -{{- with $.Params.images -}} - - -{{ else -}} -{{- $images := $.Resources.ByType "image" -}} -{{- $featured := $images.GetMatch "*feature*" -}} -{{- $featured := cond (ne $featured nil) $featured ($images.GetMatch "{*cover*,*thumbnail*}") -}} -{{- with $featured -}} - - -{{- else -}} -{{- with $.Site.Params.images -}} - - -{{ else -}} - -{{- end -}} -{{- end -}} -{{- end }} - - -{{ with .Site.Social.twitter -}} - -{{ end -}} -{{ range .Site.Authors }} -{{ with .twitter -}} - -{{ end -}} -{{ end -}} \ No newline at end of file diff --git a/tpl/tplimpl/shortcodes.go b/tpl/tplimpl/shortcodes.go deleted file mode 100644 index b41725463..000000000 --- a/tpl/tplimpl/shortcodes.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 tplimpl - -import ( - "strings" - - "github.com/gohugoio/hugo/tpl" -) - -// Currently lang, outFormat, suffix -const numTemplateVariants = 3 - -type shortcodeVariant struct { - - // The possible variants: lang, outFormat, suffix - // gtag - // gtag.html - // gtag.no.html - // gtag.no.amp.html - // A slice of length numTemplateVariants. - variants []string - - info tpl.Info - templ tpl.Template -} - -type shortcodeTemplates struct { - variants []shortcodeVariant -} - -func (s *shortcodeTemplates) indexOf(variants []string) int { -L: - for i, v1 := range s.variants { - for i, v2 := range v1.variants { - if v2 != variants[i] { - continue L - } - } - return i - } - return -1 -} - -func (s *shortcodeTemplates) fromVariants(variants tpl.TemplateVariants) (shortcodeVariant, bool) { - return s.fromVariantsSlice([]string{ - variants.Language, - strings.ToLower(variants.OutputFormat.Name), - variants.OutputFormat.MediaType.Suffix(), - }) -} - -// Get the most specific template given a full name, e.g gtag.no.amp.html. -func (s *shortcodeTemplates) fromName(name string) (shortcodeVariant, bool) { - return s.fromVariantsSlice(templateVariants(name)) -} - -func (s *shortcodeTemplates) fromVariantsSlice(variants []string) (shortcodeVariant, bool) { - var ( - bestMatch shortcodeVariant - bestMatchWeight int - ) - - for _, variant := range s.variants { - w := s.compareVariants(variants, variant.variants) - if bestMatchWeight == 0 || w > bestMatchWeight { - bestMatch = variant - bestMatchWeight = w - } - } - - return bestMatch, true -} - -// calculate a weight for two string slices of same length. -// higher value means "better match". -func (s *shortcodeTemplates) compareVariants(a, b []string) int { - - weight := 0 - for i, av := range a { - bv := b[i] - if av == bv { - weight++ - } else { - weight-- - } - } - return weight -} - -func templateVariants(name string) []string { - _, variants := templateNameAndVariants(name) - return variants -} - -func templateNameAndVariants(name string) (string, []string) { - - variants := make([]string, numTemplateVariants) - - parts := strings.Split(name, ".") - - if len(parts) <= 1 { - // No variants. - return name, variants - } - - name = parts[0] - parts = parts[1:] - lp := len(parts) - start := len(variants) - lp - - for i, j := start, 0; i < len(variants); i, j = i+1, j+1 { - variants[i] = parts[j] - } - - if lp > 1 && lp < len(variants) { - for i := lp - 1; i > 0; i-- { - variants[i-1] = variants[i] - } - } - - if lp == 1 { - // Suffix only. Duplicate it into the output format field to - // make HTML win over AMP. - variants[len(variants)-2] = variants[len(variants)-1] - } - - return name, variants -} - -func resolveTemplateType(name string) templateType { - if isShortcode(name) { - return templateShortcode - } - - if strings.Contains(name, "partials/") { - return templatePartial - } - - return templateUndefined -} - -func isShortcode(name string) bool { - return strings.Contains(name, shortcodesPathPrefix) -} - -func isInternal(name string) bool { - return strings.HasPrefix(name, internalPathPrefix) -} diff --git a/tpl/tplimpl/shortcodes_test.go b/tpl/tplimpl/shortcodes_test.go deleted file mode 100644 index 08200444d..000000000 --- a/tpl/tplimpl/shortcodes_test.go +++ /dev/null @@ -1,97 +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 tplimpl - -import ( - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestShortcodesTemplate(t *testing.T) { - - t.Run("isShortcode", func(t *testing.T) { - c := qt.New(t) - c.Assert(isShortcode("shortcodes/figures.html"), qt.Equals, true) - c.Assert(isShortcode("_internal/shortcodes/figures.html"), qt.Equals, true) - c.Assert(isShortcode("shortcodes\\figures.html"), qt.Equals, false) - c.Assert(isShortcode("myshortcodes"), qt.Equals, false) - - }) - - t.Run("variantsFromName", func(t *testing.T) { - c := qt.New(t) - c.Assert(templateVariants("figure.html"), qt.DeepEquals, []string{"", "html", "html"}) - c.Assert(templateVariants("figure.no.html"), qt.DeepEquals, []string{"no", "no", "html"}) - c.Assert(templateVariants("figure.no.amp.html"), qt.DeepEquals, []string{"no", "amp", "html"}) - c.Assert(templateVariants("figure.amp.html"), qt.DeepEquals, []string{"amp", "amp", "html"}) - - name, variants := templateNameAndVariants("figure.html") - c.Assert(name, qt.Equals, "figure") - c.Assert(variants, qt.DeepEquals, []string{"", "html", "html"}) - - }) - - t.Run("compareVariants", func(t *testing.T) { - c := qt.New(t) - var s *shortcodeTemplates - - tests := []struct { - name string - name1 string - name2 string - expected int - }{ - {"Same suffix", "figure.html", "figure.html", 3}, - {"Same suffix and output format", "figure.html.html", "figure.html.html", 3}, - {"Same suffix, output format and language", "figure.no.html.html", "figure.no.html.html", 3}, - {"No suffix", "figure", "figure", 3}, - {"Different output format", "figure.amp.html", "figure.html.html", -1}, - {"One with output format, one without", "figure.amp.html", "figure.html", -1}, - } - - for _, test := range tests { - w := s.compareVariants(templateVariants(test.name1), templateVariants(test.name2)) - c.Assert(w, qt.Equals, test.expected) - } - - }) - - t.Run("indexOf", func(t *testing.T) { - c := qt.New(t) - - s := &shortcodeTemplates{ - variants: []shortcodeVariant{ - {variants: []string{"a", "b", "c"}}, - {variants: []string{"a", "b", "d"}}, - }, - } - - c.Assert(s.indexOf([]string{"a", "b", "c"}), qt.Equals, 0) - c.Assert(s.indexOf([]string{"a", "b", "d"}), qt.Equals, 1) - c.Assert(s.indexOf([]string{"a", "b", "x"}), qt.Equals, -1) - - }) - - t.Run("Name", func(t *testing.T) { - c := qt.New(t) - - c.Assert(templateBaseName(templateShortcode, "shortcodes/foo.html"), qt.Equals, "foo.html") - c.Assert(templateBaseName(templateShortcode, "_internal/shortcodes/foo.html"), qt.Equals, "foo.html") - c.Assert(templateBaseName(templateShortcode, "shortcodes/test/foo.html"), qt.Equals, "test/foo.html") - - c.Assert(true, qt.Equals, true) - - }) -} diff --git a/tpl/tplimpl/template.go b/tpl/tplimpl/template.go deleted file mode 100644 index 35407722a..000000000 --- a/tpl/tplimpl/template.go +++ /dev/null @@ -1,1057 +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 tplimpl - -import ( - "fmt" - "html/template" - "strings" - texttemplate "text/template" - "text/template/parse" - - "github.com/gohugoio/hugo/hugofs" - "github.com/gohugoio/hugo/tpl/tplimpl/embedded" - "github.com/pkg/errors" - - "github.com/eknkc/amber" - - "os" - - "github.com/gohugoio/hugo/output" - - "path/filepath" - "sync" - - "github.com/gohugoio/hugo/deps" - "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/tpl" - "github.com/spf13/afero" -) - -const ( - textTmplNamePrefix = "_text/" -) - -var ( - _ tpl.TemplateHandler = (*templateHandler)(nil) - _ tpl.TemplateDebugger = (*templateHandler)(nil) - _ tpl.TemplateFuncsGetter = (*templateHandler)(nil) - _ tpl.TemplateTestMocker = (*templateHandler)(nil) - _ tpl.TemplateFinder = (*htmlTemplates)(nil) - _ tpl.TemplateFinder = (*textTemplates)(nil) - _ templateLoader = (*htmlTemplates)(nil) - _ templateLoader = (*textTemplates)(nil) - _ templateFuncsterTemplater = (*htmlTemplates)(nil) - _ templateFuncsterTemplater = (*textTemplates)(nil) -) - -// Protecting global map access (Amber) -var amberMu sync.Mutex - -type templateErr struct { - name string - err error -} - -type templateLoader interface { - handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (templateInfo, error)) error - addTemplate(name, tpl string) (*templateContext, error) - addLateTemplate(name, tpl string) error -} - -type templateFuncsterTemplater interface { - templateFuncsterSetter - tpl.TemplateFinder - setFuncs(funcMap map[string]interface{}) -} - -type templateFuncsterSetter interface { - setTemplateFuncster(f *templateFuncster) -} - -// templateHandler holds the templates in play. -// It implements the templateLoader and tpl.TemplateHandler interfaces. -type templateHandler struct { - mu sync.Mutex - - // shortcodes maps shortcode name to template variants - // (language, output format etc.) of that shortcode. - shortcodes map[string]*shortcodeTemplates - - // templateInfo maps template name to some additional information about that template. - // Note that for shortcodes that same information is embedded in the - // shortcodeTemplates type. - templateInfo map[string]tpl.Info - - // text holds all the pure text templates. - text *textTemplates - html *htmlTemplates - - extTextTemplates []*textTemplate - - amberFuncMap template.FuncMap - - errors []*templateErr - - // This is the filesystem to load the templates from. All the templates are - // stored in the root of this filesystem. - layoutsFs afero.Fs - - *deps.Deps -} - -const ( - shortcodesPathPrefix = "shortcodes/" - internalPathPrefix = "_internal/" -) - -// resolves _internal/shortcodes/param.html => param.html etc. -func templateBaseName(typ templateType, name string) string { - name = strings.TrimPrefix(name, internalPathPrefix) - switch typ { - case templateShortcode: - return strings.TrimPrefix(name, shortcodesPathPrefix) - default: - panic("not implemented") - } - -} - -func (t *templateHandler) addShortcodeVariant(name string, info tpl.Info, templ tpl.Template) { - base := templateBaseName(templateShortcode, name) - - shortcodename, variants := templateNameAndVariants(base) - - templs, found := t.shortcodes[shortcodename] - if !found { - templs = &shortcodeTemplates{} - t.shortcodes[shortcodename] = templs - } - - sv := shortcodeVariant{variants: variants, info: info, templ: templ} - - i := templs.indexOf(variants) - - if i != -1 { - // Only replace if it's an override of an internal template. - if !isInternal(name) { - templs.variants[i] = sv - } - } else { - templs.variants = append(templs.variants, sv) - } -} - -// NewTextTemplate provides a text template parser that has all the Hugo -// template funcs etc. built-in. -func (t *templateHandler) NewTextTemplate() tpl.TemplateParseFinder { - t.mu.Lock() - defer t.mu.Unlock() - - tt := &textTemplate{t: texttemplate.New("")} - t.extTextTemplates = append(t.extTextTemplates, tt) - - return struct { - tpl.TemplateParser - tpl.TemplateLookup - tpl.TemplateLookupVariant - }{ - tt, - tt, - new(nopLookupVariant), - } - -} - -type nopLookupVariant int - -func (l nopLookupVariant) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) { - return nil, false, false -} - -func (t *templateHandler) Debug() { - fmt.Println("HTML templates:\n", t.html.t.DefinedTemplates()) - fmt.Println("\n\nText templates:\n", t.text.t.DefinedTemplates()) -} - -// Lookup tries to find a template with the given name in both template -// collections: First HTML, then the plain text template collection. -func (t *templateHandler) Lookup(name string) (tpl.Template, bool) { - - if strings.HasPrefix(name, textTmplNamePrefix) { - // The caller has explicitly asked for a text template, so only look - // in the text template collection. - // The templates are stored without the prefix identificator. - name = strings.TrimPrefix(name, textTmplNamePrefix) - - return t.applyTemplateInfo(t.text.Lookup(name)) - } - - // Look in both - if te, found := t.html.Lookup(name); found { - return t.applyTemplateInfo(te, true) - } - - return t.applyTemplateInfo(t.text.Lookup(name)) - -} - -func (t *templateHandler) applyTemplateInfo(templ tpl.Template, found bool) (tpl.Template, bool) { - if adapter, ok := templ.(*tpl.TemplateAdapter); ok { - if adapter.Info.IsZero() { - if info, found := t.templateInfo[templ.Name()]; found { - adapter.Info = info - } - } - } - - return templ, found -} - -// This currently only applies to shortcodes and what we get here is the -// shortcode name. -func (t *templateHandler) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) { - name = templateBaseName(templateShortcode, name) - s, found := t.shortcodes[name] - if !found { - return nil, false, false - } - - sv, found := s.fromVariants(variants) - if !found { - return nil, false, false - } - - more := len(s.variants) > 1 - - return &tpl.TemplateAdapter{ - Template: sv.templ, - Info: sv.info, - Metrics: t.Deps.Metrics, - Fs: t.layoutsFs, - NameBaseTemplateName: t.html.nameBaseTemplateName}, true, more - -} - -func (t *textTemplates) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) { - return t.handler.LookupVariant(name, variants) -} - -func (t *htmlTemplates) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) { - return t.handler.LookupVariant(name, variants) -} - -func (t *templateHandler) lookupTemplate(in interface{}) tpl.Template { - switch templ := in.(type) { - case *texttemplate.Template: - return t.text.lookup(templ.Name()) - case *template.Template: - return t.html.lookup(templ.Name()) - } - - panic(fmt.Sprintf("%T is not a template", in)) -} - -func (t *templateHandler) setFuncMapInTemplate(in interface{}, funcs map[string]interface{}) { - switch templ := in.(type) { - case *texttemplate.Template: - templ.Funcs(funcs) - return - case *template.Template: - templ.Funcs(funcs) - return - } - - panic(fmt.Sprintf("%T is not a template", in)) -} - -func (t *templateHandler) clone(d *deps.Deps) *templateHandler { - c := &templateHandler{ - Deps: d, - layoutsFs: d.BaseFs.Layouts.Fs, - shortcodes: make(map[string]*shortcodeTemplates), - templateInfo: t.templateInfo, - html: &htmlTemplates{t: template.Must(t.html.t.Clone()), overlays: make(map[string]*template.Template), templatesCommon: t.html.templatesCommon}, - text: &textTemplates{textTemplate: &textTemplate{t: texttemplate.Must(t.text.t.Clone())}, overlays: make(map[string]*texttemplate.Template), templatesCommon: t.text.templatesCommon}, - errors: make([]*templateErr, 0), - } - - for k, v := range t.shortcodes { - other := *v - variantsc := make([]shortcodeVariant, len(v.variants)) - for i, variant := range v.variants { - variantsc[i] = shortcodeVariant{ - info: variant.info, - variants: variant.variants, - templ: c.lookupTemplate(variant.templ), - } - } - other.variants = variantsc - c.shortcodes[k] = &other - } - - d.Tmpl = c - - c.initFuncs() - - for k, v := range t.html.overlays { - vc := template.Must(v.Clone()) - // The extra lookup is a workaround, see - // * https://github.com/golang/go/issues/16101 - // * https://github.com/gohugoio/hugo/issues/2549 - vc = vc.Lookup(vc.Name()) - vc.Funcs(c.html.funcster.funcMap) - c.html.overlays[k] = vc - } - - for k, v := range t.text.overlays { - vc := texttemplate.Must(v.Clone()) - vc = vc.Lookup(vc.Name()) - vc.Funcs(texttemplate.FuncMap(c.text.funcster.funcMap)) - c.text.overlays[k] = vc - } - - return c - -} - -func newTemplateAdapter(deps *deps.Deps) *templateHandler { - common := &templatesCommon{ - nameBaseTemplateName: make(map[string]string), - transformNotFound: make(map[string]bool), - } - - htmlT := &htmlTemplates{ - t: template.New(""), - overlays: make(map[string]*template.Template), - templatesCommon: common, - } - textT := &textTemplates{ - textTemplate: &textTemplate{t: texttemplate.New("")}, - overlays: make(map[string]*texttemplate.Template), - templatesCommon: common, - } - h := &templateHandler{ - Deps: deps, - layoutsFs: deps.BaseFs.Layouts.Fs, - shortcodes: make(map[string]*shortcodeTemplates), - templateInfo: make(map[string]tpl.Info), - html: htmlT, - text: textT, - errors: make([]*templateErr, 0), - } - - common.handler = h - - return h - -} - -// Shared by both HTML and text templates. -type templatesCommon struct { - handler *templateHandler - funcster *templateFuncster - - // Used to get proper filenames in errors - nameBaseTemplateName map[string]string - - // Holds names of the templates not found during the first AST transformation - // pass. - transformNotFound map[string]bool -} -type htmlTemplates struct { - mu sync.RWMutex - - *templatesCommon - - t *template.Template - - // This looks, and is, strange. - // The clone is used by non-renderable content pages, and these need to be - // re-parsed on content change, and to avoid the - // "cannot Parse after Execute" error, we need to re-clone it from the original clone. - clone *template.Template - cloneClone *template.Template - - // a separate storage for the overlays created from cloned master templates. - // note: No mutex protection, so we add these in one Go routine, then just read. - overlays map[string]*template.Template -} - -func (t *htmlTemplates) setTemplateFuncster(f *templateFuncster) { - t.funcster = f -} - -func (t *htmlTemplates) Lookup(name string) (tpl.Template, bool) { - templ := t.lookup(name) - if templ == nil { - return nil, false - } - - return &tpl.TemplateAdapter{Template: templ, Metrics: t.funcster.Deps.Metrics, Fs: t.handler.layoutsFs, NameBaseTemplateName: t.nameBaseTemplateName}, true -} - -func (t *htmlTemplates) lookup(name string) *template.Template { - t.mu.RLock() - defer t.mu.RUnlock() - - // Need to check in the overlay registry first as it will also be found below. - if t.overlays != nil { - if templ, ok := t.overlays[name]; ok { - return templ - } - } - - if templ := t.t.Lookup(name); templ != nil { - return templ - } - - if t.clone != nil { - return t.clone.Lookup(name) - } - - return nil -} - -func (t *textTemplates) setTemplateFuncster(f *templateFuncster) { - t.funcster = f -} - -type textTemplates struct { - *templatesCommon - *textTemplate - clone *texttemplate.Template - cloneClone *texttemplate.Template - - overlays map[string]*texttemplate.Template -} - -func (t *textTemplates) Lookup(name string) (tpl.Template, bool) { - templ := t.lookup(name) - if templ == nil { - return nil, false - } - return &tpl.TemplateAdapter{Template: templ, Metrics: t.funcster.Deps.Metrics, Fs: t.handler.layoutsFs, NameBaseTemplateName: t.nameBaseTemplateName}, true -} - -func (t *textTemplates) lookup(name string) *texttemplate.Template { - - // Need to check in the overlay registry first as it will also be found below. - if t.overlays != nil { - if templ, ok := t.overlays[name]; ok { - return templ - } - } - - if templ := t.t.Lookup(name); templ != nil { - return templ - } - - if t.clone != nil { - return t.clone.Lookup(name) - } - - return nil -} - -func (t *templateHandler) setFuncs(funcMap map[string]interface{}) { - t.html.setFuncs(funcMap) - t.text.setFuncs(funcMap) -} - -// SetFuncs replaces the funcs in the func maps with new definitions. -// This is only used in tests. -func (t *templateHandler) SetFuncs(funcMap map[string]interface{}) { - t.setFuncs(funcMap) -} - -func (t *templateHandler) GetFuncs() map[string]interface{} { - return t.html.funcster.funcMap -} - -func (t *htmlTemplates) setFuncs(funcMap map[string]interface{}) { - t.t.Funcs(funcMap) -} - -func (t *textTemplates) setFuncs(funcMap map[string]interface{}) { - t.t.Funcs(funcMap) -} - -// LoadTemplates loads the templates from the layouts filesystem. -// A prefix can be given to indicate a template namespace to load the templates -// into, i.e. "_internal" etc. -func (t *templateHandler) LoadTemplates(prefix string) error { - return t.loadTemplates(prefix) - -} - -func (t *htmlTemplates) addTemplateIn(tt *template.Template, name, tpl string) (*templateContext, error) { - t.mu.Lock() - defer t.mu.Unlock() - - templ, err := tt.New(name).Parse(tpl) - if err != nil { - return nil, err - } - - typ := resolveTemplateType(name) - - c, err := applyTemplateTransformersToHMLTTemplate(typ, templ) - if err != nil { - return nil, err - } - - for k := range c.notFound { - t.transformNotFound[k] = true - } - - if typ == templateShortcode { - t.handler.addShortcodeVariant(name, c.Info, templ) - } else { - t.handler.templateInfo[name] = c.Info - } - - return c, nil -} - -func (t *htmlTemplates) addTemplate(name, tpl string) (*templateContext, error) { - return t.addTemplateIn(t.t, name, tpl) -} - -func (t *htmlTemplates) addLateTemplate(name, tpl string) error { - _, err := t.addTemplateIn(t.clone, name, tpl) - return err -} - -type textTemplate struct { - mu sync.RWMutex - t *texttemplate.Template -} - -func (t *textTemplate) Parse(name, tpl string) (tpl.Template, error) { - return t.parseIn(t.t, name, tpl) -} - -func (t *textTemplate) Lookup(name string) (tpl.Template, bool) { - t.mu.RLock() - defer t.mu.RUnlock() - - tpl := t.t.Lookup(name) - return tpl, tpl != nil -} - -func (t *textTemplate) parseIn(tt *texttemplate.Template, name, tpl string) (*texttemplate.Template, error) { - t.mu.Lock() - defer t.mu.Unlock() - - templ, err := tt.New(name).Parse(tpl) - if err != nil { - return nil, err - } - - if _, err := applyTemplateTransformersToTextTemplate(templateUndefined, templ); err != nil { - return nil, err - } - return templ, nil -} - -func (t *textTemplates) addTemplateIn(tt *texttemplate.Template, name, tpl string) (*templateContext, error) { - name = strings.TrimPrefix(name, textTmplNamePrefix) - templ, err := t.parseIn(tt, name, tpl) - if err != nil { - return nil, err - } - - typ := resolveTemplateType(name) - - c, err := applyTemplateTransformersToTextTemplate(typ, templ) - if err != nil { - return nil, err - } - - for k := range c.notFound { - t.transformNotFound[k] = true - } - - if typ == templateShortcode { - t.handler.addShortcodeVariant(name, c.Info, templ) - } else { - t.handler.templateInfo[name] = c.Info - } - - return c, nil -} - -func (t *textTemplates) addTemplate(name, tpl string) (*templateContext, error) { - return t.addTemplateIn(t.t, name, tpl) -} - -func (t *textTemplates) addLateTemplate(name, tpl string) error { - _, err := t.addTemplateIn(t.clone, name, tpl) - return err -} - -func (t *templateHandler) addTemplate(name, tpl string) error { - return t.AddTemplate(name, tpl) -} - -func (t *templateHandler) postTransform() error { - if len(t.html.transformNotFound) == 0 && len(t.text.transformNotFound) == 0 { - return nil - } - - defer func() { - t.text.transformNotFound = make(map[string]bool) - t.html.transformNotFound = make(map[string]bool) - }() - - for _, s := range []struct { - lookup func(name string) *parse.Tree - transformNotFound map[string]bool - }{ - // html templates - {func(name string) *parse.Tree { - templ := t.html.lookup(name) - if templ == nil { - return nil - } - return templ.Tree - }, t.html.transformNotFound}, - // text templates - {func(name string) *parse.Tree { - templT := t.text.lookup(name) - if templT == nil { - return nil - } - return templT.Tree - }, t.text.transformNotFound}, - } { - for name := range s.transformNotFound { - templ := s.lookup(name) - if templ != nil { - _, err := applyTemplateTransformers(templateUndefined, templ, s.lookup) - if err != nil { - return err - } - } - } - } - - return nil -} - -func (t *templateHandler) addLateTemplate(name, tpl string) error { - return t.AddLateTemplate(name, tpl) -} - -// AddLateTemplate is used to add a template late, i.e. after the -// regular templates have started its execution. -func (t *templateHandler) AddLateTemplate(name, tpl string) error { - h := t.getTemplateHandler(name) - if err := h.addLateTemplate(name, tpl); err != nil { - return err - } - return nil -} - -// AddTemplate parses and adds a template to the collection. -// Templates with name prefixed with "_text" will be handled as plain -// text templates. -// TODO(bep) clean up these addTemplate variants -func (t *templateHandler) AddTemplate(name, tpl string) error { - h := t.getTemplateHandler(name) - _, err := h.addTemplate(name, tpl) - if err != nil { - return err - } - return nil -} - -// MarkReady marks the templates as "ready for execution". No changes allowed -// after this is set. -// TODO(bep) if this proves to be resource heavy, we could detect -// earlier if we really need this, or make it lazy. -func (t *templateHandler) MarkReady() error { - if err := t.postTransform(); err != nil { - return err - } - - if t.html.clone == nil { - t.html.clone = template.Must(t.html.t.Clone()) - t.html.cloneClone = template.Must(t.html.clone.Clone()) - } - if t.text.clone == nil { - t.text.clone = texttemplate.Must(t.text.t.Clone()) - t.text.cloneClone = texttemplate.Must(t.text.clone.Clone()) - } - - return nil -} - -// RebuildClone rebuilds the cloned templates. Used for live-reloads. -func (t *templateHandler) RebuildClone() { - if t.html != nil && t.html.cloneClone != nil { - t.html.clone = template.Must(t.html.cloneClone.Clone()) - } - if t.text != nil && t.text.cloneClone != nil { - t.text.clone = texttemplate.Must(t.text.cloneClone.Clone()) - } -} - -func (t *templateHandler) loadTemplates(prefix string) error { - - walker := func(path string, fi hugofs.FileMetaInfo, err error) error { - if err != nil || fi.IsDir() { - return err - } - - if isDotFile(path) || isBackupFile(path) || isBaseTemplate(path) { - return nil - } - - workingDir := t.PathSpec.WorkingDir - - descriptor := output.TemplateLookupDescriptor{ - WorkingDir: workingDir, - RelPath: path, - Prefix: prefix, - OutputFormats: t.OutputFormatsConfig, - FileExists: func(filename string) (bool, error) { - return helpers.Exists(filename, t.Layouts.Fs) - }, - ContainsAny: func(filename string, subslices [][]byte) (bool, error) { - return helpers.FileContainsAny(filename, subslices, t.Layouts.Fs) - }, - } - - tplID, err := output.CreateTemplateNames(descriptor) - if err != nil { - t.Log.ERROR.Printf("Failed to resolve template in path %q: %s", path, err) - return nil - } - - if err := t.addTemplateFile(tplID.Name, tplID.MasterFilename, tplID.OverlayFilename); err != nil { - return err - } - - return nil - } - - if err := helpers.SymbolicWalk(t.Layouts.Fs, "", walker); err != nil { - if !os.IsNotExist(err) { - return err - } - return nil - } - - return nil - -} - -func (t *templateHandler) initFuncs() { - - // Both template types will get their own funcster instance, which - // in the current case contains the same set of funcs. - funcMap := createFuncMap(t.Deps) - for _, funcsterHolder := range []templateFuncsterSetter{t.html, t.text} { - funcster := newTemplateFuncster(t.Deps) - - // The URL funcs in the funcMap is somewhat language dependent, - // so we need to wait until the language and site config is loaded. - funcster.initFuncMap(funcMap) - - funcsterHolder.setTemplateFuncster(funcster) - - } - - for _, v := range t.shortcodes { - for _, variant := range v.variants { - t.setFuncMapInTemplate(variant.templ, funcMap) - } - } - - for _, extText := range t.extTextTemplates { - extText.t.Funcs(funcMap) - } - - // Amber is HTML only. - t.amberFuncMap = template.FuncMap{} - - amberMu.Lock() - for k, v := range amber.FuncMap { - t.amberFuncMap[k] = v - } - - for k, v := range t.html.funcster.funcMap { - t.amberFuncMap[k] = v - // Hacky, but we need to make sure that the func names are in the global map. - amber.FuncMap[k] = func() string { - panic("should never be invoked") - } - } - amberMu.Unlock() - -} - -func (t *templateHandler) getTemplateHandler(name string) templateLoader { - if strings.HasPrefix(name, textTmplNamePrefix) { - return t.text - } - return t.html -} - -func (t *templateHandler) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (templateInfo, error)) error { - h := t.getTemplateHandler(name) - return h.handleMaster(name, overlayFilename, masterFilename, onMissing) -} - -func (t *htmlTemplates) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (templateInfo, error)) error { - - masterTpl := t.lookup(masterFilename) - - if masterTpl == nil { - templ, err := onMissing(masterFilename) - if err != nil { - return err - } - - masterTpl, err = t.t.New(overlayFilename).Parse(templ.template) - if err != nil { - return templ.errWithFileContext("parse master failed", err) - } - } - - templ, err := onMissing(overlayFilename) - if err != nil { - return err - } - - overlayTpl, err := template.Must(masterTpl.Clone()).Parse(templ.template) - if err != nil { - return templ.errWithFileContext("parse failed", err) - } - - // The extra lookup is a workaround, see - // * https://github.com/golang/go/issues/16101 - // * https://github.com/gohugoio/hugo/issues/2549 - overlayTpl = overlayTpl.Lookup(overlayTpl.Name()) - if _, err := applyTemplateTransformersToHMLTTemplate(templateUndefined, overlayTpl); err != nil { - return err - } - - t.overlays[name] = overlayTpl - t.nameBaseTemplateName[name] = masterFilename - - return err - -} - -func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (templateInfo, error)) error { - - name = strings.TrimPrefix(name, textTmplNamePrefix) - masterTpl := t.lookup(masterFilename) - - if masterTpl == nil { - templ, err := onMissing(masterFilename) - if err != nil { - return err - } - - masterTpl, err = t.t.New(masterFilename).Parse(templ.template) - if err != nil { - return errors.Wrapf(err, "failed to parse %q:", templ.filename) - } - t.nameBaseTemplateName[masterFilename] = templ.filename - } - - templ, err := onMissing(overlayFilename) - if err != nil { - return err - } - - overlayTpl, err := texttemplate.Must(masterTpl.Clone()).Parse(templ.template) - if err != nil { - return errors.Wrapf(err, "failed to parse %q:", templ.filename) - } - - overlayTpl = overlayTpl.Lookup(overlayTpl.Name()) - if _, err := applyTemplateTransformersToTextTemplate(templateUndefined, overlayTpl); err != nil { - return err - } - t.overlays[name] = overlayTpl - t.nameBaseTemplateName[name] = templ.filename - - return err - -} - -func removeLeadingBOM(s string) string { - const bom = '\ufeff' - - for i, r := range s { - if i == 0 && r != bom { - return s - } - if i > 0 { - return s[i:] - } - } - - return s - -} - -func (t *templateHandler) addTemplateFile(name, baseTemplatePath, path string) error { - t.checkState() - - t.Log.DEBUG.Printf("Add template file: name %q, baseTemplatePath %q, path %q", name, baseTemplatePath, path) - - getTemplate := func(filename string) (templateInfo, error) { - fs := t.Layouts.Fs - b, err := afero.ReadFile(fs, filename) - if err != nil { - return templateInfo{filename: filename, fs: fs}, err - } - - s := removeLeadingBOM(string(b)) - - realFilename := filename - if fi, err := fs.Stat(filename); err == nil { - if fim, ok := fi.(hugofs.FileMetaInfo); ok { - realFilename = fim.Meta().Filename() - } - } - - return templateInfo{template: s, filename: filename, realFilename: realFilename, fs: fs}, nil - } - - // get the suffix and switch on that - ext := filepath.Ext(path) - switch ext { - case ".amber": - // Only HTML support for Amber - withoutExt := strings.TrimSuffix(name, filepath.Ext(name)) - templateName := withoutExt + ".html" - b, err := afero.ReadFile(t.Layouts.Fs, path) - - if err != nil { - return err - } - - amberMu.Lock() - templ, err := t.compileAmberWithTemplate(b, path, t.html.t.New(templateName)) - amberMu.Unlock() - if err != nil { - return err - } - - typ := resolveTemplateType(name) - - c, err := applyTemplateTransformersToHMLTTemplate(typ, templ) - if err != nil { - return err - } - - if typ == templateShortcode { - t.addShortcodeVariant(templateName, c.Info, templ) - } else { - t.templateInfo[name] = c.Info - } - - return nil - - case ".ace": - // Only HTML support for Ace - var innerContent, baseContent []byte - innerContent, err := afero.ReadFile(t.Layouts.Fs, path) - - if err != nil { - return err - } - - if baseTemplatePath != "" { - baseContent, err = afero.ReadFile(t.Layouts.Fs, baseTemplatePath) - if err != nil { - return err - } - } - - return t.addAceTemplate(name, baseTemplatePath, path, baseContent, innerContent) - default: - - if baseTemplatePath != "" { - return t.handleMaster(name, path, baseTemplatePath, getTemplate) - } - - templ, err := getTemplate(path) - - if err != nil { - return err - } - - err = t.AddTemplate(name, templ.template) - if err != nil { - return templ.errWithFileContext("parse failed", err) - } - return nil - } -} - -var embeddedTemplatesAliases = map[string][]string{ - "shortcodes/twitter.html": {"shortcodes/tweet.html"}, -} - -func (t *templateHandler) loadEmbedded() error { - for _, kv := range embedded.EmbeddedTemplates { - name, templ := kv[0], kv[1] - if err := t.addInternalTemplate(name, templ); err != nil { - return err - } - if aliases, found := embeddedTemplatesAliases[name]; found { - for _, alias := range aliases { - if err := t.addInternalTemplate(alias, templ); err != nil { - return err - } - } - - } - } - - return nil - -} - -func (t *templateHandler) addInternalTemplate(name, tpl string) error { - return t.AddTemplate("_internal/"+name, tpl) -} - -func (t *templateHandler) checkState() { - if t.html.clone != nil || t.text.clone != nil { - panic("template is cloned and cannot be modfified") - } -} - -func isDotFile(path string) bool { - return filepath.Base(path)[0] == '.' -} - -func isBackupFile(path string) bool { - return path[len(path)-1] == '~' -} - -const baseFileBase = "baseof" - -func isBaseTemplate(path string) bool { - return strings.Contains(filepath.Base(path), baseFileBase) -} diff --git a/tpl/tplimpl/templateFuncster.go b/tpl/tplimpl/templateFuncster.go deleted file mode 100644 index ad51fbad7..000000000 --- a/tpl/tplimpl/templateFuncster.go +++ /dev/null @@ -1,33 +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 tplimpl - -import ( - "html/template" - - "github.com/gohugoio/hugo/deps" -) - -// Some of the template funcs are'nt entirely stateless. -type templateFuncster struct { - funcMap template.FuncMap - - *deps.Deps -} - -func newTemplateFuncster(deps *deps.Deps) *templateFuncster { - return &templateFuncster{ - Deps: deps, - } -} diff --git a/tpl/tplimpl/templateProvider.go b/tpl/tplimpl/templateProvider.go deleted file mode 100644 index 932222344..000000000 --- a/tpl/tplimpl/templateProvider.go +++ /dev/null @@ -1,63 +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 tplimpl - -import ( - "github.com/gohugoio/hugo/deps" -) - -// TemplateProvider manages templates. -type TemplateProvider struct{} - -// DefaultTemplateProvider is a globally available TemplateProvider. -var DefaultTemplateProvider *TemplateProvider - -// Update updates the Hugo Template System in the provided Deps -// with all the additional features, templates & functions. -func (*TemplateProvider) Update(deps *deps.Deps) error { - - newTmpl := newTemplateAdapter(deps) - deps.Tmpl = newTmpl - - deps.TextTmpl = newTmpl.NewTextTemplate() - - newTmpl.initFuncs() - - if err := newTmpl.loadEmbedded(); err != nil { - return err - } - - if deps.WithTemplate != nil { - err := deps.WithTemplate(newTmpl) - if err != nil { - return err - } - - } - - return newTmpl.MarkReady() - -} - -// Clone clones. -func (*TemplateProvider) Clone(d *deps.Deps) error { - - t := d.Tmpl.(*templateHandler) - clone := t.clone(d) - - d.Tmpl = clone - - return clone.MarkReady() - -} diff --git a/tpl/tplimpl/template_ast_transformers.go b/tpl/tplimpl/template_ast_transformers.go deleted file mode 100644 index fb0728a63..000000000 --- a/tpl/tplimpl/template_ast_transformers.go +++ /dev/null @@ -1,535 +0,0 @@ -// 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 tplimpl - -import ( - "html/template" - "strings" - texttemplate "text/template" - "text/template/parse" - - "github.com/pkg/errors" - - "github.com/gohugoio/hugo/tpl" - "github.com/mitchellh/mapstructure" - "github.com/spf13/cast" -) - -// decl keeps track of the variable mappings, i.e. $mysite => .Site etc. -type decl map[string]string - -const ( - paramsIdentifier = "Params" -) - -// Containers that may contain Params that we will not touch. -var reservedContainers = map[string]bool{ - // Aka .Site.Data.Params which must stay case sensitive. - "Data": true, -} - -type templateType int - -const ( - templateUndefined templateType = iota - templateShortcode - templatePartial -) - -type templateContext struct { - decl decl - visited map[string]bool - notFound map[string]bool - lookupFn func(name string) *parse.Tree - - // The last error encountered. - err error - - typ templateType - - // Set when we're done checking for config header. - configChecked bool - - // Contains some info about the template - tpl.Info - - // Store away the return node in partials. - returnNode *parse.CommandNode -} - -func (c templateContext) getIfNotVisited(name string) *parse.Tree { - if c.visited[name] { - return nil - } - c.visited[name] = true - templ := c.lookupFn(name) - if templ == nil { - // This may be a inline template defined outside of this file - // and not yet parsed. Unusual, but it happens. - // Store the name to try again later. - c.notFound[name] = true - } - - return templ -} - -func newTemplateContext(lookupFn func(name string) *parse.Tree) *templateContext { - return &templateContext{ - Info: tpl.Info{Config: tpl.DefaultConfig}, - lookupFn: lookupFn, - decl: make(map[string]string), - visited: make(map[string]bool), - notFound: make(map[string]bool)} -} - -func createParseTreeLookup(templ *template.Template) func(nn string) *parse.Tree { - return func(nn string) *parse.Tree { - tt := templ.Lookup(nn) - if tt != nil { - return tt.Tree - } - return nil - } -} - -func applyTemplateTransformersToHMLTTemplate(typ templateType, templ *template.Template) (*templateContext, error) { - return applyTemplateTransformers(typ, templ.Tree, createParseTreeLookup(templ)) -} - -func applyTemplateTransformersToTextTemplate(typ templateType, templ *texttemplate.Template) (*templateContext, error) { - return applyTemplateTransformers(typ, templ.Tree, - func(nn string) *parse.Tree { - tt := templ.Lookup(nn) - if tt != nil { - return tt.Tree - } - return nil - }) -} - -func applyTemplateTransformers(typ templateType, templ *parse.Tree, lookupFn func(name string) *parse.Tree) (*templateContext, error) { - if templ == nil { - return nil, errors.New("expected template, but none provided") - } - - c := newTemplateContext(lookupFn) - c.typ = typ - - _, err := c.applyTransformations(templ.Root) - - if err == nil && c.returnNode != nil { - // This is a partial with a return statement. - c.Info.HasReturn = true - templ.Root = c.wrapInPartialReturnWrapper(templ.Root) - } - - return c, err -} - -const ( - partialReturnWrapperTempl = `{{ $_hugo_dot := $ }}{{ $ := .Arg }}{{ with .Arg }}{{ $_hugo_dot.Set ("PLACEHOLDER") }}{{ end }}` -) - -var partialReturnWrapper *parse.ListNode - -func init() { - templ, err := texttemplate.New("").Parse(partialReturnWrapperTempl) - if err != nil { - panic(err) - } - partialReturnWrapper = templ.Tree.Root -} - -func (c *templateContext) wrapInPartialReturnWrapper(n *parse.ListNode) *parse.ListNode { - wrapper := partialReturnWrapper.CopyList() - withNode := wrapper.Nodes[2].(*parse.WithNode) - retn := withNode.List.Nodes[0] - setCmd := retn.(*parse.ActionNode).Pipe.Cmds[0] - setPipe := setCmd.Args[1].(*parse.PipeNode) - // Replace PLACEHOLDER with the real return value. - // Note that this is a PipeNode, so it will be wrapped in parens. - setPipe.Cmds = []*parse.CommandNode{c.returnNode} - withNode.List.Nodes = append(n.Nodes, retn) - - return wrapper - -} - -// The truth logic in Go's template package is broken for certain values -// for the if and with keywords. This works around that problem by wrapping -// the node passed to if/with in a getif conditional. -// getif works slightly different than the Go built-in in that it also -// considers any IsZero methods on the values (as in time.Time). -// See https://github.com/gohugoio/hugo/issues/5738 -func (c *templateContext) wrapWithGetIf(p *parse.PipeNode) { - if len(p.Cmds) == 0 { - return - } - - // getif will return an empty string if not evaluated as truthful, - // which is when we need the value in the with clause. - firstArg := parse.NewIdentifier("getif") - secondArg := p.CopyPipe() - newCmd := p.Cmds[0].Copy().(*parse.CommandNode) - - // secondArg is a PipeNode and will behave as it was wrapped in parens, e.g: - // {{ getif (len .Params | eq 2) }} - newCmd.Args = []parse.Node{firstArg, secondArg} - - p.Cmds = []*parse.CommandNode{newCmd} - -} - -// applyTransformations do 3 things: -// 1) Make all .Params.CamelCase and similar into lowercase. -// 2) Wraps every with and if pipe in getif -// 3) Collects some information about the template content. -func (c *templateContext) applyTransformations(n parse.Node) (bool, error) { - switch x := n.(type) { - case *parse.ListNode: - if x != nil { - c.applyTransformationsToNodes(x.Nodes...) - } - case *parse.ActionNode: - c.applyTransformationsToNodes(x.Pipe) - case *parse.IfNode: - c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList) - c.wrapWithGetIf(x.Pipe) - case *parse.WithNode: - c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList) - c.wrapWithGetIf(x.Pipe) - case *parse.RangeNode: - c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList) - case *parse.TemplateNode: - subTempl := c.getIfNotVisited(x.Name) - if subTempl != nil { - c.applyTransformationsToNodes(subTempl.Root) - } - case *parse.PipeNode: - c.collectConfig(x) - if len(x.Decl) == 1 && len(x.Cmds) == 1 { - // maps $site => .Site etc. - c.decl[x.Decl[0].Ident[0]] = x.Cmds[0].String() - } - - for i, cmd := range x.Cmds { - keep, _ := c.applyTransformations(cmd) - if !keep { - x.Cmds = append(x.Cmds[:i], x.Cmds[i+1:]...) - } - } - - case *parse.CommandNode: - c.collectInner(x) - keep := c.collectReturnNode(x) - - for _, elem := range x.Args { - switch an := elem.(type) { - case *parse.FieldNode: - c.updateIdentsIfNeeded(an.Ident) - case *parse.VariableNode: - c.updateIdentsIfNeeded(an.Ident) - case *parse.PipeNode: - c.applyTransformations(an) - case *parse.ChainNode: - // site.Params... - if len(an.Field) > 1 && an.Field[0] == paramsIdentifier { - c.updateIdentsIfNeeded(an.Field) - } - } - } - return keep, c.err - } - - return true, c.err -} - -func (c *templateContext) applyTransformationsToNodes(nodes ...parse.Node) { - for _, node := range nodes { - c.applyTransformations(node) - } -} - -func (c *templateContext) updateIdentsIfNeeded(idents []string) { - index := c.decl.indexOfReplacementStart(idents) - - if index == -1 { - return - } - - for i := index; i < len(idents); i++ { - idents[i] = strings.ToLower(idents[i]) - } - -} - -func (c *templateContext) hasIdent(idents []string, ident string) bool { - for _, id := range idents { - if id == ident { - return true - } - } - return false -} - -// collectConfig collects and parses any leading template config variable declaration. -// This will be the first PipeNode in the template, and will be a variable declaration -// on the form: -// {{ $_hugo_config:= `{ "version": 1 }` }} -func (c *templateContext) collectConfig(n *parse.PipeNode) { - if c.typ != templateShortcode { - return - } - if c.configChecked { - return - } - c.configChecked = true - - if len(n.Decl) != 1 || len(n.Cmds) != 1 { - // This cannot be a config declaration - return - } - - v := n.Decl[0] - - if len(v.Ident) == 0 || v.Ident[0] != "$_hugo_config" { - return - } - - cmd := n.Cmds[0] - - if len(cmd.Args) == 0 { - return - } - - if s, ok := cmd.Args[0].(*parse.StringNode); ok { - errMsg := "failed to decode $_hugo_config in template" - m, err := cast.ToStringMapE(s.Text) - if err != nil { - c.err = errors.Wrap(err, errMsg) - return - } - if err := mapstructure.WeakDecode(m, &c.Info.Config); err != nil { - c.err = errors.Wrap(err, errMsg) - } - } - -} - -// collectInner determines if the given CommandNode represents a -// shortcode call to its .Inner. -func (c *templateContext) collectInner(n *parse.CommandNode) { - if c.typ != templateShortcode { - return - } - if c.Info.IsInner || len(n.Args) == 0 { - return - } - - for _, arg := range n.Args { - var idents []string - switch nt := arg.(type) { - case *parse.FieldNode: - idents = nt.Ident - case *parse.VariableNode: - idents = nt.Ident - } - - if c.hasIdent(idents, "Inner") { - c.Info.IsInner = true - break - } - } - -} - -func (c *templateContext) collectReturnNode(n *parse.CommandNode) bool { - if c.typ != templatePartial || c.returnNode != nil { - return true - } - - if len(n.Args) < 2 { - return true - } - - ident, ok := n.Args[0].(*parse.IdentifierNode) - if !ok || ident.Ident != "return" { - return true - } - - c.returnNode = n - // Remove the "return" identifiers - c.returnNode.Args = c.returnNode.Args[1:] - - return false - -} - -// indexOfReplacementStart will return the index of where to start doing replacement, -// -1 if none needed. -func (d decl) indexOfReplacementStart(idents []string) int { - - l := len(idents) - - if l == 0 { - return -1 - } - - if l == 1 { - first := idents[0] - if first == "" || first == paramsIdentifier || first[0] == '$' { - // This can not be a Params.x - return -1 - } - } - - var lookFurther bool - var needsVarExpansion bool - for _, ident := range idents { - if ident[0] == '$' { - lookFurther = true - needsVarExpansion = true - break - } else if ident == paramsIdentifier { - lookFurther = true - break - } - } - - if !lookFurther { - return -1 - } - - var resolvedIdents []string - - if !needsVarExpansion { - resolvedIdents = idents - } else { - var ok bool - resolvedIdents, ok = d.resolveVariables(idents) - if !ok { - return -1 - } - } - - var paramFound bool - for i, ident := range resolvedIdents { - if ident == paramsIdentifier { - if i > 0 { - container := resolvedIdents[i-1] - if reservedContainers[container] { - // .Data.Params.someKey - return -1 - } - } - - paramFound = true - break - } - } - - if !paramFound { - return -1 - } - - var paramSeen bool - idx := -1 - for i, ident := range idents { - if ident == "" || ident[0] == '$' { - continue - } - - if ident == paramsIdentifier { - paramSeen = true - idx = -1 - - } else { - if paramSeen { - return i - } - if idx == -1 { - idx = i - } - } - } - return idx - -} - -func (d decl) resolveVariables(idents []string) ([]string, bool) { - var ( - replacements []string - replaced []string - ) - - // An Ident can start out as one of - // [Params] [$blue] [$colors.Blue] - // We need to resolve the variables, so - // $blue => [Params Colors Blue] - // etc. - replacements = []string{idents[0]} - - // Loop until there are no more $vars to resolve. - for i := 0; i < len(replacements); i++ { - - if i > 20 { - // bail out - return nil, false - } - - potentialVar := replacements[i] - - if potentialVar == "$" { - continue - } - - if potentialVar == "" || potentialVar[0] != '$' { - // leave it as is - replaced = append(replaced, strings.Split(potentialVar, ".")...) - continue - } - - replacement, ok := d[potentialVar] - - if !ok { - // Temporary range vars. We do not care about those. - return nil, false - } - - if !d.isKeyword(replacement) { - // This can not be .Site.Params etc. - return nil, false - } - - replacement = strings.TrimPrefix(replacement, ".") - - if replacement == "" { - continue - } - - if replacement[0] == '$' { - // Needs further expansion - replacements = append(replacements, strings.Split(replacement, ".")...) - } else { - replaced = append(replaced, strings.Split(replacement, ".")...) - } - } - - return append(replaced, idents[1:]...), true - -} - -func (d decl) isKeyword(s string) bool { - return !strings.ContainsAny(s, " -\"") -} diff --git a/tpl/tplimpl/template_ast_transformers_test.go b/tpl/tplimpl/template_ast_transformers_test.go deleted file mode 100644 index 39b5c460a..000000000 --- a/tpl/tplimpl/template_ast_transformers_test.go +++ /dev/null @@ -1,556 +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 tplimpl - -import ( - "bytes" - "fmt" - "html/template" - "testing" - "time" - - "github.com/gohugoio/hugo/tpl" - - "github.com/spf13/cast" - - qt "github.com/frankban/quicktest" -) - -var ( - testFuncs = map[string]interface{}{ - "getif": func(v interface{}) interface{} { return v }, - "ToTime": func(v interface{}) interface{} { return cast.ToTime(v) }, - "First": func(v ...interface{}) interface{} { return v[0] }, - "Echo": func(v interface{}) interface{} { return v }, - "where": func(seq, key interface{}, args ...interface{}) (interface{}, error) { - return map[string]interface{}{ - "ByWeight": fmt.Sprintf("%v:%v:%v", seq, key, args), - }, nil - }, - "site": func() interface{} { - return map[string]interface{}{ - "Params": map[string]interface{}{ - "lower": "global-site", - }, - } - }, - } - - paramsData = map[string]interface{}{ - "NotParam": "Hi There", - "Slice": []int{1, 3}, - "Params": map[string]interface{}{ - "lower": "P1L", - "slice": []int{1, 3}, - "mydate": "1972-01-28", - }, - "Pages": map[string]interface{}{ - "ByWeight": []int{1, 3}, - }, - "CurrentSection": map[string]interface{}{ - "Params": map[string]interface{}{ - "lower": "pcurrentsection", - }, - }, - "Site": map[string]interface{}{ - "Params": map[string]interface{}{ - "lower": "P2L", - "slice": []int{1, 3}, - }, - "Language": map[string]interface{}{ - "Params": map[string]interface{}{ - "lower": "P22L", - "nested": map[string]interface{}{ - "lower": "P22L_nested", - }, - }, - }, - "Data": map[string]interface{}{ - "Params": map[string]interface{}{ - "NOLOW": "P3H", - }, - }, - }, - } - - paramsTempl = ` -{{ $page := . }} -{{ $pages := .Pages }} -{{ $pageParams := .Params }} -{{ $site := .Site }} -{{ $siteParams := .Site.Params }} -{{ $data := .Site.Data }} -{{ $notparam := .NotParam }} - -PCurrentSection: {{ .CurrentSection.Params.LOWER }} -P1: {{ .Params.LOWER }} -P1_2: {{ $.Params.LOWER }} -P1_3: {{ $page.Params.LOWER }} -P1_4: {{ $pageParams.LOWER }} -P2: {{ .Site.Params.LOWER }} -P2_2: {{ $.Site.Params.LOWER }} -P2_3: {{ $site.Params.LOWER }} -P2_4: {{ $siteParams.LOWER }} -P22: {{ .Site.Language.Params.LOWER }} -P22_nested: {{ .Site.Language.Params.NESTED.LOWER }} -P3: {{ .Site.Data.Params.NOLOW }} -P3_2: {{ $.Site.Data.Params.NOLOW }} -P3_3: {{ $site.Data.Params.NOLOW }} -P3_4: {{ $data.Params.NOLOW }} -P4: {{ range $i, $e := .Site.Params.SLICE }}{{ $e }}{{ end }} -P5: {{ Echo .Params.LOWER }} -P5_2: {{ Echo $site.Params.LOWER }} -{{ if .Params.LOWER }} -IF: {{ .Params.LOWER }} -{{ end }} -{{ if .Params.NOT_EXIST }} -{{ else }} -ELSE: {{ .Params.LOWER }} -{{ end }} - - -{{ with .Params.LOWER }} -WITH: {{ . }} -{{ end }} - - -{{ range .Slice }} -RANGE: {{ . }}: {{ $.Params.LOWER }} -{{ end }} -{{ index .Slice 1 }} -{{ .NotParam }} -{{ .NotParam }} -{{ .NotParam }} -{{ .NotParam }} -{{ .NotParam }} -{{ .NotParam }} -{{ .NotParam }} -{{ .NotParam }} -{{ .NotParam }} -{{ .NotParam }} -{{ $notparam }} - - -{{ $lower := .Site.Params.LOWER }} -F1: {{ printf "themes/%s-theme" .Site.Params.LOWER }} -F2: {{ Echo (printf "themes/%s-theme" $lower) }} -F3: {{ Echo (printf "themes/%s-theme" .Site.Params.LOWER) }} - -PSLICE: {{ range .Params.SLICE }}PSLICE{{.}}|{{ end }} - -{{ $pages := "foo" }} -{{ $pages := where $pages ".Params.toc_hide" "!=" true }} -PARAMS STRING: {{ $pages.ByWeight }} -PARAMS STRING2: {{ with $pages }}{{ .ByWeight }}{{ end }} -{{ $pages3 := where ".Params.TOC_HIDE" "!=" .Params.LOWER }} -PARAMS STRING3: {{ $pages3.ByWeight }} -{{ $first := First .Pages .Site.Params.LOWER }} -PARAMS COMPOSITE: {{ $first.ByWeight }} - - -{{ $time := $.Params.MyDate | ToTime }} -{{ $time = $time.AddDate 0 1 0 }} -PARAMS TIME: {{ $time.Format "2006-01-02" }} - -{{ $_x := $.Params.MyDate | ToTime }} -PARAMS TIME2: {{ $_x.AddDate 0 1 0 }} - -PARAMS SITE GLOBAL1: {{ site.Params.LOwER }} -{{ $lower := site.Params.LOwER }} -{{ $site := site }} -PARAMS SITE GLOBAL2: {{ $lower }} -PARAMS SITE GLOBAL3: {{ $site.Params.LOWER }} -` -) - -func TestParamsKeysToLower(t *testing.T) { - t.Parallel() - c := qt.New(t) - - _, err := applyTemplateTransformers(templateUndefined, nil, nil) - c.Assert(err, qt.Not(qt.IsNil)) - - templ, err := template.New("foo").Funcs(testFuncs).Parse(paramsTempl) - - c.Assert(err, qt.IsNil) - - ctx := newTemplateContext(createParseTreeLookup(templ)) - - c.Assert(ctx.decl.indexOfReplacementStart([]string{}), qt.Equals, -1) - - ctx.applyTransformations(templ.Tree.Root) - - var b bytes.Buffer - - c.Assert(templ.Execute(&b, paramsData), qt.IsNil) - - result := b.String() - - c.Assert(result, qt.Contains, "P1: P1L") - c.Assert(result, qt.Contains, "P1_2: P1L") - c.Assert(result, qt.Contains, "P1_3: P1L") - c.Assert(result, qt.Contains, "P1_4: P1L") - c.Assert(result, qt.Contains, "P2: P2L") - c.Assert(result, qt.Contains, "P2_2: P2L") - c.Assert(result, qt.Contains, "P2_3: P2L") - c.Assert(result, qt.Contains, "P2_4: P2L") - c.Assert(result, qt.Contains, "P22: P22L") - c.Assert(result, qt.Contains, "P22_nested: P22L_nested") - c.Assert(result, qt.Contains, "P3: P3H") - c.Assert(result, qt.Contains, "P3_2: P3H") - c.Assert(result, qt.Contains, "P3_3: P3H") - c.Assert(result, qt.Contains, "P3_4: P3H") - c.Assert(result, qt.Contains, "P4: 13") - c.Assert(result, qt.Contains, "P5: P1L") - c.Assert(result, qt.Contains, "P5_2: P2L") - - c.Assert(result, qt.Contains, "IF: P1L") - c.Assert(result, qt.Contains, "ELSE: P1L") - - c.Assert(result, qt.Contains, "WITH: P1L") - - c.Assert(result, qt.Contains, "RANGE: 3: P1L") - - c.Assert(result, qt.Contains, "Hi There") - - // Issue #2740 - c.Assert(result, qt.Contains, "F1: themes/P2L-theme") - c.Assert(result, qt.Contains, "F2: themes/P2L-theme") - c.Assert(result, qt.Contains, "F3: themes/P2L-theme") - - c.Assert(result, qt.Contains, "PSLICE: PSLICE1|PSLICE3|") - c.Assert(result, qt.Contains, "PARAMS STRING: foo:.Params.toc_hide:[!= true]") - c.Assert(result, qt.Contains, "PARAMS STRING2: foo:.Params.toc_hide:[!= true]") - c.Assert(result, qt.Contains, "PARAMS STRING3: .Params.TOC_HIDE:!=:[P1L]") - - // Issue #5094 - c.Assert(result, qt.Contains, "PARAMS COMPOSITE: [1 3]") - - // Issue #5068 - c.Assert(result, qt.Contains, "PCurrentSection: pcurrentsection") - - // Issue #5541 - c.Assert(result, qt.Contains, "PARAMS TIME: 1972-02-28") - c.Assert(result, qt.Contains, "PARAMS TIME2: 1972-02-28") - - // Issue ##5615 - c.Assert(result, qt.Contains, "PARAMS SITE GLOBAL1: global-site") - c.Assert(result, qt.Contains, "PARAMS SITE GLOBAL2: global-site") - c.Assert(result, qt.Contains, "PARAMS SITE GLOBAL3: global-site") - -} - -func BenchmarkTemplateParamsKeysToLower(b *testing.B) { - templ, err := template.New("foo").Funcs(testFuncs).Parse(paramsTempl) - - if err != nil { - b.Fatal(err) - } - - templates := make([]*template.Template, b.N) - - for i := 0; i < b.N; i++ { - templates[i], err = templ.Clone() - if err != nil { - b.Fatal(err) - } - } - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - c := newTemplateContext(createParseTreeLookup(templates[i])) - c.applyTransformations(templ.Tree.Root) - } -} - -func TestParamsKeysToLowerVars(t *testing.T) { - t.Parallel() - c := qt.New(t) - - var ( - data = map[string]interface{}{ - "Params": map[string]interface{}{ - "colors": map[string]interface{}{ - "blue": "Amber", - "pretty": map[string]interface{}{ - "first": "Indigo", - }, - }, - }, - } - - // This is how Amber behaves: - paramsTempl = ` -{{$__amber_1 := .Params.Colors}} -{{$__amber_2 := $__amber_1.Blue}} -{{$__amber_3 := $__amber_1.Pretty}} -{{$__amber_4 := .Params}} - -Color: {{$__amber_2}} -Blue: {{ $__amber_1.Blue}} -Pretty First1: {{ $__amber_3.First}} -Pretty First2: {{ $__amber_1.Pretty.First}} -Pretty First3: {{ $__amber_4.COLORS.PRETTY.FIRST}} -` - ) - - templ, err := template.New("foo").Parse(paramsTempl) - - c.Assert(err, qt.IsNil) - - ctx := newTemplateContext(createParseTreeLookup(templ)) - - ctx.applyTransformations(templ.Tree.Root) - - var b bytes.Buffer - - c.Assert(templ.Execute(&b, data), qt.IsNil) - - result := b.String() - - c.Assert(result, qt.Contains, "Color: Amber") - c.Assert(result, qt.Contains, "Blue: Amber") - c.Assert(result, qt.Contains, "Pretty First1: Indigo") - c.Assert(result, qt.Contains, "Pretty First2: Indigo") - c.Assert(result, qt.Contains, "Pretty First3: Indigo") - -} - -func TestParamsKeysToLowerInBlockTemplate(t *testing.T) { - t.Parallel() - c := qt.New(t) - - var ( - data = map[string]interface{}{ - "Params": map[string]interface{}{ - "lower": "P1L", - }, - } - - master = ` -P1: {{ .Params.LOWER }} -{{ block "main" . }}DEFAULT{{ end }}` - overlay = ` -{{ define "main" }} -P2: {{ .Params.LOWER }} -{{ end }}` - ) - - masterTpl, err := template.New("foo").Parse(master) - c.Assert(err, qt.IsNil) - - overlayTpl, err := template.Must(masterTpl.Clone()).Parse(overlay) - c.Assert(err, qt.IsNil) - overlayTpl = overlayTpl.Lookup(overlayTpl.Name()) - - ctx := newTemplateContext(createParseTreeLookup(overlayTpl)) - - ctx.applyTransformations(overlayTpl.Tree.Root) - - var b bytes.Buffer - - c.Assert(overlayTpl.Execute(&b, data), qt.IsNil) - - result := b.String() - - c.Assert(result, qt.Contains, "P1: P1L") - c.Assert(result, qt.Contains, "P2: P1L") -} - -// Issue #2927 -func TestTransformRecursiveTemplate(t *testing.T) { - c := qt.New(t) - - recursive := ` -{{ define "menu-nodes" }} -{{ template "menu-node" }} -{{ end }} -{{ define "menu-node" }} -{{ template "menu-node" }} -{{ end }} -{{ template "menu-nodes" }} -` - - templ, err := template.New("foo").Parse(recursive) - c.Assert(err, qt.IsNil) - - ctx := newTemplateContext(createParseTreeLookup(templ)) - ctx.applyTransformations(templ.Tree.Root) - -} - -type I interface { - Method0() -} - -type T struct { - NonEmptyInterfaceTypedNil I -} - -func (T) Method0() { -} - -func TestInsertIsZeroFunc(t *testing.T) { - t.Parallel() - - c := qt.New(t) - - var ( - ctx = map[string]interface{}{ - "True": true, - "Now": time.Now(), - "TimeZero": time.Time{}, - "T": &T{NonEmptyInterfaceTypedNil: (*T)(nil)}, - } - - templ1 = ` -{{ if .True }}.True: TRUE{{ else }}.True: FALSE{{ end }} -{{ if .TimeZero }}.TimeZero1: TRUE{{ else }}.TimeZero1: FALSE{{ end }} -{{ if (.TimeZero) }}.TimeZero2: TRUE{{ else }}.TimeZero2: FALSE{{ end }} -{{ if not .TimeZero }}.TimeZero3: TRUE{{ else }}.TimeZero3: FALSE{{ end }} -{{ if .Now }}.Now: TRUE{{ else }}.Now: FALSE{{ end }} -{{ with .TimeZero }}.TimeZero1 with: {{ . }}{{ else }}.TimeZero1 with: FALSE{{ end }} -{{ template "mytemplate" . }} -{{ if .T.NonEmptyInterfaceTypedNil }}.NonEmptyInterfaceTypedNil: TRUE{{ else }}.NonEmptyInterfaceTypedNil: FALSE{{ end }} - -{{ template "other-file-template" . }} - -{{ define "mytemplate" }} -{{ if .TimeZero }}.TimeZero1: mytemplate: TRUE{{ else }}.TimeZero1: mytemplate: FALSE{{ end }} -{{ end }} - -` - - // https://github.com/gohugoio/hugo/issues/5865 - templ2 = `{{ define "other-file-template" }} -{{ if .TimeZero }}.TimeZero1: other-file-template: TRUE{{ else }}.TimeZero1: other-file-template: FALSE{{ end }} -{{ end }} -` - ) - - d := newD(c) - h := d.Tmpl.(tpl.TemplateHandler) - - // HTML templates - c.Assert(h.AddTemplate("mytemplate.html", templ1), qt.IsNil) - c.Assert(h.AddTemplate("othertemplate.html", templ2), qt.IsNil) - - // Text templates - c.Assert(h.AddTemplate("_text/mytexttemplate.txt", templ1), qt.IsNil) - c.Assert(h.AddTemplate("_text/myothertexttemplate.txt", templ2), qt.IsNil) - - c.Assert(h.MarkReady(), qt.IsNil) - - for _, name := range []string{"mytemplate.html", "mytexttemplate.txt"} { - tt, _ := d.Tmpl.Lookup(name) - result, err := tt.(tpl.TemplateExecutor).ExecuteToString(ctx) - c.Assert(err, qt.IsNil) - - c.Assert(result, qt.Contains, ".True: TRUE") - c.Assert(result, qt.Contains, ".TimeZero1: FALSE") - c.Assert(result, qt.Contains, ".TimeZero2: FALSE") - c.Assert(result, qt.Contains, ".TimeZero3: TRUE") - c.Assert(result, qt.Contains, ".Now: TRUE") - c.Assert(result, qt.Contains, "TimeZero1 with: FALSE") - c.Assert(result, qt.Contains, ".TimeZero1: mytemplate: FALSE") - c.Assert(result, qt.Contains, ".TimeZero1: other-file-template: FALSE") - c.Assert(result, qt.Contains, ".NonEmptyInterfaceTypedNil: FALSE") - } - -} - -func TestCollectInfo(t *testing.T) { - - configStr := `{ "version": 42 }` - - tests := []struct { - name string - tplString string - expected tpl.Info - }{ - {"Basic Inner", `{{ .Inner }}`, tpl.Info{IsInner: true, Config: tpl.DefaultConfig}}, - {"Basic config map", "{{ $_hugo_config := `" + configStr + "` }}", tpl.Info{ - Config: tpl.Config{ - Version: 42, - }, - }}, - } - - echo := func(in interface{}) interface{} { - return in - } - - funcs := template.FuncMap{ - "highlight": echo, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - c := qt.New(t) - - templ, err := template.New("foo").Funcs(funcs).Parse(test.tplString) - c.Assert(err, qt.IsNil) - - ctx := newTemplateContext(createParseTreeLookup(templ)) - ctx.typ = templateShortcode - ctx.applyTransformations(templ.Tree.Root) - - c.Assert(ctx.Info, qt.Equals, test.expected) - }) - } - -} - -func TestPartialReturn(t *testing.T) { - - tests := []struct { - name string - tplString string - expected bool - }{ - {"Basic", ` -{{ $a := "Hugo Rocks!" }} -{{ return $a }} -`, true}, - {"Expression", ` -{{ return add 32 }} -`, true}, - } - - echo := func(in interface{}) interface{} { - return in - } - - funcs := template.FuncMap{ - "return": echo, - "add": echo, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - c := qt.New(t) - - templ, err := template.New("foo").Funcs(funcs).Parse(test.tplString) - c.Assert(err, qt.IsNil) - - _, err = applyTemplateTransformers(templatePartial, templ.Tree, createParseTreeLookup(templ)) - - // Just check that it doesn't fail in this test. We have functional tests - // in hugoblib. - c.Assert(err, qt.IsNil) - - }) - } - -} diff --git a/tpl/tplimpl/template_errors.go b/tpl/tplimpl/template_errors.go deleted file mode 100644 index 63695c5f6..000000000 --- a/tpl/tplimpl/template_errors.go +++ /dev/null @@ -1,46 +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 tplimpl - -import ( - "github.com/gohugoio/hugo/common/herrors" - "github.com/pkg/errors" - "github.com/spf13/afero" -) - -type templateInfo struct { - template string - - // Used to create some error context in error situations - fs afero.Fs - - // The filename relative to the fs above. - filename string - - // The real filename (if possible). Used for logging. - realFilename string -} - -func (info templateInfo) errWithFileContext(what string, err error) error { - err = errors.Wrapf(err, what) - - err, _ = herrors.WithFileContextForFile( - err, - info.realFilename, - info.filename, - info.fs, - herrors.SimpleLineMatcher) - - return err -} diff --git a/tpl/tplimpl/template_funcs.go b/tpl/tplimpl/template_funcs.go deleted file mode 100644 index bbaf44ae2..000000000 --- a/tpl/tplimpl/template_funcs.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2017-present The Hugo Authors. All rights reserved. -// -// Portions Copyright The Go Authors. - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package tplimpl - -import ( - "html/template" - - "github.com/gohugoio/hugo/deps" - - "github.com/gohugoio/hugo/tpl/internal" - - // Init the namespaces - _ "github.com/gohugoio/hugo/tpl/cast" - _ "github.com/gohugoio/hugo/tpl/collections" - _ "github.com/gohugoio/hugo/tpl/compare" - _ "github.com/gohugoio/hugo/tpl/crypto" - _ "github.com/gohugoio/hugo/tpl/data" - _ "github.com/gohugoio/hugo/tpl/encoding" - _ "github.com/gohugoio/hugo/tpl/fmt" - _ "github.com/gohugoio/hugo/tpl/hugo" - _ "github.com/gohugoio/hugo/tpl/images" - _ "github.com/gohugoio/hugo/tpl/inflect" - _ "github.com/gohugoio/hugo/tpl/lang" - _ "github.com/gohugoio/hugo/tpl/math" - _ "github.com/gohugoio/hugo/tpl/os" - _ "github.com/gohugoio/hugo/tpl/partials" - _ "github.com/gohugoio/hugo/tpl/path" - _ "github.com/gohugoio/hugo/tpl/reflect" - _ "github.com/gohugoio/hugo/tpl/resources" - _ "github.com/gohugoio/hugo/tpl/safe" - _ "github.com/gohugoio/hugo/tpl/site" - _ "github.com/gohugoio/hugo/tpl/strings" - _ "github.com/gohugoio/hugo/tpl/templates" - _ "github.com/gohugoio/hugo/tpl/time" - _ "github.com/gohugoio/hugo/tpl/transform" - _ "github.com/gohugoio/hugo/tpl/urls" -) - -func createFuncMap(d *deps.Deps) map[string]interface{} { - funcMap := template.FuncMap{} - - // Merge the namespace funcs - for _, nsf := range internal.TemplateFuncsNamespaceRegistry { - ns := nsf(d) - if _, exists := funcMap[ns.Name]; exists { - panic(ns.Name + " is a duplicate template func") - } - funcMap[ns.Name] = ns.Context - for _, mm := range ns.MethodMappings { - for _, alias := range mm.Aliases { - if _, exists := funcMap[alias]; exists { - panic(alias + " is a duplicate template func") - } - funcMap[alias] = mm.Method - } - - } - - } - - return funcMap - -} -func (t *templateFuncster) initFuncMap(funcMap template.FuncMap) { - t.funcMap = funcMap - t.Tmpl.(*templateHandler).setFuncs(funcMap) -} diff --git a/tpl/tplimpl/template_funcs_test.go b/tpl/tplimpl/template_funcs_test.go deleted file mode 100644 index 10fbc2375..000000000 --- a/tpl/tplimpl/template_funcs_test.go +++ /dev/null @@ -1,234 +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 tplimpl - -import ( - "bytes" - "fmt" - "path/filepath" - "reflect" - "testing" - "time" - - "github.com/gohugoio/hugo/modules" - - "github.com/gohugoio/hugo/resources/page" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/common/hugo" - "github.com/gohugoio/hugo/common/loggers" - "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/deps" - "github.com/gohugoio/hugo/hugofs" - "github.com/gohugoio/hugo/langs" - "github.com/gohugoio/hugo/langs/i18n" - "github.com/gohugoio/hugo/tpl" - "github.com/gohugoio/hugo/tpl/internal" - "github.com/gohugoio/hugo/tpl/partials" - "github.com/spf13/afero" - "github.com/spf13/viper" -) - -var ( - logger = loggers.NewErrorLogger() -) - -func newTestConfig() config.Provider { - v := viper.New() - v.Set("contentDir", "content") - v.Set("dataDir", "data") - v.Set("i18nDir", "i18n") - v.Set("layoutDir", "layouts") - v.Set("archetypeDir", "archetypes") - v.Set("assetDir", "assets") - v.Set("resourceDir", "resources") - v.Set("publishDir", "public") - - langs.LoadLanguageSettings(v, nil) - mod, err := modules.CreateProjectModule(v) - if err != nil { - panic(err) - } - v.Set("allModules", modules.Modules{mod}) - - return v -} - -func newDepsConfig(cfg config.Provider) deps.DepsCfg { - l := langs.NewLanguage("en", cfg) - return deps.DepsCfg{ - Language: l, - Site: page.NewDummyHugoSite(cfg), - Cfg: cfg, - Fs: hugofs.NewMem(l), - Logger: logger, - TemplateProvider: DefaultTemplateProvider, - TranslationProvider: i18n.NewTranslationProvider(), - } -} - -func TestTemplateFuncsExamples(t *testing.T) { - t.Parallel() - c := qt.New(t) - - workingDir := "/home/hugo" - - v := newTestConfig() - - v.Set("workingDir", workingDir) - v.Set("multilingual", true) - v.Set("contentDir", "content") - v.Set("assetDir", "assets") - v.Set("baseURL", "http://mysite.com/hugo/") - v.Set("CurrentContentLanguage", langs.NewLanguage("en", v)) - - fs := hugofs.NewMem(v) - - afero.WriteFile(fs.Source, filepath.Join(workingDir, "files", "README.txt"), []byte("Hugo Rocks!"), 0755) - - depsCfg := newDepsConfig(v) - depsCfg.Fs = fs - d, err := deps.New(depsCfg) - c.Assert(err, qt.IsNil) - - var data struct { - Title string - Section string - Hugo map[string]interface{} - Params map[string]interface{} - } - - data.Title = "**BatMan**" - data.Section = "blog" - data.Params = map[string]interface{}{"langCode": "en"} - data.Hugo = map[string]interface{}{"Version": hugo.MustParseVersion("0.36.1").Version()} - - for _, nsf := range internal.TemplateFuncsNamespaceRegistry { - ns := nsf(d) - for _, mm := range ns.MethodMappings { - for i, example := range mm.Examples { - in, expected := example[0], example[1] - d.WithTemplate = func(templ tpl.TemplateHandler) error { - c.Assert(templ.AddTemplate("test", in), qt.IsNil) - c.Assert(templ.AddTemplate("partials/header.html", "Hugo Rocks!"), qt.IsNil) - return nil - } - c.Assert(d.LoadResources(), qt.IsNil) - - var b bytes.Buffer - templ, _ := d.Tmpl.Lookup("test") - c.Assert(templ.Execute(&b, &data), qt.IsNil) - if b.String() != expected { - t.Fatalf("%s[%d]: got %q expected %q", ns.Name, i, b.String(), expected) - } - } - } - } -} - -// TODO(bep) it would be dandy to put this one into the partials package, but -// we have some package cycle issues to solve first. -func TestPartialCached(t *testing.T) { - t.Parallel() - - c := qt.New(t) - - partial := `Now: {{ now.UnixNano }}` - name := "testing" - - var data struct { - } - - v := newTestConfig() - - config := newDepsConfig(v) - - config.WithTemplate = func(templ tpl.TemplateHandler) error { - err := templ.AddTemplate("partials/"+name, partial) - if err != nil { - return err - } - - return nil - } - - de, err := deps.New(config) - c.Assert(err, qt.IsNil) - c.Assert(de.LoadResources(), qt.IsNil) - - ns := partials.New(de) - - res1, err := ns.IncludeCached(name, &data) - c.Assert(err, qt.IsNil) - - for j := 0; j < 10; j++ { - time.Sleep(2 * time.Nanosecond) - res2, err := ns.IncludeCached(name, &data) - c.Assert(err, qt.IsNil) - - if !reflect.DeepEqual(res1, res2) { - t.Fatalf("cache mismatch") - } - - res3, err := ns.IncludeCached(name, &data, fmt.Sprintf("variant%d", j)) - c.Assert(err, qt.IsNil) - - if reflect.DeepEqual(res1, res3) { - t.Fatalf("cache mismatch") - } - } - -} - -func BenchmarkPartial(b *testing.B) { - doBenchmarkPartial(b, func(ns *partials.Namespace) error { - _, err := ns.Include("bench1") - return err - }) -} - -func BenchmarkPartialCached(b *testing.B) { - doBenchmarkPartial(b, func(ns *partials.Namespace) error { - _, err := ns.IncludeCached("bench1", nil) - return err - }) -} - -func doBenchmarkPartial(b *testing.B, f func(ns *partials.Namespace) error) { - c := qt.New(b) - config := newDepsConfig(viper.New()) - config.WithTemplate = func(templ tpl.TemplateHandler) error { - err := templ.AddTemplate("partials/bench1", `{{ shuffle (seq 1 10) }}`) - if err != nil { - return err - } - - return nil - } - - de, err := deps.New(config) - c.Assert(err, qt.IsNil) - c.Assert(de.LoadResources(), qt.IsNil) - - ns := partials.New(de) - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - if err := f(ns); err != nil { - b.Fatalf("error executing template: %s", err) - } - } - }) -} diff --git a/tpl/tplimpl/template_info_test.go b/tpl/tplimpl/template_info_test.go deleted file mode 100644 index c96e82d06..000000000 --- a/tpl/tplimpl/template_info_test.go +++ /dev/null @@ -1,57 +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 tplimpl - -import ( - "testing" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/deps" - "github.com/gohugoio/hugo/hugofs" - "github.com/gohugoio/hugo/tpl" -) - -func TestTemplateInfoShortcode(t *testing.T) { - c := qt.New(t) - d := newD(c) - h := d.Tmpl.(tpl.TemplateHandler) - - c.Assert(h.AddTemplate("shortcodes/mytemplate.html", ` -{{ .Inner }} -`), qt.IsNil) - - tt, found, _ := d.Tmpl.LookupVariant("mytemplate", tpl.TemplateVariants{}) - - c.Assert(found, qt.Equals, true) - tti, ok := tt.(tpl.TemplateInfoProvider) - c.Assert(ok, qt.Equals, true) - c.Assert(tti.TemplateInfo().IsInner, qt.Equals, true) - -} - -// TODO(bep) move and use in other places -func newD(c *qt.C) *deps.Deps { - v := newTestConfig() - fs := hugofs.NewMem(v) - - depsCfg := newDepsConfig(v) - depsCfg.Fs = fs - d, err := deps.New(depsCfg) - c.Assert(err, qt.IsNil) - - provider := DefaultTemplateProvider - provider.Update(d) - - return d - -} diff --git a/tpl/transform/init.go b/tpl/transform/init.go deleted file mode 100644 index 2783a513f..000000000 --- a/tpl/transform/init.go +++ /dev/null @@ -1,111 +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 transform - -import ( - "github.com/gohugoio/hugo/deps" - "github.com/gohugoio/hugo/tpl/internal" -) - -const name = "transform" - -func init() { - f := func(d *deps.Deps) *internal.TemplateFuncsNamespace { - ctx := New(d) - - ns := &internal.TemplateFuncsNamespace{ - Name: name, - Context: func(args ...interface{}) interface{} { return ctx }, - } - - ns.AddMethodMapping(ctx.Emojify, - []string{"emojify"}, - [][2]string{ - {`{{ "I :heart: Hugo" | emojify }}`, `I ❤ Hugo`}, - }, - ) - - ns.AddMethodMapping(ctx.Highlight, - []string{"highlight"}, - [][2]string{}, - ) - - ns.AddMethodMapping(ctx.HTMLEscape, - []string{"htmlEscape"}, - [][2]string{ - { - `{{ 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 `}, - }, - ) - - ns.AddMethodMapping(ctx.HTMLUnescape, - []string{"htmlUnescape"}, - [][2]string{ - { - `{{ 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>`}, - }, - ) - - ns.AddMethodMapping(ctx.Markdownify, - []string{"markdownify"}, - [][2]string{ - {`{{ .Title | markdownify}}`, `BatMan`}, - }, - ) - - ns.AddMethodMapping(ctx.Plainify, - []string{"plainify"}, - [][2]string{ - {`{{ plainify "Hello world, gophers!" }}`, `Hello world, gophers!`}, - }, - ) - - ns.AddMethodMapping(ctx.Remarshal, - nil, - [][2]string{ - {`{{ "title = \"Hello World\"" | transform.Remarshal "json" | safeHTML }}`, "{\n \"title\": \"Hello World\"\n}\n"}, - }, - ) - - ns.AddMethodMapping(ctx.Unmarshal, - []string{"unmarshal"}, - [][2]string{ - {`{{ "hello = \"Hello World\"" | transform.Unmarshal }}`, "map[hello:Hello World]"}, - {`{{ "hello = \"Hello World\"" | resources.FromString "data/greetings.toml" | transform.Unmarshal }}`, "map[hello:Hello World]"}, - }, - ) - - return ns - - } - - internal.AddTemplateFuncsNamespace(f) -} diff --git a/tpl/transform/init_test.go b/tpl/transform/init_test.go deleted file mode 100644 index 47bd8a391..000000000 --- a/tpl/transform/init_test.go +++ /dev/null @@ -1,40 +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 transform - -import ( - "testing" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/deps" - "github.com/gohugoio/hugo/htesting/hqt" - "github.com/gohugoio/hugo/tpl/internal" -) - -func TestInit(t *testing.T) { - c := qt.New(t) - var found bool - var ns *internal.TemplateFuncsNamespace - - for _, nsf := range internal.TemplateFuncsNamespaceRegistry { - ns = nsf(&deps.Deps{}) - if ns.Name == name { - found = true - break - } - } - - c.Assert(found, qt.Equals, true) - c.Assert(ns.Context(), hqt.IsSameType, &Namespace{}) -} diff --git a/tpl/transform/remarshal.go b/tpl/transform/remarshal.go deleted file mode 100644 index 182bd21d6..000000000 --- a/tpl/transform/remarshal.go +++ /dev/null @@ -1,62 +0,0 @@ -package transform - -import ( - "bytes" - "strings" - - "github.com/pkg/errors" - - "github.com/gohugoio/hugo/parser" - "github.com/gohugoio/hugo/parser/metadecoders" - "github.com/spf13/cast" -) - -// 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. -func (ns *Namespace) Remarshal(format string, data interface{}) (string, error) { - from, err := cast.ToStringE(data) - if err != nil { - return "", err - } - - from = strings.TrimSpace(from) - format = strings.TrimSpace(strings.ToLower(format)) - - if from == "" { - return "", nil - } - - mark, err := toFormatMark(format) - if err != nil { - return "", err - } - - fromFormat := metadecoders.Default.FormatFromContentString(from) - if fromFormat == "" { - return "", errors.New("failed to detect format from content") - } - - meta, err := metadecoders.Default.UnmarshalToMap([]byte(from), fromFormat) - if err != nil { - return "", err - } - - var result bytes.Buffer - if err := parser.InterfaceToConfig(meta, mark, &result); err != nil { - return "", err - } - - return result.String(), nil -} - -func toFormatMark(format string) (metadecoders.Format, error) { - if f := metadecoders.FormatFromString(format); f != "" { - return f, nil - } - - return "", errors.New("failed to detect target data serialization format") -} diff --git a/tpl/transform/remarshal_test.go b/tpl/transform/remarshal_test.go deleted file mode 100644 index 06bae42d4..000000000 --- a/tpl/transform/remarshal_test.go +++ /dev/null @@ -1,171 +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 transform - -import ( - "testing" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/helpers" - "github.com/spf13/viper" -) - -func TestRemarshal(t *testing.T) { - t.Parallel() - - v := viper.New() - v.Set("contentDir", "content") - ns := New(newDeps(v)) - c := qt.New(t) - - tomlExample := `title = "Test Metadata" - -[[resources]] - src = "**image-4.png" - title = "The Fourth Image!" - [resources.params] - byline = "picasso" - -[[resources]] - name = "my-cool-image-:counter" - src = "**.png" - title = "TOML: The Image #:counter" - [resources.params] - byline = "bep" -` - - yamlExample := `resources: -- params: - byline: picasso - src: '**image-4.png' - title: The Fourth Image! -- name: my-cool-image-:counter - params: - byline: bep - src: '**.png' - title: 'TOML: The Image #:counter' -title: Test Metadata -` - - jsonExample := `{ - "resources": [ - { - "params": { - "byline": "picasso" - }, - "src": "**image-4.png", - "title": "The Fourth Image!" - }, - { - "name": "my-cool-image-:counter", - "params": { - "byline": "bep" - }, - "src": "**.png", - "title": "TOML: The Image #:counter" - } - ], - "title": "Test Metadata" -} -` - - variants := []struct { - format string - data string - }{ - {"yaml", yamlExample}, - {"json", jsonExample}, - {"toml", tomlExample}, - {"TOML", tomlExample}, - {"Toml", tomlExample}, - {" TOML ", tomlExample}, - } - - for _, v1 := range variants { - for _, v2 := range variants { - // Both from and to may be the same here, but that is fine. - fromTo := qt.Commentf("%s => %s", v2.format, v1.format) - - converted, err := ns.Remarshal(v1.format, v2.data) - c.Assert(err, qt.IsNil, fromTo) - diff := helpers.DiffStrings(v1.data, converted) - if len(diff) > 0 { - t.Errorf("[%s] Expected \n%v\ngot\n%v\ndiff:\n%v", fromTo, v1.data, converted, diff) - } - - } - } - -} - -func TestRemarshalComments(t *testing.T) { - t.Parallel() - - v := viper.New() - v.Set("contentDir", "content") - ns := New(newDeps(v)) - - c := qt.New(t) - - input := ` -Hugo = "Rules" - -# It really does! - -[m] -# A comment -a = "b" - -` - - expected := ` -Hugo = "Rules" - -[m] - a = "b" -` - - for _, format := range []string{"json", "yaml", "toml"} { - fromTo := qt.Commentf("%s => %s", "toml", format) - - converted := input - var err error - // Do a round-trip conversion - for _, toFormat := range []string{format, "toml"} { - converted, err = ns.Remarshal(toFormat, converted) - c.Assert(err, qt.IsNil, fromTo) - } - - diff := helpers.DiffStrings(expected, converted) - if len(diff) > 0 { - t.Fatalf("[%s] Expected \n%v\ngot\n%v\ndiff:\n%v\n", fromTo, expected, converted, diff) - } - } -} - -func TestTestRemarshalError(t *testing.T) { - t.Parallel() - - v := viper.New() - v.Set("contentDir", "content") - ns := New(newDeps(v)) - c := qt.New(t) - - _, err := ns.Remarshal("asdf", "asdf") - c.Assert(err, qt.Not(qt.IsNil)) - - _, err = ns.Remarshal("json", "asdf") - c.Assert(err, qt.Not(qt.IsNil)) - -} diff --git a/tpl/transform/transform.go b/tpl/transform/transform.go deleted file mode 100644 index 2aa0c1959..000000000 --- a/tpl/transform/transform.go +++ /dev/null @@ -1,123 +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 transform provides template functions for transforming content. -package transform - -import ( - "html" - "html/template" - - "github.com/gohugoio/hugo/cache/namedmemcache" - - "github.com/gohugoio/hugo/deps" - "github.com/gohugoio/hugo/helpers" - "github.com/spf13/cast" -) - -// New returns a new instance of the transform-namespaced template functions. -func New(deps *deps.Deps) *Namespace { - cache := namedmemcache.New() - deps.BuildStartListeners.Add( - func() { - cache.Clear() - }) - - return &Namespace{ - cache: cache, - deps: deps, - } -} - -// Namespace provides template functions for the "transform" namespace. -type Namespace struct { - cache *namedmemcache.Cache - deps *deps.Deps -} - -// Emojify returns a copy of s with all emoji codes replaced with actual emojis. -// -// See http://www.emoji-cheat-sheet.com/ -func (ns *Namespace) Emojify(s interface{}) (template.HTML, error) { - ss, err := cast.ToStringE(s) - if err != nil { - return "", err - } - - return template.HTML(helpers.Emojify([]byte(ss))), nil -} - -// Highlight returns a copy of s as an HTML string with syntax -// highlighting applied. -func (ns *Namespace) Highlight(s interface{}, lang, opts string) (template.HTML, error) { - ss, err := cast.ToStringE(s) - if err != nil { - return "", err - } - - highlighted, _ := ns.deps.ContentSpec.Highlight(ss, lang, opts) - return template.HTML(highlighted), nil -} - -// HTMLEscape returns a copy of s with reserved HTML characters escaped. -func (ns *Namespace) HTMLEscape(s interface{}) (string, error) { - ss, err := cast.ToStringE(s) - if err != nil { - return "", err - } - - return html.EscapeString(ss), nil -} - -// HTMLUnescape returns a copy of with HTML escape requences converted to plain -// text. -func (ns *Namespace) HTMLUnescape(s interface{}) (string, error) { - ss, err := cast.ToStringE(s) - if err != nil { - return "", err - } - - return html.UnescapeString(ss), nil -} - -// Markdownify renders a given input from Markdown to HTML. -func (ns *Namespace) Markdownify(s interface{}) (template.HTML, error) { - ss, err := cast.ToStringE(s) - if err != nil { - return "", err - } - - m := ns.deps.ContentSpec.RenderBytes( - &helpers.RenderingContext{ - Cfg: ns.deps.Cfg, - Content: []byte(ss), - PageFmt: "markdown", - Config: ns.deps.ContentSpec.BlackFriday, - }, - ) - - // Strip if this is a short inline type of text. - m = ns.deps.ContentSpec.TrimShortHTML(m) - - return helpers.BytesToHTML(m), nil -} - -// Plainify returns a copy of s with all HTML tags removed. -func (ns *Namespace) Plainify(s interface{}) (string, error) { - ss, err := cast.ToStringE(s) - if err != nil { - return "", err - } - - return helpers.StripHTML(ss), nil -} diff --git a/tpl/transform/transform_test.go b/tpl/transform/transform_test.go deleted file mode 100644 index 10b020a0c..000000000 --- a/tpl/transform/transform_test.go +++ /dev/null @@ -1,252 +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 transform - -import ( - "html/template" - "testing" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/deps" - "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/hugofs" - "github.com/gohugoio/hugo/langs" - "github.com/spf13/viper" -) - -type tstNoStringer struct{} - -func TestEmojify(t *testing.T) { - t.Parallel() - c := qt.New(t) - - v := viper.New() - ns := New(newDeps(v)) - - for _, test := range []struct { - s interface{} - expect interface{} - }{ - {":notamoji:", template.HTML(":notamoji:")}, - {"I :heart: Hugo", template.HTML("I ❤ Hugo")}, - // errors - {tstNoStringer{}, false}, - } { - - result, err := ns.Emojify(test.s) - - if b, ok := test.expect.(bool); ok && !b { - c.Assert(err, qt.Not(qt.IsNil)) - continue - } - - c.Assert(err, qt.IsNil) - c.Assert(result, qt.Equals, test.expect) - } -} - -func TestHighlight(t *testing.T) { - t.Parallel() - c := qt.New(t) - - v := viper.New() - v.Set("contentDir", "content") - ns := New(newDeps(v)) - - for _, test := range []struct { - s interface{} - lang string - opts string - expect interface{} - }{ - {"func boo() {}", "go", "", "boo"}, - // Issue #4179 - {``, "xml", "", `&lt;`}, - {tstNoStringer{}, "go", "", false}, - } { - - result, err := ns.Highlight(test.s, test.lang, test.opts) - - if b, ok := test.expect.(bool); ok && !b { - c.Assert(err, qt.Not(qt.IsNil)) - continue - } - - c.Assert(err, qt.IsNil) - c.Assert(string(result), qt.Contains, test.expect.(string)) - } -} - -func TestHTMLEscape(t *testing.T) { - t.Parallel() - c := qt.New(t) - - v := viper.New() - v.Set("contentDir", "content") - ns := New(newDeps(v)) - - for _, test := range []struct { - s interface{} - expect interface{} - }{ - {`"Foo & Bar's Diner" `, `"Foo & Bar's Diner" <y@z>`}, - {"Hugo & Caddy > Wordpress & Apache", "Hugo & Caddy > Wordpress & Apache"}, - // errors - {tstNoStringer{}, false}, - } { - - result, err := ns.HTMLEscape(test.s) - - if b, ok := test.expect.(bool); ok && !b { - c.Assert(err, qt.Not(qt.IsNil)) - continue - } - - c.Assert(err, qt.IsNil) - c.Assert(result, qt.Equals, test.expect) - } -} - -func TestHTMLUnescape(t *testing.T) { - t.Parallel() - c := qt.New(t) - - v := viper.New() - v.Set("contentDir", "content") - ns := New(newDeps(v)) - - for _, test := range []struct { - s interface{} - expect interface{} - }{ - {`"Foo & Bar's Diner" <y@z>`, `"Foo & Bar's Diner" `}, - {"Hugo & Caddy > Wordpress & Apache", "Hugo & Caddy > Wordpress & Apache"}, - // errors - {tstNoStringer{}, false}, - } { - - result, err := ns.HTMLUnescape(test.s) - - if b, ok := test.expect.(bool); ok && !b { - c.Assert(err, qt.Not(qt.IsNil)) - continue - } - - c.Assert(err, qt.IsNil) - c.Assert(result, qt.Equals, test.expect) - } -} - -func TestMarkdownify(t *testing.T) { - t.Parallel() - c := qt.New(t) - - v := viper.New() - v.Set("contentDir", "content") - ns := New(newDeps(v)) - - for _, test := range []struct { - s interface{} - expect interface{} - }{ - {"Hello **World!**", template.HTML("Hello World!")}, - {[]byte("Hello Bytes **World!**"), template.HTML("Hello Bytes World!")}, - {tstNoStringer{}, false}, - } { - - result, err := ns.Markdownify(test.s) - - if b, ok := test.expect.(bool); ok && !b { - c.Assert(err, qt.Not(qt.IsNil)) - continue - } - - c.Assert(err, qt.IsNil) - c.Assert(result, qt.Equals, test.expect) - } -} - -// Issue #3040 -func TestMarkdownifyBlocksOfText(t *testing.T) { - t.Parallel() - c := qt.New(t) - v := viper.New() - v.Set("contentDir", "content") - ns := New(newDeps(v)) - - text := ` -#First - -This is some *bold* text. - -## Second - -This is some more text. - -And then some. -` - - result, err := ns.Markdownify(text) - c.Assert(err, qt.IsNil) - c.Assert(result, qt.Equals, template.HTML( - "

    #First

    \n\n

    This is some bold text.

    \n\n

    Second

    \n\n

    This is some more text.

    \n\n

    And then some.

    \n")) - -} - -func TestPlainify(t *testing.T) { - t.Parallel() - c := qt.New(t) - - v := viper.New() - ns := New(newDeps(v)) - - for _, test := range []struct { - s interface{} - expect interface{} - }{ - {"Note: blah blah", "Note: blah blah"}, - // errors - {tstNoStringer{}, false}, - } { - - result, err := ns.Plainify(test.s) - - if b, ok := test.expect.(bool); ok && !b { - c.Assert(err, qt.Not(qt.IsNil)) - continue - } - - c.Assert(err, qt.IsNil) - c.Assert(result, qt.Equals, test.expect) - } -} - -func newDeps(cfg config.Provider) *deps.Deps { - cfg.Set("contentDir", "content") - cfg.Set("i18nDir", "i18n") - - l := langs.NewLanguage("en", cfg) - - cs, err := helpers.NewContentSpec(l) - if err != nil { - panic(err) - } - - return &deps.Deps{ - Cfg: cfg, - Fs: hugofs.NewMem(l), - ContentSpec: cs, - } -} diff --git a/tpl/transform/unmarshal.go b/tpl/transform/unmarshal.go deleted file mode 100644 index da06b6aa1..000000000 --- a/tpl/transform/unmarshal.go +++ /dev/null @@ -1,167 +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 transform - -import ( - "io/ioutil" - "strings" - - "github.com/mitchellh/mapstructure" - - "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/parser/metadecoders" - "github.com/gohugoio/hugo/resources/resource" - "github.com/pkg/errors" - - "github.com/spf13/cast" -) - -// Unmarshal unmarshals the data given, which can be either a string -// or a Resource. Supported formats are JSON, TOML, YAML, and CSV. -// You can optionally provide an options map as the first argument. -func (ns *Namespace) Unmarshal(args ...interface{}) (interface{}, error) { - if len(args) < 1 || len(args) > 2 { - return nil, errors.New("unmarshal takes 1 or 2 arguments") - } - - var data interface{} - var decoder = metadecoders.Default - - if len(args) == 1 { - data = args[0] - } else { - m, ok := args[0].(map[string]interface{}) - if !ok { - return nil, errors.New("first argument must be a map") - } - - var err error - - data = args[1] - decoder, err = decodeDecoder(m) - if err != nil { - return nil, errors.WithMessage(err, "failed to decode options") - } - } - - if r, ok := data.(unmarshableResource); ok { - key := r.Key() - - if key == "" { - return nil, errors.New("no Key set in Resource") - } - - if decoder != metadecoders.Default { - key += decoder.OptionsKey() - } - - return ns.cache.GetOrCreate(key, func() (interface{}, error) { - f := metadecoders.FormatFromMediaType(r.MediaType()) - if f == "" { - return nil, errors.Errorf("MIME %q not supported", r.MediaType()) - } - - reader, err := r.ReadSeekCloser() - if err != nil { - return nil, err - } - defer reader.Close() - - b, err := ioutil.ReadAll(reader) - if err != nil { - return nil, err - } - - return decoder.Unmarshal(b, f) - }) - } - - dataStr, err := cast.ToStringE(data) - if err != nil { - return nil, errors.Errorf("type %T not supported", data) - } - - key := helpers.MD5String(dataStr) - - return ns.cache.GetOrCreate(key, func() (interface{}, error) { - f := decoder.FormatFromContentString(dataStr) - if f == "" { - return nil, errors.New("unknown format") - } - - return decoder.Unmarshal([]byte(dataStr), f) - }) -} - -// All the relevant resources implements this interface. -type unmarshableResource interface { - resource.ReadSeekCloserResource - resource.Identifier -} - -func decodeDecoder(m map[string]interface{}) (metadecoders.Decoder, error) { - opts := metadecoders.Default - - if m == nil { - return opts, nil - } - - // mapstructure does not support string to rune conversion, so do that manually. - // See https://github.com/mitchellh/mapstructure/issues/151 - for k, v := range m { - if strings.EqualFold(k, "Delimiter") { - r, err := stringToRune(v) - if err != nil { - return opts, err - } - opts.Delimiter = r - delete(m, k) - - } else if strings.EqualFold(k, "Comment") { - r, err := stringToRune(v) - if err != nil { - return opts, err - } - opts.Comment = r - delete(m, k) - } - } - - err := mapstructure.WeakDecode(m, &opts) - - return opts, err -} - -func stringToRune(v interface{}) (rune, error) { - s, err := cast.ToStringE(v) - if err != nil { - return 0, err - } - - if len(s) == 0 { - return 0, nil - } - - var r rune - - for i, rr := range s { - if i == 0 { - r = rr - } else { - return 0, errors.Errorf("invalid character: %q", v) - } - } - - return r, nil -} diff --git a/tpl/transform/unmarshal_test.go b/tpl/transform/unmarshal_test.go deleted file mode 100644 index 7b0caa07f..000000000 --- a/tpl/transform/unmarshal_test.go +++ /dev/null @@ -1,225 +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 transform - -import ( - "fmt" - "math/rand" - "strings" - "testing" - - "github.com/gohugoio/hugo/common/hugio" - - "github.com/gohugoio/hugo/media" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/resources/resource" - "github.com/spf13/viper" -) - -const ( - testJSON = ` - -{ - "ROOT_KEY": { - "title": "example glossary", - "GlossDiv": { - "title": "S", - "GlossList": { - "GlossEntry": { - "ID": "SGML", - "SortAs": "SGML", - "GlossTerm": "Standard Generalized Markup Language", - "Acronym": "SGML", - "Abbrev": "ISO 8879:1986", - "GlossDef": { - "para": "A meta-markup language, used to create markup languages such as DocBook.", - "GlossSeeAlso": ["GML", "XML"] - }, - "GlossSee": "markup" - } - } - } - } -} - - ` -) - -var _ resource.ReadSeekCloserResource = (*testContentResource)(nil) - -type testContentResource struct { - content string - mime media.Type - - key string -} - -func (t testContentResource) ReadSeekCloser() (hugio.ReadSeekCloser, error) { - return hugio.NewReadSeekerNoOpCloserFromString(t.content), nil -} - -func (t testContentResource) MediaType() media.Type { - return t.mime -} - -func (t testContentResource) Key() string { - return t.key -} - -func TestUnmarshal(t *testing.T) { - - v := viper.New() - ns := New(newDeps(v)) - c := qt.New(t) - - assertSlogan := func(m map[string]interface{}) { - c.Assert(m["slogan"], qt.Equals, "Hugo Rocks!") - } - - for _, test := range []struct { - data interface{} - options interface{} - expect interface{} - }{ - {`{ "slogan": "Hugo Rocks!" }`, nil, func(m map[string]interface{}) { - assertSlogan(m) - }}, - {`slogan: "Hugo Rocks!"`, nil, func(m map[string]interface{}) { - assertSlogan(m) - }}, - {`slogan = "Hugo Rocks!"`, nil, func(m map[string]interface{}) { - assertSlogan(m) - }}, - {testContentResource{key: "r1", content: `slogan: "Hugo Rocks!"`, mime: media.YAMLType}, nil, func(m map[string]interface{}) { - assertSlogan(m) - }}, - {testContentResource{key: "r1", content: `{ "slogan": "Hugo Rocks!" }`, mime: media.JSONType}, nil, func(m map[string]interface{}) { - assertSlogan(m) - }}, - {testContentResource{key: "r1", content: `slogan = "Hugo Rocks!"`, mime: media.TOMLType}, nil, func(m map[string]interface{}) { - assertSlogan(m) - }}, - {testContentResource{key: "r1", content: `1997,Ford,E350,"ac, abs, moon",3000.00 -1999,Chevy,"Venture ""Extended Edition""","",4900.00`, mime: media.CSVType}, nil, func(r [][]string) { - c.Assert(len(r), qt.Equals, 2) - first := r[0] - c.Assert(len(first), qt.Equals, 5) - c.Assert(first[1], qt.Equals, "Ford") - }}, - {testContentResource{key: "r1", content: `a;b;c`, mime: media.CSVType}, map[string]interface{}{"delimiter": ";"}, func(r [][]string) { - c.Assert([][]string{{"a", "b", "c"}}, qt.DeepEquals, r) - - }}, - {"a,b,c", nil, func(r [][]string) { - c.Assert([][]string{{"a", "b", "c"}}, qt.DeepEquals, r) - - }}, - {"a;b;c", map[string]interface{}{"delimiter": ";"}, func(r [][]string) { - c.Assert([][]string{{"a", "b", "c"}}, qt.DeepEquals, r) - - }}, - {testContentResource{key: "r1", content: ` -% This is a comment -a;b;c`, mime: media.CSVType}, map[string]interface{}{"DElimiter": ";", "Comment": "%"}, func(r [][]string) { - c.Assert([][]string{{"a", "b", "c"}}, qt.DeepEquals, r) - - }}, - // errors - {"thisisnotavaliddataformat", nil, false}, - {testContentResource{key: "r1", content: `invalid&toml"`, mime: media.TOMLType}, nil, false}, - {testContentResource{key: "r1", content: `unsupported: MIME"`, mime: media.CalendarType}, nil, false}, - {"thisisnotavaliddataformat", nil, false}, - {`{ notjson }`, nil, false}, - {tstNoStringer{}, nil, false}, - } { - - ns.cache.Clear() - - var args []interface{} - - if test.options != nil { - args = []interface{}{test.options, test.data} - } else { - args = []interface{}{test.data} - } - - result, err := ns.Unmarshal(args...) - - if b, ok := test.expect.(bool); ok && !b { - c.Assert(err, qt.Not(qt.IsNil)) - } else if fn, ok := test.expect.(func(m map[string]interface{})); ok { - c.Assert(err, qt.IsNil) - m, ok := result.(map[string]interface{}) - c.Assert(ok, qt.Equals, true) - fn(m) - } else if fn, ok := test.expect.(func(r [][]string)); ok { - c.Assert(err, qt.IsNil) - r, ok := result.([][]string) - c.Assert(ok, qt.Equals, true) - fn(r) - } else { - c.Assert(err, qt.IsNil) - c.Assert(result, qt.Equals, test.expect) - } - - } -} - -func BenchmarkUnmarshalString(b *testing.B) { - v := viper.New() - ns := New(newDeps(v)) - - const numJsons = 100 - - var jsons [numJsons]string - for i := 0; i < numJsons; i++ { - jsons[i] = strings.Replace(testJSON, "ROOT_KEY", fmt.Sprintf("root%d", i), 1) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - result, err := ns.Unmarshal(jsons[rand.Intn(numJsons)]) - if err != nil { - b.Fatal(err) - } - if result == nil { - b.Fatal("no result") - } - } -} - -func BenchmarkUnmarshalResource(b *testing.B) { - v := viper.New() - ns := New(newDeps(v)) - - const numJsons = 100 - - var jsons [numJsons]testContentResource - for i := 0; i < numJsons; i++ { - key := fmt.Sprintf("root%d", i) - jsons[i] = testContentResource{key: key, content: strings.Replace(testJSON, "ROOT_KEY", key, 1), mime: media.JSONType} - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - result, err := ns.Unmarshal(jsons[rand.Intn(numJsons)]) - if err != nil { - b.Fatal(err) - } - if result == nil { - b.Fatal("no result") - } - } -} diff --git a/tpl/urls/init.go b/tpl/urls/init.go deleted file mode 100644 index debaaabf9..000000000 --- a/tpl/urls/init.go +++ /dev/null @@ -1,74 +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 urls - -import ( - "github.com/gohugoio/hugo/deps" - "github.com/gohugoio/hugo/tpl/internal" -) - -const name = "urls" - -func init() { - f := func(d *deps.Deps) *internal.TemplateFuncsNamespace { - ctx := New(d) - - ns := &internal.TemplateFuncsNamespace{ - Name: name, - Context: func(args ...interface{}) interface{} { return ctx }, - } - - ns.AddMethodMapping(ctx.AbsURL, - []string{"absURL"}, - [][2]string{}, - ) - - ns.AddMethodMapping(ctx.AbsLangURL, - []string{"absLangURL"}, - [][2]string{}, - ) - ns.AddMethodMapping(ctx.Ref, - []string{"ref"}, - [][2]string{}, - ) - ns.AddMethodMapping(ctx.RelURL, - []string{"relURL"}, - [][2]string{}, - ) - ns.AddMethodMapping(ctx.RelLangURL, - []string{"relLangURL"}, - [][2]string{}, - ) - ns.AddMethodMapping(ctx.RelRef, - []string{"relref"}, - [][2]string{}, - ) - ns.AddMethodMapping(ctx.URLize, - []string{"urlize"}, - [][2]string{}, - ) - - ns.AddMethodMapping(ctx.Anchorize, - []string{"anchorize"}, - [][2]string{ - {`{{ "This is a title" | anchorize }}`, `this-is-a-title`}, - }, - ) - - return ns - - } - - internal.AddTemplateFuncsNamespace(f) -} diff --git a/tpl/urls/init_test.go b/tpl/urls/init_test.go deleted file mode 100644 index f88aaf398..000000000 --- a/tpl/urls/init_test.go +++ /dev/null @@ -1,41 +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 urls - -import ( - "testing" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/deps" - "github.com/gohugoio/hugo/htesting/hqt" - "github.com/gohugoio/hugo/tpl/internal" - "github.com/spf13/viper" -) - -func TestInit(t *testing.T) { - c := qt.New(t) - var found bool - var ns *internal.TemplateFuncsNamespace - - for _, nsf := range internal.TemplateFuncsNamespaceRegistry { - ns = nsf(&deps.Deps{Cfg: viper.New()}) - if ns.Name == name { - found = true - break - } - } - - c.Assert(found, qt.Equals, true) - c.Assert(ns.Context(), hqt.IsSameType, &Namespace{}) -} diff --git a/tpl/urls/urls.go b/tpl/urls/urls.go deleted file mode 100644 index eaa6538b3..000000000 --- a/tpl/urls/urls.go +++ /dev/null @@ -1,189 +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 urls provides template functions to deal with URLs. -package urls - -import ( - "errors" - "fmt" - - "html/template" - "net/url" - - "github.com/gohugoio/hugo/common/urls" - "github.com/gohugoio/hugo/deps" - _errors "github.com/pkg/errors" - "github.com/russross/blackfriday" - "github.com/spf13/cast" -) - -// New returns a new instance of the urls-namespaced template functions. -func New(deps *deps.Deps) *Namespace { - return &Namespace{ - deps: deps, - multihost: deps.Cfg.GetBool("multihost"), - } -} - -// Namespace provides template functions for the "urls" namespace. -type Namespace struct { - deps *deps.Deps - multihost bool -} - -// AbsURL takes a given string and converts it to an absolute URL. -func (ns *Namespace) AbsURL(a interface{}) (template.HTML, error) { - s, err := cast.ToStringE(a) - if err != nil { - return "", nil - } - - return template.HTML(ns.deps.PathSpec.AbsURL(s, false)), nil -} - -// Parse parses rawurl into a URL structure. The rawurl may be relative or -// absolute. -func (ns *Namespace) Parse(rawurl interface{}) (*url.URL, error) { - s, err := cast.ToStringE(rawurl) - if err != nil { - return nil, _errors.Wrap(err, "Error in Parse") - } - - return url.Parse(s) -} - -// RelURL takes a given string and prepends the relative path according to a -// page's position in the project directory structure. -func (ns *Namespace) RelURL(a interface{}) (template.HTML, error) { - s, err := cast.ToStringE(a) - if err != nil { - return "", nil - } - - return template.HTML(ns.deps.PathSpec.RelURL(s, false)), nil -} - -// URLize returns the given argument formatted as URL. -func (ns *Namespace) URLize(a interface{}) (string, error) { - s, err := cast.ToStringE(a) - if err != nil { - return "", nil - } - return ns.deps.PathSpec.URLize(s), nil -} - -// Anchorize creates sanitized anchor names that are compatible with Blackfriday. -func (ns *Namespace) Anchorize(a interface{}) (string, error) { - s, err := cast.ToStringE(a) - if err != nil { - return "", nil - } - return blackfriday.SanitizedAnchorName(s), nil -} - -// Ref returns the absolute URL path to a given content item. -func (ns *Namespace) Ref(in interface{}, args interface{}) (template.HTML, error) { - p, ok := in.(urls.RefLinker) - if !ok { - return "", errors.New("invalid Page received in Ref") - } - argsm, err := ns.refArgsToMap(args) - if err != nil { - return "", err - } - s, err := p.Ref(argsm) - return template.HTML(s), err -} - -// RelRef returns the relative URL path to a given content item. -func (ns *Namespace) RelRef(in interface{}, args interface{}) (template.HTML, error) { - p, ok := in.(urls.RefLinker) - if !ok { - return "", errors.New("invalid Page received in RelRef") - } - argsm, err := ns.refArgsToMap(args) - if err != nil { - return "", err - } - - s, err := p.RelRef(argsm) - return template.HTML(s), err -} - -func (ns *Namespace) refArgsToMap(args interface{}) (map[string]interface{}, error) { - var ( - s string - of string - ) - - v := args - if _, ok := v.([]interface{}); ok { - v = cast.ToStringSlice(v) - } - - switch v := v.(type) { - case map[string]interface{}: - return v, nil - case map[string]string: - m := make(map[string]interface{}) - for k, v := range v { - m[k] = v - } - return m, nil - case []string: - if len(v) == 0 || len(v) > 2 { - return nil, fmt.Errorf("invalid numer of arguments to ref") - } - // These where the options before we introduced the map type: - s = v[0] - if len(v) == 2 { - of = v[1] - } - default: - var err error - s, err = cast.ToStringE(args) - if err != nil { - return nil, err - } - - } - - return map[string]interface{}{ - "path": s, - "outputFormat": of, - }, nil -} - -// RelLangURL takes a given string and prepends the relative path according to a -// page's position in the project directory structure and the current language. -func (ns *Namespace) RelLangURL(a interface{}) (template.HTML, error) { - s, err := cast.ToStringE(a) - if err != nil { - return "", err - } - - return template.HTML(ns.deps.PathSpec.RelURL(s, !ns.multihost)), nil -} - -// AbsLangURL takes a given string and converts it to an absolute URL according -// to a page's position in the project directory structure and the current -// language. -func (ns *Namespace) AbsLangURL(a interface{}) (template.HTML, error) { - s, err := cast.ToStringE(a) - if err != nil { - return "", err - } - - return template.HTML(ns.deps.PathSpec.AbsURL(s, !ns.multihost)), nil -} diff --git a/tpl/urls/urls_test.go b/tpl/urls/urls_test.go deleted file mode 100644 index 9c005d2df..000000000 --- a/tpl/urls/urls_test.go +++ /dev/null @@ -1,69 +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 urls - -import ( - "net/url" - "testing" - - "github.com/gohugoio/hugo/htesting/hqt" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/deps" - "github.com/spf13/viper" -) - -var ns = New(&deps.Deps{Cfg: viper.New()}) - -type tstNoStringer struct{} - -func TestParse(t *testing.T) { - t.Parallel() - c := qt.New(t) - - for _, test := range []struct { - rawurl interface{} - expect interface{} - }{ - { - "http://www.google.com", - &url.URL{ - Scheme: "http", - Host: "www.google.com", - }, - }, - { - "http://j@ne:password@google.com", - &url.URL{ - Scheme: "http", - User: url.UserPassword("j@ne", "password"), - Host: "google.com", - }, - }, - // errors - {tstNoStringer{}, false}, - } { - - result, err := ns.Parse(test.rawurl) - - if b, ok := test.expect.(bool); ok && !b { - c.Assert(err, qt.Not(qt.IsNil)) - continue - } - - c.Assert(err, qt.IsNil) - c.Assert(result, - qt.CmpEquals(hqt.DeepAllowUnexported(&url.URL{}, url.Userinfo{})), test.expect) - } -} diff --git a/transform/chain.go b/transform/chain.go deleted file mode 100644 index 74217dc72..000000000 --- a/transform/chain.go +++ /dev/null @@ -1,112 +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 transform - -import ( - "bytes" - "io" - - bp "github.com/gohugoio/hugo/bufferpool" -) - -// Transformer is the func that needs to be implemented by a transformation step. -type Transformer func(ft FromTo) error - -// BytesReader wraps the Bytes method, usually implemented by bytes.Buffer, and an -// io.Reader. -type BytesReader interface { - // The slice given by Bytes is valid for use only until the next buffer modification. - // That is, if you want to use this value outside of the current transformer step, - // you need to take a copy. - Bytes() []byte - - io.Reader -} - -// FromTo is sent to each transformation step in the chain. -type FromTo interface { - From() BytesReader - To() io.Writer -} - -// Chain is an ordered processing chain. The next transform operation will -// receive the output from the previous. -type Chain []Transformer - -// New creates a content transformer chain given the provided transform funcs. -func New(trs ...Transformer) Chain { - return trs -} - -// NewEmpty creates a new slice of transformers with a capacity of 20. -func NewEmpty() Chain { - return make(Chain, 0, 20) -} - -// Implements contentTransformer -// Content is read from the from-buffer and rewritten to to the to-buffer. -type fromToBuffer struct { - from *bytes.Buffer - to *bytes.Buffer -} - -func (ft fromToBuffer) From() BytesReader { - return ft.from -} - -func (ft fromToBuffer) To() io.Writer { - return ft.to -} - -// Apply passes the given from io.Reader through the transformation chain. -// The result is written to to. -func (c *Chain) Apply(to io.Writer, from io.Reader) error { - if len(*c) == 0 { - _, err := io.Copy(to, from) - return err - } - - b1 := bp.GetBuffer() - defer bp.PutBuffer(b1) - - if _, err := b1.ReadFrom(from); err != nil { - return err - } - - b2 := bp.GetBuffer() - defer bp.PutBuffer(b2) - - fb := &fromToBuffer{from: b1, to: b2} - - for i, tr := range *c { - if i > 0 { - if fb.from == b1 { - fb.from = b2 - fb.to = b1 - fb.to.Reset() - } else { - fb.from = b1 - fb.to = b2 - fb.to.Reset() - } - } - - if err := tr(fb); err != nil { - return err - } - } - - _, err := fb.to.WriteTo(to) - return err -} diff --git a/transform/chain_test.go b/transform/chain_test.go deleted file mode 100644 index af3ae61d6..000000000 --- a/transform/chain_test.go +++ /dev/null @@ -1,70 +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 transform - -import ( - "bytes" - "strings" - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestChainZeroTransformers(t *testing.T) { - tr := New() - in := new(bytes.Buffer) - out := new(bytes.Buffer) - if err := tr.Apply(in, out); err != nil { - t.Errorf("A zero transformer chain returned an error.") - } -} - -func TestChaingMultipleTransformers(t *testing.T) { - f1 := func(ct FromTo) error { - _, err := ct.To().Write(bytes.Replace(ct.From().Bytes(), []byte("f1"), []byte("f1r"), -1)) - return err - } - f2 := func(ct FromTo) error { - _, err := ct.To().Write(bytes.Replace(ct.From().Bytes(), []byte("f2"), []byte("f2r"), -1)) - return err - } - f3 := func(ct FromTo) error { - _, err := ct.To().Write(bytes.Replace(ct.From().Bytes(), []byte("f3"), []byte("f3r"), -1)) - return err - } - - f4 := func(ct FromTo) error { - _, err := ct.To().Write(bytes.Replace(ct.From().Bytes(), []byte("f4"), []byte("f4r"), -1)) - return err - } - - tr := New(f1, f2, f3, f4) - - out := new(bytes.Buffer) - if err := tr.Apply(out, strings.NewReader("Test: f4 f3 f1 f2 f1 The End.")); err != nil { - t.Errorf("Multi transformer chain returned an error: %s", err) - } - - expected := "Test: f4r f3r f1r f2r f1r The End." - - if out.String() != expected { - t.Errorf("Expected %s got %s", expected, out.String()) - } -} - -func TestNewEmptyTransforms(t *testing.T) { - c := qt.New(t) - transforms := NewEmpty() - c.Assert(cap(transforms), qt.Equals, 20) -} diff --git a/transform/livereloadinject/livereloadinject.go b/transform/livereloadinject/livereloadinject.go deleted file mode 100644 index bee40f25c..000000000 --- a/transform/livereloadinject/livereloadinject.go +++ /dev/null @@ -1,47 +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 livereloadinject - -import ( - "bytes" - "fmt" - - "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/transform" -) - -// New creates a function that can be used -// to inject a script tag for the livereload JavaScript in a HTML document. -func New(port int) transform.Transformer { - return func(ft transform.FromTo) error { - b := ft.From().Bytes() - endBodyTag := "" - match := []byte(endBodyTag) - replaceTemplate := `%s` - replace := []byte(fmt.Sprintf(replaceTemplate, port, endBodyTag)) - - newcontent := bytes.Replace(b, match, replace, 1) - if len(newcontent) == len(b) { - endBodyTag = "" - replace := []byte(fmt.Sprintf(replaceTemplate, port, endBodyTag)) - match := []byte(endBodyTag) - newcontent = bytes.Replace(b, match, replace, 1) - } - - if _, err := ft.To().Write(newcontent); err != nil { - helpers.DistinctWarnLog.Println("Failed to inject LiveReload script:", err) - } - return nil - } -} diff --git a/transform/livereloadinject/livereloadinject_test.go b/transform/livereloadinject/livereloadinject_test.go deleted file mode 100644 index 413ca7b43..000000000 --- a/transform/livereloadinject/livereloadinject_test.go +++ /dev/null @@ -1,41 +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 livereloadinject - -import ( - "bytes" - "fmt" - "strings" - "testing" - - "github.com/gohugoio/hugo/transform" -) - -func TestLiveReloadInject(t *testing.T) { - doTestLiveReloadInject(t, "") - doTestLiveReloadInject(t, "") -} - -func doTestLiveReloadInject(t *testing.T, bodyEndTag string) { - out := new(bytes.Buffer) - in := strings.NewReader(bodyEndTag) - - tr := transform.New(New(1313)) - tr.Apply(out, in) - - expected := fmt.Sprintf(`%s`, bodyEndTag) - if out.String() != expected { - t.Errorf("Expected %s got %s", expected, out.String()) - } -} diff --git a/transform/metainject/hugogenerator.go b/transform/metainject/hugogenerator.go deleted file mode 100644 index 5f3a8f63b..000000000 --- a/transform/metainject/hugogenerator.go +++ /dev/null @@ -1,55 +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 metainject - -import ( - "bytes" - "fmt" - "regexp" - - "github.com/gohugoio/hugo/common/hugo" - "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/transform" -) - -var metaTagsCheck = regexp.MustCompile(`(?i)`, hugo.CurrentVersion) - -// HugoGenerator injects a meta generator tag for Hugo if none present. -func HugoGenerator(ft transform.FromTo) error { - b := ft.From().Bytes() - if metaTagsCheck.Match(b) { - if _, err := ft.To().Write(b); err != nil { - helpers.DistinctWarnLog.Println("Failed to inject Hugo generator tag:", err) - } - return nil - } - - head := "" - replace := []byte(fmt.Sprintf("%s\n\t%s", head, hugoGeneratorTag)) - newcontent := bytes.Replace(b, []byte(head), replace, 1) - - if len(newcontent) == len(b) { - head := "" - replace := []byte(fmt.Sprintf("%s\n\t%s", head, hugoGeneratorTag)) - newcontent = bytes.Replace(b, []byte(head), replace, 1) - } - - if _, err := ft.To().Write(newcontent); err != nil { - helpers.DistinctWarnLog.Println("Failed to inject Hugo generator tag:", err) - } - - return nil - -} diff --git a/transform/metainject/hugogenerator_test.go b/transform/metainject/hugogenerator_test.go deleted file mode 100644 index ffb4c1425..000000000 --- a/transform/metainject/hugogenerator_test.go +++ /dev/null @@ -1,61 +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 metainject - -import ( - "bytes" - "strings" - "testing" - - "github.com/gohugoio/hugo/transform" -) - -func TestHugoGeneratorInject(t *testing.T) { - hugoGeneratorTag = "META" - for i, this := range []struct { - in string - expect string - }{ - {` - -`, ` - META - -`}, - {` - -`, ` - META - -`}, - {``, ``}, - {``, ``}, - {``, ``}, - {``, ``}, - {"", ""}, - {"", ""}, - {"", "\n\tMETA"}, - } { - in := strings.NewReader(this.in) - out := new(bytes.Buffer) - - tr := transform.New(HugoGenerator) - tr.Apply(out, in) - - if out.String() != this.expect { - t.Errorf("[%d] Expected \n%q got \n%q", i, this.expect, out.String()) - } - } - -} diff --git a/transform/urlreplacers/absurl.go b/transform/urlreplacers/absurl.go deleted file mode 100644 index 029d94da2..000000000 --- a/transform/urlreplacers/absurl.go +++ /dev/null @@ -1,36 +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 urlreplacers - -import "github.com/gohugoio/hugo/transform" - -var ar = newAbsURLReplacer() - -// NewAbsURLTransformer replaces relative URLs with absolute ones -// in HTML files, using the baseURL setting. -func NewAbsURLTransformer(path string) transform.Transformer { - return func(ft transform.FromTo) error { - ar.replaceInHTML(path, ft) - return nil - } -} - -// NewAbsURLInXMLTransformer replaces relative URLs with absolute ones -// in XML files, using the baseURL setting. -func NewAbsURLInXMLTransformer(path string) transform.Transformer { - return func(ft transform.FromTo) error { - ar.replaceInXML(path, ft) - return nil - } -} diff --git a/transform/urlreplacers/absurlreplacer.go b/transform/urlreplacers/absurlreplacer.go deleted file mode 100644 index 3fef013fb..000000000 --- a/transform/urlreplacers/absurlreplacer.go +++ /dev/null @@ -1,260 +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 urlreplacers - -import ( - "bytes" - "io" - "unicode" - "unicode/utf8" - - "github.com/gohugoio/hugo/transform" -) - -type absurllexer struct { - // the source to absurlify - content []byte - // the target for the new absurlified content - w io.Writer - - // path may be set to a "." relative path - path []byte - - pos int // input position - start int // item start position - - quotes [][]byte -} - -type prefix struct { - disabled bool - b []byte - f func(l *absurllexer) - - nextPos int -} - -func (p *prefix) find(bs []byte, start int) bool { - if p.disabled { - return false - } - - if p.nextPos == -1 { - idx := bytes.Index(bs[start:], p.b) - - if idx == -1 { - p.disabled = true - // Find the closest match - return false - } - - p.nextPos = start + idx + len(p.b) - } - - return true -} - -func newPrefixState() []*prefix { - return []*prefix{ - {b: []byte("src="), f: checkCandidateBase}, - {b: []byte("href="), f: checkCandidateBase}, - {b: []byte("action="), f: checkCandidateBase}, - {b: []byte("srcset="), f: checkCandidateSrcset}, - } -} - -func (l *absurllexer) emit() { - l.w.Write(l.content[l.start:l.pos]) - l.start = l.pos -} - -var ( - relURLPrefix = []byte("/") - relURLPrefixLen = len(relURLPrefix) -) - -func (l *absurllexer) consumeQuote() []byte { - for _, q := range l.quotes { - if bytes.HasPrefix(l.content[l.pos:], q) { - l.pos += len(q) - l.emit() - return q - } - } - return nil -} - -// handle URLs in src and href. -func checkCandidateBase(l *absurllexer) { - l.consumeQuote() - - if !bytes.HasPrefix(l.content[l.pos:], relURLPrefix) { - return - } - - // check for schemaless URLs - posAfter := l.pos + relURLPrefixLen - if posAfter >= len(l.content) { - return - } - r, _ := utf8.DecodeRune(l.content[posAfter:]) - if r == '/' { - // schemaless: skip - return - } - if l.pos > l.start { - l.emit() - } - l.pos += relURLPrefixLen - l.w.Write(l.path) - l.start = l.pos -} - -func (l *absurllexer) posAfterURL(q []byte) int { - if len(q) > 0 { - // look for end quote - return bytes.Index(l.content[l.pos:], q) - } - - return bytes.IndexFunc(l.content[l.pos:], func(r rune) bool { - return r == '>' || unicode.IsSpace(r) - }) - -} - -// handle URLs in srcset. -func checkCandidateSrcset(l *absurllexer) { - q := l.consumeQuote() - if q == nil { - // srcset needs to be quoted. - return - } - - // special case, not frequent (me think) - if !bytes.HasPrefix(l.content[l.pos:], relURLPrefix) { - return - } - - // check for schemaless URLs - posAfter := l.pos + relURLPrefixLen - if posAfter >= len(l.content) { - return - } - r, _ := utf8.DecodeRune(l.content[posAfter:]) - if r == '/' { - // schemaless: skip - return - } - - posEnd := l.posAfterURL(q) - - // safe guard - if posEnd < 0 || posEnd > 2000 { - return - } - - if l.pos > l.start { - l.emit() - } - - section := l.content[l.pos : l.pos+posEnd+1] - - fields := bytes.Fields(section) - for i, f := range fields { - if f[0] == '/' { - l.w.Write(l.path) - l.w.Write(f[1:]) - - } else { - l.w.Write(f) - } - - if i < len(fields)-1 { - l.w.Write([]byte(" ")) - } - } - - l.pos += len(section) - l.start = l.pos - -} - -// main loop -func (l *absurllexer) replace() { - contentLength := len(l.content) - - prefixes := newPrefixState() - - for { - if l.pos >= contentLength { - break - } - - var match *prefix - - for _, p := range prefixes { - if !p.find(l.content, l.pos) { - continue - } - - if match == nil || p.nextPos < match.nextPos { - match = p - } - } - - if match == nil { - // Done! - l.pos = contentLength - break - } else { - l.pos = match.nextPos - match.nextPos = -1 - match.f(l) - } - } - // Done! - if l.pos > l.start { - l.emit() - } -} - -func doReplace(path string, ct transform.FromTo, quotes [][]byte) { - - lexer := &absurllexer{ - content: ct.From().Bytes(), - w: ct.To(), - path: []byte(path), - quotes: quotes} - - lexer.replace() -} - -type absURLReplacer struct { - htmlQuotes [][]byte - xmlQuotes [][]byte -} - -func newAbsURLReplacer() *absURLReplacer { - return &absURLReplacer{ - htmlQuotes: [][]byte{[]byte("\""), []byte("'")}, - xmlQuotes: [][]byte{[]byte("""), []byte("'")}} -} - -func (au *absURLReplacer) replaceInHTML(path string, ct transform.FromTo) { - doReplace(path, ct, au.htmlQuotes) -} - -func (au *absURLReplacer) replaceInXML(path string, ct transform.FromTo) { - doReplace(path, ct, au.xmlQuotes) -} diff --git a/transform/urlreplacers/absurlreplacer_test.go b/transform/urlreplacers/absurlreplacer_test.go deleted file mode 100644 index 3c2dbf777..000000000 --- a/transform/urlreplacers/absurlreplacer_test.go +++ /dev/null @@ -1,237 +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 urlreplacers - -import ( - "path/filepath" - "testing" - - bp "github.com/gohugoio/hugo/bufferpool" - - "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/transform" -) - -const ( - h5JsContentDoubleQuote = "" - h5JsContentSingleQuote = "" - h5JsContentAbsURL = "" - h5JsContentAbsURLSchemaless = "" - corectOutputSrcHrefSq = "" - - h5XMLXontentAbsURL = "<p><a href="/foobar">foobar</a></p> <p>A video: <iframe src='/foo'></iframe></p>" - correctOutputSrcHrefInXML = "<p><a href="http://base/foobar">foobar</a></p> <p>A video: <iframe src='http://base/foo'></iframe></p>" - h5XMLContentGuarded = "<p><a href="//foobar">foobar</a></p> <p>A video: <iframe src='//foo'></iframe></p>" -) - -const ( - // additional sanity tests for replacements testing - replace1 = "No replacements." - replace2 = "ᚠᛇᚻ ᛒᛦᚦ ᚠᚱᚩᚠᚢᚱ\nᚠᛁᚱᚪ ᚷᛖᚻᚹᛦᛚᚳᚢᛗ" - replace3 = `End of file: src="/` - replace5 = `Srcsett with no closing quote: srcset="/img/small.jpg do be do be do.` - - // Issue: 816, schemaless links combined with others - replaceSchemalessHTML = `Pre. src='//schemaless' src='/normal' Schemaless. normal. Post.` - replaceSchemalessHTMLCorrect = `Pre. src='//schemaless' src='http://base/normal' Schemaless. normal. Post.` - replaceSchemalessXML = `Pre. src='//schemaless' src='/normal' Schemaless. normal. Post.` - replaceSchemalessXMLCorrect = `Pre. src='//schemaless' src='http://base/normal' Schemaless. normal. Post.` -) - -const ( - // srcset= - srcsetBasic = `Pre. text` - srcsetBasicCorrect = `Pre. text` - srcsetSingleQuote = `Pre. text POST.` - srcsetSingleQuoteCorrect = `Pre. text POST.` - srcsetXMLBasic = `Pre. "text"` - srcsetXMLBasicCorrect = `Pre. "text"` - srcsetXMLSingleQuote = `Pre. "text"` - srcsetXMLSingleQuoteCorrect = `Pre. "text"` - srcsetVariations = `Pre. -Missing start quote: text src='/img/foo.jpg'> FOO. - -schemaless: -schemaless2: text src='http://base/img/foo.jpg'> FOO. - -schemaless: -schemaless2: ASDF`, - expected: `Link: ASDF`, - }, - { - content: `Link: ASDF`, - expected: `Link: ASDF`, - }, - }) -} - -func TestRelativeURL(t *testing.T) { - tr := transform.New(NewAbsURLTransformer(helpers.GetDottedRelativePath(filepath.FromSlash("/post/sub/")))) - - applyWithPath(t.Errorf, tr, relurlTests) - -} - -func TestAbsURLSrcSet(t *testing.T) { - tr := transform.New(NewAbsURLTransformer(testBaseURL)) - - apply(t.Errorf, tr, srcsetTests) -} - -func TestAbsXMLURLSrcSet(t *testing.T) { - tr := transform.New(NewAbsURLInXMLTransformer(testBaseURL)) - - apply(t.Errorf, tr, srcsetXMLTests) -} - -func BenchmarkXMLAbsURL(b *testing.B) { - tr := transform.New(NewAbsURLInXMLTransformer(testBaseURL)) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - apply(b.Errorf, tr, xmlAbsURLBenchTests) - } -} - -func TestXMLAbsURL(t *testing.T) { - tr := transform.New(NewAbsURLInXMLTransformer(testBaseURL)) - apply(t.Errorf, tr, xmlAbsURLTests) -} - -func apply(ef errorf, tr transform.Chain, tests []test) { - applyWithPath(ef, tr, tests) -} - -func applyWithPath(ef errorf, tr transform.Chain, tests []test) { - out := bp.GetBuffer() - defer bp.PutBuffer(out) - - in := bp.GetBuffer() - defer bp.PutBuffer(in) - - for _, test := range tests { - var err error - in.WriteString(test.content) - err = tr.Apply(out, in) - if err != nil { - ef("Unexpected error: %s", err) - } - if test.expected != out.String() { - ef("Expected:\n%s\nGot:\n%s", test.expected, out.String()) - } - out.Reset() - in.Reset() - } -} - -type test struct { - content string - expected string -} - -type errorf func(string, ...interface{}) diff --git a/watcher/batcher.go b/watcher/batcher.go deleted file mode 100644 index 6f4b276cf..000000000 --- a/watcher/batcher.go +++ /dev/null @@ -1,73 +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 watcher - -import ( - "time" - - "github.com/fsnotify/fsnotify" -) - -// Batcher batches file watch events in a given interval. -type Batcher struct { - *fsnotify.Watcher - interval time.Duration - done chan struct{} - - Events chan []fsnotify.Event // Events are returned on this channel -} - -// New creates and starts a Batcher with the given time interval. -func New(interval time.Duration) (*Batcher, error) { - watcher, err := fsnotify.NewWatcher() - - batcher := &Batcher{} - batcher.Watcher = watcher - batcher.interval = interval - batcher.done = make(chan struct{}, 1) - batcher.Events = make(chan []fsnotify.Event, 1) - - if err == nil { - go batcher.run() - } - - return batcher, err -} - -func (b *Batcher) run() { - tick := time.Tick(b.interval) - evs := make([]fsnotify.Event, 0) -OuterLoop: - for { - select { - case ev := <-b.Watcher.Events: - evs = append(evs, ev) - case <-tick: - if len(evs) == 0 { - continue - } - b.Events <- evs - evs = make([]fsnotify.Event, 0) - case <-b.done: - break OuterLoop - } - } - close(b.done) -} - -// Close stops the watching of the files. -func (b *Batcher) Close() { - b.done <- struct{}{} - b.Watcher.Close() -}