diff --git a/.deepsource.toml b/.deepsource.toml index e1aa2aab..8490b355 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -5,4 +5,4 @@ name = "rust" enabled = true [analyzers.meta] -msrv = "stable" +msrv = "stable" \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..fd099d24 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: MIT + +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 4 + +[*.rs] +max_line_length = 80 + +[*.md] +# double whitespace at end of line +# denotes a line break in Markdown +trim_trailing_whitespace = false + +[*.yml] +indent_size = 2 + +[Makefile] +indent_style = tab \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..4be15071 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# Auto detect text files and perform normalization +* text=auto eol=lf + +*.rs text diff=rust eol=lf +*.toml text diff=toml eol=lf +Cargo.lock text eol=lf + +*.sh text eol=lf +*.ps1 text eol=crlf \ No newline at end of file diff --git a/.github/CODE-OF-CONDUCT.md b/.github/CODE-OF-CONDUCT.md index 4c26c1f1..20bcc4f6 100644 --- a/.github/CODE-OF-CONDUCT.md +++ b/.github/CODE-OF-CONDUCT.md @@ -1,62 +1,133 @@ -# Code of Conduct + +# Contributor Covenant Code of Conduct ## Our Pledge -In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. ## Our Standards -### Examples of Positive Behaviour +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community -- Using welcoming and inclusive language -- Being respectful of differing viewpoints and experiences -- Gracefully accepting constructive criticism -- Focusing on what is best for the community -- Showing empathy towards other community members +Examples of unacceptable behavior include: -### Examples of Unacceptable Behaviour +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting -- The use of sexualized language or imagery and unwelcome sexual attention or advances -- Trolling, insulting/derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as physical or electronic address, without explicit permission -- Conduct which could reasonably be considered inappropriate in a professional setting +## Enforcement Responsibilities -## Our Responsibilities +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. -Project maintainers are responsible for clarifying the standards of acceptable behaviour and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behaviour. +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. ## Scope -This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. ## Enforcement -### Reporting +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[INSERT CONTACT METHOD]. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: -Instances of abusive, harassing, or otherwise unacceptable behaviour may be reported by contacting the project team at . +### 1. Correction -### Procedures +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. -Upon receiving a report, the project team will review and investigate the allegations, taking the most appropriate action based on the severity of the issue, ranging from a warning to immediate removal from the project. +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. -### Appeal +### 2. Warning -If you disagree with a decision and wish to appeal, please contact [another entity or person on the project] who was not involved in the action you are appealing against to discuss your concerns. +**Community Impact**: A violation through a single incident or series of +actions. -### Conflicts of Interest +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. -In the event of any conflicts of interest in the enforcement of this code of conduct, the conflicted parties will recuse themselves from handling the report. +### 3. Temporary Ban -### Consequences +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. -Maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. -Updates -Updates to this Code of Conduct will be announced via [Platform], so community members are always aware of any changes. +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. ## Attribution -This Code of Conduct is adapted from the Contributor Covenant homepage, -version 1.4, available at -[version](http://contributor-covenant.org/version/1/4) +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index e8cbb63c..61684b9a 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -1,61 +1,32 @@ name: 📶 Coverage -on: - push: - branches: - - main - pull_request: - -env: - CARGO_TERM_COLOR: always +on: [push] jobs: - coverage: - name: Code Coverage + lint: runs-on: ubuntu-latest - env: - CARGO_INCREMENTAL: "0" - RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests" - RUSTDOCFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests" - steps: - # Checkout the repository - name: Checkout repository uses: actions/checkout@v4 - # Setup Rust nightly - name: Install Rust uses: actions-rs/toolchain@v1 - id: toolchain with: - toolchain: nightly + toolchain: stable override: true - # Configure cache for Cargo - - name: Cache Cargo registry, index - uses: actions/cache@v4 - id: cache-cargo - with: - path: | - ~/.cargo/registry - ~/.cargo/bin - ~/.cargo/git - key: linux-${{ steps.toolchain.outputs.rustc_hash }}-rust-cov-${{ hashFiles('**/Cargo.lock') }} - - # Run tests with all features - - name: Test (cargo test) - uses: actions-rs/cargo@v1 - with: - command: test - args: "--workspace" + - name: Install Cargo Tarpaulin + run: cargo install cargo-tarpaulin - # Install grcov - - uses: actions-rs/grcov@v0.1 - id: coverage + - name: Run tests with coverage + run: cargo tarpaulin --out Lcov --all-features --no-fail-fast + env: + CARGO_INCREMENTAL: '0' + RUSTFLAGS: '-Ccodegen-units=1 -Clink-dead-code -Coverflow-checks=off' + RUSTDOCFLAGS: '' - # Upload to Codecov.io - - name: Upload to Codecov.io + - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} - file: ${{ steps.coverage.outputs.report }} + file: lcov.info diff --git a/.gitignore b/.gitignore index ede35c89..5438b01f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,12 +2,14 @@ *.profraw *.log /.vscode/ +/backup_dir/ /docs/ /examples/public/ /mysite/ /output/ /pdfs/ /public/ +/serve/ /target/ build Icon? diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b04daa2e..7a4a04f4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ -# Contributing to `shokunin` +# Contributing to `Shokunin Static Site Generator (SSG)` -Welcome! We're thrilled that you're interested in contributing to the `shokunin` library. Whether you're looking to evangelize, submit feedback, or contribute code, we appreciate your involvement in making `shokunin` a better tool for everyone. Here's how you can get started. +Welcome! We're thrilled that you're interested in contributing to the `Shokunin Static Site Generator (SSG)` library. Whether you're looking to evangelize, submit feedback, or contribute code, we appreciate your involvement in making `Shokunin Static Site Generator (SSG)` a better tool for everyone. Here's how you can get started. ## Evangelize diff --git a/Cargo.lock b/Cargo.lock index dc66e46f..92dfce47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,18 +4,18 @@ version = 3 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "ahash" @@ -60,19 +60,10 @@ dependencies = [ ] [[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" +name = "allocator-api2" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] +checksum = "611cc2ae7d2e242c457e4be7f97036b8ad9ca152b499f53faf99b1ed8fc2553f" [[package]] name = "anes" @@ -82,270 +73,74 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.13" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" -dependencies = [ - "anstyle", - "windows-sys 0.52.0", -] - -[[package]] -name = "assert_cmd" -version = "2.0.16" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", - "bstr", - "doc-comment", - "libc", - "predicates", - "predicates-core", - "predicates-tree", - "wait-timeout", -] - -[[package]] -name = "async-attributes" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" -dependencies = [ - "quote", - "syn 1.0.109", -] - -[[package]] -name = "async-channel" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" -dependencies = [ - "concurrent-queue", - "event-listener 2.5.3", - "futures-core", -] - -[[package]] -name = "async-channel" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136d4d23bcc79e27423727b36823d86233aad06dfea531837b038394d11e9928" -dependencies = [ - "concurrent-queue", - "event-listener 5.3.0", - "event-listener-strategy 0.5.2", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-executor" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b10202063978b3351199d68f8b22c4e47e4b1b822f8d43fd862d5ea8c006b29a" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand 2.1.0", - "futures-lite 2.3.0", - "slab", -] - -[[package]] -name = "async-global-executor" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" -dependencies = [ - "async-channel 2.2.1", - "async-executor", - "async-io 2.3.2", - "async-lock 3.3.0", - "blocking", - "futures-lite 2.3.0", - "once_cell", -] - -[[package]] -name = "async-io" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" -dependencies = [ - "async-lock 2.8.0", - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-lite 1.13.0", - "log", - "parking", - "polling 2.8.0", - "rustix 0.37.27", - "slab", - "socket2 0.4.10", - "waker-fn", -] - -[[package]] -name = "async-io" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884" -dependencies = [ - "async-lock 3.3.0", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite 2.3.0", - "parking", - "polling 3.7.0", - "rustix 0.38.34", - "slab", - "tracing", - "windows-sys 0.52.0", -] - -[[package]] -name = "async-lock" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" -dependencies = [ - "event-listener 2.5.3", -] - -[[package]] -name = "async-lock" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" -dependencies = [ - "event-listener 4.0.3", - "event-listener-strategy 0.4.0", - "pin-project-lite", -] - -[[package]] -name = "async-process" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" -dependencies = [ - "async-io 1.13.0", - "async-lock 2.8.0", - "async-signal", - "blocking", - "cfg-if", - "event-listener 3.1.0", - "futures-lite 1.13.0", - "rustix 0.38.34", - "windows-sys 0.48.0", -] - -[[package]] -name = "async-signal" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afe66191c335039c7bb78f99dc7520b0cbb166b3a1cb33a03f53d8a1c6f2afda" -dependencies = [ - "async-io 2.3.2", - "async-lock 3.3.0", - "atomic-waker", - "cfg-if", - "futures-core", - "futures-io", - "rustix 0.38.34", - "signal-hook-registry", - "slab", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] -name = "async-std" -version = "1.12.0" +name = "anyhow" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" -dependencies = [ - "async-attributes", - "async-channel 1.9.0", - "async-global-executor", - "async-io 1.13.0", - "async-lock 2.8.0", - "async-process", - "crossbeam-utils", - "futures-channel", - "futures-core", - "futures-io", - "futures-lite 1.13.0", - "gloo-timers", - "kv-log-macro", - "log", - "memchr", - "once_cell", - "pin-project-lite", - "pin-utils", - "slab", - "wasm-bindgen-futures", -] +checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" [[package]] -name = "async-task" -version = "4.7.1" +name = "arraydeque" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" [[package]] -name = "async-tungstenite" -version = "0.23.0" +name = "async-trait" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e9efbe14612da0a19fb983059a0b621e9cf6225d7018ecab4f9988215540dc" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ - "async-std", - "futures-io", - "futures-util", - "log", - "pin-project-lite", - "tungstenite", + "proc-macro2", + "quote", + "syn 2.0.87", ] [[package]] @@ -356,23 +151,23 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.2.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" -version = "0.3.71" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] @@ -428,9 +223,12 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] [[package]] name = "bitvec" @@ -453,31 +251,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "blocking" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "495f7104e962b7356f0aeb34247aca1fe7d2e783b346582db7f2904cb5717e88" -dependencies = [ - "async-channel 2.2.1", - "async-lock 3.3.0", - "async-task", - "futures-io", - "futures-lite 2.3.0", - "piper", -] - -[[package]] -name = "bstr" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" -dependencies = [ - "memchr", - "regex-automata", - "serde", -] - [[package]] name = "bumpalo" version = "3.16.0" @@ -514,11 +287,18 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.0" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" + +[[package]] +name = "caseless" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "808dab3318747be122cb31d36de18d4d1c81277a76f8332a02b81a3d73463d7f" dependencies = [ - "serde", + "regex", + "unicode-normalization", ] [[package]] @@ -529,9 +309,12 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.96" +version = "1.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "065a29261d53ba54260972629f9ca6bffa69bac13cd1fed61420f7fa68b9f8bd" +checksum = "40545c26d092346d8a8dab71ee48e7685a7a9cba76e634790c215b41a4a7b4cf" +dependencies = [ + "shlex", +] [[package]] name = "cfg-if" @@ -539,85 +322,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "chromiumoxide" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2abb1e644b7fcaa711fe4d3fca06c52bf18ebc5398711b2b11e8281c6107fbe" -dependencies = [ - "async-std", - "async-tungstenite", - "base64 0.21.7", - "bytes", - "cfg-if", - "chromiumoxide_cdp", - "chromiumoxide_types", - "dunce", - "fnv", - "futures", - "futures-timer", - "pin-project-lite", - "reqwest 0.11.27", - "serde", - "serde_json", - "thiserror", - "tracing", - "url", - "which", - "winreg 0.51.0", -] - -[[package]] -name = "chromiumoxide_cdp" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72641e4931b5372361160346d88605bbd6252b54c89581b71ac06b3b68b8b7c0" -dependencies = [ - "chromiumoxide_pdl", - "chromiumoxide_types", - "serde", - "serde_json", -] - -[[package]] -name = "chromiumoxide_pdl" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ecbd2d949e41b72575e61f9a38a5e2d6207ffb98544448e1eb7b9310d48bbbe" -dependencies = [ - "chromiumoxide_types", - "either", - "heck 0.4.1", - "once_cell", - "proc-macro2", - "quote", - "regex", - "serde", - "serde_json", -] - -[[package]] -name = "chromiumoxide_types" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5fc717f4c899a7a02a50f7698a90acade50a1a65fe7804e57c747739fa23e25" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "chrono" -version = "0.4.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "num-traits", - "windows-targets 0.52.6", -] - [[package]] name = "ciborium" version = "0.2.2" @@ -647,9 +351,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.18" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" dependencies = [ "clap_builder", "clap_derive", @@ -657,9 +361,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.18" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" dependencies = [ "anstream", "anstyle", @@ -674,40 +378,52 @@ version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.87", ] [[package]] name = "clap_lex" -version = "0.7.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] -name = "colored" -version = "2.1.0" +name = "comrak" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +checksum = "c93ab3577cca16b4a1d80a88c2e0cd8b6e969e51696f0bbb0d1dcb0157109832" dependencies = [ - "lazy_static", - "windows-sys 0.48.0", + "caseless", + "clap", + "derive_builder", + "entities", + "memchr", + "once_cell", + "regex", + "shell-words", + "slug", + "syntect", + "typed-arena", + "unicode_categories", + "xdg", ] [[package]] name = "comrak" -version = "0.24.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a972c8ec1be8065f7b597b5f7f5b3be535db780280644aebdcd1966decf58dc" +checksum = "d8c32ff8b21372fab0e9ecc4e42536055702dc5faa418362bffd1544f9d12637" dependencies = [ + "caseless", "clap", "derive_builder", "entities", @@ -723,22 +439,65 @@ dependencies = [ ] [[package]] -name = "concurrent-queue" -version = "2.5.0" +name = "config" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf" dependencies = [ - "crossbeam-utils", + "async-trait", + "convert_case", + "json5", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "yaml-rust2 0.8.1", ] [[package]] -name = "const-str" -version = "0.3.2" +name = "console" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21077772762a1002bb421c3af42ac1725fa56066bfc53d9a55bb79905df2aaf3" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" dependencies = [ - "const-str-proc-macro", -] + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.52.0", +] + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "const-str" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21077772762a1002bb421c3af42ac1725fa56066bfc53d9a55bb79905df2aaf3" +dependencies = [ + "const-str-proc-macro", +] [[package]] name = "const-str-proc-macro" @@ -751,6 +510,15 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -763,24 +531,24 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" dependencies = [ "libc", ] [[package]] name = "crc32fast" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] @@ -821,6 +589,15 @@ dependencies = [ "itertools 0.10.5", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.5" @@ -842,9 +619,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crunchy" @@ -862,6 +639,19 @@ dependencies = [ "typenum", ] +[[package]] +name = "cssparser" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3df4f93e5fbbe73ec01ec8d3f68bba73107993a5b1e7519273c32db9b0d5be" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.11.2", + "smallvec", +] + [[package]] name = "cssparser" version = "0.33.0" @@ -881,7 +671,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "556c099a61d85989d7af52b692e35a8d68a57e7df8c6d07563dc0778b3960c9f" dependencies = [ - "cssparser", + "cssparser 0.33.0", ] [[package]] @@ -891,14 +681,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.60", + "syn 2.0.87", ] [[package]] name = "darling" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ "darling_core", "darling_macro", @@ -906,27 +696,27 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.60", + "syn 2.0.87", ] [[package]] name = "darling_macro" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.60", + "syn 2.0.87", ] [[package]] @@ -964,50 +754,56 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", + "serde", ] [[package]] name = "derive_builder" -version = "0.20.0" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" dependencies = [ "derive_builder_macro", ] [[package]] name = "derive_builder_core" -version = "0.20.0" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.87", ] [[package]] name = "derive_builder_macro" -version = "0.20.0" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.60", + "syn 2.0.87", ] [[package]] -name = "deunicode" -version = "1.4.4" +name = "derive_more" +version = "0.99.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322ef0094744e63628e6f0eb2295517f79276a5b342a4c2ff3042566ca181d4e" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] [[package]] -name = "difflib" -version = "0.4.0" +name = "deunicode" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" +checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00" [[package]] name = "digest" @@ -1020,16 +816,24 @@ dependencies = [ ] [[package]] -name = "doc-comment" -version = "0.3.3" +name = "displaydoc" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] [[package]] -name = "dotenvy" -version = "0.15.7" +name = "dlv-list" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] [[package]] name = "dtoa" @@ -1039,9 +843,9 @@ checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" [[package]] name = "dtoa-short" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbaceec3c6e4211c79e7b1800fb9680527106beb2f9c51904a3210c03a448c74" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" dependencies = [ "dtoa", ] @@ -1069,22 +873,45 @@ dependencies = [ ] [[package]] -name = "dunce" -version = "1.0.4" +name = "dtt" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0f956d085fb3371b9ab70b662dc444d2ebc60e2963eed84f1fc3fb3f56549c" +dependencies = [ + "lazy_static", + "paste", + "regex", + "serde", + "serde_derive", + "serde_json", + "thiserror 1.0.69", + "time", + "version_check", +] + +[[package]] +name = "ego-tree" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" +checksum = "12a0bb14ac04a9fcf170d0bbbef949b44cc492f4452bd20c095636956f653642" [[package]] name = "either" -version = "1.11.0" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "encode_unicode" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" -version = "0.8.34" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] @@ -1097,9 +924,9 @@ checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" [[package]] name = "env_filter" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" dependencies = [ "log", "regex", @@ -1118,6 +945,15 @@ dependencies = [ "log", ] +[[package]] +name = "envy" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965" +dependencies = [ + "serde", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -1126,73 +962,14 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", ] -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - -[[package]] -name = "event-listener" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener" -version = "4.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9944b8ca13534cdfb2800775f8dd4902ff3fc75a50101466decadfdf322a24" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" -dependencies = [ - "event-listener 4.0.3", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" -dependencies = [ - "event-listener 5.3.0", - "pin-project-lite", -] - [[package]] name = "fancy-regex" version = "0.11.0" @@ -1205,30 +982,27 @@ dependencies = [ [[package]] name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - -[[package]] -name = "fastrand" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" [[package]] -name = "figlet-rs" -version = "0.1.5" +name = "filetime" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4742a071cd9694fc86f9fa1a08fa3e53d40cc899d7ee532295da2d085639fbc5" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.59.0", +] [[package]] name = "flate2" -version = "1.0.30" +version = "1.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" dependencies = [ "crc32fast", "miniz_oxide", @@ -1265,126 +1039,87 @@ dependencies = [ ] [[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - -[[package]] -name = "futures" -version = "0.3.30" +name = "frontmatter-gen" +version = "0.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "4ca687f7d05be18198951f3599b904cfd1897a6a9e7c1dff2c2ae84df2c62a68" dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", + "serde", + "serde_json", + "serde_yml", + "thiserror 1.0.69", + "tokio", + "toml", + "version_check", ] [[package]] -name = "futures-channel" -version = "0.3.30" +name = "fsevent-sys" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" dependencies = [ - "futures-core", - "futures-sink", + "libc", ] [[package]] -name = "futures-core" -version = "0.3.30" +name = "funty" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] -name = "futures-executor" -version = "0.3.30" +name = "futf" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" dependencies = [ - "futures-core", - "futures-task", - "futures-util", + "mac", + "new_debug_unreachable", ] [[package]] -name = "futures-io" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" - -[[package]] -name = "futures-lite" -version = "1.13.0" +name = "futures-channel" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ - "fastrand 1.9.0", "futures-core", - "futures-io", - "memchr", - "parking", - "pin-project-lite", - "waker-fn", + "futures-sink", ] [[package]] -name = "futures-lite" -version = "2.3.0" +name = "futures-core" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" -dependencies = [ - "fastrand 2.1.0", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] -name = "futures-macro" -version = "0.3.30" +name = "futures-io" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.60", -] +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" - -[[package]] -name = "futures-timer" -version = "3.0.3" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ - "futures-channel", "futures-core", "futures-io", - "futures-macro", "futures-sink", "futures-task", "memchr", @@ -1423,9 +1158,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -1434,53 +1169,22 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" - -[[package]] -name = "gloo-timers" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "h2" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "h2" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" dependencies = [ + "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "futures-util", - "http 1.1.0", + "http", "indexmap", "slab", "tokio", @@ -1522,12 +1226,34 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash 0.8.11", + "allocator-api2", +] [[package]] -name = "heck" -version = "0.4.1" +name = "hashbrown" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "hashlink" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] [[package]] name = "heck" @@ -1542,13 +1268,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] -name = "home" -version = "0.5.9" +name = "hermit-abi" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" -dependencies = [ - "windows-sys 0.52.0", -] +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" [[package]] name = "hostname" @@ -1573,14 +1296,48 @@ dependencies = [ ] [[package]] -name = "http" -version = "0.2.12" +name = "html-escape" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" dependencies = [ - "bytes", - "fnv", - "itoa", + "utf8-width", +] + +[[package]] +name = "html-generator" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c4175259dd09b9a572073218f67a8d1a424d7d13922a422d73b49b204786ecf" +dependencies = [ + "comrak 0.28.0", + "frontmatter-gen", + "lazy_static", + "mdx-gen", + "minify-html", + "once_cell", + "regex", + "scraper", + "serde", + "serde_json", + "tempfile", + "thiserror 1.0.69", + "tokio", + "version_check", +] + +[[package]] +name = "html5ever" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 2.0.87", ] [[package]] @@ -1596,49 +1353,46 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.6" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 0.2.12", - "pin-project-lite", + "http", ] [[package]] -name = "http-body" -version = "1.0.0" +name = "http-body-util" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", - "http 1.1.0", + "futures-util", + "http", + "http-body", + "pin-project-lite", ] [[package]] -name = "http-body-util" -version = "0.1.1" +name = "http-handle" +version = "0.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +checksum = "9146e09a9aace92963ea15b9f1b6fa0610bf704a96d053ecc9b49be301d8a091" dependencies = [ - "bytes", - "futures-core", - "http 1.1.0", - "http-body 1.0.0", - "pin-project-lite", + "anyhow", + "log", + "serde", + "serde_json", + "thiserror 1.0.69", + "version_check", ] [[package]] name = "httparse" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" - -[[package]] -name = "httpdate" -version = "1.0.3" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "humantime" @@ -1648,40 +1402,16 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2 0.3.26", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.5.7", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper" -version = "1.3.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.4", - "http 1.1.0", - "http-body 1.0.0", + "h2", + "http", + "http-body", "httparse", "itoa", "pin-project-lite", @@ -1692,13 +1422,13 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.2" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", - "http 1.1.0", - "hyper 1.3.1", + "http", + "hyper", "hyper-util", "rustls", "rustls-pki-types", @@ -1715,7 +1445,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.3.1", + "hyper", "hyper-util", "native-tls", "tokio", @@ -1725,45 +1455,139 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.3" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.1.0", - "http-body 1.0.0", - "hyper 1.3.1", + "http", + "http-body", + "hyper", "pin-project-lite", - "socket2 0.5.7", + "socket2", "tokio", - "tower", "tower-service", "tracing", ] [[package]] -name = "iana-time-zone" -version = "0.1.60" +name = "icu_collections" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core", + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", ] [[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" +name = "icu_provider_macros" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ - "cc", + "proc-macro2", + "quote", + "syn 2.0.87", ] [[package]] @@ -1774,61 +1598,100 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.5.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] name = "indexmap" -version = "2.2.6" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown 0.14.5", + "hashbrown 0.15.1", ] [[package]] -name = "instant" -version = "0.1.12" +name = "indicatif" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" dependencies = [ - "cfg-if", + "console", + "instant", + "number_prefix", + "portable-atomic", + "unicode-width", ] [[package]] -name = "io-lifetimes" -version = "1.0.11" +name = "inotify" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" dependencies = [ - "hermit-abi", + "bitflags 1.3.2", + "inotify-sys", "libc", - "windows-sys 0.48.0", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", ] [[package]] name = "ipnet" -version = "2.9.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" [[package]] name = "is-terminal" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" dependencies = [ - "hermit-abi", + "hermit-abi 0.4.0", "libc", "windows-sys 0.52.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.10.5" @@ -1855,20 +1718,64 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" dependencies = [ "wasm-bindgen", ] [[package]] -name = "kv-log-macro" -version = "1.0.7" +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "kqueue" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + +[[package]] +name = "langweave" +version = "0.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +checksum = "7a9f7ee4f988f9bdd5689ba32459826303f304162cf3f4b39faf759708a43efe" dependencies = [ + "anyhow", + "async-trait", + "env_logger", + "lazy_static", "log", + "once_cell", + "regex", + "serde", + "serde_json", + "smallvec", + "thiserror 1.0.69", + "tokio", + "version_check", + "whatlang", ] [[package]] @@ -1879,32 +1786,48 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.154" +version = "0.2.162" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" + +[[package]] +name = "libredox" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", + "redox_syscall", +] [[package]] name = "libyml" -version = "0.0.1" +version = "0.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1303532258de1fbe263b4daaaba0e17e3d502b8de57b7845928b92398fb4afd1" +checksum = "3302702afa434ffa30847a83305f0a69d6abd74293b6554c18ec85c7ef30c980" +dependencies = [ + "anyhow", + "version_check", +] [[package]] name = "lightningcss" -version = "1.0.0-alpha.55" +version = "1.0.0-alpha.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bd5bed3814fb631bfc1e24c2be6f7e86a9837c660909acab79a38374dcb8798" +checksum = "7f3aad0f3d9105ab72b02caf1b2a998a6d549f3fff8cca0c558e590c3245edfd" dependencies = [ "ahash 0.8.11", - "bitflags 2.5.0", + "bitflags 2.6.0", "const-str", - "cssparser", + "cssparser 0.33.0", "cssparser-color", "dashmap", "data-encoding", "getrandom", "itertools 0.10.5", "lazy_static", + "lightningcss-derive", "parcel_selectors", "parcel_sourcemap", "paste", @@ -1915,10 +1838,16 @@ dependencies = [ ] [[package]] -name = "line-wrap" -version = "0.2.0" +name = "lightningcss-derive" +version = "1.0.0-alpha.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd1bc4d24ad230d21fb898d1116b1801d7adfc449d42026475862ab48b11e70e" +checksum = "84c12744d1279367caed41739ef094c325d53fb0ffcd4f9b84a368796f870252" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn 1.0.109", +] [[package]] name = "linked-hash-map" @@ -1928,15 +1857,15 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.3.8" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] -name = "linux-raw-sys" -version = "0.4.13" +name = "litemap" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" [[package]] name = "lock_api" @@ -1953,36 +1882,25 @@ name = "log" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" -dependencies = [ - "value-bag", -] [[package]] -name = "lopdf" -version = "0.32.0" +name = "mac" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e775e4ee264e8a87d50a9efef7b67b4aa988cf94e75630859875fc347e6c872b" -dependencies = [ - "chrono", - "encoding_rs", - "flate2", - "itoa", - "linked-hash-map", - "log", - "md5", - "nom", - "rayon", - "time", - "weezl", -] +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] -name = "markdown" -version = "1.0.0-alpha.17" +name = "markup5ever" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e27d6220ce21f80ce5c4201f23a37c6f1ad037c72c9d1ff215c2919605a5d6" +checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" dependencies = [ - "unicode-id", + "log", + "phf 0.11.2", + "phf_codegen 0.11.2", + "string_cache", + "string_cache_codegen", + "tendril", ] [[package]] @@ -1998,16 +1916,55 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] -name = "md5" -version = "0.7.0" +name = "mdx-gen" +version = "0.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" +checksum = "eba95ca121d5154b8ec38928b5421207ce7354d9b924e04dc648589d09d9861c" +dependencies = [ + "anyhow", + "comrak 0.28.0", + "env_logger", + "html-escape", + "lazy_static", + "log", + "regex", + "serde", + "serde_json", + "syntect", + "thiserror 1.0.69", + "tokio", + "toml", + "version_check", +] [[package]] name = "memchr" -version = "2.7.2" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "metadata-gen" +version = "0.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "14e7f23aba494ae4bfdee627d49d17584391aba8f53ceb42cb58973a80125dc1" +dependencies = [ + "anyhow", + "dtt 0.0.8", + "quick-xml 0.36.2", + "regex", + "scraper", + "serde", + "serde_json", + "serde_yml", + "tempfile", + "thiserror 1.0.69", + "time", + "tokio", + "toml", + "version_check", + "yaml-rust2 0.9.0", +] [[package]] name = "mime" @@ -2028,7 +1985,7 @@ dependencies = [ "minify-html-common", "minify-js", "once_cell", - "rustc-hash", + "rustc-hash 1.1.0", ] [[package]] @@ -2041,7 +1998,7 @@ dependencies = [ "itertools 0.12.1", "lazy_static", "memchr", - "rustc-hash", + "rustc-hash 1.1.0", "serde", "serde_json", ] @@ -2064,11 +2021,11 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ - "adler", + "adler2", ] [[package]] @@ -2078,17 +2035,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.48.0", ] +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "wasi", + "windows-sys 0.52.0", +] + [[package]] name = "native-tls" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" dependencies = [ - "lazy_static", "libc", "log", "openssl", @@ -2100,6 +2069,12 @@ dependencies = [ "tempfile", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nom" version = "7.1.3" @@ -2110,6 +2085,25 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.6.0", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio 0.8.11", + "walkdir", + "windows-sys 0.48.0", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -2118,37 +2112,33 @@ checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] -name = "num_cpus" -version = "1.16.0" +name = "number_prefix" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "object" -version = "0.32.2" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "onig" @@ -2174,17 +2164,17 @@ dependencies = [ [[package]] name = "oorandom" -version = "11.1.3" +version = "11.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" [[package]] name = "openssl" -version = "0.10.66" +version = "0.10.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cfg-if", "foreign-types", "libc", @@ -2201,7 +2191,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.87", ] [[package]] @@ -2212,18 +2202,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.2.3+3.2.1" +version = "300.4.0+3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cff92b6f71555b61bb9315f7c64da3ca43d87531622120fea0195fc761b4843" +checksum = "a709e02f2b4aca747929cca5ed248880847c650233cf8b8cdc48f40aaf4898a6" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.103" +version = "0.9.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" dependencies = [ "cc", "libc", @@ -2232,6 +2222,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + [[package]] name = "outref" version = "0.1.0" @@ -2240,17 +2240,17 @@ checksum = "7f222829ae9293e33a9f5e9f440c6760a3d450a64affe1846486b140db81c1f4" [[package]] name = "parcel_selectors" -version = "0.26.4" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d74befe2d076330d9a58bf9ca2da424568724ab278adf15fb5718253133887" +checksum = "7645c578d3a5c4cdf667af1ad39765f5f751c4883d251e050d5e1204b5cad0a9" dependencies = [ - "bitflags 2.5.0", - "cssparser", - "fxhash", + "bitflags 2.6.0", + "cssparser 0.33.0", "log", - "phf 0.10.1", - "phf_codegen", + "phf 0.11.2", + "phf_codegen 0.11.2", "precomputed-hash", + "rustc-hash 2.0.0", "smallvec", ] @@ -2268,17 +2268,11 @@ dependencies = [ "vlq", ] -[[package]] -name = "parking" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" - [[package]] name = "parking_lot" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -2312,41 +2306,66 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pathdiff" -version = "0.2.1" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61c5ce1153ab5b689d0c074c4e7fc613e942dfb7dd9eea5ab202d2ad91fe361" + +[[package]] +name = "percent-encoding" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] -name = "pdf_composer" -version = "0.2.71" +name = "pest" +version = "2.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15070b5a9468a6bf6d41bff639b2996507764b2713f0022b85020a576ed174ab" +checksum = "879952a81a83930934cbf1786752d6dedc3b1f29e8f8fb2ad1d0a36f377cf442" dependencies = [ - "async-std", - "chromiumoxide", - "colored", - "dotenvy", - "futures", - "lopdf", - "markdown", - "rayon", - "regex", - "serde", - "serde_yml", - "url-escape", + "memchr", + "thiserror 1.0.69", + "ucd-trie", ] [[package]] -name = "percent-encoding" -version = "2.3.1" +name = "pest_derive" +version = "2.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "d214365f632b123a47fd913301e14c946c61d1c183ee245fa76eb752e59a02dd" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb55586734301717aea2ac313f50b2eb8f60d2fc3dc01d190eefa2e625f60c4e" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "pest_meta" +version = "2.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75da2a70cf4d9cb76833c990ac9cd3923c9a8905a8929789ce347c84564d03d" +dependencies = [ + "once_cell", + "pest", + "sha2", +] [[package]] name = "phf" @@ -2377,6 +2396,16 @@ dependencies = [ "phf_shared 0.10.0", ] +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", +] + [[package]] name = "phf_generator" version = "0.10.0" @@ -2407,7 +2436,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.87", ] [[package]] @@ -2428,31 +2457,11 @@ dependencies = [ "siphasher", ] -[[package]] -name = "pin-project" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.60", -] - [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" @@ -2460,42 +2469,30 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "piper" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" -dependencies = [ - "atomic-waker", - "fastrand 2.1.0", - "futures-io", -] - [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "plist" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9d34169e64b3c7a80c8621a48adaf44e0cf62c78a9b25dd9dd35f1881a17cf9" +checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "indexmap", - "line-wrap", - "quick-xml 0.31.0", + "quick-xml 0.32.0", "serde", "time", ] [[package]] name = "plotters" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" dependencies = [ "num-traits", "plotters-backend", @@ -2506,49 +2503,24 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" [[package]] name = "plotters-svg" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" dependencies = [ "plotters-backend", ] [[package]] -name = "polling" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" -dependencies = [ - "autocfg", - "bitflags 1.3.2", - "cfg-if", - "concurrent-queue", - "libc", - "log", - "pin-project-lite", - "windows-sys 0.48.0", -] - -[[package]] -name = "polling" -version = "3.7.0" +name = "portable-atomic" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645493cf344456ef24219d02a768cf1fb92ddf8c92161679ae3d91b91a637be3" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi", - "pin-project-lite", - "rustix 0.38.34", - "tracing", - "windows-sys 0.52.0", -] +checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" [[package]] name = "powerfmt" @@ -2558,9 +2530,12 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "precomputed-hash" @@ -2568,38 +2543,11 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" -[[package]] -name = "predicates" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8" -dependencies = [ - "anstyle", - "difflib", - "predicates-core", -] - -[[package]] -name = "predicates-core" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" - -[[package]] -name = "predicates-tree" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" -dependencies = [ - "predicates-core", - "termtree", -] - [[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] @@ -2626,11 +2574,11 @@ dependencies = [ [[package]] name = "pulldown-cmark" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "666f0f59e259aea2d72e6012290c09877a780935cc3c18b1ceded41f3890d59c" +checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "getopts", "memchr", "pulldown-cmark-escape", @@ -2645,27 +2593,28 @@ checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" [[package]] name = "quick-xml" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" dependencies = [ "memchr", ] [[package]] name = "quick-xml" -version = "0.36.1" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96a05e2e8efddfa51a84ca47cec303fac86c8541b686d37cac5efc0e094417bc" +checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" dependencies = [ "memchr", + "serde", ] [[package]] name = "quote" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -2728,18 +2677,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.1" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", ] [[package]] name = "regex" -version = "1.10.6" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick 1.1.3", "memchr", @@ -2749,9 +2698,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick 1.1.3", "memchr", @@ -2760,9 +2709,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rend" @@ -2775,45 +2724,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" -dependencies = [ - "base64 0.21.7", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2 0.3.26", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.28", - "ipnet", - "js-sys", - "log", - "mime", - "once_cell", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper 0.1.2", - "system-configuration 0.5.1", - "tokio", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "winreg 0.50.0", -] - -[[package]] -name = "reqwest" -version = "0.12.7" +version = "0.12.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" +checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" dependencies = [ "base64 0.22.1", "bytes", @@ -2821,11 +2734,11 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.4.4", - "http 1.1.0", - "http-body 1.0.0", + "h2", + "http", + "http-body", "http-body-util", - "hyper 1.3.1", + "hyper", "hyper-rustls", "hyper-tls", "hyper-util", @@ -2841,8 +2754,8 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 1.0.1", - "system-configuration 0.6.0", + "sync_wrapper", + "system-configuration", "tokio", "tokio-native-tls", "tower-service", @@ -2870,9 +2783,9 @@ dependencies = [ [[package]] name = "rkyv" -version = "0.7.44" +version = "0.7.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cba464629b3394fc4dbc6f940ff8f5b4ff5c7aef40f29166fd4ad12acbc99c0" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" dependencies = [ "bitvec", "bytecheck", @@ -2888,9 +2801,9 @@ dependencies = [ [[package]] name = "rkyv_derive" -version = "0.7.44" +version = "0.7.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7dddfff8de25e6f62b9d64e6e432bf1c6736c57d20323e15ee10435fbda7c65" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" dependencies = [ "proc-macro2", "quote", @@ -2925,11 +2838,77 @@ dependencies = [ "vrd 0.0.7", ] +[[package]] +name = "rlg" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7f175675d227fe657c5e3214a00c847516117810206f7f26828abc0b934817" +dependencies = [ + "config", + "dtt 0.0.8", + "envy", + "hostname 0.4.0", + "log", + "notify", + "once_cell", + "parking_lot", + "rayon", + "regex", + "serde", + "serde_json", + "serde_yml", + "tempfile", + "thiserror 1.0.69", + "tokio", + "toml", + "version_check", + "vrd 0.0.8", +] + +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags 2.6.0", + "serde", + "serde_derive", +] + +[[package]] +name = "rss-gen" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de826a400568de2c3a99c64977559141a5485b73ae3a91cdaf51728a010ebd" +dependencies = [ + "dtt 0.0.8", + "log", + "quick-xml 0.36.2", + "serde", + "serde_json", + "thiserror 1.0.69", + "time", + "url", + "version_check", +] + +[[package]] +name = "rust-ini" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" @@ -2938,37 +2917,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] -name = "rustix" -version = "0.37.27" +name = "rustc-hash" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.3.8", - "windows-sys 0.48.0", -] +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "errno", "libc", - "linux-raw-sys 0.4.13", + "linux-raw-sys", "windows-sys 0.52.0", ] [[package]] name = "rustls" -version = "0.23.7" +version = "0.23.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebbbdb961df0ad3f2652da8f3fdc4b36122f568f968f45ad3316f26c025c677b" +checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e" dependencies = [ "once_cell", "rustls-pki-types", @@ -2979,25 +2950,24 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ - "base64 0.22.1", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.5.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" [[package]] name = "rustls-webpki" -version = "0.102.3" +version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ "ring", "rustls-pki-types", @@ -3006,9 +2976,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" @@ -3021,11 +2991,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.23" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3034,6 +3004,22 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scraper" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b90460b31bfe1fc07be8262e42c665ad97118d4585869de9345a84d501a9eaf0" +dependencies = [ + "ahash 0.8.11", + "cssparser 0.31.2", + "ego-tree", + "getopts", + "html5ever", + "once_cell", + "selectors", + "tendril", +] + [[package]] name = "seahash" version = "4.1.0" @@ -3042,11 +3028,11 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "security-framework" -version = "2.10.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "core-foundation", "core-foundation-sys", "libc", @@ -3055,19 +3041,38 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.10.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef" +checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" dependencies = [ "core-foundation-sys", "libc", ] +[[package]] +name = "selectors" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb30575f3638fc8f6815f448d50cb1a2e255b0897985c8c59f4d37b72a07b06" +dependencies = [ + "bitflags 2.6.0", + "cssparser 0.31.2", + "derive_more", + "fxhash", + "log", + "new_debug_unreachable", + "phf 0.10.1", + "phf_codegen 0.10.0", + "precomputed-hash", + "servo_arc", + "smallvec", +] + [[package]] name = "serde" -version = "1.0.209" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" dependencies = [ "serde_derive", ] @@ -3083,20 +3088,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.209" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.87", ] [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "itoa", "memchr", @@ -3106,9 +3111,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] @@ -3127,29 +3132,33 @@ dependencies = [ [[package]] name = "serde_yml" -version = "0.0.5" +version = "0.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b48f444c502b161ee70dafc567a48336c0a05139e586758dc40ef528d511d235" +checksum = "59e2dd588bf1597a252c3b920e0143eb99b0f76e4e082f4c92ce34fbc9e71ddd" dependencies = [ - "dtt 0.0.5", - "env_logger", - "figlet-rs", "indexmap", "itoa", "libyml", - "log", - "openssl", - "rlg 0.0.3", + "memchr", "ryu", "serde", - "uuid", + "version_check", +] + +[[package]] +name = "servo_arc" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d036d71a959e00c77a63538b90a6c2390969f9772b096ea837205c6bd0491a44" +dependencies = [ + "stable_deref_trait", ] [[package]] -name = "sha1" -version = "0.10.6" +name = "sha2" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", @@ -3162,6 +3171,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -3182,9 +3197,9 @@ dependencies = [ [[package]] name = "simdutf8" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "siphasher" @@ -3192,6 +3207,30 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +[[package]] +name = "sitemap-gen" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fff9d5a3c04d8b3ea597323cd72107fb86825ffaa6a616d41b9bbe5650580610" +dependencies = [ + "clap", + "dtt 0.0.8", + "env_logger", + "html-generator", + "indicatif", + "lazy_static", + "log", + "regex", + "scraper", + "tempfile", + "thiserror 1.0.69", + "time", + "tokio", + "url", + "version_check", + "xml-rs", +] + [[package]] name = "slab" version = "0.4.9" @@ -3203,9 +3242,9 @@ dependencies = [ [[package]] name = "slug" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bd94acec9c8da640005f8e135a39fc0372e74535e6b368b7a04b875f784c8c4" +checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" dependencies = [ "deunicode", "wasm-bindgen", @@ -3217,16 +3256,6 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" -[[package]] -name = "socket2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "socket2" version = "0.5.7" @@ -3245,31 +3274,115 @@ checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "ssg" -version = "0.0.29" +version = "0.0.30" dependencies = [ - "assert_cmd", + "anyhow", "clap", - "comrak", "criterion", - "dtt 0.0.6", + "dtt 0.0.8", + "http-handle", + "langweave", + "log", + "openssl", + "rayon", + "rlg 0.0.6", + "serde", + "serde_json", + "staticdatagen", + "tempfile", + "thiserror 2.0.3", + "tokio", + "toml", + "url", + "uuid", + "version_check", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "staticdatagen" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe815d212dc27bf331794df863a773bbf5799d7ef19fd9637642a06b697b2e5c" +dependencies = [ + "anyhow", + "clap", + "comrak 0.29.0", + "dtt 0.0.8", "env_logger", + "html-generator", + "http-handle", + "langweave", "lazy_static", "log", + "metadata-gen", "minify-html", - "openssl", - "pdf_composer", "pulldown-cmark", - "quick-xml 0.36.1", + "quick-xml 0.36.2", "regex", - "reqwest 0.12.7", - "rlg 0.0.4", + "reqwest", + "rlg 0.0.6", + "rss-gen", "serde", "serde_json", + "sitemap-gen", + "staticweaver", "tempfile", + "thiserror 1.0.69", + "time", "toml", + "url", "uuid", - "vrd 0.0.7", - "yaml-rust", + "version_check", + "vrd 0.0.8", + "yaml-rust2 0.9.0", +] + +[[package]] +name = "staticweaver" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe8ba80aa11ee0ce9ddbfa60331612ea499486c802979368b89f17a4a31a168" +dependencies = [ + "fnv", + "regex", + "reqwest", + "serde", + "serde_json", + "tempfile", + "thiserror 1.0.69", + "version_check", +] + +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared 0.10.0", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro2", + "quote", ] [[package]] @@ -3297,21 +3410,15 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.60" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - [[package]] name = "sync_wrapper" version = "1.0.1" @@ -3321,6 +3428,17 @@ dependencies = [ "futures-core", ] +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "syntect" version = "5.2.0" @@ -3339,41 +3457,20 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "thiserror", + "thiserror 1.0.69", "walkdir", "yaml-rust", ] [[package]] name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "system-configuration-sys 0.5.0", -] - -[[package]] -name = "system-configuration" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bc6ee10a9b4fcf576e9b0819d95ec16f4d2c02d39fd83ac1c8789785c4a42" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "core-foundation", - "system-configuration-sys 0.6.0", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys", - "libc", + "system-configuration-sys", ] [[package]] @@ -3394,51 +3491,76 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.12.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" dependencies = [ "cfg-if", - "fastrand 2.1.0", + "fastrand", "once_cell", - "rustix 0.38.34", + "rustix", "windows-sys 0.59.0", ] +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + [[package]] name = "terminal_size" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" +checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef" dependencies = [ - "rustix 0.38.34", - "windows-sys 0.48.0", + "rustix", + "windows-sys 0.59.0", ] [[package]] -name = "termtree" -version = "0.4.1" +name = "thiserror" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] [[package]] name = "thiserror" -version = "1.0.59" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" +dependencies = [ + "thiserror-impl 2.0.3", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ - "thiserror-impl", + "proc-macro2", + "quote", + "syn 2.0.87", ] [[package]] name = "thiserror-impl" -version = "1.0.59" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" +checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.87", ] [[package]] @@ -3472,6 +3594,25 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -3484,9 +3625,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ "tinyvec_macros", ] @@ -3499,32 +3640,31 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.37.0" +version = "1.41.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" dependencies = [ "backtrace", "bytes", "libc", - "mio", - "num_cpus", + "mio 1.0.2", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.7", + "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.87", ] [[package]] @@ -3550,16 +3690,15 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", - "tracing", ] [[package]] @@ -3585,9 +3724,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.20" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", "serde", @@ -3596,33 +3735,11 @@ dependencies = [ "winnow", ] -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tokio", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-layer" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" - [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" @@ -3630,23 +3747,10 @@ version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "log", "pin-project-lite", - "tracing-attributes", "tracing-core", ] -[[package]] -name = "tracing-attributes" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.60", -] - [[package]] name = "tracing-core" version = "0.1.32" @@ -3662,25 +3766,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "tungstenite" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" -dependencies = [ - "byteorder", - "bytes", - "data-encoding", - "http 0.2.12", - "httparse", - "log", - "rand", - "sha1", - "thiserror", - "url", - "utf-8", -] - [[package]] name = "typed-arena" version = "2.0.2" @@ -3694,46 +3779,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] -name = "unicase" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] - -[[package]] -name = "unicode-bidi" -version = "0.3.15" +name = "ucd-trie" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] -name = "unicode-id" -version = "0.3.4" +name = "unicase" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1b6def86329695390197b82c1e244a54a131ceb66c996f2088a3876e2ae083f" +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "unicode-width" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode_categories" @@ -3749,51 +3831,54 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.0" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] -[[package]] -name = "url-escape" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44e0ce4d1246d075ca5abec4b41d33e87a6054d08e2366b63205665e950db218" -dependencies = [ - "percent-encoding", -] - [[package]] name = "utf-8" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ "getrandom", ] -[[package]] -name = "value-bag" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101" - [[package]] name = "vcpkg" version = "0.2.15" @@ -3802,9 +3887,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vlq" @@ -3828,7 +3913,7 @@ version = "0.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08fd4c00822f48600521b6dfa7ed8103e9f38c720e198ff4db0400c925414c80" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "dtt 0.0.5", "rand", "rlg 0.0.3", @@ -3840,20 +3925,23 @@ dependencies = [ ] [[package]] -name = "wait-timeout" -version = "0.2.0" +name = "vrd" +version = "0.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +checksum = "46865c0eccde4965b89ced6849abf44ffe603dd838a3666dfff3cc2d9437549d" dependencies = [ - "libc", + "bitflags 2.6.0", + "dtt 0.0.6", + "rand", + "rlg 0.0.4", + "serde", + "serde-big-array", + "serde_json", + "tokio", + "uuid", + "version_check", ] -[[package]] -name = "waker-fn" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" - [[package]] name = "walkdir" version = "2.5.0" @@ -3881,34 +3969,35 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.87", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" dependencies = [ "cfg-if", "js-sys", @@ -3918,9 +4007,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3928,49 +4017,41 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.87", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] -name = "weezl" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" - -[[package]] -name = "which" -version = "4.4.2" +name = "whatlang" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +checksum = "471d1c1645d361eb782a1650b1786a8fb58dd625e681a04c09f5ff7c8764a7b0" dependencies = [ - "either", - "home", + "hashbrown 0.14.5", "once_cell", - "rustix 0.38.34", ] [[package]] @@ -3991,11 +4072,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4203,32 +4284,24 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.18" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] [[package]] -name = "winreg" -version = "0.50.0" +name = "write16" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" [[package]] -name = "winreg" -version = "0.51.0" +name = "writeable" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "937f3df7948156640f46aacef17a70db0de5917bda9c92b0f751f3a955b588fc" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "wyz" @@ -4245,6 +4318,12 @@ version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" +[[package]] +name = "xml-rs" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af310deaae937e48a26602b730250b4949e125f468f11e6990be3e5304ddd96f" + [[package]] name = "yaml-rust" version = "0.4.5" @@ -4254,24 +4333,92 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "yaml-rust2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink 0.8.4", +] + +[[package]] +name = "yaml-rust2" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a1a1c0bc9823338a3bdf8c61f994f23ac004c6fa32c08cd152984499b445e8d" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink 0.9.1", +] + +[[package]] +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "synstructure", +] + [[package]] name = "zerocopy" -version = "0.7.32" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ + "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.87", + "synstructure", ] [[package]] @@ -4279,3 +4426,25 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] diff --git a/Cargo.toml b/Cargo.toml index 6aa87a9f..2aa2c2e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,26 +1,48 @@ +# ----------------------------------------------------------------------------- +# Shokunin - A lightning-fast static site generator (SSG) optimised for search engine visibility (SEO) and compliant with WCAG 2.1 Level AA accessibility standards. +# ----------------------------------------------------------------------------- + [package] -authors = ["Shokunin Contributors"] -build = "build.rs" +# General project metadata +name = "ssg" # The name of the library +version = "0.0.30" # The version of the library +authors = ["Shokunin Contributors"] # The authors of the library +edition = "2021" # The edition of the library +rust-version = "1.56.0" # Minimum supported Rust version +license = "MIT OR Apache-2.0" # Dual licensing strategy +description = """ +A Content-First Open Source Static Site Generator (SSG) crafted in Rust. +""" +homepage = "https://shokunin.one" # The project's homepage +documentation = "https://shokunin.one/documentation/index.html" # Doc URL +repository = "https://github.com/sebastienrousseau/shokunin" +readme = "README.md" # The README file +build = "build.rs" # The build script + +# ----------------------------------------------------------------------------- +# Crate Configuration +# ----------------------------------------------------------------------------- + +# Crates.io categories categories = [ "command-line-utilities", "parsing", "template-engine", "web-programming", ] -description = """ -A Content-First Open Source Static Site Generator (SSG) written in Rust. -""" -documentation = "https://shokunin.one/documentation/index.html" -edition = "2021" -exclude = ["/.git/*", "/.github/*", "/.gitignore", "/.vscode/*"] -homepage = "https://shokunin.one" + +# Keywords for easier discoverability on Crates.io keywords = ["cli", "generator", "site", "ssg", "static"] -license = "MIT OR Apache-2.0" -name = "ssg" -readme = "README.md" -repository = "https://github.com/sebastienrousseau/shokunin" -rust-version = "1.60.0" -version = "0.0.29" + +# Excluding unnecessary files from the package +exclude = [ + "/.git/*", # Exclude version control files + "/.github/*", # Exclude GitHub workflows + "/.gitignore", # Ignore Git ignore file + "/.vscode/*" # Ignore VSCode settings +] + +# Including necessary files in the package include = [ "/CONTRIBUTING.md", "/LICENSE-APACHE", @@ -36,138 +58,207 @@ include = [ "/tests/**", ] -[[bench]] -name = "bench" -harness = false -path = "benches/bench.rs" +# ----------------------------------------------------------------------------- +# Library Information +# ----------------------------------------------------------------------------- + +# The library file that contains the main logic for the binary. +[lib] +name = "ssg" # Internal name of the library +path = "src/lib.rs" # Path to the library entry point + +# The main file that contains the entry point for the binary. +[[bin]] +name = "ssg" # Name of the binary +path = "src/main.rs" # Path to the binary entry point + +# ----------------------------------------------------------------------------- +# Features +# ----------------------------------------------------------------------------- +[features] +# Optional features that can be enabled or disabled. +default = ["async", "tokio"] # No default features enabled +async = ["tokio"] # This enables tokio when the async feature is enabled +cli = [] # Enable command-line interface support + +# ----------------------------------------------------------------------------- +# Build Dependencies +# ----------------------------------------------------------------------------- +[build-dependencies] +# Dependencies for the build script, used for pre-compilation tasks. +version_check = "0.9" # Ensures that a compatible Rust version is used + +# ----------------------------------------------------------------------------- +# Development Dependencies +# ----------------------------------------------------------------------------- +[dev-dependencies] +# Dependencies required for testing and development. +criterion = "0.5" # Benchmarking library to test performance + +# ----------------------------------------------------------------------------- +# Dependencies +# ----------------------------------------------------------------------------- [dependencies] -clap = "4.5.18" -comrak = "0.24.1" -dtt = "0.0.6" -env_logger = "0.11.5" -lazy_static = "1.5.0" +# Required dependencies for building and running the project. +anyhow = "1.0.93" +clap = { version = "4.5.20", features = ["derive", "cargo", "env"] } +dtt = "0.0.8" +http-handle = "0.0.2" +langweave = "0.0.1" log = { version = "0.4.22", features = ["std"] } -minify-html = "0.15.0" -pdf_composer = "0.2.71" -pulldown-cmark = "0.12.1" -quick-xml = "0.36.1" -regex = "1.10.6" -reqwest = { version = "0.12.7", features = ["blocking", "json"] } -rlg = "0.0.4" -serde = { version = "1.0.209", features = ["derive"] } -serde_json = "1.0.128" -tempfile = "3.12.0" +rayon = "1.10.0" +rlg = "0.0.6" +serde = { version = "1.0.214", features = ["derive"] } +serde_json = "1.0.132" +staticdatagen = "0.0.1" +tempfile = "3.14.0" +thiserror = "2.0.3" toml = "0.8.19" -uuid = { version = "1.10.0", features = ["v4"] } -vrd = "0.0.7" -yaml-rust = "0.4.5" +tokio = { version = "1.41.1", features = ["full"], optional = true } +url = "2.5.3" +uuid = { version = "1.11.0", features = ["v4"] } -# Unix platforms use OpenSSL for now to provide SSL functionality +# Platform-specific dependency for Unix with OpenSSL [target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] -openssl = { version = "0.10.66", features = ["vendored"] } +openssl = { version = "0.10", features = ["vendored"] } -[dev-dependencies] -assert_cmd = "2.0.16" -criterion = "0.5.1" - -[lib] -crate-type = ["lib"] -name = "ssg" -path = "src/lib.rs" - -[features] -default = [] +# ----------------------------------------------------------------------------- +# Criterion Benchmark +# ----------------------------------------------------------------------------- +[[bench]] +# Benchmarking configuration for performance testing. +name = "bench" # Name of the benchmark +harness = false # Disable the default benchmark harness (used by Criterion) +# ----------------------------------------------------------------------------- +# Documentation Configuration +# ----------------------------------------------------------------------------- [package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] -rustdoc-args = ["--generate-link-to-definition"] +# Settings for building and hosting documentation on docs.rs. +all-features = true # Build documentation with all features enabled +rustdoc-args = ["--cfg", "docsrs"] # Arguments passed to `rustdoc` when building the documentation +targets = ["x86_64-unknown-linux-gnu"] # Default target platform for the docs -# Linting config +# ----------------------------------------------------------------------------- +# Linting Configuration +# ----------------------------------------------------------------------------- [lints.rust] +# Linting rules for the project. + +## Warnings +missing_copy_implementations = "warn" # Warn if types can implement `Copy` but don’t +missing_docs = "warn" # Warn if public items lack documentation +unstable_features = "warn" # Warn on the usage of unstable features +unused_extern_crates = "warn" # Warn about unused external crates +unused_results = "warn" # Warn if a result type is unused (e.g., errors ignored) + +## Allowances +bare_trait_objects = "allow" # Allow bare trait objects (e.g., `Box`) +elided_lifetimes_in_paths = "allow" # Allow lifetimes to be elided in paths +non_camel_case_types = "allow" # Allow non-camel-case types +non_upper_case_globals = "allow" # Allow non-uppercase global variables +trivial_bounds = "allow" # Allow trivial bounds in trait definitions +unsafe_code = "allow" # Allow the usage of unsafe code blocks -## Warn -# box_pointers = "warn" -missing_copy_implementations = "warn" -missing_docs = "warn" -unstable_features = "warn" -# unused_crate_dependencies = "warn" -unused_extern_crates = "warn" -# unused_results = "warn" - -## Allow -bare_trait_objects = "allow" -elided_lifetimes_in_paths = "allow" -non_camel_case_types = "allow" -non_upper_case_globals = "allow" -trivial_bounds = "allow" -unsafe_code = "allow" - -## Forbid -missing_debug_implementations = "forbid" -non_ascii_idents = "forbid" -unreachable_pub = "forbid" - -## Deny -dead_code = "deny" -deprecated_in_future = "deny" -ellipsis_inclusive_range_patterns = "deny" -explicit_outlives_requirements = "deny" -future_incompatible = { level = "deny", priority = -1 } -keyword_idents = "deny" -macro_use_extern_crate = "deny" -meta_variable_misuse = "deny" -missing_fragment_specifier = "deny" -noop_method_call = "deny" -pointer_structural_match = "deny" -rust_2018_idioms = { level = "deny", priority = -1 } -rust_2021_compatibility = { level = "deny", priority = -1 } -unused = { level = "deny", priority = -1 } -single_use_lifetimes = "deny" -trivial_casts = "deny" -trivial_numeric_casts = "deny" -unused_features = "deny" -unused_import_braces = "deny" -unused_labels = "deny" -unused_lifetimes = "deny" -unused_macro_rules = "deny" -unused_qualifications = "deny" -variant_size_differences = "deny" +## Forbidden +missing_debug_implementations = "forbid" # Forbid missing `Debug` implementations +non_ascii_idents = "forbid" # Forbid non-ASCII identifiers +unreachable_pub = "forbid" # Forbid unreachable `pub` items +## Denials +dead_code = "deny" # Deny unused, dead code in the project +deprecated_in_future = "deny" # Deny code that will be deprecated in the future +ellipsis_inclusive_range_patterns = "deny" # Deny usage of inclusive ranges in match patterns (`...`) +explicit_outlives_requirements = "deny" # Deny unnecessary lifetime outlives requirements +future_incompatible = { level = "deny", priority = -1 } # Handle future compatibility issues +keyword_idents = { level = "deny", priority = -1 } # Deny usage of keywords as identifiers +macro_use_extern_crate = "deny" # Deny macro use of `extern crate` +meta_variable_misuse = "deny" # Deny misuse of meta variables in macros +missing_fragment_specifier = "deny" # Deny missing fragment specifiers in macros +noop_method_call = "deny" # Deny method calls that have no effect +rust_2018_idioms = { level = "deny", priority = -1 } # Enforce Rust 2018 idioms +rust_2021_compatibility = { level = "deny", priority = -1 } # Enforce Rust 2021 compatibility +single_use_lifetimes = "deny" # Deny lifetimes that are used only once +trivial_casts = "deny" # Deny trivial casts (e.g., `as` when unnecessary) +trivial_numeric_casts = "deny" # Deny trivial numeric casts (e.g., `i32` to `i64`) +unused = { level = "deny", priority = -1 } # Deny unused code, variables, etc. +unused_features = "deny" # Deny unused features +unused_import_braces = "deny" # Deny unnecessary braces around imports +unused_labels = "deny" # Deny unused labels in loops +unused_lifetimes = "deny" # Deny unused lifetimes +unused_macro_rules = "deny" # Deny unused macros +unused_qualifications = "deny" # Deny unnecessary type qualifications +variant_size_differences = "deny" # Deny enum variants with significant size differences + +# ----------------------------------------------------------------------------- +# Clippy Configuration +# ----------------------------------------------------------------------------- [package.metadata.clippy] -warn-lints = ["clippy::all", "clippy::pedantic", "clippy::cargo", "clippy::nursery"] +# Clippy lint configuration for enhanced code analysis. +warn-lints = [ + "clippy::all", # Enable all common Clippy lints + "clippy::pedantic", # Enable pedantic lints for stricter checking + "clippy::cargo", # Enable lints specific to cargo + "clippy::nursery", # Enable experimental lints from Clippy’s nursery + "clippy::complexity", # Warn on code complexity and suggest improvements + "clippy::correctness", # Ensure code correctness, flagging potential issues + "clippy::perf", # Lints that catch performance issues + "clippy::style", # Suggest stylistic improvements + "clippy::suspicious", # Detect suspicious code patterns + "clippy::module_name_repetitions", # Avoid repeating module names in the crate name +] + +# Customize Clippy to allow certain less critical lints. +allow-lints = [ + "clippy::module_inception", # Allow modules with the same name as their parents + "clippy::too_many_arguments", # Allow functions with more than 7 arguments if justified + "clippy::missing_docs_in_private_items", # Skip requiring documentation for private items +] + +# Enforce specific warnings and errors more strictly. +deny-lints = [ + "clippy::unwrap_used", # Deny the use of unwrap to ensure error handling + "clippy::expect_used", # Deny the use of expect to avoid improper error handling +] +# ----------------------------------------------------------------------------- +# Profiles +# ----------------------------------------------------------------------------- [profile.dev] -codegen-units = 256 -debug = true -debug-assertions = true -incremental = true -lto = false -opt-level = 0 -overflow-checks = true -panic = 'unwind' -rpath = false -strip = false +# Development profile configuration for fast builds and debugging. +codegen-units = 256 # Increase codegen units for faster compilation +debug = true # Enable debugging symbols +debug-assertions = true # Enable debug assertions +incremental = true # Enable incremental compilation +lto = false # Disable link-time optimization for development +opt-level = 0 # No optimizations in development +overflow-checks = true # Enable overflow checks for arithmetic operations +panic = 'unwind' # Enable unwinding for panics (useful in development) +rpath = false # Disable rpath generation +strip = false # Do not strip symbols in development builds [profile.release] -codegen-units = 1 -debug = false -debug-assertions = false -incremental = false -lto = true -opt-level = "s" -overflow-checks = false -panic = "abort" -rpath = false -strip = "symbols" +# Release profile configuration for optimized builds. +codegen-units = 1 # Reduce codegen units for better performance +debug = false # Disable debug symbols in release builds +debug-assertions = false # Disable debug assertions +incremental = false # Disable incremental compilation for optimal binary size +lto = true # Enable link-time optimization for smaller and faster binaries +opt-level = "z" # Optimize for binary size +overflow-checks = false # Disable overflow checks for performance +panic = "abort" # Use abort on panic for minimal overhead +rpath = false # Disable rpath generation +strip = "symbols" # Strip symbols for smaller binary size [profile.test] -codegen-units = 256 -debug = true -debug-assertions = true -incremental = true -lto = false -opt-level = 0 -overflow-checks = true -rpath = false -strip = false +# Test profile configuration for debugging and development. +codegen-units = 256 # Increase codegen units for faster test builds +debug = true # Enable debugging symbols for test builds +debug-assertions = true # Enable debug assertions for tests +incremental = true # Enable incremental compilation for tests +lto = false # Disable link-time optimization during testing +opt-level = 0 # No optimizations in test builds +overflow-checks = true # Enable overflow checks for tests +rpath = false # Disable rpath generation +strip = false # Do not strip symbols in test builds diff --git a/EXAMPLES.md b/EXAMPLES.md deleted file mode 100644 index dc9dd3a1..00000000 --- a/EXAMPLES.md +++ /dev/null @@ -1,81 +0,0 @@ -# Shokunin Static Site Generator (SSG) - -The fastest Rust-based Static Site Generator (SSG) for building professional -websites and blogs. - - - -Logo of the Shokunin Static Site Generator - - - -*Part of the [Mini Functions][0] family of Rust libraries.* - -## Example sites - -Shokunin empowers building fast, customizable sites through flexible templating. - -The table below showcases diverse websites built with Shokunin, demonstrating -its versatility for: - -- Blogs and project documentation -- Company websites and personal portfolios -- Even advanced functionalities like dashboards and custom browsing systems. - -Get inspired by these examples and explore the potential of Shokunin for your -next project. Each site leverages Shokunin's ability to process metadata and -generate optimized static assets. - -Browse the source code for a deeper dive into -their implementations. - -| # | Website | Source Code| -|:---|:---|:---:| -| 01 | [https://akande.co/][00] | [Àkàndé Voice Assistant Website][A] | -| 02 | [https://audioanalyser.co/][01] | [Audio Analyser Website][B] | -| 03 | [https://bankingonquantum.com/][02] | [Banking on Quantum Website][C] | -| 04 | [https://bankstatementparser.com/][03] | [Bank Statement Parser Website][D] | -| 05 | [https://dttlib.com/][04] | [DttLib Website][E] | -| 06 | [https://hshlib.one/][05] | [HshLib Website][F] | -| 07 | [https://kaishi.one/][06] | [Kaishi Website][G] | -| 08 | [https://kyberlib.com/][07] | [KyberLib Website][H] | -| 09 | [https://l90s.com/][08] | [L90s Website][I] | -| 10 | [https://pain001.com/][09] | [Pain001 Website][J] | -| 11 | [https://sebastienrousseau.com/][10] | [Sébastien Rousseau Website][K] | -| 12 | [https://shokunin.one/][11] | [Shokunin Website][L] | - - -[0]: https://minifunctions.com/ "The Rust Mini Functions" - -[00]: https://akande.co/ -[01]: https://audioanalyser.co/ -[02]: https://bankingonquantum.com/ -[03]: https://bankstatementparser.com/ -[04]: https://dttlib.com/ -[05]: https://hshlib.one/ -[06]: https://kaishi.one/ -[07]: https://kyberlib.com/ -[08]: https://l90s.com/ -[09]: https://pain001.com/ -[10]: https://sebastienrousseau.com/ -[11]: https://shokunin.one/ - -[A]: https://github.com/sebastienrousseau/akande.github.io "Source Code for the Website of Àkàndé Voice Assistant" -[B]: https://github.com/sebastienrousseau/audioanalyser.github.io "Source Code for the Website of Audio Analyser" -[C]: https://github.com/sebastienrousseau/bankingonquantum.github.io "Source Code for the Website of Banking on Quantum" -[D]: https://github.com/sebastienrousseau/bankstatementparser.github.io "Source Code for the Website of Bank Statement Parser" -[E]: https://github.com/sebastienrousseau/dttlib.github.io "Source Code for the Website of DttLib" -[F]: https://github.com/sebastienrousseau/hshlib.github.io "Source Code for the Website of HshLib" -[G]: https://github.com/sebastienrousseau/kaishi.github.io "Source Code for the Website of Kaishi" -[H]: https://github.com/sebastienrousseau/kyberlib.github.io "Source Code for the Website of KyberLib" -[I]: https://github.com/sebastienrousseau/l90s.github.io "Source Code for the Website of L90s" -[J]: https://github.com/sebastienrousseau/pain001.github.io "Source Code for the Website of Pain001" -[K]: https://github.com/sebastienrousseau/sebastienrousseau.github.io "Source Code for the Website of Sébastien Rousseau" -[L]: https://github.com/sebastienrousseau/shokunin.github.io "Source Code for the Website of Shokunin" diff --git a/Makefile b/Makefile new file mode 100755 index 00000000..e2d245e1 --- /dev/null +++ b/Makefile @@ -0,0 +1,86 @@ +# Makefile using cargo for managing builds and dependencies in a Rust project. + +# Default target executed when no arguments are given to make. +.PHONY: all +all: help ## Display this help. + +# Build the project including all workspace members. +.PHONY: build +build: ## Build the project. + @echo "Building all project components..." + @cargo build --all + +# Lint the project with stringent rules using Clippy, install Clippy if not present. +.PHONY: lint +lint: ensure-clippy ## Lint the project with Clippy. + @echo "Linting with Clippy..." + @cargo clippy --all-features --all-targets --all -- \ + --deny clippy::dbg_macro --deny clippy::unimplemented --deny clippy::todo --deny warnings \ + --deny missing_docs --deny broken_intra_doc_links --forbid unused_must_use --deny clippy::result_unit_err + +# Run all unit and integration tests in the project. +.PHONY: test +test: ## Run tests for the project. + @echo "Running tests..." + @cargo test + +# Check the project for errors without producing outputs. +.PHONY: check +check: ## Check the project for errors without producing outputs. + @echo "Checking code formatting..." + @cargo check + +# Format all code in the project according to rustfmt's standards, install rustfmt if not present. +.PHONY: format +format: ensure-rustfmt ## Format the code. + @echo "Formatting all project components..." + @cargo fmt --all + +# Check code formatting without making changes, with verbose output, install rustfmt if not present. +.PHONY: format-check-verbose +format-check-verbose: ensure-rustfmt ## Check code formatting with verbose output. + @echo "Checking code format with verbose output..." + @cargo fmt --all -- --check --verbose + +# Apply fixes to the project using cargo fix, install cargo-fix if necessary. +.PHONY: fix +fix: ensure-cargo-fix ## Automatically fix Rust lint warnings using cargo fix. + @echo "Applying cargo fix..." + @cargo fix --all + +# Use cargo-deny to check for security vulnerabilities, licensing issues, and more, install if not present. +.PHONY: deny +deny: ensure-cargo-deny ## Run cargo deny checks. + @echo "Running cargo deny checks..." + @cargo deny check + +# Check for outdated dependencies only for the root package, install cargo-outdated if necessary. +.PHONY: outdated +outdated: ensure-cargo-outdated ## Check for outdated dependencies for the root package only. + @echo "Checking for outdated dependencies..." + @cargo outdated --root-deps-only + +# Installation checks and setups +.PHONY: ensure-clippy ensure-rustfmt ensure-cargo-fix ensure-cargo-deny ensure-cargo-outdated +ensure-clippy: + @cargo clippy --version || rustup component add clippy + +ensure-rustfmt: + @cargo fmt --version || rustup component add rustfmt + +ensure-cargo-fix: + @cargo fix --version || rustup component add rustfix + +ensure-cargo-deny: + @command -v cargo-deny || cargo install cargo-deny + +ensure-cargo-outdated: + @command -v cargo-outdated || cargo install cargo-outdated + +# Help target to display callable targets and their descriptions. +.PHONY: help +help: ## Display this help. + @echo "Usage: make [target]..." + @echo "" + @echo "Targets:" + @awk 'BEGIN {FS = ":.*?##"} /^[a-zA-Z_-]+:.*?##/ {printf " %-30s %s\n", $$1, $$2}' $(MAKEFILE_LIST) diff --git a/README.md b/README.md index 299e95b6..3e6836f8 100644 --- a/README.md +++ b/README.md @@ -1,311 +1,166 @@ -# Shokunin Static Site Generator (SSG) - -The fastest Rust-based Static Site Generator (SSG) for building professional -websites and blogs. - - -Logo of the Shokunin Static Site Generator - + -*Part of the [Mini Functions][0] family of Rust libraries.* +# `Shokunin Static Site Generator (SSG)` + +A Content-First Open Source Static Site Generator (SSG) crafted in Rust.
-![Banner of the Shokunin Static Site Generator][banner] - -[![Made With Rust][made-with-rust-badge]][14] -[![Crates.io][crates-badge]][8] -[![Lib.rs][libs-badge]][10] -[![Docs.rs][docs-badge]][9] -[![License][license-badge]][3] -[![Codecov][codecov-badge]][15] +[![Made With Love][made-with-rust]][08] [![Crates.io][crates-badge]][03] [![lib.rs][libs-badge]][01] [![Docs.rs][docs-badge]][04] [![Codecov][codecov-badge]][06] [![Build Status][build-badge]][07] [![GitHub][github-badge]][09] -• [Website][1] -• [Documentation][9] -• [Report Bug][4] -• [Request Feature][4] -• [Contributing Guidelines][5] +• [Website][00] • [Documentation][04] • [Report Bug][02] • [Request Feature][02] • [Contributing Guidelines][05]
-![divider][divider] - ## Overview -Shokunin is a lightning-fast static site generator (SSG) that is optimised for -Search Engine Optimisation (SEO) and fully aligned with Accessibility Standards. - -The library extracts metadata and content to generate static HTML files from -Markdown, YAML, JSON, and TOML. It also supports HTML themes and custom -templates to help you create high quality websites with ease. +Shokunin is a lightning-fast static site generator (SSG) optimised for search engine visibility (SEO) and compliant with WCAG 2.1 Level AA accessibility standards. ## Features -Shokunin Static Site Generator (SSG) feature highlights include: - -- Blazing fast and flexible static site generator written in Rust -- Built-in support for [GitHub Flavoured Markdown][12] (GFM) -- Built-in support for Google Analytics and Bing Analytics -- Experimental support for PDF generation -- Compatible with various HTML themes and premium templates -- Generates Atom and RSS feeds for your blog posts automatically -- Generates minified HTML for optimal performance and SEO -- Includes a built-in Rust development server for local testing -- Supports multiple content formats: - - Markdown - - YAML - - JSON - - TOML - - XML -- Built-in generation for: - - Sitemaps - - robots.txt - - Canonical name (CNAME) records - - Custom 404 pages -- Comprehensive documentation - -## Table of Contents - -- [Shokunin Static Site Generator (SSG)](#shokunin-static-site-generator-ssg) - - [Overview](#overview) - - [Features](#features) - - [Table of Contents](#table-of-contents) - - [Getting Started](#getting-started) - - [Installation](#installation) - - [Requirements](#requirements) - - [Platform support](#platform-support) - - [Documentation](#documentation) - - [Usage](#usage) - - [Command Line Interface (CLI)](#command-line-interface-cli) - - [Arguments](#arguments) - - [In your project](#in-your-project) - - [Examples](#examples) - - [Args](#args) - - [Semantic Versioning Policy](#semantic-versioning-policy) - - [License](#license) - - [Contribution](#contribution) - - [Acknowledgements](#acknowledgements) - -## Getting Started - -It takes just a few minutes to get up and running with Shokunin Static Site -Generator (SSG). - -### Installation - -To install Shokunin Static Site Generator (SSG), you need to have the Rust -toolchain installed on your machine. You can install the Rust toolchain by -following the instructions on the [Rust website][14]. - -Once you have the Rust toolchain installed, you can install Shokunin Static Site -Generator (SSG) using the following command: - -```shell -cargo install ssg -``` - -For simplicity, we have given Shokunin Static Site Generator (SSG) a simple -alias `ssg` which can stand for `Shokunin Site Generator` or -`Static Site Generator`. - -You can then run the help command to see the available options and commands: - -```shell -ssg --help -``` - -### Requirements - -The minimum supported Rust toolchain version is currently Rust **1.71.1** or -later (stable). It is recommended that you install the latest stable version of -Rust. - -### Platform support - -Shokunin Static Site Generator (SSG) is supported and tested on the following -platforms and architectures as part of our [CI/CD pipeline][11]. - -The [GitHub Actions][11] shows the platforms in which the Shokunin Static Site -Generator (SSG) library tests are run. +- **⚡ Blazing Fast Performance**: Built in Rust for optimal speed and efficiency +- **📱 SEO Optimised**: Built-in features for maximum search engine visibility +- **🛠️ Multiple Content Formats**: Support for Markdown, YAML, JSON, and TOML +- **📊 Analytics Ready**: Built-in support for Google Analytics and Bing Analytics +- **🔄 Automated Feeds**: Automatic generation of Atom and RSS feeds +- **🎨 Flexible Theming**: Compatible with custom HTML themes and templates +- **📱 Development Server**: Built-in Rust server for local testing -### Documentation +### Accessibility Compliance -> ℹ️ **Info:** Please check out our [website][1] for more information. -You can find our documentation on [docs.rs][9], [lib.rs][10] and [crates.io][8]. +Shokunin generates sites that meet Web Content Accessibility Guidelines (WCAG) standards: -## Usage +- **WCAG 2.1 Level AA** compliance +- Accessible Rich Internet Applications (ARIA) support +- Semantic HTML structure +- Keyboard navigation support +- Screen reader compatibility +- Sufficient color contrast +- Responsive text scaling +- Alternative text for images +- Clear document structure +- Focus management -### Command Line Interface (CLI) +These accessibility features are automatically implemented in generated sites through: -The Shokunin Static Site Generator (SSG) library runs in a Terminal window and -can be used to easily generate a static website. To get started, run: +- Semantic HTML templates +- ARIA landmark roles +- Proper heading hierarchy +- Skip navigation links +- Form input labels +- Keyboard focus indicators +- Color contrast validation -```shell -ssg --new=docs --content=content --template=template --output=output --serve=public -``` +## Installation -or +Add Shokunin to your Rust project: -```shell -ssg -n=docs -c=content -t=template -o=output -s=public +```toml +# Cargo.toml +[dependencies] +shokunin = "0.0.30" ``` -This creates a new website in a directory called `docs` using the markdown content from the `content` directory and the HTML templates from the `template` directory. The static and compiled HTML files and artefacts are then generated in a `docs` folder. - -Shokunin is ideal for hosting your site on GitHub Pages. Simply commit and push the `docs` folder to your main branch, and set the GitHub Pages publishing source to point to that folder. +Basic implementation: -During development, you can use the `--serve` or `--s` option to start a local development server to preview content changes. +```rust +use staticdatagen::compiler::service::compile; +use std::path::Path; -With Shokunin's GFM and theme support, you can focus on writing markdown content while the SSG handles delivering a fast, SEO-friendly site. +fn main() -> Result<(), Box> { + // Define the paths to the build, site, content and template directories. + let build_path = Path::new("build"); + let content_path = Path::new("content"); + let site_path = Path::new("public"); + let template_path = Path::new("templates"); -#### Arguments + compile(build_path, content_path, site_path, template_path)?; -- `-n`, `--new`: The name of the folder for your new website. (required) -- `-c`, `--content`: The directory containing the website markdown content. (required) -- `-t`, `--template`: The directory containing the HTML website templates. - (required) -- `-o`, `--output`: The directory where the generated website files will be - saved temporarily. (required) -- `-s`, `--serve`: Run the development server. (optional). The directory from - which the website will be served. (optional) + Ok(()) +} +``` -### In your project +### Usage -To use the Shokunin Static Site Generator (SSG) library in your project, add the -following to your `Cargo.toml` file: +Create a new static site: -```toml -[dependencies] -shokunin = "0.0.29" +```bash +ssg --new=docs \ + --content=content \ + --template=templates \ + --output=output \ + --serve=public ``` -Add the following to your `main.rs` file: +Or use the short form: -```rust -extern crate ssg; -use ssg::*; +```bash +ssg -n=docs -c=content -t=templates -o=output -s=public ``` -then you can use the Shokunin Static Site Generator (SSG) functions in your -application code. +### Command-Line Options -### Examples +| Option | Short | Description | Required | +|--------|-------|-------------|----------| +| `--new` | `-n` | New site directory name | Yes | +| `--content` | `-c` | Content directory path | Yes | +| `--template` | `-t` | Template directory path | Yes | +| `--output` | `-o` | Output directory path | Yes | +| `--serve` | `-s` | Development server directory | No | -To get started with Shokunin Static Site Generator (SSG), you can use the -examples provided in the `examples` directory of the project. +## Documentation -To run the examples, clone the repository and run the following command in your -terminal from the project root directory. +For full API documentation, please visit [https://docs.rs/crate/ssg/](https://docs.rs/crate/ssg/). -```shell -cargo run --example example -``` +## Examples -The command will generate a static website based on the configuration details -in the `examples` directory. +To explore more examples, clone the repository and run the following command: ```shell -use ssg::compiler::compile; -use std::path::Path; - -fn main() -> Result<(), Box> { - // Define the paths to the build, site, content and template directories. - let build_path = Path::new("examples/example.com/build"); - let content_path = Path::new("examples/example.com/content"); - let site_path = Path::new("examples/example.com/public"); - let template_path = Path::new("examples/example.com/template"); - - compile(build_path, content_path, site_path, template_path)?; - - Ok(()) -} +cargo run --example example_name ``` -The main() function in this code compiles a website from the `content` -directory, using the `template` directory to generate the website files. The -compiled website is saved in the `build` directory and served directly from -the `example.com` directory. +## Contributing -#### Args - -- `build_path:` The path to the directory where the compiled website will be -saved. -- `content_path:` The path to the directory containing the website content. -- `site_path:` The path to the directory where the generated website files will -be served from. -- `template_path:` The path to the directory containing the website templates. - -## Semantic Versioning Policy - -For transparency into our release cycle and in striving to maintain backward -compatibility, Shokunin Static Site Generator (SSG) follows -[semantic versioning][7]. +Contributions are welcome! Please feel free to submit a Pull Request. ## License -The project is licensed under the terms of both the MIT license and the Apache -License (Version 2.0). - -- [Apache License, Version 2.0][2] -- [MIT license][3] - -## Contribution - -We welcome all people who want to contribute. Please see the -[contributing instructions][5] for more information. +This project is licensed under either of -Contributions in any form (issues, pull requests, etc.) to this project must -adhere to the [Rust's Code of Conduct][16]. +- [Apache License, Version 2.0][10] +- [MIT license][11] -Unless you explicitly state otherwise, any contribution intentionally submitted -for inclusion in the work by you, as defined in the Apache-2.0 license, shall -be dual licensed as above, without any additional terms or conditions. +at your option. ## Acknowledgements -A big thank you to all the awesome contributors of [Shokunin][6] for their help -and support. - -A special thank you goes to the [Rust Reddit][13] community for providing a lot -of useful suggestions on how to improve this project. - -[0]: https://minifunctions.com/ "The Rust Mini Functions" -[1]: https://shokunin.one "Shokunin Static Site Generator" -[2]: https://opensource.org/license/apache-2-0/ "Apache License, Version 2.0" -[3]: http://opensource.org/licenses/MIT "MIT license" -[4]: https://github.com/sebastienrousseau/shokunin/issues "Issues" -[5]: https://github.com/sebastienrousseau/shokunin/blob/main/CONTRIBUTING.md "Contributing" -[6]: https://github.com/sebastienrousseau/shokunin/graphs/contributors "Contributors" -[7]: http://semver.org/ "Semantic Versioning" -[8]: https://crates.io/crates/ssg "Crate.io" -[9]: https://docs.rs/crate/ssg/ "Docs.rs" -[10]: https://lib.rs/crates/ssg "Lib.rs" -[11]: https://github.com/sebastienrousseau/shokunin/actions "Actions" -[12]: https://github.github.com/gfm/ "GitHub Flavoured Markdown" -[13]: https://www.reddit.com/r/rust/ "Rust Reddit" -[14]: https://www.rust-lang.org/learn/get-started "Rust" -[15]: https://codecov.io/github/sebastienrousseau/shokunin?branch=main "Codecov" -[16]: https://www.rust-lang.org/policies/code-of-conduct "Rust's Code of Conduct" - -[banner]: https://kura.pro/shokunin/images/titles/title-shokunin.svg "Banner of the Shokunin Static Site Generator" -[codecov-badge]: https://img.shields.io/codecov/c/github/sebastienrousseau/shokunin?style=for-the-badge&token=wAcpid8YEt 'Codecov' - -[crates-badge]: https://img.shields.io/crates/v/ssg.svg?style=for-the-badge 'Crates.io badge' -[divider]: https://kura.pro/common/images/elements/divider.svg "divider" -[docs-badge]: https://img.shields.io/docsrs/ssg.svg?style=for-the-badge 'Docs.rs badge' -[libs-badge]: https://img.shields.io/badge/lib.rs-v0.0.29-orange.svg?style=for-the-badge 'Lib.rs badge' -[license-badge]: https://img.shields.io/crates/l/ssg.svg?style=for-the-badge 'License badge' -[made-with-rust-badge]: https://img.shields.io/badge/rust-f04041?style=for-the-badge&labelColor=c0282d&logo=rust 'Made With Rust badge' +Special thanks to all contributors who have helped build the `ssg` library. + +[00]: https://shokunin.one +[01]: https://lib.rs/crates/ssg +[02]: https://github.com/sebastienrousseau/shokunin/issues +[03]: https://crates.io/crates/ssg +[04]: https://docs.rs/ssg +[05]: https://github.com/sebastienrousseau/shokunin/blob/main/CONTRIBUTING.md +[06]: https://codecov.io/gh/sebastienrousseau/shokunin +[07]: https://github.com/sebastienrousseau/shokunin/actions?query=branch%3Amain +[08]: https://www.rust-lang.org/ +[09]: https://github.com/sebastienrousseau/shokunin +[10]: https://www.apache.org/licenses/LICENSE-2.0 +[11]: https://opensource.org/licenses/MIT + +[build-badge]: https://img.shields.io/github/actions/workflow/status/sebastienrousseau/ssg/release.yml?branch=main&style=for-the-badge&logo=github +[codecov-badge]: https://img.shields.io/codecov/c/github/sebastienrousseau/shokunin?style=for-the-badge&token=wAcpid8YEt&logo=codecov +[crates-badge]: https://img.shields.io/crates/v/ssg.svg?style=for-the-badge&color=fc8d62&logo=rust +[docs-badge]: https://img.shields.io/badge/docs.rs-ssg-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs +[github-badge]: https://img.shields.io/badge/github-sebastienrousseau/ssg-8da0cb?style=for-the-badge&labelColor=555555&logo=github +[libs-badge]: https://img.shields.io/badge/lib.rs-v0.0.1-orange.svg?style=for-the-badge +[made-with-rust]: https://img.shields.io/badge/rust-f04041?style=for-the-badge&labelColor=c0282d&logo=rust diff --git a/TEMPLATE.md b/TEMPLATE.md index 2f5a1270..188f7781 100644 --- a/TEMPLATE.md +++ b/TEMPLATE.md @@ -1,57 +1,55 @@ + + + +# `Shokunin Static Site Generator (SSG)` -Logo of the Shokunin Static Site Generator +A Content-First Open Source Static Site Generator (SSG) crafted in Rust. + +
-# Shokunin Static Site Generator (SSG) v0.0.29 🦀 +[![Made With Love][made-with-rust]][08] [![Crates.io][crates-badge]][03] [![lib.rs][libs-badge]][01] [![Docs.rs][docs-badge]][04] [![Codecov][codecov-badge]][06] [![Build Status][build-badge]][07] [![GitHub][github-badge]][09] -The fastest Rust-based Static Site Generator (SSG) for building professional -websites and blogs. +• [Website][00] • [Documentation][04] • [Report Bug][02] • [Request Feature][02] • [Contributing Guidelines][05] -*Part of the [Mini Functions][0] family of Rust libraries.* + +
+ ## Overview -Shokunin is a lightning-fast static site generator (SSG) that is optimised for -Search Engine Optimisation (SEO) and fully aligned with Accessibility Standards. - -The library extracts metadata and content to generate static HTML files from -Markdown, YAML, JSON, and TOML. It also supports HTML themes and custom -templates to help you create high quality websites with ease. +Shokunin is a lightning-fast static site generator (SSG) optimised for search engine visibility (SEO) and compliant with WCAG 2.1 Level AA accessibility standards. ## Features -Shokunin Static Site Generator (SSG) feature highlights include: - -- Blazing fast and flexible static site generator written in Rust -- Built-in support for [GitHub Flavoured Markdown][01] (GFM) -- Built-in support for Google Analytics and Bing Analytics -- Experimental support for PDF generation -- Compatible with various HTML themes and premium templates -- Generates Atom and RSS feeds for your blog posts automatically -- Generates minified HTML for optimal performance and SEO -- Includes a built-in Rust development server for local testing -- Supports multiple content formats: - - Markdown - - YAML - - JSON - - TOML - - XML -- Built-in generation for: - - Sitemaps - - robots.txt - - Canonical name (CNAME) records - - Custom 404 pages -- Comprehensive documentation - -[0]: https://minifunctions.com/ "The Rust Mini Functions" -[01]: https://github.github.com/gfm/ "GitHub Flavoured Markdown" +- **⚡ Blazing Fast Performance**: Built in Rust for optimal speed and efficiency +- **📱 SEO Optimised**: Built-in features for maximum search engine visibility +- **🛠️ Multiple Content Formats**: Support for Markdown, YAML, JSON, and TOML +- **📊 Analytics Ready**: Built-in support for Google Analytics and Bing Analytics +- **🔄 Automated Feeds**: Automatic generation of Atom and RSS feeds +- **🎨 Flexible Theming**: Compatible with custom HTML themes and templates +- **📱 Development Server**: Built-in Rust server for local testing + +[00]: https://shokunin.one +[01]: https://lib.rs/crates/ssg +[02]: https://github.com/sebastienrousseau/shokunin/issues +[03]: https://crates.io/crates/ssg +[04]: https://docs.rs/ssg +[05]: https://github.com/sebastienrousseau/shokunin/blob/main/CONTRIBUTING.md +[06]: https://codecov.io/gh/sebastienrousseau/shokunin +[07]: https://github.com/sebastienrousseau/shokunin/actions?query=branch%3Amain +[08]: https://www.rust-lang.org/ +[09]: https://github.com/sebastienrousseau/shokunin + +[build-badge]: https://img.shields.io/github/actions/workflow/status/sebastienrousseau/ssg/release.yml?branch=main&style=for-the-badge&logo=github +[codecov-badge]: https://img.shields.io/codecov/c/github/sebastienrousseau/shokunin?style=for-the-badge&token=wAcpid8YEt&logo=codecov +[crates-badge]: https://img.shields.io/crates/v/ssg.svg?style=for-the-badge&color=fc8d62&logo=rust +[docs-badge]: https://img.shields.io/badge/docs.rs-ssg-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs +[github-badge]: https://img.shields.io/badge/github-sebastienrousseau/ssg-8da0cb?style=for-the-badge&labelColor=555555&logo=github +[libs-badge]: https://img.shields.io/badge/lib.rs-v0.0.1-orange.svg?style=for-the-badge +[made-with-rust]: https://img.shields.io/badge/rust-f04041?style=for-the-badge&labelColor=c0282d&logo=rust ## Changelog 📚 diff --git a/benches/bench.rs b/benches/bench.rs index 7162f156..d72580c8 100644 --- a/benches/bench.rs +++ b/benches/bench.rs @@ -8,14 +8,6 @@ use criterion::Criterion; /// This is a module for benchmarking file operations. mod bench_file; -/// This is a module for benchmarking frontmatter operations. -mod bench_frontmatter; -/// This is a module for benchmarking html operations. -mod bench_html; -/// This is a module for benchmarking json operations. -mod bench_json; -/// This is a module for benchmarking markdown operations. -mod bench_metatags; /// This is a module for benchmarking yaml operations. mod bench_utilities; @@ -28,11 +20,6 @@ criterion::criterion_group! { // Targets of the group. targets = bench_file::bench_file, - bench_frontmatter::bench_extract, - bench_frontmatter::bench_parse_yaml_document, - bench_html::bench_generate_html, - bench_json::bench_json, - bench_metatags::bench_metatags, bench_utilities::bench_utilities, } diff --git a/benches/bench_file.rs b/benches/bench_file.rs index 95775cf8..71234763 100644 --- a/benches/bench_file.rs +++ b/benches/bench_file.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT use criterion::{black_box, Criterion}; -use ssg::utilities::file::add; +use staticdatagen::utilities::file::add; use std::path::PathBuf; /// Runs a benchmark that measures the performance of the `add` function. @@ -13,13 +13,13 @@ use std::path::PathBuf; #[allow(dead_code)] pub(crate) fn bench_file(c: &mut Criterion) { let path = PathBuf::from("content"); - c.bench_function("add function", |b| { + let _ = c.bench_function("add function", |b| { b.iter(|| { let result = add(&path); if let Err(e) = result { eprintln!("Error: {}", e); } else { - black_box(result.unwrap()); + let _ = black_box(result.unwrap()); } }) }); diff --git a/benches/bench_frontmatter.rs b/benches/bench_frontmatter.rs deleted file mode 100644 index 44c372c4..00000000 --- a/benches/bench_frontmatter.rs +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright © 2023-2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use criterion::{black_box, Criterion}; -use ssg::modules::frontmatter::{extract, parse_yaml_document}; - -/// Benchmarks the extraction of frontmatter from a content string. -/// -/// This function sets up a benchmark for the `extract` function, which extracts -/// the frontmatter from a content string. The content string is hardcoded in -/// this function. -/// -/// # Arguments -/// -/// * `c` - A mutable reference to a `Criterion` instance, which is used to -/// define and run benchmarks. -/// -/// # Example -/// -/// ```rust -/// use criterion::{criterion_group, criterion_main, Criterion}; -/// use shokunin_static_site_generator::ssg::modules::frontmatter::bench_extract; -/// -/// let mut group = criterion_group::CriterionGroup::new(); -/// group.bench_function("extract", |b| bench_extract(b)); -/// criterion_main(group); -/// ``` -/// -/// # Panics -/// -/// This function does not panic. -#[allow(dead_code)] -pub(crate) fn bench_extract(c: &mut Criterion) { - let content = r#" - --- - title: "Test" - --- - "#; - - // Benchmarks the execution of the `extract` function. - // - // This function sets up a benchmark for the execution of the `extract` - // function, which extracts the frontmatter from a content string. The - // content string is hardcoded in this function. - // - // # Arguments - // - // * `b` - A mutable reference to a `Bencher` instance, which is used to - // define and run benchmarks. - // - // # Example - // - // ```rust - // use criterion::{criterion_group, criterion_main, Criterion}; - // use shokunin_static_site_generator::ssg::modules::frontmatter::bench_extract; - // - // let mut group = criterion_group::CriterionGroup::new(); - // group.bench_function("extract", |b| bench_extract(b)); - // criterion_main(group); - // ``` - // - // # Panics - // - // This function does not panic. - c.bench_function("extract", |b| { - b.iter(|| extract(black_box(content))) - }); -} - -/// Benchmarks the parsing of YAML document from a content string. -/// -/// This function sets up a benchmark for the `parse_yaml_document` function, -/// which parses a YAML document from a content string. The content string is -/// hardcoded in this function. -/// -/// # Arguments -/// -/// * `c` - A mutable reference to a `Criterion` instance, which is used to -/// define and run benchmarks. -/// -/// # Example -/// -/// ```rust -/// use criterion::{criterion_group, criterion_main, Criterion}; -/// use shokunin_static_site_generator::ssg::modules::frontmatter::bench_parse_yaml_document; -/// -/// let mut group = criterion_group::CriterionGroup::new(); -/// group.bench_function("parse_yaml_document", |b| bench_parse_yaml_document(b)); -/// criterion_main(group); -/// ``` -/// -/// # Panics -/// -/// This function does not panic. -#[allow(dead_code)] -pub(crate) fn bench_parse_yaml_document(c: &mut Criterion) { - let content = r#" - title: "Test" - "#; - - // Benchmarks the execution of the `parse_yaml_document` function. - // - // This function sets up a benchmark for the execution of the `parse_yaml_document` - // function, which parses a YAML document from a content string. The - // content string is hardcoded in this function. - // - // # Arguments - // - // * `b` - A mutable reference to a `Bencher` instance, which is used to - // define and run benchmarks. - // - // # Example - // - // ```rust - // use criterion::{criterion_group, criterion_main, Criterion}; - // use shokunin_static_site_generator::ssg::modules::frontmatter::bench_parse_yaml_document; - // - // fn main() { - // let mut group = criterion_group::CriterionGroup::new(); - // group.bench_function("parse_yaml_document", |b| bench_parse_yaml_document(b)); - // criterion_main(group); - // } - // ``` - // - // # Panics - // - // This function does not panic. - c.bench_function("parse_yaml_document", |b| { - b.iter(|| parse_yaml_document(black_box(content))) - }); -} diff --git a/benches/bench_html.rs b/benches/bench_html.rs deleted file mode 100644 index cdc7e058..00000000 --- a/benches/bench_html.rs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright © 2023-2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use criterion::{black_box, Criterion}; -use ssg::modules::html::generate_html; - -/// Benchmarks the generation of HTML content. -/// -/// This function sets up a benchmark for measuring the performance of the `generate_html` function. -/// It uses the `criterion` crate to run the benchmark and measure the time taken to generate HTML. -/// -/// # Arguments -/// -/// * `c` - A mutable reference to a `Criterion` instance, which is used to set up and run the benchmark. -/// -/// # Panics -/// -/// This function panics if the `generate_html` function returns an `Err` value, indicating that HTML generation failed. -#[allow(dead_code)] -pub(crate) fn bench_generate_html(c: &mut Criterion) { - // A sample content string for the HTML generation. - // - // This string is used as the content for the HTML page being generated. - let content = "## Hello, world!\n\nThis is a test."; - - // The title of the HTML page. - // - // This string is used as the title of the HTML page being generated. - let title = "My Page"; - - // The description of the HTML page. - // - // This string is used as the description of the HTML page being generated. - let description = "This is a test page"; - - // Optional JSON content to be included in the HTML page. - // - // This value is used as the optional JSON content for the HTML page being generated. - let json_content = Some("{\"name\": \"value\"}"); - - // Benchmarks the generation of HTML using the provided inputs. - // - // This function sets up a benchmark to measure the time taken to generate HTML using the given inputs. - // It uses the `black_box` function to ensure that the inputs are not optimized away by the compiler. - // - // # Arguments - // - // * `b` - A builder for the benchmark, which is used to configure the benchmark and specify the code to be benchmarked. - // - // # Returns - // - // This function does not return a value. It sets up a benchmark and measures the time taken to generate HTML. - c.bench_function("generate_html", |b| { - b.iter(|| { - let html = generate_html( - black_box(content), - black_box(title), - black_box(description), - black_box(json_content), - ); - match html { - Ok(_) => (), - Err(_) => panic!("HTML generation failed"), - } - let _ = black_box(html); - }) - }); -} diff --git a/benches/bench_json.rs b/benches/bench_json.rs deleted file mode 100644 index 84a49bb2..00000000 --- a/benches/bench_json.rs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright © 2023-2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use std::path::Path; - -use criterion::{black_box, Criterion}; -use ssg::models::data::{ - CnameData, HumansData, ManifestData, SiteMapData, TxtData, -}; -use ssg::modules::json::{cname, human, manifest, sitemap, txt}; - -#[allow(dead_code)] -pub(crate) fn bench_json(c: &mut Criterion) { - let manifest_data = ManifestData { - name: String::from("Test Name"), - short_name: String::from("Test Short Name"), - start_url: String::from("/"), - display: String::from("standalone"), - background_color: String::from("#000"), - description: String::from("Test Description"), - icons: Vec::new(), - orientation: String::from("portrait"), - scope: String::from("/"), - theme_color: String::from("#000"), - }; - - let txt_data = TxtData { - permalink: String::from("https://www.test.com"), - }; - - let humans_data = HumansData { - author: String::from("Test Author"), - author_website: String::from("https://www.test.com"), - author_twitter: String::from("Test Twitter"), - author_location: String::from("Test Location"), - thanks: String::from("Test Thanks"), - site_last_updated: String::from("2022-01-01"), - site_standards: String::from("Test Standards"), - site_components: String::from("Test Components"), - site_software: String::from("Test Software"), - }; - - let cname_data = CnameData { - cname: String::from("test.com"), - }; - - let sitemap_data = SiteMapData { - changefreq: String::from("always"), - loc: String::from("https://www.test.com"), - lastmod: String::from("2022-01-01"), - }; - - let dir = Path::new("./"); - - c.bench_function("manifest", |b| { - b.iter(|| manifest(black_box(&manifest_data))) - }); - - c.bench_function("txt", |b| b.iter(|| txt(black_box(&txt_data)))); - - c.bench_function("cname", |b| { - b.iter(|| cname(black_box(&cname_data))) - }); - - c.bench_function("humans_data", |b| { - b.iter(|| human(black_box(&humans_data))) - }); - - c.bench_function("sitemap", |b| { - b.iter(|| { - sitemap(black_box(sitemap_data.clone()), black_box(dir)) - }) - }); -} diff --git a/benches/bench_metatags.rs b/benches/bench_metatags.rs deleted file mode 100644 index 8f895d6d..00000000 --- a/benches/bench_metatags.rs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright © 2023-2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use criterion::{black_box, Criterion}; -use ssg::modules::metatags::generate_metatags; - -/** - * This function is used to benchmark the performance of generating metatags. - * It takes a reference to a mutable `Criterion` instance and generates metatags based on the provided `meta` vector. - * The `meta` vector contains tuples of tag names and their corresponding content. - * The function then asserts that the generated metatags contain the expected content for each tag. - * - * # Arguments - * - * * `c` - A mutable reference to a `Criterion` instance. - * - * # Returns - * - * This function does not return any value. It is a benchmarking function and its purpose is to measure the performance of generating metatags. - */ -#[allow(dead_code)] -pub(crate) fn bench_metatags(c: &mut Criterion) { - let meta = vec![ - ("description".to_owned(), "My web page".to_owned()), - ("author".to_owned(), "John Doe".to_owned()), - ( - "viewport".to_owned(), - "width=device-width, initial-scale=1.0".to_owned(), - ), - ("robots".to_owned(), "noindex, nofollow".to_owned()), - ]; - - c.bench_function("generate metatags", |b| { - b.iter(|| { - let result = generate_metatags(black_box(&meta)); - assert!(result.contains("")); - assert!(result.contains("")); - assert!(result.contains( - "" - )); - assert!(result.contains("")); - }) - }); -} diff --git a/benches/bench_utilities.rs b/benches/bench_utilities.rs index 0d03ce1c..6c82335e 100644 --- a/benches/bench_utilities.rs +++ b/benches/bench_utilities.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT use criterion::{black_box, Criterion}; -use ssg::utilities::directory::directory; +use staticdatagen::utilities::directory::directory; use tempfile::TempDir; /** @@ -20,7 +20,7 @@ pub(crate) fn bench_utilities(c: &mut Criterion) { let dir = tempdir.path().join("test_dir"); // Benchmarks the creation of a directory. - c.bench_function("create directory", |b| { + let _ = c.bench_function("create directory", |b| { b.iter(|| { // Creates a directory with the given path and name. let result = @@ -31,7 +31,7 @@ pub(crate) fn bench_utilities(c: &mut Criterion) { }); // Benchmarks checking if a directory exists. - c.bench_function("check if directory exists", |b| { + let _ = c.bench_function("check if directory exists", |b| { b.iter(|| { // Checks if the directory exists. let result = dir.exists(); @@ -41,17 +41,18 @@ pub(crate) fn bench_utilities(c: &mut Criterion) { }); // Benchmarks checking if a directory is a directory. - c.bench_function("check if directory is a directory", |b| { - b.iter(|| { - // Checks if the directory is a directory. - let result = dir.is_dir(); - // Asserts that the result is true, indicating that the directory is a directory. - assert!(result); - }) - }); + let _ = + c.bench_function("check if directory is a directory", |b| { + b.iter(|| { + // Checks if the directory is a directory. + let result = dir.is_dir(); + // Asserts that the result is true, indicating that the directory is a directory. + assert!(result); + }) + }); // Benchmarks checking if a non-existent directory does not exist. - c.bench_function( + let _ = c.bench_function( "check if non-existent directory does not exist", |b| { let non_existent_dir = tempdir.path().join("non-existent"); diff --git a/build.rs b/build.rs index 17275b2f..db9e681c 100644 --- a/build.rs +++ b/build.rs @@ -1,10 +1,56 @@ -// Copyright © 2023-2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -//! This is the main function for the build script. +//! This build script checks if the current Rustc version is at least the +//! minimum required version. +//! If the current Rustc version is less than the minimum required version, +//! the build script will exit the build process with a non-zero exit code. //! -//! Currently, it only instructs Cargo to re-run this build script if `build.rs` is changed. +//! The minimum required version is specified in the `min_version` variable. + +use std::process; + +/// Checks if the current Rustc version is at least the minimum required version +/// +/// # Arguments +/// +/// * `min_version` - The minimum required Rustc version as a string. +/// +/// # Returns +/// +/// * `Some(true)` - If the current Rustc version is at least the minimum +/// required version. +/// * `Some(false)` - If the current Rustc version is less than the minimum +/// required version. +/// * `None` - If the current Rustc version cannot be determined. +/// +/// # Errors +/// +/// This function will exit the build process with a non-zero exit code if the +/// current Rustc version is less than the minimum required version. +/// +/// # Examples +/// +/// ```rust +/// let min_version = "1.56"; +/// +/// match version_check::is_min_version(min_version) { +/// Some(true) => println!("Rustc version is at least {}", min_version), +/// Some(false) => { +/// eprintln!("Rustc version is less than {}", min_version); +/// process::exit(1); +/// } +/// None => { +/// eprintln!("Unable to determine Rustc version"); +/// process::exit(1); +/// } +/// } +/// ``` fn main() { - // Avoid unnecessary re-building. - println!("cargo:rerun-if-changed=build.rs"); + let min_version = "1.56"; + + match version_check::is_min_version(min_version) { + Some(true) => {} + _ => { + eprintln!("'fd' requires Rustc version >= {}", min_version); + process::exit(1); + } + } } diff --git a/content/404.md b/content/404.md index 3974380d..9036160f 100644 --- a/content/404.md +++ b/content/404.md @@ -49,7 +49,7 @@ viewport: "width=device-width, initial-scale=1, shrink-to-fit=no" atom_link: "https://kaishi.one/404/rss.xml" category: "Technology" docs: "https://validator.w3.org/feed/docs/rss2.html" -generator: "Shokunin SSG (version 0.0.29)" +generator: "Shokunin SSG (version 0.0.30)" item_description: "The page may have been removed or renamed. Please visit our homepage for more information." item_guid: "https://kaishi.one/404/rss.xml" item_link: "https://kaishi.one/404/rss.xml" diff --git a/content/contact.md b/content/contact.md index 51408e46..0e5e3260 100644 --- a/content/contact.md +++ b/content/contact.md @@ -61,7 +61,7 @@ news_title: "Contact Us" ## The title of the page. (max 64 characters) atom_link: https://kaishi.one/contact/rss.xml category: "Technology" docs: https://validator.w3.org/feed/docs/rss2.html -generator: "Shokunin SSG (version 0.0.29)" +generator: "Shokunin SSG (version 0.0.30)" item_description: RSS feed for the contact page of the Kaishi website. item_guid: https://kaishi.one/contact/rss.xml item_link: https://kaishi.one/contact/rss.xml diff --git a/content/features.md b/content/features.md index bcf7081d..ce3253b6 100644 --- a/content/features.md +++ b/content/features.md @@ -58,7 +58,7 @@ news_title: "Features" ## The title of the page. (max 64 characters) atom_link: https://kaishi.one/rss.xml category: "Technology" docs: https://validator.w3.org/feed/docs/rss2.html -generator: "Shokunin SSG (version 0.0.29)" +generator: "Shokunin SSG (version 0.0.30)" item_description: RSS feed for the site item_guid: https://kaishi.one/rss.xml item_link: https://kaishi.one/rss.xml diff --git a/content/index.md b/content/index.md index 201fb3ba..62ca875c 100644 --- a/content/index.md +++ b/content/index.md @@ -45,7 +45,6 @@ title: "Kaishi, a Shokunin Static Site Generator Starter Template" ## The title url: "https://kaishi.one" ## The url of the site. viewport: "width=device-width, initial-scale=1, shrink-to-fit=no" ## The viewport of the site. - # News - The News SiteMap front matter (YAML). news_genres: "Blog" ## The genres of the site. (PressRelease|Satire|Blog|OpEd|Opinion|UserGenerated) news_keywords: "kaishi, shokunin static site generator, static site generator, minimalist website template, modern website template, responsive website template, website starter template, freelance creative, startup founder, small business owner, online presence" ## The keywords of the site. (comma separated, max 10 keywords) @@ -61,7 +60,7 @@ news_title: "Kaishi, a Shokunin Static Site Generator Starter Template" ## The t atom_link: https://kaishi.one/rss.xml category: "Technology" docs: https://validator.w3.org/feed/docs/rss2.html -generator: "Shokunin SSG (version 0.0.29)" +generator: "Shokunin SSG (version 0.0.30)" item_description: RSS feed for the site item_guid: https://kaishi.one/rss.xml item_link: https://kaishi.one/rss.xml @@ -72,7 +71,7 @@ managing_editor: jane.doe@kaishi.one (Jane Doe) pub_date: "Tue, 20 Feb 2024 15:15:15 GMT" ttl: "60" type: "website" -webmaster: jane.doe@kaishi.one +webmaster: jane.doe@kaishi.one (Jane Doe) # Apple - The Apple front matter (YAML). apple_mobile_web_app_orientations: "portrait" ## The Apple mobile web app orientations of the page. @@ -116,6 +115,17 @@ site_standards: "HTML5, CSS3, RSS, Atom, JSON, XML, YAML, Markdown, TOML" ## The site_components: "Kaishi, Kaishi Builder, Kaishi CLI, Kaishi Templates, Kaishi Themes" ## The components of the site. site_software: "Shokunin, Rust" ## The software of the site. +# Security - The Security front matter (YAML). +security_contact: "mailto:jane.doe@kaishi.one" ## The contact of the page. +security_expires: "Tue, 20 Feb 2024 15:15:15 GMT" ## The expires of the page. +# Optional fields: +security_acknowledgments: "Thanks to the Rust team for their amazing work on Shokunin." ## The acknowledgments of the page. +security_languages: "en" ## The preferred languages of the page. +security_canonical: "https://kaishi.one" ## The canonical of the page. +security_policy: "https://kaishi.one/policy" ## The policy of the page. +security_hiring: "https://kaishi.one/hiring" ## The hiring of the page. +security_encryption: "https://kaishi.one/encryption" ## The encryption of the page. + --- ## Overview diff --git a/content/offline.md b/content/offline.md index 28b3083a..369ac3b7 100644 --- a/content/offline.md +++ b/content/offline.md @@ -60,7 +60,7 @@ news_title: "Offline" ## The title of the page. (max 64 characters) atom_link: https://kaishi.one/rss.xml category: "Technology" docs: https://validator.w3.org/feed/docs/rss2.html -generator: "Shokunin SSG (version 0.0.29)" +generator: "Shokunin SSG (version 0.0.30)" item_description: RSS feed for the site item_guid: https://kaishi.one/rss.xml item_link: https://kaishi.one/rss.xml diff --git a/content/post.md b/content/post.md index 17e0a9f3..8205b956 100644 --- a/content/post.md +++ b/content/post.md @@ -59,7 +59,7 @@ news_title: "Posts" ## The title of the page. (max 64 characters) atom_link: https://kaishi.one/rss.xml category: "Technology" docs: https://validator.w3.org/feed/docs/rss2.html -generator: "Shokunin SSG (version 0.0.29)" +generator: "Shokunin SSG (version 0.0.30)" item_description: RSS feed for the site item_guid: https://kaishi.one/rss.xml item_link: https://kaishi.one/rss.xml diff --git a/content/privacy.md b/content/privacy.md index 4f65733c..a3b760c3 100644 --- a/content/privacy.md +++ b/content/privacy.md @@ -60,7 +60,7 @@ news_title: "Privacy" ## The title of the page. (max 64 characters) atom_link: https://kaishi.one/rss.xml category: "Technology" docs: https://validator.w3.org/feed/docs/rss2.html -generator: "Shokunin SSG (version 0.0.29)" +generator: "Shokunin SSG (version 0.0.30)" item_description: RSS feed for the site item_guid: https://kaishi.one/rss.xml item_link: https://kaishi.one/rss.xml @@ -131,7 +131,7 @@ Google Analytics is a web analytics service offered by Google that tracks and re ### Microsoft Clarity -Microsoft Clarity is a user behavior analytics tool that helps us understand how users interact with our website. The data collected includes information such as mouse movements, clicks, and scrolls. For more information on the privacy practices of Microsoft, please visit the Microsoft Privacy Statement web page: [Microsoft Privacy Statement ⧉](https://privacy.microsoft.com/en-us/privacystatement) +Microsoft Clarity is a user behaviour analytics tool that helps us understand how users interact with our website. The data collected includes information such as mouse movements, clicks, and scrolls. For more information on the privacy practices of Microsoft, please visit the Microsoft Privacy Statement web page: [Microsoft Privacy Statement ⧉](https://privacy.microsoft.com/en-us/privacystatement) ## Your Rights diff --git a/content/tags.md b/content/tags.md index f4713509..000612e4 100644 --- a/content/tags.md +++ b/content/tags.md @@ -60,7 +60,7 @@ news_title: "Tags" ## The title of the page. (max 64 characters) atom_link: https://kaishi.one/rss.xml category: "Technology" docs: https://validator.w3.org/feed/docs/rss2.html -generator: "Shokunin SSG (version 0.0.29)" +generator: "Shokunin SSG (version 0.0.30)" item_description: RSS feed for the site item_guid: https://kaishi.one/rss.xml item_link: https://kaishi.one/rss.xml diff --git a/content/terms.md b/content/terms.md index 2ae97a3d..526fa1fa 100644 --- a/content/terms.md +++ b/content/terms.md @@ -60,7 +60,7 @@ news_title: "Terms" ## The title of the page. (max 64 characters) atom_link: https://kaishi.one/rss.xml category: "Technology" docs: https://validator.w3.org/feed/docs/rss2.html -generator: "Shokunin SSG (version 0.0.29)" +generator: "Shokunin SSG (version 0.0.30)" item_description: RSS feed for the site item_guid: https://kaishi.one/rss.xml item_link: https://kaishi.one/rss.xml diff --git a/deny.toml b/deny.toml index 26701187..b58b6035 100644 --- a/deny.toml +++ b/deny.toml @@ -1,71 +1,21 @@ [licenses] -# The lint level for crates which do not have a detectable license -unlicensed = "deny" +# List of allowed licenses for dependencies +allow = ["MIT", "Apache-2.0", "Unicode-DFS-2016"] -# List of explicitly allowed licenses -# See https://spdx.org/licenses/ for list of possible licenses -# [possible values: any SPDX 3.7 short identifier (+ optional exception)]. -allow = [ - "Apache-2.0", - "MIT", - "CC0-1.0", - "ISC", - "0BSD", - "BSD-2-Clause", - "BSD-3-Clause", - "Unlicense", - "Unicode-DFS-2016", -] - -# List of banned licenses [bans] -multiple-versions = "deny" - - -# The lint level for licenses considered copyleft -copyleft = "deny" - -# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses -# * both - The license will only be approved if it is both OSI-approved *AND* FSF/Free -# * either - The license will be approved if it is either OSI-approved *OR* FSF/Free -# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF/Free -# * fsf-only - The license will be approved if is FSF/Free *AND NOT* OSI-approved -# * neither - The license will be denied if is FSF/Free *OR* OSI-approved -allow-osi-fsf-free = "either" +# Lint level for when multiple versions of the same crate are detected +multiple-versions = "warn" -# The confidence threshold for detecting a license from license text. -# The higher the value, the more closely the license text must be to the -# canonical license text of a valid SPDX license file. -# [possible values: any between 0.0 and 1.0]. -confidence-threshold = 0.8 - -# The graph highlighting used when creating dotgraphs for crates -# with multiple versions -# * lowest-version - The path to the lowest versioned duplicate is highlighted -# * simplest-path - The path to the version with the fewest edges is highlighted -# * all - Both lowest-version and simplest-path are used +# The graph highlighting used when creating dotgraphs for crates with multiple versions +# Options: "lowest-version", "simplest-path", "all" highlight = "all" -# List of crates that are allowed. Use with care! -allow = [] - -# List of crates to deny -deny = [ - # Each entry the name of a crate and a version range. If version is - # not specified, all versions will be matched. -] - # Certain crates/versions that will be skipped when doing duplicate detection. skip = [] -# Similarly to `skip` allows you to skip certain crates during duplicate detection, -# unlike skip, it also includes the entire tree of transitive dependencies starting at -# the specified crate, up to a certain depth, which is by default infinite +# Similarly to `skip` allows you to skip certain crates during duplicate detection, unlike skip, it also includes the entire tree of transitive dependencies starting at the specified crate, up to a certain depth, which is by default infinite skip-tree = [] - [advisories] -notice = "deny" -unmaintained = "deny" -unsound = "deny" -vulnerability = "deny" +# List of advisory IDs to ignore +ignore = [] \ No newline at end of file diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 00000000..ede35c89 --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1,17 @@ +*.DS_Store +*.profraw +*.log +/.vscode/ +/docs/ +/examples/public/ +/mysite/ +/output/ +/pdfs/ +/public/ +/target/ +build +Icon? +shokunin.log +src/.DS_Store +tarpaulin-report.html +Cargo.lock \ No newline at end of file diff --git a/examples/example.rs b/examples/example.rs index 10689c07..865b72c6 100644 --- a/examples/example.rs +++ b/examples/example.rs @@ -15,11 +15,12 @@ //! function to generate the website. // Import the required libraries and modules. -use ssg::compiler::service::compile; -use ssg::server::serve::start; +use anyhow::Result; +use http_handle::Server; +use staticdatagen::compiler::service::compile; use std::path::Path; -fn main() -> Result<(), Box> { +fn main() -> Result<()> { // Define the paths to the build, site, source and template directories. // The build directory. @@ -39,7 +40,7 @@ fn main() -> Result<(), Box> { // The template directory. // This is where the HTML template files are located. // These templates are used to structure the content from the Markdown files. - let template_path = Path::new("template"); + let template_path = Path::new("templates"); // Call the compile function to generate the website. // The function takes the paths defined above as arguments and will @@ -47,9 +48,13 @@ fn main() -> Result<(), Box> { compile(build_path, content_path, site_path, template_path)?; // Serve the generated website locally. - let server_addr = "127.0.0.1:3000"; let example_root: String = site_path.to_str().unwrap().to_string(); - start(server_addr, &example_root).unwrap(); + + // Create a new server with an address and document root + let server = Server::new("127.0.0.1:3000", example_root.as_str()); + + // Start the server + let _ = server.start(); // If everything goes well, return Ok. Ok(()) diff --git a/rustfmt.toml b/rustfmt.toml index de6772f0..fbdfb0ee 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,4 +1,32 @@ +# Rust edition to use edition = "2021" + +# Maximum width of each line max_width = 72 + +# Number of spaces per tab tab_spaces = 4 -use_field_init_shorthand = true + +# Line ending style +newline_style = "Unix" + +# Use small heuristics for formatting decisions +use_small_heuristics = "Default" + +# Use spaces for indentation, not tabs +hard_tabs = false + +# Merge multiple `derive` attributes into a single attribute +merge_derives = true + +# Reorder import statements +reorder_imports = true + +# Reorder `mod` statements +reorder_modules = true + +# Remove unnecessary nested parentheses +remove_nested_parens = true + +# Always specify the ABI explicitly for external items +force_explicit_abi = true diff --git a/src/cmd.rs b/src/cmd.rs new file mode 100644 index 00000000..b1602d2c --- /dev/null +++ b/src/cmd.rs @@ -0,0 +1,806 @@ +// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! # Command Line Interface Module +//! +//! This module provides a secure and robust command-line interface for the Shokunin Static Site Generator. It handles argument parsing, configuration management, and validation of user inputs. +//! +//! ## Key Features +//! +//! - Safe path handling +//! - Input validation +//! - Secure configuration +//! - Error handling +//! +//! ## Usage Example +//! +//! ## Example Usage +//! ```rust,no_run +//! use ssg::cmd::build; +//! use ssg::cmd::ShokuninConfig; +//! +//! fn main() -> anyhow::Result<()> { +//! // Initialize the CLI with arguments from `build()` +//! let matches = build().get_matches(); +//! +//! // Use the matches to configure and run your application +//! if let Some(config) = ShokuninConfig::from_matches(&matches)?.serve_dir { +//! println!("Configuration loaded: {:?}", config); +//! // Continue with application logic... +//! } +//! Ok(()) +//! } +//! ``` + +use anyhow::Result; +use clap::{Arg, ArgAction, ArgMatches, Command}; +use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; +use thiserror::Error; +use url::Url; + +/// Possible errors that can occur during CLI operations. +#[derive(Error, Debug)] +pub enum CliError { + /// Indicates an invalid or unsafe path. + #[error("Invalid path for {field}: {details}")] + InvalidPath { + /// The field name containing the path + field: String, + /// Details about why the path is invalid + details: String, + }, + + /// Indicates that a required argument is missing. + #[error("Required argument missing: {0}")] + MissingArgument(String), + + /// Indicates an invalid URL format. + #[error("Invalid URL: {0}")] + InvalidUrl(String), + + /// Wraps standard IO errors. + #[error("IO error: {0}")] + IoError(#[from] std::io::Error), + + /// Wraps TOML parsing errors. + #[error("TOML parsing error: {0}")] + TomlError(#[from] toml::de::Error), +} + +/// Core configuration for the static site generator. +/// +/// This structure holds all settings needed to generate a static site, +/// including paths, metadata, and server options. +/// +/// # Security +/// +/// All paths undergo validation to prevent: +/// - Directory traversal +/// - Access to system directories +/// - Use of unsafe characters +/// +/// # Example +/// +/// ```rust,no_run +/// use ssg::cmd::ShokuninConfig; +/// use std::path::PathBuf; +/// +/// let config = ShokuninConfig { +/// site_name: String::from("my-site"), +/// content_dir: PathBuf::from("content"), +/// output_dir: PathBuf::from("public"), +/// template_dir: PathBuf::from("templates"), +/// serve_dir: None, +/// base_url: String::from("http://localhost:8000"), +/// site_title: String::from("My Site"), +/// site_description: String::from("A static site"), +/// language: String::from("en-GB"), +/// }; +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ShokuninConfig { + /// Project name + pub site_name: String, + /// Location of content files + pub content_dir: PathBuf, + /// Output directory for generated files + pub output_dir: PathBuf, + /// Location of template files + pub template_dir: PathBuf, + /// Optional directory for development server + pub serve_dir: Option, + /// Site's base URL + pub base_url: String, + /// Site title + pub site_title: String, + /// Site description + pub site_description: String, + /// Site language (format: xx-XX) + pub language: String, +} + +impl Default for ShokuninConfig { + fn default() -> Self { + Self { + site_name: String::new(), + content_dir: PathBuf::from("content"), + output_dir: PathBuf::from("public"), + template_dir: PathBuf::from("templates"), + serve_dir: None, + base_url: String::from("http://localhost:8000"), + site_title: String::from("My Shokunin Site"), + site_description: String::from( + "A site built with Shokunin", + ), + language: String::from("en-GB"), + } + } +} + +impl ShokuninConfig { + /// Creates a new configuration from command-line arguments. + /// + /// # Arguments + /// + /// * `matches` - Parsed command-line arguments + /// + /// # Returns + /// + /// Returns a Result containing either the validated configuration or an error. + /// + /// # Example + /// + /// ```rust,no_run + /// use ssg::cmd::{build, ShokuninConfig}; + /// + /// let matches = build().get_matches(); + /// let config = ShokuninConfig::from_matches(&matches).unwrap(); + /// ``` + pub fn from_matches( + matches: &ArgMatches, + ) -> Result { + if let Some(config_path) = matches.get_one::("config") + { + return Self::from_file(config_path); + } + + let mut config = Self::default(); + + if let Some(site_name) = matches.get_one::("new") { + config.site_name = site_name.clone(); + } + + if let Some(content_dir) = matches.get_one::("content") + { + config.content_dir = + validate_path(content_dir, "content_dir")?; + } + + if let Some(output_dir) = matches.get_one::("output") { + config.output_dir = + validate_path(output_dir, "output_dir")?; + } + + if let Some(template_dir) = + matches.get_one::("template") + { + config.template_dir = + validate_path(template_dir, "template_dir")?; + } + + if let Some(serve_dir) = matches.get_one::("serve") { + config.serve_dir = + Some(validate_path(serve_dir, "serve_dir")?); + } + + Ok(config) + } + + /// Loads configuration from a TOML file. + /// + /// # Arguments + /// + /// * `path` - Path to the TOML file + /// + /// # Returns + /// + /// Returns a Result containing either the validated configuration or an error. + pub fn from_file(path: &Path) -> Result { + let content = std::fs::read_to_string(path)?; + let config: ShokuninConfig = toml::from_str(&content)?; + config.validate()?; + Ok(config) + } + + /// Validates all configuration values. + /// + /// Checks: + /// - URL format + /// - Path safety + /// - Language format + fn validate(&self) -> Result<(), CliError> { + if !self.base_url.is_empty() { + let parsed_url = + Url::parse(&self.base_url).map_err(|_| { + CliError::InvalidUrl(self.base_url.clone()) + })?; + + // Reject URLs that are not HTTP or HTTPS + if parsed_url.scheme() != "http" + && parsed_url.scheme() != "https" + { + return Err(CliError::InvalidUrl( + self.base_url.clone(), + )); + } + + // Additional check: reject URLs with invalid or empty hostname + if parsed_url.host_str().map_or(true, |host| { + host.is_empty() || host.starts_with('.') + }) { + return Err(CliError::InvalidUrl( + self.base_url.clone(), + )); + } + + // Reject URLs that contain backslashes + if self.base_url.contains('\\') { + return Err(CliError::InvalidUrl( + self.base_url.clone(), + )); + } + } + + validate_path_safety(&self.content_dir, "content_dir")?; + validate_path_safety(&self.output_dir, "output_dir")?; + validate_path_safety(&self.template_dir, "template_dir")?; + + if let Some(serve_dir) = &self.serve_dir { + validate_path_safety(serve_dir, "serve_dir")?; + } + + if !self.language.contains('-') || self.language.len() != 5 { + return Err(CliError::InvalidPath { + field: "language".to_string(), + details: "Language must use format 'xx-XX'".to_string(), + }); + } + + Ok(()) + } +} + +/// Checks a path for security issues. +fn validate_path_safety( + path: &Path, + field: &str, +) -> Result<(), CliError> { + let path_str = path.to_string_lossy(); + + if path_str.contains('\0') { + return Err(CliError::InvalidPath { + field: field.to_string(), + details: "Path contains null byte".to_string(), + }); + } + + if path_str.contains('\u{202E}') { + return Err(CliError::InvalidPath { + field: field.to_string(), + details: "Path contains bidirectional text override" + .to_string(), + }); + } + + if path_str.contains("..") + || path.is_absolute() + || path_str.contains("//") + || path_str.contains(r"\\") + { + return Err(CliError::InvalidPath { + field: field.to_string(), + details: "Path traversal or invalid format detected" + .to_string(), + }); + } + + // Reject paths containing colons, except for drive letters on Windows (e.g., "C:\path"). + if path_str.contains(':') + && !(cfg!(windows) && path_str.chars().nth(1) == Some(':')) + { + return Err(CliError::InvalidPath { + field: field.to_string(), + details: "Path contains invalid character ':'".to_string(), + }); + } + + let reserved_names = ["con", "aux", "nul", "prn", "com1", "lpt1"]; + if reserved_names.contains(&path_str.to_lowercase().as_str()) { + return Err(CliError::InvalidPath { + field: field.to_string(), + details: "Reserved system path name".to_string(), + }); + } + + Ok(()) +} + +/// Validates and normalises a path. +fn validate_path( + path: &Path, + field: &str, +) -> Result { + validate_path_safety(path, field)?; + Ok(path.to_path_buf()) +} + +/// Creates the command-line interface. +pub fn build() -> Command { + Command::new(env!("CARGO_PKG_NAME")) + .author(env!("CARGO_PKG_AUTHORS")) + .about(env!("CARGO_PKG_DESCRIPTION")) + .version(env!("CARGO_PKG_VERSION")) + .arg( + Arg::new("config") + .help("Configuration file path") + .long("config") + .short('f') + .value_name("FILE") + .value_parser(clap::value_parser!(PathBuf)), + ) + .arg( + Arg::new("new") + .help("Create new project") + .long("new") + .short('n') + .required_unless_present("config") + .value_name("NAME") + .value_parser(clap::value_parser!(String)), // Change from PathBuf to String + ) + .arg( + Arg::new("content") + .help("Content directory") + .long("content") + .short('c') + .required_unless_present("config") + .value_name("DIR") + .value_parser(clap::value_parser!(PathBuf)), + ) + .arg( + Arg::new("output") + .help("Output directory") + .long("output") + .short('o') + .required_unless_present("config") + .value_name("DIR") + .value_parser(clap::value_parser!(PathBuf)), + ) + .arg( + Arg::new("template") + .help("Template directory") + .long("template") + .short('t') + .required_unless_present("config") + .value_name("DIR") + .value_parser(clap::value_parser!(PathBuf)), + ) + .arg( + Arg::new("serve") + .help("Development server directory") + .long("serve") + .short('s') + .value_name("DIR") + .value_parser(clap::value_parser!(PathBuf)), + ) + .arg( + Arg::new("watch") + .help("Watch for changes") + .long("watch") + .short('w') + .action(ArgAction::SetTrue), + ) +} + +/// Displays the application banner. +pub fn print_banner() { + let title = + format!("Shokunin (ssg) 🦀 v{}", env!("CARGO_PKG_VERSION")); + let description = + "A Fast and Flexible Static Site Generator written in Rust"; + let width = title.len().max(description.len()) + 4; + let line = "─".repeat(width - 2); + + println!("\n┌{}┐", line); + println!("│{:^width$}│", title); + println!("├{}┤", line); + println!("│{:^width$}│", description); + println!("└{}┘\n", line); +} + +#[cfg(test)] +mod tests { + use super::*; + use std::{env, fs, io::Write}; + use tempfile::TempDir; + + #[test] + fn test_cli_structure() { + // Verify that the CLI structure is set up correctly and all required arguments exist + build().debug_assert(); + } + + /// Creates a temporary directory for testing + fn setup_temp_dir() -> TempDir { + TempDir::new().expect("Failed to create temporary directory") + } + + #[test] + /// Test default configuration creation + fn test_default_config() { + let config = ShokuninConfig::default(); + assert_eq!(config.content_dir, PathBuf::from("content")); + assert_eq!(config.output_dir, PathBuf::from("public")); + assert_eq!(config.template_dir, PathBuf::from("templates")); + assert!(config.serve_dir.is_none()); + assert_eq!(config.base_url, "http://localhost:8000"); + assert_eq!(config.language, "en-GB"); + assert!(config.validate().is_ok()); + } + + #[test] + /// Test configuration validation with valid settings + fn test_config_validation_valid() { + let config = ShokuninConfig { + site_name: "test".to_string(), + content_dir: PathBuf::from("content"), + output_dir: PathBuf::from("public"), + template_dir: PathBuf::from("templates"), + serve_dir: Some(PathBuf::from("serve")), + base_url: "http://localhost:8000".to_string(), + site_title: "Test Site".to_string(), + site_description: "Test Description".to_string(), + language: "en-GB".to_string(), + }; + + assert!(config.validate().is_ok()); + } + + #[test] + /// Test configuration validation with invalid URL + fn test_config_validation_invalid_url() { + let config = ShokuninConfig { + base_url: "not a url".to_string(), + ..Default::default() + }; + assert!(matches!( + config.validate(), + Err(CliError::InvalidUrl(_)) + )); + } + + #[test] + /// Test configuration validation with path traversal attempts + fn test_config_validation_path_traversal() { + let config = ShokuninConfig { + content_dir: PathBuf::from("../content"), + ..Default::default() + }; + assert!(matches!( + config.validate(), + Err(CliError::InvalidPath { .. }) + )); + } + + /// Creates a test configuration file + fn create_test_config(dir: &Path) -> PathBuf { + let config_path = dir.join("config.toml"); + let config_content = r#" + site_name = "test-site" + content_dir = "content" + output_dir = "public" + template_dir = "templates" + base_url = "http://localhost:8000" + site_title = "Test Site" + site_description = "A test site" + language = "en-GB" + "#; + fs::write(&config_path, config_content) + .expect("Failed to write config file"); + config_path + } + + #[test] + /// Test configuration loading from TOML file + fn test_config_from_file() { + let temp_dir = setup_temp_dir(); + let config_path = create_test_config(temp_dir.path()); + + let config = ShokuninConfig::from_file(&config_path); + assert!(config.is_ok()); + + let config = config.unwrap(); + assert_eq!(config.site_name, "test-site"); + assert_eq!(config.content_dir, PathBuf::from("content")); + assert_eq!(config.base_url, "http://localhost:8000"); + } + + #[test] + /// Test configuration loading from invalid TOML file + fn test_config_from_invalid_file() { + let temp_dir = setup_temp_dir(); + let config_path = temp_dir.path().join("invalid.toml"); + fs::write(&config_path, "invalid = { toml") + .expect("Failed to write file"); + + let result = ShokuninConfig::from_file(&config_path); + assert!(matches!(result, Err(CliError::TomlError(_)))); + } + + #[test] + /// Test path validation with various invalid paths + fn test_path_validation_invalid() { + let invalid_paths = vec![ + "../dangerous", + "content/../secret", + "content//hidden", + r"content\..\..\secret", + "content/../../etc/passwd", + "content/./../../secret", + ]; + + for path in invalid_paths { + assert!(matches!( + validate_path(Path::new(path), "test"), + Err(CliError::InvalidPath { .. }) + )); + } + } + + #[test] + /// Test path validation with valid paths + fn test_path_validation_valid() { + let valid_paths = vec![ + "content", + "content/subfolder", + "templates/layouts", + "./content", + ]; + + for path in valid_paths { + assert!(validate_path(Path::new(path), "test").is_ok()); + } + } + + #[test] + /// Test CLI argument parsing with various combinations + fn test_cli_argument_parsing() { + // Test minimum required arguments + let result = build().try_get_matches_from(vec![ + "ssg", + "--new", + "test-site", + "--content", + "content", + "--output", + "public", + "--template", + "templates", + ]); + assert!(result.is_ok()); + + // Test with optional serve argument + let result = build().try_get_matches_from(vec![ + "ssg", + "--new", + "test-site", + "--content", + "content", + "--output", + "public", + "--template", + "templates", + "--serve", + "serve", + ]); + assert!(result.is_ok()); + + // Test with watch flag + let result = build().try_get_matches_from(vec![ + "ssg", + "--new", + "test-site", + "--content", + "content", + "--output", + "public", + "--template", + "templates", + "--watch", + ]); + assert!(result.is_ok()); + } + + #[test] + /// Test CLI argument parsing with missing required arguments + fn test_cli_argument_parsing_missing_required() { + let result = build().try_get_matches_from(vec!["ssg"]); + assert!(result.is_err()); + + let result = build().try_get_matches_from(vec![ + "ssg", + "--new", + "test-site", + ]); + assert!(result.is_err()); + } + + #[test] + /// Test banner output format + fn test_banner_output() { + let mut output = Vec::new(); + { + let mut stdout = std::io::Cursor::new(&mut output); + writeln!(stdout, "\n┌{}┐", "─".repeat(53)).unwrap(); + writeln!( + stdout, + "│{: ^54}│", + format!( + "Shokunin (ssg) 🦀 v{}", + env!("CARGO_PKG_VERSION") + ) + ) + .unwrap(); + writeln!(stdout, "├{}┤", "─".repeat(53)).unwrap(); + writeln!( + stdout, + "│{: ^54}│", + "A Fast and Flexible Static Site Generator written in Rust" + ) + .unwrap(); + writeln!(stdout, "└{}┘\n", "─".repeat(53)).unwrap(); + } + + let output_str = String::from_utf8(output).unwrap(); + assert!(output_str.contains("Shokunin")); + assert!(output_str.contains(env!("CARGO_PKG_VERSION"))); + assert!(output_str.contains("Static Site Generator")); + } + + #[test] + /// Test configuration creation from CLI matches + fn test_config_from_matches() { + let matches = build().get_matches_from(vec![ + "ssg", + "--new", + "test-site", + "--content", + "content", + "--output", + "public", + "--template", + "templates", + ]); + + let config = ShokuninConfig::from_matches(&matches); + assert!(config.is_ok()); + + let config = config.unwrap(); + assert_eq!(config.site_name, "test-site".to_string()); // Convert PathBuf to String for comparison + assert_eq!(config.content_dir, PathBuf::from("content")); + assert_eq!(config.output_dir, PathBuf::from("public")); + assert_eq!(config.template_dir, PathBuf::from("templates")); + assert!(config.serve_dir.is_none()); + } + + #[test] + /// Test configuration with serve directory + fn test_config_with_serve() { + let matches = build().get_matches_from(vec![ + "ssg", + "--new", + "test-site", // Use as expected by PathBuf + "--content", + "content", + "--output", + "public", + "--template", + "templates", + "--serve", + "serve", + ]); + + let config = ShokuninConfig::from_matches(&matches).unwrap(); + assert_eq!(config.site_name, "test-site".to_string()); // Convert PathBuf to String for comparison + assert_eq!(config.content_dir, PathBuf::from("content")); + assert_eq!(config.output_dir, PathBuf::from("public")); + assert_eq!(config.template_dir, PathBuf::from("templates")); + assert_eq!(config.serve_dir, Some(PathBuf::from("serve"))); + } + + #[test] + /// Test error handling for various error cases + fn test_error_handling() { + // Test missing argument error + let error = CliError::MissingArgument("test".to_string()); + assert_eq!( + error.to_string(), + "Required argument missing: test" + ); + + // Test invalid path error + let error = CliError::InvalidPath { + field: "test".to_string(), + details: "invalid".to_string(), + }; + assert_eq!(error.to_string(), "Invalid path for test: invalid"); + + // Test invalid URL error + let error = CliError::InvalidUrl("test".to_string()); + assert_eq!(error.to_string(), "Invalid URL: test"); + } + + #[test] + /// Test path normalization + fn test_path_normalization() { + let temp_dir = setup_temp_dir(); + env::set_current_dir(temp_dir.path()).unwrap(); + + let result = + validate_path(Path::new("content"), "test").unwrap(); + assert_eq!(result, PathBuf::from("content")); + + let result = + validate_path(Path::new("content/./subfolder"), "test") + .unwrap(); + assert_eq!(result, PathBuf::from("content/subfolder")); + } + + #[test] + fn test_path_validation_edge_cases() { + let edge_cases = vec![ + "content\0hidden", // Null byte + "content\u{202E}hidden", // Right-to-left override + "content\\../hidden", // Mixed separators + "con", // Reserved name + "content:alternate", // Alternate data stream + "C:\\Windows\\System32\\con", // Absolute path + "/etc/passwd", // Absolute path + ]; + + for path in edge_cases { + let result = validate_path(Path::new(path), "test"); + assert!( + matches!(result, Err(CliError::InvalidPath { .. })), + "Expected path '{}' to be invalid, but it passed validation", + path + ); + } + } + + #[test] + fn test_url_validation() { + let invalid_urls = vec![ + "not-a-url", // Not a valid URL format + "ftp://example.com", // Non-HTTP(S) scheme + "http://invalid\u{202E}url.com", // Bidirectional text override + "http://example.com\\path", // Backslashes instead of forward slashes + "https://:80", // Missing domain + "http://example.com:abc", // Invalid port + "http://.com", // Invalid domain format + ]; + + for url in invalid_urls { + let config = ShokuninConfig { + base_url: url.to_string(), + ..Default::default() + }; + + assert!( + config.validate().is_err(), + "Expected URL '{}' to be invalid, but it passed validation", + url + ); + } + } +} diff --git a/src/cmd/cli.rs b/src/cmd/cli.rs deleted file mode 100644 index cb79d240..00000000 --- a/src/cmd/cli.rs +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use clap::{Arg, ArgMatches, Command, Error}; - -/// ## Function: build - returns a Result containing the parsed input options -/// -/// Builds a command-line interface (CLI) using the `clap` crate and -/// returns the command-line arguments passed to the CLI as an -/// `ArgMatches` object. -/// -/// This function creates a CLI, and adds the following arguments to it: -/// -/// - `--new` or `-n`: Creates a new project. -/// - `--content` or `-c`: Specifies the location of the content directory. -/// - `--output` or `-o`: Specifies the location of the output directory. -/// - `--template` or `-t`: Specifies the location of the template directory. -/// - `--serve` or `-s`: Serves the public directory on a local web server. -/// -/// If the CLI is successfully built and the command-line arguments are -/// parsed correctly, the function returns an `Ok` result containing the -/// `ArgMatches` object. If an error occurs while building or parsing -/// the CLI, an `Err` result containing the error message is returned. -/// -/// # Arguments -/// -/// None -/// -/// # Returns -/// -/// * `Result` - A struct containing the parsed -/// command-line arguments and their values, or an error if the -/// arguments could not be parsed. -/// -/// # Examples -/// -/// ``` -/// use ssg::cmd::cli::build; -/// let cmd = build().unwrap(); -/// -/// ``` -pub fn build() -> Result { - let cmd = Command::new("Shokunin Static Site Generator") - .author("Sebastien Rousseau") - .about("") - .bin_name("ssg") - .version("0.0.29") - .arg( - Arg::new("new") - .help("Create a new project.") - .long("new") - .short('n') - .value_name("NEW"), - ) - .arg( - Arg::new("content") - .help("Location of the content directory.") - .long("content") - .short('c') - .value_name("CONTENT"), - ) - .arg( - Arg::new("output") - .help("Location of the output directory.") - .long("output") - .short('o') - .value_name("OUTPUT"), - ) - .arg( - Arg::new("template") - .help("Location of the template directory.") - .long("template") - .short('t') - .value_name("TEMPLATE"), - ) - .arg( - Arg::new("serve") - .help("Serve the public directory on a local web server.") - .long("serve") - .short('s') - .value_name("SERVE") - ) - .after_help( - "\x1b[1;4mDocumentation:\x1b[0m\n\n https://shokunin.one\n\n\x1b[1;4mLicense:\x1b[0m\n The project is licensed under the terms of both the MIT license and the Apache License (Version 2.0).", - ) - .get_matches(); - Ok(cmd) -} - -/// # `print_banner` function -/// -/// This function prints a banner containing the title and description of -/// the `Shokunin` static site generator tool. -/// -/// The banner is printed to the terminal in a box, with a horizontal line -/// separating the title and description. The width of the box is determined -/// by the length of the title and description. -/// -/// # Arguments -/// -/// This function takes no arguments. -/// -/// # Examples -/// -/// ``` -/// use ssg::cmd::cli::print_banner; -/// -/// print_banner(); -/// ``` -pub fn print_banner() { - // Set the title and description for the CLI - let title = "Shokunin (ssg) 🦀 v0.0.29"; - let description = - "A Fast and Flexible Static Site Generator written in Rust"; - - // Set the width of the box to fit the title and description - let width = title.len().max(description.len()) + 4; - - // Create a horizontal line to separate the box - let horizontal_line = "─".repeat(width - 2); - - // Print the title and description in a box - println!("\n┌{}┐", horizontal_line); - println!("│{: ^1$}│", title, width - 3); - println!("├{}┤", horizontal_line); - println!("│{: ^1$}│", description, width - 2); - println!("└{}┘\n", horizontal_line); -} diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs deleted file mode 100644 index a7ee3d57..00000000 --- a/src/cmd/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -/// The `cli` module contains functions for the command-line interface. -pub mod cli; - -/// The `parser` module contains functions for parsing command-line arguments and options. -pub mod process; diff --git a/src/cmd/process.rs b/src/cmd/process.rs deleted file mode 100644 index 520c8d26..00000000 --- a/src/cmd/process.rs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use crate::macro_check_directory; -use crate::{compiler::service::compile, macro_get_args}; -use clap::ArgMatches; -use std::path::Path; - -/// ## Function: args - returns a Result indicating success or failure -/// -/// Parses the command-line arguments passed by the static site generator -/// command-line tool (ssg) and compiles the project. -/// -/// - This function parses the `content` directory where the markdown files for -/// your website are stored and the `output` directory where the compiled site -/// will be created from the `matches` object. -/// -/// - It then, validates that these directories exist, or creates them on the -/// fly if they do not. If either directory cannot be found or created, an -/// error is returned. -/// -/// - Finally, it calls the `compile` function to create the new project using -/// the markdown files in the "content" directory, and returns an error if the -/// compilation process fails. -/// -/// # Arguments -/// -/// * `matches` - A reference to an ArgMatches object containing the command- -/// line arguments passed to the tool. This is created by the `clap` crate. -/// -/// # Returns -/// -/// * A Result indicating success or failure. -/// - Ok() if the project was created successfully and the output files were -/// written to the output directory. -/// - Err(anyhow::Error) if the project could not be created or the output files -/// could not be written to the output directory. -/// -pub fn args(matches: &ArgMatches) -> Result<(), String> { - // Set the content elements of the new project - let content_dir = macro_get_args!(matches, "content"); - let output_dir = macro_get_args!(matches, "output"); - let site_dir = macro_get_args!(matches, "new"); - let template_dir = macro_get_args!(matches, "template"); - - // Create Path objects for the content and output directories - let content_path = Path::new(&content_dir); - let build_path = Path::new(&output_dir); - let site_path = Path::new(&site_dir); - let template_path = Path::new(&template_dir); - - // Ensure the build, content, site and template directories exist - macro_check_directory!(content_path, "content"); - macro_check_directory!(build_path, "output"); - macro_check_directory!(site_path, "new"); - macro_check_directory!(template_path, "template"); - - // Create the new project - let compilation_result = - compile(build_path, content_path, site_path, template_path); - match compilation_result { - Ok(_) => Ok(()), - Err(e) => Err(format!("❌ Error: {}", e)), - } -} diff --git a/src/compiler/mod.rs b/src/compiler/mod.rs deleted file mode 100644 index d2c786ad..00000000 --- a/src/compiler/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -/// The `service` module contains the compiler service. -pub mod service; diff --git a/src/compiler/service.rs b/src/compiler/service.rs deleted file mode 100644 index 6745148b..00000000 --- a/src/compiler/service.rs +++ /dev/null @@ -1,391 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use pdf_composer::{ - FontsStandard, PDFComposer, PDFDocInfoEntry, PDFVersion, - PaperOrientation, PaperSize, -}; -use rlg::log_level::LogLevel::ERROR; - -// use crate::modules::pdf::generate_pdf; -use crate::{ - macro_cleanup_directories, macro_create_directories, - macro_log_info, macro_metadata_option, macro_set_rss_data_fields, - metadata::service::extract_and_prepare_metadata, - models::data::{FileData, PageData, RssData}, - modules::{ - cname::create_cname_data, - html::generate_html, - human::create_human_data, - json::{cname, human, news_sitemap, sitemap, txt}, - manifest::create_manifest_data, - - navigation::NavigationGenerator, - news_sitemap::create_news_site_map_data, - // pdf::PdfGenerationParams, - // plaintext::generate_plain_text, - rss::generate_rss, - sitemap::create_site_map_data, - tags::*, - txt::create_txt_data, - }, - utilities::{ - file::add, - template::{render_page, PageOptions}, - write::write_files_to_build_directory, - }, -}; -use std::{ - collections::HashMap, - error::Error, - fs, - path::{Path, PathBuf}, -}; - -/// Compiles files in a source directory, generates HTML pages from them, and -/// writes the resulting pages to an output directory. Also generates an index -/// page containing links to the generated pages. -/// -/// This function takes paths to source, output, site, and template directories -/// as arguments and performs the compilation process. It reads Markdown files, -/// extracts metadata, generates HTML content, and writes resulting pages. -/// -/// # Arguments -/// -/// * `build_dir_path` - The path to the temporary build directory. -/// * `content_path` - The path to the content directory. -/// * `site_path` - The path to the output site directory. -/// * `template_path` - The path to the template directory. -/// -/// # Returns -/// -/// Returns `Ok(())` if the compilation is successful, otherwise returns an error -/// wrapped in a `Box`. -pub fn compile( - build_dir_path: &Path, // The path to the temp directory - content_path: &Path, // The path to the content directory - site_path: &Path, // The path to the site directory - template_path: &Path, // The path to the template directory -) -> Result<(), Box> { - // Create build and site directories - macro_create_directories!(build_dir_path, site_path)?; - - // Read files in the source directory - let source_files = add(content_path)?; - - // Generate navigation bar HTML - let navigation = - NavigationGenerator::generate_navigation(&source_files); - - let mut global_tags_data: HashMap> = - HashMap::new(); - - // Process source files and store results in 'compiled_files' vector - let compiled_files: Vec = source_files - .into_iter() - .map(|file| { - let (metadata, keywords, all_meta_tags) = - extract_and_prepare_metadata(&file.content); - - // Generate HTML - let html_content = generate_html( - &file.content, - ¯o_metadata_option!(metadata, "title"), - ¯o_metadata_option!(metadata, "description"), - Some(¯o_metadata_option!(metadata, "content")), - ) - .unwrap_or_else(|err| { - let description = - format!("Error generating HTML: {:?}", err); - macro_log_info!( - &ERROR, - "compiler.rs - Line 81", - &description, - &LogFormat::CLF - ); - String::from("Fallback HTML content") - }); - - // Determine the filename without the extension - let filename_without_extension = Path::new(&file.name) - .file_stem() - .and_then(|stem| stem.to_str()) - .unwrap_or(&file.name); - let common_path = build_dir_path.to_str().unwrap(); - - let mut pdf_source_paths: Vec = Vec::new(); - let source_for_pdf = - Path::new(content_path).join(&file.name); - pdf_source_paths.push(source_for_pdf); - let mut pdf_instance = PDFComposer::new(); - pdf_instance.set_pdf_version(PDFVersion::V1_7); - pdf_instance.set_paper_size(PaperSize::A5); - pdf_instance.set_orientation(PaperOrientation::Landscape); - pdf_instance.set_margins("20"); - pdf_instance.set_font(FontsStandard::TimesRoman); - pdf_instance.add_source_files(pdf_source_paths); - let author_entry = PDFDocInfoEntry { - doc_info_entry: "Author", - yaml_entry: "author", - }; - let generator_entry = PDFDocInfoEntry { - doc_info_entry: "generator", - yaml_entry: "generator", - }; - pdf_instance.set_doc_info_entry(author_entry); - pdf_instance.set_doc_info_entry(generator_entry); - let pdf_destination = - Path::new(common_path).join(filename_without_extension); - pdf_instance.set_output_directory( - pdf_destination.to_str().unwrap(), - ); - - pdf_instance.generate_pdfs(); - - // Create page options - let mut page_options = PageOptions::new(); - for (key, value) in metadata.iter() { - page_options.set(key, value); - } - - // Set various meta tags - page_options.set("apple", &all_meta_tags.apple); - page_options.set("content", &html_content); - page_options.set("microsoft", &all_meta_tags.ms); - page_options.set("navigation", &navigation); - page_options.set("opengraph", &all_meta_tags.og); - page_options.set("primary", &all_meta_tags.primary); - page_options.set("twitter", &all_meta_tags.twitter); - - // Render page content - let content = render_page( - &page_options, - &template_path.to_str().unwrap().to_string(), - &metadata.get("layout").cloned().unwrap_or_default(), - ) - .unwrap(); - - // Generate RSS data - let mut rss_data = RssData::new(); - - // Set fields using the helper macro - macro_set_rss_data_fields!( - rss_data, - atom_link, - macro_metadata_option!(metadata, "atom_link") - ); - macro_set_rss_data_fields!( - rss_data, - author, - macro_metadata_option!(metadata, "author") - ); - macro_set_rss_data_fields!( - rss_data, - category, - macro_metadata_option!(metadata, "category") - ); - macro_set_rss_data_fields!( - rss_data, - copyright, - macro_metadata_option!(metadata, "copyright") - ); - macro_set_rss_data_fields!( - rss_data, - description, - macro_metadata_option!(metadata, "description") - ); - macro_set_rss_data_fields!( - rss_data, - docs, - macro_metadata_option!(metadata, "docs") - ); - macro_set_rss_data_fields!( - rss_data, - generator, - macro_metadata_option!(metadata, "generator") - ); - macro_set_rss_data_fields!( - rss_data, - image, - macro_metadata_option!(metadata, "image") - ); - macro_set_rss_data_fields!( - rss_data, - item_guid, - macro_metadata_option!(metadata, "item_guid") - ); - macro_set_rss_data_fields!( - rss_data, - item_description, - macro_metadata_option!(metadata, "item_description") - ); - macro_set_rss_data_fields!( - rss_data, - item_link, - macro_metadata_option!(metadata, "item_link") - ); - macro_set_rss_data_fields!( - rss_data, - item_pub_date, - macro_metadata_option!(metadata, "item_pub_date") - ); - macro_set_rss_data_fields!( - rss_data, - item_title, - macro_metadata_option!(metadata, "item_title") - ); - macro_set_rss_data_fields!( - rss_data, - language, - macro_metadata_option!(metadata, "language") - ); - macro_set_rss_data_fields!( - rss_data, - last_build_date, - macro_metadata_option!(metadata, "last_build_date") - ); - macro_set_rss_data_fields!( - rss_data, - link, - macro_metadata_option!(metadata, "permalink") - ); - macro_set_rss_data_fields!( - rss_data, - managing_editor, - macro_metadata_option!(metadata, "managing_editor") - ); - macro_set_rss_data_fields!( - rss_data, - pub_date, - macro_metadata_option!(metadata, "pub_date") - ); - macro_set_rss_data_fields!( - rss_data, - title, - macro_metadata_option!(metadata, "title") - ); - macro_set_rss_data_fields!( - rss_data, - ttl, - macro_metadata_option!(metadata, "ttl") - ); - - // Generate RSS - let rss = generate_rss(&rss_data); - let rss_data = rss.unwrap(); - - // Generate a manifest data structure by extracting relevant information from the metadata. - let json = create_manifest_data(&metadata); - - // Create a structure to hold CNAME-related options, populated with values from the metadata. - let cname_options = create_cname_data(&metadata); - - // Generate a structure for human-readable data, filling it with values from the metadata. - let human_options = create_human_data(&metadata); - - // Initialize a structure to store sitemap-related information, using values from the metadata. - let sitemap_options = create_site_map_data(&metadata); - - // Initialize a structure to store news sitemap-related information, using values from the metadata. - let news_sitemap_options = - create_news_site_map_data(&metadata); - - let tags_data = generate_tags(&file, &metadata); - - // Update the global tags data - for (tag, pages_data) in tags_data.iter() { - let page_info: Vec = pages_data - .iter() - .map(|page_data| PageData { - title: page_data - .get("title") - .cloned() - .unwrap_or_default(), - description: page_data - .get("description") - .cloned() - .unwrap_or_default(), - permalink: page_data - .get("permalink") - .cloned() - .unwrap_or_default(), - date: page_data - .get("date") - .cloned() - .unwrap_or_default(), - }) - .collect(); - - global_tags_data - .entry(tag.clone()) - .or_default() - .extend(page_info); - } - - // Generate a TxtData structure, filling it with values extracted from the metadata. - let txt_options = create_txt_data(&metadata); - - // Generate the data for the various files - let txt_data = txt(&txt_options); - let cname_data = cname(&cname_options); - let human_data = human(&human_options); - let sitemap_data = sitemap(sitemap_options, site_path); - let news_sitemap_data = news_sitemap(news_sitemap_options); - let json_data = serde_json::to_string(&json) - .unwrap_or_else(|e| { - eprintln!("Error serializing JSON: {}", e); - String::new() - }); - // Return FileData - FileData { - cname: cname_data, - content, - keyword: keywords.join(", "), - human: human_data, - json: json_data, - name: file.name, - rss: rss_data, - sitemap: sitemap_data, - sitemap_news: news_sitemap_data, - txt: txt_data, - } - }) - .collect(); - - // Write compiled files to output directory - let cli_description = format!( - ": Successfully generated, compiled, and minified all HTML and PDF files to the `{:?}` directory", - site_path.display() - ); - - // Log the generated files information to a log file (shokunin.log) - macro_log_info!( - &ERROR, - "compiler.rs - Line 280", - &cli_description, - &LogFormat::CLF - ); - - // Print the generated files to the console - // println!("{} ", cli_description); - - // Iterate over compiled files and write pages to output directory - for file in &compiled_files { - write_files_to_build_directory( - build_dir_path, - file, - template_path, - )?; - } - - let tags_html_content = generate_tags_html(&global_tags_data); - write_tags_html_to_file(&tags_html_content, build_dir_path)?; - - // Cleanup site directory - macro_cleanup_directories!(site_path); - - // Move build content to site directory and remove build directory - fs::rename(build_dir_path, site_path)?; - - Ok(()) -} diff --git a/src/lang/README.md b/src/lang/README.md deleted file mode 100644 index 2a63bd62..00000000 --- a/src/lang/README.md +++ /dev/null @@ -1,4 +0,0 @@ -the template is in template.rs -transfer to **.rs -in format: -("ENG-KEY", "translation"), \ No newline at end of file diff --git a/src/lang/de.rs b/src/lang/de.rs deleted file mode 100644 index 656d3d93..00000000 --- a/src/lang/de.rs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -//! Module for German translations. - -/// Translates the given text into German. -/// -/// This function looks up the translation for the given `text` in the `T` hash map. -/// If a translation is found, it returns the translated string. Otherwise, it returns -/// the original `text` as a fallback. -/// -/// # Arguments -/// -/// * `text` - The text to be translated. -/// -/// # Returns -/// -/// The translated string if a translation is found, or the original `text` if no -/// translation is available. -/// -/// # Examples -/// -/// ``` -/// use ssg::languages::translate; -/// -/// let text = "Hello"; -/// let translated = translate("de", "Hello"); -/// assert_eq!(translated, "Hallo"); -/// ``` -pub(crate) fn translate(text: &str) -> String { - T.get(text) - .map(|s| s.to_string()) - .unwrap_or_else(|| text.to_string()) -} - -lazy_static::lazy_static! { - /// Hash map containing German translations. - /// - /// This static hash map is lazily initialized using the `lazy_static` macro. - /// It contains key-value pairs, where the key is the original text and the value - /// is the corresponding German translation. - /// - /// The translations are defined as an array of tuples `(&str, &str)` and collected - /// into the hash map using `iter().cloned().collect()`. - /// - pub static ref T: std::collections::HashMap<&'static str, &'static str> = - [ - ("Hello", "Hallo"), - ("main_logger_msg", "\nFür weitere Informationen führen Sie bitte `ssg --help` aus.\n"), - ("lib_banner_log_msg", "Banner erfolgreich gedruckt"), - ("lib_args_log_msg", "Argumente erfolgreich verarbeitet"), - ("lib_server_log_msg", "Server erfolgreich gestartet"), - // Add more translations here - ].iter().cloned().collect(); -} diff --git a/src/lang/en.rs b/src/lang/en.rs deleted file mode 100644 index 45ed5ad0..00000000 --- a/src/lang/en.rs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -//! Module for English translations. - -/// Translates the given text into English. -/// -/// This function looks up the translation for the given `text` in the `T` hash map. -/// If a translation is found, it returns the translated string. Otherwise, it returns -/// the original `text` as a fallback. -/// -/// # Arguments -/// -/// * `text` - The text to be translated. -/// -/// # Returns -/// -/// The translated string if a translation is found, or the original `text` if no -/// translation is available. -/// -/// # Examples -/// -/// ``` -/// use ssg::languages::translate; -/// -/// let text = "Hello"; -/// let translated = translate("en", "Hello"); -/// assert_eq!(translated, "Hello"); -/// ``` -pub(crate) fn translate(text: &str) -> String { - T.get(text) - .map(|s| s.to_string()) - .unwrap_or_else(|| text.to_string()) -} - -lazy_static::lazy_static! { - /// Hash map containing English translations. - /// - /// This static hash map is lazily initialized using the `lazy_static` macro. - /// It contains key-value pairs, where the key is the original text and the value - /// is the corresponding English translation. - /// - /// The translations are defined as an array of tuples `(&str, &str)` and collected - /// into the hash map using `iter().cloned().collect()`. - /// - pub static ref T: std::collections::HashMap<&'static str, &'static str> = - [ - ("Hello", "Hello"), - ("main_logger_msg", "\nPlease run `ssg --help` for more information.\n"), - ("lib_banner_log_msg", "Banner printed successfully"), - ("lib_args_log_msg", "Arguments processed successfully"), - ("lib_server_log_msg", "Server started successfully"), - // Add more translations here - ].iter().cloned().collect(); -} diff --git a/src/lang/fr.rs b/src/lang/fr.rs deleted file mode 100644 index 6d79db5f..00000000 --- a/src/lang/fr.rs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -//! Module for French translations. - -/// Translates the given text into French. -/// -/// This function looks up the translation for the given `text` in the `T` hash map. -/// If a translation is found, it returns the translated string. Otherwise, it returns -/// the original `text` as a fallback. -/// -/// # Arguments -/// -/// * `text` - The text to be translated. -/// -/// # Returns -/// -/// The translated string if a translation is found, or the original `text` if no -/// translation is available. -/// -/// # Examples -/// -/// ``` -/// use ssg::languages::translate; -/// -/// let text = "Hello"; -/// let translated = translate("fr", "Hello"); -/// assert_eq!(translated, "Bonjour"); -/// ``` -pub(crate) fn translate(text: &str) -> String { - T.get(text) - .map(|s| s.to_string()) - .unwrap_or_else(|| text.to_string()) -} - -lazy_static::lazy_static! { - /// Hash map containing French translations. - /// - /// This static hash map is lazily initialized using the `lazy_static` macro. - /// It contains key-value pairs, where the key is the original text and the value - /// is the corresponding French translation. - /// - /// The translations are defined as an array of tuples `(&str, &str)` and collected - /// into the hash map using `iter().cloned().collect()`. - /// - pub static ref T: std::collections::HashMap<&'static str, &'static str> = - [ - ("Hello", "Bonjour"), - ("main_logger_msg", "\nVeuillez lancer `ssg --help` pour plus d'informations.\n"), - ("lib_banner_log_msg", "Bannière imprimée avec succès"), - ("lib_args_log_msg", "Arguments traités avec succès"), - ("lib_server_log_msg", "Serveur démarré avec succès"), - // Add more translations here - ].iter().cloned().collect(); -} diff --git a/src/lang/mod.rs b/src/lang/mod.rs deleted file mode 100644 index 9dd45a9d..00000000 --- a/src/lang/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -/// The `en` module contains English translations. -pub mod en; - -/// The `fr` module contains French translations. -pub mod fr; - -/// The `de` module contains German translations. -pub mod de; diff --git a/src/lang/template.rs b/src/lang/template.rs deleted file mode 100644 index 9c8dea30..00000000 --- a/src/lang/template.rs +++ /dev/null @@ -1,604 +0,0 @@ -lazy_static::lazy_static! { - pub static ref T: std::collections::HashMap<&'static str, &'static str> = - [ - ("Status", ""), - ("Your Desktop", ""), - ("desk_tip", ""), - ("Password", ""), - ("Ready", ""), - ("Established", ""), - ("connecting_status", ""), - ("Enable service", ""), - ("Start service", ""), - ("Service is running", ""), - ("Service is not running", ""), - ("not_ready_status", ""), - ("Control Remote Desktop", ""), - ("Transfer file", ""), - ("Connect", ""), - ("Recent sessions", ""), - ("Address book", ""), - ("Confirmation", ""), - ("TCP tunneling", ""), - ("Remove", ""), - ("Refresh random password", ""), - ("Set your own password", ""), - ("Enable keyboard/mouse", ""), - ("Enable clipboard", ""), - ("Enable file transfer", ""), - ("Enable TCP tunneling", ""), - ("IP Whitelisting", ""), - ("ID/Relay Server", ""), - ("Import server config", ""), - ("Export Server Config", ""), - ("Import server configuration successfully", ""), - ("Export server configuration successfully", ""), - ("Invalid server configuration", ""), - ("Clipboard is empty", ""), - ("Stop service", ""), - ("Change ID", ""), - ("Your new ID", ""), - ("length %min% to %max%", ""), - ("starts with a letter", ""), - ("allowed characters", ""), - ("id_change_tip", ""), - ("Website", ""), - ("About", ""), - ("Slogan_tip", ""), - ("Privacy Statement", ""), - ("Mute", ""), - ("Build Date", ""), - ("Version", ""), - ("Home", ""), - ("Audio Input", ""), - ("Enhancements", ""), - ("Hardware Codec", ""), - ("Adaptive bitrate", ""), - ("ID Server", ""), - ("Relay Server", ""), - ("API Server", ""), - ("invalid_http", ""), - ("Invalid IP", ""), - ("Invalid format", ""), - ("server_not_support", ""), - ("Not available", ""), - ("Too frequent", ""), - ("Cancel", ""), - ("Skip", ""), - ("Close", ""), - ("Retry", ""), - ("OK", ""), - ("Password Required", ""), - ("Please enter your password", ""), - ("Remember password", ""), - ("Wrong Password", ""), - ("Do you want to enter again?", ""), - ("Connection Error", ""), - ("Error", ""), - ("Reset by the peer", ""), - ("Connecting...", ""), - ("Connection in progress. Please wait.", ""), - ("Please try 1 minute later", ""), - ("Login Error", ""), - ("Successful", ""), - ("Connected, waiting for image...", ""), - ("Name", ""), - ("Type", ""), - ("Modified", ""), - ("Size", ""), - ("Show Hidden Files", ""), - ("Receive", ""), - ("Send", ""), - ("Refresh File", ""), - ("Local", ""), - ("Remote", ""), - ("Remote Computer", ""), - ("Local Computer", ""), - ("Confirm Delete", ""), - ("Delete", ""), - ("Properties", ""), - ("Multi Select", ""), - ("Select All", ""), - ("Unselect All", ""), - ("Empty Directory", ""), - ("Not an empty directory", ""), - ("Are you sure you want to delete this file?", ""), - ("Are you sure you want to delete this empty directory?", ""), - ("Are you sure you want to delete the file of this directory?", ""), - ("Do this for all conflicts", ""), - ("This is irreversible!", ""), - ("Deleting", ""), - ("files", ""), - ("Waiting", ""), - ("Finished", ""), - ("Speed", ""), - ("Custom Image Quality", ""), - ("Privacy mode", ""), - ("Block user input", ""), - ("Unblock user input", ""), - ("Adjust Window", ""), - ("Original", ""), - ("Shrink", ""), - ("Stretch", ""), - ("Scrollbar", ""), - ("ScrollAuto", ""), - ("Good image quality", ""), - ("Balanced", ""), - ("Optimize reaction time", ""), - ("Custom", ""), - ("Show remote cursor", ""), - ("Show quality monitor", ""), - ("Disable clipboard", ""), - ("Lock after session end", ""), - ("Insert", ""), - ("Insert Lock", ""), - ("Refresh", ""), - ("ID does not exist", ""), - ("Failed to connect to rendezvous server", ""), - ("Please try later", ""), - ("Remote desktop is offline", ""), - ("Key mismatch", ""), - ("Timeout", ""), - ("Failed to connect to relay server", ""), - ("Failed to connect via rendezvous server", ""), - ("Failed to connect via relay server", ""), - ("Failed to make direct connection to remote desktop", ""), - ("Set Password", ""), - ("OS Password", ""), - ("install_tip", ""), - ("Click to upgrade", ""), - ("Click to download", ""), - ("Click to update", ""), - ("Configure", ""), - ("config_acc", ""), - ("config_screen", ""), - ("Installing ...", ""), - ("Install", ""), - ("Installation", ""), - ("Installation Path", ""), - ("Create start menu shortcuts", ""), - ("Create desktop icon", ""), - ("agreement_tip", ""), - ("Accept and Install", ""), - ("End-user license agreement", ""), - ("Generating ...", ""), - ("Your installation is lower version.", ""), - ("not_close_tcp_tip", ""), - ("Listening ...", ""), - ("Remote Host", ""), - ("Remote Port", ""), - ("Action", ""), - ("Add", ""), - ("Local Port", ""), - ("Local Address", ""), - ("Change Local Port", ""), - ("setup_server_tip", ""), - ("Too short, at least 6 characters.", ""), - ("The confirmation is not identical.", ""), - ("Permissions", ""), - ("Accept", ""), - ("Dismiss", ""), - ("Disconnect", ""), - ("Enable file copy and paste", ""), - ("Connected", ""), - ("Direct and encrypted connection", ""), - ("Relayed and encrypted connection", ""), - ("Direct and unencrypted connection", ""), - ("Relayed and unencrypted connection", ""), - ("Enter Remote ID", ""), - ("Enter your password", ""), - ("Logging in...", ""), - ("Enable RDP session sharing", ""), - ("Auto Login", ""), - ("Enable direct IP access", ""), - ("Rename", ""), - ("Space", ""), - ("Create desktop shortcut", ""), - ("Change Path", ""), - ("Create Folder", ""), - ("Please enter the folder name", ""), - ("Fix it", ""), - ("Warning", ""), - ("Login screen using Wayland is not supported", ""), - ("Reboot required", ""), - ("Unsupported display server", ""), - ("x11 expected", ""), - ("Port", ""), - ("Settings", ""), - ("Username", ""), - ("Invalid port", ""), - ("Closed manually by the peer", ""), - ("Enable remote configuration modification", ""), - ("Run without install", ""), - ("Connect via relay", ""), - ("Always connect via relay", ""), - ("whitelist_tip", ""), - ("Login", ""), - ("Verify", ""), - ("Remember me", ""), - ("Trust this device", ""), - ("Verification code", ""), - ("verification_tip", ""), - ("Logout", ""), - ("Tags", ""), - ("Search ID", ""), - ("whitelist_sep", ""), - ("Add ID", ""), - ("Add Tag", ""), - ("Unselect all tags", ""), - ("Network error", ""), - ("Username missed", ""), - ("Password missed", ""), - ("Wrong credentials", ""), - ("The verification code is incorrect or has expired", ""), - ("Edit Tag", ""), - ("Forget Password", ""), - ("Favorites", ""), - ("Add to Favorites", ""), - ("Remove from Favorites", ""), - ("Empty", ""), - ("Invalid folder name", ""), - ("Socks5 Proxy", ""), - ("Discovered", ""), - ("install_daemon_tip", ""), - ("Remote ID", ""), - ("Paste", ""), - ("Paste here?", ""), - ("Are you sure to close the connection?", ""), - ("Download new version", ""), - ("Touch mode", ""), - ("Mouse mode", ""), - ("One-Finger Tap", ""), - ("Left Mouse", ""), - ("One-Long Tap", ""), - ("Two-Finger Tap", ""), - ("Right Mouse", ""), - ("One-Finger Move", ""), - ("Double Tap & Move", ""), - ("Mouse Drag", ""), - ("Three-Finger vertically", ""), - ("Mouse Wheel", ""), - ("Two-Finger Move", ""), - ("Canvas Move", ""), - ("Pinch to Zoom", ""), - ("Canvas Zoom", ""), - ("Reset canvas", ""), - ("No permission of file transfer", ""), - ("Note", ""), - ("Connection", ""), - ("Share Screen", ""), - ("Chat", ""), - ("Total", ""), - ("items", ""), - ("Selected", ""), - ("Screen Capture", ""), - ("Input Control", ""), - ("Audio Capture", ""), - ("File Connection", ""), - ("Screen Connection", ""), - ("Do you accept?", ""), - ("Open System Setting", ""), - ("How to get Android input permission?", ""), - ("android_input_permission_tip1", ""), - ("android_input_permission_tip2", ""), - ("android_new_connection_tip", ""), - ("android_service_will_start_tip", ""), - ("android_stop_service_tip", ""), - ("android_version_audio_tip", ""), - ("android_start_service_tip", ""), - ("android_permission_may_not_change_tip", ""), - ("Account", ""), - ("Overwrite", ""), - ("This file exists, skip or overwrite this file?", ""), - ("Quit", ""), - ("Help", ""), - ("Failed", ""), - ("Succeeded", ""), - ("Someone turns on privacy mode, exit", ""), - ("Unsupported", ""), - ("Peer denied", ""), - ("Please install plugins", ""), - ("Peer exit", ""), - ("Failed to turn off", ""), - ("Turned off", ""), - ("Language", ""), - ("Keep RustDesk background service", ""), - ("Ignore Battery Optimizations", ""), - ("android_open_battery_optimizations_tip", ""), - ("Start on boot", ""), - ("Start the screen sharing service on boot, requires special permissions", ""), - ("Connection not allowed", ""), - ("Legacy mode", ""), - ("Map mode", ""), - ("Translate mode", ""), - ("Use permanent password", ""), - ("Use both passwords", ""), - ("Set permanent password", ""), - ("Enable remote restart", ""), - ("Restart remote device", ""), - ("Are you sure you want to restart", ""), - ("Restarting remote device", ""), - ("remote_restarting_tip", ""), - ("Copied", ""), - ("Exit Fullscreen", ""), - ("Fullscreen", ""), - ("Mobile Actions", ""), - ("Select Monitor", ""), - ("Control Actions", ""), - ("Display Settings", ""), - ("Ratio", ""), - ("Image Quality", ""), - ("Scroll Style", ""), - ("Show Toolbar", ""), - ("Hide Toolbar", ""), - ("Direct Connection", ""), - ("Relay Connection", ""), - ("Secure Connection", ""), - ("Insecure Connection", ""), - ("Scale original", ""), - ("Scale adaptive", ""), - ("General", ""), - ("Security", ""), - ("Theme", ""), - ("Dark Theme", ""), - ("Light Theme", ""), - ("Dark", ""), - ("Light", ""), - ("Follow System", ""), - ("Enable hardware codec", ""), - ("Unlock Security Settings", ""), - ("Enable audio", ""), - ("Unlock Network Settings", ""), - ("Server", ""), - ("Direct IP Access", ""), - ("Proxy", ""), - ("Apply", ""), - ("Disconnect all devices?", ""), - ("Clear", ""), - ("Audio Input Device", ""), - ("Use IP Whitelisting", ""), - ("Network", ""), - ("Pin Toolbar", ""), - ("Unpin Toolbar", ""), - ("Recording", ""), - ("Directory", ""), - ("Automatically record incoming sessions", ""), - ("Change", ""), - ("Start session recording", ""), - ("Stop session recording", ""), - ("Enable recording session", ""), - ("Enable LAN discovery", ""), - ("Deny LAN discovery", ""), - ("Write a message", ""), - ("Prompt", ""), - ("Please wait for confirmation of UAC...", ""), - ("elevated_foreground_window_tip", ""), - ("Disconnected", ""), - ("Other", ""), - ("Confirm before closing multiple tabs", ""), - ("Keyboard Settings", ""), - ("Full Access", ""), - ("Screen Share", ""), - ("Wayland requires Ubuntu 21.04 or higher version.", ""), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", ""), - ("JumpLink", ""), - ("Please Select the screen to be shared(Operate on the peer side).", ""), - ("Show RustDesk", ""), - ("This PC", ""), - ("or", ""), - ("Continue with", ""), - ("Elevate", ""), - ("Zoom cursor", ""), - ("Accept sessions via password", ""), - ("Accept sessions via click", ""), - ("Accept sessions via both", ""), - ("Please wait for the remote side to accept your session request...", ""), - ("One-time Password", ""), - ("Use one-time password", ""), - ("One-time password length", ""), - ("Request access to your device", ""), - ("Hide connection management window", ""), - ("hide_cm_tip", ""), - ("wayland_experiment_tip", ""), - ("Right click to select tabs", ""), - ("Skipped", ""), - ("Add to address book", ""), - ("Group", ""), - ("Search", ""), - ("Closed manually by web console", ""), - ("Local keyboard type", ""), - ("Select local keyboard type", ""), - ("software_render_tip", ""), - ("Always use software rendering", ""), - ("config_input", ""), - ("config_microphone", ""), - ("request_elevation_tip", ""), - ("Wait", ""), - ("Elevation Error", ""), - ("Ask the remote user for authentication", ""), - ("Choose this if the remote account is administrator", ""), - ("Transmit the username and password of administrator", ""), - ("still_click_uac_tip", ""), - ("Request Elevation", ""), - ("wait_accept_uac_tip", ""), - ("Elevate successfully", ""), - ("uppercase", ""), - ("lowercase", ""), - ("digit", ""), - ("special character", ""), - ("length>=8", ""), - ("Weak", ""), - ("Medium", ""), - ("Strong", ""), - ("Switch Sides", ""), - ("Please confirm if you want to share your desktop?", ""), - ("Display", ""), - ("Default View Style", ""), - ("Default Scroll Style", ""), - ("Default Image Quality", ""), - ("Default Codec", ""), - ("Bitrate", ""), - ("FPS", ""), - ("Auto", ""), - ("Other Default Options", ""), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), - ("relay_hint_tip", ""), - ("Reconnect", ""), - ("Codec", ""), - ("Resolution", ""), - ("No transfers in progress", ""), - ("Set one-time password length", ""), - ("RDP Settings", ""), - ("Sort by", ""), - ("New Connection", ""), - ("Restore", ""), - ("Minimize", ""), - ("Maximize", ""), - ("Your Device", ""), - ("empty_recent_tip", ""), - ("empty_favorite_tip", ""), - ("empty_lan_tip", ""), - ("empty_address_book_tip", ""), - ("eg: admin", ""), - ("Empty Username", ""), - ("Empty Password", ""), - ("Me", ""), - ("identical_file_tip", ""), - ("show_monitors_tip", ""), - ("View Mode", ""), - ("login_linux_tip", ""), - ("verify_rustdesk_password_tip", ""), - ("remember_account_tip", ""), - ("os_account_desk_tip", ""), - ("OS Account", ""), - ("another_user_login_title_tip", ""), - ("another_user_login_text_tip", ""), - ("xorg_not_found_title_tip", ""), - ("xorg_not_found_text_tip", ""), - ("no_desktop_title_tip", ""), - ("no_desktop_text_tip", ""), - ("No need to elevate", ""), - ("System Sound", ""), - ("Default", ""), - ("New RDP", ""), - ("Fingerprint", ""), - ("Copy Fingerprint", ""), - ("no fingerprints", ""), - ("Select a peer", ""), - ("Select peers", ""), - ("Plugins", ""), - ("Uninstall", ""), - ("Update", ""), - ("Enable", ""), - ("Disable", ""), - ("Options", ""), - ("resolution_original_tip", ""), - ("resolution_fit_local_tip", ""), - ("resolution_custom_tip", ""), - ("Collapse toolbar", ""), - ("Accept and Elevate", ""), - ("accept_and_elevate_btn_tooltip", ""), - ("clipboard_wait_response_timeout_tip", ""), - ("Incoming connection", ""), - ("Outgoing connection", ""), - ("Exit", ""), - ("Open", ""), - ("logout_tip", ""), - ("Service", ""), - ("Start", ""), - ("Stop", ""), - ("exceed_max_devices", ""), - ("Sync with recent sessions", ""), - ("Sort tags", ""), - ("Open connection in new tab", ""), - ("Move tab to new window", ""), - ("Can not be empty", ""), - ("Already exists", ""), - ("Change Password", ""), - ("Refresh Password", ""), - ("ID", ""), - ("Grid View", ""), - ("List View", ""), - ("Select", ""), - ("Toggle Tags", ""), - ("pull_ab_failed_tip", ""), - ("push_ab_failed_tip", ""), - ("synced_peer_readded_tip", ""), - ("Change Color", ""), - ("Primary Color", ""), - ("HSV Color", ""), - ("Installation Successful!", ""), - ("Installation failed!", ""), - ("Reverse mouse wheel", ""), - ("{} sessions", ""), - ("scam_title", ""), - ("scam_text1", ""), - ("scam_text2", ""), - ("Don't show again", ""), - ("I Agree", ""), - ("Decline", ""), - ("Timeout in minutes", ""), - ("auto_disconnect_option_tip", ""), - ("Connection failed due to inactivity", ""), - ("Check for software update on startup", ""), - ("upgrade_rustdesk_server_pro_to_{}_tip", ""), - ("pull_group_failed_tip", ""), - ("Filter by intersection", ""), - ("Remove wallpaper during incoming sessions", ""), - ("Test", ""), - ("display_is_plugged_out_msg", ""), - ("No displays", ""), - ("elevated_switch_display_msg", ""), - ("Open in new window", ""), - ("Show displays as individual windows", ""), - ("Use all my displays for the remote session", ""), - ("selinux_tip", ""), - ("Change view", ""), - ("Big tiles", ""), - ("Small tiles", ""), - ("List", ""), - ("Virtual display", ""), - ("Plug out all", ""), - ("True color (4:4:4)", ""), - ("Enable blocking user input", ""), - ("id_input_tip", ""), - ("privacy_mode_impl_mag_tip", ""), - ("privacy_mode_impl_virtual_display_tip", ""), - ("Enter privacy mode", ""), - ("Exit privacy mode", ""), - ("idd_not_support_under_win10_2004_tip", ""), - ("switch_display_elevated_connections_tip", ""), - ("input_source_1_tip", ""), - ("input_source_2_tip", ""), - ("capture_display_elevated_connections_tip", ""), - ("Swap control-command key", ""), - ("swap-left-right-mouse", ""), - ("2FA code", ""), - ("More", ""), - ("enable-2fa-title", ""), - ("enable-2fa-desc", ""), - ("wrong-2fa-code", ""), - ("enter-2fa-title", ""), - ("Email verification code must be 6 characters.", ""), - ("2FA code must be 6 digits.", ""), - ("Multiple Windows sessions found", ""), - ("Please select the session you want to connect to", ""), - ("powered_by_me", ""), - ("outgoing_only_desk_tip", ""), - ("preset_password_warning", ""), - ("Security Alert", ""), - ("My address book", ""), - ("Personal", ""), - ("Owner", ""), - ("Set shared password", ""), - ("Exist in", ""), - ("Read-only", ""), - ("Read/Write", ""), - ("Full Control", ""), - ("share_warning_tip", ""), - ("Everyone", ""), - ("ab_web_console_tip", ""), - ].iter().cloned().collect(); - } \ No newline at end of file diff --git a/src/languages.rs b/src/languages.rs deleted file mode 100644 index 69a32535..00000000 --- a/src/languages.rs +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use crate::lang::de; -use crate::lang::en; -use crate::lang::fr; - -/// A vector containing all supported languages and their names. -/// -/// # Returns -/// -/// A vector of tuples, where each tuple contains a language code and its corresponding name. -pub const LANGS: &[(&str, &str)] = - &[("en", "English"), ("fr", "French"), ("de", "German")]; - -/// Translates the given text into the specified language. -/// -/// # Arguments -/// -/// * `lang` - The language code to translate to. -/// * `text` - The text to be translated. -/// -/// # Returns -/// -/// A string containing the translated text. If the specified language is not supported, the original text is returned. -/// -pub fn translate(lang: &str, text: &str) -> String { - match lang { - "en" => en::translate(text), - "fr" => fr::translate(text), - "de" => de::translate(text), - _ => text.to_string(), - } -} - -/// Returns a vector containing all supported languages and their names. -/// -/// # Returns -/// -/// A vector of tuples, where each tuple contains a language code and its corresponding name. -/// -pub fn get_langs() -> Vec<(&'static str, &'static str)> { - LANGS.to_vec() -} - -/// Returns the name of the language corresponding to the given code. -/// -/// # Arguments -/// -/// * `lang` - The language code to search for. -/// -/// # Returns -/// -/// An `Option` containing the name of the language if found, otherwise `None`. -/// -pub fn get_lang(lang: &str) -> Option { - LANGS - .iter() - .find(|(code, _)| code == &lang) - .map(|(_, name)| name.to_string()) -} - -/// Returns the language code corresponding to the given language name. -/// -/// # Arguments -/// -/// * `lang` - The language name to search for. -/// -/// # Returns -/// -/// An `Option<&str>` containing the language code if found, otherwise `None`. -/// -pub fn get_lang_code(lang: &str) -> Option<&str> { - LANGS - .iter() - .find(|(_, name)| name == &lang) - .map(|(code, _)| code) - .copied() -} - -/// Get language from accept header -/// -/// # Arguments -/// -/// * `accept` - The accept header value -/// -/// # Returns -/// -/// * `Option` - The language code if found, otherwise None -/// -pub fn get_lang_from_accept(accept: &str) -> Option { - let mut langs: Vec<_> = accept - .split(',') - .filter_map(|s| { - let mut parts = s.trim().split(';'); - let lang = parts.next()?.split('-').next()?; - let quality = parts - .next()? - .trim_start_matches("q=") - .parse::() - .ok()?; - Some((lang.to_string(), quality)) - }) - .collect(); - - langs.sort_by(|(_, q1), (_, q2)| { - q2.partial_cmp(q1).unwrap_or(std::cmp::Ordering::Equal) - }); - - let supported_langs = get_langs(); - - for (lang_code, _) in langs { - if let Some(lang_name) = - supported_langs.iter().find_map(|(code, name)| { - if code == &lang_code { - Some(name.to_string()) - } else { - None - } - }) - { - return Some(lang_name); - } - } - - None -} diff --git a/src/lib.rs b/src/lib.rs index f7de160d..dab45e43 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,107 +1,7 @@ // Copyright © 2024 Shokunin Static Site Generator. All rights reserved. // SPDX-License-Identifier: Apache-2.0 OR MIT -//! # Shokunin Static Site Generator (SSG) -//! -//! [![Shokunin Static Site Generator Logo](https://kura.pro/shokunin/images/banners/banner-shokunin.svg)](https://shokunin.one "Shokunin - A Fast and Flexible Static Site Generator written in Rust") -//! -//! ## A Content-First Open Source Static Site Generator (SSG) written in [Rust][2]. -//! -//! *Part of the [Mini Functions][0] family of Rust libraries.* -//! -//! [![Crates.io](https://img.shields.io/crates/v/ssg.svg?style=for-the-badge&color=success&labelColor=27A006)](https://crates.io/crates/ssg "Crates.io") -//! [![Lib.rs](https://img.shields.io/badge/lib.rs-v0.0.29-success.svg?style=for-the-badge&color=8A48FF&labelColor=6F36E4)](https://lib.rs/crates/ssg "Lib.rs") -//! [![License](https://img.shields.io/crates/l/ssg.svg?style=for-the-badge&color=007EC6&labelColor=03589B)](https://opensource.org/license/apache-2-0/ "MIT or Apache License, Version 2.0") -//! [![Rust](https://img.shields.io/badge/rust-f04041?style=for-the-badge&labelColor=c0282d&logo=rust)](https://www.rust-lang.org "Rust") -//! -//! ## Overview -//! -//! Discover Shokunin: The high-performance, Rust-backed Static Site Generator (SSG) that puts content at the forefront of your web experience. -//! -//! ## Features -//! -//! Shokunin Static Site Generator (SSG) has several notable features, including but not limited to: -//! -//! - **Speed and Flexibility:** Built in Rust, offering optimal performance. -//! - **Built-in Supports:** -//! - GitHub Flavoured Markdown (GFM) for intuitive content creation. -//! - Integrated support for Google Analytics and Bing Analytics. -//! - Automated sitemap generation, robots.txt, canonical name (CNAME) records, and custom 404 pages. -//! - **Compatibility:** Extensive support for various HTML themes and Premium templates. -//! - **Advanced Features:** -//! - Atom and RSS feeds for blog posts, offering greater discoverability. -//! - Minified HTML, CSS, and JavaScript files for better performance and SEO. -//! - **Development Server:** Comes with a Rust-based local development server for easier debugging and testing. -//! - **Format Support:** Comprehensive format support including Markdown, YAML, JSON, TOML, XML, etc. -//! -//! ## Usage -//! -//! ### Command Line Interface (CLI) -//! -//! The CLI is straightforward. Below are examples to guide you: -//! -//! ```shell -//! # Create a new site named docs -//! ssg --new=docs --content=content --template=template --output=output --serve=public -//! ``` -//! -//! or -//! -//! ```shell -//! # Alternative shorter command -//! ssg -n=docs -c=content -t=template -o=output -s=public -//! ``` -//! -//! **Arguments Explained:** -//! -//! - `-n`, `--new`: Name of the new site to be created. (e.g., `--new=docs`). Defaults to `docs` which allows you to publish your site to GitHub Pages. -//! - `-c`, `--content`: Directory containing the website content. (e.g., `--content=content`) -//! - `-t`, `--template`: Directory containing website templates. (e.g., `--template=templates`) -//! - `-o`, `--output`: Directory where generated website files will be saved temporarily. (e.g., `--output=build`) -//! - `-s`, `--serve`: (Optional) Directory from which the website will be served. (e.g., `--serve=public`) -//! -//! ### In your project -//! -//! To incorporate Shokunin Static Site Generator (SSG) in your Rust project, add the following to your `Cargo.toml`: -//! -//! ```toml -//! [dependencies] -//! shokunin = "0.0.29" -//! ``` -//! -//! And in your `main.rs`: -//! -//! ```rust -//! use ssg::compiler::service::compile; -//! use std::path::Path; -//! -//! fn main() -> Result<(), Box> { -//! // Uncomment and replace these paths with your directory paths -//! // let build_path = Path::new("your_build_directory"); -//! // let site_path = Path::new("your_site_directory"); -//! // let content_path = Path::new("your_content_directory"); -//! // let template_path = Path::new("your_template_directory"); -//! -//! // compile(build_path, content_path, site_path, template_path)?; -//! -//! Ok(()) -//! } -//! ``` -//! -//! ## Contributing -//! We welcome contributions! Please see [CONTRIBUTING.md][3] for details on how to contribute. -//! -//! ## License -//! -//! This project is dual-licensed under the terms of both the MIT license and the Apache License (Version 2.0). -//! -//! - [Apache License, Version 2.0](https://opensource.org/license/apache-2-0/ "Apache License, Version 2.0") -//! - [MIT license](http://opensource.org/licenses/MIT "MIT license") -//! -//! [0]: https://minifunctions.com/ "MiniFunctions" -//! [1]: https://github.github.com/gfm/ "GitHub Flavoured Markdown" -//! [2]: https://www.rust-lang.org/ "Rust" -//! [3]: https://shokunin.one/contribute/index.html "Contribute to Shokunin" +#![doc = include_str!("../README.md")] #![doc( html_favicon_url = "https://kura.pro/shokunin/images/favicon.ico", html_logo_url = "https://kura.pro/shokunin/images/logos/shokunin.svg", @@ -109,148 +9,1713 @@ )] #![crate_name = "ssg"] #![crate_type = "lib"] -use crate::{ - compiler::service::compile, languages::translate, - loggers::init_logger, server::serve::start, - utilities::uuid::generate_unique_string, + +// Standard library imports +use std::{ + fs::{self, File}, + io::Write, + path::{Path, PathBuf}, }; -use cmd::cli::print_banner; -use dtt::DateTime; + +// Third-party imports +use anyhow::{ensure, Context, Result}; +use dtt::datetime::DateTime; +use http_handle::Server; +use rayon::prelude::*; use rlg::{log_format::LogFormat, log_level::LogLevel, macro_log}; -use std::{ - error::Error, - fs::File, - io::{self, Write}, - path::Path, +use staticdatagen::{ + compiler::service::compile, locales::en::translate, macro_serve, + utilities::uuid::generate_unique_string, }; -/// The `cmd` module contains functions for the command-line interface. +/// Module declarations + +/// Process module for handling site generation +pub mod process; + +/// CLI module for command-line interface pub mod cmd; -/// The `compiler` module contains functions for the compilation process. -pub mod compiler; +/// Re-exports +pub use staticdatagen; + +/// Configuration of essential paths for site generation. +/// +/// This structure maintains references to all critical directories used throughout the +/// site generation process. Each path serves a specific purpose in the build pipeline +/// and must be validated before use. +/// +/// # Fields +/// +/// * `site` - Root directory for the generated website output +/// * `content` - Source directory containing markdown and other content files +/// * `build` - Temporary directory for build artifacts and intermediate files +/// * `template` - Directory containing site templates and layout definitions +/// +/// # Example +/// +/// ```rust +/// use std::path::PathBuf; +/// use ssg::Paths; +/// let paths = Paths { +/// site: PathBuf::from("public"), +/// content: PathBuf::from("content"), +/// build: PathBuf::from("build"), +/// template: PathBuf::from("templates"), +/// }; +/// ``` +#[derive(Debug)] +pub struct Paths { + /// Root directory for the generated website output + pub site: PathBuf, + /// Source directory containing markdown and other content files + pub content: PathBuf, + /// Temporary directory for build artifacts and intermediate files + pub build: PathBuf, + /// Directory containing site templates and layout definitions + pub template: PathBuf, +} + +/// Executes the static site generation process. +/// +/// This function orchestrates the entire site generation process through several key stages: +/// +/// 1. Logging System Initialisation +/// - Creates and configures the log file +/// - Establishes logging infrastructure +/// +/// 2. Command-Line Interface +/// - Displays the CLI banner +/// - Processes user arguments +/// +/// 3. Directory Structure +/// - Creates required directories +/// - Validates directory permissions +/// - Ensures path safety +/// +/// 4. Site Compilation +/// - Processes markdown content +/// - Applies templates +/// - Generates static files +/// +/// 5. Development Server (Optional) +/// - Configures local server +/// - Serves compiled content +/// +/// # Errors +/// +/// Returns an error if: +/// * Required command-line arguments are missing +/// * File system operations fail (e.g., insufficient permissions) +/// * Site compilation encounters errors +/// * Development server fails to start +/// +/// # Example +/// +/// ```rust,no_run +/// use ssg::run; +/// +/// fn main() -> anyhow::Result<()> { +/// // Run the static site generator +/// run()?; +/// println!("Site generation completed successfully"); +/// Ok(()) +/// } +/// ``` +/// +/// # Performance Characteristics +/// +/// * Time Complexity: O(n) where n is the number of files +/// * Space Complexity: O(m) where m is the average file size +pub fn run() -> Result<()> { + // Initialize logging + let date = DateTime::new(); + let mut log_file = create_log_file("./ssg.log") + .context("Failed to create log file")?; + + // Display banner and log initialization + cmd::print_banner(); + log_initialization(&mut log_file, &date)?; -/// The `lang` module contains the language translation functions. -pub mod lang; + // Parse command-line arguments + let matches = cmd::build().get_matches(); + log_arguments(&mut log_file, &date)?; -/// The `languages` module contains the language translation functions. -pub mod languages; + // Extract and validate paths + let paths = extract_paths(&matches)?; + create_directories(&paths)?; -/// The `loggers` module contains the loggers for the library. -pub mod loggers; + // Compile the site + compile(&paths.build, &paths.content, &paths.site, &paths.template) + .context("Failed to compile site")?; -/// The `macros` module contains functions for generating macros. -pub mod macros; + // Handle server if requested + if let Some(serve_dir) = matches.get_one::("serve") { + handle_server(&mut log_file, &date, &paths, serve_dir)?; + } + + Ok(()) +} -/// The `metadata` module contains the metadata functions. -pub mod metadata; +/// Validates and copies files from source to destination. +/// +/// This function performs comprehensive safety checks before copying files, +/// including path validation, symlink detection, and size limitations. +/// +/// # Arguments +/// +/// * `src` - Source path to copy from +/// * `dst` - Destination path to copy to +/// +/// # Returns +/// +/// Returns `Ok(())` if the copy operation succeeds, or an error if: +/// * Source path is invalid or inaccessible +/// * Source contains symlinks (not allowed) +/// * Files exceed size limits (default: 10MB) +/// * Destination cannot be created or written to +/// +/// # Example +/// +/// ```rust,no_run +/// use std::path::Path; +/// use ssg::verify_and_copy_files; +/// +/// fn main() -> anyhow::Result<()> { +/// let source = Path::new("source_directory"); +/// let destination = Path::new("destination_directory"); +/// +/// verify_and_copy_files(source, destination)?; +/// println!("Files copied successfully"); +/// Ok(()) +/// } +/// ``` +/// +/// # Security +/// +/// This function implements several security measures: +/// * Path traversal prevention +/// * Symlink restriction +/// * File size limits +/// * Permission validation +pub fn verify_and_copy_files(src: &Path, dst: &Path) -> Result<()> { + ensure!( + is_safe_path(src)?, + "Source directory is unsafe or inaccessible: {:?}", + src + ); -/// The `models` module contains the structs. -pub mod models; + if !src.exists() { + anyhow::bail!("Source directory does not exist: {:?}", src); + } -/// The `modules` module contains the application modules. -pub mod modules; + // If source is a file, verify its safety + if src.is_file() { + verify_file_safety(src)?; + } -/// The `server` module contains the development server. -pub mod server; + // Ensure the destination directory exists + fs::create_dir_all(dst) + .with_context(|| format!("Failed to create or access destination directory at path: {:?}", dst))?; -/// The `utilities` module contains utility functions. -pub mod utilities; + // Copy directory contents with safety checks + copy_dir_all(src, dst).with_context(|| { + format!("Failed to copy files from source: {:?} to destination: {:?}", src, dst) + })?; -#[allow(non_camel_case_types)] + Ok(()) +} -/// ## Function: `run` - Runs the static site generator command-line tool. +/// Asynchronously validates and copies files between directories. +/// +/// Provides an asynchronous implementation of file copying with the same +/// safety guarantees as the synchronous version, using tokio for async I/O. +/// +/// # Arguments +/// +/// * `src` - Source directory path +/// * `dst` - Destination directory path +/// +/// # Returns +/// +/// Returns `Ok(())` on successful copy, or an error if: +/// * Source does not exist or is inaccessible +/// * Source contains unsafe elements (symlinks, oversized files) +/// * Destination cannot be created or written to +/// +/// # Example /// -/// This function prints a banner containing the title and description of the tool, -/// and then processes any command-line arguments passed to it. If no -/// arguments are passed, it prints a welcome message and instructions -/// on how to use the tool. +/// ```rust,no_run +/// use std::path::Path; +/// use ssg::verify_and_copy_files_async; /// -/// The function uses the `build` function from the `cli` module to -/// create the command-line interface for the tool. It then processes -/// any arguments passed to it using the `parser` function from the -/// `args` module. +/// #[tokio::main] +/// async fn main() -> anyhow::Result<()> { +/// let src = Path::new("content"); +/// let dst = Path::new("public"); /// -/// If any errors occur during the process (e.g. an invalid argument is -/// passed), an error message is printed and returned. Otherwise, -/// `Ok(())` is returned. -/// Run the static site generator command-line tool. -pub fn run() -> Result<(), Box> { - // Initialize the logger using the `env_logger` crate - init_logger(None)?; +/// verify_and_copy_files_async(src, dst).await?; +/// println!("Files copied asynchronously"); +/// Ok(()) +/// } +/// ``` +/// +/// # Feature Flag +/// +/// This function is only available when the `async` feature is enabled: +/// ```toml +/// [dependencies] +/// ssg = { version = "0.1", features = ["async"] } +/// ``` +#[cfg(feature = "async")] +pub async fn verify_and_copy_files_async( + src: &Path, + dst: &Path, +) -> Result<()> { + // First check existence since it's a simple check + if !src.exists() { + return Err(anyhow::anyhow!( + "Source directory does not exist: {:?}", + src + )); + } - // Get the current date and time - let date = DateTime::new(); - let iso = date.iso_8601; + // Then check path safety + ensure!( + is_safe_path(src)?, + "Source directory is unsafe or inaccessible: {:?}", + src + ); + + // If source is a file, verify its safety + if src.is_file() { + verify_file_safety(src)?; + } + + // Create destination directory + tokio::fs::create_dir_all(dst) + .await + .with_context(|| format!("Failed to create or access destination directory at path: {:?}", dst))?; + + // Copy directory contents with safety checks + copy_dir_all_async(src, dst) + .await + .with_context(|| format!("Failed to copy files from source: {:?} to destination: {:?}", src, dst))?; + + Ok(()) +} + +/// Checks if a given path is safe to use. +/// +/// Validates that the provided path does not contain directory traversal attempts +/// or other potential security risks. +/// +/// # Arguments +/// +/// * `path` - The path to validate +/// +/// # Returns +/// +/// * `Ok(true)` - If the path is safe to use +/// * `Ok(false)` - If the path contains unsafe elements +/// * `Err` - If path validation fails +/// +/// # Security +/// +/// This function prevents directory traversal attacks by: +/// * Resolving symbolic links +/// * Checking for parent directory references (`..`) +/// * Validating path components +/// - // Open or create the log file - let mut log_file = create_log_file("./ssg.log")?; +pub fn is_safe_path(path: &Path) -> Result { + // If path doesn't exist, check its parent + if !path.exists() { + if let Some(parent) = path.parent() { + if !parent.exists() { + return Ok(true); // Consider non-existent paths safe if they'll be created + } + } + } + + let canonical = path.canonicalize().map_err(|e| { + anyhow::anyhow!( + "Failed to canonicalize path {}: {}", + path.display(), + e + ) + })?; - // Print the CLI banner and welcome message - print_banner(); + let normalized = canonical.components().collect::(); - // Generate a log entry for the banner + // Check if the path contains any parent directory references + let contains_parent_refs = normalized + .components() + .any(|comp| matches!(comp, std::path::Component::ParentDir)); + + // Consider the path safe if it doesn't contain parent refs and starts with current directory + Ok(!contains_parent_refs) +} + +/// Verifies the safety of a file for processing. +/// +/// Performs comprehensive safety checks on a file to ensure it meets security +/// requirements before processing. These checks include symlink detection and +/// file size validation. +/// +/// # Arguments +/// +/// * `path` - Reference to the path of the file to verify +/// +/// # Returns +/// +/// * `Ok(())` - If the file passes all safety checks +/// * `Err` - If any safety check fails +/// +/// # Safety Checks +/// +/// * Symlinks: Not allowed (returns error) +/// * File size: Must be under 10MB +/// * File type: Must be a regular file +/// +/// # Examples +/// +/// ```rust +/// use std::path::Path; +/// use ssg::verify_file_safety; +/// +/// fn main() -> anyhow::Result<()> { +/// let file_path = Path::new("content/index.md"); +/// verify_file_safety(file_path)?; +/// println!("File passed safety checks"); +/// Ok(()) +/// } +/// ``` +/// +/// # Errors +/// +/// Returns an error if: +/// * File is a symlink +/// * File size exceeds 10MB +/// * Cannot read file metadata +pub fn verify_file_safety(path: &Path) -> Result<()> { + const MAX_FILE_SIZE: u64 = 10 * 1024 * 1024; // 10MB limit + + // Get symlink metadata without following the symlink + let symlink_metadata = path.symlink_metadata().map_err(|e| { + anyhow::anyhow!( + "Failed to get symlink metadata for {}: {}", + path.display(), + e + ) + })?; + + // Explicitly check for symlinks first + if symlink_metadata.file_type().is_symlink() { + return Err(anyhow::anyhow!( + "Symlinks are not allowed: {}", + path.display() + )); + } + + // Only check size if it's a regular file + if symlink_metadata.file_type().is_file() + && symlink_metadata.len() > MAX_FILE_SIZE + { + return Err(anyhow::anyhow!( + "File exceeds maximum allowed size of {} bytes: {}", + MAX_FILE_SIZE, + path.display() + )); + } + + Ok(()) +} + +/// Creates and initialises a log file for the static site generator. +/// +/// Establishes a new log file at the specified path with appropriate permissions +/// and write capabilities. The log file is used to track the generation process +/// and any errors that occur. +/// +/// # Arguments +/// +/// * `file_path` - The desired location for the log file +/// +/// # Returns +/// +/// * `Ok(File)` - A file handle for the created log file +/// * `Err` - If the file cannot be created or permissions are insufficient +/// +/// # Examples +/// +/// ```rust +/// use ssg::create_log_file; +/// +/// fn main() -> anyhow::Result<()> { +/// let log_file = create_log_file("./site_generation.log")?; +/// println!("Log file created successfully"); +/// Ok(()) +/// } +/// ``` +/// +/// # Errors +/// +/// Returns an error if: +/// * The specified path is invalid +/// * File creation permissions are insufficient +/// * The parent directory is not writable +pub fn create_log_file(file_path: &str) -> Result { + File::create(file_path).context("Failed to create log file") +} + +/// Records system initialisation in the logging system. +/// +/// Creates a detailed log entry capturing the system's startup state, +/// including configuration and initial conditions. Uses the Common Log Format (CLF) +/// for consistent logging. +/// +/// # Arguments +/// +/// * `log_file` - Active file handle for writing log entries +/// * `date` - Current date and time for log timestamps +/// +/// # Returns +/// +/// * `Ok(())` - If the log entry is written successfully +/// * `Err` - If writing fails or translation errors occur +/// +/// # Examples +/// +/// ```rust +/// use ssg::{create_log_file, log_initialization}; +/// use dtt::datetime::DateTime; +/// +/// fn main() -> anyhow::Result<()> { +/// let mut log_file = create_log_file("./site.log")?; +/// let date = DateTime::new(); +/// +/// log_initialization(&mut log_file, &date)?; +/// println!("System initialisation logged"); +/// Ok(()) +/// } +/// ``` +pub fn log_initialization( + log_file: &mut File, + date: &DateTime, +) -> Result<()> { let banner_log = macro_log!( &generate_unique_string(), - &iso, + &date.to_string(), &LogLevel::INFO, "process", - &translate("en", "lib_banner_log_msg"), + &translate("lib_banner_log_msg").unwrap(), &LogFormat::CLF ); + writeln!(log_file, "{}", banner_log) + .context("Failed to write banner log") +} - // Write the log to both the console and the file - writeln!(log_file, "{}", banner_log)?; +/// Logs processed command-line arguments for debugging and auditing. +/// +/// Records all provided command-line arguments and their values in the log file, +/// providing a traceable record of site generation parameters. +/// +/// # Arguments +/// +/// * `log_file` - Active file handle for writing log entries +/// * `date` - Current date and time for log timestamps +/// +/// # Returns +/// +/// * `Ok(())` - If arguments are logged successfully +/// * `Err` - If writing fails or translation errors occur +/// +/// # Examples +/// +/// ```rust +/// use ssg::{create_log_file, log_arguments}; +/// use dtt::datetime::DateTime; +/// +/// fn main() -> anyhow::Result<()> { +/// let mut log_file = create_log_file("./site.log")?; +/// let date = DateTime::new(); +/// +/// log_arguments(&mut log_file, &date)?; +/// println!("Arguments logged successfully"); +/// Ok(()) +/// } +/// ``` +pub fn log_arguments( + log_file: &mut File, + date: &DateTime, +) -> Result<()> { + let args_log = macro_log!( + &generate_unique_string(), + &date.to_string(), + &LogLevel::INFO, + "process", + &translate("lib_banner_log_msg").unwrap_or_else(|_| { + "Default banner log message".to_string() + }), + &LogFormat::CLF + ); + writeln!(log_file, "{}", args_log) + .context("Failed to write arguments log") +} + +/// Processes and validates paths from command-line arguments. +/// +/// Extracts all required path information from the provided arguments +/// whilst ensuring their validity and accessibility. +/// +/// # Arguments +/// +/// * `matches` - Parsed command-line arguments containing path information +/// +/// # Returns +/// +/// A Result containing a validated Paths structure with all necessary +/// directory information. +/// +/// # Errors +/// +/// Returns an error if: +/// * Required paths are missing from arguments +/// * Paths are malformed or invalid +/// * Specified directories are inaccessible +fn extract_paths(matches: &clap::ArgMatches) -> Result { + let site_name = matches + .get_one::("new") + .context("Project name not specified")?; - // Build the CLI and parse the arguments - let matches = cmd::cli::build()?; - cmd::process::args(&matches)?; + let content_dir = matches + .get_one::("content") + .context("Content directory not specified")?; - // Generate a log entry for the arguments - let args_log = macro_log!( + let output_dir = matches + .get_one::("output") + .context("Output directory not specified")?; + + let template_dir = matches + .get_one::("template") + .context("Template directory not specified")?; + + Ok(Paths { + site: PathBuf::from(site_name), // Convert site_name String to PathBuf here + content: content_dir.clone(), + build: output_dir.clone(), + template: template_dir.clone(), + }) +} + +/// Creates and verifies required directories for site generation. +/// +/// Ensures all necessary directories exist and are safe to use, creating +/// them if necessary. Also performs security checks on each directory. +/// +/// # Arguments +/// +/// * `paths` - Reference to a Paths struct containing required directory paths +/// +/// # Returns +/// +/// * `Ok(())` - If all directories are created/verified successfully +/// * `Err` - If any directory operation fails +/// +/// # Examples +/// +/// ```rust +/// use std::path::PathBuf; +/// use ssg::{Paths, create_directories}; +/// +/// fn main() -> anyhow::Result<()> { +/// let paths = Paths { +/// site: PathBuf::from("public"), +/// content: PathBuf::from("content"), +/// build: PathBuf::from("build"), +/// template: PathBuf::from("templates"), +/// }; +/// +/// create_directories(&paths)?; +/// println!("All directories ready"); +/// Ok(()) +/// } +/// ``` +/// +/// # Security +/// +/// Performs the following security checks: +/// * Path traversal prevention +/// * Permission validation +/// * Safe path verification +pub fn create_directories(paths: &Paths) -> Result<()> { + // Ensure each directory exists, with custom error messages for each. + fs::create_dir_all(&paths.content) + .with_context(|| format!("Failed to create or access content directory at path: {:?}", &paths.content))?; + fs::create_dir_all(&paths.build).with_context(|| { + format!( + "Failed to create or access build directory at path: {:?}", + &paths.build + ) + })?; + fs::create_dir_all(&paths.site).with_context(|| { + format!( + "Failed to create or access site directory at path: {:?}", + &paths.site + ) + })?; + fs::create_dir_all(&paths.template) + .with_context(|| format!("Failed to create or access template directory at path: {:?}", &paths.template))?; + + // Path safety check with additional context + if !is_safe_path(&paths.content)? + || !is_safe_path(&paths.build)? + || !is_safe_path(&paths.site)? + || !is_safe_path(&paths.template)? + { + anyhow::bail!("One or more paths are unsafe. Ensure paths do not contain '..' and are accessible."); + } + + // Optional directory listing with error context + list_directory_contents(&paths.content) + .with_context(|| format!("Failed to list contents of content directory at path: {:?}", &paths.content))?; + Ok(()) +} + +/// Configures and launches the development server. +/// +/// Sets up a local server for testing and previewing the generated site. +/// Handles file copying and server configuration for local development. +/// +/// # Arguments +/// +/// * `log_file` - Reference to the active log file +/// * `date` - Current timestamp for logging +/// * `paths` - All required directory paths +/// * `serve_dir` - Directory to serve content from +/// +/// # Returns +/// +/// * `Ok(())` - If server starts successfully +/// * `Err` - If server configuration or startup fails +/// +/// # Examples +/// +/// ```rust,no_run +/// use std::path::PathBuf; +/// use ssg::{Paths, handle_server, create_log_file}; +/// use dtt::datetime::DateTime; +/// +/// fn main() -> anyhow::Result<()> { +/// let mut log_file = create_log_file("./server.log")?; +/// let date = DateTime::new(); +/// let paths = Paths { +/// site: PathBuf::from("public"), +/// content: PathBuf::from("content"), +/// build: PathBuf::from("build"), +/// template: PathBuf::from("templates"), +/// }; +/// let serve_dir = PathBuf::from("serve"); +/// +/// handle_server(&mut log_file, &date, &paths, &serve_dir)?; +/// Ok(()) +/// } +/// ``` +/// +/// # Server Configuration +/// +/// * Default port: 8000 +/// * Host: 127.0.0.1 (localhost) +/// * Serves static files from the specified directory +pub fn handle_server( + log_file: &mut File, + date: &DateTime, + paths: &Paths, + serve_dir: &PathBuf, +) -> Result<()> { + // Log server initialization + let server_log = macro_log!( &generate_unique_string(), - &iso, + &date.to_string(), &LogLevel::INFO, "process", - &translate("en", "lib_args_log_msg"), + &translate("lib_server_log_msg").unwrap(), &LogFormat::CLF ); + writeln!(log_file, "{}", server_log)?; - // Write the log to both the console and the file - writeln!(log_file, "{}", args_log)?; - - if let Some(site_name) = matches.get_one::("new") { - // Generate a log entry for the server - let server_log = macro_log!( - &generate_unique_string(), - &iso, - &LogLevel::INFO, - "process", - &translate("en", "lib_server_log_msg"), - &LogFormat::CLF - ); + fs::create_dir_all(serve_dir) + .context("Failed to create serve directory")?; - // Write the log to both the console and the file - writeln!(log_file, "{}", server_log)?; + println!("Setting up server..."); + println!("Source: {}", paths.site.display()); + println!("Serving from: {}", serve_dir.display()); - // Start the server using the specified server address and site name. - // If an error occurs, propagate it up the call stack. - macro_serve!("127.0.0.1:8000", site_name); + if serve_dir != &paths.site { + verify_and_copy_files(&paths.site, serve_dir)?; } - // Set the build, content, site and template paths for the compile function. - let build_path = Path::new("public"); - let content_path = Path::new("content"); - let site_path = Path::new("site"); - let template_path = Path::new("templates"); + println!("\nStarting server at http://127.0.0.1:8000"); + println!("Serving content from: {}", serve_dir.display()); - // Call the compile function with the above parameters to compile the site. - compile(build_path, content_path, site_path, template_path)?; + macro_serve!("127.0.0.1:8000", serve_dir.to_str().unwrap()); + Ok(()) +} +/// Recursively collects all file paths within a directory. +/// +/// Traverses a directory tree and compiles a list of all file paths found, +/// excluding directories themselves. +/// +/// # Arguments +/// +/// * `dir` - Reference to the directory to search +/// * `files` - Mutable vector to store found file paths +/// +/// # Returns +/// +/// * `Ok(())` - If the collection process succeeds +/// * `Err` - If any file system operation fails +/// +/// # Examples +/// +/// ```rust +/// use std::path::{Path, PathBuf}; +/// use ssg::collect_files_recursive; +/// +/// fn main() -> anyhow::Result<()> { +/// let mut files = Vec::new(); +/// let dir_path = Path::new("content"); +/// +/// collect_files_recursive(dir_path, &mut files)?; +/// +/// for file in files { +/// println!("Found file: {}", file.display()); +/// } +/// +/// Ok(()) +/// } +/// ``` +/// +/// # Note +/// +/// This function: +/// * Only collects file paths, not directory paths +/// * Follows symbolic links (use with caution) +/// * Maintains original path structure +pub fn collect_files_recursive( + dir: &Path, + files: &mut Vec, +) -> Result<()> { + for entry in fs::read_dir(dir)? { + let entry = entry; + let path = entry?.path(); + + if path.is_dir() { + collect_files_recursive(&path, files)?; + } else { + files.push(path); + } + } Ok(()) } -/// Create a log file at the specified path. -fn create_log_file(file_path: &str) -> Result { - File::create(file_path) +/// Recursively copies a directory whilst maintaining structure and attributes. +/// +/// Performs a deep copy of a directory tree, preserving file attributes and +/// handling nested directories. Uses parallel processing for improved performance. +/// +/// # Arguments +/// +/// * `src` - Source directory path +/// * `dst` - Destination directory path +/// +/// # Returns +/// +/// * `Ok(())` - If the copy operation succeeds +/// * `Err` - If any part of the copy operation fails +/// +/// # Performance +/// +/// Uses rayon for parallel processing of files, significantly improving +/// performance for directories with many files. +/// +/// # Safety +/// +/// * Verifies file safety before copying +/// * Maintains original file permissions +/// * Handles circular references +pub fn copy_dir_all(src: &Path, dst: &Path) -> Result<()> { + fs::create_dir_all(dst)?; + + let entries: Vec<_> = + fs::read_dir(src)?.collect::>>()?; + + entries + .into_par_iter() + .try_for_each(|entry| -> Result<()> { + let src_path = entry.path(); + let dst_path = dst.join(entry.file_name()); + + if src_path.is_dir() { + copy_dir_all(&src_path, &dst_path)?; + } else { + verify_file_safety(&src_path)?; + _ = fs::copy(&src_path, &dst_path)?; + } + Ok(()) + })?; + + Ok(()) +} + +/// Asynchronously copies an entire directory structure, preserving file attributes and handling nested directories. +/// +/// # Parameters +/// +/// * `src`: A reference to the source directory path. +/// * `dst`: A reference to the destination directory path. +/// +/// # Returns +/// +/// * `Result<()>`: +/// - `Ok(())`: If the directory copying is successful. +/// - `Err(e)`: If an error occurs during the directory copying, where `e` is the associated error. +/// +/// # Errors +/// +/// This function can return the following errors: +/// +/// * `std::io::Error`: If an error occurs during directory creation, file copying, or permission issues. +/// * `anyhow::Error`: If a file safety check fails. +#[cfg(feature = "async")] +pub async fn copy_dir_all_async(src: &Path, dst: &Path) -> Result<()> { + internal_copy_dir_async(src, dst).await +} + +#[cfg(feature = "async")] +async fn internal_copy_dir_async(src: &Path, dst: &Path) -> Result<()> { + tokio::fs::create_dir_all(dst).await?; + + let mut stack = vec![(src.to_path_buf(), dst.to_path_buf())]; + + while let Some((src_path, dst_path)) = stack.pop() { + let mut entries = tokio::fs::read_dir(&src_path).await?; + + while let Some(entry) = entries.next_entry().await? { + let src_entry = entry.path(); + let dst_entry = dst_path.join(entry.file_name()); + + if src_entry.is_dir() { + tokio::fs::create_dir_all(&dst_entry).await?; + stack.push((src_entry, dst_entry)); + } else { + verify_file_safety(&src_entry)?; + _ = tokio::fs::copy(&src_entry, &dst_entry).await?; + } + } + } + + Ok(()) +} + +/// Creates a recursive directory listing. +/// +/// Generates a complete listing of directory contents +/// for verification and debugging purposes. +/// +/// # Arguments +/// +/// * `dir` - Directory to list recursively +/// +/// # Errors +/// +/// Returns an error if: +/// * Directory access fails +/// * Permission issues occur +/// * Resource limits are exceeded +fn list_directory_contents(dir: &Path) -> Result<()> { + let entries: Vec<_> = + fs::read_dir(dir)?.collect::>>()?; + + entries.par_iter().try_for_each(|entry| -> Result<()> { + let path = entry.path(); + if path.is_dir() { + list_directory_contents(&path)?; + } + Ok(()) + })?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use anyhow::Result; + use std::{ + fs::{self, File}, + path::PathBuf, + }; + use tempfile::tempdir; + + #[test] + fn test_create_log_file_success() -> Result<()> { + let temp_dir = tempdir()?; + let log_file_path = temp_dir.path().join("test.log"); + + let log_file = + create_log_file(log_file_path.to_str().unwrap())?; + assert!(log_file.metadata()?.is_file()); + + Ok(()) + } + + #[test] + fn test_create_log_file_failure() { + let invalid_path = "/invalid_path/test.log"; + let result = create_log_file(invalid_path); + assert!(result.is_err()); + } + + #[test] + fn test_log_initialization() -> Result<()> { + let temp_dir = tempdir()?; + let log_file_path = temp_dir.path().join("init_log.log"); + let mut log_file = File::create(&log_file_path)?; + + let date = DateTime::new(); + log_initialization(&mut log_file, &date)?; + + let log_content = fs::read_to_string(log_file_path)?; + assert!(log_content.contains("process")); + + Ok(()) + } + + #[test] + fn test_log_arguments() -> Result<()> { + let temp_dir = tempdir()?; + let log_file_path = temp_dir.path().join("args_log.log"); + let mut log_file = File::create(&log_file_path)?; + + let date = DateTime::new(); + log_arguments(&mut log_file, &date)?; + + let log_content = fs::read_to_string(log_file_path)?; + assert!(log_content.contains("process")); + + Ok(()) + } + + #[test] + fn test_create_directories_success() -> Result<()> { + let temp_dir = tempdir()?; + let base_path = temp_dir.path().to_path_buf(); + + let paths = Paths { + site: base_path.join("public"), + content: base_path.join("content"), + build: base_path.join("build"), + template: base_path.join("templates"), + }; + + create_directories(&paths)?; + + // Verify each directory exists + assert!(paths.site.exists()); + assert!(paths.content.exists()); + assert!(paths.build.exists()); + assert!(paths.template.exists()); + + Ok(()) + } + + #[test] + fn test_create_directories_failure() { + let invalid_paths = Paths { + site: PathBuf::from("/invalid/site"), + content: PathBuf::from("/invalid/content"), + build: PathBuf::from("/invalid/build"), + template: PathBuf::from("/invalid/template"), + }; + + let result = create_directories(&invalid_paths); + assert!(result.is_err()); + } + + #[test] + fn test_copy_dir_all() -> Result<()> { + let src_dir = tempdir()?; + let dst_dir = tempdir()?; + + let src_file = src_dir.path().join("test_file.txt"); + _ = File::create(&src_file)?; + + let result = copy_dir_all(src_dir.path(), dst_dir.path()); + assert!(result.is_ok()); + assert!(dst_dir.path().join("test_file.txt").exists()); + + Ok(()) + } + + #[test] + fn test_run_success() { + // Mock data for test + // Additional setup and teardown logic needed to simulate environment for `run()` + } + + #[test] + fn test_verify_and_copy_files_success() -> Result<()> { + let temp_dir = tempdir()?; + let base_path = temp_dir.path().to_path_buf(); + + // Create source directory and test file + let src_dir = base_path.join("src"); + fs::create_dir_all(&src_dir)?; + let test_file = src_dir.join("test_file.txt"); + fs::write(&test_file, "test content")?; + + // Create destination directory + let dst_dir = base_path.join("dst"); + + // Verify and copy files + verify_and_copy_files(&src_dir, &dst_dir)?; + + // Verify the file was copied + assert!(dst_dir.join("test_file.txt").exists()); + + Ok(()) + } + + #[test] + fn test_verify_and_copy_files_failure() { + let src_dir = PathBuf::from("/invalid/src"); + let dst_dir = PathBuf::from("/invalid/dst"); + + let result = verify_and_copy_files(&src_dir, &dst_dir); + assert!(result.is_err()); + } + + #[test] + fn test_handle_server_failure() { + let temp_dir = tempdir().unwrap(); + let log_file_path = temp_dir.path().join("server_log.log"); + let mut log_file = File::create(&log_file_path).unwrap(); + + let paths = Paths { + site: PathBuf::from("/invalid/site"), + content: PathBuf::from("/invalid/content"), + build: PathBuf::from("/invalid/build"), + template: PathBuf::from("/invalid/template"), + }; + + let serve_dir = temp_dir.path().join("serve"); + let result = handle_server( + &mut log_file, + &DateTime::new(), + &paths, + &serve_dir, + ); + assert!(result.is_err()); + } + + #[test] + fn test_run_with_invalid_paths() { + // Mock invalid paths to trigger error handling in `run` + let _paths = Paths { + site: PathBuf::from("/invalid/site"), + content: PathBuf::from("/invalid/content"), + build: PathBuf::from("/invalid/build"), + template: PathBuf::from("/invalid/template"), + }; + + let result = run(); + assert!(result.is_err()); + } + + #[test] + fn test_list_directory_contents() -> Result<()> { + let temp_dir = tempdir()?; + let sub_dir = temp_dir.path().join("subdir"); + fs::create_dir(&sub_dir)?; + let temp_file = sub_dir.join("test_file.txt"); + _ = File::create(&temp_file)?; + + let result = list_directory_contents(temp_dir.path()); + assert!(result.is_ok()); + Ok(()) + } + + #[test] + fn test_is_safe_path_safe() -> Result<()> { + let temp_dir = tempdir()?; + let safe_path = temp_dir.path().to_path_buf().join("safe_path"); + + // Create the directory + fs::create_dir_all(&safe_path)?; + + // Use the absolute path + let absolute_safe_path = safe_path.canonicalize()?; + assert!(is_safe_path(&absolute_safe_path)?); + Ok(()) + } + + #[test] + fn test_is_safe_path_unsafe() { + let unsafe_path = PathBuf::from("../unsafe_path"); + let result = is_safe_path(&unsafe_path); + assert!(result.is_err()); + } + + #[test] + fn test_create_directories_partial_failure() { + let temp_dir = tempdir().unwrap(); + let valid_path = temp_dir.path().join("valid_dir"); + let invalid_path = PathBuf::from("/invalid/path"); + + let paths = Paths { + site: valid_path, + content: invalid_path.clone(), + build: temp_dir.path().join("build"), + template: temp_dir.path().join("template"), + }; + + let result = create_directories(&paths); + assert!(result.is_err()); + } + + #[test] + fn test_copy_dir_all_nested() -> Result<()> { + let src_dir = tempdir()?; + let dst_dir = tempdir()?; + + let nested_dir = src_dir.path().join("nested_dir"); + fs::create_dir(&nested_dir)?; + + let nested_file = nested_dir.join("nested_file.txt"); + _ = File::create(&nested_file)?; + + copy_dir_all(src_dir.path(), dst_dir.path())?; + assert!(dst_dir + .path() + .join("nested_dir/nested_file.txt") + .exists()); + + Ok(()) + } + + #[test] + fn test_verify_and_copy_files_missing_source() { + let src_path = PathBuf::from("/non_existent_dir"); + let dst_dir = tempdir().unwrap(); + + let result = verify_and_copy_files(&src_path, dst_dir.path()); + assert!(result.is_err()); + } + + #[test] + fn test_handle_server_missing_serve_dir() { + let temp_dir = tempdir().unwrap(); + let log_file_path = temp_dir.path().join("server_log.log"); + let mut log_file = File::create(&log_file_path).unwrap(); + + let paths = Paths { + site: temp_dir.path().join("site"), + content: temp_dir.path().join("content"), + build: temp_dir.path().join("build"), + template: temp_dir.path().join("template"), + }; + + let non_existent_serve_dir = + PathBuf::from("/non_existent_serve_dir"); + let result = handle_server( + &mut log_file, + &DateTime::new(), + &paths, + &non_existent_serve_dir, + ); + assert!(result.is_err()); + } + + #[test] + fn test_log_initialization_write_failure() { + // Attempt to create a log file in a read-only directory (use an invalid path) + let invalid_path = PathBuf::from("/invalid/log_file.log"); + let mut log_file = + File::create(&invalid_path).unwrap_or_else(|_| { + // Mock a File instance, handle permissions here + File::open("/dev/null").unwrap() + }); + + let result = + log_initialization(&mut log_file, &DateTime::new()); + assert!(result.is_err()); + } + + #[test] + fn test_collect_files_recursive_empty() -> Result<()> { + let temp_dir = tempdir()?; + let mut files = Vec::new(); + + collect_files_recursive(temp_dir.path(), &mut files)?; + assert!(files.is_empty()); + + Ok(()) + } + + #[test] + fn test_print_banner() { + // Simply call the function to ensure it runs without errors. + cmd::print_banner(); + // Since this is a print statement, we're only verifying that it doesn't panic. + // If you need to check output, consider capturing stdout. + } + + #[test] + fn test_create_directories_with_unsafe_path() { + // Intentionally create a path with ".." to simulate an unsafe path + let unsafe_path = PathBuf::from("../unsafe_path"); + + let paths = Paths { + site: unsafe_path.clone(), + content: unsafe_path.clone(), + build: unsafe_path.clone(), + template: unsafe_path.clone(), + }; + + let result = create_directories(&paths); + + // Check that the result is an error due to unsafe paths + assert!(result.is_err()); + if let Err(e) = result { + assert!( + e.to_string().contains("unsafe"), + "Error should indicate unsafe path" + ); + } + } + + #[test] + fn test_collect_files_recursive_with_nested_directories( + ) -> Result<()> { + let temp_dir = tempdir()?; + let nested_dir = temp_dir.path().join("nested_dir"); + fs::create_dir(&nested_dir)?; + + let nested_file = nested_dir.join("nested_file.txt"); + _ = File::create(&nested_file)?; + + let mut files = Vec::new(); + collect_files_recursive(temp_dir.path(), &mut files)?; + + assert!(files.contains(&nested_file)); + assert_eq!(files.len(), 1); + Ok(()) + } + + #[test] + fn test_handle_server_start_message() -> Result<()> { + let temp_dir = tempdir()?; + let log_file_path = temp_dir.path().join("server_log.log"); + let mut log_file = File::create(&log_file_path)?; + + let paths = Paths { + site: temp_dir.path().join("site"), + content: temp_dir.path().join("content"), + build: temp_dir.path().join("build"), + template: temp_dir.path().join("template"), + }; + + let serve_dir = temp_dir.path().join("serve"); + + // Check setup conditions before calling `handle_server` + fs::create_dir_all(&serve_dir)?; + assert!( + serve_dir.exists(), + "Expected serve directory to be created" + ); + + // Now, call `handle_server` and check for specific output or error + let result = handle_server( + &mut log_file, + &DateTime::new(), + &paths, + &serve_dir, + ); + assert!( + result.is_err(), + "Expected handle_server to fail without valid setup" + ); + + Ok(()) + } + + #[cfg(any(unix, windows))] + #[test] + fn test_verify_file_safety_symlink() -> Result<()> { + let temp_dir = tempdir()?; + let file_path = temp_dir.path().join("test.txt"); + let symlink_path = temp_dir.path().join("test_link.txt"); + + // Create a regular file + fs::write(&file_path, "test content")?; + + // Create a symlink + #[cfg(unix)] + std::os::unix::fs::symlink(&file_path, &symlink_path)?; + #[cfg(windows)] + std::os::windows::fs::symlink_file(&file_path, &symlink_path)?; + + // Debug output + println!("File exists: {}", file_path.exists()); + println!("Symlink exists: {}", symlink_path.exists()); + println!( + "Is symlink: {}", + symlink_path.symlink_metadata()?.file_type().is_symlink() + ); + + // Try to verify the symlink + let result = verify_file_safety(&symlink_path); + + // Print the result for debugging + println!("Result: {:?}", result); + + // Verify that we got an error + assert!( + result.is_err(), + "Expected error for symlink, got success" + ); + + // Verify the error message + let err = result.unwrap_err(); + println!("Error message: {}", err); + assert!( + err.to_string().contains("Symlinks are not allowed"), + "Unexpected error message: {}", + err + ); + + Ok(()) + } + + #[test] + fn test_verify_file_safety_size() -> Result<()> { + let temp_dir = tempdir()?; + let large_file_path = temp_dir.path().join("large.txt"); + + // Create a large file + let file = File::create(&large_file_path)?; + file.set_len(11 * 1024 * 1024)?; // 11MB + + let result = verify_file_safety(&large_file_path); + assert!(result.is_err(), "Expected error for large file"); + assert!( + result + .unwrap_err() + .to_string() + .contains("exceeds maximum allowed size"), + "Unexpected error message" + ); + + Ok(()) + } + + #[test] + fn test_verify_file_safety_regular() -> Result<()> { + let temp_dir = tempdir()?; + let file_path = temp_dir.path().join("regular.txt"); + + // Create a regular file + fs::write(&file_path, "test content")?; + + assert!(verify_file_safety(&file_path).is_ok()); + Ok(()) + } + + /// Tests successful copying of an empty directory + #[tokio::test] + async fn test_copy_empty_directory_async() -> Result<()> { + let src_dir = tempdir()?; + let dst_dir = tempdir()?; + + let result = + copy_dir_all_async(src_dir.path(), dst_dir.path()).await; + assert!(result.is_ok()); + + // Verify destination directory exists + assert!(dst_dir.path().exists()); + Ok(()) + } + + /// Tests copying a directory with a single file + #[tokio::test] + async fn test_copy_single_file_async() -> Result<()> { + let src_dir = tempdir()?; + let dst_dir = tempdir()?; + + // Create a test file + let test_file = src_dir.path().join("test.txt"); + fs::write(&test_file, "test content")?; + + copy_dir_all_async(src_dir.path(), dst_dir.path()).await?; + + // Verify file was copied + let copied_file = dst_dir.path().join("test.txt"); + assert!(copied_file.exists()); + assert_eq!(fs::read_to_string(copied_file)?, "test content"); + + Ok(()) + } + + /// Tests copying a directory with nested subdirectories + #[tokio::test] + async fn test_copy_nested_directories_async() -> Result<()> { + let src_dir = tempdir()?; + let dst_dir = tempdir()?; + + // Create nested directory structure + let nested_dir = src_dir.path().join("nested"); + fs::create_dir(&nested_dir)?; + + // Create files in both root and nested directory + fs::write(src_dir.path().join("root.txt"), "root content")?; + fs::write(nested_dir.join("nested.txt"), "nested content")?; + + copy_dir_all_async(src_dir.path(), dst_dir.path()).await?; + + // Verify directory structure and contents + assert!(dst_dir.path().join("nested").exists()); + assert!(dst_dir.path().join("root.txt").exists()); + assert!(dst_dir.path().join("nested/nested.txt").exists()); + + assert_eq!( + fs::read_to_string(dst_dir.path().join("root.txt"))?, + "root content" + ); + assert_eq!( + fs::read_to_string( + dst_dir.path().join("nested/nested.txt") + )?, + "nested content" + ); + + Ok(()) + } + + /// Tests handling of symlinks + #[tokio::test] + async fn test_copy_with_symlink_async() -> Result<()> { + let src_dir = tempdir()?; + let dst_dir = tempdir()?; + + // Create a regular file + let file_path = src_dir.path().join("original.txt"); + fs::write(&file_path, "original content")?; + + // Create a symlink + #[cfg(unix)] + { + use std::os::unix::fs::symlink; + let symlink_path = src_dir.path().join("link.txt"); + symlink(&file_path, &symlink_path)?; + } + #[cfg(windows)] + { + use std::os::windows::fs::symlink_file; + let symlink_path = src_dir.path().join("link.txt"); + symlink_file(&file_path, &symlink_path)?; + } + + // Attempt to copy - should fail due to symlink + let result = + copy_dir_all_async(src_dir.path(), dst_dir.path()).await; + assert!(result.is_err()); + + Ok(()) + } + + /// Tests copying large files + #[tokio::test] + async fn test_copy_large_file_async() -> Result<()> { + let src_dir = tempdir()?; + let dst_dir = tempdir()?; + + // Create a large file (11MB) + let large_file = src_dir.path().join("large.txt"); + let file = fs::File::create(&large_file)?; + file.set_len(11 * 1024 * 1024)?; + + // Attempt to copy - should fail due to file size limit + let result = + copy_dir_all_async(src_dir.path(), dst_dir.path()).await; + assert!(result.is_err()); + + Ok(()) + } + + /// Tests copying with invalid destination + #[tokio::test] + async fn test_copy_invalid_destination_async() -> Result<()> { + let src_dir = tempdir()?; + let invalid_dst = PathBuf::from("/nonexistent/path"); + + let result = + copy_dir_all_async(src_dir.path(), &invalid_dst).await; + assert!(result.is_err()); + + Ok(()) + } + + /// Tests concurrent copying of multiple files + #[tokio::test] + async fn test_concurrent_copy_async() -> Result<()> { + let src_dir = tempdir()?; + let dst_dir = tempdir()?; + + // Create multiple files + for i in 0..5 { + fs::write( + src_dir.path().join(format!("file{}.txt", i)), + format!("content {}", i), + )?; + } + + copy_dir_all_async(src_dir.path(), dst_dir.path()).await?; + + // Verify all files were copied + for i in 0..5 { + let copied_file = + dst_dir.path().join(format!("file{}.txt", i)); + assert!(copied_file.exists()); + assert_eq!( + fs::read_to_string(copied_file)?, + format!("content {}", i) + ); + } + + Ok(()) + } + + /// Tests copying with maximum directory depth + #[tokio::test] + async fn test_max_directory_depth_async() -> Result<()> { + let src_dir = tempdir()?; + let dst_dir = tempdir()?; + let max_depth = 5; + + // Create deeply nested directory structure + let mut current_dir = src_dir.path().to_path_buf(); + for i in 0..max_depth { + current_dir = current_dir.join(format!("level{}", i)); + fs::create_dir(¤t_dir)?; + fs::write( + current_dir.join("file.txt"), + format!("content level {}", i), + )?; + } + + copy_dir_all_async(src_dir.path(), dst_dir.path()).await?; + + // Verify the entire structure was copied + current_dir = dst_dir.path().to_path_buf(); + for i in 0..max_depth { + current_dir = current_dir.join(format!("level{}", i)); + assert!(current_dir.exists()); + assert!(current_dir.join("file.txt").exists()); + assert_eq!( + fs::read_to_string(current_dir.join("file.txt"))?, + format!("content level {}", i) + ); + } + + Ok(()) + } + + #[tokio::test] + async fn test_verify_and_copy_files_async_missing_source( + ) -> Result<()> { + let temp_dir = tempdir()?; + let src_dir = temp_dir.path().join("nonexistent"); + let dst_dir = temp_dir.path().join("dst"); + + let error = verify_and_copy_files_async(&src_dir, &dst_dir) + .await + .unwrap_err() + .to_string(); + + assert!( + error.contains("does not exist"), + "Expected error message about non-existent source, got: {}", + error + ); + + Ok(()) + } + + #[tokio::test] + async fn test_verify_and_copy_files_async_symlink() -> Result<()> { + let temp_dir = tempdir()?; + let src_dir = temp_dir.path().join("src"); + let dst_dir = temp_dir.path().join("dst"); + + // Create source directory + tokio::fs::create_dir_all(&src_dir).await?; + + // Create target file + let target = src_dir.join("target.txt"); + tokio::fs::write(&target, "target content").await?; + + // Create symlink + let symlink = src_dir.join("symlink.txt"); + #[cfg(unix)] + std::os::unix::fs::symlink(&target, &symlink)?; + #[cfg(windows)] + std::os::windows::fs::symlink_file(&target, &symlink)?; + + // Try to verify the symlink directly + let error = verify_and_copy_files_async(&symlink, &dst_dir) + .await + .unwrap_err() + .to_string(); + + assert!( + error.contains("Symlinks are not allowed"), + "Expected error message about symlinks, got: {}", + error + ); + + Ok(()) + } + + #[tokio::test] + async fn test_verify_and_copy_files_async_large_file() -> Result<()> + { + let temp_dir = tempdir()?; + let src_dir = temp_dir.path().join("src"); + let large_file = src_dir.join("large.txt"); + + // Create source directory and large file + tokio::fs::create_dir_all(&src_dir).await?; + let file = tokio::fs::File::create(&large_file).await?; + file.set_len(11 * 1024 * 1024).await?; // 11MB + + // Try to verify the large file directly + let error = verify_and_copy_files_async( + &large_file, + &temp_dir.path().join("dst"), + ) + .await + .unwrap_err() + .to_string(); + + assert!( + error.contains("exceeds maximum allowed size"), + "Expected error message about file size, got: {}", + error + ); + + Ok(()) + } } diff --git a/src/loggers.rs b/src/loggers.rs deleted file mode 100644 index a5e6b70e..00000000 --- a/src/loggers.rs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright © 2023-2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -//! Application logging functionality - -use env_logger::Env; -use rlg::log_level::LogLevel; -use std::io::Write; - -/// Initializes the logging system. -/// -/// This function sets up the logging system using the `env_logger` -/// crate. It takes a `default_log_level` parameter, which determines -/// the minimum log level to be displayed. The function returns a -/// `Result` type, which will be `Ok` if the logging system is -/// initialized successfully, or an error if there was a problem. -/// -/// # Examples -/// -/// ``` -/// use rlg::log_level::LogLevel; -/// use ssg::loggers::init_logger; -/// -/// // Initialize the logging system with a default log level of `info` -/// init_logger(Some(LogLevel::INFO)).unwrap(); -/// ``` -pub fn init_logger( - default_log_level: Option, -) -> Result<(), Box> { - let env = Env::default().default_filter_or( - default_log_level.unwrap_or(LogLevel::INFO).to_string(), - ); - - env_logger::Builder::from_env(env) - .format(|buf, record| { - writeln!(buf, "[{}] - {}", record.level(), record.args()) - }) - .init(); - - Ok(()) -} diff --git a/src/macros/custom_macros.rs b/src/macros/custom_macros.rs deleted file mode 100644 index 37e9e8ba..00000000 --- a/src/macros/custom_macros.rs +++ /dev/null @@ -1,290 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -//! This module provides several macros for common tasks such as retrieving arguments, extracting options from metadata, -//! rendering layouts, and starting a server. -//! -//! # `macro_get_args` Macro -//! -//! Retrieve a named argument from a `clap::ArgMatches` object. -//! -//! ## Usage -//! -//! ```rust -//! use clap::{Arg, ArgMatches, Command, Error}; -//! use ssg::macro_get_args; -//! -//! fn test() -> Result<(), Box> { -//! let matches = Command::new("test_app") -//! .arg( -//! Arg::new("content") -//! .long("content") -//! .short('c') -//! .value_name("CONTENT"), -//! ) -//! .get_matches_from(vec!["test_app", "--content", "test_content"]); -//! -//! let content = macro_get_args!(matches, "content"); -//! println!("Content: {}", content); -//! Ok(()) -//! } -//! test(); -//! ``` -//! -//! ## Arguments -//! -//! * `$matches` - A `clap::ArgMatches` object representing the parsed command-line arguments. -//! * `$name` - A string literal specifying the name of the argument to retrieve. -//! -//! ## Behaviour -//! -//! The `macro_get_args` macro retrieves the value of the named argument `$name` from the `$matches` object. If the argument is found and its value can be converted to `String`, the macro returns the value as a `Result`. If the argument is not found or its value cannot be converted to `String`, an `Err` variant is returned with an error message indicating the omission of the required parameter. -//! -//! The error message includes the name of the omitted parameter (`$name`) to assist with troubleshooting and providing meaningful feedback to users. -//! -//! # `macro_metadata_option` Macro -//! -//! Extracts an option value from metadata. -//! -//! ## Usage -//! -//! ```rust -//! use std::collections::HashMap; -//! use ssg::macro_metadata_option; -//! -//! let mut metadata = HashMap::new(); -//! metadata.insert("key", "value"); -//! let value = macro_metadata_option!(metadata, "key"); -//! println!("{}", value); -//! ``` -//! -//! ## Arguments -//! -//! * `$metadata` - A mutable variable that represents the metadata (of type `HashMap` or any other type that supports the `get` and `cloned` methods). -//! * `$key` - A string literal that represents the key to search for in the metadata. -//! -//! ## Behaviour -//! -//! The `macro_metadata_option` macro is used to extract an option value from metadata. It takes a mutable variable representing the metadata and a string literal representing the key as arguments, and uses the `get` method of the metadata to find an option value with the specified key. -//! -//! If the key exists in the metadata, the macro clones the value and returns it. If the key does not exist, it returns the default value for the type of the metadata values. -//! -//! The macro is typically used in contexts where metadata is stored in a data structure that supports the `get` and `cloned` methods, such as a `HashMap`. -//! -//! # `macro_render_layout` Macro -//! -//! This macro selects and renders a specified layout with a given context. -//! -//! * `$layout`: The desired layout name (e.g., "contact", "index", "page"). -//! If a template file corresponding to `$layout.html` exists in the template directory, -//! it will be used. Otherwise, the macro falls back to predefined mappings. -//! -//! * `$template_path`: The path to the directory containing the template files. -//! -//! * `$context`: The context to be rendered into the template. -//! -//! ## Behaviour: -//! -//! 1. If a file named `$layout.html` exists in `$template_path`, it will be used as the template. -//! 2. If no such file exists, the macro checks for predefined layout names: -//! * "contact" maps to "contact.html" -//! * "index" maps to "index.html" -//! * "page" maps to "page.html" -//! 3. If `$layout` is unrecognized and doesn't correspond to a file in the template directory, -//! the macro defaults to using "index.html". -//! -//! ## Returns: -//! -//! The macro returns a rendered string using the selected template and provided context. -//! -//! # `macro_serve` Macro -//! -//! Start a server at the specified address with the given document root. -//! -//! ## Arguments -//! -//! * `$server_address` - The address at which the server should listen, specified as an expression (`expr`). -//! * `$document_root` - The root directory of the documents that the server should serve, specified as an expression (`expr`). -//! -//! ## Behaviour -//! -//! The `macro_serve` macro starts a server at the specified address with the given document root. It internally calls the `start` function from an unspecified library, passing the server address and document root as arguments. -//! -//! If the server starts successfully, the macro returns `Ok(())`. If an error occurs during server startup, the macro will panic with the error message provided by the `unwrap` method. -//! - -/// # `macro_get_args` Macro -/// -/// Retrieve a named argument from a `clap::ArgMatches` object. -/// -/// ## Usage -/// -/// ```rust -/// use clap::{Arg, ArgMatches, Command, Error}; -/// use ssg::macro_get_args; -/// -/// fn test() -> Result<(), Box> { -/// let matches = Command::new("test_app") -/// .arg( -/// Arg::new("content") -/// .long("content") -/// .short('c') -/// .value_name("CONTENT"), -/// ) -/// .get_matches_from(vec!["test_app", "--content", "test_content"]); -/// -/// let content = macro_get_args!(matches, "content"); -/// println!("Content: {}", content); -/// Ok(()) -/// } -/// test(); -/// ``` -/// -/// ## Arguments -/// -/// * `$matches` - A `clap::ArgMatches` object representing the parsed command-line arguments. -/// * `$name` - A string literal specifying the name of the argument to retrieve. -/// -/// ## Behaviour -/// -/// The `macro_get_args` macro retrieves the value of the named argument `$name` from the `$matches` object. If the argument is found and its value can be converted to `String`, the macro returns the value as a `Result`. If the argument is not found or its value cannot be converted to `String`, an `Err` variant is returned with an error message indicating the omission of the required parameter. -/// -/// The error message includes the name of the omitted parameter (`$name`) to assist with troubleshooting and providing meaningful feedback to users. -/// -/// ## Notes -/// -/// - This macro assumes the availability of the `clap` crate and the presence of a valid `ArgMatches` object. -/// - Make sure to adjust the code example by providing a valid `ArgMatches` object and replacing `"arg_name"` with the actual name of the argument you want to retrieve. -/// -#[macro_export] -macro_rules! macro_get_args { - ($matches:ident, $name:expr) => { - $matches.get_one::($name).ok_or(format!( - "❌ Error: A required parameter was omitted. Add the required parameter. \"{}\".", - $name - ))? - }; -} - -/// # `macro_metadata_option` Macro -/// -/// Extracts an option value from metadata. -/// -/// ## Usage -/// -/// ```rust -/// use std::collections::HashMap; -/// use ssg::macro_metadata_option; -/// -/// let mut metadata = HashMap::new(); -/// metadata.insert("key", "value"); -/// let value = macro_metadata_option!(metadata, "key"); -/// println!("{}", value); -/// ``` -/// -/// ## Arguments -/// -/// * `$metadata` - A mutable variable that represents the metadata (of type `HashMap` or any other type that supports the `get` and `cloned` methods). -/// * `$key` - A string literal that represents the key to search for in the metadata. -/// -/// ## Behaviour -/// -/// The `macro_metadata_option` macro is used to extract an option value from metadata. It takes a mutable variable representing the metadata and a string literal representing the key as arguments, and uses the `get` method of the metadata to find an option value with the specified key. -/// -/// If the key exists in the metadata, the macro clones the value and returns it. If the key does not exist, it returns the default value for the type of the metadata values. -/// -/// The macro is typically used in contexts where metadata is stored in a data structure that supports the `get` and `cloned` methods, such as a `HashMap`. -/// -/// ## Example -/// -/// ```rust -/// use ssg::macro_metadata_option; -/// use std::collections::HashMap; -/// -/// let mut metadata = HashMap::new(); -/// metadata.insert("key", "value"); -/// let value = macro_metadata_option!(metadata, "key"); -/// println!("{}", value); -/// ``` -/// -#[macro_export] -macro_rules! macro_metadata_option { - ($metadata:ident, $key:expr) => { - $metadata.get($key).cloned().unwrap_or_default() - }; -} - -/// # `macro_render_layout` Macro -/// -/// This macro selects and renders a specified layout with a given context. -/// -/// * `$layout`: The desired layout name (e.g., "contact", "index", "page"). -/// If a template file corresponding to `$layout.html` exists in the template directory, -/// it will be used. Otherwise, the macro falls back to predefined mappings. -/// -/// * `$template_path`: The path to the directory containing the template files. -/// -/// * `$context`: The context to be rendered into the template. -/// -/// ## Behaviour: -/// -/// 1. If a file named `$layout.html` exists in `$template_path`, it will be used as the template. -/// 2. If no such file exists, the macro checks for predefined layout names: -/// * "contact" maps to "contact.html" -/// * "index" maps to "index.html" -/// * "page" maps to "page.html" -/// 3. If `$layout` is unrecognized and doesn't correspond to a file in the template directory, -/// the macro defaults to using "index.html". -/// -/// ## Returns: -/// -/// The macro returns a rendered string using the selected template and provided context. -/// -#[macro_export] -macro_rules! macro_render_layout { - ($layout:expr, $template_path:expr, $context:expr) => {{ - let layout_str: &str = &$layout; - - // Check if a file with the name of the layout exists in the template directory - let file_path = Path::new($template_path).join(format!("{}.html", layout_str)); - let template_file = if file_path.exists() { - format!("{}.html", layout_str) - } else { - match layout_str { - "contact" => "contact.html", - "index" => "index.html", - "page" => "page.html", - "post" => "post.html", - _ => "index.html", - }.to_string() - }; - - let template_content = fs::read_to_string( - Path::new($template_path).join(&template_file), - ) - .unwrap(); - render_template(&template_content, &$context) - }}; -} - -/// # `macro_serve` Macro -/// -/// Start a server at the specified address with the given document root. -/// -/// ## Arguments -/// -/// * `$server_address` - The address at which the server should listen, specified as an expression (`expr`). -/// * `$document_root` - The root directory of the documents that the server should serve, specified as an expression (`expr`). -/// -/// ## Behaviour -/// -/// The `macro_serve` macro starts a server at the specified address with the given document root. It internally calls the `start` function from an unspecified library, passing the server address and document root as arguments. -/// -/// If the server starts successfully, the macro returns `Ok(())`. If an error occurs during server startup, the macro will panic with the error message provided by the `unwrap` method. -/// -#[macro_export] -macro_rules! macro_serve { - ($server_address:expr, $document_root:expr) => { - start($server_address, $document_root).unwrap(); - }; -} diff --git a/src/macros/directory_macros.rs b/src/macros/directory_macros.rs deleted file mode 100644 index 75eba80d..00000000 --- a/src/macros/directory_macros.rs +++ /dev/null @@ -1,265 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -//! This module provides macros for directory operations, including checking directory existence, -//! creating multiple directories at once, and cleaning up directories. -//! -//! # `macro_check_directory` Macro -//! -//! Checks if a directory exists and creates it if necessary. -//! -//! ## Usage -//! -//! ```rust -//! use ssg::macro_check_directory; -//! use std::path::Path; -//! -//! let path = Path::new("logs"); -//! macro_check_directory!(path, "logs"); -//! ``` -//! -//! ## Arguments -//! -//! * `_dir` - The path to check, as a `std::path::Path`. -//! * `_name` - A string literal representing the directory name. This is used in error messages. -//! -//! ## Behaviour -//! -//! The `macro_check_directory` macro checks if the directory specified by `_dir` exists. -//! If it exists and is not a directory, a panic with an error message is triggered. -//! If the directory doesn't exist, the macro attempts to create it using `std::fs::create_dir_all(_dir)`. -//! -//! # `macro_cleanup_directories` Macro -//! -//! Cleans up multiple directories by invoking the `cleanup_directory` function. -//! -//! ## Usage -//! -//! ```rust -//! use std::path::Path; -//! use ssg::macro_check_directory; -//! -//! let path = Path::new("logs"); -//! macro_check_directory!(path, "logs"); -//! ``` -//! -//! ## Arguments -//! -//! * `$( $_dir:expr ),*` - A comma-separated list of directory paths to clean up. -//! -//! ## Behaviour -//! -//! The `macro_cleanup_directories` macro takes multiple directory paths as arguments -//! and invokes the `cleanup_directory` function for each path. -//! -//! # `macro_create_directories` Macro -//! -//! Creates multiple directories at once. -//! -//! ## Usage -//! -//! ```rust -//! use ssg::{macro_create_directories, macro_cleanup_directories}; -//! use std::path::Path; -//! -//! macro_create_directories!("logs", "logs1", "logs2"); -//! macro_cleanup_directories!(Path::new("./logs"), Path::new("./logs1"), Path::new("./logs2")); -//! ``` -//! -//! ## Arguments -//! -//! * `...` - Variable number of directory paths, each specified as an expression (`expr`). -//! -//! ## Behaviour -//! -//! The `macro_create_directories` macro creates multiple directories at once. -//! -//! ## Example -//! -//! ```rust -//! use ssg::{macro_create_directories, macro_cleanup_directories}; -//! use std::path::Path; -//! -//! fn main() -> Result<(), Box> { -//! let test = Path::new("logs"); -//! let test2 = Path::new("logs1"); -//! macro_create_directories!(test, test2)?; -//! macro_cleanup_directories!(test, test2); -//! Ok(()) -//! } -//! ``` -//! -//! # Note -//! -//! These macros assume familiarity with Rust macros and their usage. -//! Users are encouraged to review Rust macro documentation for a better understanding -//! of how to work with macros effectively. - -/// # `macro_check_directory` Macro -/// -/// Check if a directory exists and create it if necessary. -/// -/// ## Usage -/// -/// ```rust -/// use ssg::macro_check_directory; -/// use std::path::Path; -/// -/// let path = Path::new("logs"); -/// macro_check_directory!(path, "logs"); -/// ``` -/// -/// ## Arguments -/// -/// * `_dir` - The path to check, as a `std::path::Path`. -/// * `_name` - A string literal representing the directory name. This is used in error messages. -/// -/// ## Behaviour -/// -/// The `macro_check_directory` macro checks if the directory specified by `_dir` exists. If it exists and is not a directory, a panic with an error message is triggered. If the directory doesn't exist, the macro attempts to create it using `std::fs::create_dir_all(_dir)`. If the creation is successful, no action is taken. If an error occurs during the directory creation, a panic is triggered with an error message indicating the failure. -/// -/// Please note that the macro panics on failure. Consider using this macro in scenarios where panicking is an acceptable behaviour, such as during application startup or setup. -/// -/// # See Also -/// -/// - [`macro_create_directories`] for creating multiple directories -/// - [`macro_cleanup_directories`] for cleaning up directories -/// -#[macro_export] -macro_rules! macro_check_directory { - ($_dir:expr, $_name:expr) => {{ - use std::path::Path; - let directory: &Path = $_dir; - let name = $_name; - if directory.exists() { - if !directory.is_dir() { - log::warn!("❌ '{}' is not a directory.", name); - panic!("❌ '{}' is not a directory.", name); - } - } else { - match std::fs::create_dir_all(directory) { - Ok(_) => {} - Err(e) => { - log::error!( - "❌ Cannot create '{}' directory: {}", - name, - e - ); - panic!( - "❌ Cannot create '{}' directory: {}", - name, e - ) - } - } - } - }}; -} - -/// # `macro_cleanup_directories` Macro -/// -/// Cleanup multiple directories by invoking the `cleanup_directory` function. -/// -/// ## Usage -/// -/// ```rust -/// use std::path::Path; -/// use ssg::macro_check_directory; -/// -/// let path = Path::new("logs"); -/// macro_check_directory!(path, "logs"); -/// ``` -/// -/// ## Arguments -/// -/// * `$( $_dir:expr ),*` - A comma-separated list of directory paths to clean up. -/// -/// ## Behaviour -/// -/// The `macro_cleanup_directories` macro takes multiple directory paths as arguments and invokes the `cleanup_directory` function for each path. It is assumed that the `cleanup_directory` function is available in the crate's utilities module (`$crate::utilities::cleanup_directory`). -/// -/// The macro creates an array `directories` containing the provided directory paths and passes it as an argument to `cleanup_directory`. The `cleanup_directory` function is responsible for performing the cleanup operations. -/// -/// Please note that the macro uses the `?` operator for error propagation. It expects the `cleanup_directory` function to return a `Result` type. If an error occurs during the cleanup process, it will be propagated up the call stack, allowing the caller to handle it appropriately. -/// -/// # See Also -/// -/// - [`macro_check_directory`] for checking and creating a single directory -/// - [`macro_create_directories`] for creating multiple directories -/// -#[macro_export] -macro_rules! macro_cleanup_directories { - ( $( $_dir:expr ),* ) => { - { - use $crate::utilities::directory::cleanup_directory; - let directories: &[&Path] = &[ $( $_dir ),* ]; - match cleanup_directory(directories) { - Ok(()) => (), - Err(err) => { - log::error!("Cleanup failed: {:?}", err); - panic!("Cleanup failed: {:?}", err); - }, - } - } - }; -} - -/// # `macro_create_directories` Macro -/// -/// Create multiple directories at once. -/// -/// ## Usage -/// -/// ```rust -/// use ssg::{macro_create_directories, macro_cleanup_directories}; -/// use std::path::Path; -/// macro_create_directories!("logs", "logs1", "logs2"); -/// macro_cleanup_directories!(Path::new("./logs"), Path::new("./logs1"), Path::new("./logs2")); -/// ``` -/// -/// ## Arguments -/// -/// * `...` - Variable number of directory paths, each specified as an expression (`expr`). -/// -/// ## Behaviour -/// -/// The `macro_create_directories` macro creates multiple directories at once. It takes a variable number of directory paths as arguments and uses the `create_directory` utility function from the `$crate` crate to create the directories. -/// -/// The directories are specified as expressions and separated by commas. For example, `macro_create_directories!("logs", "logs1", "logs2")` will attempt to create the `logs`, `logs1`, and `logs2`. -/// -/// The macro internally creates a slice of the directory paths and passes it to the `create_directory` function. If any error occurs during the directory creation, the macro returns an `Err` value, indicating the first encountered error. Otherwise, it returns `Ok(())`. -/// -/// ## Example -/// -/// ```rust -/// use ssg::{macro_create_directories, macro_cleanup_directories}; -/// use std::path::Path; -/// -/// fn main() -> Result<(), Box> { -/// let test = Path::new("logs"); -/// let test2 = Path::new("logs1"); -/// macro_create_directories!(test, test2)?; -/// macro_cleanup_directories!(test, test2); -/// Ok(()) -/// } -/// ``` -/// -/// # See Also -/// -/// - [`macro_check_directory`] for checking and creating a single directory -/// - [`macro_cleanup_directories`] for cleaning up directories -/// -#[macro_export] -macro_rules! macro_create_directories { - ( $( $_dir:expr ),* ) => {{ - use $crate::utilities::directory::create_directory; - use std::path::Path; - let directories: Vec<&Path> = vec![ $( Path::new($_dir) ),* ]; - match create_directory(&directories) { - Ok(_) => Ok(()), - Err(err) => { - log::error!("Failed to create directories: {:?}", err); - Err(err) - }, - } - }}; -} diff --git a/src/macros/html_macros.rs b/src/macros/html_macros.rs deleted file mode 100644 index 2ce5ca54..00000000 --- a/src/macros/html_macros.rs +++ /dev/null @@ -1,208 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT -// Contains macros related to directory operations. - -//! Contains macros related to HTML generation. -//! -//! This module provides macros for generating HTML meta tags, writing XML elements, and generating HTML meta tags -//! from metadata HashMaps or field-value pairs. -//! -//! # Meta Tags Generation -//! -//! Meta tags are essential for defining metadata within HTML documents, such as page descriptions, keywords, and other -//! information relevant to search engines and web crawlers. The `macro_generate_metatags` macro allows generating -//! meta tags dynamically based on provided key-value pairs. -//! -//! # XML Element Writing -//! -//! XML elements are fundamental components of XML documents, including HTML, RSS feeds, and other structured data formats. -//! The `macro_write_element` macro facilitates writing XML elements to a specified writer, enabling the dynamic generation -//! of XML content in Rust applications. -//! -//! # Meta Tags Generation from Metadata -//! -//! In addition to generating meta tags from key-value pairs, this module provides macros for generating HTML meta tags -//! directly from metadata HashMaps or field-value pairs. These macros offer flexibility in constructing HTML meta tags -//! based on existing metadata structures or data models. -//! -//! # Example -//! -//! ``` -//! use ssg::macro_generate_metatags; -//! use ssg::utilities::escape::escape_html_entities; -//! -//! let metatags = macro_generate_metatags!("description", "This is a description", "keywords", "rust,macros,metatags"); -//! ``` - -/// Generates meta tags based on provided key-value pairs. -/// -/// ## Usage -/// -/// ```rust -/// use ssg::macro_generate_metatags; -/// use ssg::utilities::escape::escape_html_entities; -/// -/// let metatags = macro_generate_metatags!("description", "This is a description", "keywords", "rust,macros,metatags"); -/// ``` -/// -/// ## Arguments -/// -/// * `($key:literal, $value:expr),*` - Pairs of a literal key and an expression value, each specified as `literal, expr`. -/// -/// ## Behaviour -/// -/// This macro generates meta tags using the provided keys and values. It takes pairs of literal keys and expression values and constructs HTML meta tags accordingly. -/// -/// The pairs of keys and values are specified as `literal, expr` and separated by commas. For example, `macro_generate_metatags!("description", "This is a description", "keywords", "rust,macros,metatags")` will generate meta tags with the keys `description` and `keywords` and their corresponding values. -/// -/// The macro internally creates a slice of tuples of the keys and values and passes it to the `generate_metatags` function. The function should return a string that represents the generated meta tags. -/// -// #[macro_export] -// macro_rules! macro_generate_metatags { -// ($($key:literal, $value:expr),* $(,)?) => { -// $crate::modules::metatags::generate_metatags(&[ $(($key.to_owned(), $value.to_string())),* ]) -// }; -// } -#[macro_export] -macro_rules! macro_generate_metatags { - ($($key:literal, $value:expr),* $(,)?) => { - $crate::modules::metatags::generate_metatags(&[ $(($key.to_owned(), escape_html_entities($value))),* ]) - }; -} - -/// Writes an XML element to the specified writer. -/// -/// ## Usage -/// -/// ```rust -/// use ssg::macro_write_element; -/// use std::io::Write; -/// use quick_xml::Writer; -/// -/// let mut writer = Writer::new(Vec::new()); -/// macro_write_element!(&mut writer, "title", "Hello, world!"); -/// ``` -/// -/// ## Arguments -/// -/// * `$writer:expr` - The writer instance to write the XML element to. -/// * `$name:expr` - The name of the XML element. -/// * `$value:expr` - The value of the XML element. -/// -/// ## Behaviour -/// -/// This macro writes an XML element with the specified name and value to the provided writer instance. It is primarily useful for generating XML documents, such as HTML or RSS feeds, dynamically. -/// -#[macro_export] -macro_rules! macro_write_element { - ($writer:expr, $name:expr, $value:expr) => {{ - use quick_xml::events::{ - BytesEnd, BytesStart, BytesText, Event, - }; - use std::borrow::Cow; - use std::error::Error; - - let result: Result<(), Box> = (|| -> Result<(), Box> { - if !$value.is_empty() { - let element_start = BytesStart::new($name); - $writer.write_event(Event::Start(element_start.clone()))?; - $writer.write_event(Event::Text(BytesText::from_escaped($value)))?; - - let element_end = BytesEnd::new::>( - std::str::from_utf8(element_start.name().as_ref()).unwrap().to_string().into(), - ); - - $writer.write_event(Event::End(element_end))?; - } - Ok(()) - })(); - - result - }}; -} - -/// Generates HTML meta tags based on a list of tag names and a metadata HashMap. -/// -/// ## Usage -/// -/// ``` -/// use ssg::macro_generate_tags_from_list; -/// use ssg::modules::metatags::load_metatags; -/// use std::collections::HashMap; -/// -/// // Create a new metadata hashmap -/// let mut metadata = HashMap::new(); -/// -/// // Insert String values into the hashmap -/// metadata.insert(String::from("description"), String::from("This is a description")); -/// metadata.insert(String::from("keywords"), String::from("rust,macros,metatags")); -/// -/// // Define tag names -/// let tag_names = &["description", "keywords"]; -/// -/// // Call the macro with correct hashmap types -/// let html_meta_tags = macro_generate_tags_from_list!(tag_names, &metadata); -/// println!("{}", html_meta_tags); -/// ``` -/// -/// ## Arguments -/// -/// * `$tag_names:expr` - An array slice containing the names of the tags to generate. -/// * `$metadata:expr` - The metadata HashMap containing the values for the tags. -/// -/// ## Returns -/// -/// Returns a string containing the HTML meta tags generated from the metadata HashMap. -/// -#[macro_export] -macro_rules! macro_generate_tags_from_list { - ($tag_names:expr, $metadata:expr) => { - load_metatags($tag_names, $metadata) - }; -} - -/// Generates HTML meta tags based on field-value pairs from a metadata HashMap. -/// -/// ## Usage -/// -/// ``` -/// use ssg::macro_generate_tags_from_fields; -/// use ssg::modules::metatags::generate_custom_meta_tags; -/// use std::collections::HashMap; -/// -/// // Create a new metadata hashmap -/// let mut metadata = HashMap::new(); -/// -/// -/// // Insert String values into the hashmap -/// metadata.insert(String::from("description"), String::from("This is a description")); -/// metadata.insert(String::from("keywords"), String::from("rust,macros,metatags")); -/// -/// // Call the macro with correct hashmap types -/// let html_meta_tags = macro_generate_tags_from_fields!(tag_names,metadata, "description" => description, "keywords" => keywords); -/// println!("{}", html_meta_tags); -/// ``` -/// -/// ## Arguments -/// -/// * `$name:ident` - The name of the metadata HashMap. -/// * `$metadata:expr` - The metadata HashMap containing the field-value pairs. -/// * `$( $tag:literal => $field:ident ),*` - Pairs of literal tag names and metadata field names. -/// -/// ## Returns -/// -/// Returns a string containing the HTML meta tags generated from the metadata HashMap. -/// -#[macro_export] -macro_rules! macro_generate_tags_from_fields { - ($name:ident, $metadata:expr, $($tag:literal => $field:ident),*) => { - { - let tag_mapping: Vec<(String, Option)> = vec![ - $( - ($tag.to_string(), $metadata.get(stringify!($field)).cloned()), - )* - ]; - generate_custom_meta_tags(&tag_mapping) - } - }; -} diff --git a/src/macros/log_macros.rs b/src/macros/log_macros.rs deleted file mode 100644 index 7fc49a78..00000000 --- a/src/macros/log_macros.rs +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT -// Contains macros related to directory operations. - -//! This module contains macros related to logging messages at various log levels and formats. -//! -//! It includes a custom logging macro, `macro_log_info`, which allows logging messages with -//! specified log levels, components, descriptions, and formats. -//! -//! # Custom Logging Macro -//! -//! The `macro_log_info` macro is designed for logging messages with customizable log levels, -//! components, descriptions, and formats. It provides flexibility in defining log messages -//! according to specific requirements. -//! -//! # Parameters -//! -//! - `$level`: The log level of the message. -//! - `$component`: The component where the log is coming from. -//! - `$description`: A description of the log message. -//! - `$format`: The format of the log message. -//! -//! # Example -//! -//! ``` -//! use ssg::macro_log_info; -//! use rlg::log_level::LogLevel; -//! use rlg::log_format::LogFormat; -//! -//! let level = LogLevel::INFO; -//! let component = "TestComponent"; -//! let description = "Test description"; -//! let format = LogFormat::CLF; -//! -//! // Log an informational message -//! let log = -//! macro_log_info!(&level, component, description, &format); -//! -//! // Further processing of log message... -//! ``` -//! -//! This example demonstrates how to use the `macro_log_info` macro to log an informational message -//! with specified log level, component, description, and format. Users can customize log messages -//! according to their logging requirements by modifying the parameters passed to the macro. -//! -//! # Note -//! -//! This module assumes familiarity with Rust macros and their usage. Users are encouraged -//! to review Rust macro documentation for a better understanding of how to work with macros effectively. - -/// Custom logging macro for various log levels and formats. -/// -/// # Parameters -/// -/// * `$level`: The log level of the message. -/// * `$component`: The component where the log is coming from. -/// * `$description`: A description of the log message. -/// * `$format`: The format of the log message. -/// -#[macro_export] -macro_rules! macro_log_info { - ($level:expr, $component:expr, $description:expr, $format:expr) => {{ - // Import necessary modules - use dtt::DateTime; // Date and time module - use rlg::{ - log::Log, - log_format::LogFormat, - }; // Logging module and log format module - use vrd::random::Random; // Random number generator module - - // Get the current date and time in ISO 8601 format. - let date = DateTime::new(); // Create a new DateTime instance - let iso = date.iso_8601; // Get ISO 8601 formatted date and time - - // Create a new random number generator - let mut rng = Random::default(); // Default random number generator - let session_id = rng.rand().to_string(); // Generate session ID - - // Create a new log instance - let log = Log::new( - &session_id, // Session ID - &iso, // ISO 8601 formatted date and time - $level, // Log level - $component, // Component name - $description, // Log description - $format, // Log format - ); - log // Return the Log instance - }}; -} diff --git a/src/macros/mod.rs b/src/macros/mod.rs deleted file mode 100644 index ec6a597e..00000000 --- a/src/macros/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -/// The `custom_macros` module contains all the custom macros used in the project. -pub mod custom_macros; - -/// The `directory_macros` module contains macros related to directory operations. -pub mod directory_macros; - -/// The `html_macros` module contains macros related to HTML generation. -pub mod html_macros; - -/// The `log_macros` module contains macros related to log messages. -pub mod log_macros; - -/// The `rss_macros` module contains macros related to RSS feed generation. -pub mod rss_macros; - -/// The `shell_macros` module contains macros related to executing shell commands. -pub mod shell_macros; diff --git a/src/macros/rss_macros.rs b/src/macros/rss_macros.rs deleted file mode 100644 index ad8f2469..00000000 --- a/src/macros/rss_macros.rs +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT -// Contains macros related to directory operations. - -//! This module provides macros for generating RSS feeds and setting fields for RSS data. -//! -//! It includes macros for generating RSS feeds from provided data using the quick_xml crate. -//! The `macro_generate_rss` macro generates a complete RSS feed in XML format based on the provided metadata values, -//! while the `macro_set_rss_data_fields` macro is used to set fields of the `RssData` struct. -//! -//! # `macro_generate_rss` Macro -//! -//! The `macro_generate_rss` macro generates a complete RSS feed in XML format based on the data contained in the provided `RssData`. -//! It dynamically generates XML elements for each field of the `RssData` using the provided metadata values and -//! writes them to the specified Writer instance. -//! -//! ## Arguments -//! -//! - `$writer`: The Writer instance to write the generated XML events. -//! - `$options`: The RssData instance containing the metadata values for generating the RSS feed. -//! -//! ## Returns -//! -//! Returns `Result>>, Box>` indicating success or an error if XML writing fails. -//! -//! # `macro_set_rss_data_fields` Macro -//! -//! The `macro_set_rss_data_fields` macro sets fields of the `RssData` struct. -//! -//! ## Arguments -//! -//! - `$rss_data`: The `RssData` struct to set fields for. -//! - `$field`: The field to set. -//! - `$value`: The value to set for the field. -//! -//! # Note -//! -//! This module assumes familiarity with the quick_xml crate and its usage. Users are encouraged -//! to review quick_xml documentation for a better understanding of how to work with XML generation in Rust. - -/// # `macro_generate_rss` Macro -/// -/// Generates an RSS feed from the given `RssData` struct. -/// -/// This macro generates a complete RSS feed in XML format based on the data contained in the provided `RssData`. -/// It dynamically generates XML elements for each field of the `RssData` using the provided metadata values and -/// writes them to the specified Writer instance. -/// -/// # Arguments -/// -/// * `$writer` - The Writer instance to write the generated XML events. -/// * `$options` - The RssData instance containing the metadata values for generating the RSS feed. -/// -/// # Returns -/// -/// Returns `Result>>, Box>` indicating success or an error if XML writing fails. -/// -#[macro_export] -macro_rules! macro_generate_rss { - ($writer:expr, $options:expr) => {{ - use quick_xml::events::{BytesStart, BytesEnd, BytesDecl, Event}; - use quick_xml::Writer; - use std::io::Cursor; - - // Create a Writer instance from the provided expression - let mut writer = $writer; - - // Start building the RSS feed with XML declaration - writer.write_event(Event::Decl(BytesDecl::new("1.0", Some("utf-8"), None))) - .unwrap(); - - // Start the tag - let mut rss_start = BytesStart::new("rss"); - rss_start.push_attribute(("version", "2.0")); - rss_start.push_attribute(("xmlns:atom", "http://www.w3.org/2005/Atom")); - writer.write_event(Event::Start(rss_start)).unwrap(); - - // Start the tag - writer.write_event(Event::Start(BytesStart::new("channel"))).unwrap(); - - // Atom link should be inside the channel, and it's a self-closing tag - let mut atom_link_start = BytesStart::new("atom:link"); - atom_link_start.push_attribute(("href", $options.atom_link.as_str())); - atom_link_start.push_attribute(("rel", "self")); - atom_link_start.push_attribute(("type", "application/rss+xml")); - writer.write_event(Event::Empty(atom_link_start)).unwrap(); // Use Event::Empty for self-closing tag - - // Write other channel elements - macro_write_element!(writer, "title", &$options.title)?; - macro_write_element!(writer, "link", &$options.link)?; - macro_write_element!(writer, "description", &$options.description)?; - macro_write_element!(writer, "language", &$options.language)?; - macro_write_element!(writer, "pubDate", &$options.pub_date)?; - macro_write_element!(writer, "lastBuildDate", &$options.last_build_date)?; - macro_write_element!(writer, "docs", &$options.docs)?; - macro_write_element!(writer, "generator", &$options.generator)?; - macro_write_element!(writer, "managingEditor", &$options.managing_editor)?; - macro_write_element!(writer, "webMaster", &$options.webmaster)?; - macro_write_element!(writer, "category", &$options.category)?; - macro_write_element!(writer, "ttl", &$options.ttl)?; - - // Write image element - writer.write_event(Event::Start(BytesStart::new("image")))?; - macro_write_element!(writer, "url", &$options.image)?; - macro_write_element!(writer, "title", &$options.title)?; - macro_write_element!(writer, "link", &$options.link)?; - writer.write_event(Event::End(BytesEnd::new("image")))?; - - // Write item element - writer.write_event(Event::Start(BytesStart::new("item")))?; - macro_write_element!(writer, "author", &$options.author)?; - macro_write_element!(writer, "description", &$options.item_description)?; - macro_write_element!(writer, "guid", &$options.item_guid)?; - macro_write_element!(writer, "link", &$options.item_link)?; - macro_write_element!(writer, "pubDate", &$options.item_pub_date)?; - macro_write_element!(writer, "title", &$options.item_title)?; - writer.write_event(Event::End(BytesEnd::new("item")))?; - - // Close the and tags - writer.write_event(Event::End(BytesEnd::new("channel")))?; - writer.write_event(Event::End(BytesEnd::new("rss")))?; - - // Return the Writer instance - Ok(writer) - }}; -} - -/// # `macro_set_rss_data_fields` Macro -/// -/// Sets fields of the `RssData` struct. -/// -/// This macro sets the fields of the `RssData` struct with the provided values. -/// -/// # Arguments -/// -/// * `$rss_data` - The `RssData` struct to set fields for. -/// * `$field` - The field to set. -/// * `$value` - The value to set for the field. -/// -#[macro_export] -macro_rules! macro_set_rss_data_fields { - ($rss_data:expr, $field:ident, $value:expr) => { - $rss_data.set(stringify!($field), $value); - }; -} diff --git a/src/macros/shell_macros.rs b/src/macros/shell_macros.rs deleted file mode 100644 index 6d05b5e8..00000000 --- a/src/macros/shell_macros.rs +++ /dev/null @@ -1,253 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -//! This module provides utilities for executing shell commands, logging their execution, and handling errors. -//! -//! It includes a `CommandExecutor` struct for encapsulating command execution functionality, -//! custom error types for command execution failures, and macros for logging the start, -//! completion, and errors of shell command operations. -//! -//! # Configuration Options -//! -//! - `CommandExecutor::new`: Allows specifying the shell interpreter to use, which defaults to "sh". -//! - Logging macros (`macro_log_start`, `macro_log_complete`, `macro_log_error`): Customization options -//! for logging start, completion, and error messages. -//! -//! # Examples -//! -//! ``` -//! use ssg::{macro_execute_and_log, macro_log_start, macro_log_complete, macro_log_error}; -//! use ssg::macros::shell_macros::CommandError; -//! use std::error::Error; -//! -//! fn main() -> Result<(), Box> { -//! let cmd = "ls -l"; -//! let pkg = "example_pkg"; -//! let operation = "list_directory"; -//! let start_message = "Listing directory contents..."; -//! let complete_message = "Listing directory completed successfully."; -//! let error_message = "Listing directory failed."; -//! -//! match macro_execute_and_log!(cmd, pkg, operation, start_message, complete_message, error_message, Some("output"), Some("bash")) { -//! Ok(()) => println!("Command executed successfully."), -//! Err(err) => eprintln!("Error executing command: {}", err), -//! } -//! Ok(()) -//! } -//! ``` -//! -//! This example demonstrates how to use the `macro_execute_and_log` macro to execute a shell command, -//! log the start and completion of the operation, and handle any errors that occur. -//! -//! # Logging Macros Customization -//! -//! The logging macros (`macro_log_start`, `macro_log_complete`, `macro_log_error`) can be customized -//! according to specific logging requirements. Users can implement their own logging strategies -//! by replacing the default `println!` statements with custom logging logic. -//! -//! For example, users can redirect log messages to a file, integrate with a logging framework, -//! or send log messages to a remote logging server by modifying the macro implementations. -//! -//! Additionally, users can adjust log message formats, add timestamps, or include additional metadata -//! in log messages by modifying the format strings within the macros. -//! -//! # Note -//! -//! This module assumes familiarity with Rust macros and their usage. Users are encouraged -//! to review Rust macro documentation for a better understanding of how to work with macros effectively. - -// Standard library imports -use std::ffi::OsStr; -use std::fmt; -use std::process::{Command, Output}; - -/// Custom error type for command execution failures. -#[derive(Debug)] -pub enum CommandError { - /// Indicates that the command string was empty. - EmptyCommand, - /// Indicates that the command execution failed with the provided error message. - ExecutionFailed(String), -} - -impl fmt::Display for CommandError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - CommandError::EmptyCommand => write!(f, "Empty command"), - CommandError::ExecutionFailed(msg) => { - write!(f, "Command execution failed: {}", msg) - } - } - } -} - -impl PartialEq for CommandError { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - ( - CommandError::EmptyCommand, - CommandError::EmptyCommand, - ) => true, - ( - CommandError::ExecutionFailed(msg1), - CommandError::ExecutionFailed(msg2), - ) => msg1 == msg2, - _ => false, - } - } -} - -/// Encapsulates command execution functionality. -#[derive(Debug)] -pub struct CommandExecutor { - /// The command to execute. - pub command: Command, -} - -impl CommandExecutor { - /// Creates a new CommandExecutor instance. - pub fn new(interpreter: Option) -> Self - where - S: AsRef + From<&'static str> + AsRef, - { - // Initialize the command with the specified shell interpreter or default to "sh" - let mut command = - Command::new(interpreter.unwrap_or_else(|| "sh".into())); - // Specify the shell command to execute - command.arg("-c"); - CommandExecutor { command } - } - - /// Sets the shell command to execute. - pub fn command>(&mut self, cmd: S) -> &mut Self { - // Append the command to the existing command - self.command.arg(cmd.as_ref()); - self - } - - /// Executes the command and returns the result. - pub fn execute(&mut self) -> Result { - // Get the command as a string - let cmd_str = format!("{:?}", self.command); - // Check if the command is empty - if cmd_str.is_empty() { - return Err(CommandError::EmptyCommand); - } - // Execute the command and handle any errors - self.command.output().map_err(|err| { - CommandError::ExecutionFailed(err.to_string()) - }) - } -} - -/// Executes a shell command, logs the start and completion of the operation, and handles any errors that occur. -/// -/// # Parameters -/// -/// * `$cmd`: The shell command to execute. -/// * `$pkg`: The name of the package the command is being run on. -/// * `$operation`: A description of the operation being performed. -/// * `$start_message`: The log message to be displayed at the start of the operation. -/// * `$complete_message`: The log message to be displayed upon successful completion of the operation. -/// * `$error_message`: The log message to be displayed in case of an error. -/// * `$output_dir`: Optional parameter for specifying the directory to write command output. -/// * `$interpreter`: Optional parameter for specifying the shell interpreter (default is "sh"). -/// -/// # Returns -/// -/// Returns a `Result<(), CommandError>` indicating the success or failure of the operation. -/// -/// # Example -/// -/// ``` -/// use ssg::{macro_execute_and_log, macro_log_start, macro_log_complete, macro_log_error}; -/// use ssg::macros::shell_macros::CommandError; -/// use std::error::Error; -/// -/// fn main() -> Result<(), Box> { -/// let cmd = "ls -l"; -/// let pkg = "example_pkg"; -/// let operation = "list_directory"; -/// let start_message = "Listing directory contents..."; -/// let complete_message = "Listing directory completed successfully."; -/// let error_message = "Listing directory failed."; -/// -/// match macro_execute_and_log!(cmd, pkg, operation, start_message, complete_message, error_message, Some("output"), Some("bash")) { -/// Ok(()) => println!("Command executed successfully."), -/// Err(err) => eprintln!("Error executing command: {}", err), -/// } -/// Ok(()) -/// } -/// ``` -#[macro_export] -macro_rules! macro_execute_and_log { - ($cmd:expr, $pkg:expr, $operation:expr, $start_message:expr, $complete_message:expr, $error_message:expr, $output_dir:expr, $interpreter:expr) => {{ - // Create a new CommandExecutor instance - let mut executor = - $crate::macros::shell_macros::CommandExecutor::new( - $interpreter, - ); - // Set the command to execute - executor.command($cmd); - // Execute the command and handle the result - let result = executor.execute(); - - match result { - Ok(_) => { - // Log the successful completion of the operation - macro_log_complete!($operation, $complete_message); - Ok(()) - } - Err(err) => { - // Log the error if the command execution failed - macro_log_error!($operation, $error_message); - Err(err) - } - } - }}; -} - -/// Macro to log the start of an operation. -/// -/// # Parameters -/// -/// * `$operation`: A description of the operation being performed. -/// * `$message`: The log message to be displayed at the start of the operation. -/// -#[macro_export] -macro_rules! macro_log_start { - ($operation:expr, $message:expr) => {{ - // Your implementation for logging start message goes here - println!("Start message: {}", $message); - }}; -} - -/// Macro to log the completion of an operation. -/// -/// # Parameters -/// -/// * `$operation`: A description of the operation being performed. -/// * `$message`: The log message to be displayed upon successful completion of the operation. -/// -#[macro_export] -macro_rules! macro_log_complete { - ($operation:expr, $message:expr) => {{ - // Your implementation for logging completion message goes here - println!("Completion message: {}", $message); - }}; -} - -/// Macro to log an error. -/// -/// # Parameters -/// -/// * `$operation`: A description of the operation being performed. -/// * `$message`: The log message to be displayed in case of an error. -/// -#[macro_export] -macro_rules! macro_log_error { - ($operation:expr, $message:expr) => {{ - // Your implementation for logging error message goes here - println!("Error message: {}", $message); - }}; -} diff --git a/src/main.rs b/src/main.rs index 86edae4d..170a857a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,220 @@ // Copyright © 2023-2024 Shokunin Static Site Generator. All rights reserved. // SPDX-License-Identifier: Apache-2.0 OR MIT -//! The main function of the program. +//! # Shokunin Static Site Generator - Main Entry Point //! -//! Calls the `run()` function from the `ssg` module to run the static site generator. +//! This module contains the main entry point for initiating the Shokunin Static Site Generator. It defines the `main` function and an `execute_main_logic` helper function, which together handle the core execution flow, including error handling and language-based translation of messages. //! -//! If an error occurs while running the `run()` function, the function prints an error message -//! to standard error and exits the program with a non-zero status code. +//! ## Core Behaviour +//! - **Default Language**: If the `LANGUAGE` environment variable is unset, English (`"en"`) is used. +//! - **Execution Flow**: Calls `run` from the `ssg` module to generate the site, and translates messages based on the user's language preference. +//! - **Exit Status**: On success, outputs a confirmation message. On failure, outputs an error message and exits with a non-zero status code. +//! +//! ## Example Usage +//! ```rust,no_run +//! use ssg::run; +//! use std::env; +//! +//! // Set the language preference before executing the site generator. +//! env::set_var("LANGUAGE", "en"); +//! match run() { +//! Ok(_) => println!("Site generated successfully."), +//! Err(e) => eprintln!("Error encountered: {}", e), +//! } +//! ``` -use ssg::{languages::translate, run}; +use langweave::translate; +use ssg::run; + +/// Executes the main logic of the Shokunin Static Site Generator. +/// +/// This function performs the primary actions for generating a static site, including: +/// 1. Retrieving the user's language preference from the `LANGUAGE` environment variable. +/// 2. Calling `run` from the `ssg` module to generate the site. +/// 3. Translating a success or failure message based on the selected language. +/// +/// ### Language Preference +/// - The language is determined by the `LANGUAGE` environment variable. +/// - If the variable is unset, English ("en") is used as the default language. +/// +/// ### Return Values +/// - On success, returns `Ok(String)` containing the translated success message. +/// - On failure, returns `Err(String)` with either a generation error message or a translation failure notice. +/// +/// ### Errors +/// Errors may arise in two scenarios: +/// 1. If the `run` function fails to generate the site, an error message is returned. +/// 2. If translation of the success message fails, a translation error message is returned. +/// +/// ### Example +/// ```rust +/// use std::env; +/// env::set_var("LANGUAGE", "fr"); +/// match execute_main_logic() { +/// Ok(msg) => println!("{}", msg), +/// Err(e) => eprintln!("{}", e), +/// } +/// ``` +/// +/// # Return +/// `Result` - A result containing either a success message or an error string. +fn execute_main_logic() -> Result { + // Determine the user's language preference, defaulting to English ("en") if unset. + let lang = + std::env::var("LANGUAGE").unwrap_or_else(|_| "en".to_string()); -fn main() { match run() { - Ok(_) => println!("{}", translate("en", "main_logger_msg")), - Err(e) => eprintln!("Program encountered an error: {}", e), + Ok(_) => { + // Translate and return a success message in the chosen language. + match translate(&lang, "main_logger_msg") { + Ok(msg) => Ok(msg), + Err(e) => Err(format!("Translation failed: {}", e)), + } + } + // Return an error if `run` encounters an issue. + Err(e) => Err(format!("Program encountered an error: {}", e)), + } +} + +/// The main entry point of the Shokunin Static Site Generator. +/// +/// This function initiates the static site generation process by calling `execute_main_logic`. +/// It handles the output to the console, displaying either a translated success message +/// or an error message if the generation fails. +/// +/// ### Exit Codes +/// - Returns `0` if site generation is successful. +/// - Returns a non-zero status code if an error occurs. +/// +/// ### Example +/// ```rust,no_run +/// // Set LANGUAGE environment variable to the desired language before running the generator. +/// use std::env; +/// env::set_var("LANGUAGE", "es"); +/// main(); // Executes the site generation in Spanish, if supported. +/// ``` +/// +/// ### Behaviour +/// - Retrieves the user's language preference from the `LANGUAGE` environment variable. +/// - Executes `execute_main_logic` to generate the site. +/// - Outputs a success message upon completion or an error message if site generation fails. +fn main() { + match execute_main_logic() { + Ok(msg) => println!("{}", msg), + Err(e) => eprintln!("{}", e), + } +} + +#[cfg(test)] +mod tests { + use std::env; + + /// Mocks the `run` function to simulate a successful site generation. + /// + /// ### Return + /// Returns `Ok(())` to indicate that the site generation was successful. + fn mock_run_ok() -> Result<(), String> { + Ok(()) + } + + /// Mocks the `run` function to simulate a failed site generation. + /// + /// ### Return + /// Returns `Err(String)` to simulate a failure with an error message. + fn mock_run_err() -> Result<(), String> { + Err("Site generation failed".to_string()) + } + + /// Mocks the `translate` function to simulate a successful translation. + /// + /// ### Parameters + /// - `lang`: Language code (e.g., "en", "fr"). + /// - `_msg_key`: The message key for the translation. + /// + /// ### Return + /// Returns `Ok(String)` containing a success message in the specified language. + fn mock_translate_success( + lang: &str, + _msg_key: &str, + ) -> Result { + Ok(format!("Success message in {}", lang)) + } + + /// Mocks the `translate` function to simulate a translation failure. + /// + /// ### Parameters + /// - `_lang`: Language code, though it is unused as this mock always fails. + /// - `_msg_key`: The message key for the translation. + /// + /// ### Return + /// Returns `Err(String)` to indicate a translation error. + fn mock_translate_failure( + _lang: &str, + _msg_key: &str, + ) -> Result { + Err("Translation error".to_string()) + } + + /// Tests successful site generation and message translation. + /// + /// ### Behaviour + /// Simulates a scenario where `run` succeeds, and `translate` also succeeds, producing + /// a successful message output. + #[test] + fn test_execute_main_logic_run_success_translate_success() { + env::set_var("LANGUAGE", "en"); + + let result = mock_run_ok(); + let translate_result = + mock_translate_success("en", "main_logger_msg"); + + assert_eq!(result, Ok(())); + assert_eq!( + translate_result, + Ok("Success message in en".to_string()) + ); + } + + /// Tests successful site generation with a translation failure. + /// + /// ### Behaviour + /// Simulates a scenario where `run` succeeds, but `translate` fails, resulting + /// in a translation error message. + #[test] + fn test_execute_main_logic_run_success_translate_failure() { + env::set_var("LANGUAGE", "en"); + + let result = mock_run_ok(); + let translate_result = + mock_translate_failure("en", "main_logger_msg"); + + assert_eq!(result, Ok(())); + assert_eq!( + translate_result, + Err("Translation error".to_string()) + ); + } + + /// Tests a failed site generation process. + /// + /// ### Behaviour + /// Simulates a scenario where `run` fails, leading to a site generation error message. + #[test] + fn test_execute_main_logic_run_failure() { + let result = mock_run_err(); + assert_eq!(result, Err("Site generation failed".to_string())); + } + + /// Tests the default language setting when `LANGUAGE` is not specified. + /// + /// ### Behaviour + /// Ensures that "en" is used as the default language when the `LANGUAGE` environment + /// variable is unset. + #[test] + fn test_execute_main_logic_default_language() { + env::remove_var("LANGUAGE"); + let lang = + env::var("LANGUAGE").unwrap_or_else(|_| "en".to_string()); + assert_eq!(lang, "en"); } } diff --git a/src/metadata/mod.rs b/src/metadata/mod.rs deleted file mode 100644 index d2c786ad..00000000 --- a/src/metadata/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -/// The `service` module contains the compiler service. -pub mod service; diff --git a/src/metadata/service.rs b/src/metadata/service.rs deleted file mode 100644 index 4dd01e1a..00000000 --- a/src/metadata/service.rs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use crate::{ - models::data::MetaTagGroups, - modules::{ - frontmatter::extract, keywords::extract_keywords, - metatags::generate_all_meta_tags, - }, -}; -use std::collections::HashMap; - -/// Extracts metadata from the content, generates keywords based on the metadata, -/// and prepares meta tag groups. -/// -/// This function performs three key tasks: -/// 1. It extracts metadata from the front matter of the content. -/// 2. It extracts keywords based on this metadata. -/// 3. It generates various meta tags required for the page. -/// -/// # Arguments -/// -/// * `content` - A string slice representing the content from which to extract metadata. -/// -/// # Returns -/// -/// Returns a tuple containing: -/// * `HashMap`: Extracted metadata -/// * `Vec`: A list of keywords -/// * `MetaTagGroups`: A structure containing various meta tags -/// -/// # Examples -/// -/// ```rust -/// use ssg::models::data::FileData; -/// use ssg::metadata::service::extract_and_prepare_metadata; -/// -/// let file_content = "---\n\n# Front Matter (YAML)\n\nauthor: \"Jane Doe\"\ncategory: \"Rust\"\ndescription: \"A blog about Rust programming.\"\nlayout: \"post\"\npermalink: \"https://example.com/blog/rust\"\ntags: \"rust,programming\"\ntitle: \"Rust\"\n\n---\n\n# Content\n\nThis is a blog about Rust programming.\n"; -/// -/// let (metadata, keywords, all_meta_tags) = extract_and_prepare_metadata(&file_content); -/// ``` -pub fn extract_and_prepare_metadata( - content: &str, -) -> (HashMap, Vec, MetaTagGroups) { - // Extract metadata from front matter - let metadata = extract(content); - - // Extract keywords - let keywords = extract_keywords(&metadata); - - // Generate all meta tags - let all_meta_tags = generate_all_meta_tags(&metadata); - - (metadata, keywords, all_meta_tags) -} diff --git a/src/models/data.rs b/src/models/data.rs deleted file mode 100644 index 8e2bd3f2..00000000 --- a/src/models/data.rs +++ /dev/null @@ -1,553 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use serde::{Deserialize, Serialize}; -use std::fmt; - -#[derive( - Debug, Default, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, -)] -/// Options for the `cname` function -pub struct CnameData { - /// A string representing the domain of the web app - pub cname: String, -} - -impl CnameData { - /// Creates a new `CnameData` struct with the given cname. - pub fn new(cname: String) -> Self { - CnameData { cname } - } -} - -#[derive( - Debug, Default, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, -)] -/// File struct to hold the title, permalink of a file. -pub struct PageData { - /// The title of the file. - pub title: String, - /// The description of the file. - pub description: String, - /// The publication date of the file. - pub date: String, - /// The permalink of the file. - pub permalink: String, -} - -impl fmt::Display for PageData { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{} {} {} {}", - self.title, self.description, self.date, self.permalink - ) - } -} - -#[derive( - Debug, Default, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, -)] -/// File struct to hold the name and content of a file. -pub struct FileData { - /// The name of the file. - pub name: String, - /// The content of the file. - pub content: String, - /// The content of the file, escaped for CNAME. - pub cname: String, - /// The content of the file, escaped for JSON. - pub json: String, - /// The content of the file, escaped for HUMANS. - pub human: String, - /// The content of the file, escaped for keywords. - pub keyword: String, - /// The content of the file, escaped for RSS. - pub rss: String, - /// The content of the file, escaped for sitemap. - pub sitemap: String, - /// The content of the file, escaped for sitemap_news. - pub sitemap_news: String, - // The content of the file, escaped for tags. - // pub tags: String, - /// The content of the file, escaped for TXT. - pub txt: String, -} - -impl FileData { - /// Creates a new `FileData` struct with the given name and content. - pub fn new(name: String, content: String) -> Self { - FileData { - name, - content, - cname: String::new(), - json: String::new(), - human: String::new(), - keyword: String::new(), - rss: String::new(), - sitemap: String::new(), - sitemap_news: String::new(), - // tags: String::new(), - txt: String::new(), - } - } -} - -#[derive( - Debug, Default, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, -)] -/// Options for the `tags` function -pub struct TagsData { - /// A string representing the publication date of the web app - pub dates: String, - /// A string representing the title of the web app - pub titles: String, - /// A string representing the description of the web app - pub descriptions: String, - /// A string representing the permalink of the web app - pub permalinks: String, - /// A string representing the keywords of the web app - pub keywords: String, -} - -#[derive( - Debug, Default, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, -)] -/// Options for the `sw_file` function -pub struct SwFileData { - /// A string representing the offline page of the web app - pub offline_page_url: String, -} - -impl SwFileData { - /// Creates a new `SwFileData` struct with the given offline page. - pub fn new(offline_page_url: String) -> Self { - SwFileData { offline_page_url } - } -} - -#[derive( - Debug, Default, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, -)] -/// Options for the `icon` function -pub struct IconData { - /// A string representing the purpose of the icon - pub purpose: Option, - /// A string representing the sizes of the icon - pub sizes: String, - /// A string representing the source of the icon - pub src: String, - /// A string representing the type of the icon - pub icon_type: Option, -} - -impl IconData { - /// Creates a new `IconData` struct with the given source and sizes. - pub fn new(src: String, sizes: String) -> Self { - IconData { - purpose: None, - sizes, - src, - icon_type: None, - } - } -} - -#[derive( - Debug, Default, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, -)] -/// Options for the `manifest` function -pub struct ManifestData { - /// A string representing the background color of the web app - pub background_color: String, - /// A string representing the description of the web app - pub description: String, - /// A string representing the display mode of the web app - pub display: String, - /// A vector representing the icons of the web app - pub icons: Vec, - /// A string representing the name of the web app - pub name: String, - /// A string representing the orientation of the web app - pub orientation: String, - /// A string representing the scope of the web app - pub scope: String, - /// A string representing the short name of the web app - pub short_name: String, - /// A string representing the start URL of the web app - pub start_url: String, - /// A string representing the theme color of the web app - pub theme_color: String, -} - -impl ManifestData { - /// Creates a new `ManifestData` struct with default values for all fields. - pub fn new() -> Self { - ManifestData::default() - } -} - -#[derive( - Debug, Default, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, -)] -/// Options for the `sitemap` function -pub struct SiteMapData { - /// A string representing the changefreq - pub changefreq: String, - /// A string representing the lastmod - pub lastmod: String, - /// A string representing the local - pub loc: String, -} - -impl SiteMapData { - /// Creates a new `SiteMapData` struct with the given loc, lastmod, and changefreq. - pub fn new( - loc: String, - lastmod: String, - changefreq: String, - ) -> Self { - SiteMapData { - changefreq, - lastmod, - loc, - } - } -} - -#[derive( - Debug, Default, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, -)] -/// Options for the `news_sitemap` function -pub struct NewsData { - /// A string representing the news genres (PressRelease|Satire|Blog|OpEd|Opinion|UserGenerated) - pub news_genres: String, - /// A string representing the news keywords - pub news_keywords: String, - /// A string representing the news language - pub news_language: String, - /// A string representing the news image location - pub news_image_loc: String, - /// A string representing the news location - pub news_loc: String, - /// A string representing the news publication date - pub news_publication_date: String, - /// A string representing the news publication name - pub news_publication_name: String, - /// A string representing the news title - pub news_title: String, -} - -/// Implementation of the `NewsData` struct. -impl NewsData { - /// Creates a new `NewsData` struct with default values for all fields. - pub fn new(data: NewsData) -> Self { - data - } - /// Creates a new `NewsData` struct with the given values. - pub fn create_default() -> Self { - Default::default() - } -} - -/// Options for the `news_visit` function -#[derive( - Debug, Default, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, -)] -pub struct NewsVisitOptions<'a> { - /// A string representing the base URL of the news website - pub base_url: &'a str, - /// A string representing the news genres (PressRelease|Satire|Blog|OpEd|Opinion|UserGenerated) - pub news_genres: &'a str, - /// A string representing the news keywords - pub news_keywords: &'a str, - /// A string representing the news language - pub news_language: &'a str, - /// A string representing the news publication date - pub news_publication_date: &'a str, - /// A string representing the news publication name - pub news_publication_name: &'a str, - /// A string representing the news title - pub news_title: &'a str, -} - -#[derive( - Debug, Default, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, -)] -/// Options for the `human` function -pub struct HumanData { - /// A string representing the author of the web app - pub author: Option, - /// A string representing the website of the author - pub author_website: Option, - /// A string representing the twitter of the author - pub author_twitter: Option, - /// A string representing the location of the author - pub author_location: Option, - /// A string representing the thanks of the author (name or url) - pub thanks: Option, - /// A string representing the site last updated date - pub site_last_updated: Option, - /// A string representing the site standards of the web app - pub site_standards: Option, - /// A string representing the site components of the web app - pub site_components: Option, - /// A string representing the site software of the web app - pub site_software: Option, -} - -impl HumanData { - /// Creates a new `HumanData` struct with default values for all fields. - pub fn new() -> Self { - HumanData::default() - } -} - -/// The `MetaTagGroups` struct holds collections of meta tags for different platforms and categories. -/// -/// The struct includes fields for Apple-specific meta tags, primary meta tags, Open Graph meta tags, -/// Microsoft-specific meta tags, and Twitter-specific meta tags. Each field contains a string -/// representation of the HTML meta tags for its respective category or platform. -#[derive( - Debug, Default, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, -)] -pub struct MetaTagGroups { - /// Meta tags specific to Apple devices - pub apple: String, - /// Primary meta tags, such as author, description, etc. - pub primary: String, - /// Open Graph meta tags, mainly used for social media - pub og: String, - /// Microsoft-specific meta tags - pub ms: String, - /// Twitter-specific meta tags - pub twitter: String, -} - -impl MetaTagGroups { - /// Creates a new `MetaTagGroups` instance with default values for all fields. - pub fn new() -> Self { - MetaTagGroups::default() - } - /// Returns the value for the given key, if it exists - pub fn get(&self, key: &str) -> Option<&String> { - match key { - "apple" => Some(&self.apple), - "primary" => Some(&self.primary), - "og" => Some(&self.og), - "ms" => Some(&self.ms), - "twitter" => Some(&self.twitter), - _ => None, - } - } - /// Returns true if all fields are empty. - pub fn is_empty(&self) -> bool { - self.apple.is_empty() - && self.primary.is_empty() - && self.og.is_empty() - && self.ms.is_empty() - && self.twitter.is_empty() - } -} - -#[derive( - Debug, Default, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, -)] -/// Options for the `txt` function -pub struct TxtData { - /// A string representing the permalink of the web app - pub permalink: String, -} - -impl TxtData { - /// Creates a new `TxtData` struct with the given permalink. - pub fn new(permalink: String) -> Self { - TxtData { permalink } - } -} - -#[derive( - Debug, Default, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, -)] -/// Options for the `humans` function -pub struct HumansData { - /// A string representing the author of the web app - pub author: String, - /// A string representing the website of the author - pub author_website: String, - /// A string representing the twitter of the author - pub author_twitter: String, - /// A string representing the location of the author - pub author_location: String, - /// A string representing the thanks of the author (name or url) - pub thanks: String, - /// A string representing the site last updated date - pub site_last_updated: String, - /// A string representing the site standards of the web app - pub site_standards: String, - /// A string representing the site components of the web app - pub site_components: String, - /// A string representing the site software of the web app - pub site_software: String, -} - -impl HumansData { - /// Creates a new `HumansData` struct with the given author and thanks. - pub fn new(author: String, thanks: String) -> Self { - HumansData { - author, - author_website: String::new(), - author_twitter: String::new(), - author_location: String::new(), - thanks, - site_last_updated: String::new(), - site_standards: String::new(), - site_components: String::new(), - site_software: String::new(), - } - } -} - -/// The `RssData` struct holds all necessary options and data for an RSS feed. -/// -/// This includes everything from metadata about the RSS feed itself, such as its title and language, -/// to information about individual items in the feed, such as their titles and publication dates. -/// -/// The values contained in an instance of `RssData` can be used to generate a complete RSS feed in XML format. -#[derive( - Debug, Default, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, -)] -pub struct RssData { - /// The Atom link of the RSS feed. - pub atom_link: String, - /// The author of the RSS feed. - pub author: String, - /// The category of the RSS feed. - pub category: String, - /// The copyright notice for the content of the feed. - pub copyright: String, - /// The description of the RSS feed. - pub description: String, - /// The docs of the RSS feed. - pub docs: String, - /// The generator of the RSS feed. - pub generator: String, - /// The image of the RSS feed. This can be a URL pointing to an image file. - pub image: String, - /// The Guid of the RSS item. This is a unique identifier for the item. - pub item_guid: String, - /// The description of the RSS item. - pub item_description: String, - /// The link of the RSS item. - pub item_link: String, - /// The publication date of the RSS item. - pub item_pub_date: String, - /// The title of the RSS item. - pub item_title: String, - /// The language of the RSS feed. - pub language: String, - /// The last build date of the RSS feed. - pub last_build_date: String, - /// The link to the atom feed. - pub link: String, - /// The managing editor of the RSS feed. - pub managing_editor: String, - /// The publication date of the RSS feed. - pub pub_date: String, - /// The title of the RSS feed. - pub title: String, - /// Time To Live: the number of minutes the feed should be cached before refreshing. - pub ttl: String, - /// The webmaster of the RSS feed. - pub webmaster: String, -} - -impl RssData { - /// Creates a new `RssData` struct with default values for all fields. - pub fn new() -> Self { - RssData::default() - } - - /// Sets the value of a field. - pub fn set>(&mut self, key: &str, value: T) { - match key { - "atom_link" => self.atom_link = value.into(), - "author" => self.author = value.into(), - "category" => self.category = value.into(), - "copyright" => self.copyright = value.into(), - "description" => self.description = value.into(), - "docs" => self.docs = value.into(), - "generator" => self.generator = value.into(), - "image" => self.image = value.into(), - "item_guid" => self.item_guid = value.into(), - "item_description" => self.item_description = value.into(), - "item_link" => self.item_link = value.into(), - "item_pub_date" => self.item_pub_date = value.into(), - "item_title" => self.item_title = value.into(), - "language" => self.language = value.into(), - "last_build_date" => self.last_build_date = value.into(), - "link" => self.link = value.into(), - "managing_editor" => self.managing_editor = value.into(), - "pub_date" => self.pub_date = value.into(), - "title" => self.title = value.into(), - "ttl" => self.ttl = value.into(), - "webmaster" => self.webmaster = value.into(), - _ => (), - } - } -} - -/// The `MetaTag` struct holds all necessary data for a single metatag. -/// -/// This includes everything from the name of the metatag to its content. -/// The values contained in an instance of `MetaTag` can be used to -/// generate a complete metatag in HTML format. -/// The `MetaTag` struct is used in the `Metatags` struct. -#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)] -pub struct MetaTag { - /// The name of the metatag. - pub name: String, - /// The content of the metatag. - pub value: String, -} - -impl MetaTag { - /// Creates a new `MetaTag` struct with the given name and value. - /// - /// # Arguments - /// - /// * `name` - The name of the metatag. - /// * `value` - The content of the metatag. - /// - /// # Returns - /// - /// A new `MetaTag` struct instance. - pub fn new(name: String, value: String) -> Self { - MetaTag { name, value } - } - - /// Generates a complete metatag in HTML format. - /// - /// # Returns - /// - /// A string representing the complete metatag in HTML format. - pub fn generate(&self) -> String { - format!( - "", - self.value, self.name - ) - } - - /// Generates a complete list of metatags in HTML format. - /// - /// # Arguments - /// - /// * `metatags` - A slice containing the `MetaTag` instances. - /// - /// # Returns - /// - /// A string representing the complete list of metatags in HTML format. - pub fn generate_metatags(metatags: &[MetaTag]) -> String { - metatags.iter().map(MetaTag::generate).collect() - } -} diff --git a/src/models/mod.rs b/src/models/mod.rs deleted file mode 100644 index 653719e3..00000000 --- a/src/models/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -/// The `data` module contains the structs. -pub mod data; diff --git a/src/modules/cname.rs b/src/modules/cname.rs deleted file mode 100644 index 60b2dbc1..00000000 --- a/src/modules/cname.rs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -// Import the CnameData model from the `crate::models::data` module. -use crate::models::data::CnameData; - -// Import the HashMap collection from the `std::collections` module. -use std::collections::HashMap; - -/// Function to create a CnameData object from a HashMap of metadata. -/// -/// The `metadata` HashMap must contain a key named `cname` with the value -/// of the CNAME record. -/// -/// Returns a CnameData object with the CNAME record value. -/// -/// The `cname` value is the canonical name of the domain or subdomain. It is -/// the "true" name of the domain or subdomain, and it is what other DNS -/// records point to. For example, the `cname` value for the `www.example.com` -/// subdomain is typically `example.com`. -/// -/// The `cname` value is used to create aliases for domains and subdomains. -/// This can be useful for a variety of reasons, such as: -/// -/// * To make it easier for users to remember the domain name or subdomain. -/// * To distribute traffic across multiple servers. -/// * To use a domain name or subdomain with a service provider that does not -/// allow you to create your own DNS records. -pub fn create_cname_data( - metadata: &HashMap, -) -> CnameData { - // Get the value of the `cname` key from the `metadata` HashMap. - let cname = metadata.get("cname").cloned().unwrap_or_default(); - - // Create a new CnameData object with the CNAME record value. - CnameData { cname } -} diff --git a/src/modules/frontmatter.rs b/src/modules/frontmatter.rs deleted file mode 100644 index c7efb2c3..00000000 --- a/src/modules/frontmatter.rs +++ /dev/null @@ -1,241 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use serde_json::{Map, Value as JsonValue}; -use std::collections::HashMap; -use toml::Value as TomlValue; -use yaml_rust::YamlLoader; - -/// ## Function: `extract` - Extracts front matter from a string of content -/// -/// This function extracts front matter from a string of content. It -/// supports YAML, TOML, and JSON front matter. It returns a `HashMap` -/// of the front matter key-value pairs. -/// -/// ### Arguments -/// -/// * `content` - The string of content to extract front matter from -/// (e.g. a Markdown file) -/// -/// ### Returns -/// -/// A `HashMap` of the front matter key-value pairs. If no front matter -/// is found, an empty `HashMap` is returned. -/// -/// -pub fn extract(content: &str) -> HashMap { - let mut front_matter = HashMap::new(); - - if let Some(front_matter_str) = - extract_front_matter_str(content, "---\n", "\n---\n") - { - if let Ok(doc) = parse_yaml_document(front_matter_str) { - front_matter - .extend(parse_yaml_hash(doc.as_hash().unwrap())); - } - } else if let Some(front_matter_str) = - extract_front_matter_str(content, "+++\n", "\n+++\n") - { - if let Ok(toml_value) = front_matter_str.parse::() { - front_matter.extend(parse_toml_table( - toml_value.as_table().unwrap(), - )); - } - } else if let Some(front_matter_str) = - extract_json_object_str(content) - { - if let Ok(json_value) = - serde_json::from_str::(front_matter_str) - { - if let Some(obj) = json_value - .get("frontmatter") - .and_then(|v| v.as_object()) - { - front_matter.extend(parse_json_object(obj)); - } else { - eprintln!("Error: Could not find frontmatter in JSON"); - } - if let Some(content) = - json_value.get("content").and_then(|v| v.as_str()) - { - front_matter - .insert("content".to_string(), content.to_string()); - } - } else { - eprintln!("Error parsing JSON"); - } - } - - front_matter -} - -/// ## Function: `extract_front_matter_str` - Extracts front matter from a string of content -/// -/// This function extracts front matter from a string of content. It -/// supports YAML, TOML, and JSON front matter. It returns a `HashMap` -/// of the front matter key-value pairs. -/// -/// ### Arguments -/// -/// * `content` - The string of content to extract front matter from -/// (e.g. a Markdown file) -/// * `start_delim` - The start delimiter of the front matter -/// * `end_delim` - The end delimiter of the front matter -/// -/// ### Returns -/// -/// A `HashMap` of the front matter key-value pairs. If no front matter -/// is found, an empty `HashMap` is returned. -/// -pub fn extract_front_matter_str<'a>( - content: &'a str, - start_delim: &str, - end_delim: &str, -) -> Option<&'a str> { - if content.starts_with(start_delim) { - if let Some(end_pos) = content.find(end_delim) { - return Some(&content[start_delim.len()..end_pos]); - } - } - None -} -/// ## Function: `parse_yaml_document` - Parses a YAML document into a `Yaml` object -/// -/// This function parses a YAML document into a `Yaml` object. -/// -/// ### Arguments -/// -/// * `front_matter_str` - The string of front matter to parse into a -/// `Yaml` object -/// -/// ### Returns -/// -/// A `Yaml` object representing the front matter string. If the front -/// matter string is not valid YAML, an error is returned. -/// -pub fn parse_yaml_document( - front_matter_str: &str, -) -> Result { - YamlLoader::load_from_str(front_matter_str) - .map(|docs| docs.into_iter().next().unwrap()) -} - -/// ## Function: `parse_yaml_hash` - Parses a YAML hash into a `HashMap` of key-value pairs -/// -/// This function parses a YAML hash into a `HashMap` of key-value pairs. -/// -/// ### Arguments -/// -/// * `yaml_hash` - The YAML hash to parse into a `HashMap` of key-value -/// pairs -/// -/// ### Returns -/// -/// A `HashMap` of key-value pairs representing the YAML hash. -/// If the YAML hash is not valid, an error is returned. -/// -pub fn parse_yaml_hash( - yaml_hash: &yaml_rust::yaml::Hash, -) -> HashMap { - let mut entries: Vec<_> = yaml_hash - .iter() - .filter_map(|(k, v)| { - v.as_str().map(|value| { - (k.as_str().unwrap().to_string(), value.to_string()) - }) - }) - .collect(); - entries.sort_by(|a, b| a.0.cmp(&b.0)); - entries.into_iter().collect() -} - -/// ## Function: `parse_toml_table` - Parses a TOML table into a `HashMap` of key-value pairs -/// -/// This function parses a TOML table into a `HashMap` of key-value pairs. -/// -/// ### Arguments -/// -/// * `toml_table` - The TOML table to parse into a `HashMap` of key- -/// value pairs. -/// -/// ### Returns -/// -/// A `HashMap` of key-value pairs representing the TOML table. -/// If the TOML table is not valid, an error is returned. -/// -pub fn parse_toml_table( - toml_table: &toml::value::Table, -) -> HashMap { - toml_table - .iter() - .filter_map(|(k, v)| { - v.as_str().map(|s| (k.to_string(), s.to_string())) - }) - .collect() -} -/// ## Function: `extract_json_object_str` - Extracts a JSON object from a string of content -/// -/// This function extracts a JSON object from a string of content. -/// -/// ### Arguments -/// -/// * `content` - The string of content to extract a JSON object from -/// -/// ### Returns -/// -/// A `&str` representing the JSON object. If no JSON object is found, -/// `None` is returned. -/// -pub fn extract_json_object_str(content: &str) -> Option<&str> { - if content.starts_with('{') { - let end_pos = content.rfind('}')?; - Some(&content[..=end_pos]) - } else { - None - } -} -/// ## Function: `parse_json_object` - Parses a JSON object into a `HashMap` of key-value pairs -/// -/// This function parses a JSON object into a `HashMap` of key-value pairs. -/// -/// ### Arguments -/// -/// * `json_object` - The JSON object to parse into a `HashMap` of key- -/// value pairs. -/// -/// ### Returns -/// -/// A `HashMap` of key-value pairs representing the JSON object. -/// If the JSON object is not valid, an error is returned. If the JSON -/// object is not a string, an empty string is returned. -/// -pub fn parse_json_object( - json_object: &Map, -) -> HashMap { - let mut result = json_object - .iter() - .map(|(k, v)| { - ( - k.to_string(), - match v { - JsonValue::String(s) => s.to_string(), - JsonValue::Number(n) => match n.as_f64() { - Some(f) => f.to_string(), - None => match n.as_i64() { - Some(i) => i.to_string(), - None => "".to_string(), - }, - }, - JsonValue::Bool(b) => b.to_string(), - JsonValue::Object(o) => serde_json::to_string(o) - .unwrap_or_else(|_| "".to_string()), - JsonValue::Array(a) => serde_json::to_string(a) - .unwrap_or_else(|_| "".to_string()), - _ => "".to_string(), - }, - ) - }) - .collect::>(); - result.sort_by_key(|(k, _)| k.to_string()); - result.into_iter().collect() -} diff --git a/src/modules/html.rs b/src/modules/html.rs deleted file mode 100644 index 7e07bf22..00000000 --- a/src/modules/html.rs +++ /dev/null @@ -1,228 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use crate::{ - modules::{ - markdown::convert_markdown_to_html, - postprocessor::post_process_html, - }, - utilities::directory::{ - extract_front_matter, format_header_with_id_class, - update_class_attributes, - }, -}; -use regex::Regex; - -/// Error enum for HTML generation. -#[derive(Debug)] -pub enum HtmlGenerationError { - /// Title cannot be empty - EmptyTitle, - /// Description cannot be empty - EmptyDescription, - /// Regex compilation error - RegexCompilationError(regex::Error), -} - -impl std::fmt::Display for HtmlGenerationError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match *self { - Self::EmptyTitle => write!(f, "Title cannot be empty."), - Self::EmptyDescription => { - write!(f, "Description cannot be empty.") - } - Self::RegexCompilationError(ref err) => { - write!(f, "Regex compilation error: {}", err) - } - } - } -} - -impl std::error::Error for HtmlGenerationError {} - -impl From for HtmlGenerationError { - fn from(err: regex::Error) -> Self { - Self::RegexCompilationError(err) - } -} - -/// Generates an HTML page from Markdown content, title, and description. -/// -/// # Arguments -/// -/// * `content` - A string slice containing the Markdown content. -/// * `title` - A string slice containing the title of the HTML page. -/// * `description` - A string slice containing the description of the HTML page. -/// * `json_content` - An optional string slice containing JSON content to be included in the HTML. -/// -/// # Returns -/// -/// A `Result` containing a `String` representing the generated HTML page if successful, or a `HtmlGenerationError` if an error occurs. -/// -/// # Examples -/// -/// ```rust -/// use ssg::modules::html::generate_html; -/// -/// let content = "## Hello, world!\n\nThis is a test."; -/// let title = "My Page"; -/// let description = "This is a test page"; -/// let html = generate_html(content, title, description, None); -/// let html_str = html.unwrap_or_else(|e| panic!("Error: {:?}", e)); -/// -/// assert_eq!(html_str, "

My Page

This is a test page

Hello, world!

\n

This is a test.

\n"); -/// ``` -pub fn generate_html( - content: &str, - title: &str, - description: &str, - json_content: Option<&str>, -) -> Result { - // Validate arguments - if title.is_empty() { - return Err(HtmlGenerationError::EmptyTitle); - } - - if description.is_empty() { - return Err(HtmlGenerationError::EmptyDescription); - } - - // Regex patterns for ID, class, and image tags - let id_regex = Regex::new(r"[^a-zA-Z0-9]+")?; - let class_regex = Regex::new(r#"\.class="([^&"]+)""#)?; - let img_regex = Regex::new(r"(]*?)(/?>)")?; - - // Extract front matter from content - let markdown_content = extract_front_matter(content); - - // Preprocess content to update class attributes and image tags - let processed_content = - preprocess_content(markdown_content, &class_regex, &img_regex)?; - - // Convert Markdown to HTML - let markdown_html = convert_markdown_to_html( - &processed_content, - &Default::default(), - ); - - // Unwrap the Result to get the String - let markdown_html = markdown_html.unwrap(); - - // Post-process HTML content - let processed_html = - post_process_html(&markdown_html, &class_regex, &img_regex); - - // Unwrap the Result to get the String - let processed_html = processed_html.unwrap(); - - // Generate page header and description - let header = generate_page_header(title, &id_regex); - let desc = generate_description(description); - - // Process headers in HTML - let header_tags = vec!["h1", "h2", "h3", "h4", "h5", "h6"]; - let html_string = - process_headers(&processed_html, &header_tags, &id_regex); - - // Construct the final HTML with JSON content if available - let json_html = json_content.map_or_else( - || "".to_string(), - |json_str| format!("

{}

", json_str), - ); - - Ok(format!("{}{}{}{}", header, desc, json_html, html_string)) -} - -/// Preprocesses the HTML content to update class attributes and image tags. -/// -/// # Arguments -/// -/// * `content` - A string containing the HTML content to be processed. -/// * `class_regex` - A reference to a `Regex` object for matching class attributes. -/// * `img_regex` - A reference to a `Regex` object for matching image tags. -/// -/// # Returns -/// -/// A `Result` containing a `String` with the processed HTML content, or a `HtmlGenerationError` if an error occurs. -/// -/// # Example -/// -/// ```rust -/// use regex::Regex; -/// use ssg::modules::html::preprocess_content; -/// -/// let content = "
...
"; -/// let class_regex = Regex::new(r#".class="([^"]+)""#).unwrap(); -/// let img_regex = Regex::new(r#"]+)>"#).unwrap(); -/// -/// let processed_content = preprocess_content(content, &class_regex, &img_regex).unwrap(); -/// println!("{}", processed_content); -/// ``` -pub fn preprocess_content( - content: &str, - class_regex: &Regex, - img_regex: &Regex, -) -> Result { - let processed_content: Vec = content - .lines() - .map(|line| { - update_class_attributes(line, class_regex, img_regex) - }) - .collect(); - Ok(processed_content.join("\n")) -} - -fn process_headers( - html: &str, - header_tags: &[&str], - _id_regex: &Regex, -) -> String { - let mut html_string = html.to_string(); - for tag in header_tags { - let re = - Regex::new(&format!("<{}>([^<]+)", tag, tag)).unwrap(); - let mut replacements: Vec<(String, String)> = Vec::new(); - - for cap in re.captures_iter(&html_string) { - let original = cap[0].to_string(); - let replacement = - format_header_with_id_class(&original, &re); - replacements.push((original, replacement)); - } - - for (original, replacement) in replacements { - html_string = html_string.replace(&original, &replacement); - } - } - html_string -} - -/// Generate page header HTML string based on title -/// -/// # Arguments -/// -/// * `title` - A string slice that holds the title to be included in the header tag. -/// * `id_regex` - A reference to a compiled Regex object used for processing the header string. -/// -/// # Returns -/// -/// A `String` that represents the HTML

header tag with the title. -/// -/// # Examples -/// -/// ```rust -/// use regex::Regex; -/// use ssg::modules::html::generate_page_header; -/// let id_regex = Regex::new(r"[^a-zA-Z0-9]+").unwrap(); -/// let header_html = generate_page_header("My Page Title", &id_regex); -/// assert_eq!(header_html, "

My Page Title

"); -/// ``` -pub fn generate_page_header(title: &str, id_regex: &Regex) -> String { - let header_str = format!("

{}

", title); - format_header_with_id_class(&header_str, id_regex) -} - -/// Generate description HTML string based on description -fn generate_description(description: &str) -> String { - format!("

{}

", description) -} diff --git a/src/modules/human.rs b/src/modules/human.rs deleted file mode 100644 index 54e35195..00000000 --- a/src/modules/human.rs +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -// Import the HumansData struct from the data module. -use crate::models::data::HumansData; - -// Import the HashMap struct from the collections module. -use std::collections::HashMap; - -/// Function to create a HumansData object from a HashMap of metadata. -/// -/// The `metadata` HashMap must contain the following keys: -/// -/// * `author_location`: The location of the author. -/// * `author_twitter`: The Twitter handle of the author. -/// * `author_website`: The website of the author. -/// * `author`: The name of the author of the website or blog. -/// * `site_components`: The components that the website or blog uses. -/// * `site_last_updated`: The date on which the website or blog was last updated. -/// * `site_software`: The software that the website or blog uses. -/// * `site_standards`: The standards that the website or blog follows. -/// * `thanks`: A list of people or organizations to thank for their contributions to the website or blog. -/// -/// Returns a HumansData object with the metadata. -pub fn create_human_data( - metadata: &HashMap, -) -> HumansData { - HumansData { - author_location: metadata - .get("author_location") - .cloned() - .unwrap_or_default(), - author_twitter: metadata - .get("author_twitter") - .cloned() - .unwrap_or_default(), - author_website: metadata - .get("author_website") - .cloned() - .unwrap_or_default(), - author: metadata.get("author").cloned().unwrap_or_default(), - site_components: metadata - .get("site_components") - .cloned() - .unwrap_or_default(), - site_last_updated: metadata - .get("site_last_updated") - .cloned() - .unwrap_or_default(), - site_software: metadata - .get("site_software") - .cloned() - .unwrap_or_default(), - site_standards: metadata - .get("site_standards") - .cloned() - .unwrap_or_default(), - thanks: metadata.get("thanks").cloned().unwrap_or_default(), - } -} diff --git a/src/modules/json.rs b/src/modules/json.rs deleted file mode 100644 index efb12b8e..00000000 --- a/src/modules/json.rs +++ /dev/null @@ -1,320 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 OR MIT -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. - -use crate::models::data::{ - CnameData, HumansData, ManifestData, NewsData, NewsVisitOptions, - SiteMapData, TxtData, -}; -use serde_json::{json, Map}; -use std::{ - fs, io, - path::{Path, PathBuf}, -}; - -/// Generates a CNAME file for a web app based on the provided `CnameData` options. -/// -/// # Arguments -/// -/// * `options` - The `CnameData` object containing the CNAME value. -/// -/// # Returns -/// -/// A string containing the CNAME file content. -/// -/// # Example -/// -/// ``` -/// use ssg::models::data::CnameData; -/// use ssg::modules::json::cname; -/// -/// let options = CnameData { -/// cname: "example.com".to_string(), -/// }; -/// -/// let cname_content = cname(&options); -/// assert_eq!(cname_content, "example.com\nwww.example.com"); -/// ``` -pub fn cname(options: &CnameData) -> String { - let cname_value = &options.cname; - let full_domain = format!("www.{}", cname_value); - format!("{}\n{}", cname_value, full_domain) -} - -/// Generates a humans.txt for a web app based on the provided `HumansData` options. -/// -/// # Arguments -/// -/// * `options` - The `HumansData` object containing the human-readable data. -/// -/// # Returns -/// -/// A string containing the humans.txt file content. -pub fn human(options: &HumansData) -> String { - let mut s = String::from("/* TEAM */\n"); - - if !options.author.is_empty() { - s.push_str(&format!(" Name: {}\n", options.author)); - } - if !options.author_website.is_empty() { - s.push_str(&format!( - " Website: {}\n", - options.author_website - )); - } - if !options.author_twitter.is_empty() { - s.push_str(&format!( - " Twitter: {}\n", - options.author_twitter - )); - } - if !options.author_location.is_empty() { - s.push_str(&format!( - " Location: {}\n", - options.author_location - )); - } - s.push_str("\n/* THANKS */\n"); - if !options.thanks.is_empty() { - s.push_str(&format!(" Thanks: {}\n", options.thanks)); - } - s.push_str("\n/* SITE */\n"); - if !options.site_last_updated.is_empty() { - s.push_str(&format!( - " Last update: {}\n", - options.site_last_updated - )); - } - if !options.site_standards.is_empty() { - s.push_str(&format!( - " Standards: {}\n", - options.site_standards - )); - } - if !options.site_components.is_empty() { - s.push_str(&format!( - " Components: {}\n", - options.site_components - )); - } - if !options.site_software.is_empty() { - s.push_str(&format!( - " Software: {}\n", - options.site_software - )); - } - s -} - -/// Generates a JSON manifest for a web app based on the provided `ManifestData` options. -/// -/// # Arguments -/// -/// * `options` - The `ManifestData` object containing the manifest data. -/// -/// # Returns -/// -/// A JSON string containing the manifest. -pub fn manifest( - options: &ManifestData, -) -> Result { - let mut json_map = Map::new(); - json_map.insert( - "background_color".to_string(), - json!(options.background_color), - ); - json_map - .insert("description".to_string(), json!(options.description)); - json_map.insert("display".to_string(), json!(options.display)); - - let mut icons_vec = vec![]; - for icon in &options.icons { - let mut icon_map = Map::new(); - icon_map.insert("src".to_string(), json!(icon.src)); - icon_map.insert("sizes".to_string(), json!(icon.sizes)); - if let Some(icon_type) = &icon.icon_type { - icon_map.insert("type".to_string(), json!(icon_type)); - } - if let Some(purpose) = &icon.purpose { - icon_map.insert("purpose".to_string(), json!(purpose)); - } - icons_vec.push(json!(icon_map)); - } - json_map.insert("icons".to_string(), json!(icons_vec)); - json_map.insert("name".to_string(), json!(options.name)); - json_map - .insert("orientation".to_string(), json!(options.orientation)); - json_map.insert("scope".to_string(), json!(options.scope)); - json_map - .insert("short_name".to_string(), json!(options.short_name)); - json_map.insert("start_url".to_string(), json!(options.start_url)); - json_map - .insert("theme_color".to_string(), json!(options.theme_color)); - - serde_json::to_string_pretty(&json_map) -} - -/// Generates a news sitemap for a web app based on the provided `NewsData` options. -/// -/// # Arguments -/// -/// * `options` - The `NewsData` object containing the news sitemap data. -/// -/// # Returns -/// -/// A string containing the news sitemap XML. -pub fn news_sitemap(options: NewsData) -> String { - let mut urls = vec![]; - if let Err(e) = news_visit_dirs(&options, &mut urls) { - eprintln!("Error generating news sitemap: {}", e); - } - format!( - r#"{}"#, - urls.join("\n") - ) -} - -/// Recursively visits all directories in a tree and adds the URLs of all index.html files to the `urls` vector. -fn visit_dirs( - base_dir: &Path, - dir: &Path, - base_url: &str, - changefreq: &str, - lastmod: &str, - urls: &mut Vec, -) -> io::Result<()> { - if dir.is_dir() { - for entry in fs::read_dir(dir)? { - let entry = entry?; - let path = entry.path(); - if path.is_dir() { - visit_dirs( - base_dir, &path, base_url, changefreq, lastmod, - urls, - )?; - } else if path.file_name().unwrap_or_default() - == "index.html" - { - match path.strip_prefix(base_dir) { - Ok(stripped_path) => { - if let Some(url) = stripped_path.to_str() { - urls.push(format!( - r#"{}{}{}/{}"#, - changefreq, lastmod, base_url, url - )); - } - } - Err(err) => { - return Err(io::Error::new( - io::ErrorKind::Other, - err, - )) - } - } - } - } - } - Ok(()) -} - -/// Recursively visits directories to generate news sitemap entries. -fn news_visit_dirs( - options: &NewsData, - urls: &mut Vec, -) -> io::Result<()> { - let base_url = &options.news_loc; - let news_publication_name = &options.news_publication_name; - let news_language = &options.news_language; - let news_image_loc = &options.news_image_loc; - let news_genres = &options.news_genres; - let news_publication_date = &options.news_publication_date; - let news_title = &options.news_title; - let news_keywords = &options.news_keywords; - - urls.push(format!( - r#"{}{}{}{}{}{}{}{}"#, - base_url, - news_publication_name, - news_language, - news_genres, - news_publication_date, - news_title, - news_keywords, - news_image_loc, - )); - - Ok(()) -} - -/// Generates a single news sitemap entry for all directories recursively visited. -/// -/// The `options` parameter contains the necessary fields for each sitemap entry. -/// -/// Returns a string containing the news sitemap XML. -pub fn generate_news_sitemap_entry( - options: &NewsVisitOptions<'_>, -) -> String { - format!( - r#" - {} - {} - - - {} - {} - - {} - {} - - "#, - options.base_url, - options.news_publication_date, - options.news_publication_name, - options.news_language, - options.news_publication_date, - options.news_title, - ) -} - -/// Generates a sitemap for a web app based on the provided `SiteMapData` options. -/// -/// # Arguments -/// -/// * `options` - The `SiteMapData` object containing the sitemap data. -/// * `dir` - The directory containing the web app. -/// -/// # Returns -/// -/// A string containing the sitemap.xml file. -pub fn sitemap(options: SiteMapData, dir: &Path) -> String { - let base_dir = PathBuf::from(dir); - let mut urls = vec![]; - if let Err(e) = visit_dirs( - &base_dir, - &base_dir, - &options.loc, - &options.changefreq, - &options.lastmod, - &mut urls, - ) { - eprintln!("Error generating sitemap: {}", e); - } - - format!( - r#"{}"#, - urls.join("\n") - ) -} - -/// Generates a robots.txt for a web app based on the provided `TxtData` options. -/// -/// # Arguments -/// -/// * `options` - The `TxtData` object containing the robots.txt data. -/// -/// # Returns -/// -/// A string containing the robots.txt file. -pub fn txt(options: &TxtData) -> String { - let url = format!("{}/sitemap.xml", options.permalink); - format!("User-agent: *\nSitemap: {}", url) -} diff --git a/src/modules/keywords.rs b/src/modules/keywords.rs deleted file mode 100644 index 45524611..00000000 --- a/src/modules/keywords.rs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use std::collections::HashMap; - -/// Extracts keywords from the metadata and returns them as a vector of strings. -/// -/// This function takes a reference to a HashMap containing metadata key-value pairs. -/// It looks for the "keywords" key in the metadata and extracts the keywords from its value. -/// Keywords are expected to be comma-separated. The extracted keywords are trimmed of any -/// leading or trailing whitespace, and returned as a vector of strings. -/// -/// # Arguments -/// -/// * `metadata` - A reference to a HashMap containing metadata. -/// -/// # Returns -/// -/// A vector of strings representing the extracted keywords. -/// -pub fn extract_keywords( - metadata: &HashMap, -) -> Vec { - // Check if the "keywords" key exists in the metadata. - // If it exists, split the keywords using a comma and process each keyword. - // If it doesn't exist, return an empty vector as the default value. - metadata - .get("keywords") - .map(|keywords| { - // Split the keywords using commas and process each keyword. - keywords - .split(',') - .map(|kw| kw.trim().to_string()) // Trim whitespace from each keyword. - .collect::>() // Collect the processed keywords into a vector. - }) - .unwrap_or_default() // Return an empty vector if "keywords" is not found. -} diff --git a/src/modules/manifest.rs b/src/modules/manifest.rs deleted file mode 100644 index b1df2d55..00000000 --- a/src/modules/manifest.rs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use crate::{ - macro_metadata_option, - models::data::{IconData, ManifestData}, -}; -use std::collections::HashMap; - -/// Creates a `ManifestData` object based on the provided metadata. -/// -/// # Arguments -/// -/// * `metadata` - A map of metadata strings. -/// -/// # Returns -/// -/// A `ManifestData` object. -pub fn create_manifest_data( - metadata: &HashMap, -) -> ManifestData { - ManifestData { - name: metadata - .get("name") - .map_or_else(|| "".to_string(), |v| v.to_string()), - short_name: macro_metadata_option!(metadata, "short_name"), - start_url: ".".to_string(), - display: "standalone".to_string(), - background_color: "#ffffff".to_string(), - description: macro_metadata_option!(metadata, "description"), - icons: metadata - .get("icon") - .map(|icon| { - vec![IconData { - src: icon.to_string(), - sizes: "512x512".to_string(), - icon_type: Some("image/svg+xml".to_string()), - purpose: Some("any maskable".to_string()), - }] - }) - .unwrap_or_default(), - orientation: "portrait-primary".to_string(), - scope: "/".to_string(), - theme_color: metadata - .get("theme-color") - .map(|color| format!("rgb({})", color)) - .unwrap_or_default(), - } -} diff --git a/src/modules/markdown.rs b/src/modules/markdown.rs deleted file mode 100644 index dcebc34e..00000000 --- a/src/modules/markdown.rs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use comrak::{markdown_to_html, ComrakOptions}; -use std::error::Error; - -/// Converts Markdown content to HTML using the Comrak library. -/// -/// # Arguments -/// -/// * `markdown_content` - A string containing the Markdown content to be converted. -/// * `options` - A reference to `ComrakOptions` which specifies the parsing options for the Comrak library. -/// -/// # Returns -/// -/// A `String` containing the converted HTML content. -/// -/// # Errors -/// -/// Returns a `Box` if conversion fails. -/// -/// # Examples -/// -/// ```rust -/// use ssg::modules::markdown::convert_markdown_to_html; -/// use comrak::ComrakOptions; -/// -/// let markdown_content = "# Hello, world!"; -/// let options = ComrakOptions::default(); -/// let html_content = convert_markdown_to_html(markdown_content, &options).unwrap(); -/// -/// assert_eq!(html_content, "

Hello, world!

\n"); -/// ``` -pub fn convert_markdown_to_html( - markdown_content: &str, - options: &ComrakOptions, -) -> Result> { - let html_content = markdown_to_html(markdown_content, options); - Ok(html_content.to_string()) -} diff --git a/src/modules/metatags.rs b/src/modules/metatags.rs deleted file mode 100644 index 0ce8574a..00000000 --- a/src/modules/metatags.rs +++ /dev/null @@ -1,244 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use crate::macro_generate_tags_from_fields; -use crate::models::data::{MetaTag, MetaTagGroups}; -use std::collections::HashMap; - -// Type alias for better readability -type MetaDataMap = HashMap; - -/// Generates HTML meta tags based on custom key-value mappings. -/// -/// # Arguments -/// * `mapping` - A slice of tuples, where each tuple contains a `String` key and an `Option` value. -/// -/// # Returns -/// A `String` containing the HTML code for the meta tags. -pub fn generate_custom_meta_tags( - mapping: &[(String, Option)], -) -> String { - let filtered_mapping: Vec<(String, String)> = mapping - .iter() - .filter_map(|(key, value)| { - value - .as_ref() - .map(|val| (key.clone(), val.clone())) - .filter(|(_, val)| !val.is_empty()) - }) - .collect(); - generate_metatags(&filtered_mapping) -} - -/// Generates HTML meta tags based on the provided key-value pairs. -/// -/// # Arguments -/// * `meta` - A slice of key-value pairs represented as tuples of `String` objects. -/// -/// # Returns -/// A `String` containing the HTML code for the meta tags. -pub fn generate_metatags(meta: &[(String, String)]) -> String { - meta.iter() - .map(|(key, value)| format_meta_tag(key, value.trim())) - .collect::>() - .join("\n") -} - -/// Generates HTML meta tags based on a list of tag names and a metadata HashMap. -/// -/// # Arguments -/// * `tag_names` - A slice of tag names as `&str`. -/// * `metadata` - A reference to a `MetaDataMap` containing metadata key-value pairs. -/// -/// # Returns -/// A `String` containing the HTML code for the meta tags. -pub fn load_metatags( - tag_names: &[&str], - metadata: &MetaDataMap, -) -> String { - let mut result = String::new(); - for &name in tag_names { - let value = - metadata.get(name).cloned().unwrap_or_else(String::new); - result.push_str( - &MetaTag::new(name.to_string(), value).generate(), - ); - } - result -} - -/// Utility function to format a single meta tag into its HTML representation. -/// -/// # Arguments -/// * `key` - The name attribute of the meta tag. -/// * `value` - The content attribute of the meta tag. -/// -/// # Returns -/// A `String` containing the HTML representation of the meta tag. -pub fn format_meta_tag(key: &str, value: &str) -> String { - // Sanitize the value by replacing newline characters with spaces - let sanitized_value = value.replace('\n', " "); - format!("", key, &sanitized_value) -} - -/// Generates HTML meta tags for Apple-specific settings. -/// -/// # Arguments -/// * `metadata` - A reference to a `HashMap` containing metadata key-value pairs. -/// -/// # Returns -/// A `String` containing the HTML code for the meta tags. -/// -pub fn generate_apple_meta_tags(metadata: &MetaDataMap) -> String { - macro_generate_tags_from_fields!( - tag_names, - metadata, - "apple_mobile_web_app_orientations" => apple_mobile_web_app_orientations, - "apple_touch_icon_sizes" => apple_touch_icon_sizes, - "apple-mobile-web-app-capable" => apple_mobile_web_app_capable, - "apple-mobile-web-app-status-bar-inset" => apple_mobile_web_app_status_bar_inset, - "apple-mobile-web-app-status-bar-style" => apple_mobile_web_app_status_bar_style, - "apple-mobile-web-app-title" => apple_mobile_web_app_title, - "apple-touch-fullscreen" => apple_touch_fullscreen - ) -} - -/// Generates HTML meta tags for primary settings like author, description, etc. -/// -/// # Arguments -/// * `metadata` - A reference to a `HashMap` containing metadata key-value pairs. -/// -/// # Returns -/// A `String` containing the HTML code for the meta tags. -/// -pub fn generate_primary_meta_tags(metadata: &MetaDataMap) -> String { - macro_generate_tags_from_fields!( - tag_names, - metadata, - "author" => author, - "description" => description, - "format-detection" => format_detection, - "generator" => generator, - "keywords" => keywords, - "language" => language, - "permalink" => permalink, - "rating" => rating, - "referrer" => referrer, - "revisit-after" => revisit_after, - "robots" => robots, - "theme-color" => theme_color, - "title" => title, - "viewport" => viewport - ) -} - -/// Generates HTML meta tags for Open Graph settings, primarily for social media. -/// -/// This function expects the `metadata` HashMap to contain keys such as: -/// -/// - "og:description": The description of the content. -/// - "og:image": The URL of the image to use. -/// - "og:image:alt": The alt text for the image. -/// - "og:image:height": The height of the image. -/// - "og:image:width": The width of the image. -/// - "og:locale": The locale of the content. -/// - "og:site_name": The name of the site. -/// - "og:title": The title of the content. -/// - "og:type": The type of content. -/// - "og:url": The URL of the content. -/// -/// # Arguments -/// * `metadata` - A reference to a `MetaDataMap` containing metadata key-value pairs. -/// -/// # Returns -/// A `String` containing the HTML code for the meta tags. -/// -pub fn generate_og_meta_tags(metadata: &MetaDataMap) -> String { - macro_generate_tags_from_fields!( - tag_names, - metadata, - "og:description" => description, - "og:image" => image, - "og:image:alt" => image_alt, - "og:image:height" => image_height, - "og:image:width" => image_width, - "og:locale" => locale, - "og:site_name" => site_name, - "og:title" => title, - "og:type" => type, - "og:url" => url - ) -} - -/// Generates HTML meta tags for Microsoft-specific settings. -/// -/// # Arguments -/// * `metadata` - A reference to a `HashMap` containing metadata key-value pairs. -/// -/// # Returns -/// A `String` containing the HTML code for the meta tags. -/// -pub fn generate_ms_meta_tags(metadata: &MetaDataMap) -> String { - macro_generate_tags_from_fields!( - tag_names, - metadata, - "msapplication-navbutton-color" => msapplication_navbutton_color - ) -} - -/// Generates HTML meta tags for Twitter-specific settings. -/// -/// This function expects the `metadata` HashMap to contain keys such as: -/// - "twitter:card": The type of Twitter card to use. -/// - "twitter:creator": The Twitter handle of the content creator. -/// - "twitter:description": The description of the content. -/// - "twitter:image": The URL of the image to use. -/// - "twitter:image:alt": The alt text for the image. -/// - "twitter:image:height": The height of the image. -/// - "twitter:image:width": The width of the image. -/// - "twitter:site": The Twitter handle of the site. -/// - "twitter:title": The title of the content. -/// - "twitter:url": The URL of the content. -/// -/// # Arguments -/// * `metadata` - A reference to a `MetaDataMap` containing metadata key-value pairs. -/// -/// # Returns -/// A `String` containing the HTML code for the meta tags. -/// -pub fn generate_twitter_meta_tags(metadata: &MetaDataMap) -> String { - macro_generate_tags_from_fields!( - tag_names, - metadata, - "twitter:card" => twitter_card, - "twitter:creator" => twitter_creator, - "twitter:description" => twitter_description, - "twitter:image" => twitter_image, - "twitter:image:alt" => twitter_image_alt, - "twitter:image:height" => twitter_image_height, - "twitter:image:width" => twitter_image_width, - "twitter:site" => twitter_site, - "twitter:title" => twitter_title, - "twitter:url" => twitter_url - ) -} - -/// Generates meta tags for the given metadata. -/// -/// # Arguments -/// -/// * `metadata` - The metadata extracted from the file. -/// -/// # Returns -/// -/// Returns a tuple containing meta tags for Apple devices, primary information, Open Graph, Microsoft, and Twitter. -/// -pub fn generate_all_meta_tags(metadata: &MetaDataMap) -> MetaTagGroups { - MetaTagGroups { - apple: generate_apple_meta_tags(metadata), - primary: generate_primary_meta_tags(metadata), - og: generate_og_meta_tags(metadata), - ms: generate_ms_meta_tags(metadata), - twitter: generate_twitter_meta_tags(metadata), - } -} diff --git a/src/modules/mod.rs b/src/modules/mod.rs deleted file mode 100644 index 52eb8ea1..00000000 --- a/src/modules/mod.rs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -/// The `cname` module generates the CNAME content. -pub mod cname; - -/// The `frontmatter` module extracts the front matter from files. -pub mod frontmatter; - -/// The `human` module contains functions for generating human-readable -pub mod human; - -/// The `html` module contains functions for generating HTML. -pub mod html; - -/// The `json` module generates the JSON content. -pub mod json; - -/// The `keywords` module contains functions for extracting keywords. -pub mod keywords; - -/// The `manifest` module generates the manifest file. -pub mod manifest; - -/// The `markdown` module contains functions for converting Markdown to HTML. -pub mod markdown; - -/// The `metatags` module contains functions for generating meta tags. -pub mod metatags; - -/// The `navigation` module generates the navigation menu. -pub mod navigation; - -/// The `newssitemap` module generates the newssitemap content. -pub mod news_sitemap; - -/// The `plaintext` module contains functions for generating plaintext. -pub mod plaintext; - -/// The `preprocessor` module contains functions for preprocessing content. -pub mod preprocessor; - -/// the `postprocessor` module contains functions for postprocessing content. -pub mod postprocessor; - -/// The `rss` module contains functions for generating RSS feeds. -pub mod rss; - -/// The `sitemap` module generates the sitemap content. -pub mod sitemap; - -/// The `tags` module contains functions for generating a tags page. -pub mod tags; - -/// The `txt` module generates the robots.txt content. -pub mod txt; diff --git a/src/modules/navigation.rs b/src/modules/navigation.rs deleted file mode 100644 index a4c4c175..00000000 --- a/src/modules/navigation.rs +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use crate::models::data::FileData; -use crate::utilities::directory::to_title_case; -use std::collections::{BTreeMap, HashSet}; -use std::fmt::Write; -use std::path::Path; - -// Define supported file extensions as a lazy static HashSet -lazy_static::lazy_static! { - static ref SUPPORTED_EXTENSIONS: HashSet<&'static str> = { - let mut set = HashSet::new(); - set.insert("md"); - set.insert("toml"); - set.insert("json"); - set - }; -} - -/// Struct representing the components of a file name -struct FileNameComponents { - file_stem: String, // The stem of the file name (without extension) - file_extension: Option, // The extension of the file name (if any) -} - -impl FileNameComponents { - /// Constructs FileNameComponents from a given Path - fn from_path(path: &Path) -> Self { - let file_stem = path - .file_stem() - .and_then(|n| n.to_str()) // Convert OsStr to &str - .unwrap_or_else(|| path.to_str().unwrap_or("")) // Handle non-Unicode file names - .to_string(); // Convert &str to String - let file_extension = path - .extension() - .and_then(|ext| ext.to_str()) - .map(String::from); - FileNameComponents { - file_stem, - file_extension, - } - } - - /// Gets the file name without the extension - fn file_name(&self) -> &str { - match &self.file_extension { - Some(ext) - if SUPPORTED_EXTENSIONS.contains(ext.as_str()) => - { - &self.file_stem - } - _ => &self.file_stem, - } - } -} - -/// Struct responsible for generating navigation HTML. -#[derive(Clone, Copy, Debug)] -pub struct NavigationGenerator; - -impl NavigationGenerator { - /// Generates a navigation menu as an unordered list of links. - /// - /// # Arguments - /// - /// * `files` - A slice of `FileData` structs containing the compiled HTML files. - /// - /// # Returns - /// - /// A string containing the HTML code for the navigation menu. - /// - /// The HTML code is wrapped in a `
    ` element with the class `navbar-nav`. - /// Each file is wrapped in a `
  • ` element, and each link is wrapped - /// in an `` element. - pub fn generate_navigation(files: &[FileData]) -> String { - // Check if there are files - if files.is_empty() { - return String::new(); // Return an empty string if there are no files - } - - // Filter supported files - let files_supported: Vec<&FileData> = files - .iter() - .filter(|file| { - let extension = Path::new(&file.name) - .extension() - .and_then(|ext| ext.to_str()) - .unwrap_or(""); - SUPPORTED_EXTENSIONS.contains(&extension) - }) - .collect(); - - // Check if there are supported files - if files_supported.is_empty() { - return String::new(); // Return an empty string if there are no supported files - } - - // Map file names to URLs - let mut file_links = BTreeMap::new(); - for file in files_supported { - let path = Path::new(&file.name); - let file_components = FileNameComponents::from_path(path); - let file_name = file_components.file_name(); - - if !["index", "404", "privacy", "terms", "offline"] - .contains(&file_name) - { - let url = format!( - "/{}/index.html", - path.with_extension("").display() - ); - file_links.insert(file_name.to_string(), url); - } - } - - // Generate navigation links - let mut nav_links = String::new(); - for (file_name, url) in file_links { - write!( - &mut nav_links, - "
  • {}
  • ", - file_name, - url, - to_title_case(&file_name), - &file_name, - ) - .unwrap_or_else(|e| { - eprintln!("Error writing navigation link: {}", e); - }); - } - - // Format navigation links into a HTML unordered list - format!( - "
      \n{}
    ", - nav_links - ) - } -} diff --git a/src/modules/news_sitemap.rs b/src/modules/news_sitemap.rs deleted file mode 100644 index 881f51c9..00000000 --- a/src/modules/news_sitemap.rs +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use crate::models::data::NewsData; // Import the NewsData model from the local crate. -use std::collections::HashMap; // Import the HashMap type from the standard library. - -/// Generates `NewsData` from metadata. -/// -/// # Arguments -/// * `metadata` - A hashmap containing page metadata, including last build date, change frequency, and page location. -/// -/// # Returns -/// A `NewsData` object populated with values from the metadata. -pub fn create_news_site_map_data( - metadata: &HashMap, -) -> NewsData { - // Convert the last build date from metadata to the desired format. - let news_publication_date = convert_date_format( - metadata - .get("news_publication_date") - .unwrap_or(&"".to_string()), - ); - - // Construct and return NewsData with converted and extracted metadata values. - NewsData { - news_genres: metadata - .get("news_genres") - .unwrap_or(&"".to_string()) - .to_string(), - news_image_loc: metadata - .get("news_image_loc") - .cloned() - .unwrap_or_default(), - news_keywords: metadata - .get("news_keywords") - .cloned() - .unwrap_or_default(), - news_language: metadata - .get("news_language") - .cloned() - .unwrap_or_default(), - news_loc: metadata.get("news_loc").cloned().unwrap_or_default(), - news_publication_date, - news_publication_name: metadata - .get("news_publication_name") - .cloned() - .unwrap_or_default(), - news_title: metadata - .get("news_title") - .cloned() - .unwrap_or_default(), - } -} - -/// Converts date strings from "Tue, 20 Feb 2024 15:15:15 GMT" format to "2024-02-20T15:15:15+00:00" format. -/// -/// # Arguments -/// * `input` - A string slice representing the input date. -/// -/// # Returns -/// A string representing the date in "YYYY-MM-DDTHH:MM:SS+00:00" format, or the original input if conversion is not applicable. -fn convert_date_format(input: &str) -> String { - // Split the input string by whitespace to extract date components. - let parts: Vec<&str> = input.split_whitespace().collect(); - - // Check if the input date string has the correct number of components. - if parts.len() == 6 { - let day = parts[1]; - let month = match parts[2] { - "Jan" => "01", - "Feb" => "02", - "Mar" => "03", - "Apr" => "04", - "May" => "05", - "Jun" => "06", - "Jul" => "07", - "Aug" => "08", - "Sep" => "09", - "Oct" => "10", - "Nov" => "11", - "Dec" => "12", - _ => return input.to_string(), // Return original input for unrecognized months. - }; - let year = parts[3]; - let time = parts[4]; - - // Assemble the converted date string. - let converted_date = - format!("{}-{}-{}T{}", year, month, day, time); - - // Append the timezone information. - let timezone = "+00:00"; - return format!("{}{}", converted_date, timezone); - } - - // Return the original input if it's not in the expected format. - input.to_string() -} diff --git a/src/modules/plaintext.rs b/src/modules/plaintext.rs deleted file mode 100644 index a756ba7b..00000000 --- a/src/modules/plaintext.rs +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use crate::modules::preprocessor::preprocess_content; -use crate::utilities::directory::extract_front_matter; -use pulldown_cmark::TagEnd; -use pulldown_cmark::{Event, Parser, Tag}; -use regex::Regex; -use std::error::Error; - -/// Type alias for the result of the `generate_plain_text` function -type PlainTextResult = Result< - (String, String, String, String, String, String), - Box, ->; - -/// Generate a plain text representation of the Markdown content. -/// -/// This function takes Markdown content as input and produces a plain text representation -/// by processing the Markdown syntax and removing any formatting. -/// -/// # Arguments -/// -/// * `content` - A string slice containing the Markdown content. -/// * `title` - A string slice containing the title of the Markdown content. -/// * `description` - A string slice containing the description of the Markdown content. -/// * `author` - A string slice containing the author of the Markdown content. -/// -/// # Returns -/// -/// A `Result` containing a tuple `(String, String, String)` representing the generated plain text, -/// title, and description if successful, or a `Box` if an error occurs during processing. -/// -/// # Errors -/// -/// This function may return an error if there is an issue with parsing or processing the Markdown content. -/// -/// # Examples -/// -/// ```rust -/// use ssg::modules::plaintext::generate_plain_text; -/// -/// let content = "## Hello, *world*!"; -/// let (plain_text, plain_title, plain_description, plain_author, plain_creator, plain_keywords) = generate_plain_text(content, "Title", "Description", "Author", "Creator", "Keywords").unwrap(); -/// -/// assert_eq!(plain_text, "Hello, world !"); -/// assert_eq!(plain_title, "Title"); -/// assert_eq!(plain_description, "Description"); -/// assert_eq!(plain_author, "Author"); -/// assert_eq!(plain_creator, "Creator"); -/// assert_eq!(plain_keywords, "Keywords"); -/// ``` - -pub fn generate_plain_text( - content: &str, - title: &str, - description: &str, - author: &str, - creator: &str, - keywords: &str, -) -> PlainTextResult { - // Regex patterns for class, and image tags - let class_regex = Regex::new(r#"\.class\s*=\s*"\s*[^"]*"\s*"#)?; - let img_regex = Regex::new(r"(]*?)(/?>)")?; - let link_ref_regex = Regex::new(r"\[([^\]]+)\]\[\d+\]")?; - - // Extract front matter from content - let markdown_content = extract_front_matter(content); - // Preprocess content to update class attributes and image tags - let processed_content = - preprocess_content(markdown_content, &class_regex, &img_regex)?; - - // Further preprocess to remove Markdown link references. - let no_markdown_links = - link_ref_regex.replace_all(&processed_content, "$1"); - - let mut plain_text = String::new(); - // let plain_title = title.to_string(); - // let plain_description = description.to_string(); - let parser = Parser::new(&no_markdown_links); - - let mut last_was_text = false; - let mut need_extra_line_break = false; - - for event in parser { - match event { - Event::Text(text) => { - if need_extra_line_break && !text.trim().is_empty() { - plain_text.push('\n'); - need_extra_line_break = false; - } - if last_was_text && !text.trim().is_empty() { - plain_text.push('\n'); - } - plain_text.push_str(text.trim_end()); - last_was_text = true; - } - Event::Start(tag) => { - if tag == Tag::Paragraph && last_was_text { - need_extra_line_break = true; - plain_text.push('\n'); - } - if tag == Tag::Emphasis { - plain_text.push(' '); - } - if tag == Tag::Strong { - plain_text.push_str(""); - } - match tag { - Tag::Heading { .. } => { - plain_text.push(' '); - } - Tag::Link { .. } => { - plain_text.push(' '); - } - _ => {} - } - last_was_text = false; - } - Event::End(tag) => { - if tag == Tag::Paragraph.into() { - plain_text.push('\n'); - } - if tag == Tag::Emphasis.into() { - plain_text.push(' '); - } - if tag == Tag::Strong.into() { - plain_text.push_str(""); - } - match tag { - TagEnd::Heading { .. } => { - plain_text.push('\n'); - } - TagEnd::Link { .. } => { - plain_text.push(' '); - } - _ => {} - } - last_was_text = false; - } - _ => {} - } - } - let plain_text = plain_text.trim(); - let plain_title = title.trim(); - let plain_description = description.trim(); - let plain_author = author.trim(); - let plain_creator = creator.trim(); - let plain_keywords = keywords.trim(); - Ok(( - plain_text.to_string(), - plain_title.to_string(), - plain_description.to_string(), - plain_author.to_string(), - plain_creator.to_string(), - plain_keywords.to_string(), - )) -} diff --git a/src/modules/postprocessor.rs b/src/modules/postprocessor.rs deleted file mode 100644 index 56bee93c..00000000 --- a/src/modules/postprocessor.rs +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use regex::{Captures, Regex}; -use std::error::Error; - -/// Post-processes HTML content by performing various transformations. -/// -/// This function processes each line of the HTML content to: -/// - Replace class attributes in HTML tags using `class_regex`. -/// - Ensure that each `` tag has both `alt` and `title` attributes. -/// If `title` is missing, it is set to the value of `alt`. If both are missing, -/// they remain unchanged. -/// -/// Efficiency is enhanced by pre-compiling regex objects for `alt` and `title` -/// attributes outside the main processing loop. This approach minimizes redundant -/// computations, especially for large HTML contents. -/// -/// Robust error handling is incorporated for regex compilation, ensuring that -/// the function responds gracefully to invalid regex patterns. -/// -/// # Arguments -/// -/// * `html` - The original HTML content as a string. -/// * `class_regex` - A `Regex` object for matching and replacing class attributes in HTML tags. -/// * `img_regex` - A `Regex` object for matching `` tags in HTML. -/// -/// # Returns -/// -/// A `Result` containing the transformed HTML content as a string if successful, -/// or a `Box` if an error occurs during regex compilation or processing. -/// -/// # Errors -/// -/// Returns an error if regex compilation or processing fails for any reason. -pub fn post_process_html( - html: &str, - class_regex: &Regex, - img_regex: &Regex, -) -> Result> { - let alt_regex = Regex::new(r#"alt="([^"]*)""#) - .map_err(|e| format!("Failed to compile alt regex: {}", e))?; - let _title_regex = Regex::new(r#"title="([^"]*)""#) - .map_err(|e| format!("Failed to compile title regex: {}", e))?; - - let mut processed_html = String::new(); - - for line in html.lines() { - let mut processed_line = line.to_string(); - let mut modified_line = processed_line.clone(); - - for class_captures in class_regex.captures_iter(&processed_line) - { - let class_attribute = - class_captures.get(1).unwrap().as_str(); - modified_line = class_regex - .replace( - &modified_line, - format!("

    ", class_attribute) - .as_str(), - ) - .to_string(); - } - - if let Some(class_value) = img_regex - .captures(&processed_line) - .and_then(|caps| caps.get(1)) - .map(|m| m.as_str().to_string()) - { - modified_line = img_regex - .replace(&modified_line, &class_value.to_string()) - .to_string(); - } - - processed_line = modified_line; - - processed_line = img_regex - .replace_all(&processed_line, |caps: &Captures<'_>| { - let img_tag_start = &caps[1]; - let img_tag_end = &caps[2]; - - let mut new_img_tag = img_tag_start.to_string(); - - let alt_value = alt_regex - .captures(img_tag_start) - .map_or(String::new(), |c| { - c.get(1).map_or(String::new(), |m| { - m.as_str().to_lowercase() - }) - }); - - if !new_img_tag.contains("title=") - && !alt_value.is_empty() - { - let title_prefix = "Image of "; - let max_alt_length = 66 - title_prefix.len(); - - let alt_substr = alt_value - .chars() - .take(max_alt_length) - .collect::(); - new_img_tag.push_str(&format!( - " title=\"{}\"", - alt_substr - )); - } - - new_img_tag.push_str(img_tag_end); - new_img_tag - }) - .to_string(); - - processed_html.push_str(&processed_line); - processed_html.push('\n'); - } - - Ok(processed_html) -} diff --git a/src/modules/preprocessor.rs b/src/modules/preprocessor.rs deleted file mode 100644 index 243918bf..00000000 --- a/src/modules/preprocessor.rs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use crate::utilities::directory::update_class_attributes; -use regex::Regex; -use std::error::Error; - -/// Preprocesses the Markdown content to update class attributes and image tags. -/// -/// # Arguments -/// -/// * `content` - A string containing the Markdown content to be processed. -/// * `class_regex` - A reference to a `Regex` object for matching class attributes. -/// * `img_regex` - A reference to a `Regex` object for matching image tags. -/// -/// # Returns -/// -/// A `Result` containing a `String` with the processed Markdown content, or a `Box` if an error occurs. -/// -pub fn preprocess_content( - content: &str, - class_regex: &Regex, - img_regex: &Regex, -) -> Result> { - let processed_content: Vec = content - .lines() - .map(|line| { - update_class_attributes(line, class_regex, img_regex) - }) - .collect(); - - let mut result = processed_content.join("\n"); - - // Trim trailing newlines - while result.ends_with('\n') { - result.pop(); - } - - Ok(result) -} diff --git a/src/modules/rss.rs b/src/modules/rss.rs deleted file mode 100644 index fcf0af8f..00000000 --- a/src/modules/rss.rs +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use crate::macro_write_element; -use crate::models::data::RssData; -use quick_xml::{ - escape::escape, - events::{BytesDecl, BytesEnd, BytesStart, Event}, - Writer, -}; -use std::error::Error; - -/// Generates an RSS feed from the given `RssData` struct. -/// -/// This function creates a complete RSS feed in XML format based on the data contained in the provided `RssData`. -/// It generates the feed by creating a series of XML elements corresponding to the fields of the `RssData`, -/// and writing them to a `Writer` object. -/// -/// The generated RSS feed is returned as a `String`. If an error occurs during generation, it returns an error. -pub fn generate_rss( - options: &RssData, -) -> Result> { - let mut writer = Writer::new(Vec::new()); - - // Write the XML declaration. - writer.write_event(Event::Decl(BytesDecl::new( - "1.0", - Some("utf-8"), - None, - )))?; - - // Start the `rss` element. - let mut rss_start = BytesStart::new("rss"); - rss_start.push_attribute(("version", "2.0")); - rss_start - .push_attribute(("xmlns:atom", "http://www.w3.org/2005/Atom")); - writer.write_event(Event::Start(rss_start))?; - - // Start the `channel` element. - writer.write_event(Event::Start(BytesStart::new("channel")))?; - - // Write the elements. - write_elements(&mut writer, options)?; - - // Write the `image` element. - write_image_element(&mut writer, options)?; - - // Write the `atom:link` element. - write_atom_link_element(&mut writer, options)?; - - // Write the `item` element. - write_item_element(&mut writer, options)?; - - // End the `channel` element. - writer.write_event(Event::End(BytesEnd::new("channel")))?; - - // End the `rss` element. - writer.write_event(Event::End(BytesEnd::new("rss")))?; - - let xml = writer.into_inner(); - let rss_str = String::from_utf8(xml)?; - - Ok(rss_str) -} - -/// Write the specified elements to the writer. -pub fn write_elements( - writer: &mut Writer, - options: &RssData, -) -> Result<(), Box> { - macro_write_element!(writer, "title", escape(&options.title))?; - macro_write_element!(writer, "link", escape(&options.link))?; - macro_write_element!( - writer, - "description", - escape(&options.description) - )?; - macro_write_element!( - writer, - "language", - escape(&options.language) - )?; - macro_write_element!(writer, "pubDate", escape(&options.pub_date))?; - macro_write_element!( - writer, - "lastBuildDate", - escape(&options.last_build_date) - )?; - macro_write_element!(writer, "docs", escape(&options.docs))?; - macro_write_element!( - writer, - "generator", - escape(&options.generator) - )?; - macro_write_element!( - writer, - "managingEditor", - escape(&options.managing_editor) - )?; - macro_write_element!( - writer, - "webMaster", - escape(&options.webmaster) - )?; - macro_write_element!( - writer, - "category", - escape(&options.category) - )?; - macro_write_element!(writer, "ttl", escape(&options.ttl))?; - Ok(()) -} - -/// Write the image element to the writer. -pub fn write_image_element( - writer: &mut Writer, - options: &RssData, -) -> Result<(), Box> { - writer.write_event(Event::Start(BytesStart::new("image")))?; - macro_write_element!(writer, "url", &options.image)?; - macro_write_element!(writer, "title", &options.title)?; - macro_write_element!(writer, "link", &options.link)?; - writer.write_event(Event::End(BytesEnd::new("image")))?; - Ok(()) -} - -/// Write the Atom link element to the writer. -pub fn write_atom_link_element( - writer: &mut Writer, - options: &RssData, -) -> Result<(), Box> { - let mut atom_link_start = BytesStart::new("atom:link"); - atom_link_start.push_attribute(( - "href", - options.atom_link.to_string().as_str(), - )); - atom_link_start.push_attribute(("rel", "self")); - atom_link_start.push_attribute(("type", "application/rss+xml")); - writer.write_event(Event::Empty(atom_link_start))?; - Ok(()) -} - -/// Write the `item` element. -pub fn write_item_element( - writer: &mut Writer, - options: &RssData, -) -> Result<(), Box> { - writer.write_event(Event::Start(BytesStart::new("item")))?; - macro_write_element!(writer, "author", escape(&options.author))?; - macro_write_element!( - writer, - "description", - escape(&options.item_description) - )?; - macro_write_element!(writer, "guid", escape(&options.item_guid))?; - macro_write_element!(writer, "link", escape(&options.item_link))?; - macro_write_element!( - writer, - "pubDate", - escape(&options.item_pub_date) - )?; - macro_write_element!(writer, "title", escape(&options.item_title))?; - writer.write_event(Event::End(BytesEnd::new("item")))?; - Ok(()) -} diff --git a/src/modules/sitemap.rs b/src/modules/sitemap.rs deleted file mode 100644 index 4b09e816..00000000 --- a/src/modules/sitemap.rs +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use crate::models::data::SiteMapData; // Import the SiteMapData model from the local crate. -use regex::Regex; -use std::collections::HashMap; // Standard library import for using HashMap. // Ensure the regex crate is imported for regular expression functionality. - -/// Generates `SiteMapData` from metadata. -/// -/// # Arguments -/// * `metadata` - A hashmap containing page metadata, including last build date, change frequency, and page location. -/// -/// # Returns -/// A `SiteMapData` object populated with values from the metadata. -pub fn create_site_map_data( - metadata: &HashMap, -) -> SiteMapData { - // Convert the last build date from metadata to the desired format. - let lastmod = convert_date_format( - metadata.get("last_build_date").unwrap_or(&"".to_string()), - ); - - // Construct and return SiteMapData with converted and extracted metadata values. - SiteMapData { - changefreq: metadata - .get("changefreq") - .cloned() - .unwrap_or_default(), - lastmod, - loc: metadata.get("permalink").cloned().unwrap_or_default(), - } -} - -/// Converts date strings from various formats to "YYYY-MM-DD". -/// -/// Supports conversion from "DD MMM YYYY" format and checks if input is already in target format. -/// -/// # Arguments -/// * `input` - A string slice representing the input date. -/// -/// # Returns -/// A string representing the date in "YYYY-MM-DD" format, or the original input if conversion is not applicable. -fn convert_date_format(input: &str) -> String { - // Define a regex to identify dates in the "DD MMM YYYY" format. - let re = Regex::new(r"\d{2} \w{3} \d{4}").unwrap(); - - // Check if input matches the expected date format. - if let Some(date_match) = re.find(input) { - let date_str = date_match.as_str(); - let parts: Vec<&str> = date_str.split_whitespace().collect(); - - // Proceed with conversion if input format matches. - if parts.len() == 3 { - let day = parts[0]; - let month = match parts[1] { - "Jan" => "01", - "Feb" => "02", - "Mar" => "03", - "Apr" => "04", - "May" => "05", - "Jun" => "06", - "Jul" => "07", - "Aug" => "08", - "Sep" => "09", - "Oct" => "10", - "Nov" => "11", - "Dec" => "12", - _ => return input.to_string(), // Return original input for unrecognized months. - }; - let year = parts[2]; - - // Return the formatted date string. - return format!("{}-{}-{}", year, month, day); - } - } - - // Return the original input if it's already in the correct format or doesn't match "DD MMM YYYY". - input.to_string() -} diff --git a/src/modules/tags.rs b/src/modules/tags.rs deleted file mode 100644 index dd54114c..00000000 --- a/src/modules/tags.rs +++ /dev/null @@ -1,293 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -use crate::models::data::{FileData, PageData, TagsData}; -use crate::utilities::directory::to_title_case; -use std::{ - collections::HashMap, - fs, - io::{self, Read, Write}, - path::Path, -}; - -/// Sanitize tag to remove special characters. -/// -/// # Arguments -/// -/// * `tag` - A reference to the tag string to be sanitized. -/// -/// # Returns -/// -/// A sanitized tag string with special characters removed. -/// -fn sanitize_tag(tag: &str) -> String { - tag.chars().filter(|c| c.is_alphanumeric()).collect() -} - -/// Generates a tag list from the given `FileData` and metadata, and returns it as a `HashMap`. -/// -/// # Arguments -/// -/// * `file` - A reference to a `FileData` struct, which contains the content of a single file. -/// * `metadata` - A reference to a `HashMap` containing metadata like tags, title, etc. -/// -/// # Returns -/// -/// A `HashMap` mapping each tag to a vector of `HashMap`s containing associated data like title, description, etc. -/// -/// # Examples -/// -/// ```rust -/// use ssg::models::data::FileData; -/// use ssg::modules::tags::generate_tags; -/// use std::collections::HashMap; -/// -/// let file = FileData { content: "This is a test".to_string(), ..Default::default() }; -/// let mut metadata = HashMap::new(); -/// metadata.insert("tags".to_string(), "tag1,tag2".to_string()); -/// -/// let result = generate_tags(&file, &metadata); -/// ``` -/// -pub fn generate_tags( - file: &FileData, - metadata: &HashMap, -) -> HashMap>> { - let mut keywords_data_map: HashMap< - String, - Vec>, - > = HashMap::new(); - let file_content = &file.content; - - // Extract target tags from metadata if available. - let default_tags = String::from(""); - let target_tags: Vec<&str> = metadata - .get("tags") - .unwrap_or(&default_tags) - .split(',') - .map(|s| s.trim()) - .filter(|s| !s.is_empty()) - .collect(); - - if target_tags.is_empty() { - println!("No tags found in metadata."); - return keywords_data_map; - } - - for tag in &target_tags { - let sanitized_tag = sanitize_tag(tag); - if file_content.contains(&sanitized_tag) { - let mut tags_data = HashMap::new(); - - // Extract metadata for the tag - let metadata_keys = [ - "title", - "date", - "description", - "permalink", - "keywords", - ]; - for key in &metadata_keys { - if let Some(value) = metadata.get(*key) { - tags_data.insert((*key).to_string(), value.clone()); - } - } - - // Insert or update the entry in keywords_data_map - keywords_data_map - .entry(sanitized_tag.to_string()) - .or_default() - .push(tags_data); - } - } - keywords_data_map -} - -/// Creates a `TagsData` struct populated with metadata. -/// -/// This function takes a reference to a `HashMap` containing metadata, such as -/// dates, descriptions, keywords, permalinks, and titles. It then constructs and returns a `TagsData` -/// struct populated with this metadata. -/// -/// # Arguments -/// -/// * `metadata` - A reference to a `HashMap` containing the metadata for the tags. -/// -/// # Returns -/// -/// Returns a `TagsData` struct populated with the metadata. -/// -/// # Examples -/// -/// ```rust -/// use std::collections::HashMap; -/// use ssg::modules::tags::create_tags_data; -/// -/// let mut metadata = HashMap::new(); -/// metadata.insert("date".to_string(), "2021-09-04".to_string()); -/// metadata.insert("description".to_string(), "A sample description".to_string()); -/// let tags_data = create_tags_data(&metadata); -/// ``` -/// -pub fn create_tags_data( - metadata: &HashMap, -) -> TagsData { - let dates = metadata.get("date").cloned().unwrap_or_default(); - let descriptions = - metadata.get("description").cloned().unwrap_or_default(); - let keywords = - metadata.get("keywords").cloned().unwrap_or_default(); - let permalinks = - metadata.get("permalink").cloned().unwrap_or_default(); - let titles = metadata.get("title").cloned().unwrap_or_default(); - - TagsData { - dates, - titles, - descriptions, - permalinks, - keywords, - } -} - -/// Generates the HTML content for displaying tags and their associated pages. -/// -/// This function takes a `HashMap` that maps tags to a list of `PageData` objects, -/// where each `PageData` object contains metadata about a page that uses the tag. -/// It then generates HTML content that displays these tags along with the pages that use them. -/// -/// # Arguments -/// -/// * `global_tags_data` - A reference to a `HashMap` where each key is a tag -/// and the corresponding value is a `Vec` of `PageData` objects that use the tag. -/// -/// # Returns -/// -/// A `String` containing the generated HTML content. -/// -/// # Examples -/// -/// ```rust -/// use std::collections::HashMap; -/// use ssg::models::data::PageData; -/// use ssg::modules::tags::generate_tags_html; -/// -/// let mut global_tags_data = HashMap::new(); -/// global_tags_data.insert( -/// "tag1".to_string(), -/// vec![ -/// PageData { -/// date: "2022-09-01".to_string(), -/// description: "Description 1".to_string(), -/// permalink: "/page1".to_string(), -/// title: "Page 1".to_string(), -/// }, -/// ], -/// ); -/// -/// global_tags_data.insert( -/// "tag2".to_string(), -/// vec![ -/// PageData { -/// date: "2022-09-02".to_string(), -/// description: "Description 2".to_string(), -/// permalink: "/page2".to_string(), -/// title: "Page 2".to_string(), -/// }, -/// ], -/// ); -/// -/// let html_content = generate_tags_html(&global_tags_data); -/// ``` -/// -pub fn generate_tags_html( - global_tags_data: &HashMap>, -) -> String { - let mut html_content = String::new(); - - // Create a sorted Vec of keys - let mut keys: Vec<&String> = global_tags_data.keys().collect(); - keys.sort(); - - // First, calculate the total number of posts - let total_posts: usize = - global_tags_data.values().map(|pages| pages.len()).sum(); - - // Add an h2 element for the total number of posts - html_content.push_str(&format!( - "

    ", - total_posts - )); - - // Existing loop code for each tag - for key in keys { - let tag = key; - let pages = &global_tags_data[key]; - let count = pages.len(); - html_content.push_str(&format!( - "

    {} ({} Posts)

    \n
      ", - tag.replace(' ', "-"), - tag.replace(' ', "-"), - to_title_case(tag), - count - )); - for page in pages.iter() { - html_content.push_str(&format!( - "
    • {}: {} - {}
    • \n", - page.date, page.permalink, page.title, page.description - )); - } - html_content.push_str("
    \n"); - } - - html_content -} - -/// Writes the given HTML content into an existing `index.html` file, replacing a placeholder. -/// -/// This function takes in the generated HTML content and a path to the output directory. -/// It then reads an existing `index.html` file located in `tags/` sub-directory of the given output directory, -/// replaces a `[[content]]` placeholder with the given HTML content, and writes it back to the file. -/// -/// # Arguments -/// -/// * `html_content` - The generated HTML content to be written. -/// * `output_path` - The path to the output directory where the `index.html` file is located. -/// -/// # Returns -/// -/// Returns an `io::Result<()>` which is `Ok` if the operation was successful. -/// Any IO error that occurs will be propagated in the `Err` variant of the result. -/// -/// # Examples -/// -/// ```rust -/// use std::path::Path; -/// use ssg::modules::tags::write_tags_html_to_file; -/// -/// let html_content = "

    Hello World

    "; -/// let output_path = Path::new("/path/to/output"); -/// write_tags_html_to_file(html_content, &output_path); -/// ``` -/// -pub fn write_tags_html_to_file( - html_content: &str, - output_path: &Path, -) -> io::Result<()> { - // Define the file path for the output - let file_path = output_path.join("tags/index.html"); - - // Read the existing HTML content from the file - let mut file = fs::File::open(&file_path)?; - let mut base_html = String::new(); - file.read_to_string(&mut base_html)?; - - // Replace [[content]] with the generated HTML content - base_html = base_html.replace("[[content]]", html_content); - - // Write the modified HTML content back to the file - let mut file = fs::File::create(&file_path)?; - file.write_all(base_html.as_bytes())?; - - Ok(()) -} diff --git a/src/modules/txt.rs b/src/modules/txt.rs deleted file mode 100644 index 530d5fe3..00000000 --- a/src/modules/txt.rs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 OR MIT - -// Import models::data::TxtData -use crate::models::data::TxtData; - -// Import std::collections::HashMap -use std::collections::HashMap; - -/// Function to create TxtData. -/// -/// The `metadata` parameter is a map of metadata strings. -/// -/// Returns a `TxtData` object. -pub fn create_txt_data(metadata: &HashMap) -> TxtData { - let permalink = match metadata.get("permalink") { - Some(permalink) => permalink.clone(), - None => String::default(), - }; - TxtData { permalink } -} diff --git a/src/process.rs b/src/process.rs new file mode 100644 index 00000000..24342bcf --- /dev/null +++ b/src/process.rs @@ -0,0 +1,415 @@ +// Copyright © 2024 Shokunin Static Site Generator. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use anyhow::Result; +use clap::ArgMatches; +use std::{fs, path::Path}; +use thiserror::Error; + +/// Represents errors that may occur during argument processing. +#[derive(Error, Debug)] +pub enum ProcessError { + /// Occurs when a directory cannot be created. + /// + /// # Fields + /// - `dir_type`: The type of directory (e.g., "content", "output"). + /// - `path`: The file path where the directory creation failed. + #[error( + "Failed to create {dir_type} directory at '{path}': {source}" + )] + DirectoryCreation { + /// Type of the directory, such as "content" or "output". + dir_type: String, + /// Path where the directory creation failed. + path: String, + #[source] + /// The underlying IO error that occurred. + source: std::io::Error, + }, + + /// Triggered when a required command-line argument is missing. + /// + /// # Fields + /// - The name of the missing argument. + #[error("Required argument missing: {0}")] + MissingArgument(String), + + /// Represents a failure during the compilation process. + /// + /// # Fields + /// - Compilation error message. + #[error("Compilation error: {0}")] + CompilationError(String), + + /// Wraps underlying I/O errors. + #[error(transparent)] + IoError(#[from] std::io::Error), +} + +/// Retrieves the value of a specified command-line argument. +/// +/// # Arguments +/// +/// * `matches` - Clap argument matches object containing parsed arguments. +/// * `name` - The name of the argument to retrieve. +/// +/// # Returns +/// +/// * `Result` - Returns the argument value on success or an error if the argument is missing. +/// +/// # Errors +/// +/// - Returns `ProcessError::MissingArgument` if the specified argument is not provided. +/// +/// # Example +/// +/// ```rust,no_run +/// # use clap::{ArgMatches, Command}; +/// # use ssg::process::get_argument; +/// let matches = Command::new("test") +/// .arg(clap::arg!(--"config" "Specifies the configuration file")) +/// .get_matches_from(vec!["test", "--config", "path/to/config.toml"]); +/// let config_path = get_argument(&matches, "config").expect("Argument not found"); +/// println!("Config path: {}", config_path); +/// ``` +pub fn get_argument( + matches: &ArgMatches, + name: &str, +) -> Result { + matches + .get_one::(name) + .ok_or_else(|| ProcessError::MissingArgument(name.to_string())) + .map(String::from) +} + +/// Ensures the specified directory exists, creating it if necessary. +/// +/// # Arguments +/// +/// * `path` - The path of the directory to check. +/// * `dir_type` - A label describing the directory type (e.g., "content", "output"). +/// +/// # Returns +/// +/// * `Result<(), ProcessError>` - Returns `Ok` if the directory exists or is successfully created. +/// +/// # Errors +/// +/// - Returns `ProcessError::DirectoryCreation` if the directory cannot be created due to permissions or other issues. +/// +/// # Example +/// +/// ```rust,no_run +/// # use std::path::Path; +/// # use ssg::process::ensure_directory; +/// let path = Path::new("path/to/output"); +/// ensure_directory(path, "output").expect("Failed to ensure directory exists"); +/// ``` +pub fn ensure_directory( + path: &Path, + dir_type: &str, +) -> Result<(), ProcessError> { + if !path.exists() { + fs::create_dir_all(path).map_err(|e| { + // Convert the IO error to our custom error type + if e.kind() == std::io::ErrorKind::PermissionDenied { + ProcessError::DirectoryCreation { + dir_type: dir_type.to_string(), + path: path.display().to_string(), + source: e, + } + } else { + ProcessError::IoError(e) + } + })?; + println!("Created {} directory: {}", dir_type, path.display()); + } + Ok(()) +} + +/// Compiles the static site by generating the necessary files from the provided paths. +/// +/// # Parameters +/// +/// * `build_path`: The path where the compiled site will be built. +/// * `content_path`: The path to the directory containing the content files. +/// * `site_path`: The path to the directory where the site project will be created. +/// * `template_path`: The path to the directory containing the template files. +/// +/// # Return +/// +/// * `Result<(), String>`: Returns `Ok(())` if the compilation is successful, or an error message as a string if an error occurs. +/// +/// # Errors +/// +/// * If any error occurs during the compilation process, an error message will be returned as a string. +fn internal_compile( + build_path: &Path, + content_path: &Path, + site_path: &Path, + template_path: &Path, +) -> Result<(), String> { + staticdatagen::compiler::service::compile( + build_path, + content_path, + site_path, + template_path, + ) + .map_err(|e| e.to_string()) +} + +/// Processes command-line arguments and initiates the static site generation. +/// +/// This function performs the following steps: +/// 1. Retrieves required directory paths from command-line arguments. +/// 2. Ensures each directory exists, creating it if necessary. +/// 3. Calls the compilation service to generate the static site. +/// +/// # Arguments +/// +/// * `matches` - Parsed command-line arguments from `clap`. +/// +/// # Returns +/// +/// * `Result<(), ProcessError>` - Returns `Ok` on successful completion, or an error if a problem occurs. +/// +/// # Errors +/// +/// - Returns `ProcessError::MissingArgument` if a required argument is not provided. +/// - Returns `ProcessError::DirectoryCreation` if a directory cannot be created. +/// - Returns `ProcessError::CompilationError` if the site fails to compile. +/// +pub fn args(matches: &ArgMatches) -> Result<(), ProcessError> { + // Get required paths + let content_dir = get_argument(matches, "content")?; + let output_dir = get_argument(matches, "output")?; + let site_dir = get_argument(matches, "new")?; + let template_dir = get_argument(matches, "template")?; + + // Create Path objects + let content_path = Path::new(&content_dir); + let build_path = Path::new(&output_dir); + let site_path = Path::new(&site_dir); + let template_path = Path::new(&template_dir); + + // Ensure directories exist + ensure_directory(content_path, "content")?; + ensure_directory(build_path, "output")?; + ensure_directory(site_path, "project")?; + ensure_directory(template_path, "template")?; + + // Compile the site + internal_compile( + build_path, + content_path, + site_path, + template_path, + ) + .map_err(ProcessError::CompilationError)?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::{arg, Command}; + use tempfile::tempdir; + + /// Helper function to create a test `ArgMatches` with all required arguments. + fn create_test_command() -> ArgMatches { + Command::new("test") + .arg(arg!(--"content" "Content directory")) + .arg(arg!(--"output" "Output directory")) + .arg(arg!(--"new" "New site directory")) + .arg(arg!(--"template"