diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml new file mode 100644 index 0000000000..784fc28593 --- /dev/null +++ b/.github/workflows/build-and-test.yml @@ -0,0 +1,66 @@ +name: Build & Test + +on: + push: + branches: ["*"] + +env: + # Cross-compilation for aarch64 requires a different linker + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc + +permissions: + contents: read + +jobs: + Tests: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + target: + - x86_64-unknown-linux-gnu + - aarch64-unknown-linux-gnu + - x86_64-pc-windows-msvc + - x86_64-apple-darwin + - aarch64-apple-darwin + rustup_toolchain: [stable] + include: + - os: windows-2022 + target: x86_64-pc-windows-msvc + - os: ubuntu-20.04 + target: x86_64-unknown-linux-gnu + - os: ubuntu-20.04 + target: aarch64-unknown-linux-gnu + - os: macos-13 + target: x86_64-apple-darwin + - os: macos-14 + target: aarch64-apple-darwin + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ matrix.rustup_toolchain }} + + - name: Install Rust crosscompile tools + if: ${{ contains(matrix.target, 'aarch64-unknown-linux-gnu') }} + run: | + sudo apt-get update -y + sudo apt-get install -y make g++ libssl-dev gcc-aarch64-linux-gnu + rustup target add aarch64-unknown-linux-gnu + + - name: Cargo build (Native TLS) + run: | + cargo build --all --no-default-features --features=native-tls + cargo clean + + - name: Cargo build (Rust TLS) + run: cargo build --all + + - name: Cargo test + run: cargo test --all + + - name: Cargo fmt + run: cargo fmt --check diff --git a/.github/workflows/cd-workflow.yml b/.github/workflows/cd-workflow.yml deleted file mode 100644 index 6d20ea7664..0000000000 --- a/.github/workflows/cd-workflow.yml +++ /dev/null @@ -1,50 +0,0 @@ -# Mostly copied from https://docs.github.com/en/packages/managing-github-packages-using-github-actions-workflows/publishing-and-installing-a-package-with-github-actions#publishing-a-package-using-an-action -# Main difference is the push filter on the tag. -# -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -name: Create and publish a Docker image - -on: - push: - tags: [ 'v*.*.*' ] - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - -jobs: - build-and-push-image: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - name: Log in to the Container registry - uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - flavor: latest=false - - - name: Build and push Docker image - uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc - with: - context: . - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 1f1db2fb77..ba06fdeccf 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -20,7 +20,7 @@ jobs: env: BUILD_DIR: docs/ BUILD_ONLY: true - + build_and_deploy: runs-on: ubuntu-latest if: github.ref == 'refs/heads/master' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..ae79c850ba --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,165 @@ +name: Release + +on: + push: + tags: ["v*.*.*"] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + # Cross-compilation for aarch64 requires a different linker + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc + +permissions: + contents: read + +jobs: + Release-Build: + runs-on: ${{ matrix.os }} + permissions: + contents: read + attestations: write + id-token: write + strategy: + matrix: + target: + - x86_64-unknown-linux-gnu + - aarch64-unknown-linux-gnu + - x86_64-pc-windows-msvc + - x86_64-apple-darwin + - aarch64-apple-darwin + rustup_toolchain: [stable] + include: + - os: windows-2022 + target: x86_64-pc-windows-msvc + - os: ubuntu-20.04 + target: x86_64-unknown-linux-gnu + - os: ubuntu-20.04 + target: aarch64-unknown-linux-gnu + - os: macos-13 + target: x86_64-apple-darwin + - os: macos-14 + target: aarch64-apple-darwin + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ matrix.rustup_toolchain }} + + - name: Install Rust crosscompile tools + if: ${{ contains(matrix.target, 'aarch64-unknown-linux-gnu') }} + run: | + sudo apt-get update -y + sudo apt-get install -y make g++ libssl-dev gcc-aarch64-linux-gnu + rustup target add aarch64-unknown-linux-gnu + + - name: Cargo build + run: cargo build --release --target ${{ matrix.target }} + + - name: Archive (UNIX) + run: | + mkdir -p artifacts + cp -av target/${{ matrix.target }}/release/zola . + tar -czf ${{ github.event.repository.name }}-${{ github.ref_name }}-${{ matrix.target }}.tar.gz zola + if: ${{ ! startsWith(matrix.os, 'windows') }} + + - name: Archive (Windows) + run: | + mkdir -p artifacts + cp target/${{ matrix.target }}/release/zola.exe . + Compress-Archive zola.exe ${{ github.event.repository.name }}-${{ github.ref_name }}-${{ matrix.target }}.zip + if: ${{ startsWith(matrix.os, 'windows') }} + + - name: Attest Build Provenance + uses: actions/attest-build-provenance@v1 + continue-on-error: true + with: + subject-path: ${{ github.event.repository.name }}-${{ github.ref_name }}-${{ matrix.target }}.* + + - uses: actions/upload-artifact@v4 + with: + name: ${{ github.event.repository.name }}-${{ github.ref_name }}-${{ matrix.target }} + path: ${{ github.event.repository.name }}-${{ github.ref_name }}-${{ matrix.target }}.* + if-no-files-found: error + retention-days: 7 + + Release: + needs: [Release-Build] + runs-on: ubuntu-22.04 + permissions: + contents: write + + steps: + - name: Ensure artifacts dir exists + run: mkdir -p artifacts + + - name: Download Artifact + uses: actions/download-artifact@v4 + with: + path: artifacts + merge-multiple: true + + - name: Release + uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 + with: + name: ${{ github.ref_name }} + tag_name: ${{ github.ref_name }} + generate_release_notes: true + fail_on_unmatched_files: true + body: | + Welcome to this new release of Zola ${{ github.ref_name }}! + + All artifacts are signed with this repos identity using Sigstore. + You can verify the signatures using the `GitHub` CLI. + + ```shell + gh attestation verify --owner ${{ github.repository_owner }} + ``` + token: ${{ secrets.GITHUB_TOKEN }} + prerelease: ${{ contains(github.ref, '-pre') }} + files: artifacts/* + + Release-Container-Image: + needs: [Release] + runs-on: ubuntu-22.04 + permissions: + contents: read + packages: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf + + - name: Setup Docker buildx + uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db + + - name: Log in to the Container registry + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + flavor: latest=false + + - name: Build and push Docker image + uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc + with: + context: . + push: true + build-args: | + USE_GH_RELEASE=true + ZOLA_RELEASE_VERSION=${{ github.ref_name }} + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.gitmodules b/.gitmodules index aba5d052a8..19bd59a65c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,78 +1,78 @@ [submodule "sublime/syntaxes/Packages"] - path = sublime/syntaxes/Packages + path = components/config/sublime/syntaxes/Packages url = https://github.com/sublimehq/Packages.git [submodule "sublime/syntaxes/awk-sublime"] - path = sublime/syntaxes/extra/awk-sublime + path = components/config/sublime/syntaxes/extra/awk-sublime url = https://github.com/JohnNilsson/awk-sublime.git [submodule "sublime/syntaxes/AsciiDoc"] path = sublime/syntaxes/AsciiDoc url = https://github.com/SublimeText/AsciiDoc.git [submodule "sublime/syntaxes/Sublime-CMakeLists"] - path = sublime/syntaxes/extra/Sublime-CMakeLists + path = components/config/sublime/syntaxes/extra/Sublime-CMakeLists url = https://github.com/zyxar/Sublime-CMakeLists.git [submodule "sublime/syntaxes/SublimeTextLinkerSyntax"] - path = sublime/syntaxes/extra/SublimeTextLinkerSyntax + path = components/config/sublime/syntaxes/extra/SublimeTextLinkerSyntax url = https://github.com/jbw3/SublimeTextLinkerSyntax [submodule "sublime/syntaxes/Docker.tmbundle"] - path = sublime/syntaxes/extra/Docker.tmbundle + path = components/config/sublime/syntaxes/extra/Docker.tmbundle url = https://github.com/asbjornenge/Docker.tmbundle.git [submodule "sublime/syntaxes/Sublime-VimL"] path = sublime/syntaxes/Sublime-VimL url = https://github.com/SalGnt/Sublime-VimL.git [submodule "sublime/syntaxes/elixir-sublime-syntax"] - path = sublime/syntaxes/extra/elixir-sublime-syntax + path = components/config/sublime/syntaxes/extra/elixir-sublime-syntax url = https://github.com/princemaple/elixir-sublime-syntax.git [submodule "sublime/syntaxes/SublimeElmLanguageSupport"] - path = sublime/syntaxes/extra/SublimeElmLanguageSupport + path = components/config/sublime/syntaxes/extra/SublimeElmLanguageSupport url = https://github.com/elm-community/SublimeElmLanguageSupport.git [submodule "sublime/syntaxes/sublimetext-fsharp"] - path = sublime/syntaxes/extra/sublimetext-fsharp + path = components/config/sublime/syntaxes/extra/sublimetext-fsharp url = https://github.com/hoest/sublimetext-fsharp.git [submodule "sublime/syntaxes/sublime-fish"] - path = sublime/syntaxes/extra/sublime-fish + path = components/config/sublime/syntaxes/extra/sublime-fish url = https://github.com/Phidica/sublime-fish.git [submodule "sublime/syntaxes/SublimeFortran"] - path = sublime/syntaxes/extra/SublimeFortran + path = components/config/sublime/syntaxes/extra/SublimeFortran url = https://github.com/315234/SublimeFortran.git [submodule "sublime/syntaxes/GraphQL-SublimeText3"] - path = sublime/syntaxes/extra/GraphQL-SublimeText3 + path = components/config/sublime/syntaxes/extra/GraphQL-SublimeText3 url = https://github.com/dncrews/GraphQL-SublimeText3.git [submodule "sublime/syntaxes/Sublime-GenericConfig"] - path = sublime/syntaxes/extra/Sublime-GenericConfig + path = components/config/sublime/syntaxes/extra/Sublime-GenericConfig url = https://github.com/skozlovf/Sublime-GenericConfig.git [submodule "sublime/syntaxes/sublime-jinja2"] - path = sublime/syntaxes/extra/sublime-jinja2 + path = components/config/sublime/syntaxes/extra/sublime-jinja2 url = https://github.com/Martin819/sublime-jinja2.git [submodule "sublime/syntaxes/Julia-sublime"] - path = sublime/syntaxes/extra/Julia-sublime + path = components/config/sublime/syntaxes/extra/Julia-sublime url = https://github.com/JuliaEditorSupport/Julia-sublime.git [submodule "sublime/syntaxes/LESS-sublime"] - path = sublime/syntaxes/extra/LESS-sublime + path = components/config/sublime/syntaxes/extra/LESS-sublime url = https://github.com/danro/LESS-sublime.git [submodule "sublime/syntaxes/sublime-purescript-syntax"] - path = sublime/syntaxes/extra/sublime-purescript-syntax + path = components/config/sublime/syntaxes/extra/sublime-purescript-syntax url = https://github.com/tellnobody1/sublime-purescript-syntax.git [submodule "sublime/syntaxes/SublimeSass"] - path = sublime/syntaxes/extra/SublimeSass + path = components/config/sublime/syntaxes/extra/SublimeSass url = https://github.com/braver/SublimeSass.git [submodule "sublime/syntaxes/sublime_toml_highlighting"] - path = sublime/syntaxes/extra/sublime_toml_highlighting + path = components/config/sublime/syntaxes/extra/sublime_toml_highlighting url = https://github.com/jasonwilliams/sublime_toml_highlighting.git [submodule "sublime/syntaxes/vue-syntax-highlight"] - path = sublime/syntaxes/extra/vue-syntax-highlight + path = components/config/sublime/syntaxes/extra/vue-syntax-highlight url = https://github.com/vuejs/vue-syntax-highlight.git [submodule "sublime/syntaxes/sublime-glsl"] - path = sublime/syntaxes/extra/sublime-glsl + path = components/config/sublime/syntaxes/extra/sublime-glsl url = https://github.com/euler0/sublime-glsl.git [submodule "sublime/syntaxes/GDScript-sublime"] - path = sublime/syntaxes/extra/GDScript-sublime + path = components/config/sublime/syntaxes/extra/GDScript-sublime url = https://github.com/beefsack/GDScript-sublime.git [submodule "sublime/syntaxes/extra/sublime-clojure"] - path = sublime/syntaxes/extra/sublime-clojure + path = components/config/sublime/syntaxes/extra/sublime-clojure url = https://github.com/tonsky/sublime-clojure.git [submodule "sublime/syntaxes/extra/sublime-zig-language"] - path = sublime/syntaxes/extra/sublime-zig-language + path = components/config/sublime/syntaxes/extra/sublime-zig-language url = https://github.com/ziglang/sublime-zig-language.git [submodule "sublime/syntaxes/extra/protobuf-syntax-highlighting"] - path = sublime/syntaxes/extra/protobuf-syntax-highlighting + path = components/config/sublime/syntaxes/extra/protobuf-syntax-highlighting url = https://github.com/VcamX/protobuf-syntax-highlighting.git diff --git a/CHANGELOG.md b/CHANGELOG.md index b446f55c3d..dd9e84c390 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## 0.20.0 (unreleased) + ## 0.19.2 (2024-08-15) - Fix some of YAML date parsing diff --git a/Dockerfile b/Dockerfile index 87b3ec247b..0741cc93c4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,26 @@ FROM rust:slim-bookworm AS builder +ARG USE_GH_RELEASE=false +ARG ZOLA_RELEASE_VERSION=latest RUN apt-get update -y && \ - apt-get install -y make g++ libssl-dev && \ - rustup target add x86_64-unknown-linux-gnu + apt-get install -y pkg-config make g++ libssl-dev curl jq tar gzip WORKDIR /app COPY . . -RUN cargo build --release --target x86_64-unknown-linux-gnu - +RUN if [ "${USE_GH_RELEASE}" = "true" ]; then \ + if [ "${ZOLA_RELEASE_VERSION}" = "latest" ]; then \ + export ZOLA_VERSION=$(curl -sL https://api.github.com/repos/getzola/zola/releases/latest | jq -r .name); \ + else \ + export ZOLA_VERSION="${ZOLA_RELEASE_VERSION}"; \ + fi && \ + curl -sL --fail --output zola.tar.gz https://github.com/getzola/zola/releases/download/${ZOLA_VERSION}/zola-${ZOLA_VERSION}-$(uname -m)-unknown-linux-gnu.tar.gz && \ + tar -xzvf zola.tar.gz zola; \ + else \ + cargo build --release && \ + cp target/$(uname -m)-unknown-linux-gnu/release/zola zola; \ + fi && ./zola --version FROM gcr.io/distroless/cc-debian12 -COPY --from=builder /app/target/x86_64-unknown-linux-gnu/release/zola /bin/zola +COPY --from=builder /app/zola /bin/zola ENTRYPOINT [ "/bin/zola" ] diff --git a/components/config/src/config/markup.rs b/components/config/src/config/markup.rs index 580a665ae6..9ea5b2337b 100644 --- a/components/config/src/config/markup.rs +++ b/components/config/src/config/markup.rs @@ -27,6 +27,8 @@ pub struct ThemeCss { pub struct Markdown { /// Whether to highlight all code blocks found in markdown files. Defaults to false pub highlight_code: bool, + /// Emit an error for missing highlight languages. Defaults to false + pub error_on_missing_highlight: bool, /// Which themes to use for code highlighting. See Readme for supported themes /// Defaults to "base16-ocean-dark" pub highlight_theme: String, @@ -198,6 +200,7 @@ impl Default for Markdown { fn default() -> Markdown { Markdown { highlight_code: false, + error_on_missing_highlight: false, highlight_theme: DEFAULT_HIGHLIGHT_THEME.to_owned(), highlight_themes_css: Vec::new(), render_emoji: false, diff --git a/components/config/src/config/mod.rs b/components/config/src/config/mod.rs index 88d949fc21..a1f1cfaf41 100644 --- a/components/config/src/config/mod.rs +++ b/components/config/src/config/mod.rs @@ -29,6 +29,13 @@ pub enum Mode { Check, } +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ExcludePaginatedPagesInSitemap { + None, + All, +} + #[derive(Clone, Debug, Deserialize)] #[serde(default, deny_unknown_fields)] pub struct Config { @@ -102,6 +109,8 @@ pub struct Config { pub generate_sitemap: bool, /// Enables the generation of robots.txt pub generate_robots_txt: bool, + /// Whether to exclude paginated pages in sitemap; can take values "none", "all" + pub exclude_paginated_pages_in_sitemap: ExcludePaginatedPagesInSitemap, } #[derive(Serialize)] @@ -123,6 +132,7 @@ pub struct SerializedConfig<'a> { search: search::SerializedSearch<'a>, generate_sitemap: bool, generate_robots_txt: bool, + exclude_paginated_pages_in_sitemap: ExcludePaginatedPagesInSitemap, } impl Config { @@ -287,6 +297,10 @@ impl Config { self.mode == Mode::Check } + pub fn should_exclude_paginated_pages_in_sitemap(&self) -> bool { + self.exclude_paginated_pages_in_sitemap == ExcludePaginatedPagesInSitemap::All + } + pub fn enable_serve_mode(&mut self) { self.mode = Mode::Serve; } @@ -340,6 +354,7 @@ impl Config { search: self.search.serialize(), generate_sitemap: self.generate_sitemap, generate_robots_txt: self.generate_robots_txt, + exclude_paginated_pages_in_sitemap: self.exclude_paginated_pages_in_sitemap, } } } @@ -405,6 +420,7 @@ impl Default for Config { extra: HashMap::new(), generate_sitemap: true, generate_robots_txt: true, + exclude_paginated_pages_in_sitemap: ExcludePaginatedPagesInSitemap::None, } } } @@ -1066,4 +1082,6 @@ base_url = "example.com" let config = Config::parse(config).unwrap(); assert!(config.generate_robots_txt); } + + // TODO: add a test for excluding paginated pages } diff --git a/components/config/src/highlighting.rs b/components/config/src/highlighting.rs index 03cfc95d19..ff50d2258b 100644 --- a/components/config/src/highlighting.rs +++ b/components/config/src/highlighting.rs @@ -9,10 +9,10 @@ use crate::config::Config; pub const CLASS_STYLE: ClassStyle = ClassStyle::SpacedPrefixed { prefix: "z-" }; pub static SYNTAX_SET: Lazy = - Lazy::new(|| from_binary(include_bytes!("../../../sublime/syntaxes/newlines.packdump"))); + Lazy::new(|| from_binary(include_bytes!("../sublime/syntaxes/newlines.packdump"))); pub static THEME_SET: Lazy = - Lazy::new(|| from_binary(include_bytes!("../../../sublime/themes/all.themedump"))); + Lazy::new(|| from_binary(include_bytes!("../sublime/themes/all.themedump"))); #[derive(Clone, Debug, PartialEq, Eq)] pub enum HighlightSource { diff --git a/sublime/syntaxes/Packages b/components/config/sublime/syntaxes/Packages similarity index 100% rename from sublime/syntaxes/Packages rename to components/config/sublime/syntaxes/Packages diff --git a/sublime/syntaxes/extra/Assembly x86.sublime-syntax b/components/config/sublime/syntaxes/extra/Assembly x86.sublime-syntax similarity index 100% rename from sublime/syntaxes/extra/Assembly x86.sublime-syntax rename to components/config/sublime/syntaxes/extra/Assembly x86.sublime-syntax diff --git a/sublime/syntaxes/extra/CSV.sublime-syntax b/components/config/sublime/syntaxes/extra/CSV.sublime-syntax similarity index 100% rename from sublime/syntaxes/extra/CSV.sublime-syntax rename to components/config/sublime/syntaxes/extra/CSV.sublime-syntax diff --git a/sublime/syntaxes/extra/Crystal.sublime-syntax b/components/config/sublime/syntaxes/extra/Crystal.sublime-syntax similarity index 100% rename from sublime/syntaxes/extra/Crystal.sublime-syntax rename to components/config/sublime/syntaxes/extra/Crystal.sublime-syntax diff --git a/sublime/syntaxes/extra/Dart.sublime-syntax b/components/config/sublime/syntaxes/extra/Dart.sublime-syntax similarity index 100% rename from sublime/syntaxes/extra/Dart.sublime-syntax rename to components/config/sublime/syntaxes/extra/Dart.sublime-syntax diff --git a/sublime/syntaxes/extra/Docker.tmbundle b/components/config/sublime/syntaxes/extra/Docker.tmbundle similarity index 100% rename from sublime/syntaxes/extra/Docker.tmbundle rename to components/config/sublime/syntaxes/extra/Docker.tmbundle diff --git a/sublime/syntaxes/extra/GDScript-sublime b/components/config/sublime/syntaxes/extra/GDScript-sublime similarity index 100% rename from sublime/syntaxes/extra/GDScript-sublime rename to components/config/sublime/syntaxes/extra/GDScript-sublime diff --git a/sublime/syntaxes/extra/GraphQL-SublimeText3 b/components/config/sublime/syntaxes/extra/GraphQL-SublimeText3 similarity index 100% rename from sublime/syntaxes/extra/GraphQL-SublimeText3 rename to components/config/sublime/syntaxes/extra/GraphQL-SublimeText3 diff --git a/sublime/syntaxes/extra/Handlebars.sublime-syntax b/components/config/sublime/syntaxes/extra/Handlebars.sublime-syntax similarity index 100% rename from sublime/syntaxes/extra/Handlebars.sublime-syntax rename to components/config/sublime/syntaxes/extra/Handlebars.sublime-syntax diff --git a/sublime/syntaxes/extra/Julia-sublime b/components/config/sublime/syntaxes/extra/Julia-sublime similarity index 100% rename from sublime/syntaxes/extra/Julia-sublime rename to components/config/sublime/syntaxes/extra/Julia-sublime diff --git a/sublime/syntaxes/extra/Kotlin.sublime-syntax b/components/config/sublime/syntaxes/extra/Kotlin.sublime-syntax similarity index 100% rename from sublime/syntaxes/extra/Kotlin.sublime-syntax rename to components/config/sublime/syntaxes/extra/Kotlin.sublime-syntax diff --git a/sublime/syntaxes/extra/LESS-sublime b/components/config/sublime/syntaxes/extra/LESS-sublime similarity index 100% rename from sublime/syntaxes/extra/LESS-sublime rename to components/config/sublime/syntaxes/extra/LESS-sublime diff --git a/sublime/syntaxes/extra/MZN.sublime-syntax b/components/config/sublime/syntaxes/extra/MZN.sublime-syntax similarity index 100% rename from sublime/syntaxes/extra/MZN.sublime-syntax rename to components/config/sublime/syntaxes/extra/MZN.sublime-syntax diff --git a/sublime/syntaxes/extra/Nim.sublime-syntax b/components/config/sublime/syntaxes/extra/Nim.sublime-syntax similarity index 100% rename from sublime/syntaxes/extra/Nim.sublime-syntax rename to components/config/sublime/syntaxes/extra/Nim.sublime-syntax diff --git a/sublime/syntaxes/extra/Nix.sublime-syntax b/components/config/sublime/syntaxes/extra/Nix.sublime-syntax similarity index 100% rename from sublime/syntaxes/extra/Nix.sublime-syntax rename to components/config/sublime/syntaxes/extra/Nix.sublime-syntax diff --git a/sublime/syntaxes/extra/PowerShell.sublime-syntax b/components/config/sublime/syntaxes/extra/PowerShell.sublime-syntax similarity index 100% rename from sublime/syntaxes/extra/PowerShell.sublime-syntax rename to components/config/sublime/syntaxes/extra/PowerShell.sublime-syntax diff --git a/sublime/syntaxes/extra/Prolog.sublime-syntax b/components/config/sublime/syntaxes/extra/Prolog.sublime-syntax similarity index 100% rename from sublime/syntaxes/extra/Prolog.sublime-syntax rename to components/config/sublime/syntaxes/extra/Prolog.sublime-syntax diff --git a/sublime/syntaxes/extra/README.md b/components/config/sublime/syntaxes/extra/README.md similarity index 100% rename from sublime/syntaxes/extra/README.md rename to components/config/sublime/syntaxes/extra/README.md diff --git a/sublime/syntaxes/extra/Racket.sublime-syntax b/components/config/sublime/syntaxes/extra/Racket.sublime-syntax similarity index 100% rename from sublime/syntaxes/extra/Racket.sublime-syntax rename to components/config/sublime/syntaxes/extra/Racket.sublime-syntax diff --git a/sublime/syntaxes/extra/Reason.sublime-syntax b/components/config/sublime/syntaxes/extra/Reason.sublime-syntax similarity index 100% rename from sublime/syntaxes/extra/Reason.sublime-syntax rename to components/config/sublime/syntaxes/extra/Reason.sublime-syntax diff --git a/sublime/syntaxes/extra/Stylus.sublime-syntax b/components/config/sublime/syntaxes/extra/Stylus.sublime-syntax similarity index 100% rename from sublime/syntaxes/extra/Stylus.sublime-syntax rename to components/config/sublime/syntaxes/extra/Stylus.sublime-syntax diff --git a/sublime/syntaxes/extra/Sublime-CMakeLists b/components/config/sublime/syntaxes/extra/Sublime-CMakeLists similarity index 100% rename from sublime/syntaxes/extra/Sublime-CMakeLists rename to components/config/sublime/syntaxes/extra/Sublime-CMakeLists diff --git a/sublime/syntaxes/extra/Sublime-GenericConfig b/components/config/sublime/syntaxes/extra/Sublime-GenericConfig similarity index 100% rename from sublime/syntaxes/extra/Sublime-GenericConfig rename to components/config/sublime/syntaxes/extra/Sublime-GenericConfig diff --git a/sublime/syntaxes/extra/SublimeElmLanguageSupport b/components/config/sublime/syntaxes/extra/SublimeElmLanguageSupport similarity index 100% rename from sublime/syntaxes/extra/SublimeElmLanguageSupport rename to components/config/sublime/syntaxes/extra/SublimeElmLanguageSupport diff --git a/sublime/syntaxes/extra/SublimeFortran b/components/config/sublime/syntaxes/extra/SublimeFortran similarity index 100% rename from sublime/syntaxes/extra/SublimeFortran rename to components/config/sublime/syntaxes/extra/SublimeFortran diff --git a/sublime/syntaxes/extra/SublimeSass b/components/config/sublime/syntaxes/extra/SublimeSass similarity index 100% rename from sublime/syntaxes/extra/SublimeSass rename to components/config/sublime/syntaxes/extra/SublimeSass diff --git a/sublime/syntaxes/extra/SublimeTextLinkerSyntax b/components/config/sublime/syntaxes/extra/SublimeTextLinkerSyntax similarity index 100% rename from sublime/syntaxes/extra/SublimeTextLinkerSyntax rename to components/config/sublime/syntaxes/extra/SublimeTextLinkerSyntax diff --git a/sublime/syntaxes/extra/Swift.sublime-syntax b/components/config/sublime/syntaxes/extra/Swift.sublime-syntax similarity index 100% rename from sublime/syntaxes/extra/Swift.sublime-syntax rename to components/config/sublime/syntaxes/extra/Swift.sublime-syntax diff --git a/sublime/syntaxes/extra/TypeScript.sublime-syntax b/components/config/sublime/syntaxes/extra/TypeScript.sublime-syntax similarity index 100% rename from sublime/syntaxes/extra/TypeScript.sublime-syntax rename to components/config/sublime/syntaxes/extra/TypeScript.sublime-syntax diff --git a/sublime/syntaxes/extra/TypeScriptReact.sublime-syntax b/components/config/sublime/syntaxes/extra/TypeScriptReact.sublime-syntax similarity index 100% rename from sublime/syntaxes/extra/TypeScriptReact.sublime-syntax rename to components/config/sublime/syntaxes/extra/TypeScriptReact.sublime-syntax diff --git a/sublime/syntaxes/extra/VimL.sublime-syntax b/components/config/sublime/syntaxes/extra/VimL.sublime-syntax similarity index 100% rename from sublime/syntaxes/extra/VimL.sublime-syntax rename to components/config/sublime/syntaxes/extra/VimL.sublime-syntax diff --git a/sublime/syntaxes/extra/awk-sublime b/components/config/sublime/syntaxes/extra/awk-sublime similarity index 100% rename from sublime/syntaxes/extra/awk-sublime rename to components/config/sublime/syntaxes/extra/awk-sublime diff --git a/sublime/syntaxes/extra/elixir-sublime-syntax b/components/config/sublime/syntaxes/extra/elixir-sublime-syntax similarity index 100% rename from sublime/syntaxes/extra/elixir-sublime-syntax rename to components/config/sublime/syntaxes/extra/elixir-sublime-syntax diff --git a/sublime/syntaxes/extra/lrc.sublime-syntax b/components/config/sublime/syntaxes/extra/lrc.sublime-syntax similarity index 100% rename from sublime/syntaxes/extra/lrc.sublime-syntax rename to components/config/sublime/syntaxes/extra/lrc.sublime-syntax diff --git a/sublime/syntaxes/extra/protobuf-syntax-highlighting b/components/config/sublime/syntaxes/extra/protobuf-syntax-highlighting similarity index 100% rename from sublime/syntaxes/extra/protobuf-syntax-highlighting rename to components/config/sublime/syntaxes/extra/protobuf-syntax-highlighting diff --git a/sublime/syntaxes/extra/srt.sublime-syntax b/components/config/sublime/syntaxes/extra/srt.sublime-syntax similarity index 100% rename from sublime/syntaxes/extra/srt.sublime-syntax rename to components/config/sublime/syntaxes/extra/srt.sublime-syntax diff --git a/sublime/syntaxes/extra/sublime-clojure b/components/config/sublime/syntaxes/extra/sublime-clojure similarity index 100% rename from sublime/syntaxes/extra/sublime-clojure rename to components/config/sublime/syntaxes/extra/sublime-clojure diff --git a/sublime/syntaxes/extra/sublime-fish b/components/config/sublime/syntaxes/extra/sublime-fish similarity index 100% rename from sublime/syntaxes/extra/sublime-fish rename to components/config/sublime/syntaxes/extra/sublime-fish diff --git a/sublime/syntaxes/extra/sublime-glsl b/components/config/sublime/syntaxes/extra/sublime-glsl similarity index 100% rename from sublime/syntaxes/extra/sublime-glsl rename to components/config/sublime/syntaxes/extra/sublime-glsl diff --git a/sublime/syntaxes/extra/sublime-jinja2 b/components/config/sublime/syntaxes/extra/sublime-jinja2 similarity index 100% rename from sublime/syntaxes/extra/sublime-jinja2 rename to components/config/sublime/syntaxes/extra/sublime-jinja2 diff --git a/sublime/syntaxes/extra/sublime-purescript-syntax b/components/config/sublime/syntaxes/extra/sublime-purescript-syntax similarity index 100% rename from sublime/syntaxes/extra/sublime-purescript-syntax rename to components/config/sublime/syntaxes/extra/sublime-purescript-syntax diff --git a/sublime/syntaxes/extra/sublime-zig-language b/components/config/sublime/syntaxes/extra/sublime-zig-language similarity index 100% rename from sublime/syntaxes/extra/sublime-zig-language rename to components/config/sublime/syntaxes/extra/sublime-zig-language diff --git a/sublime/syntaxes/extra/sublime_toml_highlighting b/components/config/sublime/syntaxes/extra/sublime_toml_highlighting similarity index 100% rename from sublime/syntaxes/extra/sublime_toml_highlighting rename to components/config/sublime/syntaxes/extra/sublime_toml_highlighting diff --git a/sublime/syntaxes/extra/sublimetext-fsharp b/components/config/sublime/syntaxes/extra/sublimetext-fsharp similarity index 100% rename from sublime/syntaxes/extra/sublimetext-fsharp rename to components/config/sublime/syntaxes/extra/sublimetext-fsharp diff --git a/sublime/syntaxes/extra/vue-syntax-highlight b/components/config/sublime/syntaxes/extra/vue-syntax-highlight similarity index 100% rename from sublime/syntaxes/extra/vue-syntax-highlight rename to components/config/sublime/syntaxes/extra/vue-syntax-highlight diff --git a/sublime/syntaxes/newlines.packdump b/components/config/sublime/syntaxes/newlines.packdump similarity index 100% rename from sublime/syntaxes/newlines.packdump rename to components/config/sublime/syntaxes/newlines.packdump diff --git a/sublime/themes/1337.tmTheme b/components/config/sublime/themes/1337.tmTheme similarity index 100% rename from sublime/themes/1337.tmTheme rename to components/config/sublime/themes/1337.tmTheme diff --git a/sublime/themes/OneHalfDark.tmTheme b/components/config/sublime/themes/OneHalfDark.tmTheme similarity index 100% rename from sublime/themes/OneHalfDark.tmTheme rename to components/config/sublime/themes/OneHalfDark.tmTheme diff --git a/sublime/themes/OneHalfLight.tmTheme b/components/config/sublime/themes/OneHalfLight.tmTheme similarity index 100% rename from sublime/themes/OneHalfLight.tmTheme rename to components/config/sublime/themes/OneHalfLight.tmTheme diff --git a/sublime/themes/Tomorrow.tmTheme b/components/config/sublime/themes/Tomorrow.tmTheme similarity index 100% rename from sublime/themes/Tomorrow.tmTheme rename to components/config/sublime/themes/Tomorrow.tmTheme diff --git a/sublime/themes/agola-dark.tmTheme b/components/config/sublime/themes/agola-dark.tmTheme similarity index 100% rename from sublime/themes/agola-dark.tmTheme rename to components/config/sublime/themes/agola-dark.tmTheme diff --git a/sublime/themes/all.themedump b/components/config/sublime/themes/all.themedump similarity index 100% rename from sublime/themes/all.themedump rename to components/config/sublime/themes/all.themedump diff --git a/sublime/themes/ascetic-white.tmTheme b/components/config/sublime/themes/ascetic-white.tmTheme similarity index 100% rename from sublime/themes/ascetic-white.tmTheme rename to components/config/sublime/themes/ascetic-white.tmTheme diff --git a/sublime/themes/axar.tmTheme b/components/config/sublime/themes/axar.tmTheme similarity index 100% rename from sublime/themes/axar.tmTheme rename to components/config/sublime/themes/axar.tmTheme diff --git a/sublime/themes/ayu-dark.tmTheme b/components/config/sublime/themes/ayu-dark.tmTheme similarity index 100% rename from sublime/themes/ayu-dark.tmTheme rename to components/config/sublime/themes/ayu-dark.tmTheme diff --git a/sublime/themes/ayu-light.tmTheme b/components/config/sublime/themes/ayu-light.tmTheme similarity index 100% rename from sublime/themes/ayu-light.tmTheme rename to components/config/sublime/themes/ayu-light.tmTheme diff --git a/sublime/themes/ayu-mirage.tmTheme b/components/config/sublime/themes/ayu-mirage.tmTheme similarity index 100% rename from sublime/themes/ayu-mirage.tmTheme rename to components/config/sublime/themes/ayu-mirage.tmTheme diff --git a/sublime/themes/base16-atelierdune-light.tmTheme b/components/config/sublime/themes/base16-atelierdune-light.tmTheme similarity index 100% rename from sublime/themes/base16-atelierdune-light.tmTheme rename to components/config/sublime/themes/base16-atelierdune-light.tmTheme diff --git a/sublime/themes/base16-ocean-dark.tmTheme b/components/config/sublime/themes/base16-ocean-dark.tmTheme similarity index 100% rename from sublime/themes/base16-ocean-dark.tmTheme rename to components/config/sublime/themes/base16-ocean-dark.tmTheme diff --git a/sublime/themes/base16-ocean-light.tmTheme b/components/config/sublime/themes/base16-ocean-light.tmTheme similarity index 100% rename from sublime/themes/base16-ocean-light.tmTheme rename to components/config/sublime/themes/base16-ocean-light.tmTheme diff --git a/sublime/themes/bbedit.tmTheme b/components/config/sublime/themes/bbedit.tmTheme similarity index 100% rename from sublime/themes/bbedit.tmTheme rename to components/config/sublime/themes/bbedit.tmTheme diff --git a/sublime/themes/boron.tmTheme b/components/config/sublime/themes/boron.tmTheme similarity index 100% rename from sublime/themes/boron.tmTheme rename to components/config/sublime/themes/boron.tmTheme diff --git a/sublime/themes/charcoal.tmTheme b/components/config/sublime/themes/charcoal.tmTheme similarity index 100% rename from sublime/themes/charcoal.tmTheme rename to components/config/sublime/themes/charcoal.tmTheme diff --git a/sublime/themes/cheerfully-light.tmTheme b/components/config/sublime/themes/cheerfully-light.tmTheme similarity index 100% rename from sublime/themes/cheerfully-light.tmTheme rename to components/config/sublime/themes/cheerfully-light.tmTheme diff --git a/sublime/themes/classic-modified.tmTheme b/components/config/sublime/themes/classic-modified.tmTheme similarity index 100% rename from sublime/themes/classic-modified.tmTheme rename to components/config/sublime/themes/classic-modified.tmTheme diff --git a/sublime/themes/demain.tmTheme b/components/config/sublime/themes/demain.tmTheme similarity index 100% rename from sublime/themes/demain.tmTheme rename to components/config/sublime/themes/demain.tmTheme diff --git a/sublime/themes/dimmed-fluid.tmTheme b/components/config/sublime/themes/dimmed-fluid.tmTheme similarity index 100% rename from sublime/themes/dimmed-fluid.tmTheme rename to components/config/sublime/themes/dimmed-fluid.tmTheme diff --git a/sublime/themes/dracula.tmTheme b/components/config/sublime/themes/dracula.tmTheme similarity index 100% rename from sublime/themes/dracula.tmTheme rename to components/config/sublime/themes/dracula.tmTheme diff --git a/sublime/themes/gray-matter-dark.tmTheme b/components/config/sublime/themes/gray-matter-dark.tmTheme similarity index 100% rename from sublime/themes/gray-matter-dark.tmTheme rename to components/config/sublime/themes/gray-matter-dark.tmTheme diff --git a/sublime/themes/green.tmTheme b/components/config/sublime/themes/green.tmTheme similarity index 100% rename from sublime/themes/green.tmTheme rename to components/config/sublime/themes/green.tmTheme diff --git a/sublime/themes/gruvbox-dark.tmTheme b/components/config/sublime/themes/gruvbox-dark.tmTheme similarity index 100% rename from sublime/themes/gruvbox-dark.tmTheme rename to components/config/sublime/themes/gruvbox-dark.tmTheme diff --git a/sublime/themes/gruvbox-light.tmTheme b/components/config/sublime/themes/gruvbox-light.tmTheme similarity index 100% rename from sublime/themes/gruvbox-light.tmTheme rename to components/config/sublime/themes/gruvbox-light.tmTheme diff --git a/sublime/themes/idle.tmTheme b/components/config/sublime/themes/idle.tmTheme similarity index 100% rename from sublime/themes/idle.tmTheme rename to components/config/sublime/themes/idle.tmTheme diff --git a/sublime/themes/inspired-github.tmTheme b/components/config/sublime/themes/inspired-github.tmTheme similarity index 100% rename from sublime/themes/inspired-github.tmTheme rename to components/config/sublime/themes/inspired-github.tmTheme diff --git a/sublime/themes/ir-white.tmTheme b/components/config/sublime/themes/ir-white.tmTheme similarity index 100% rename from sublime/themes/ir-white.tmTheme rename to components/config/sublime/themes/ir-white.tmTheme diff --git a/sublime/themes/kronuz.tmTheme b/components/config/sublime/themes/kronuz.tmTheme similarity index 100% rename from sublime/themes/kronuz.tmTheme rename to components/config/sublime/themes/kronuz.tmTheme diff --git a/sublime/themes/material-dark.tmTheme b/components/config/sublime/themes/material-dark.tmTheme similarity index 100% rename from sublime/themes/material-dark.tmTheme rename to components/config/sublime/themes/material-dark.tmTheme diff --git a/sublime/themes/material-light.tmTheme b/components/config/sublime/themes/material-light.tmTheme similarity index 100% rename from sublime/themes/material-light.tmTheme rename to components/config/sublime/themes/material-light.tmTheme diff --git a/sublime/themes/monokai.tmTheme b/components/config/sublime/themes/monokai.tmTheme similarity index 100% rename from sublime/themes/monokai.tmTheme rename to components/config/sublime/themes/monokai.tmTheme diff --git a/sublime/themes/nord.tmTheme b/components/config/sublime/themes/nord.tmTheme similarity index 100% rename from sublime/themes/nord.tmTheme rename to components/config/sublime/themes/nord.tmTheme diff --git a/sublime/themes/nyx-bold.tmTheme b/components/config/sublime/themes/nyx-bold.tmTheme similarity index 100% rename from sublime/themes/nyx-bold.tmTheme rename to components/config/sublime/themes/nyx-bold.tmTheme diff --git a/sublime/themes/one-dark.tmTheme b/components/config/sublime/themes/one-dark.tmTheme similarity index 100% rename from sublime/themes/one-dark.tmTheme rename to components/config/sublime/themes/one-dark.tmTheme diff --git a/sublime/themes/railsbase16-green-screen-dark.tmTheme b/components/config/sublime/themes/railsbase16-green-screen-dark.tmTheme similarity index 100% rename from sublime/themes/railsbase16-green-screen-dark.tmTheme rename to components/config/sublime/themes/railsbase16-green-screen-dark.tmTheme diff --git a/sublime/themes/solarized-dark.tmTheme b/components/config/sublime/themes/solarized-dark.tmTheme similarity index 100% rename from sublime/themes/solarized-dark.tmTheme rename to components/config/sublime/themes/solarized-dark.tmTheme diff --git a/sublime/themes/solarized-light.tmTheme b/components/config/sublime/themes/solarized-light.tmTheme similarity index 100% rename from sublime/themes/solarized-light.tmTheme rename to components/config/sublime/themes/solarized-light.tmTheme diff --git a/sublime/themes/subway-madrid.tmTheme b/components/config/sublime/themes/subway-madrid.tmTheme similarity index 100% rename from sublime/themes/subway-madrid.tmTheme rename to components/config/sublime/themes/subway-madrid.tmTheme diff --git a/sublime/themes/subway-moscow.tmTheme b/components/config/sublime/themes/subway-moscow.tmTheme similarity index 100% rename from sublime/themes/subway-moscow.tmTheme rename to components/config/sublime/themes/subway-moscow.tmTheme diff --git a/sublime/themes/two-dark.tmTheme b/components/config/sublime/themes/two-dark.tmTheme similarity index 100% rename from sublime/themes/two-dark.tmTheme rename to components/config/sublime/themes/two-dark.tmTheme diff --git a/sublime/themes/visual-studio-dark.tmTheme b/components/config/sublime/themes/visual-studio-dark.tmTheme similarity index 100% rename from sublime/themes/visual-studio-dark.tmTheme rename to components/config/sublime/themes/visual-studio-dark.tmTheme diff --git a/sublime/themes/zenburn.tmTheme b/components/config/sublime/themes/zenburn.tmTheme similarity index 100% rename from sublime/themes/zenburn.tmTheme rename to components/config/sublime/themes/zenburn.tmTheme diff --git a/components/content/src/file_info.rs b/components/content/src/file_info.rs index a94cb4310a..fd58f21d70 100644 --- a/components/content/src/file_info.rs +++ b/components/content/src/file_info.rs @@ -109,6 +109,18 @@ impl FileInfo { format!("{}.md", name) }; let grand_parent = parent.parent().map(|p| p.to_path_buf()); + let mut colocated_path = None; + + // If we have a folder with an asset, don't consider it as a component + // Splitting on `.` as we might have a language so it isn't *only* index but also index.fr + // etc + if !components.is_empty() && name.split('.').collect::>()[0] == "_index" { + colocated_path = Some({ + let mut val = components.join("/"); + val.push('/'); + val + }); + } FileInfo { filename: file_path.file_name().unwrap().to_string_lossy().to_string(), @@ -119,7 +131,7 @@ impl FileInfo { name, components, relative, - colocated_path: None, + colocated_path: colocated_path, } } @@ -289,4 +301,41 @@ mod tests { Path::new("/home/vincent/code/site/content/posts/tutorials/python/index") ); } + + #[test] + fn correct_colocated_path() { + struct Test<'a> { + file_info: FileInfo, + expected_colocated_path: &'a str, + } + + // A colocated path: + // - MUST NOT start with a '/' + // - MUST end with a '/' + // Breaking those assumptions may have uncontrolled side effects in some other code, including but not limited to assets permalinks generation. + let tests = vec![ + Test { + file_info: FileInfo::new_page( + Path::new("/home/vincent/code/site/content/posts/tutorials/python/index.md"), + &PathBuf::new(), + ), + expected_colocated_path: "posts/tutorials/python/", + }, + Test { + file_info: FileInfo::new_section( + Path::new("/home/vincent/code/site/content/posts/tutorials/_index.fr.md"), + &PathBuf::new(), + ), + expected_colocated_path: "posts/tutorials/", + }, + ]; + + for test in tests { + assert!(test.file_info.colocated_path.is_some()); + assert_eq!( + test.file_info.colocated_path.as_ref().unwrap(), + test.expected_colocated_path + ) + } + } } diff --git a/components/content/src/page.rs b/components/content/src/page.rs index 041ab981da..71fed7153a 100644 --- a/components/content/src/page.rs +++ b/components/content/src/page.rs @@ -19,7 +19,7 @@ use crate::front_matter::{split_page_content, PageFrontMatter}; use crate::library::Library; use crate::ser::SerializingPage; use crate::utils::get_reading_analytics; -use crate::utils::{find_related_assets, has_anchor}; +use crate::utils::{find_related_assets, get_assets_permalinks, has_anchor, serialize_assets}; use utils::anchors::has_anchor_id; use utils::fs::read_file; @@ -45,6 +45,8 @@ pub struct Page { pub assets: Vec, /// All the non-md files we found next to the .md file pub serialized_assets: Vec, + /// The permalinks of all the non-md files we found next to the .md file + pub assets_permalinks: HashMap, /// The HTML rendered of the page pub content: String, /// The slug of that page. @@ -195,7 +197,20 @@ impl Page { if page.file.name == "index" { let parent_dir = path.parent().unwrap(); page.assets = find_related_assets(parent_dir, config, true); - page.serialized_assets = page.serialize_assets(base_path); + if !page.assets.is_empty() { + let colocated_path = page + .file + .colocated_path + .as_ref() + .expect("Should have colocated path for assets"); + page.serialized_assets = serialize_assets( + &page.assets, + page.file.path.parent().unwrap(), + colocated_path, + ); + page.assets_permalinks = + get_assets_permalinks(&page.serialized_assets, &page.permalink, colocated_path); + } } else { page.assets = vec![]; } @@ -255,28 +270,6 @@ impl Page { .with_context(|| format!("Failed to render page '{}'", self.file.path.display())) } - /// Creates a vectors of asset URLs. - fn serialize_assets(&self, base_path: &Path) -> Vec { - self.assets - .iter() - .filter_map(|asset| asset.strip_prefix(self.file.path.parent().unwrap()).ok()) - .filter_map(|filename| filename.to_str()) - .map(|filename| { - let mut path = self.file.path.clone(); - // Popping the index.md from the path since file.parent would be one level too high - // for our need here - path.pop(); - path.push(filename); - path = path - .strip_prefix(&base_path.join("content")) - .expect("Should be able to stripe prefix") - .to_path_buf(); - path - }) - .map(|path| format!("/{}", path.display())) - .collect() - } - pub fn has_anchor(&self, anchor: &str) -> bool { has_anchor(&self.toc, anchor) } @@ -589,8 +582,19 @@ And here's another. [^3] assert_eq!(page.file.parent, path.join("content").join("posts")); assert_eq!(page.slug, "with-assets"); assert_eq!(page.assets.len(), 3); + assert_eq!(page.serialized_assets.len(), 3); assert!(page.serialized_assets[0].starts_with('/')); assert_eq!(page.permalink, "http://a-website.com/posts/with-assets/"); + assert_eq!(page.assets_permalinks.len(), 3); + let random_assets_permalinks_key = + page.assets_permalinks.keys().next().expect("assets permalinks key should be present"); + assert!(!random_assets_permalinks_key.starts_with('/')); + let random_assets_permalinks_value = page + .assets_permalinks + .values() + .next() + .expect("assets permalinks value should be present"); + assert!(random_assets_permalinks_value.starts_with(&page.permalink)); } #[test] @@ -614,6 +618,13 @@ And here's another. [^3] assert_eq!(page.slug, "hey"); assert_eq!(page.assets.len(), 3); assert_eq!(page.permalink, "http://a-website.com/posts/hey/"); + assert_eq!(page.assets_permalinks.len(), 3); + let random_assets_permalinks_value = page + .assets_permalinks + .values() + .next() + .expect("assets permalinks value should be present"); + assert!(random_assets_permalinks_value.starts_with(&page.permalink)); } // https://github.com/getzola/zola/issues/674 @@ -640,6 +651,17 @@ And here's another. [^3] // We should not get with-assets since that's the slugified version assert!(page.serialized_assets[0].contains("with_assets")); assert_eq!(page.permalink, "http://a-website.com/posts/with-assets/"); + assert_eq!(page.assets_permalinks.len(), 3); + let random_assets_permalinks_key = + page.assets_permalinks.keys().next().expect("assets permalinks key should be present"); + // We should not get with-assets since that's the slugified version + assert!(random_assets_permalinks_key.contains("with_assets")); + let random_assets_permalinks_value = page + .assets_permalinks + .values() + .next() + .expect("assets permalinks value should be present"); + assert!(random_assets_permalinks_value.starts_with(&page.permalink)); } // https://github.com/getzola/zola/issues/607 @@ -664,7 +686,21 @@ And here's another. [^3] assert_eq!(page.slug, "with-assets"); assert_eq!(page.meta.date, Some("2013-06-02".to_string())); assert_eq!(page.assets.len(), 3); + assert_eq!(page.serialized_assets.len(), 3); + // We should not get with-assets since that's the slugified version + assert!(page.serialized_assets[0].contains("2013-06-02")); assert_eq!(page.permalink, "http://a-website.com/posts/with-assets/"); + assert_eq!(page.assets_permalinks.len(), 3); + let random_assets_permalinks_key = + page.assets_permalinks.keys().next().expect("assets permalinks key should be present"); + // We should not get with-assets since that's the slugified version + assert!(random_assets_permalinks_key.contains("2013-06-02")); + let random_assets_permalinks_value = page + .assets_permalinks + .values() + .next() + .expect("assets permalinks value should be present"); + assert!(random_assets_permalinks_value.starts_with(&page.permalink)); } #[test] @@ -692,6 +728,8 @@ And here's another. [^3] let page = res.unwrap(); assert_eq!(page.assets.len(), 1); assert_eq!(page.assets[0].file_name().unwrap().to_str(), Some("graph.jpg")); + assert_eq!(page.serialized_assets.len(), 1); + assert_eq!(page.assets_permalinks.len(), 1); } // https://github.com/getzola/zola/issues/1566 diff --git a/components/content/src/section.rs b/components/content/src/section.rs index d4c84a9bd3..b4ac035e03 100644 --- a/components/content/src/section.rs +++ b/components/content/src/section.rs @@ -15,7 +15,9 @@ use crate::file_info::FileInfo; use crate::front_matter::{split_section_content, SectionFrontMatter}; use crate::library::Library; use crate::ser::{SectionSerMode, SerializingSection}; -use crate::utils::{find_related_assets, get_reading_analytics, has_anchor}; +use crate::utils::{ + find_related_assets, get_assets_permalinks, get_reading_analytics, has_anchor, serialize_assets, +}; // Default is used to create a default index section if there is no _index.md in the root content directory #[derive(Clone, Debug, Default, PartialEq, Eq)] @@ -38,6 +40,8 @@ pub struct Section { pub assets: Vec, /// All the non-md files we found next to the .md file as string pub serialized_assets: Vec, + /// The permalinks of all the non-md files we found next to the .md file + pub assets_permalinks: HashMap, /// All direct pages of that section pub pages: Vec, /// All pages that cannot be sorted in this section @@ -125,7 +129,23 @@ impl Section { let parent_dir = path.parent().unwrap(); section.assets = find_related_assets(parent_dir, config, false); - section.serialized_assets = section.serialize_assets(); + if !section.assets.is_empty() { + let colocated_path = section + .file + .colocated_path + .as_ref() + .expect("Should have colocated path for assets"); + section.serialized_assets = serialize_assets( + §ion.assets, + section.file.path.parent().unwrap(), + colocated_path, + ); + section.assets_permalinks = get_assets_permalinks( + §ion.serialized_assets, + §ion.permalink, + colocated_path, + ); + } Ok(section) } @@ -202,16 +222,6 @@ impl Section { self.file.components.is_empty() } - /// Creates a vectors of asset URLs. - fn serialize_assets(&self) -> Vec { - self.assets - .iter() - .filter_map(|asset| asset.strip_prefix(self.file.path.parent().unwrap()).ok()) - .filter_map(|filename| filename.to_str()) - .map(|filename| format!("{}{}", self.path, filename)) - .collect() - } - pub fn has_anchor(&self, anchor: &str) -> bool { has_anchor(&self.toc, anchor) } @@ -269,8 +279,22 @@ mod tests { assert!(res.is_ok()); let section = res.unwrap(); assert_eq!(section.assets.len(), 3); + assert_eq!(section.serialized_assets.len(), 3); assert!(section.serialized_assets[0].starts_with('/')); assert_eq!(section.permalink, "http://a-website.com/posts/with-assets/"); + assert_eq!(section.assets_permalinks.len(), 3); + let random_assets_permalinks_key = section + .assets_permalinks + .keys() + .next() + .expect("assets permalinks key should be present"); + assert!(!random_assets_permalinks_key.starts_with('/')); + let random_assets_permalinks_value = section + .assets_permalinks + .values() + .next() + .expect("assets permalinks value should be present"); + assert!(random_assets_permalinks_value.starts_with(§ion.permalink)); } #[test] @@ -300,9 +324,11 @@ mod tests { Section::from_file(article_path.join("_index.md").as_path(), &config, &PathBuf::new()); assert!(res.is_ok()); - let page = res.unwrap(); - assert_eq!(page.assets.len(), 1); - assert_eq!(page.assets[0].file_name().unwrap().to_str(), Some("graph.jpg")); + let section = res.unwrap(); + assert_eq!(section.assets.len(), 1); + assert_eq!(section.assets[0].file_name().unwrap().to_str(), Some("graph.jpg")); + assert_eq!(section.serialized_assets.len(), 1); + assert_eq!(section.assets_permalinks.len(), 1); } #[test] diff --git a/components/content/src/ser.rs b/components/content/src/ser.rs index 919d0d1bb6..d77a43dd16 100644 --- a/components/content/src/ser.rs +++ b/components/content/src/ser.rs @@ -162,6 +162,8 @@ pub struct SerializingSection<'a> { backlinks: Vec>, generate_feeds: bool, transparent: bool, + paginate_by: &'a Option, + paginate_reversed: bool, } #[derive(Debug)] @@ -226,6 +228,8 @@ impl<'a> SerializingSection<'a> { subsections, translations, backlinks, + paginate_by: §ion.meta.paginate_by, + paginate_reversed: section.meta.paginate_reversed, } } } diff --git a/components/content/src/utils.rs b/components/content/src/utils.rs index e269988624..9952f259b3 100644 --- a/components/content/src/utils.rs +++ b/components/content/src/utils.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::path::{Path, PathBuf}; use libs::unicode_segmentation::UnicodeSegmentation; @@ -58,6 +59,47 @@ pub fn find_related_assets(path: &Path, config: &Config, recursive: bool) -> Vec assets } +/// Serializes assets source path for assets colocated with a section or a page +pub fn serialize_assets( + assets: &Vec, + parent_path: &Path, + colocated_path: &str, +) -> Vec { + assets + .iter() + .filter_map(|asset| asset.strip_prefix(parent_path).ok()) + .map(|asset_relative_path| { + asset_relative_path + .components() + .map(|component| component.as_os_str().to_string_lossy().to_string()) + .collect::>() + .join("/") + }) + .map(|asset_relative_path_as_string| { + format!("/{}{}", colocated_path, asset_relative_path_as_string) + }) + .collect() +} + +/// Create assets permalinks based on the permalin of the section or the page they are colocated with +pub fn get_assets_permalinks( + serialized_assets: &Vec, + parent_permalink: &str, + colocated_path: &str, +) -> HashMap { + serialized_assets + .iter() + .map(|asset| asset.strip_prefix("/").unwrap_or(asset)) + .map(|asset_file_path| { + let page_relative_asset_path = asset_file_path + .strip_prefix(colocated_path) + .expect("Should be able to stripe colocated path from asset path"); + let asset_permalink = format!("{}{}", parent_permalink, page_relative_asset_path); + (asset_file_path.to_string(), asset_permalink.to_string()) + }) + .collect() +} + /// Get word count and estimated reading time pub fn get_reading_analytics(content: &str) -> (usize, usize) { // code fences "toggle" the state from non-code to code and back, so anything inbetween the @@ -149,6 +191,96 @@ mod tests { ); } } + + #[test] + fn can_serialize_assets() { + let parent_path = Path::new("/tmp/test"); + let page_folder_path = parent_path.join("content").join("posts").join("my-article"); + let assets = vec![ + page_folder_path.join("example.js"), + page_folder_path.join("graph.jpg"), + page_folder_path.join("fail.png"), + page_folder_path.join("extensionless"), + page_folder_path.join("subdir").join("example.js"), + page_folder_path.join("FFF.txt"), + page_folder_path.join("GRAPH.txt"), + page_folder_path.join("subdir").join("GGG.txt"), + ]; + let colocated_path = "posts/my-article/".to_string(); + let expected_serialized_assets = vec![ + "/posts/my-article/example.js", + "/posts/my-article/graph.jpg", + "/posts/my-article/fail.png", + "/posts/my-article/extensionless", + "/posts/my-article/subdir/example.js", + "/posts/my-article/FFF.txt", + "/posts/my-article/GRAPH.txt", + "/posts/my-article/subdir/GGG.txt", + ]; + + let serialized_assets = serialize_assets(&assets, &page_folder_path, &colocated_path); + + assert_eq!( + serialized_assets, expected_serialized_assets, + "Serialized assets (left) are different from expected (right)", + ); + } + + #[test] + fn can_get_assets_permalinks() { + let serialized_assets = vec![ + "/posts/my-article/example.js".to_string(), + "/posts/my-article/graph.jpg".to_string(), + "/posts/my-article/fail.png".to_string(), + "/posts/my-article/extensionless".to_string(), + "/posts/my-article/subdir/example.js".to_string(), + "/posts/my-article/FFF.txt".to_string(), + "/posts/my-article/GRAPH.txt".to_string(), + "/posts/my-article/subdir/GGG.txt".to_string(), + ]; + let parent_permalink = "https://remplace-par-ton-url.fr/posts/my-super-article/"; + let colocated_path = "posts/my-article/".to_string(); + let expected_assets_permalinks: HashMap<_, _> = HashMap::from_iter([ + ( + "posts/my-article/example.js".to_string(), + format!("{}{}", parent_permalink, "example.js"), + ), + ( + "posts/my-article/graph.jpg".to_string(), + format!("{}{}", parent_permalink, "graph.jpg"), + ), + ( + "posts/my-article/fail.png".to_string(), + format!("{}{}", parent_permalink, "fail.png"), + ), + ( + "posts/my-article/extensionless".to_string(), + format!("{}{}", parent_permalink, "extensionless"), + ), + ( + "posts/my-article/subdir/example.js".to_string(), + format!("{}{}", parent_permalink, "subdir/example.js"), + ), + ("posts/my-article/FFF.txt".to_string(), format!("{}{}", parent_permalink, "FFF.txt")), + ( + "posts/my-article/GRAPH.txt".to_string(), + format!("{}{}", parent_permalink, "GRAPH.txt"), + ), + ( + "posts/my-article/subdir/GGG.txt".to_string(), + format!("{}{}", parent_permalink, "subdir/GGG.txt"), + ), + ]); + + let assets_permalinks = + get_assets_permalinks(&serialized_assets, &parent_permalink, &colocated_path); + + assert_eq!( + assets_permalinks, expected_assets_permalinks, + "Assets permalinks (left) are different from expected (right)", + ); + } + #[test] fn can_find_anchor_at_root() { let input = vec![ diff --git a/components/markdown/src/codeblock/mod.rs b/components/markdown/src/codeblock/mod.rs index 5e297926ff..fd0c1d93f6 100644 --- a/components/markdown/src/codeblock/mod.rs +++ b/components/markdown/src/codeblock/mod.rs @@ -3,6 +3,7 @@ mod highlight; use std::ops::RangeInclusive; +use errors::{bail, Result}; use libs::syntect::util::LinesWithEndings; use crate::codeblock::highlight::SyntaxHighlighter; @@ -75,14 +76,19 @@ impl<'config> CodeBlock<'config> { config: &'config Config, // path to the current file if there is one, to point where the error is path: Option<&'config str>, - ) -> (Self, String) { + ) -> Result<(Self, String)> { let syntax_and_theme = resolve_syntax_and_theme(fence.language, config); if syntax_and_theme.source == HighlightSource::NotFound && config.markdown.highlight_code { let lang = fence.language.unwrap(); - if let Some(p) = path { - eprintln!("Warning: Highlight language {} not found in {}", lang, p); + let msg = if let Some(p) = path { + format!("Highlight language {} not found in {}", lang, p) } else { - eprintln!("Warning: Highlight language {} not found", lang); + format!("Highlight language {} not found", lang) + }; + if config.markdown.error_on_missing_highlight { + bail!(msg); + } else { + eprintln!("Warning: {}", msg); } } let highlighter = SyntaxHighlighter::new(config.markdown.highlight_code, syntax_and_theme); @@ -93,7 +99,7 @@ impl<'config> CodeBlock<'config> { highlighter.pre_class(), fence.line_numbers, ); - ( + Ok(( Self { highlighter, line_numbers: fence.line_numbers, @@ -102,7 +108,7 @@ impl<'config> CodeBlock<'config> { hide_lines: fence.hide_lines, }, html_start, - ) + )) } pub fn highlight(&mut self, content: &str) -> String { diff --git a/components/markdown/src/markdown.rs b/components/markdown/src/markdown.rs index 8bdc778e22..bcbca47999 100644 --- a/components/markdown/src/markdown.rs +++ b/components/markdown/src/markdown.rs @@ -277,7 +277,7 @@ fn convert_footnotes_to_github_style(old_events: &mut Vec) { // nr is a number of references to this footnote let (n, nr) = footnote_numbers.entry(name.clone()).or_insert((n, 0usize)); *nr += 1; - let reference = Event::Html(format!(r##"[{n}]"##).into()); + let reference = Event::Html(format!(r##"{n}"##).into()); if footnote_bodies_stack.is_empty() { // we are in the main text, just output the reference @@ -304,7 +304,8 @@ fn convert_footnotes_to_github_style(old_events: &mut Vec) { return; } - old_events.push(Event::Html("
    \n".into())); + old_events + .push(Event::Html("
    \n
      \n".into())); // Step 2: retain only footnotes which was actually referenced footnotes.retain(|f| match f.first() { @@ -341,7 +342,7 @@ fn convert_footnotes_to_github_style(old_events: &mut Vec) { // // HTML: // - //

      five [1].

      + //

      five 1.

      // //
        //
      1. @@ -394,7 +395,7 @@ fn convert_footnotes_to_github_style(old_events: &mut Vec) { }); old_events.extend(footnotes); - old_events.push(Event::Html("
      \n".into())); + old_events.push(Event::Html("
    \n
    \n".into())); } pub fn markdown_to_html( @@ -559,7 +560,13 @@ pub fn markdown_to_html( cmark::CodeBlockKind::Fenced(fence_info) => FenceSettings::new(fence_info), _ => FenceSettings::new(""), }; - let (block, begin) = CodeBlock::new(fence, context.config, path); + let (block, begin) = match CodeBlock::new(fence, context.config, path) { + Ok(cb) => cb, + Err(e) => { + error = Some(e); + break; + } + }; code_block = Some(block); events.push(Event::Html(begin.into())); } diff --git a/components/markdown/src/shortcode/mod.rs b/components/markdown/src/shortcode/mod.rs index 4239ed4d37..92ca6641df 100644 --- a/components/markdown/src/shortcode/mod.rs +++ b/components/markdown/src/shortcode/mod.rs @@ -19,7 +19,7 @@ pub fn extract_shortcodes( if let Some(def) = definitions.get(&sc.name) { sc.tera_name = def.tera_name.clone(); } else { - return Err(Error::msg(format!("Found usage of a shortcode named `{}` but we do not know about. Make sure it's not a typo and that a field name `{}.{{html,md}} exists in the `templates/shortcodes` directory.", sc.name, sc.name))); + return Err(Error::msg(format!("Found usage of a shortcode named `{}` but we do not know about. Make sure it's not a typo and that a field name `{}.{{html,md}}` exists in the `templates/shortcodes` directory.", sc.name, sc.name))); } } diff --git a/components/markdown/src/snapshots/markdown__markdown__tests__def_before_use.snap b/components/markdown/src/snapshots/markdown__markdown__tests__def_before_use.snap index 57e3a922ad..39dca0a91b 100644 --- a/components/markdown/src/snapshots/markdown__markdown__tests__def_before_use.snap +++ b/components/markdown/src/snapshots/markdown__markdown__tests__def_before_use.snap @@ -2,9 +2,11 @@ source: components/markdown/src/markdown.rs expression: html --- -

    There is footnote definition?[1]

    -
      +

      There is footnote definition?1

      +
      +
      1. It's before the reference.

      +
      diff --git a/components/markdown/src/snapshots/markdown__markdown__tests__footnote_inside_footnote.snap b/components/markdown/src/snapshots/markdown__markdown__tests__footnote_inside_footnote.snap index 6b5e28d476..2572a62c68 100644 --- a/components/markdown/src/snapshots/markdown__markdown__tests__footnote_inside_footnote.snap +++ b/components/markdown/src/snapshots/markdown__markdown__tests__footnote_inside_footnote.snap @@ -2,12 +2,14 @@ source: components/markdown/src/markdown.rs expression: html --- -

      This text has a footnote[1]

      -
        +

        This text has a footnote1

        +
        +
        1. -

          But the footnote has another footnote[2].

          +

          But the footnote has another footnote2.

        2. That's it.

        +
        diff --git a/components/markdown/src/snapshots/markdown__markdown__tests__multiple_refs.snap b/components/markdown/src/snapshots/markdown__markdown__tests__multiple_refs.snap index 1f7eaff186..bde7c95a7e 100644 --- a/components/markdown/src/snapshots/markdown__markdown__tests__multiple_refs.snap +++ b/components/markdown/src/snapshots/markdown__markdown__tests__multiple_refs.snap @@ -2,9 +2,11 @@ source: components/markdown/src/markdown.rs expression: html --- -

        This text has two[1] identical footnotes[1]

        -
          +

          This text has two1 identical footnotes1

          + diff --git a/components/markdown/src/snapshots/markdown__markdown__tests__reordered_footnotes.snap b/components/markdown/src/snapshots/markdown__markdown__tests__reordered_footnotes.snap index 865a344e5c..490f53ad03 100644 --- a/components/markdown/src/snapshots/markdown__markdown__tests__reordered_footnotes.snap +++ b/components/markdown/src/snapshots/markdown__markdown__tests__reordered_footnotes.snap @@ -2,8 +2,9 @@ source: components/markdown/src/markdown.rs expression: html --- -

          This text has two[1] footnotes[2]

          -
            +

            This text has two1 footnotes2

            +
            +
            1. But they are

            2. @@ -11,3 +12,4 @@ expression: html

              not sorted.

            +
            diff --git a/components/markdown/src/snapshots/markdown__markdown__tests__single_footnote.snap b/components/markdown/src/snapshots/markdown__markdown__tests__single_footnote.snap index 34a7f2d3e6..98761285a5 100644 --- a/components/markdown/src/snapshots/markdown__markdown__tests__single_footnote.snap +++ b/components/markdown/src/snapshots/markdown__markdown__tests__single_footnote.snap @@ -2,9 +2,11 @@ source: components/markdown/src/markdown.rs expression: html --- -

            This text has a footnote[1]

            -
              +

              This text has a footnote1

              +
              +
              1. But it is meaningless.

              +
              diff --git a/components/markdown/tests/snapshots/markdown__github_style_footnotes.snap b/components/markdown/tests/snapshots/markdown__github_style_footnotes.snap index de55e63fb7..56cf3e2944 100644 --- a/components/markdown/tests/snapshots/markdown__github_style_footnotes.snap +++ b/components/markdown/tests/snapshots/markdown__github_style_footnotes.snap @@ -2,13 +2,14 @@ source: components/markdown/tests/markdown.rs expression: body --- -

              This text has a footnote[1]

              -

              This text has two[2] footnotes[3].

              -

              There is footnote definition?[4]

              -

              This text has two[5] identical footnotes[5]

              -

              This text has a footnote[6]

              -

              Footnotes can also be referenced with identifiers[8].

              -
                +

                This text has a footnote1

                +

                This text has two2 footnotes3.

                +

                There is footnote definition?4

                +

                This text has two5 identical footnotes5

                +

                This text has a footnote6

                +

                Footnotes can also be referenced with identifiers8.

                +
                +
                1. But it is meaningless.

                2. @@ -25,7 +26,7 @@ expression: body

                  So one is present. ↩2

                3. -

                  But the footnote has another footnote[7].

                  +

                  But the footnote has another footnote7.

                4. That's it.

                  @@ -34,3 +35,4 @@ expression: body

                  Like this: [^first].

                +
                diff --git a/components/site/src/lib.rs b/components/site/src/lib.rs index cd48c219e4..bab47a0d29 100644 --- a/components/site/src/lib.rs +++ b/components/site/src/lib.rs @@ -56,9 +56,12 @@ pub struct Site { pub static_path: PathBuf, pub templates_path: PathBuf, pub taxonomies: Vec, - /// A map of all .md files (section and pages) and their permalink + /// A map of all .md files (sections and pages) and their permalink /// We need that if there are relative links in the content that need to be resolved pub permalinks: HashMap, + /// A map of all assets (non .md files colocated to sections and pages) and their permalink, distributed per lang + /// We need that if there are relative links in the content that need to be resolved + pub assets_permalinks: HashMap>, /// Contains all pages and sections of the site pub library: Arc>, /// Whether to load draft pages @@ -103,6 +106,7 @@ impl Site { templates_path, taxonomies: Vec::new(), permalinks: HashMap::new(), + assets_permalinks: HashMap::new(), include_drafts: false, // We will allocate it properly later on library: Arc::new(RwLock::new(Library::default())), @@ -477,6 +481,10 @@ impl Site { } self.permalinks.insert(page.file.relative.clone(), page.permalink.clone()); + let assets_permalinks = + self.assets_permalinks.entry(page.lang.to_string()).or_insert(HashMap::new()); + assets_permalinks.extend(page.assets_permalinks.clone().into_iter()); + if render_md { let insert_anchor = self.find_parent_section_insert_anchor(&page.file.parent, &page.lang); @@ -512,6 +520,10 @@ impl Site { /// The `render` parameter is used in the serve command with --fast, when rebuilding a page. pub fn add_section(&mut self, mut section: Section, render_md: bool) -> Result<()> { self.permalinks.insert(section.file.relative.clone(), section.permalink.clone()); + let assets_permalinks = + self.assets_permalinks.entry(section.lang.to_string()).or_insert(HashMap::new()); + assets_permalinks.extend(section.assets_permalinks.clone().into_iter()); + if render_md { section.render_markdown( &self.permalinks, diff --git a/components/site/src/sitemap.rs b/components/site/src/sitemap.rs index 2d9960741b..d6c94ecd5a 100644 --- a/components/site/src/sitemap.rs +++ b/components/site/src/sitemap.rs @@ -83,10 +83,12 @@ pub fn find_entries<'a>( } if let Some(paginate_by) = s.paginate_by() { - let number_pagers = (s.pages.len() as f64 / paginate_by as f64).ceil() as isize; - for i in 1..=number_pagers { - let permalink = format!("{}{}/{}/", s.permalink, s.meta.paginate_path, i); - entries.insert(SitemapEntry::new(Cow::Owned(permalink), &None)); + if !config.should_exclude_paginated_pages_in_sitemap() { + let number_pagers = (s.pages.len() as f64 / paginate_by as f64).ceil() as isize; + for i in 1..=number_pagers { + let permalink = format!("{}{}/{}/", s.permalink, s.meta.paginate_path, i); + entries.insert(SitemapEntry::new(Cow::Owned(permalink), &None)); + } } } } @@ -100,7 +102,7 @@ pub fn find_entries<'a>( for item in &taxonomy.items { entries.insert(SitemapEntry::new(Cow::Borrowed(&item.permalink), &None)); - if taxonomy.kind.is_paginated() { + if taxonomy.kind.is_paginated() && !config.should_exclude_paginated_pages_in_sitemap() { let number_pagers = (item.pages.len() as f64 / taxonomy.kind.paginate_by.unwrap() as f64) .ceil() as isize; diff --git a/components/site/src/tpls.rs b/components/site/src/tpls.rs index c51d0de175..09595056ca 100644 --- a/components/site/src/tpls.rs +++ b/components/site/src/tpls.rs @@ -16,6 +16,7 @@ pub fn register_early_global_fns(site: &mut Site) -> TeraResult<()> { site.base_path.clone(), site.config.clone(), site.permalinks.clone(), + site.assets_permalinks.clone(), site.output_path.clone(), ), ); diff --git a/components/templates/src/global_fns/files.rs b/components/templates/src/global_fns/files.rs index eaad1231d5..ab5d595d90 100644 --- a/components/templates/src/global_fns/files.rs +++ b/components/templates/src/global_fns/files.rs @@ -30,6 +30,7 @@ pub struct GetUrl { base_path: PathBuf, config: Config, permalinks: HashMap, + assets_permalinks: HashMap>, output_path: PathBuf, } @@ -38,15 +39,21 @@ impl GetUrl { base_path: PathBuf, config: Config, permalinks: HashMap, + assets_permalinks: HashMap>, output_path: PathBuf, ) -> Self { - Self { base_path, config, permalinks, output_path } + Self { base_path, config, permalinks, assets_permalinks, output_path } } } -fn make_path_with_lang(path: String, lang: &str, config: &Config) -> Result { +fn make_path_with_lang(path: String, lang: &str, config: &Config) -> Result<(String, bool)> { + let mut split_path: Vec = path.split('.').map(String::from).collect(); + let ilast = split_path.len() - 1; + + let is_markdown = split_path[ilast].starts_with("md"); + if lang == config.default_language { - return Ok(path); + return Ok((path, is_markdown)); } if !config.other_languages().contains_key(lang) { @@ -55,10 +62,12 @@ fn make_path_with_lang(path: String, lang: &str, config: &Config) -> Result = path.split('.').map(String::from).collect(); - let ilast = split_path.len() - 1; - split_path[ilast] = format!("{}.{}", lang, split_path[ilast]); - Ok(split_path.join(".")) + if is_markdown { + split_path[ilast] = format!("{}.{}", lang, split_path[ilast]); + Ok((split_path.join("."), is_markdown)) + } else { + Ok((path, is_markdown)) + } } impl TeraFn for GetUrl { @@ -85,12 +94,22 @@ impl TeraFn for GetUrl { // if it starts with @/, resolve it as an internal link if path.starts_with("@/") { - let path_with_lang = match make_path_with_lang(path, &lang, &self.config) { + let (path_with_lang, is_markdown) = match make_path_with_lang(path, &lang, &self.config) + { Ok(x) => x, Err(e) => return Err(e), }; - match resolve_internal_link(&path_with_lang, &self.permalinks) { + let permalinks = if is_markdown { + &self.permalinks + } else { + match self.assets_permalinks.get(&lang) { + Some(permalink) => permalink, + None => &HashMap::new(), + } + }; + + match resolve_internal_link(&path_with_lang, permalinks) { Ok(resolved) => Ok(to_value(resolved.permalink).unwrap()), Err(_) => Err(format!( "`get_url`: could not resolve URL for link `{}` not found.", @@ -282,6 +301,7 @@ title = "A title" dir.path().to_path_buf(), Config::default(), HashMap::new(), + HashMap::new(), PathBuf::new(), ); let mut args = HashMap::new(); @@ -310,6 +330,7 @@ title = "A title" dir.path().to_path_buf(), Config::default(), HashMap::new(), + HashMap::new(), PathBuf::new(), ); let mut args = HashMap::new(); @@ -325,6 +346,7 @@ title = "A title" dir.path().to_path_buf(), Config::default(), HashMap::new(), + HashMap::new(), PathBuf::new(), ); let mut args = HashMap::new(); @@ -344,6 +366,7 @@ title = "A title" dir.path().to_path_buf(), Config::default(), HashMap::new(), + HashMap::new(), PathBuf::new(), ); let mut args = HashMap::new(); @@ -363,8 +386,13 @@ title = "A title" create_file(&public.join("style.css"), "// Hello world") .expect("Failed to create file in output directory"); - let static_fn = - GetUrl::new(dir.path().to_path_buf(), Config::default(), HashMap::new(), public); + let static_fn = GetUrl::new( + dir.path().to_path_buf(), + Config::default(), + HashMap::new(), + HashMap::new(), + public, + ); let mut args = HashMap::new(); args.insert("path".to_string(), to_value("style.css").unwrap()); assert_eq!(static_fn.call(&args).unwrap(), "http://a-website.com/style.css"); @@ -374,8 +402,13 @@ title = "A title" fn error_when_language_not_available() { let config = Config::parse(CONFIG_DATA).unwrap(); let dir = create_temp_dir(); - let static_fn = - GetUrl::new(dir.path().to_path_buf(), config, HashMap::new(), PathBuf::new()); + let static_fn = GetUrl::new( + dir.path().to_path_buf(), + config, + HashMap::new(), + HashMap::new(), + PathBuf::new(), + ); let mut args = HashMap::new(); args.insert("path".to_string(), to_value("@/a_section/a_page.md").unwrap()); args.insert("lang".to_string(), to_value("it").unwrap()); @@ -403,6 +436,7 @@ title = "A title" dir.path().to_path_buf(), config.clone(), permalinks.clone(), + HashMap::new(), PathBuf::new(), ); let mut args = HashMap::new(); @@ -427,7 +461,13 @@ title = "A title" "https://remplace-par-ton-url.fr/en/a_section/a_page/".to_string(), ); let dir = create_temp_dir(); - let static_fn = GetUrl::new(dir.path().to_path_buf(), config, permalinks, PathBuf::new()); + let static_fn = GetUrl::new( + dir.path().to_path_buf(), + config, + permalinks, + HashMap::new(), + PathBuf::new(), + ); let mut args = HashMap::new(); args.insert("path".to_string(), to_value("@/a_section/a_page.md").unwrap()); args.insert("lang".to_string(), to_value("en").unwrap()); @@ -437,6 +477,135 @@ title = "A title" ); } + #[test] + fn can_get_asset_url_with_default_language() { + let config = Config::parse(CONFIG_DATA).unwrap(); + let asset_path = "a_section/an_asset.jpg"; + let mut assets_permalinks = HashMap::new(); + let mut fr_assets_permalinks = HashMap::new(); + fr_assets_permalinks.insert( + asset_path.to_string(), + "https://remplace-par-ton-url.fr/a_section/an_asset.jpg".to_string(), + ); + assets_permalinks.insert("fr".to_string(), fr_assets_permalinks); + let mut en_assets_permalinks = HashMap::new(); + en_assets_permalinks.insert( + asset_path.to_string(), + "https://remplace-par-ton-url.fr/en/a_section/an_asset.jpg".to_string(), + ); + assets_permalinks.insert("en".to_string(), en_assets_permalinks); + let dir = create_temp_dir(); + let static_fn = GetUrl::new( + dir.path().to_path_buf(), + config.clone(), + HashMap::new(), + assets_permalinks.clone(), + PathBuf::new(), + ); + let mut args = HashMap::new(); + args.insert("path".to_string(), to_value("@/a_section/an_asset.jpg").unwrap()); + args.insert("lang".to_string(), to_value("fr").unwrap()); + assert_eq!( + static_fn.call(&args).unwrap(), + "https://remplace-par-ton-url.fr/a_section/an_asset.jpg" + ); + } + + #[test] + fn can_get_asset_url_with_other_language() { + let config = Config::parse(CONFIG_DATA).unwrap(); + let asset_path = "a_section/an_asset.jpg"; + let mut assets_permalinks = HashMap::new(); + let mut fr_assets_permalinks = HashMap::new(); + fr_assets_permalinks.insert( + asset_path.to_string(), + "https://remplace-par-ton-url.fr/a_section/an_asset.jpg".to_string(), + ); + assets_permalinks.insert("fr".to_string(), fr_assets_permalinks); + let mut en_assets_permalinks = HashMap::new(); + en_assets_permalinks.insert( + asset_path.to_string(), + "https://remplace-par-ton-url.fr/en/a_section/an_asset.jpg".to_string(), + ); + assets_permalinks.insert("en".to_string(), en_assets_permalinks); + let dir = create_temp_dir(); + let static_fn = GetUrl::new( + dir.path().to_path_buf(), + config.clone(), + HashMap::new(), + assets_permalinks.clone(), + PathBuf::new(), + ); + let mut args = HashMap::new(); + args.insert("path".to_string(), to_value("@/a_section/an_asset.jpg").unwrap()); + args.insert("lang".to_string(), to_value("en").unwrap()); + assert_eq!( + static_fn.call(&args).unwrap(), + "https://remplace-par-ton-url.fr/en/a_section/an_asset.jpg" + ); + } + + #[test] + fn look_for_markdown_in_permalinks() { + let config = Config::parse(CONFIG_DATA).unwrap(); + let non_asset_path = "a_section/a_page.md"; + let mut permalinks = HashMap::new(); + permalinks.insert( + non_asset_path.to_string(), + "https://remplace-par-ton-url.fr/a_section/a_page".to_string(), + ); + let mut assets_permalinks = HashMap::new(); + let mut fr_assets_permalinks = HashMap::new(); + fr_assets_permalinks + .insert(non_asset_path.to_string(), "only assets should be there".to_string()); + assets_permalinks.insert("fr".to_string(), fr_assets_permalinks); + let dir = create_temp_dir(); + let static_fn = GetUrl::new( + dir.path().to_path_buf(), + config.clone(), + permalinks.clone(), + assets_permalinks.clone(), + PathBuf::new(), + ); + let mut args = HashMap::new(); + args.insert("path".to_string(), to_value("@/a_section/a_page.md").unwrap()); + args.insert("lang".to_string(), to_value("fr").unwrap()); + assert_eq!( + static_fn.call(&args).unwrap(), + "https://remplace-par-ton-url.fr/a_section/a_page" + ); + } + + #[test] + fn look_for_asset_in_assets_permalinks() { + let config = Config::parse(CONFIG_DATA).unwrap(); + let asset_path = "a_section/an_asset.jpg"; + let mut permalinks = HashMap::new(); + permalinks.insert(asset_path.to_string(), "only markdown should be there".to_string()); + let mut assets_permalinks = HashMap::new(); + let mut fr_assets_permalinks = HashMap::new(); + fr_assets_permalinks.insert( + asset_path.to_string(), + "https://remplace-par-ton-url.fr/a_section/an_asset.jpg".to_string(), + ); + assets_permalinks.insert("fr".to_string(), fr_assets_permalinks); + let dir = create_temp_dir(); + let static_fn = GetUrl::new( + dir.path().to_path_buf(), + config.clone(), + permalinks.clone(), + assets_permalinks.clone(), + PathBuf::new(), + ); + let mut args = HashMap::new(); + args.insert("path".to_string(), to_value("@/a_section/an_asset.jpg").unwrap()); + args.insert("lang".to_string(), to_value("fr").unwrap()); + assert_eq!( + static_fn.call(&args).unwrap(), + "https://remplace-par-ton-url.fr/a_section/an_asset.jpg" + ); + } + #[test] fn does_not_duplicate_lang() { let config = Config::parse(CONFIG_DATA).unwrap(); @@ -450,7 +619,13 @@ title = "A title" "https://remplace-par-ton-url.fr/en/a_section/a_page/".to_string(), ); let dir = create_temp_dir(); - let static_fn = GetUrl::new(dir.path().to_path_buf(), config, permalinks, PathBuf::new()); + let static_fn = GetUrl::new( + dir.path().to_path_buf(), + config, + permalinks, + HashMap::new(), + PathBuf::new(), + ); let mut args = HashMap::new(); args.insert("path".to_string(), to_value("/en/a_section/a_page/").unwrap()); args.insert("lang".to_string(), to_value("en").unwrap()); @@ -464,8 +639,13 @@ title = "A title" fn can_get_feed_urls_with_default_language() { let config = Config::parse(CONFIG_DATA).unwrap(); let dir = create_temp_dir(); - let static_fn = - GetUrl::new(dir.path().to_path_buf(), config.clone(), HashMap::new(), PathBuf::new()); + let static_fn = GetUrl::new( + dir.path().to_path_buf(), + config.clone(), + HashMap::new(), + HashMap::new(), + PathBuf::new(), + ); for feed_filename in &config.feed_filenames { let mut args = HashMap::new(); args.insert("path".to_string(), to_value(feed_filename).unwrap()); @@ -478,8 +658,13 @@ title = "A title" fn can_get_feed_urls_with_other_language() { let config = Config::parse(CONFIG_DATA).unwrap(); let dir = create_temp_dir(); - let static_fn = - GetUrl::new(dir.path().to_path_buf(), config.clone(), HashMap::new(), PathBuf::new()); + let static_fn = GetUrl::new( + dir.path().to_path_buf(), + config.clone(), + HashMap::new(), + HashMap::new(), + PathBuf::new(), + ); for feed_filename in &config.feed_filenames { let mut args = HashMap::new(); args.insert("path".to_string(), to_value(feed_filename).unwrap()); diff --git a/docs/content/documentation/content/image-processing/index.md b/docs/content/documentation/content/image-processing/index.md index c04e46d986..6534eb2d0c 100644 --- a/docs/content/documentation/content/image-processing/index.md +++ b/docs/content/documentation/content/image-processing/index.md @@ -153,7 +153,7 @@ picture gallery with the following shortcode named `gallery.html`: {% for asset in page.assets -%} {%- if asset is matching("[.](jpg|png)$") -%} {% set image = resize_image(path=asset, width=240, height=180) %} - + {%- endif %} diff --git a/docs/content/documentation/getting-started/configuration.md b/docs/content/documentation/getting-started/configuration.md index 2565e0913f..0591b021d5 100644 --- a/docs/content/documentation/getting-started/configuration.md +++ b/docs/content/documentation/getting-started/configuration.md @@ -67,6 +67,9 @@ ignored_static = [] # When set to "true", a feed is automatically generated. generate_feeds = false +# When set to "all", paginated pages are not a part of the sitemap, default is "none" +exclude_paginated_pages_in_sitemap = "none" + # The filenames to use for the feeds. Used as the template filenames, too. # Defaults to ["atom.xml"], which has a built-in template that renders an Atom 1.0 feed. # There is also a built-in template "rss.xml" that renders an RSS 2.0 feed. @@ -111,6 +114,9 @@ generate_robots_txt = true # When set to "true", all code blocks are highlighted. highlight_code = false +# When set to "true", missing highlight languages are treated as errors. Defaults to false. +error_on_missing_highlight = false + # A list of directories used to search for additional `.sublime-syntax` and `.tmTheme` files. extra_syntaxes_and_themes = [] diff --git a/docs/content/documentation/templates/pages-sections.md b/docs/content/documentation/templates/pages-sections.md index 97f9559614..1061796f48 100644 --- a/docs/content/documentation/templates/pages-sections.md +++ b/docs/content/documentation/templates/pages-sections.md @@ -112,8 +112,16 @@ backlinks: Array<{permalink: String, title: String?}>; generate_feeds: bool; // Whether this section is transparent. Taken from the front-matter if set transparent: bool; +// How many items per pager (if defined) +paginate_by: Number?; +// If items order is reversed in the pagination (defaults to false) +paginate_reversed: bool; ``` +Information about pagination is useful when using the `get_section` Tera function for which the `paginator` is not available. + +See [pagination template documentation](@/documentation/templates/pagination.md) for more information on the `paginator` variable. + ## Table of contents Both page and section templates have a `toc` variable that corresponds to an array of `Header`. diff --git a/docs/content/documentation/templates/pagination.md b/docs/content/documentation/templates/pagination.md index e1851bdce4..ad4c956cca 100644 --- a/docs/content/documentation/templates/pagination.md +++ b/docs/content/documentation/templates/pagination.md @@ -52,6 +52,12 @@ A paginated taxonomy gets two variables aside from the `paginator` variable: See the [taxonomies page](@/documentation/templates/taxonomies.md) for a detailed version of the types. +## SEO + +It is preferable to not include paginated pages in sitemap since they are non-canonical pages. +To exclude paginated pages in sitemap, set the +`exclude_paginated_pages_in_sitemap` as `all` in `config.toml`. + ## Example Here is an example from a theme on how to use pagination on a page (`index.html` in this case): diff --git a/docs/templates/shortcodes/gallery.html b/docs/templates/shortcodes/gallery.html index d171d84974..af325d14a4 100644 --- a/docs/templates/shortcodes/gallery.html +++ b/docs/templates/shortcodes/gallery.html @@ -2,7 +2,7 @@ {% for asset in page.assets -%} {%- if asset is matching("[.](jpg|png)$") -%} {% set image = resize_image(path=asset, width=240, height=180) %} - + {%- endif %} diff --git a/q b/q new file mode 100644 index 0000000000..1d3c7540e3 --- /dev/null +++ b/q @@ -0,0 +1,74 @@ +diff --git a/components/content/src/page.rs b/components/content/src/page.rs +index 9fff5ed4..71fed715 100644 +--- a/components/content/src/page.rs ++++ b/components/content/src/page.rs +@@ -198,24 +198,18 @@ impl Page { + let parent_dir = path.parent().unwrap(); + page.assets = find_related_assets(parent_dir, config, true); + if !page.assets.is_empty() { ++ let colocated_path = page ++ .file ++ .colocated_path ++ .as_ref() ++ .expect("Should have colocated path for assets"); + page.serialized_assets = serialize_assets( + &page.assets, + page.file.path.parent().unwrap(), +- page.file +- .colocated_path +- .as_ref() +- .expect("Should have colocated path for assets"), +- ); +- } +- if !page.serialized_assets.is_empty() { +- page.assets_permalinks = get_assets_permalinks( +- &page.serialized_assets, +- &page.permalink, +- page.file +- .colocated_path +- .as_ref() +- .expect("Should have colocated path for assets"), ++ colocated_path, + ); ++ page.assets_permalinks = ++ get_assets_permalinks(&page.serialized_assets, &page.permalink, colocated_path); + } + } else { + page.assets = vec![]; +diff --git a/components/content/src/section.rs b/components/content/src/section.rs +index 2cf36644..b4ac035e 100644 +--- a/components/content/src/section.rs ++++ b/components/content/src/section.rs +@@ -130,25 +130,20 @@ impl Section { + let parent_dir = path.parent().unwrap(); + section.assets = find_related_assets(parent_dir, config, false); + if !section.assets.is_empty() { ++ let colocated_path = section ++ .file ++ .colocated_path ++ .as_ref() ++ .expect("Should have colocated path for assets"); + section.serialized_assets = serialize_assets( + §ion.assets, + section.file.path.parent().unwrap(), +- section +- .file +- .colocated_path +- .as_ref() +- .expect("Should have colocated path for assets"), ++ colocated_path, + ); +- } +- if !section.serialized_assets.is_empty() { + section.assets_permalinks = get_assets_permalinks( + §ion.serialized_assets, + §ion.permalink, +- section +- .file +- .colocated_path +- .as_ref() +- .expect("Should have colocated path for assets"), ++ colocated_path, + ); + } +  diff --git a/src/fs_utils.rs b/src/fs_utils.rs index 9cd3da8a75..6530de437f 100644 --- a/src/fs_utils.rs +++ b/src/fs_utils.rs @@ -35,6 +35,10 @@ pub type MeaningfulEvent = (PathBuf, PathBuf, SimpleFileSystemEventKind); /// return `None`. fn get_relevant_event_kind(event_kind: &EventKind) -> Option { match event_kind { + // Nova on macOS reports this as it's final event on change + EventKind::Modify(ModifyKind::Name(RenameMode::Any)) => { + Some(SimpleFileSystemEventKind::Modify) + } EventKind::Create(CreateKind::File) | EventKind::Create(CreateKind::Folder) => { Some(SimpleFileSystemEventKind::Create) }