diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index dc523f3..631bb1b 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -19,4 +19,5 @@ jobs: - name: Upload to codecov.io uses: codecov/codecov-action@v2 with: + token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true diff --git a/.github/workflows/languagetool.yml b/.github/workflows/languagetool.yml index 874a519..5b312b7 100644 --- a/.github/workflows/languagetool.yml +++ b/.github/workflows/languagetool.yml @@ -1,18 +1,19 @@ +name: LanguageTool + on: pull_request: - path: | - "README.md" workflow_dispatch: -name: LanguageTool check - jobs: languagetool_check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - uses: reviewdog/action-languagetool@v1 + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Check and report + uses: reviewdog/action-languagetool@v1 with: reporter: github-pr-review - patterns: README.md + patterns: '*.md src/**.rs' level: warning diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..170f287 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,54 @@ +# Lint code and (optionally) apply fixes +name: Lint code + +on: + pull_request: + push: + branches: [main] + schedule: + - cron: 0 0 * * 1 # Every monday + workflow_dispatch: + +jobs: + auto-update: + runs-on: ubuntu-latest + if: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }} + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install pre-commit + run: pip install pre-commit + + - name: Run autoupdate + run: pre-commit autoupdate + + - name: Create a pull request with updated versions + uses: peter-evans/create-pull-request@v6 + with: + branch: update/pre-commit-hooks + title: 'chore(deps): update pre-commit hooks' + commit-message: 'chore(deps): update pre-commit hooks' + pre-commit: + runs-on: ubuntu-latest + if: ${{ github.event_name != 'schedule' }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust nightly + uses: dtolnay/rust-toolchain@nightly + with: + components: clippy,rustfmt + + - name: Run pre-commit hooks + uses: pre-commit/action@v3.0.1 + + - name: Apply fixes when present + uses: pre-commit-ci/lite-action@v1.0.2 + if: always() + with: + msg: 'chore(fmt): auto fixes from pre-commit hooks' diff --git a/.github/workflows/CI.yml b/.github/workflows/publish.yml similarity index 76% rename from .github/workflows/CI.yml rename to .github/workflows/publish.yml index b110d15..db97644 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/publish.yml @@ -24,9 +24,8 @@ jobs: toolchain: stable override: true - - run: cargo publish --features full --token ${CRATES_TOKEN} - env: - CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }} + - name: Publish on crates.io + run: cargo publish --features full --token ${{ secrets.CRATES_TOKEN }} check-publish: name: Check Publish runs-on: ubuntu-latest @@ -42,6 +41,5 @@ jobs: toolchain: stable override: true - - run: cargo publish --features cli --token ${CRATES_TOKEN} --dry-run -v - env: - CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }} + - name: Check if can publish on crates.io + run: cargo publish --features cli --token ${{ secrets.CRATES_TOKEN }} --dry-run -v diff --git a/.github/workflows/rustcheck.yml b/.github/workflows/rustcheck.yml index d50fd8d..94eba66 100644 --- a/.github/workflows/rustcheck.yml +++ b/.github/workflows/rustcheck.yml @@ -13,4 +13,13 @@ jobs: steps: - uses: actions/checkout@v3 - uses: taiki-e/install-action@cargo-hack - - run: cargo hack check --feature-powerset --no-dev-deps + - run: > + cargo hack check + --feature-powerset + --no-dev-deps + --clean-per-run + --group-features cli,log,pretty_env_logger,color,clap-verbosity-flag + --group-features cli-complete,docker + --group-features typst,html,markdown + --mutually-exclusive-features native-tls,native-tls-vendored + --exclude-features snapshots diff --git a/.github/workflows/rustdoc.yml b/.github/workflows/rustdoc.yml index 4264261..babfee1 100644 --- a/.github/workflows/rustdoc.yml +++ b/.github/workflows/rustdoc.yml @@ -1,7 +1,7 @@ on: pull_request: paths: - - src/lib/**.rs + - src/**.rs - Cargo.toml workflow_dispatch: diff --git a/.github/workflows/rustlib.yml b/.github/workflows/rustlib.yml index 13b9422..89b3643 100644 --- a/.github/workflows/rustlib.yml +++ b/.github/workflows/rustlib.yml @@ -1,7 +1,7 @@ on: pull_request: paths: - - src/lib/**.rs + - '**.rs' - .github/workflows/rustlib.yml - Cargo.toml workflow_dispatch: @@ -30,13 +30,19 @@ jobs: strategy: fail-fast: false matrix: - tag: [latest, '5.5', '5.6', '5.7', '5.8', '5.9', '6.0', '6.1'] + tag: [latest, '5.5', '5.6', '5.7', '5.8', '5.9', '6.0', '6.1', '6.2', '6.3', '6.4', '6.5'] + features: [''] + include: + - tag: latest + features: --all-features runs-on: ubuntu-latest services: languagetool: image: erikvl87/languagetool:${{ matrix.tag }} ports: - 8010:8010 + env: + langtool_maxTextLength: 1500 env: LANGUAGETOOL_HOSTNAME: http://localhost LANGUAGETOOL_PORT: 8010 @@ -51,4 +57,4 @@ jobs: uses: Swatinem/rust-cache@v2 - run: curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C ${CARGO_HOME:-~/.cargo}/bin - - run: cargo nextest run --all-features + - run: cargo nextest run ${{ matrix.features }} --no-capture diff --git a/.github/workflows/rustlints.yml b/.github/workflows/rustlints.yml deleted file mode 100644 index 5783cad..0000000 --- a/.github/workflows/rustlints.yml +++ /dev/null @@ -1,39 +0,0 @@ -on: - pull_request: - paths: - - '**.rs' - - Cargo.toml - workflow_dispatch: - -name: Rust lints - -jobs: - clippy: - name: Clippy - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@v3 - - - name: Install stable toolchain - uses: dtolnay/rust-toolchain@stable - with: - components: clippy - - - name: Check clippy - run: cargo clippy --all-features -- -D warnings - - rustfmt: - name: Rustfmt - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@v3 - - - name: Install stable toolchain - uses: dtolnay/rust-toolchain@nightly - with: - components: rustfmt - - - name: Check format - run: cargo +nightly fmt --check diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d16ef2f..7d49267 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,20 +1,38 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.6.0 hooks: - id: check-yaml - id: check-toml - id: end-of-file-fixer - id: trailing-whitespace +- repo: https://github.com/igorshubovych/markdownlint-cli + rev: v0.35.0 + hooks: + - id: markdownlint-fix + args: [--ignore, LICENSE.md] - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks - rev: v2.10.0 + rev: v2.13.0 hooks: - id: pretty-format-yaml args: [--autofix] - id: pretty-format-toml - args: [--autofix] -- repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.35.0 + args: [--autofix, --trailing-commas] +- repo: https://github.com/doublify/pre-commit-rust + rev: v1.0 hooks: - - id: markdownlint-fix - args: [--ignore, LICENSE.md] + - id: cargo-check + - id: clippy +- repo: local + hooks: + - id: fmt + name: fmt + description: Format files with cargo fmt + entry: cargo +nightly fmt -- + language: system + types: [rust] + args: [] +- repo: https://github.com/codespell-project/codespell + rev: v2.2.6 + hooks: + - id: codespell diff --git a/CHANGELOG.md b/CHANGELOG.md index b1b9e8f..921f3c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/jeertmans/languagetool-rust/compare/v2.1.4...HEAD) +### Added + +- Added support for Mardown and Typst files. + [#117](https://github.com/jeertmans/languagetool-rust/pull/117) +- Added basic logger (e.g., increase verbosity with `ltrs -v`). + [#117](https://github.com/jeertmans/languagetool-rust/pull/117) + +### Changed + +- Changed *info* output when reading from STDIN to writing logs with INFO level, + instead of always writing to STDOUT. + [#117](https://github.com/jeertmans/languagetool-rust/pull/117) + ### Chore - Fixed dependency declaration in README.md. [#118](https://github.com/jeertmans/languagetool-rust/pull/118) @@ -22,13 +35,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed api calls to language tool server to adhere to spec. Pass POST calls as `application/x-www-form-urlencoded`. [#129](https://github.com/jeertmans/languagetool-rust/pull/129) - Fixed `RELEASE-PROCESS.md` description [#133](https://github.com/jeertmans/languagetool-rust/pull/133) -## [2.1.4](https://github.com/jeertmans/languagetool-rust/compare/v2.1.3...v2.1.4) +## [2.1.4](https://github.com/jeertmans/languagetool-rust/compare/v2.1.3...v2.1.4) - 2023-08-10 ### Fixed - Fixed serializing of `interpretAs` in `data`. [#103](https://github.com/jeertmans/languagetool-rust/pull/103) -## [2.1.3](https://github.com/jeertmans/languagetool-rust/compare/v2.1.2...v2.1.3) +## [2.1.3](https://github.com/jeertmans/languagetool-rust/compare/v2.1.2...v2.1.3) - 2023-07-24 ### Chore @@ -40,122 +53,172 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Allow text starting with hyphens. [#100](https://github.com/jeertmans/languagetool-rust/pull/100) -## [2.1.2](https://github.com/jeertmans/languagetool-rust/compare/v2.1.1...v2.1.2) 2023-05-29 +## [2.1.2](https://github.com/jeertmans/languagetool-rust/compare/v2.1.1...v2.1.2) - 2023-05-29 ### Fixed -- Fixed serializing of comma-separated values. [#86](https://github.com/jeertmans/languagetool-rust/pull/86) +- Fixed serializing of comma-separated values. + [#86](https://github.com/jeertmans/languagetool-rust/pull/86) -## [2.1.1](https://github.com/jeertmans/languagetool-rust/compare/v2.1.0...v2.1.1) 2023-04-07 +## [2.1.1](https://github.com/jeertmans/languagetool-rust/compare/v2.1.0...v2.1.1) - 2023-04-07 ### Chore -- Added Arch Linux installation ([@Dosx001](https://github.com/Dosx001)). [#77](https://github.com/jeertmans/languagetool-rust/pull/77) +- Added Arch Linux installation ([@Dosx001](https://github.com/Dosx001)). + [#77](https://github.com/jeertmans/languagetool-rust/pull/77) -## [2.1.0](https://github.com/jeertmans/languagetool-rust/compare/v2.0.0...v2.1.0) 2023-02-09 +## [2.1.0](https://github.com/jeertmans/languagetool-rust/compare/v2.0.0...v2.1.0) - 2023-02-09 ### Added -- Added environment variables for login arguments. [#64](https://github.com/jeertmans/languagetool-rust/pull/64) +- Added environment variables for login arguments. + [#64](https://github.com/jeertmans/languagetool-rust/pull/64) ### Fixed -- Fixed words requests. [#64](https://github.com/jeertmans/languagetool-rust/pull/64) +- Fixed words requests. + [#64](https://github.com/jeertmans/languagetool-rust/pull/64) -## [2.0.0](https://github.com/jeertmans/languagetool-rust/compare/v1.3.0...v2.0.0) 2023-02-07 +## [2.0.0](https://github.com/jeertmans/languagetool-rust/compare/v1.3.0...v2.0.0) - 2023-02-07 ### Chore -- Created founding link. [#19](https://github.com/jeertmans/languagetool-rust/pull/19) -- Added two related projects. [#21](https://github.com/jeertmans/languagetool-rust/pull/21) -- Added release dates. [#31](https://github.com/jeertmans/languagetool-rust/pull/31) -- Added `#[must_use]` flag to most structures, to please clippy pedantic. [#29](https://github.com/jeertmans/languagetool-rust/pull/29) -- Changed conditional compilation flags to directly point to dependency, e.g., `"clap"` instead of `"cli"`. [#28](https://github.com/jeertmans/languagetool-rust/pull/28) -- Use `cargo-nextest` instead of `cargo test` for faster CI testing. [#32](https://github.com/jeertmans/languagetool-rust/pull/32) -- Improve CI testing. [#41](https://github.com/jeertmans/languagetool-rust/pull/41) -- Added issue templates. [#42](https://github.com/jeertmans/languagetool-rust/pull/42) -- Added dependabot config. [#43](https://github.com/jeertmans/languagetool-rust/pull/43) -- Added PR template and codecov badge. [#44](https://github.com/jeertmans/languagetool-rust/pull/44) -- Added missing `#[must_use]`. [#50](https://github.com/jeertmans/languagetool-rust/pull/50) -- Upgraded formatting options, using nightly, and improved documentation. [#55](https://github.com/jeertmans/languagetool-rust/pull/55) -- Change example image to be SVG. [#57](https://github.com/jeertmans/languagetool-rust/pull/57) -- Added more tests. [#22](https://github.com/jeertmans/languagetool-rust/pull/22) +- Created founding link. + [#19](https://github.com/jeertmans/languagetool-rust/pull/19) +- Added two related projects. + [#21](https://github.com/jeertmans/languagetool-rust/pull/21) +- Added release dates. + [#31](https://github.com/jeertmans/languagetool-rust/pull/31) +- Added `#[must_use]` flag to most structures, to please clippy pedantic. + [#29](https://github.com/jeertmans/languagetool-rust/pull/29) +- Changed conditional compilation flags to directly point to dependency, e.g., `"clap"` instead of `"cli"`. + [#28](https://github.com/jeertmans/languagetool-rust/pull/28) +- Use `cargo-nextest` instead of `cargo test` for faster CI testing. + [#32](https://github.com/jeertmans/languagetool-rust/pull/32) +- Improve CI testing. + [#41](https://github.com/jeertmans/languagetool-rust/pull/41) +- Added issue templates. + [#42](https://github.com/jeertmans/languagetool-rust/pull/42) +- Added dependabot config. + [#43](https://github.com/jeertmans/languagetool-rust/pull/43) +- Added PR template and codecov badge. + [#44](https://github.com/jeertmans/languagetool-rust/pull/44) +- Added missing `#[must_use]`. + [#50](https://github.com/jeertmans/languagetool-rust/pull/50) +- Upgraded formatting options, using nightly, and improved documentation. + [#55](https://github.com/jeertmans/languagetool-rust/pull/55) +- Change example image to be SVG. + [#57](https://github.com/jeertmans/languagetool-rust/pull/57) +- Added more tests. + [#22](https://github.com/jeertmans/languagetool-rust/pull/22) ### Added -- Added `cli-complete` feature to generate completion files. [#23](https://github.com/jeertmans/languagetool-rust/pull/23) -- Added message when reading from STDIN. [#25](https://github.com/jeertmans/languagetool-rust/pull/25), [#26](https://github.com/jeertmans/languagetool-rust/pull/26) -- Added (regex) validator for language code. [#27](https://github.com/jeertmans/languagetool-rust/pull/27) -- Added cli requirements for `username`/`api_key` pair. [#16](https://github.com/jeertmans/languagetool-rust/pull/16), [#30](https://github.com/jeertmans/languagetool-rust/pull/30) -- Added a `CommandNotFound` error variant for when docker is not found. [#52](https://github.com/jeertmans/languagetool-rust/pull/52) -- Added a `split_len` function. [#18](https://github.com/jeertmans/languagetool-rust/pull/18) -- Automatically split long text into multiple fragments. [#58](https://github.com/jeertmans/languagetool-rust/pull/58), [#60](https://github.com/jeertmans/languagetool-rust/pull/60) -- Add `try_` variants for panicking functions. [#59](https://github.com/jeertmans/languagetool-rust/pull/59) +- Added `cli-complete` feature to generate completion files. + [#23](https://github.com/jeertmans/languagetool-rust/pull/23) +- Added message when reading from STDIN. + [#25](https://github.com/jeertmans/languagetool-rust/pull/25), [#26](https://github.com/jeertmans/languagetool-rust/pull/26) +- Added (regex) validator for language code. + [#27](https://github.com/jeertmans/languagetool-rust/pull/27) +- Added cli requirements for `username`/`api_key` pair. + [#16](https://github.com/jeertmans/languagetool-rust/pull/16), [#30](https://github.com/jeertmans/languagetool-rust/pull/30) +- Added a `CommandNotFound` error variant for when docker is not found. + [#52](https://github.com/jeertmans/languagetool-rust/pull/52) +- Added a `split_len` function. + [#18](https://github.com/jeertmans/languagetool-rust/pull/18) +- Automatically split long text into multiple fragments. + [#58](https://github.com/jeertmans/languagetool-rust/pull/58), [#60](https://github.com/jeertmans/languagetool-rust/pull/60) +- Add `try_` variants for panicking functions. + [#59](https://github.com/jeertmans/languagetool-rust/pull/59) ### Changed -- Cancelled effects of [#28](https://github.com/jeertmans/languagetool-rust/pull/28). [#45](https://github.com/jeertmans/languagetool-rust/pull/45) -- Removed `regex` and `lazy_static` dependencies. [#51](https://github.com/jeertmans/languagetool-rust/pull/51) -- **Breaking** Refactored the CLI by upgrading to Clap v4, added input from filenames and changed the library accordingly. [#53](https://github.com/jeertmans/languagetool-rust/pull/53) +- Cancelled effects of [#28](https://github.com/jeertmans/languagetool-rust/pull/28). + [#45](https://github.com/jeertmans/languagetool-rust/pull/45) +- Removed `regex` and `lazy_static` dependencies. + [#51](https://github.com/jeertmans/languagetool-rust/pull/51) +- **Breaking** Refactored the CLI by upgrading to Clap v4, added input from filenames and changed the library accordingly. + [#53](https://github.com/jeertmans/languagetool-rust/pull/53) ### Fixed -- Stopped serializing useless fields. [#17](https://github.com/jeertmans/languagetool-rust/pull/17) +- Stopped serializing useless fields. + [#17](https://github.com/jeertmans/languagetool-rust/pull/17) ## [1.3.0](https://github.com/jeertmans/languagetool-rust/compare/v1.2.0...v1.3.0) - 2022-08-25 ### Chore -- Fixed features flag in `CI.yml` action. [#12](https://github.com/jeertmans/languagetool-rust/pull/12) +- Fixed features flag in `CI.yml` action. + [#12](https://github.com/jeertmans/languagetool-rust/pull/12) ### Added -- Added basic Docker support. [#12](https://github.com/jeertmans/languagetool-rust/pull/12) +- Added basic Docker support. + [#12](https://github.com/jeertmans/languagetool-rust/pull/12) ### Fixed -- Fixed typo in message when no error was found. [#12](https://github.com/jeertmans/languagetool-rust/pull/12) +- Fixed typo in message when no error was found. + [#12](https://github.com/jeertmans/languagetool-rust/pull/12) ## [1.2.0](https://github.com/jeertmans/languagetool-rust/compare/v1.1.1...v1.2.0) - 2022-08-10 ### Chore -- Use vendored TLS for release. [#14](https://github.com/jeertmans/languagetool-rust/pull/14) -- Fixed PR links in CHANGELOG. [#15](https://github.com/jeertmans/languagetool-rust/pull/15) +- Use vendored TLS for release. + [#14](https://github.com/jeertmans/languagetool-rust/pull/14) +- Fixed PR links in CHANGELOG. + [#15](https://github.com/jeertmans/languagetool-rust/pull/15) ### Added -- Add 3 new features `native-tls`, `native-tls-vendored` and `full`. [#14](https://github.com/jeertmans/languagetool-rust/pull/14) +- Add 3 new features `native-tls`, `native-tls-vendored` and `full`. + [#14](https://github.com/jeertmans/languagetool-rust/pull/14) ## [1.1.1](https://github.com/jeertmans/languagetool-rust/compare/v1.0.1...v1.1.1) - 2022-08-09 ### Chore -- Add GitHub action to automate release process. [#12](https://github.com/jeertmans/languagetool-rust/pull/12) +- Add GitHub action to automate release process. + [#12](https://github.com/jeertmans/languagetool-rust/pull/12) ## [1.1.0](https://github.com/jeertmans/languagetool-rust/compare/v1.0.0...v1.1.0) - 2022-08-08 ### Chore - Add some LanguageTool context. -- Correct some typos in the various docs ([@kianmeng](https://github.com/kianmeng)). [#4](https://github.com/jeertmans/languagetool-rust/pull/4) -- Add lint test for Markdown files ([@kianmeng](https://github.com/kianmeng)). [#5](https://github.com/jeertmans/languagetool-rust/pull/5) -- Add grammar check for text-like files. [#6](https://github.com/jeertmans/languagetool-rust/pull/6) -- Add compare links to release tags in CHANGELOG. [#9](https://github.com/jeertmans/languagetool-rust/pull/9) -- Add action to check if can publish. [#11](https://github.com/jeertmans/languagetool-rust/pull/11) +- Correct some typos in the various docs ([@kianmeng](https://github.com/kianmeng)). + [#4](https://github.com/jeertmans/languagetool-rust/pull/4) +- Add lint test for Markdown files ([@kianmeng](https://github.com/kianmeng)). + [#5](https://github.com/jeertmans/languagetool-rust/pull/5) +- Add grammar check for text-like files. + [#6](https://github.com/jeertmans/languagetool-rust/pull/6) +- Add compare links to release tags in CHANGELOG. + [#9](https://github.com/jeertmans/languagetool-rust/pull/9) +- Add action to check if crate is publishable. + [#11](https://github.com/jeertmans/languagetool-rust/pull/11) ### Added -- Add `get_text` method for `CheckRequest`. [#8](https://github.com/jeertmans/languagetool-rust/pull/8) -- Create `CheckResponseWithContext` that enables keeping information about checked text. Hence, mutable and immutable iterators have been created to provide more tools to the end-user. [#10](https://github.com/jeertmans/languagetool-rust/pull/10) -- Add `--more-context` option flag to CLI. This enables to add more information in the JSON output. [#10](https://github.com/jeertmans/languagetool-rust/pull/10) -- Derive `Eq` for all structs that derive `PartialEq` and with fields that derive `Eq`. [#10](https://github.com/jeertmans/languagetool-rust/pull/10) -- Add `from_env` and `from_env_or_default` methods for `ServerCli` and `ServerClient`. [#10](https://github.com/jeertmans/languagetool-rust/pull/10) +- Add `get_text` method for `CheckRequest`. + [#8](https://github.com/jeertmans/languagetool-rust/pull/8) +- Create `CheckResponseWithContext` that enables keeping information about checked text. + Hence, mutable and immutable iterators have been created to provide more tools to the end-user. + [#10](https://github.com/jeertmans/languagetool-rust/pull/10) +- Add `--more-context` option flag to CLI. This enables to add more information in the JSON output. + [#10](https://github.com/jeertmans/languagetool-rust/pull/10) +- Derive `Eq` for all structs that derive `PartialEq` and with fields that derive `Eq`. + [#10](https://github.com/jeertmans/languagetool-rust/pull/10) +- Add `from_env` and `from_env_or_default` methods for `ServerCli` and `ServerClient`. + [#10](https://github.com/jeertmans/languagetool-rust/pull/10) ### Fixed -- Fixed line number in annotated responses. [#8](https://github.com/jeertmans/languagetool-rust/pull/8) -- Fixed missing bench path in `Cargo.toml`. [#11](https://github.com/jeertmans/languagetool-rust/pull/11) +- Fixed line number in annotated responses. + [#8](https://github.com/jeertmans/languagetool-rust/pull/8) +- Fixed missing bench path in `Cargo.toml`. + [#11](https://github.com/jeertmans/languagetool-rust/pull/11) ## [1.0.0](https://github.com/jeertmans/languagetool-rust/compare/v0.0.18...v1.0.0) - 2022-07-24 @@ -184,7 +247,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix compilation error in `src/lib/error.rs` when `feature = "cli"` was not set. - Remove unused print in `src/lib/server.rs`. -> **_NOTE:_** Pre v1.0.0, the changelog was generated using the `cliff` tool that is based on commits. +> ***NOTE:*** Pre v1.0.0, the changelog was generated using the `cliff` tool that is based on commits. ## [0.0.18](https://github.com/jeertmans/languagetool-rust/compare/v0.0.17...v0.0.18) - 2022-06-22 diff --git a/Cargo.lock b/Cargo.lock index 29fe19a..bacbb34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] @@ -44,9 +44,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.15" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -59,36 +59,37 @@ dependencies = [ [[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.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.4" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "once_cell", + "windows-sys 0.59.0", ] [[package]] @@ -107,6 +108,12 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + [[package]] name = "autocfg" version = "1.4.0" @@ -142,15 +149,15 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" [[package]] name = "bstr" -version = "1.10.0" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" +checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" dependencies = [ "memchr", "regex-automata", @@ -159,15 +166,21 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.2" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" +checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" [[package]] name = "cast" @@ -177,9 +190,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.1.22" +version = "1.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9540e661f81799159abee814118cc139a2004b3a3aa3ea37724a1b66530b90e0" +checksum = "e4730490333d58093109dc02c23174c3f4d490998c3fed3cc8e82d57afedb9cf" dependencies = [ "shlex", ] @@ -219,19 +232,29 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.18" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3" +checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796" dependencies = [ "clap_builder", "clap_derive", ] +[[package]] +name = "clap-verbosity-flag" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2678fade3b77aa3a8ff3aae87e9c008d3fb00473a41c71fbf74e91c8c7b37e84" +dependencies = [ + "clap", + "log", +] + [[package]] name = "clap_builder" -version = "4.5.18" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b" +checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" dependencies = [ "anstream", "anstyle", @@ -242,30 +265,30 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.29" +version = "4.5.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8937760c3f4c60871870b8c3ee5f9b30771f792a7045c48bcbba999d7d6b3b8e" +checksum = "375f9d8255adeeedd51053574fd8d4ba875ea5fa558e86617b07f09f1680c8b6" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.5.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.98", ] [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "codspeed" @@ -291,18 +314,30 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "colored" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.48.0", + "windows-sys 0.59.0", +] + +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "windows-sys 0.59.0", ] [[package]] @@ -359,9 +394,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -378,15 +413,49 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.2" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + +[[package]] +name = "cssparser" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c66d1cd8ed61bf80b38432613a7a2f09401ab8d0501110655f8b341484a3e3" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.98", +] + +[[package]] +name = "derive_more" +version = "0.99.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da29a38df43d6f156149c9b43ded5e018ddff2a855cf2cfd62e8cd7d079c69f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] [[package]] name = "difflib" @@ -394,27 +463,99 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "doc-comment" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "dtoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "ecow" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42fc0a93992b20c58b99e59d61eaf1635a25bfbe49e4275c34ba0aee98119ba" +dependencies = [ + "serde", +] + +[[package]] +name = "ego-tree" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2972feb8dffe7bc8c5463b1dacda1b0dfbed3710e50f977d965429692d74cd8" + [[package]] name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[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", ] +[[package]] +name = "enum_dispatch" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -423,25 +564,25 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "fastrand" -version = "2.1.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "float-cmp" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" dependencies = [ "num-traits", ] @@ -476,11 +617,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -493,9 +644,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -503,15 +654,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -520,38 +671,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.98", ] [[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" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +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", @@ -565,11 +716,41 @@ dependencies = [ "slab", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets 0.52.6", +] + [[package]] name = "gimli" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "h2" @@ -602,9 +783,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.5" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "heck" @@ -614,15 +795,21 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.3.9" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" [[package]] -name = "hermit-abi" -version = "0.4.0" +name = "html5ever" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +dependencies = [ + "log", + "mac", + "markup5ever", + "match_token", +] [[package]] name = "http" @@ -648,9 +835,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.4" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" [[package]] name = "httpdate" @@ -658,11 +845,17 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" -version = "0.14.30" +version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ "bytes", "futures-channel", @@ -695,41 +888,184 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "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 = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[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.5.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", "hashbrown", ] +[[package]] +name = "insta" +version = "1.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50259abbaa67d11d2bcafc7ba1d094ed7a0c70e3ce893f0d0997f73558cb3084" +dependencies = [ + "console", + "linked-hash-map", + "once_cell", + "pin-project", + "regex", + "similar", +] + [[package]] name = "ipnet" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "is-terminal" -version = "0.4.13" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" +checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" dependencies = [ - "hermit-abi 0.4.0", + "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -749,16 +1085,17 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "js-sys" -version = "0.3.70" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -768,20 +1105,32 @@ version = "2.1.4" dependencies = [ "annotate-snippets", "assert_cmd", + "assert_matches", "clap", + "clap-verbosity-flag", "clap_complete", "codspeed-criterion-compat", "criterion", + "ego-tree", + "enum_dispatch", "futures", + "insta", "is-terminal", + "lazy_static", + "lifetime", + "log", "predicates", + "pretty_env_logger", + "pulldown-cmark", "reqwest", + "scraper", "serde", "serde_json", "tempfile", "termcolor", "thiserror", "tokio", + "typst-syntax", ] [[package]] @@ -792,21 +1141,104 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.159" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "lifetime" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9ac39b3413a5c091fc208f8eb84d9bdb35b6ef8c84570bc2f6fa47a3ba948e" +dependencies = [ + "lifetime_proc_macros", +] + +[[package]] +name = "lifetime_proc_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3008bffdf1aaae933e04c9019963a422d495bd0e3913db40eb7078883df30a" +dependencies = [ + "lifetime_proc_macros_impl", + "syn 1.0.109", +] + +[[package]] +name = "lifetime_proc_macros_impl" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9b75338d55f6248964c017967525a03d48d3f3092701c2ec3c027f3c60514b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] [[package]] name = "log" -version = "0.4.22" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" + +[[package]] +name = "mac" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +dependencies = [ + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] [[package]] name = "memchr" @@ -822,30 +1254,29 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ - "hermit-abi 0.3.9", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] [[package]] name = "native-tls" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c" dependencies = [ "libc", "log", @@ -858,6 +1289,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 = "normalize-line-endings" version = "0.3.0" @@ -875,18 +1312,18 @@ dependencies = [ [[package]] name = "object" -version = "0.36.4" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 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 = "oorandom" @@ -896,11 +1333,11 @@ checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" [[package]] name = "openssl" -version = "0.10.66" +version = "0.10.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "cfg-if", "foreign-types", "libc", @@ -917,29 +1354,29 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.98", ] [[package]] name = "openssl-probe" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-src" -version = "300.3.2+3.3.2" +version = "300.4.1+3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a211a18d945ef7e648cc6e0058f4c548ee46aab922ea203e0d30e966ea23647b" +checksum = "faa4eac4138c62414b5622d1b31c5c304f34b406b013c079c2bbc652fdd6678c" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.103" +version = "0.9.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc" dependencies = [ "cc", "libc", @@ -948,17 +1385,112 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -1000,11 +1532,23 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "portable-atomic" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "predicates" -version = "3.1.2" +version = "3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" dependencies = [ "anstyle", "difflib", @@ -1015,38 +1559,82 @@ dependencies = [ ] [[package]] -name = "predicates-core" -version = "1.0.8" +name = "predicates-core" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" + +[[package]] +name = "predicates-tree" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "pretty_env_logger" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c" +dependencies = [ + "env_logger", + "log", +] + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulldown-cmark" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76979bea66e7875e7509c4ec5300112b316af87fa7a252ca91c448b32dfe3993" +dependencies = [ + "bitflags 2.8.0", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" +checksum = "bd348ff538bc9caeda7ee8cad2d1d48236a1f443c1fa3913c6a02fe0043b1dd3" [[package]] -name = "predicates-tree" -version = "1.0.11" +name = "quote" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ - "predicates-core", - "termtree", + "proc-macro2", ] [[package]] -name = "proc-macro2" -version = "1.0.86" +name = "rand" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ - "unicode-ident", + "rand_core", ] [[package]] -name = "quote" -version = "1.0.37" +name = "rand_core" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" -dependencies = [ - "proc-macro2", -] +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" [[package]] name = "rayon" @@ -1068,11 +1656,20 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redox_syscall" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +dependencies = [ + "bitflags 2.8.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", "memchr", @@ -1082,9 +1679,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -1093,9 +1690,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" @@ -1145,15 +1742,15 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" -version = "0.38.37" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1165,11 +1762,17 @@ dependencies = [ "base64", ] +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" [[package]] name = "same-file" @@ -1182,20 +1785,41 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.24" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scraper" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527e65d9d888567588db4c12da1087598d0f6f8b346cc2c5abc91f05fc2dffe2" +dependencies = [ + "cssparser", + "ego-tree", + "getopts", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + [[package]] name = "security-framework" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "core-foundation", "core-foundation-sys", "libc", @@ -1204,39 +1828,58 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.12.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", ] +[[package]] +name = "selectors" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd568a4c9bb598e291a08244a5c1f5a8a6650bee243b5b0f8dbb3d9cc1d87fe8" +dependencies = [ + "bitflags 2.8.0", + "cssparser", + "derive_more", + "fxhash", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "servo_arc", + "smallvec", +] + [[package]] name = "serde" -version = "1.0.210" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.98", ] [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" dependencies = [ "itoa", "memchr", @@ -1244,6 +1887,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1256,12 +1908,33 @@ dependencies = [ "serde", ] +[[package]] +name = "servo_arc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae65c4249478a2647db249fb43e23cec56a2c8974a427e7bd8cb5a1d0964921a" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.9" @@ -1271,16 +1944,53 @@ dependencies = [ "autocfg", ] +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "string_cache" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938d512196766101d333398efde81bc1f37b00cb42c2f8350e5df639f040bbbe" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + [[package]] name = "strsim" version = "0.11.1" @@ -1289,9 +1999,20 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.79" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" dependencies = [ "proc-macro2", "quote", @@ -1304,6 +2025,17 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[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.98", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -1327,17 +2059,29 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.13.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" dependencies = [ "cfg-if", "fastrand", + "getrandom", "once_cell", "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 = "termcolor" version = "1.4.1" @@ -1349,70 +2093,71 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.3.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" +checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" dependencies = [ "rustix", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] name = "termtree" -version = "0.4.1" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + +[[package]] +name = "thin-vec" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" +checksum = "a38c90d48152c236a3ab59271da4f4ae63d678c5d7ad6b7714d7cb9760be5e4b" [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.98", ] [[package]] -name = "tinytemplate" -version = "1.2.1" +name = "tinystr" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ - "serde", - "serde_json", + "displaydoc", + "zerovec", ] [[package]] -name = "tinyvec" -version = "1.8.0" +name = "tinytemplate" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" dependencies = [ - "tinyvec_macros", + "serde", + "serde_json", ] -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tokio" -version = "1.40.0" +version = "1.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" dependencies = [ "backtrace", "bytes", @@ -1426,13 +2171,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.98", ] [[package]] @@ -1447,9 +2192,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", @@ -1458,6 +2203,40 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower-service" version = "0.3.3" @@ -1466,9 +2245,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-core", @@ -1476,9 +2255,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", ] @@ -1490,25 +2269,65 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] -name = "unicode-bidi" -version = "0.3.15" +name = "typst-syntax" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b7be8b6ed6b2cb39ca495947d548a28d7db0ba244008e44c5a759120327693" +dependencies = [ + "ecow", + "once_cell", + "serde", + "toml", + "typst-utils", + "unicode-ident", + "unicode-math-class", + "unicode-script", + "unicode-segmentation", + "unscanny", +] + +[[package]] +name = "typst-utils" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f0305443ed97f0b658471487228f86bf835705e7525fbdcc671cebd864f7a40" +dependencies = [ + "once_cell", + "portable-atomic", + "rayon", + "siphasher", + "thin-vec", +] + +[[package]] +name = "unicase" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" [[package]] -name = "unicode-normalization" -version = "0.1.24" +name = "unicode-math-class" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" -dependencies = [ - "tinyvec", -] +checksum = "7d246cf599d5fae3c8d56e04b20eb519adb89a8af8d0b0fbcded369aa3647d65" + +[[package]] +name = "unicode-script" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" @@ -1516,17 +2335,41 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unscanny" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9df2af067a7953e9c3831320f35c1cc0600c30d44d9f7a12b01db1cd88d6b47" + [[package]] name = "url" -version = "2.5.2" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", "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_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -1541,9 +2384,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "wait-timeout" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" dependencies = [ "libc", ] @@ -1573,49 +2416,59 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.98", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.43" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ "cfg-if", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1623,28 +2476,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.98", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "web-sys" -version = "0.3.70" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", @@ -1829,6 +2685,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e49d2d35d3fad69b39b94139037ecfb4f359f08958b9c11e7315ce770462419" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" @@ -1839,6 +2704,27 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.8.0", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "yansi-term" version = "0.1.2" @@ -1847,3 +2733,70 @@ checksum = "fe5c30ade05e61656247b2e334a031dfd0cc466fadef865bdcdea8d537951bf1" dependencies = [ "winapi", ] + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", + "synstructure", +] + +[[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.98", +] diff --git a/Cargo.toml b/Cargo.toml index 87a322b..dba346f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,46 +5,86 @@ path = "benches/bench_main.rs" [[bin]] name = "ltrs" -path = "src/bin.rs" +path = "src/main.rs" required-features = ["cli"] [dependencies] annotate-snippets = {version = "^0.9.1", optional = true} -clap = {version = "^4.0", features = ["cargo", "derive", "env", "wrap_help"], optional = true} -clap_complete = {version = "^4.0", optional = true} +clap = {version = "^4.5.18", features = [ + "cargo", + "derive", + "env", + "wrap_help", +], optional = true} +clap-verbosity-flag = {version = "3.0.2", optional = true} +clap_complete = {version = "^4.5.2", optional = true} +ego-tree = {version = "0.10.0", optional = true} +enum_dispatch = {version = "0.3.13", optional = true} is-terminal = {version = "0.4.3", optional = true} +lifetime = {version = "0.1.0", features = ["macros"]} +log = {version = "0.4.25", optional = true} +pretty_env_logger = {version = "0.5.0", optional = true} +pulldown-cmark = {version = "0.10.2", optional = true} reqwest = {version = "^0.11", default-features = false, features = ["json"]} +scraper = {version = "0.23.1", optional = true} serde = {version = "^1.0", features = ["derive"]} serde_json = "^1.0" termcolor = {version = "1.2.0", optional = true} thiserror = "^1.0" -tokio = {version = "^1.0", features = ["macros", "rt-multi-thread"], optional = true} +tokio = {version = "^1.0", features = [ + "macros", + "rt-multi-thread", +], optional = true} +typst-syntax = {version = "^0.12.0", optional = true} [dev-dependencies] assert_cmd = "2.0.11" +assert_matches = "1.5.0" codspeed-criterion-compat = "2.7.0" criterion = "0.5" futures = "0.3" +insta = {version = "1.41.1", features = ["filters"]} +lazy_static = "1.5.0" predicates = "3.0.3" tempfile = "3.5.0" tokio = {version = "^1.0", features = ["macros"]} [features] annotate = ["dep:annotate-snippets"] -cli = ["annotate", "color", "dep:clap", "dep:is-terminal", "multithreaded"] +clap-verbosity-flag = ["dep:clap-verbosity-flag"] +cli = [ + "annotate", + "clap-verbosity-flag", + "color", + "dep:clap", + "dep:enum_dispatch", + "dep:is-terminal", + "html", + "log", + "markdown", + "multithreaded", + "pretty_env_logger", + "typst", +] cli-complete = ["cli", "clap_complete"] color = ["annotate-snippets?/color", "dep:termcolor"] default = ["cli", "native-tls"] docker = [] full = ["cli-complete", "docker", "unstable"] +html = ["dep:ego-tree", "dep:scraper"] +log = ["dep:log"] +markdown = ["dep:pulldown-cmark", "html"] multithreaded = ["dep:tokio"] native-tls = ["reqwest/native-tls"] native-tls-vendored = ["reqwest/native-tls-vendored"] +pretty_env_logger = ["dep:pretty_env_logger"] +snapshots = [] # Only for testing +typst = ["dep:typst-syntax"] unstable = [] [lib] name = "languagetool_rust" -path = "src/lib/lib.rs" +path = "src/lib.rs" [package] authors = ["Jérome Eertmans "] @@ -56,21 +96,25 @@ license = "MIT" name = "languagetool-rust" readme = "README.md" repository = "https://github.com/jeertmans/languagetool-rust" -rust-version = "1.74.0" +rust-version = "1.77.0" version = "2.1.4" +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + [package.metadata.release] pre-release-replacements = [ - {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, - {file="CHANGELOG.md", search="\\.\\.\\.HEAD\\)", replace="...{{version}}) {{date}}", exactly=1}, - {file="CHANGELOG.md", search="", replace="\n\n## [Unreleased](https://github.com/jeertmans/languagetool-rust/compare/v{{version}}...HEAD)", exactly=1}, + {file = "CHANGELOG.md", search = "Unreleased", replace = "{{version}}", min = 1}, + {file = "CHANGELOG.md", search = "\\.\\.\\.HEAD\\)", replace = "...{{version}}) {{date}}", exactly = 1}, + {file = "CHANGELOG.md", search = "", replace = "\n\n## [Unreleased](https://github.com/jeertmans/languagetool-rust/compare/v{{version}}...HEAD)", exactly = 1}, ] -tag = false publish = false +tag = false -[package.metadata.docs.rs] -all-features = true -rustdoc-args = ["--cfg", "docsrs"] +[profile.dev.package] +insta.opt-level = 3 +similar.opt-level = 3 [[test]] name = "cli" @@ -80,5 +124,3 @@ required-features = ["cli"] [[test]] name = "match-positions" path = "tests/match_positions.rs" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/README.md b/README.md index 1e7b90c..b6d94aa 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,12 @@ > **Rust bindings to connect with LanguageTool server API.** -*LanguageTool is an open source grammar style checker. It can correct 30+ languages and is free to use, more on that on [languagetool.org](https://languagetool.org/). There is a public API (with a free tier), but you can also host your own server locally. LanguageTool-Rust helps you communicate with those servers very easily via Rust code!* +*LanguageTool is an open source grammar style checker. +It can correct 30+ languages and is free to use, more on that on +[languagetool.org](https://languagetool.org/). +There is a public API (with a free tier), +but you can also host your own server locally. +LanguageTool-Rust helps you communicate with those servers very easily via Rust code!* [![Crates.io](https://img.shields.io/crates/v/languagetool-rust)](https://crates.io/crates/languagetool-rust) [![docs.rs](https://img.shields.io/docsrs/languagetool-rust)](https://docs.rs/languagetool-rust) @@ -19,9 +24,12 @@ ## About -LanguageTool-Rust (LTRS) is both an executable and a Rust library that aims to provide correct and safe bindings for the LanguageTool API. +LanguageTool-Rust (LTRS) is both an **executable and a Rust library** +that strives to provide correct and safe bindings for the LanguageTool API. -*Disclaimer: the current work relies on an approximation of the LanguageTool API. We try to avoid breaking changes as much as possible, but we still highly depend on the future evolutions of LanguageTool.* +*Disclaimer: the current work relies on an approximation of the LanguageTool API. +We try to avoid breaking changes as much as possible, +but we still highly depend on the future evolutions of LanguageTool.* ## Installation @@ -33,7 +41,8 @@ cargo install languagetool-rust --features full ### AUR -If you are on Arch Linux, you call also install with your [AUR helper](https://wiki.archlinux.org/title/AUR_helpers): +If you are on Arch Linux, you call also install with your +[AUR helper](https://wiki.archlinux.org/title/AUR_helpers): ```bash paru -S languagetool-rust @@ -43,7 +52,8 @@ paru -S languagetool-rust ![Screenshot from CLI](https://raw.githubusercontent.com/jeertmans/languagetool-rust/main/img/screenshot.svg) -The command line interface of LTRS allows to very quickly use any LanguageTool server to check for grammar and style errors. +The command line interface of LTRS allows to very quickly use any LanguageTool server +to check for grammar and style errors. The reference for the CLI can be accessed via `ltrs --help`. @@ -68,7 +78,7 @@ PONG! Delay: 110 ms }, # ... ] -> ltrs check --text "Some phrase with a smal mistake" +> ltrs check --text "Some phrase with a smal mistake" # codespell:ignore smal { "language": { "code": "en-US", @@ -85,7 +95,7 @@ PONG! Delay: 110 ms "context": { "length": 4, "offset": 19, - "text": "Some phrase with a smal mistake" + "text": "Some phrase with a smal mistake" # codespell:ignore smal }, "contextForSureMatch": 0, "ignoreForIncompleteSentence": false, @@ -110,7 +120,9 @@ PONG! Delay: 110 ms ### Docker -Since LanguageTool's installation might not be straightforward, we provide a basic Docker integration that allows to `pull`, `start`, and `stop` LanguageTool Docker containers in a few lines: +Since LanguageTool's installation might not be straightforward, +we provide a basic Docker integration that allows to `pull`, `start`, and `stop` +LanguageTool Docker containers in a few lines: ```bash ltrs docker pull # only once @@ -120,11 +132,16 @@ ltrs --hostname http://localhost -p 8010 check -t "Some tex" ltrs docker stop # stop the LT server ``` -> *Note:* Docker is a tool that facilitates running applications without worrying about dependencies, platform-related issues, and so on. Installation guidelines can be found [here](https://www.docker.com/get-started/). On Linux platform, you might need to circumvent the *sudo privilege issue* by doing [this](https://docs.docker.com/engine/install/linux-postinstall/). +> *Note:* Docker is a tool that facilitates running applications without worrying + about local dependencies, platform-related issues, and so on. + Installation guidelines can be found [here](https://www.docker.com/get-started/). + On Linux platforms, you might need to circumvent the *sudo privilege issue* by doing + [this](https://docs.docker.com/engine/install/linux-postinstall/). ## API Reference -If you would like to integrate LTRS within a Rust application or crate, then we recommend reading the [documentation](https://docs.rs/languagetool-rust). +If you would like to integrate LTRS within a Rust application or crate, +then we recommend reading the [documentation](https://docs.rs/languagetool-rust). To use LanguageTool-Rust in your Rust project, add to your `Cargo.toml`: @@ -136,14 +153,15 @@ languagetool-rust = "^2.1" ### Example ```rust -use languagetool_rust::{check::CheckRequest, server::ServerClient}; +use languagetool_rust::api::{check, server::ServerClient}; +use std::borrow::Cow; #[tokio::main] async fn main() -> Result<(), Box> { let client = ServerClient::from_env_or_default(); - let req = CheckRequest::default() - .with_text("Some phrase with a smal mistake".to_string()); + let req = check::Request::default() + .with_text("Some phrase with a smal mistake"); // # codespell:ignore smal println!( "{}", @@ -155,30 +173,45 @@ async fn main() -> Result<(), Box> { ### Feature Flags +Below are listed the various feature flags you can enable when compiling LTRS. + #### Default Features -- **cli**: Adds command-line related methods for multiple structures. This feature is required to install the LTRS CLI, and enables the following features: **annotate**, **color**, **multithreaded**. +- **cli**: Adds command-line related methods for multiple structures. + This feature is required to install the LTRS CLI, + and enables the following features: **annotate**, **color**, **multithreaded**. - **native-tls**: Enables TLS functionality provided by `native-tls`. #### Optional Features - **annotate**: Adds method(s) to annotate results from check request. -- **cli-complete**: Adds commands to generate completion files for various shells. This feature also activates the **cli** feature. Enter `ltrs completions --help` to get help with installing completion files. +- **cli-complete**: Adds commands to generate completion files for various shells. + This feature also activates the **cli** feature. Enter `ltrs completions --help` to get help with installing completion files. - **color**: Enables color outputting in the terminal. If **cli** feature is also enabled, the `--color=` option will be available. -- **full**: Enables all features that are mutually compatible (i.e., `cli-complete`, `docker`, and `unstable`). +- **full**: Enables all features that are mutually compatible + (i.e., `cli-complete`, `docker`, and `undoc`). - **multithreaded**: Enables multithreaded requests. - **native-tls-vendored**: Enables the `vendored` feature of `native-tls`. This or `native-tls` should be activated if you are planning to use HTTPS servers. -- **unstable**: Adds more fields to JSON responses that are not present in the [Model | Example Value](https://languagetool.org/http-api/swagger-ui/#!/default/) but might be present in some cases. All added fields are optional, hence the `Option` around them. +- **undoc**: Adds more fields to JSON responses that are not present + in the [Model | Example Value](https://languagetool.org/http-api/swagger-ui/#!/default/) + but might be present in some cases. All added fields are stored in a hashmap as + JSON values. ## Related Projects Here are listed some projects that use LTRS. -- [`null-ls`](https://github.com/jose-elias-alvarez/null-ls.nvim): Neovim plugin with LTRS builtin ([see PR](https://github.com/jose-elias-alvarez/null-ls.nvim/pull/997)) -- [`languagetool-code-comments`](https://github.com/dustinblackman/languagetool-code-comments): uses LTRS to check for grammar errors within code comments +- [`null-ls`](https://github.com/jose-elias-alvarez/null-ls.nvim): + Neovim plugin with LTRS builtin ([see PR](https://github.com/jose-elias-alvarez/null-ls.nvim/pull/997)) +- [`languagetool-code-comments`](https://github.com/dustinblackman/languagetool-code-comments): + uses LTRS to check for grammar errors within code comments *Do you use LTRS in your project? Contact me so I can add it to the list!* ## Contributing -Contributions are more than welcome! Please reach me via GitHub for any questions: [Issues](https://github.com/jeertmans/languagetool-rust/issues), [Pull requests](https://github.com/jeertmans/languagetool-rust/pulls) or [Discussions](https://github.com/jeertmans/languagetool-rust/discussions). +Contributions are more than welcome! +Please reach me via GitHub for any questions: +[Issues](https://github.com/jeertmans/languagetool-rust/issues), +[Pull requests](https://github.com/jeertmans/languagetool-rust/pulls) +or [Discussions](https://github.com/jeertmans/languagetool-rust/discussions). diff --git a/RELEASE-PROCESS.md b/RELEASE-PROCESS.md index 87f21fc..45d3fa9 100644 --- a/RELEASE-PROCESS.md +++ b/RELEASE-PROCESS.md @@ -9,9 +9,11 @@ Note that, by default, every command runs in *dry mode*, and you need to append to actually perform the action. Here are the the following steps to install `cargo-release`: + ```bash cargo install cargo-release ``` + Here are the following steps to release a new version: 1. create a branch `release-x.y.z` from the main branch; diff --git a/benches/benchmarks/check_texts.rs b/benches/benchmarks/check_texts.rs index 362e21c..f0a6f6e 100644 --- a/benches/benchmarks/check_texts.rs +++ b/benches/benchmarks/check_texts.rs @@ -1,9 +1,11 @@ use codspeed_criterion_compat::{criterion_group, Criterion, Throughput}; use futures::future::join_all; use languagetool_rust::{ - check::{CheckRequest, CheckResponse, CheckResponseWithContext}, + api::{ + check::{self, Request, Response}, + server::ServerClient, + }, error::Error, - server::ServerClient, }; static FILES: [(&str, &str); 3] = [ @@ -12,7 +14,7 @@ static FILES: [(&str, &str); 3] = [ ("large", include_str!("../large.txt")), ]; -async fn request_until_success(req: &CheckRequest, client: &ServerClient) -> CheckResponse { +async fn request_until_success<'source>(req: &Request<'source>, client: &ServerClient) -> Response { loop { match client.check(req).await { Ok(resp) => return resp, @@ -27,17 +29,17 @@ async fn request_until_success(req: &CheckRequest, client: &ServerClient) -> Che } #[tokio::main] -async fn check_text_basic(text: &str) -> CheckResponse { +async fn check_text_basic(text: &str) -> Response { let client = ServerClient::from_env().expect( "Please use a local server for benchmarking, and configure the environ variables to use \ it.", ); - let req = CheckRequest::default().with_text(text.to_string()); + let req = Request::default().with_text(text); request_until_success(&req, &client).await } #[tokio::main] -async fn check_text_split(text: &str) -> CheckResponse { +async fn check_text_split(text: &str) -> Response { let client = ServerClient::from_env().expect( "Please use a local server for benchmarking, and configure the environ variables to use \ it.", @@ -46,9 +48,9 @@ async fn check_text_split(text: &str) -> CheckResponse { let resps = join_all(lines.map(|line| { async { - let req = CheckRequest::default().with_text(line.to_string()); + let req = Request::default().with_text(line.to_string()); let resp = request_until_success(&req, &client).await; - CheckResponseWithContext::new(req.get_text(), resp) + check::ResponseWithContext::new(req.get_text(), resp) } })) .await; diff --git a/rustfmt.toml b/rustfmt.toml index 4c2e3c4..10fcdbd 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,4 +1,5 @@ condense_wildcard_suffixes = true +edition = "2021" # error_on_line_overflow = true # error_on_unformatted = true force_multiline_blocks = true @@ -9,5 +10,4 @@ imports_granularity = "Crate" match_block_trailing_comma = true normalize_doc_attributes = true unstable_features = true -version = "Two" wrap_comments = true diff --git a/src/lib/check.rs b/src/api/check.rs similarity index 72% rename from src/lib/check.rs rename to src/api/check.rs index 500af0a..53dd1ce 100644 --- a/src/lib/check.rs +++ b/src/api/check.rs @@ -1,18 +1,20 @@ //! Structures for `check` requests and responses. -use super::error::{Error, Result}; +use std::{borrow::Cow, marker::PhantomData, mem, ops::Deref}; + #[cfg(feature = "annotate")] use annotate_snippets::{ display_list::{DisplayList, FormatOptions}, snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation}, }; #[cfg(feature = "cli")] -use clap::{Args, Parser, ValueEnum}; +use clap::ValueEnum; +use lifetime::IntoStatic; use serde::{Deserialize, Serialize, Serializer}; -#[cfg(feature = "cli")] -use std::path::PathBuf; -/// Requests +use crate::error::{Error, Result}; + +// REQUESTS /// Parse `v` is valid language code. /// @@ -21,7 +23,7 @@ use std::path::PathBuf; /// - a five character string matching pattern `[a-z]{2}-[A-Z]{2} /// - or some more complex ascii string (see below) /// -/// Language code is case insensitive. +/// Language code is case-insensitive. /// /// Therefore, a valid language code must match the following: /// @@ -36,7 +38,7 @@ use std::path::PathBuf; /// # Examples /// /// ``` -/// # use languagetool_rust::check::parse_language_code; +/// # use languagetool_rust::api::check::parse_language_code; /// assert!(parse_language_code("en").is_ok()); /// /// assert!(parse_language_code("en-US").is_ok()); @@ -121,117 +123,208 @@ where } } -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, Hash)] +/// A portion of text to be checked. +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, Hash, IntoStatic)] #[non_exhaustive] #[serde(rename_all = "camelCase")] -/// A portion of text to be checked. -pub struct DataAnnotation { - /// If set, the markup will be interpreted as this. - #[serde(skip_serializing_if = "Option::is_none")] - pub interpret_as: Option, +pub struct DataAnnotation<'source> { + /// Text that should be treated as normal text. + /// + /// This or `markup` is required. #[serde(skip_serializing_if = "Option::is_none")] + pub text: Option>, /// Text that should be treated as markup. - pub markup: Option, + /// + /// This or `text` is required. #[serde(skip_serializing_if = "Option::is_none")] - /// Text that should be treated as normal text. - pub text: Option, -} - -impl Default for DataAnnotation { - fn default() -> Self { - Self { - interpret_as: None, - markup: None, - text: Some(String::new()), - } - } + pub markup: Option>, + /// If set, the markup will be interpreted as this. + #[serde(skip_serializing_if = "Option::is_none")] + pub interpret_as: Option>, } -impl DataAnnotation { +impl<'source> DataAnnotation<'source> { /// Instantiate a new `DataAnnotation` with text only. #[inline] #[must_use] - pub fn new_text(text: String) -> Self { + pub fn new_text>>(text: T) -> Self { Self { - interpret_as: None, + text: Some(text.into()), markup: None, - text: Some(text), + interpret_as: None, } } /// Instantiate a new `DataAnnotation` with markup only. #[inline] #[must_use] - pub fn new_markup(markup: String) -> Self { + pub fn new_markup>>(markup: M) -> Self { Self { - interpret_as: None, - markup: Some(markup), text: None, + markup: Some(markup.into()), + interpret_as: None, } } /// Instantiate a new `DataAnnotation` with markup and its interpretation. #[inline] #[must_use] - pub fn new_interpreted_markup(markup: String, interpret_as: String) -> Self { + pub fn new_interpreted_markup>, I: Into>>( + markup: M, + interpret_as: I, + ) -> Self { Self { - interpret_as: Some(interpret_as), - markup: Some(markup), + interpret_as: Some(interpret_as.into()), + markup: Some(markup.into()), text: None, } } + + /// Return the text or markup within the data annotation. + /// + /// # Errors + /// + /// If this data annotation does not contain text or markup. + pub fn try_get_text(&self) -> Result> { + if let Some(ref text) = self.text { + Ok(text.clone()) + } else if let Some(ref markup) = self.markup { + Ok(markup.clone()) + } else { + Err(Error::InvalidDataAnnotation(format!( + "missing either text or markup field in {self:?}" + ))) + } + } } #[cfg(test)] mod data_annotation_tests { - use crate::check::DataAnnotation; + use super::DataAnnotation; #[test] fn test_text() { - let da = DataAnnotation::new_text("Hello".to_string()); + let da = DataAnnotation::new_text("Hello"); - assert_eq!(da.text.unwrap(), "Hello".to_string()); + assert_eq!(da.text.unwrap(), "Hello"); assert!(da.markup.is_none()); assert!(da.interpret_as.is_none()); } #[test] fn test_markup() { - let da = DataAnnotation::new_markup("Hello".to_string()); + let da = DataAnnotation::new_markup("Hello"); assert!(da.text.is_none()); - assert_eq!(da.markup.unwrap(), "Hello".to_string()); + assert_eq!(da.markup.unwrap(), "Hello"); assert!(da.interpret_as.is_none()); } #[test] fn test_interpreted_markup() { - let da = - DataAnnotation::new_interpreted_markup("Hello".to_string(), "Hello".to_string()); + let da = DataAnnotation::new_interpreted_markup("Hello", "Hello"); assert!(da.text.is_none()); - assert_eq!(da.markup.unwrap(), "Hello".to_string()); - assert_eq!(da.interpret_as.unwrap(), "Hello".to_string()); + assert_eq!(da.markup.unwrap(), "Hello"); + assert_eq!(da.interpret_as.unwrap(), "Hello"); } } /// Alternative text to be checked. #[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Hash)] #[non_exhaustive] -pub struct Data { +pub struct Data<'source> { /// Vector of markup text, see [`DataAnnotation`]. - pub annotation: Vec, + pub annotation: Vec>, } -impl> FromIterator for Data { +impl Data<'_> { + /// Split data into as few fragments as possible, where each fragment + /// contains (if possible) a maximum of `n` characters in it's + /// annotations' markup and text fields. + /// + /// Pattern str `pat` is used for splitting. + #[must_use] + pub fn split(self, n: usize, pat: &str) -> Vec { + // Build vec of breakpoints and the length of the text + markup at that + // potential breakpoint + let mut break_point_lengths = vec![]; + let mut len = 0; + for (i, ann) in self.annotation.iter().enumerate() { + len += + ann.text.as_deref().unwrap_or("").len() + ann.markup.as_deref().unwrap_or("").len(); + if ann.text.as_ref().is_some_and(|t| t.contains(pat)) { + break_point_lengths.push((i, len)); + } + } + + // Decide which breakpoints to split the annotations at + let mut break_points: Vec = vec![]; + if break_point_lengths.len() > 1 { + let (mut i, mut ii) = (0, 1); + let (mut base, mut curr) = (0, 0); + while ii < break_point_lengths.len() { + curr += break_point_lengths[i].1 - base; + + if break_point_lengths[ii].1 - base + curr > n { + break_points.push(break_point_lengths[i].0); + base = break_point_lengths[i].1; + curr = 0; + } + + i += 1; + ii += 1; + } + } + + // Split annotations based on calculated break points + let mut split = Vec::with_capacity(break_points.len()); + let mut iter = self.into_iter(); + let mut taken = 0; + let mut annotations = vec![]; + for break_point in break_points { + while taken != break_point + 1 { + annotations.push(iter.next().unwrap()); + taken += 1; + } + split.push(Data::from_iter(mem::take(&mut annotations))); + } + + split + } +} + +impl IntoStatic for Data<'_> { + type Static = Data<'static>; + fn into_static(self) -> Self::Static { + Data { + annotation: self + .annotation + .into_iter() + .map(IntoStatic::into_static) + .collect(), + } + } +} + +impl<'source, T: Into>> FromIterator for Data<'source> { fn from_iter>(iter: I) -> Self { let annotation = iter.into_iter().map(std::convert::Into::into).collect(); Data { annotation } } } -impl Serialize for Data { +impl<'source> IntoIterator for Data<'source> { + type Item = DataAnnotation<'source>; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.annotation.into_iter() + } +} + +impl Serialize for Data<'_> { fn serialize(&self, serializer: S) -> std::result::Result where S: serde::Serializer, @@ -244,7 +337,7 @@ impl Serialize for Data { } #[cfg(feature = "cli")] -impl std::str::FromStr for Data { +impl std::str::FromStr for Data<'_> { type Err = Error; fn from_str(s: &str) -> Result { @@ -257,7 +350,7 @@ impl std::str::FromStr for Data { /// /// Currently, `Level::Picky` adds additional rules /// with respect to `Level::Default`. -#[derive(Clone, Default, Deserialize, Debug, PartialEq, Eq, Serialize, Hash)] +#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Hash)] #[cfg_attr(feature = "cli", derive(ValueEnum))] #[serde(rename_all = "lowercase")] #[non_exhaustive] @@ -275,7 +368,7 @@ impl Level { /// # Examples /// /// ``` - /// # use languagetool_rust::check::Level; + /// # use languagetool_rust::api::check::Level; /// /// let level: Level = Default::default(); /// @@ -294,7 +387,7 @@ impl Level { /// # Examples /// /// ``` -/// # use languagetool_rust::check::split_len; +/// # use languagetool_rust::api::check::split_len; /// let s = "I have so many friends. /// They are very funny. /// I think I am very lucky to have them. @@ -376,6 +469,21 @@ pub fn split_len<'source>(s: &'source str, n: usize, pat: &str) -> Vec<&'source vec } +/// Default value for [`Request::language`]. +pub const DEFAULT_LANGUAGE: &str = "auto"; + +/// Custom serialization for [`Request::language`]. +fn serialize_language(lang: &str, s: S) -> std::result::Result +where + S: Serializer, +{ + s.serialize_str(if lang.is_empty() { + DEFAULT_LANGUAGE + } else { + lang + }) +} + /// LanguageTool POST check request. /// /// The main feature - check a text with LanguageTool for possible style and @@ -383,18 +491,13 @@ pub fn split_len<'source>(s: &'source str, n: usize, pat: &str) -> Vec<&'source /// /// The structure below tries to follow as closely as possible the JSON API /// described [here](https://languagetool.org/http-api/swagger-ui/#!/default/post_check). -#[cfg_attr(feature = "cli", derive(Args))] -#[derive(Clone, Deserialize, Debug, PartialEq, Eq, Serialize, Hash)] +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Hash, IntoStatic)] #[serde(rename_all = "camelCase")] #[non_exhaustive] -pub struct CheckRequest { +pub struct Request<'source> { /// The text to be checked. This or 'data' is required. - #[cfg_attr( - feature = "cli", - clap(short = 't', long, conflicts_with = "data", allow_hyphen_values(true)) - )] #[serde(skip_serializing_if = "Option::is_none")] - pub text: Option, + pub text: Option>, /// The text to be checked, given as a JSON document that specifies what's /// text and what's markup. This or 'text' is required. /// @@ -419,49 +522,29 @@ pub struct CheckRequest { /// ``` /// The 'data' feature is not limited to HTML or XML, it can be used for any /// kind of markup. Entities will need to be expanded in this input. - #[cfg_attr(feature = "cli", clap(short = 'd', long, conflicts_with = "text"))] #[serde(skip_serializing_if = "Option::is_none")] - pub data: Option, + pub data: Option>, /// A language code like `en-US`, `de-DE`, `fr`, or `auto` to guess the /// language automatically (see `preferredVariants` below). /// /// For languages with variants (English, German, Portuguese) spell checking /// will only be activated when you specify the variant, e.g. `en-GB` /// instead of just `en`. - #[cfg_attr( - all(feature = "cli", feature = "cli", feature = "cli"), - clap( - short = 'l', - long, - default_value = "auto", - value_parser = parse_language_code - ) - )] + #[serde(serialize_with = "serialize_language")] pub language: String, /// Set to get Premium API access: Your username/email as used to log in at /// languagetool.org. - #[cfg_attr( - feature = "cli", - clap(short = 'u', long, requires = "api_key", env = "LANGUAGETOOL_USERNAME") - )] #[serde(skip_serializing_if = "Option::is_none")] pub username: Option, - /// Set to get Premium API access: [your API - /// key](https://languagetool.org/editor/settings/api). - #[cfg_attr( - feature = "cli", - clap(short = 'k', long, requires = "username", env = "LANGUAGETOOL_API_KEY") - )] + /// Set to get Premium API access: your API key (see ). #[serde(skip_serializing_if = "Option::is_none")] pub api_key: Option, /// Comma-separated list of dictionaries to include words from; uses special /// default dictionary if this is unset. - #[cfg_attr(feature = "cli", clap(long))] #[serde(serialize_with = "serialize_option_vec_string")] pub dicts: Option>, /// A language code of the user's native language, enabling false friends /// checks for some language pairs. - #[cfg_attr(feature = "cli", clap(long))] #[serde(skip_serializing_if = "Option::is_none")] pub mother_tongue: Option, /// Comma-separated list of preferred language variants. @@ -473,79 +556,51 @@ pub struct CheckRequest { /// should set variants for at least German and English, as otherwise the /// spell checking will not work for those, as no spelling dictionary can be /// selected for just `en` or `de`. - #[cfg_attr(feature = "cli", clap(long, conflicts_with = "language"))] #[serde(serialize_with = "serialize_option_vec_string")] pub preferred_variants: Option>, /// IDs of rules to be enabled, comma-separated. - #[cfg_attr(feature = "cli", clap(long))] #[serde(serialize_with = "serialize_option_vec_string")] pub enabled_rules: Option>, /// IDs of rules to be disabled, comma-separated. - #[cfg_attr(feature = "cli", clap(long))] #[serde(serialize_with = "serialize_option_vec_string")] pub disabled_rules: Option>, /// IDs of categories to be enabled, comma-separated. - #[cfg_attr(feature = "cli", clap(long))] #[serde(serialize_with = "serialize_option_vec_string")] pub enabled_categories: Option>, /// IDs of categories to be disabled, comma-separated. - #[cfg_attr(feature = "cli", clap(long))] #[serde(serialize_with = "serialize_option_vec_string")] pub disabled_categories: Option>, /// If true, only the rules and categories whose IDs are specified with /// `enabledRules` or `enabledCategories` are enabled. - #[cfg_attr(feature = "cli", clap(long))] - #[serde(skip_serializing_if = "is_false")] + #[serde(skip_serializing_if = "std::ops::Not::not")] pub enabled_only: bool, /// If set to `picky`, additional rules will be activated, i.e. rules that /// you might only find useful when checking formal text. - #[cfg_attr( - feature = "cli", - clap(long, default_value = "default", ignore_case = true, value_enum) - )] #[serde(skip_serializing_if = "Level::is_default")] pub level: Level, } -impl Default for CheckRequest { - #[inline] - fn default() -> CheckRequest { - CheckRequest { - text: Default::default(), - data: Default::default(), +impl<'source> Request<'source> { + /// Create a new empty request with language set to `"auto"`. + #[must_use] + pub fn new() -> Self { + Self { language: "auto".to_string(), - username: Default::default(), - api_key: Default::default(), - dicts: Default::default(), - mother_tongue: Default::default(), - preferred_variants: Default::default(), - enabled_rules: Default::default(), - disabled_rules: Default::default(), - enabled_categories: Default::default(), - disabled_categories: Default::default(), - enabled_only: Default::default(), - level: Default::default(), + ..Default::default() } } -} -#[inline] -fn is_false(b: &bool) -> bool { - !(*b) -} - -impl CheckRequest { /// Set the text to be checked and remove potential data field. #[must_use] - pub fn with_text(mut self, text: String) -> Self { - self.text = Some(text); + pub fn with_text>>(mut self, text: T) -> Self { + self.text = Some(text.into()); self.data = None; self } /// Set the data to be checked and remove potential text field. #[must_use] - pub fn with_data(mut self, data: Data) -> Self { + pub fn with_data(mut self, data: Data<'source>) -> Self { self.data = Some(data); self.text = None; self @@ -554,7 +609,7 @@ impl CheckRequest { /// Set the data (obtained from string) to be checked and remove potential /// text field pub fn with_data_str(self, data: &str) -> serde_json::Result { - Ok(self.with_data(serde_json::from_str(data)?)) + serde_json::from_str(data).map(|data| self.with_data(data)) } /// Set the language of the text / data. @@ -564,29 +619,29 @@ impl CheckRequest { self } - /// Return a copy of the text within the request. + /// Return the text within the request. /// /// # Errors /// /// If both `self.text` and `self.data` are [`None`]. /// If any data annotation does not contain text or markup. - pub fn try_get_text(&self) -> Result { + pub fn try_get_text(&self) -> Result> { if let Some(ref text) = self.text { Ok(text.clone()) } else if let Some(ref data) = self.data { - let mut text = String::new(); - for da in data.annotation.iter() { - if let Some(ref t) = da.text { - text.push_str(t.as_str()); - } else if let Some(ref t) = da.markup { - text.push_str(t.as_str()); - } else { - return Err(Error::InvalidDataAnnotation( - "missing either text or markup field in {da:?}".to_string(), - )); - } + match data.annotation.len() { + 0 => Ok(Default::default()), + 1 => data.annotation[0].try_get_text(), + _ => { + let mut text = String::new(); + + for da in data.annotation.iter() { + text.push_str(da.try_get_text()?.deref()); + } + + Ok(Cow::Owned(text)) + }, } - Ok(text) } else { Err(Error::InvalidRequest( "missing either text or data field".to_string(), @@ -595,14 +650,14 @@ impl CheckRequest { } /// Return a copy of the text within the request. - /// Call [`CheckRequest::try_get_text`] but panic on error. + /// Call [`Request::try_get_text`] but panic on error. /// /// # Panics /// /// If both `self.text` and `self.data` are [`None`]. /// If any data annotation does not contain text or markup. #[must_use] - pub fn get_text(&self) -> String { + pub fn get_text(&self) -> Cow<'source, str> { self.try_get_text().unwrap() } @@ -611,103 +666,70 @@ impl CheckRequest { /// /// # Errors /// - /// If `self.text` is none. - pub fn try_split(&self, n: usize, pat: &str) -> Result> { - let text = self - .text - .as_ref() - .ok_or(Error::InvalidRequest("missing text field".to_string()))?; + /// If `self.text` is [`None`] and `self.data` is [`None`]. + pub fn try_split(mut self, n: usize, pat: &str) -> Result> { + // DATA ANNOTATIONS + if let Some(data) = mem::take(&mut self.data) { + return Ok(data + .split(n, pat) + .into_iter() + .map(|d| self.clone().with_data(d)) + .collect()); + } + + // TEXT + let text = mem::take(&mut self.text) + .ok_or_else(|| Error::InvalidRequest("missing text or data field".to_string()))?; + let string: &str = match &text { + Cow::Owned(s) => s.as_str(), + Cow::Borrowed(s) => s, + }; - Ok(split_len(text.as_str(), n, pat) + Ok(split_len(string, n, pat) .iter() - .map(|text_fragment| self.clone().with_text(text_fragment.to_string())) + .map(|text_fragment| { + self.clone() + .with_text(Cow::Owned(text_fragment.to_string())) + }) .collect()) } /// Split this request into multiple, using [`split_len`] function to split /// text. - /// Call [`CheckRequest::try_split`] but panic on error. + /// Call [`Request::try_split`] but panic on error. /// /// # Panics /// /// If `self.text` is none. #[must_use] - pub fn split(&self, n: usize, pat: &str) -> Vec { + pub fn split(self, n: usize, pat: &str) -> Vec { self.try_split(n, pat).unwrap() } } -/// Parse a string slice into a [`PathBuf`], and error if the file does not -/// exist. -#[cfg(feature = "cli")] -fn parse_filename(s: &str) -> Result { - let path_buf: PathBuf = s.parse().unwrap(); - - if path_buf.is_file() { - Ok(path_buf) - } else { - Err(Error::InvalidFilename(s.to_string())) - } -} - -/// Check text using LanguageTool server. -#[cfg(feature = "cli")] -#[derive(Debug, Parser)] -pub struct CheckCommand { - /// If present, raw JSON output will be printed instead of annotated text. - /// This has no effect if `--data` is used, because it is never - /// annotated. - #[cfg(feature = "cli")] - #[clap(short = 'r', long)] - pub raw: bool, - /// If present, more context (i.e., line number and line offset) will be - /// added to response. - #[clap(short = 'm', long, hide = true)] - #[deprecated( - since = "2.0.0", - note = "Do not use this, it is only kept for backwards compatibility with v1" - )] - pub more_context: bool, - /// Sets the maximum number of characters before splitting. - #[clap(long, default_value_t = 1500)] - pub max_length: usize, - /// If text is too long, will split on this pattern. - #[clap(long, default_value = "\n\n")] - pub split_pattern: String, - /// Max. number of suggestions kept. If negative, all suggestions are kept. - #[clap(long, default_value_t = 5, allow_negative_numbers = true)] - pub max_suggestions: isize, - /// Inner [`CheckRequest`]. - #[command(flatten)] - pub request: CheckRequest, - /// Optional filenames from which input is read. - #[arg(conflicts_with_all(["text", "data"]), value_parser = parse_filename)] - pub filenames: Vec, -} - #[cfg(test)] mod request_tests { - use crate::CheckRequest; + use super::Request; #[test] fn test_with_text() { - let req = CheckRequest::default().with_text("hello".to_string()); + let req = Request::default().with_text("hello"); - assert_eq!(req.text.unwrap(), "hello".to_string()); + assert_eq!(req.text.unwrap(), "hello"); assert!(req.data.is_none()); } #[test] fn test_with_data() { - let req = CheckRequest::default().with_text("hello".to_string()); + let req = Request::default().with_text("hello"); - assert_eq!(req.text.unwrap(), "hello".to_string()); + assert_eq!(req.text.unwrap(), "hello"); assert!(req.data.is_none()); } } -/// Responses +// RESPONSES /// Detected language from check request. #[allow(clippy::derive_partial_eq_without_eq)] @@ -824,7 +846,7 @@ pub struct Rule { pub urls: Option>, } -/// Type of a given match. +/// Type of given match. #[derive(PartialEq, Eq, Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] #[non_exhaustive] @@ -855,7 +877,7 @@ pub struct Match { /// More context to match, post-processed using original text. #[serde(skip_serializing_if = "Option::is_none")] pub more_context: Option, - /// Char index at which the match start. + /// Char index at which the match starts. pub offset: usize, /// List of possible replacements (if applies). pub replacements: Vec, @@ -907,7 +929,7 @@ pub struct Warnings { #[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] #[non_exhaustive] -pub struct CheckResponse { +pub struct Response { /// Language information. pub language: LanguageResponse, /// List of error matches. @@ -922,7 +944,7 @@ pub struct CheckResponse { pub warnings: Option, } -impl CheckResponse { +impl Response { /// Return an iterator over matches. pub fn iter_matches(&self) -> std::slice::Iter<'_, Match> { self.matches.iter() @@ -938,7 +960,7 @@ impl CheckResponse { #[must_use] pub fn annotate(&self, text: &str, origin: Option<&str>, color: bool) -> String { if self.matches.is_empty() { - return "No error were found in provided text".to_string(); + return "No errors were found in provided text".to_string(); } let replacements: Vec<_> = self .matches @@ -997,27 +1019,59 @@ impl CheckResponse { } annotation } + + /// Joins the given [`Request`] to the current one. + /// + /// This is especially useful when a request was split into multiple + /// requests. + #[must_use] + pub fn append(mut self, mut other: Self) -> Self { + #[cfg(feature = "unstable")] + if let Some(ref mut sr_other) = other.sentence_ranges { + match self.sentence_ranges { + Some(ref mut sr_self) => { + sr_self.append(sr_other); + }, + None => { + std::mem::swap(&mut self.sentence_ranges, &mut other.sentence_ranges); + }, + } + } + + self.matches.append(&mut other.matches); + + self + } } /// Check response with additional context. /// /// This structure exists to keep a link between a check response /// and the original text that was checked. -#[derive(Debug, Clone, PartialEq)] -pub struct CheckResponseWithContext { +#[derive(Debug, Clone, PartialEq, IntoStatic)] +pub struct ResponseWithContext<'source> { /// Original text that was checked by LT. - pub text: String, + pub text: Cow<'source, str>, /// Check response. - pub response: CheckResponse, + pub response: Response, /// Text's length. pub text_length: usize, } -impl CheckResponseWithContext { +impl Deref for ResponseWithContext<'_> { + type Target = Response; + fn deref(&self) -> &Self::Target { + &self.response + } +} + +impl<'source> ResponseWithContext<'source> { /// Bind a check response with its original text. #[must_use] - pub fn new(text: String, response: CheckResponse) -> Self { + pub fn new(text: Cow<'source, str>, response: Response) -> Self { let text_length = text.chars().count(); + + // Add more context to response Self { text, response, @@ -1026,7 +1080,7 @@ impl CheckResponseWithContext { } /// Return an iterator over matches. - pub fn iter_matches(&self) -> std::slice::Iter<'_, Match> { + pub fn iter_matches(&'source self) -> std::slice::Iter<'source, Match> { self.response.iter_matches() } @@ -1038,7 +1092,7 @@ impl CheckResponseWithContext { /// Return an iterator over matches and corresponding line number and line /// offset. #[must_use] - pub fn iter_match_positions(&self) -> MatchPositions<'_, std::slice::Iter<'_, Match>> { + pub fn iter_match_positions(&self) -> MatchPositions<'_, '_, std::slice::Iter<'_, Match>> { self.into() } @@ -1069,42 +1123,59 @@ impl CheckResponseWithContext { } self.response.matches.append(&mut other.response.matches); - self.text.push_str(other.text.as_str()); + + self.text.to_mut().push_str(&other.text); self.text_length += other.text_length; + self } } -impl From for CheckResponse { - #[allow(clippy::needless_borrow)] - fn from(mut resp: CheckResponseWithContext) -> Self { - let iter: MatchPositions<'_, std::slice::IterMut<'_, Match>> = (&mut resp).into(); - - for (line_number, line_offset, m) in iter { +impl<'source> From> for Response { + fn from(mut resp: ResponseWithContext<'source>) -> Self { + for (line_number, line_offset, m) in MatchPositions::new(&resp.text, &mut resp.response) { m.more_context = Some(MoreContext { line_number, line_offset, }); } + resp.response } } /// Iterator over matches and their corresponding line number and line offset. #[derive(Clone, Debug)] -pub struct MatchPositions<'source, T> { +pub struct MatchPositions<'source, 'response, T: Iterator + 'response> { text_chars: std::str::Chars<'source>, matches: T, line_number: usize, line_offset: usize, offset: usize, + _marker: PhantomData<&'response ()>, +} + +impl<'source, 'response> MatchPositions<'source, 'response, std::slice::IterMut<'response, Match>> { + fn new(text: &'source str, response: &'response mut Response) -> Self { + MatchPositions { + _marker: Default::default(), + text_chars: text.chars(), + matches: response.iter_matches_mut(), + line_number: 1, + line_offset: 0, + offset: 0, + } + } } -impl<'source> From<&'source CheckResponseWithContext> - for MatchPositions<'source, std::slice::Iter<'source, Match>> +impl<'source, 'response> From<&'source ResponseWithContext<'source>> + for MatchPositions<'source, 'response, std::slice::Iter<'response, Match>> +where + 'source: 'response, { - fn from(response: &'source CheckResponseWithContext) -> Self { + fn from(response: &'source ResponseWithContext) -> Self { MatchPositions { + _marker: Default::default(), text_chars: response.text.chars(), matches: response.iter_matches(), line_number: 1, @@ -1114,11 +1185,14 @@ impl<'source> From<&'source CheckResponseWithContext> } } -impl<'source> From<&'source mut CheckResponseWithContext> - for MatchPositions<'source, std::slice::IterMut<'source, Match>> +impl<'source, 'response> From<&'source mut ResponseWithContext<'source>> + for MatchPositions<'source, 'response, std::slice::IterMut<'response, Match>> +where + 'source: 'response, { - fn from(response: &'source mut CheckResponseWithContext) -> Self { + fn from(response: &'source mut ResponseWithContext) -> Self { MatchPositions { + _marker: Default::default(), text_chars: response.text.chars(), matches: response.response.iter_matches_mut(), line_number: 1, @@ -1128,8 +1202,8 @@ impl<'source> From<&'source mut CheckResponseWithContext> } } -impl<'source, T> MatchPositions<'source, T> { - /// Set the line number to a give value. +impl<'response, T: Iterator + 'response> MatchPositions<'_, 'response, T> { + /// Set the line number to a given value. /// /// By default, the first line number is 1. pub fn set_line_number(mut self, line_number: usize) -> Self { @@ -1158,7 +1232,11 @@ impl<'source, T> MatchPositions<'source, T> { } } -impl<'source> Iterator for MatchPositions<'source, std::slice::Iter<'source, Match>> { +impl<'source, 'response> Iterator + for MatchPositions<'source, 'response, std::slice::Iter<'response, Match>> +where + 'response: 'source, +{ type Item = (usize, usize, &'source Match); fn next(&mut self) -> Option { @@ -1171,7 +1249,11 @@ impl<'source> Iterator for MatchPositions<'source, std::slice::Iter<'source, Mat } } -impl<'source> Iterator for MatchPositions<'source, std::slice::IterMut<'source, Match>> { +impl<'source, 'response> Iterator + for MatchPositions<'source, 'response, std::slice::IterMut<'response, Match>> +where + 'response: 'source, +{ type Item = (usize, usize, &'source mut Match); fn next(&mut self) -> Option { @@ -1194,9 +1276,6 @@ mod tests { Skip(&'source str), } - #[derive(Debug, Clone)] - struct ParseTokenError; - impl<'source> From<&'source str> for Token<'source> { fn from(s: &'source str) -> Self { if s.chars().all(|c| c.is_ascii_alphabetic()) { @@ -1207,11 +1286,11 @@ mod tests { } } - impl<'source> From> for DataAnnotation { + impl<'source> From> for DataAnnotation<'source> { fn from(token: Token<'source>) -> Self { match token { - Token::Text(s) => DataAnnotation::new_text(s.to_string()), - Token::Skip(s) => DataAnnotation::new_markup(s.to_string()), + Token::Text(s) => DataAnnotation::new_text(s), + Token::Skip(s) => DataAnnotation::new_markup(s), } } } @@ -1223,10 +1302,10 @@ mod tests { let expected_data = Data { annotation: vec![ - DataAnnotation::new_text("My".to_string()), - DataAnnotation::new_text("name".to_string()), - DataAnnotation::new_text("is".to_string()), - DataAnnotation::new_markup("Q34XY".to_string()), + DataAnnotation::new_text("My"), + DataAnnotation::new_text("name"), + DataAnnotation::new_text("is"), + DataAnnotation::new_markup("Q34XY"), ], }; diff --git a/src/lib/languages.rs b/src/api/languages.rs similarity index 92% rename from src/lib/languages.rs rename to src/api/languages.rs index 92724bf..ad4014f 100644 --- a/src/lib/languages.rs +++ b/src/api/languages.rs @@ -18,4 +18,4 @@ pub struct Language { /// LanguageTool GET languages response. /// /// List of all supported languages. -pub type LanguagesResponse = Vec; +pub type Response = Vec; diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..813a72f --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,73 @@ +//! Raw bindings to the LanguageTool API v1.1.2. +//! +//! The current bindings were generated using the +//! [HTTP API documentation](https://languagetool.org/http-api/). +//! +//! Unfortunately, the LanguageTool API is not as documented as we could +//! hope, and requests might return undocumented fields. Those are de-serialized +//! to the `undocumented` field. +pub mod check; +pub mod languages; +pub mod server; +pub mod words; + +use crate::error::Result; + +/// A HTTP client for making requests to a LanguageTool server. +#[derive(Debug)] +pub struct Client { + /// Server's hostname. + hostname: String, + /// Server's port. + port: Option, + /// Inner client to perform HTTP requests. + client: reqwest::Client, +} + +impl Default for Client { + fn default() -> Self { + Self { + hostname: "https://api.languagetoolplus.com".to_string(), + port: None, + client: Default::default(), + } + } +} + +impl Client { + /// Construct an HTTP URL base on the current hostname, optional port, + /// and provided endpoint. + #[inline] + #[must_use] + pub fn url(&self, endpoint: &str) -> String { + let hostname = &self.hostname; + match &self.port { + Some(p) => format!("{hostname}:{p}/v2{endpoint}"), + None => format!("{hostname}/v2{endpoint}"), + } + } + + /// Send a check request to the server and await for the response. + pub async fn check(&self, request: &check::Request<'_>) -> Result { + self.client + .post(self.url("/check")) + .query(request) + .send() + .await? + .json::() + .await + .map_err(Into::into) + } + + /// Send a request for the list of supported languages to the server and + /// await for the response. + pub async fn languages(&self) -> Result { + self.client + .get(self.url("/languages")) + .send() + .await? + .json::() + .await + .map_err(Into::into) + } +} diff --git a/src/lib/server.rs b/src/api/server.rs similarity index 72% rename from src/lib/server.rs rename to src/api/server.rs index bb37987..4706e63 100644 --- a/src/lib/server.rs +++ b/src/api/server.rs @@ -1,17 +1,22 @@ //! Structure to communicate with some `LanguageTool` server through the API. +#[cfg(feature = "multithreaded")] +use crate::api::check; use crate::{ - check::{CheckRequest, CheckResponse, CheckResponseWithContext}, - error::{Error, Result}, - languages::LanguagesResponse, - words::{ - WordsAddRequest, WordsAddResponse, WordsDeleteRequest, WordsDeleteResponse, WordsRequest, - WordsResponse, + api::{ + check::{Request, Response}, + languages, words, }, + error::{Error, Result}, }; #[cfg(feature = "cli")] use clap::Args; -use reqwest::{header::{HeaderValue, ACCEPT}, Client}; +#[cfg(feature = "multithreaded")] +use lifetime::IntoStatic; +use reqwest::{ + header::{HeaderValue, ACCEPT}, + Client, +}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::{io, path::PathBuf, time::Instant}; @@ -25,7 +30,7 @@ use std::{io, path::PathBuf, time::Instant}; /// # Examples /// /// ``` -/// # use languagetool_rust::server::parse_port; +/// # use languagetool_rust::api::server::parse_port; /// assert!(parse_port("8081").is_ok()); /// /// assert!(parse_port("").is_ok()); // No port specified, which is accepted @@ -264,6 +269,7 @@ impl Default for ServerParameters { /// To use your local server instead of online api, set: /// * `hostname` to "http://localhost" /// * `port` to "8081" +/// /// if you used the default configuration to start the server. #[cfg_attr(feature = "cli", derive(Args))] #[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Debug)] @@ -368,40 +374,37 @@ impl ServerClient { } /// Send a check request to the server and await for the response. - pub async fn check(&self, request: &CheckRequest) -> Result { - match self + pub async fn check(&self, request: &Request<'_>) -> Result { + let resp = self .client .post(format!("{0}/check", self.api)) .header(ACCEPT, HeaderValue::from_static("application/json")) .form(request) .send() .await - { - Ok(resp) => { - match resp.error_for_status_ref() { - Ok(_) => { - resp.json::() - .await - .map_err(Error::ResponseDecode) - .map(|mut resp| { - if self.max_suggestions > 0 { - let max = self.max_suggestions as usize; - resp.matches.iter_mut().for_each(|m| { - let len = m.replacements.len(); - if max < len { - m.replacements[max] = - format!("... ({} not shown)", len - max).into(); - m.replacements.truncate(max + 1); - } - }); + .map_err(Error::Reqwest)?; + + match resp.error_for_status_ref() { + Ok(_) => { + resp.json::() + .await + .map_err(Into::into) + .map(|mut resp| { + if self.max_suggestions > 0 { + let max = self.max_suggestions as usize; + resp.matches.iter_mut().for_each(|m| { + let len = m.replacements.len(); + if max < len { + m.replacements[max] = + format!("... ({} not shown)", len - max).into(); + m.replacements.truncate(max + 1); } - resp - }) - }, - Err(_) => Err(Error::InvalidRequest(resp.text().await?)), - } + }); + } + resp + }) }, - Err(e) => Err(Error::RequestEncode(e)), + Err(_) => Err(Error::InvalidRequest(resp.text().await?)), } } @@ -409,39 +412,90 @@ impl ServerClient { /// /// # Error /// - /// If any of the requests has `self.text` field which is none. + /// If any of the requests has `self.text` field which is none, or + /// if zero request is provided. #[cfg(feature = "multithreaded")] - pub async fn check_multiple_and_join( + pub async fn check_multiple_and_join<'source>( &self, - requests: Vec, - ) -> Result { - let mut tasks = Vec::with_capacity(requests.len()); - - for request in requests.into_iter() { - let server_client = self.clone(); - tasks.push(tokio::spawn(async move { - let response = server_client.check(&request).await?; - let text = request.text.ok_or(Error::InvalidRequest( - "missing text field; cannot join requests with data annotations".to_string(), - ))?; - Result::<(String, CheckResponse)>::Ok((text, response)) - })); + requests: Vec>, + ) -> Result> { + use std::borrow::Cow; + + if requests.is_empty() { + return Err(Error::InvalidRequest( + "no request; cannot join zero request".to_string(), + )); } - let mut response_with_context: Option = None; + let tasks = requests + .into_iter() + .map(|r| r.into_static()) + .map(|request| { + let server_client = self.clone(); + + tokio::spawn(async move { + let response = server_client.check(&request).await?; + let text = request.text.ok_or_else(|| { + Error::InvalidRequest( + "missing text field; cannot join requests with data annotations" + .to_string(), + ) + })?; + Result::<(Cow<'static, str>, Response)>::Ok((text, response)) + }) + }); + + let mut response_with_context: Option = None; for task in tasks { let (text, response) = task.await.unwrap()?; - match response_with_context { - Some(resp) => { - response_with_context = - Some(resp.append(CheckResponseWithContext::new(text, response))) - }, - None => response_with_context = Some(CheckResponseWithContext::new(text, response)), - } + + response_with_context = Some(match response_with_context { + Some(resp) => resp.append(check::ResponseWithContext::new(text, response)), + None => check::ResponseWithContext::new(text, response), + }) } - Ok(response_with_context.unwrap().into()) + Ok(response_with_context.unwrap()) + } + + /// Send multiple check requests and join them into a single response, + /// without any context. + /// + /// # Error + /// + /// If any of the requests has `self.text` or `self.data` field which is + /// [`None`]. + #[cfg(feature = "multithreaded")] + pub async fn check_multiple_and_join_without_context( + &self, + requests: Vec>, + ) -> Result { + let mut response: Option = None; + + let tasks = requests + .into_iter() + .map(|r| r.into_static()) + .map(|request| { + let server_client = self.clone(); + + tokio::spawn(async move { + let response = server_client.check(&request).await?; + Result::::Ok(response) + }) + }); + + // Make requests in sequence + for task in tasks { + let resp = task.await.unwrap()?; + + response = Some(match response { + Some(r) => r.append(resp), + None => resp, + }) + } + + Ok(response.unwrap()) } /// Send a check request to the server, await for the response and annotate @@ -449,107 +503,90 @@ impl ServerClient { #[cfg(feature = "annotate")] pub async fn annotate_check( &self, - request: &CheckRequest, + request: &Request<'_>, origin: Option<&str>, color: bool, ) -> Result { let text = request.get_text(); let resp = self.check(request).await?; - Ok(resp.annotate(text.as_str(), origin, color)) + Ok(resp.annotate(text.as_ref(), origin, color)) } /// Send a languages request to the server and await for the response. - pub async fn languages(&self) -> Result { - match self + pub async fn languages(&self) -> Result { + let resp = self .client .get(format!("{}/languages", self.api)) .send() .await - { - Ok(resp) => { - match resp.error_for_status_ref() { - Ok(_) => { - resp.json::() - .await - .map_err(Error::ResponseDecode) - }, - Err(_) => Err(Error::InvalidRequest(resp.text().await?)), - } - }, - Err(e) => Err(Error::RequestEncode(e)), + .map_err(Error::Reqwest)?; + + match resp.error_for_status_ref() { + Ok(_) => resp.json::().await.map_err(Into::into), + Err(_) => Err(Error::InvalidRequest(resp.text().await?)), } } /// Send a words request to the server and await for the response. - pub async fn words(&self, request: &WordsRequest) -> Result { - match self + pub async fn words(&self, request: &words::Request) -> Result { + let resp = self .client .get(format!("{}/words", self.api)) .header(ACCEPT, HeaderValue::from_static("application/json")) .query(request) .send() .await - { - Ok(resp) => { - match resp.error_for_status_ref() { - Ok(_) => { - resp.json::() - .await - .map_err(Error::ResponseDecode) - }, - Err(_) => Err(Error::InvalidRequest(resp.text().await?)), - } - }, - Err(e) => Err(Error::RequestEncode(e)), + .map_err(Error::Reqwest)?; + + match resp.error_for_status_ref() { + Ok(_) => resp.json::().await.map_err(Error::Reqwest), + Err(_) => Err(Error::InvalidRequest(resp.text().await?)), } } /// Send a words/add request to the server and await for the response. - pub async fn words_add(&self, request: &WordsAddRequest) -> Result { - match self + pub async fn words_add(&self, request: &words::add::Request) -> Result { + let resp = self .client .post(format!("{}/words/add", self.api)) .header(ACCEPT, HeaderValue::from_static("application/json")) .form(request) .send() .await - { - Ok(resp) => { - match resp.error_for_status_ref() { - Ok(_) => { - resp.json::() - .await - .map_err(Error::ResponseDecode) - }, - Err(_) => Err(Error::InvalidRequest(resp.text().await?)), - } + .map_err(Error::Reqwest)?; + + match resp.error_for_status_ref() { + Ok(_) => { + resp.json::() + .await + .map_err(Error::Reqwest) }, - Err(e) => Err(Error::RequestEncode(e)), + Err(_) => Err(Error::InvalidRequest(resp.text().await?)), } } /// Send a words/delete request to the server and await for the response. - pub async fn words_delete(&self, request: &WordsDeleteRequest) -> Result { - match self + pub async fn words_delete( + &self, + request: &words::delete::Request, + ) -> Result { + let resp = self .client .post(format!("{}/words/delete", self.api)) .header(ACCEPT, HeaderValue::from_static("application/json")) .form(request) .send() .await - { - Ok(resp) => { - match resp.error_for_status_ref() { - Ok(_) => { - resp.json::() - .await - .map_err(Error::ResponseDecode) - }, - Err(_) => Err(Error::InvalidRequest(resp.text().await?)), - } + .map_err(Error::Reqwest)?; + + match resp.error_for_status_ref() { + Ok(_) => { + resp.json::() + .await + .map_err(Error::Reqwest) }, - Err(e) => Err(Error::RequestEncode(e)), + Err(_) => Err(Error::InvalidRequest(resp.text().await?)), } } @@ -587,33 +624,54 @@ impl ServerClient { #[cfg(test)] mod tests { - use crate::{check::CheckRequest, ServerClient}; + use assert_matches::assert_matches; + + use super::ServerClient; + use crate::{api::check::Request, error::Error}; + + fn dbg_err(e: &Error) { + eprintln!("Error: {e:?}") + } #[tokio::test] async fn test_server_ping() { let client = ServerClient::from_env_or_default(); - assert!(client.ping().await.is_ok()); + assert!(client.ping().await.inspect_err(dbg_err).is_ok()); } #[tokio::test] async fn test_server_check_text() { let client = ServerClient::from_env_or_default(); - let req = CheckRequest::default().with_text("je suis une poupee".to_string()); - assert!(client.check(&req).await.is_ok()); + + let req = Request::default().with_text("je suis une poupee"); + assert!(client.check(&req).await.inspect_err(dbg_err).is_ok()); + + // Too long + let req = Request::default().with_text("Repeat ".repeat(1500)); + assert_matches!(client.check(&req).await, Err(Error::InvalidRequest(_))); } #[tokio::test] async fn test_server_check_data() { let client = ServerClient::from_env_or_default(); - let req = CheckRequest::default() + let req = Request::default() .with_data_str("{\"annotation\":[{\"text\": \"je suis une poupee\"}]}") .unwrap(); - assert!(client.check(&req).await.is_ok()); + assert!(client.check(&req).await.inspect_err(dbg_err).is_ok()); + + // Too long + let req = Request::default() + .with_data_str(&format!( + "{{\"annotation\":[{{\"text\": \"{}\"}}]}}", + "repeat".repeat(5000) + )) + .unwrap(); + assert_matches!(client.check(&req).await, Err(Error::InvalidRequest(_))); } #[tokio::test] async fn test_server_languages() { let client = ServerClient::from_env_or_default(); - assert!(client.languages().await.is_ok()); + assert!(client.languages().await.inspect_err(dbg_err).is_ok()); } } diff --git a/src/api/words/add.rs b/src/api/words/add.rs new file mode 100644 index 0000000..95af380 --- /dev/null +++ b/src/api/words/add.rs @@ -0,0 +1,38 @@ +//! Structures for `words` requests and responses related to adding. + +use super::*; + +/// LanguageTool POST words add request. +/// +/// Add a word to one of the user's personal dictionaries. Please note that +/// this feature is considered to be used for personal dictionaries +/// which must not contain more than 500 words. If this is an issue for +/// you, please contact us. +#[cfg_attr(feature = "cli", derive(Args))] +#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize, Hash)] +#[non_exhaustive] +pub struct Request { + /// The word to be added. Must not be a phrase, i.e., cannot contain + /// white space. The word is added to a global dictionary that + /// applies to all languages. + #[cfg_attr(feature = "cli", clap(required = true, value_parser = parse_word))] + pub word: String, + /// Login arguments. + #[cfg_attr(feature = "cli", clap(flatten))] + #[serde(flatten)] + pub login: LoginArgs, + /// Name of the dictionary to add the word to; non-existent dictionaries + /// are created after calling this; if unset, adds to special + /// default dictionary. + #[cfg_attr(feature = "cli", clap(long))] + #[serde(skip_serializing_if = "Option::is_none")] + pub dict: Option, +} + +/// LanguageTool POST word add response. +#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] +#[non_exhaustive] +pub struct Response { + /// `true` if word was correctly added. + pub added: bool, +} diff --git a/src/api/words/delete.rs b/src/api/words/delete.rs new file mode 100644 index 0000000..a1f5af4 --- /dev/null +++ b/src/api/words/delete.rs @@ -0,0 +1,33 @@ +//! Structures for `words` requests and responses related to deleting. + +use super::*; + +/// LanguageTool POST words delete request. +/// +/// Remove a word from one of the user's personal dictionaries. +#[cfg_attr(feature = "cli", derive(Args))] +#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize, Hash)] +#[non_exhaustive] +pub struct Request { + /// The word to be removed. + #[cfg_attr(feature = "cli", clap(required = true, value_parser = parse_word))] + pub word: String, + /// Login arguments. + #[cfg_attr(feature = "cli", clap(flatten))] + #[serde(flatten)] + pub login: LoginArgs, + /// Name of the dictionary to add the word to; non-existent dictionaries + /// are created after calling this; if unset, adds to special + /// default dictionary. + #[cfg_attr(feature = "cli", clap(long))] + #[serde(skip_serializing_if = "Option::is_none")] + pub dict: Option, +} + +/// LanguageTool POST word delete response. +#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] +#[non_exhaustive] +pub struct Response { + /// `true` if word was correctly removed. + pub deleted: bool, +} diff --git a/src/api/words/mod.rs b/src/api/words/mod.rs new file mode 100644 index 0000000..204d81c --- /dev/null +++ b/src/api/words/mod.rs @@ -0,0 +1,129 @@ +//! Structures for `words` requests and responses. + +use crate::error::{Error, Result}; + +use super::check::serialize_option_vec_string; +#[cfg(feature = "cli")] +use clap::Args; +use serde::{Deserialize, Serialize}; + +pub mod add; +pub mod delete; + +/// Parse `v` if valid word. +/// +/// A valid word is any string slice that does not contain any whitespace +/// +/// # Examples +/// +/// ``` +/// # use languagetool_rust::api::words::parse_word; +/// assert!(parse_word("word").is_ok()); +/// +/// assert!(parse_word("some words").is_err()); +/// ``` +pub fn parse_word(v: &str) -> Result { + if !v.contains(' ') { + return Ok(v.to_string()); + } + Err(Error::InvalidValue( + "The value should be a word that does not contain any whitespace".to_string(), + )) +} + +/// Login arguments required by the API. +#[cfg_attr(feature = "cli", derive(Args))] +#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize, Hash)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct LoginArgs { + /// Your username as used to log in at languagetool.org. + #[cfg_attr( + feature = "cli", + clap(short = 'u', long, required = true, env = "LANGUAGETOOL_USERNAME") + )] + pub username: String, + /// Your API key (see ). + #[cfg_attr( + feature = "cli", + clap(short = 'k', long, required = true, env = "LANGUAGETOOL_API_KEY") + )] + pub api_key: String, +} + +/// LanguageTool GET words request. +/// +/// List words in the user's personal dictionaries. +#[cfg_attr(feature = "cli", derive(Args))] +#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize, Hash)] +#[non_exhaustive] +pub struct Request { + /// Offset of where to start in the list of words. + /// + /// Defaults to 0. + #[cfg_attr(feature = "cli", clap(long))] + #[serde(skip_serializing_if = "Option::is_none")] + pub offset: Option, + /// Maximum number of words to return. + /// + /// Defaults to 10. + #[cfg_attr(feature = "cli", clap(long))] + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + /// Login arguments. + #[cfg_attr(feature = "cli", clap(flatten))] + #[serde(flatten)] + pub login: LoginArgs, + /// Comma-separated list of dictionaries to include words from; uses special + /// default dictionary if this is unset. + #[cfg_attr(feature = "cli", clap(long))] + #[serde(serialize_with = "serialize_option_vec_string")] + #[serde(skip_serializing_if = "Option::is_none")] + pub dicts: Option>, +} + +/// Copy of [`Request`], but used to CLI only. +/// +/// This is a temporary solution, until [#4697](https://github.com/clap-rs/clap/issues/4697) is +/// closed. +#[cfg(feature = "cli")] +#[derive(Args, Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] +#[non_exhaustive] +pub struct RequestArgs { + /// Offset of where to start in the list of words. + #[cfg_attr(feature = "cli", clap(long, default_value = "0"))] + pub offset: isize, + /// Maximum number of words to return. + #[cfg_attr(feature = "cli", clap(long, default_value = "10"))] + pub limit: isize, + /// Login arguments. + #[cfg_attr(feature = "cli", clap(flatten))] + #[serde(flatten)] + pub login: Option, + /// Comma-separated list of dictionaries to include words from; uses special + /// default dictionary if this is unset. + #[cfg_attr(feature = "cli", clap(long))] + #[serde(serialize_with = "serialize_option_vec_string")] + pub dicts: Option>, +} + +#[cfg(feature = "cli")] +impl From for Request { + #[inline] + fn from(args: RequestArgs) -> Self { + Self { + offset: Some(args.offset), + limit: Some(args.limit), + login: args.login.unwrap(), + dicts: args.dicts, + } + } +} + +/// LanguageTool GET words response. +#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] +#[non_exhaustive] +pub struct Response { + /// List of words. + pub words: Vec, +} diff --git a/src/cli/check.rs b/src/cli/check.rs new file mode 100644 index 0000000..210350a --- /dev/null +++ b/src/cli/check.rs @@ -0,0 +1,418 @@ +//! Check text using LanguageTool server. +//! +//! The input can be one of the following: +//! +//! - raw text, if `--text TEXT` is provided; +//! - annotated data, if `--data TEXT` is provided; +//! - text from file(s), if `[FILE(S)]...` are provided. +//! - raw text through `stdin`, if nothing else is provided. +use std::{borrow::Cow, io, io::Write, path::PathBuf}; + +use clap::{Args, Parser, ValueEnum}; +use is_terminal::IsTerminal; +use serde::{Deserialize, Serialize}; +use termcolor::{StandardStream, WriteColor}; + +use crate::{ + api::{ + check::{ + self, parse_language_code, Data, DataAnnotation, Level, Request, DEFAULT_LANGUAGE, + }, + server::ServerClient, + }, + error::{Error, Result}, + parsers::{html::parse_html, markdown::parse_markdown, typst::parse_typst}, +}; + +use super::ExecuteSubcommand; + +/// Parse a string slice into a [`PathBuf`], and error if the file does not +/// exist. +fn parse_filename(s: &str) -> Result { + let path_buf = PathBuf::from(s); + + if path_buf.is_file() { + Ok(path_buf) + } else { + Err(Error::InvalidFilename(s.to_string())) + } +} + +/// Command to check a text with LanguageTool for possible style and grammar +/// issues. +#[derive(Debug, Parser)] +pub struct Command { + /// If present, raw JSON output will be printed instead of annotated text. + /// This has no effect if `--data` is used, because it is never + /// annotated. + #[clap(short = 'r', long)] + pub raw: bool, + /// Sets the maximum number of characters before splitting. + #[clap(long, default_value_t = 1500)] + pub max_length: usize, + /// If text is too long, will split on this pattern. + #[clap(long, default_value = "\n\n")] + pub split_pattern: String, + /// Max. number of suggestions kept. If negative, all suggestions are kept. + #[clap(long, default_value_t = 5, allow_negative_numbers = true)] + pub max_suggestions: isize, + /// Specify the files type to use the correct parser. + /// + /// If set to auto, the type is guessed from the filename extension. + #[clap(long, value_enum, default_value_t = FileType::default(), ignore_case = true)] + pub r#type: FileType, + /// Optional filenames from which input is read. + #[arg(conflicts_with_all(["text", "data"]), value_parser = parse_filename)] + pub filenames: Vec, + /// Inner [`Request`]. + #[command(flatten, next_help_heading = "Request options")] + pub request: CliRequest, +} + +/// Support file types. +#[derive(Clone, Debug, Default, ValueEnum)] +#[non_exhaustive] +pub enum FileType { + /// Auto. + #[default] + Auto, + /// Raw text. + Raw, + /// Markdown. + Markdown, + /// HTML. + Html, + /// Typst. + Typst, +} + +/// Read lines from standard input and write to buffer string. +/// +/// Standard output is used when waiting for user to input text. +fn read_from_stdin(buffer: &mut String) -> Result<()> { + if io::stdin().is_terminal() { + #[cfg(windows)] + log::info!("Reading from STDIN, press [CTRL+Z] when you're done."); + + #[cfg(unix)] + log::info!("Reading from STDIN, press [CTRL+D] when you're done."); + } + let stdin = std::io::stdin(); + + while stdin.read_line(buffer)? > 0 {} + Ok(()) +} + +impl ExecuteSubcommand for Command { + /// Executes the `check` subcommand. + async fn execute(self, mut stdout: StandardStream, server_client: ServerClient) -> Result<()> { + let mut request: check::Request = self.request.into(); + #[cfg(feature = "annotate")] + let color = stdout.supports_color(); + + let server_client = server_client.with_max_suggestions(self.max_suggestions); + + // ANNOTATED DATA, RAW TEXT, STDIN + if self.filenames.is_empty() { + // Fallback to `stdin` if nothing else is provided + if request.text.is_none() && request.data.is_none() { + let mut text = String::new(); + read_from_stdin(&mut text)?; + request = request.with_text(Cow::Owned(text)); + } + + if let Some(ref text) = request.text { + if text.is_empty() { + log::warn!("No input text was provided, skipping."); + return Ok(()); + } + } else { + // Handle annotated data + let response = server_client.check(&request).await?; + writeln!(&mut stdout, "{}", serde_json::to_string_pretty(&response)?)?; + return Ok(()); + }; + + let requests = request.split(self.max_length, self.split_pattern.as_str()); + + let response = server_client.check_multiple_and_join(requests).await?; + + writeln!( + &mut stdout, + "{}", + &response.annotate(response.text.as_ref(), None, color) + )?; + + return Ok(()); + } + + // FILES + for filename in self.filenames.iter() { + let mut file_type = self.r#type.clone(); + + // If file type is "Auto", guess file type from extension + if matches!(self.r#type, FileType::Auto) { + file_type = match PathBuf::from(filename).extension().and_then(|e| e.to_str()) { + Some(ext) => { + match ext { + "typ" => FileType::Typst, + "md" | "markdown" | "mdown" | "mdwn" | "mkd" | "mkdn" | "mdx" => { + FileType::Markdown + }, + + "html" | "htm" => FileType::Html, + _ => { + log::debug!("Unknown file type: {ext}."); + FileType::Raw + }, + } + }, + None => { + log::debug!("No extension found for file: {filename:?}."); + FileType::Raw + }, + }; + }; + + let file_content = std::fs::read_to_string(filename)?; + + let (response, text): (check::Response, String) = match &file_type { + FileType::Auto => unreachable!(), + FileType::Raw => { + let requests = (request.clone().with_text(&file_content)) + .split(self.max_length, self.split_pattern.as_str()); + + if requests.is_empty() { + log::info!("Skipping empty file: {filename:?}."); + continue; + } + + let response = server_client.check_multiple_and_join(requests).await?; + (response.into(), file_content) + }, + FileType::Typst | FileType::Markdown | FileType::Html => { + let data = match file_type { + FileType::Typst => parse_typst(&file_content), + FileType::Html => parse_html(&file_content), + FileType::Markdown => parse_markdown(&file_content), + _ => unreachable!(), + }; + let requests = (request.clone().with_data(data)) + .split(self.max_length, self.split_pattern.as_str()); + let response = server_client + .check_multiple_and_join_without_context(requests) + .await?; + (response, file_content) + }, + }; + + if !self.raw { + writeln!( + &mut stdout, + "{}", + &response.annotate(&text, filename.to_str(), color) + )?; + } else { + writeln!(&mut stdout, "{}", serde_json::to_string_pretty(&response)?)?; + } + } + + Ok(()) + } +} + +// NOTE: The below structs are copied from `../api/check.rs` to avoid lifetime +// issues with `clap` TODO: Remove these once this upstream issue is resolved: +// ------------------------------------------------------------------------------------------------- + +/// LanguageTool POST check request. +/// +/// The main feature - check a text with LanguageTool for possible style and +/// grammar issues. +/// +/// The structure below tries to follow as closely as possible the JSON API +/// described [here](https://languagetool.org/http-api/swagger-ui/#!/default/post_check). +#[derive(Args, Clone, Debug, Default, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub struct CliRequest { + /// The text to be checked. This or 'data' is required. + #[clap(short = 't', long, conflicts_with = "data", allow_hyphen_values(true))] + pub text: Option, + /// The text to be checked, given as a JSON document that specifies what's + /// text and what's markup. This or 'text' is required. + /// + /// Markup will be ignored when looking for errors. Example text: + /// ```html + /// A test + /// ``` + /// JSON for the example text: + /// ```json + /// {"annotation":[ + /// {"text": "A "}, + /// {"markup": ""}, + /// {"text": "test"}, + /// {"markup": ""} + /// ]} + /// ``` + /// If you have markup that should be interpreted as whitespace, like `

` + /// in HTML, you can have it interpreted like this: + /// + /// ```json + /// {"markup": "

", "interpretAs": "\n\n"} + /// ``` + /// The 'data' feature is not limited to HTML or XML, it can be used for any + /// kind of markup. Entities will need to be expanded in this input. + #[clap(short = 'd', long, conflicts_with = "text")] + pub data: Option, + /// A language code like `en-US`, `de-DE`, `fr`, or `auto` to guess the + /// language automatically (see `preferredVariants` below). + /// + /// For languages with variants (English, German, Portuguese) spell checking + /// will only be activated when you specify the variant, e.g. `en-GB` + /// instead of just `en`. + #[cfg_attr( + feature = "cli", + clap( + short = 'l', + long, + default_value = DEFAULT_LANGUAGE, + value_parser = parse_language_code + ) + )] + pub language: String, + /// Set to get Premium API access: Your username/email as used to log in at + /// languagetool.org. + #[cfg_attr( + feature = "cli", + clap(short = 'u', long, requires = "api_key", env = "LANGUAGETOOL_USERNAME") + )] + pub username: Option, + /// Set to get Premium API access: your API key (see ). + #[cfg_attr( + feature = "cli", + clap(short = 'k', long, requires = "username", env = "LANGUAGETOOL_API_KEY") + )] + pub api_key: Option, + /// Comma-separated list of dictionaries to include words from; uses special + /// default dictionary if this is unset. + #[cfg_attr(feature = "cli", clap(long))] + pub dicts: Option>, + /// A language code of the user's native language, enabling false friends + /// checks for some language pairs. + #[cfg_attr(feature = "cli", clap(long))] + pub mother_tongue: Option, + /// Comma-separated list of preferred language variants. + /// + /// The language detector used with `language=auto` can detect e.g. English, + /// but it cannot decide whether British English or American English is + /// used. Thus this parameter can be used to specify the preferred variants + /// like `en-GB` and `de-AT`. Only available with `language=auto`. You + /// should set variants for at least German and English, as otherwise the + /// spell checking will not work for those, as no spelling dictionary can be + /// selected for just `en` or `de`. + #[cfg_attr(feature = "cli", clap(long, conflicts_with = "language"))] + pub preferred_variants: Option>, + /// IDs of rules to be enabled, comma-separated. + #[cfg_attr(feature = "cli", clap(long))] + pub enabled_rules: Option>, + /// IDs of rules to be disabled, comma-separated. + #[cfg_attr(feature = "cli", clap(long))] + pub disabled_rules: Option>, + /// IDs of categories to be enabled, comma-separated. + #[cfg_attr(feature = "cli", clap(long))] + pub enabled_categories: Option>, + /// IDs of categories to be disabled, comma-separated. + #[cfg_attr(feature = "cli", clap(long))] + pub disabled_categories: Option>, + /// If true, only the rules and categories whose IDs are specified with + /// `enabledRules` or `enabledCategories` are enabled. + #[cfg_attr(feature = "cli", clap(long))] + pub enabled_only: bool, + /// If set to `picky`, additional rules will be activated, i.e. rules that + /// you might only find useful when checking formal text. + #[cfg_attr( + feature = "cli", + clap(long, default_value = "default", ignore_case = true, value_enum) + )] + pub level: Level, +} + +impl From for Request<'_> { + fn from(val: CliRequest) -> Self { + Request { + text: val.text.map(Cow::Owned), + data: val.data.map(Into::into), + language: val.language, + username: val.username, + api_key: val.api_key, + dicts: val.dicts, + mother_tongue: val.mother_tongue, + preferred_variants: val.preferred_variants, + enabled_rules: val.enabled_rules, + disabled_rules: val.disabled_rules, + enabled_categories: val.enabled_categories, + disabled_categories: val.disabled_categories, + enabled_only: val.enabled_only, + level: val.level, + } + } +} + +/// Alternative text to be checked. +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub struct CliData { + /// Vector of markup text, see [`DataAnnotation`]. + pub annotation: Vec, +} + +impl From for Data<'_> { + fn from(val: CliData) -> Self { + Data { + annotation: val + .annotation + .into_iter() + .map(|a| a.into()) + .collect::>(), + } + } +} + +/// A portion of text to be checked. +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, Hash)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct CliDataAnnotation { + /// Text that should be treated as normal text. + /// + /// This or `markup` is required. + #[serde(skip_serializing_if = "Option::is_none")] + pub text: Option, + /// Text that should be treated as markup. + /// + /// This or `text` is required. + #[serde(skip_serializing_if = "Option::is_none")] + pub markup: Option, + /// If set, the markup will be interpreted as this. + #[serde(skip_serializing_if = "Option::is_none")] + pub interpret_as: Option, +} + +impl From for DataAnnotation<'_> { + fn from(val: CliDataAnnotation) -> Self { + DataAnnotation { + text: val.text.map(Cow::Owned), + markup: val.markup.map(Cow::Owned), + interpret_as: val.interpret_as.map(Cow::Owned), + } + } +} + +impl std::str::FromStr for CliData { + type Err = Error; + + fn from_str(s: &str) -> Result { + let v: Self = serde_json::from_str(s)?; + Ok(v) + } +} diff --git a/src/cli/completions.rs b/src/cli/completions.rs new file mode 100644 index 0000000..5f9d2ec --- /dev/null +++ b/src/cli/completions.rs @@ -0,0 +1,115 @@ +//! Completion scripts generation with [`clap_complete`]. + +use crate::{api::server::ServerClient, error::Result}; +use clap::Parser; +use clap_complete::{generate, shells::Shell}; +use std::io::Write; +use termcolor::StandardStream; + +use super::ExecuteSubcommand; + +/// Command structure to generate complete scripts. +#[derive(Debug, Parser)] +#[command( + about = "Generate tab-completion scripts for supported shells", + after_help = "Use --help for installation help.", + after_long_help = COMPLETIONS_HELP +)] +pub struct Command { + /// Shell for which to completion script is generated. + #[arg(value_enum, ignore_case = true)] + shell: Shell, +} + +impl Command { + /// Generate completion file for current shell and write to buffer. + pub fn generate_completion_file(&self, build_cli: F, buffer: &mut W) + where + F: FnOnce() -> clap::Command, + W: Write, + { + generate(self.shell, &mut build_cli(), "ltrs", buffer); + } +} + +impl ExecuteSubcommand for Command { + /// Executes the `completions` subcommand. + async fn execute(self, mut stdout: StandardStream, _: ServerClient) -> Result<()> { + self.generate_completion_file(super::build_cli, &mut stdout); + Ok(()) + } +} + +pub(crate) static COMPLETIONS_HELP: &str = r"DISCUSSION: + Enable tab completion for Bash, Fish, Zsh, or PowerShell + Elvish shell completion is currently supported, but not documented below. + The script is output on `stdout`, allowing one to re-direct the + output to the file of their choosing. Where you place the file + will depend on which shell, and which operating system you are + using. Your particular configuration may also determine where + these scripts need to be placed. + Here are some common set ups for the three supported shells under + Unix and similar operating systems (such as GNU/Linux). + BASH: + Completion files are commonly stored in `/etc/bash_completion.d/` for + system-wide commands, but can be stored in + `~/.local/share/bash-completion/completions` for user-specific commands. + Run the command: + $ mkdir -p ~/.local/share/bash-completion/completions + $ ltrs completions bash >> ~/.local/share/bash-completion/completions/ltrs + This installs the completion script. You may have to log out and + log back in to your shell session for the changes to take effect. + BASH (macOS/Homebrew): + Homebrew stores bash completion files within the Homebrew directory. + With the `bash-completion` brew formula installed, run the command: + $ mkdir -p $(brew --prefix)/etc/bash_completion.d + $ ltrs completions bash > $(brew --prefix)/etc/bash_completion.d/ltrs.bash-completion + FISH: + Fish completion files are commonly stored in + `$HOME/.config/fish/completions`. Run the command: + $ mkdir -p ~/.config/fish/completions + $ ltrs completions fish > ~/.config/fish/completions/ltrs.fish + This installs the completion script. You may have to log out and + log back in to your shell session for the changes to take effect. + ZSH: + ZSH completions are commonly stored in any directory listed in + your `$fpath` variable. To use these completions, you must either + add the generated script to one of those directories, or add your + own to this list. + Adding a custom directory is often the safest bet if you are + unsure of which directory to use. First create the directory; for + this example we'll create a hidden directory inside our `$HOME` + directory: + $ mkdir ~/.zfunc + Then add the following lines to your `.zshrc` just before + `compinit`: + fpath+=~/.zfunc + Now you can install the completions script using the following + command: + $ ltrs completions zsh > ~/.zfunc/_ltrs + You must then either log out and log back in, or simply run + $ exec zsh + for the new completions to take effect. + CUSTOM LOCATIONS: + Alternatively, you could save these files to the place of your + choosing, such as a custom directory inside your $HOME. Doing so + will require you to add the proper directives, such as `source`ing + inside your login script. Consult your shells documentation for + how to add such directives. + POWERSHELL: + The powershell completion scripts require PowerShell v5.0+ (which + comes with Windows 10, but can be downloaded separately for windows 7 + or 8.1). + First, check if a profile has already been set + PS C:\> Test-Path $profile + If the above command returns `False` run the following + PS C:\> New-Item -path $profile -type file -force + Now open the file provided by `$profile` (if you used the + `New-Item` command it will be + `${env:USERPROFILE}\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1` + Next, we either save the completions file into our profile, or + into a separate file and source it inside our profile. To save the + completions into our profile simply use + PS C:\> ltrs completions powershell >> ${env:USERPROFILE}\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1 + SOURCE: + This documentation is directly taken from: https://github.com/rust-lang/rustup/blob/8f6b53628ad996ad86f9c6225fa500cddf860905/src/cli/help.rs#L157"; diff --git a/src/lib/docker.rs b/src/cli/docker.rs similarity index 70% rename from src/lib/docker.rs rename to src/cli/docker.rs index e467340..eaf3a47 100644 --- a/src/lib/docker.rs +++ b/src/cli/docker.rs @@ -1,59 +1,52 @@ //! Structures and methods to easily manipulate Docker images, especially for //! LanguageTool applications. -use crate::error::{exit_status_error, Error, Result}; -#[cfg(feature = "cli")] +use std::process::{self, Output, Stdio}; + use clap::{Args, Parser}; -use std::process::{Command, Output, Stdio}; +use termcolor::StandardStream; + +use crate::{ + api::server::ServerClient, + error::{exit_status_error, Error, Result}, +}; + +use super::ExecuteSubcommand; /// Commands to pull, start and stop a `LanguageTool` container using Docker. -#[cfg_attr(feature = "cli", derive(Args))] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Args)] pub struct Docker { /// Image or repository from a registry. - #[cfg_attr( - feature = "cli", - clap( - default_value = "erikvl87/languagetool", - env = "LANGUAGETOOL_DOCKER_IMAGE" - ) + #[clap( + default_value = "erikvl87/languagetool", + env = "LANGUAGETOOL_DOCKER_IMAGE" )] name: String, /// Path to Docker's binaries. - #[cfg_attr( - feature = "cli", - clap( - short = 'b', - long, - default_value = "docker", - env = "LANGUAGETOOL_DOCKER_BIN" - ) + #[clap( + short = 'b', + long, + default_value = "docker", + env = "LANGUAGETOOL_DOCKER_BIN" )] bin: String, /// Name assigned to the container. - #[cfg_attr( - feature = "cli", - clap(long, default_value = "languagetool", env = "LANGUAGETOOL_DOCKER_NAME") - )] + #[clap(long, default_value = "languagetool", env = "LANGUAGETOOL_DOCKER_NAME")] container_name: String, /// Publish a container's port(s) to the host. - #[cfg_attr( - feature = "cli", - clap( - short = 'p', - long, - default_value = "8010:8010", - env = "LANGUAGETOOL_DOCKER_PORT" - ) + #[clap( + short = 'p', + long, + default_value = "8010:8010", + env = "LANGUAGETOOL_DOCKER_PORT" )] port: String, /// Docker action. - #[cfg_attr(feature = "cli", clap(subcommand))] + #[clap(subcommand)] action: Action, } -#[cfg_attr(feature = "cli", derive(clap::Subcommand))] -#[derive(Clone, Debug)] +#[derive(clap::Subcommand, Clone, Debug)] /// Enumerate supported Docker actions. enum Action { /// Pull a docker docker image. @@ -74,7 +67,7 @@ enum Action { impl Docker { /// Pull a Docker image from the given repository/file/... pub fn pull(&self) -> Result { - let output = Command::new(&self.bin) + let output = process::Command::new(&self.bin) .args(["pull", &self.name]) .stdout(Stdio::inherit()) .stderr(Stdio::inherit()) @@ -88,7 +81,7 @@ impl Docker { /// Start a Docker container with given specifications. pub fn start(&self) -> Result { - let output = Command::new(&self.bin) + let output = process::Command::new(&self.bin) .args([ "run", "--rm", @@ -111,7 +104,7 @@ impl Docker { /// Stop the latest Docker container with the given name. pub fn stop(&self) -> Result { - let output = Command::new(&self.bin) + let output = process::Command::new(&self.bin) .args([ "ps", "-l", @@ -130,7 +123,7 @@ impl Docker { .filter(|c| c.is_alphanumeric()) // This avoids newlines .collect(); - let output = Command::new(&self.bin) + let output = process::Command::new(&self.bin) .args(["kill", &docker_id]) .stdout(Stdio::inherit()) .stderr(Stdio::inherit()) @@ -152,21 +145,16 @@ impl Docker { } /// Commands to easily run a LanguageTool server with Docker. -#[cfg(feature = "cli")] #[derive(Debug, Parser)] -pub struct DockerCommand { +pub struct Command { /// Actual command arguments. #[command(flatten)] pub docker: Docker, } -#[cfg(feature = "cli")] -impl DockerCommand { - /// Execute a Docker command and write output to stdout. - pub fn execute(&self, _stdout: &mut W) -> Result<()> - where - W: std::io::Write, - { +impl ExecuteSubcommand for Command { + /// Execute the `docker` subcommand. + async fn execute(self, _stdout: StandardStream, _: ServerClient) -> Result<()> { self.docker.run_action()?; Ok(()) } diff --git a/src/cli/languages.rs b/src/cli/languages.rs new file mode 100644 index 0000000..a8d2a13 --- /dev/null +++ b/src/cli/languages.rs @@ -0,0 +1,21 @@ +use clap::Parser; +use std::io::Write; +use termcolor::StandardStream; + +use crate::{api::server::ServerClient, error::Result}; + +use super::ExecuteSubcommand; + +#[derive(Debug, Parser)] +pub struct Command {} + +impl ExecuteSubcommand for Command { + /// Executes the `languages` subcommand. + async fn execute(self, mut stdout: StandardStream, server_client: ServerClient) -> Result<()> { + let languages_response = server_client.languages().await?; + let languages = serde_json::to_string_pretty(&languages_response)?; + + writeln!(&mut stdout, "{languages}")?; + Ok(()) + } +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs new file mode 100644 index 0000000..9c83368 --- /dev/null +++ b/src/cli/mod.rs @@ -0,0 +1,125 @@ +//! Command line tools. +//! +//! This module is specifically designed to be used by LTRS's binary target. +//! It contains all the content needed to create LTRS's command line interface. + +pub mod check; +#[cfg(feature = "cli-complete")] +mod completions; +#[cfg(feature = "docker")] +mod docker; +mod languages; +mod ping; +mod words; + +use std::io; + +use clap::{CommandFactory, Parser, Subcommand}; +use enum_dispatch::enum_dispatch; +use is_terminal::IsTerminal; +#[cfg(feature = "annotate")] +use termcolor::{ColorChoice, StandardStream}; + +#[cfg(feature = "docker")] +pub use docker::Docker; + +use crate::{ + api::server::{ServerCli, ServerClient}, + error::Result, +}; + +/// Main command line structure. Contains every subcommand. +#[derive(Parser, Debug)] +#[command( + author, + version, + about = "LanguageTool API bindings in Rust.", + propagate_version(true), + subcommand_required(true), + verbatim_doc_comment +)] +pub struct Cli { + /// Specify WHEN to colorize output. + #[arg(short, long, value_name = "WHEN", default_value = "auto", default_missing_value = "always", num_args(0..=1), require_equals(true))] + pub color: clap::ColorChoice, + /// [`ServerCli`] arguments. + #[command(flatten, next_help_heading = "Server options")] + pub server_cli: ServerCli, + /// Subcommand. + #[command(subcommand)] + #[allow(missing_docs)] + pub command: Command, + #[command(flatten)] + #[allow(missing_docs)] + pub verbose: clap_verbosity_flag::Verbosity, +} + +/// All possible subcommands. +#[derive(Subcommand, Debug)] +#[enum_dispatch] +#[allow(missing_docs)] +pub enum Command { + /// Check text using LanguageTool server. + Check(check::Command), + /// Commands to easily run a LanguageTool server with Docker. + #[cfg(feature = "docker")] + Docker(docker::Command), + /// Return list of supported languages. + #[clap(visible_alias = "lang")] + Languages(languages::Command), + /// Ping the LanguageTool server and return time elapsed in ms if success. + Ping(ping::Command), + /// Retrieve some user's words list, or add / delete word from it. + Words(words::Command), + /// Generate tab-completion scripts for supported shells + #[cfg(feature = "cli-complete")] + Completions(completions::Command), +} + +/// Provides a common interface for executing the subcommands. +#[enum_dispatch(Command)] +trait ExecuteSubcommand { + /// Executes the subcommand. + async fn execute(self, stdout: StandardStream, server_client: ServerClient) -> Result<()>; +} + +impl Cli { + /// Return a standard output stream that optionally supports color. + #[must_use] + fn stdout(&self) -> StandardStream { + let mut choice: ColorChoice = match self.color { + clap::ColorChoice::Auto => ColorChoice::Auto, + clap::ColorChoice::Always => ColorChoice::Always, + clap::ColorChoice::Never => ColorChoice::Never, + }; + + if choice == ColorChoice::Auto && !io::stdout().is_terminal() { + choice = ColorChoice::Never; + } + + StandardStream::stdout(choice) + } + + /// Execute command, possibly returning an error. + pub async fn execute(self) -> Result<()> { + let stdout = self.stdout(); + let server_client: ServerClient = self.server_cli.into(); + + self.command.execute(stdout, server_client).await + } +} + +/// Build a command from the top-level command line structure. +#[must_use] +pub fn build_cli() -> clap::Command { + Cli::command() +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_cli() { + Cli::command().debug_assert(); + } +} diff --git a/src/cli/ping.rs b/src/cli/ping.rs new file mode 100644 index 0000000..d32d3a7 --- /dev/null +++ b/src/cli/ping.rs @@ -0,0 +1,20 @@ +use clap::Parser; +use std::io::Write; +use termcolor::StandardStream; + +use crate::{api::server::ServerClient, error::Result}; + +use super::ExecuteSubcommand; + +#[derive(Debug, Parser)] +pub struct Command {} + +impl ExecuteSubcommand for Command { + /// Execute the `languages` subcommand. + async fn execute(self, mut stdout: StandardStream, server_client: ServerClient) -> Result<()> { + let ping = server_client.ping().await?; + + writeln!(&mut stdout, "PONG! Delay: {ping} ms")?; + Ok(()) + } +} diff --git a/src/cli/words.rs b/src/cli/words.rs new file mode 100644 index 0000000..f87aa1b --- /dev/null +++ b/src/cli/words.rs @@ -0,0 +1,55 @@ +use clap::{Parser, Subcommand}; +use std::io::Write; +use termcolor::StandardStream; + +use crate::{ + api::{self, server::ServerClient, words::RequestArgs}, + error::Result, +}; + +use super::ExecuteSubcommand; + +/// Retrieve some user's words list. +#[derive(Debug, Parser)] +#[clap(args_conflicts_with_subcommands = true)] +#[clap(subcommand_negates_reqs = true)] +pub struct Command { + /// Actual GET request. + #[command(flatten)] + pub request: RequestArgs, + /// Optional subcommand. + #[command(subcommand)] + pub subcommand: Option, +} + +/// Words' optional subcommand. +#[derive(Clone, Debug, Subcommand)] +pub enum WordsSubcommand { + /// Add a word to some user's list. + Add(api::words::add::Request), + /// Remove a word from some user's list. + Delete(api::words::delete::Request), +} + +impl ExecuteSubcommand for Command { + /// Executes the `words` subcommand. + async fn execute(self, mut stdout: StandardStream, server_client: ServerClient) -> Result<()> { + let words = match self.subcommand { + Some(WordsSubcommand::Add(request)) => { + let words_response = server_client.words_add(&request).await?; + serde_json::to_string_pretty(&words_response)? + }, + Some(WordsSubcommand::Delete(request)) => { + let words_response = server_client.words_delete(&request).await?; + serde_json::to_string_pretty(&words_response)? + }, + None => { + let words_response = server_client.words(&self.request.into()).await?; + serde_json::to_string_pretty(&words_response)? + }, + }; + + writeln!(&mut stdout, "{words}")?; + Ok(()) + } +} diff --git a/src/lib/error.rs b/src/error.rs similarity index 71% rename from src/lib/error.rs rename to src/error.rs index 8ac3572..2af3644 100644 --- a/src/lib/error.rs +++ b/src/error.rs @@ -1,4 +1,5 @@ //! Error and Result structure used all across this crate. + use std::process::ExitStatus; /// Enumeration of all possible error types. @@ -18,11 +19,11 @@ pub enum Error { ExitStatus(String), /// Error specifying an invalid - /// [`DataAnnotation`](`crate::check::DataAnnotation`). + /// [`DataAnnotation`](`crate::api::check::DataAnnotation`). #[error("invalid request: {0}")] InvalidDataAnnotation(String), - /// Error from checking if `filename` exists and is a actualla a file. + /// Error from checking if `filename` exists and is a actually a file. #[error("invalid filename (got '{0}', does not exist or is not a file)")] InvalidFilename(String), @@ -51,18 +52,10 @@ pub enum Error { #[error("could not parse {0:?} in a Docker action")] ParseAction(String), - /// Error from request encoding. - #[error("request could not be properly encoded: {0}")] - RequestEncode(reqwest::Error), - /// Any other error from requests (see [`reqwest::Error`]). #[error(transparent)] Reqwest(#[from] reqwest::Error), - /// Error from request decoding. - #[error("response could not be properly decoded: {0}")] - ResponseDecode(reqwest::Error), - /// Error from reading environ variable (see [`std::env::VarError`]). #[error(transparent)] VarError(#[from] std::env::VarError), @@ -94,6 +87,7 @@ pub(crate) fn exit_status_error(exit_status: &ExitStatus) -> Result<()> { #[cfg(test)] mod tests { + use assert_matches::assert_matches; use crate::error::Error; #[cfg(feature = "cli")] @@ -108,7 +102,7 @@ mod tests { let error: Error = result.unwrap_err().into(); - assert!(matches!(error, Error::Cli(_))); + assert_matches!(error, Error::Cli(_)); } #[test] @@ -118,7 +112,7 @@ mod tests { let error: Error = result.unwrap_err().into(); - assert!(matches!(error, Error::JSON(_))); + assert_matches!(error, Error::JSON(_)); } #[test] @@ -128,61 +122,34 @@ mod tests { let error: Error = result.unwrap_err().into(); - assert!(matches!(error, Error::IO(_))); + assert_matches!(error, Error::IO(_)); } - #[ignore] #[test] fn test_error_invalid_request() { - let result = std::fs::read_to_string(""); // TODO + let result = crate::api::check::Request::new().try_get_text(); assert!(result.is_err()); let error: Error = result.unwrap_err().into(); - assert!(matches!(error, Error::InvalidRequest(_))); + assert_matches!(error, Error::InvalidRequest(_)); } - #[ignore] #[test] fn test_error_invalid_value() { - let result = std::fs::read_to_string(""); // TODO - assert!(result.is_err()); - - let error: Error = result.unwrap_err().into(); - - assert!(matches!(error, Error::InvalidValue(_))); - } - - #[ignore] - #[test] - fn test_error_request_encode() { - let result = std::fs::read_to_string(""); // TODO + let result = crate::api::server::parse_port("test"); assert!(result.is_err()); let error: Error = result.unwrap_err().into(); - assert!(matches!(error, Error::RequestEncode(_))); + assert_matches!(error, Error::InvalidValue(_)); } - #[ignore] - #[test] - fn test_error_response_decode() { - let result = std::fs::read_to_string(""); // TODO - assert!(result.is_err()); - - let error: Error = result.unwrap_err().into(); - - assert!(matches!(error, Error::ResponseDecode(_))); - } - - #[ignore] - #[test] - fn test_error_reqwest() { - let result = std::fs::read_to_string(""); // TODO - assert!(result.is_err()); - + #[tokio::test] + async fn test_error_reqwest() { + let result = reqwest::get("").await; let error: Error = result.unwrap_err().into(); - assert!(matches!(error, Error::Reqwest(_))); + assert_matches!(error, Error::Reqwest(_)); } } diff --git a/src/lib/lib.rs b/src/lib.rs similarity index 56% rename from src/lib/lib.rs rename to src/lib.rs index ed743c3..a7cde7a 100644 --- a/src/lib/lib.rs +++ b/src/lib.rs @@ -3,7 +3,7 @@ #![warn(clippy::must_use_candidate)] #![allow(clippy::doc_markdown, clippy::module_name_repetitions)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] -#![doc = include_str!("../../README.md")] +#![doc = include_str!("../README.md")] //! //! ## Note //! @@ -17,24 +17,8 @@ //! that cannot be controlled and (possible) breaking changes are to be //! expected. -pub mod check; +pub mod api; #[cfg(feature = "cli")] pub mod cli; -#[cfg(feature = "docker")] -pub mod docker; pub mod error; -pub mod languages; -pub mod server; -pub mod words; - -#[cfg(feature = "docker")] -pub use crate::docker::Docker; -pub use crate::{ - check::{CheckRequest, CheckResponse}, - languages::LanguagesResponse, - server::ServerClient, - words::{ - WordsAddRequest, WordsAddResponse, WordsDeleteRequest, WordsDeleteResponse, WordsRequest, - WordsResponse, - }, -}; +pub mod parsers; diff --git a/src/lib/cli.rs b/src/lib/cli.rs deleted file mode 100644 index 03e24e0..0000000 --- a/src/lib/cli.rs +++ /dev/null @@ -1,339 +0,0 @@ -//! Command line tools. -//! -//! This module is specifically designed to be used by LTRS's binary target. -//! It contains all the content needed to create LTRS's command line interface. - -use crate::{ - check::CheckResponseWithContext, - error::Result, - server::{ServerCli, ServerClient}, - words::WordsSubcommand, -}; -use clap::{CommandFactory, Parser, Subcommand}; -use is_terminal::IsTerminal; -use std::io::{self, Write}; -#[cfg(feature = "annotate")] -use termcolor::WriteColor; -use termcolor::{ColorChoice, StandardStream}; - -/// Read lines from standard input and write to buffer string. -/// -/// Standard output is used when waiting for user to input text. -fn read_from_stdin(stdout: &mut W, buffer: &mut String) -> Result<()> -where - W: io::Write, -{ - if io::stdin().is_terminal() { - #[cfg(windows)] - writeln!( - stdout, - "Reading from STDIN, press [CTRL+Z] when you're done." - )?; - - #[cfg(unix)] - writeln!( - stdout, - "Reading from STDIN, press [CTRL+D] when you're done." - )?; - } - let stdin = std::io::stdin(); - - while stdin.read_line(buffer)? > 0 {} - Ok(()) -} - -/// Main command line structure. Contains every subcommand. -#[derive(Parser, Debug)] -#[command( - author, - version, - about = "LanguageTool API bindings in Rust.", - propagate_version(true), - subcommand_required(true), - verbatim_doc_comment -)] -pub struct Cli { - /// Specify WHEN to colorize output. - #[arg(short, long, value_name = "WHEN", default_value = "auto", default_missing_value = "always", num_args(0..=1), require_equals(true))] - pub color: clap::ColorChoice, - /// [`ServerCli`] arguments. - #[command(flatten)] - pub server_cli: ServerCli, - /// Subcommand. - #[command(subcommand)] - #[allow(missing_docs)] - pub command: Command, -} - -/// Enumerate all possible commands. -#[derive(Subcommand, Debug)] -#[allow(missing_docs)] -pub enum Command { - /// Check text using LanguageTool server. - Check(crate::check::CheckCommand), - /// Commands to easily run a LanguageTool server with Docker. - #[cfg(feature = "docker")] - Docker(crate::docker::DockerCommand), - /// Return list of supported languages. - #[clap(visible_alias = "lang")] - Languages, - /// Ping the LanguageTool server and return time elapsed in ms if success. - Ping, - /// Retrieve some user's words list, or add / delete word from it. - Words(crate::words::WordsCommand), - /// Generate tab-completion scripts for supported shells - #[cfg(feature = "cli-complete")] - Completions(complete::CompleteCommand), -} - -impl Cli { - /// Return a standard output stream that optionally supports color. - #[must_use] - fn stdout(&self) -> StandardStream { - let mut choice: ColorChoice = match self.color { - clap::ColorChoice::Auto => ColorChoice::Auto, - clap::ColorChoice::Always => ColorChoice::Always, - clap::ColorChoice::Never => ColorChoice::Never, - }; - - if choice == ColorChoice::Auto && !io::stdout().is_terminal() { - choice = ColorChoice::Never; - } - - StandardStream::stdout(choice) - } - - /// Execute command, possibly returning an error. - pub async fn execute(self) -> Result<()> { - let mut stdout = self.stdout(); - - let server_client: ServerClient = self.server_cli.into(); - - match self.command { - Command::Check(cmd) => { - let mut request = cmd.request; - #[cfg(feature = "annotate")] - let color = stdout.supports_color(); - - let server_client = server_client.with_max_suggestions(cmd.max_suggestions); - - if cmd.filenames.is_empty() { - if request.text.is_none() && request.data.is_none() { - let mut text = String::new(); - read_from_stdin(&mut stdout, &mut text)?; - request = request.with_text(text); - } - - let mut response = if request.text.is_some() { - let requests = request.split(cmd.max_length, cmd.split_pattern.as_str()); - server_client.check_multiple_and_join(requests).await? - } else { - server_client.check(&request).await? - }; - - if request.text.is_some() && !cmd.raw { - let text = request.text.unwrap(); - response = CheckResponseWithContext::new(text.clone(), response).into(); - writeln!( - &mut stdout, - "{}", - &response.annotate(text.as_str(), None, color) - )?; - } else { - writeln!(&mut stdout, "{}", serde_json::to_string_pretty(&response)?)?; - } - - return Ok(()); - } - - for filename in cmd.filenames.iter() { - let text = std::fs::read_to_string(filename)?; - let requests = request - .clone() - .with_text(text.clone()) - .split(cmd.max_length, cmd.split_pattern.as_str()); - let response = server_client.check_multiple_and_join(requests).await?; - - if !cmd.raw { - writeln!( - &mut stdout, - "{}", - &response.annotate(text.as_str(), filename.to_str(), color) - )?; - } else { - writeln!(&mut stdout, "{}", serde_json::to_string_pretty(&response)?)?; - } - } - }, - #[cfg(feature = "docker")] - Command::Docker(cmd) => { - cmd.execute(&mut stdout)?; - }, - Command::Languages => { - let languages_response = server_client.languages().await?; - let languages = serde_json::to_string_pretty(&languages_response)?; - - writeln!(&mut stdout, "{languages}")?; - }, - Command::Ping => { - let ping = server_client.ping().await?; - writeln!(&mut stdout, "PONG! Delay: {ping} ms")?; - }, - Command::Words(cmd) => { - let words = match &cmd.subcommand { - Some(WordsSubcommand::Add(request)) => { - let words_response = server_client.words_add(request).await?; - serde_json::to_string_pretty(&words_response)? - }, - Some(WordsSubcommand::Delete(request)) => { - let words_response = server_client.words_delete(request).await?; - serde_json::to_string_pretty(&words_response)? - }, - None => { - let words_response = server_client.words(&cmd.request.into()).await?; - serde_json::to_string_pretty(&words_response)? - }, - }; - - writeln!(&mut stdout, "{words}")?; - }, - #[cfg(feature = "cli-complete")] - Command::Completions(cmd) => { - cmd.execute(&mut stdout)?; - }, - } - Ok(()) - } -} - -/// Build a command from the top-level command line structure. -#[must_use] -pub fn build_cli() -> clap::Command { - Cli::command() -} - -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn test_cli() { - Cli::command().debug_assert(); - } -} - -#[cfg(feature = "cli-complete")] -pub(crate) mod complete { - //! Completion scripts generation with [`clap_complete`]. - - use crate::error::Result; - use clap::{Command, Parser}; - use clap_complete::{generate, shells::Shell}; - use std::io::Write; - - /// Command structure to generate complete scripts. - #[derive(Debug, Parser)] - #[command( - about = "Generate tab-completion scripts for supported shells", - after_help = "Use --help for installation help.", - after_long_help = COMPLETIONS_HELP -)] - pub struct CompleteCommand { - /// Shell for which to completion script is generated. - #[arg(value_enum, ignore_case = true)] - shell: Shell, - } - - impl CompleteCommand { - /// Generate completion file for current shell and write to buffer. - pub fn generate_completion_file(&self, build_cli: F, buffer: &mut W) - where - F: FnOnce() -> Command, - W: Write, - { - generate(self.shell, &mut build_cli(), "ltrs", buffer); - } - - /// Execute command by writing completion script to stdout. - pub fn execute(&self, stdout: &mut W) -> Result<()> - where - W: Write, - { - self.generate_completion_file(super::build_cli, stdout); - Ok(()) - } - } - - pub(crate) static COMPLETIONS_HELP: &str = r"DISCUSSION: - Enable tab completion for Bash, Fish, Zsh, or PowerShell - Elvish shell completion is currently supported, but not documented below. - The script is output on `stdout`, allowing one to re-direct the - output to the file of their choosing. Where you place the file - will depend on which shell, and which operating system you are - using. Your particular configuration may also determine where - these scripts need to be placed. - Here are some common set ups for the three supported shells under - Unix and similar operating systems (such as GNU/Linux). - BASH: - Completion files are commonly stored in `/etc/bash_completion.d/` for - system-wide commands, but can be stored in - `~/.local/share/bash-completion/completions` for user-specific commands. - Run the command: - $ mkdir -p ~/.local/share/bash-completion/completions - $ ltrs completions bash >> ~/.local/share/bash-completion/completions/ltrs - This installs the completion script. You may have to log out and - log back in to your shell session for the changes to take effect. - BASH (macOS/Homebrew): - Homebrew stores bash completion files within the Homebrew directory. - With the `bash-completion` brew formula installed, run the command: - $ mkdir -p $(brew --prefix)/etc/bash_completion.d - $ ltrs completions bash > $(brew --prefix)/etc/bash_completion.d/ltrs.bash-completion - FISH: - Fish completion files are commonly stored in - `$HOME/.config/fish/completions`. Run the command: - $ mkdir -p ~/.config/fish/completions - $ ltrs completions fish > ~/.config/fish/completions/ltrs.fish - This installs the completion script. You may have to log out and - log back in to your shell session for the changes to take effect. - ZSH: - ZSH completions are commonly stored in any directory listed in - your `$fpath` variable. To use these completions, you must either - add the generated script to one of those directories, or add your - own to this list. - Adding a custom directory is often the safest bet if you are - unsure of which directory to use. First create the directory; for - this example we'll create a hidden directory inside our `$HOME` - directory: - $ mkdir ~/.zfunc - Then add the following lines to your `.zshrc` just before - `compinit`: - fpath+=~/.zfunc - Now you can install the completions script using the following - command: - $ ltrs completions zsh > ~/.zfunc/_ltrs - You must then either log out and log back in, or simply run - $ exec zsh - for the new completions to take effect. - CUSTOM LOCATIONS: - Alternatively, you could save these files to the place of your - choosing, such as a custom directory inside your $HOME. Doing so - will require you to add the proper directives, such as `source`ing - inside your login script. Consult your shells documentation for - how to add such directives. - POWERSHELL: - The powershell completion scripts require PowerShell v5.0+ (which - comes with Windows 10, but can be downloaded separately for windows 7 - or 8.1). - First, check if a profile has already been set - PS C:\> Test-Path $profile - If the above command returns `False` run the following - PS C:\> New-Item -path $profile -type file -force - Now open the file provided by `$profile` (if you used the - `New-Item` command it will be - `${env:USERPROFILE}\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1` - Next, we either save the completions file into our profile, or - into a separate file and source it inside our profile. To save the - completions into our profile simply use - PS C:\> ltrs completions powershell >> ${env:USERPROFILE}\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1 - SOURCE: - This documentation is directly taken from: https://github.com/rust-lang/rustup/blob/8f6b53628ad996ad86f9c6225fa500cddf860905/src/cli/help.rs#L157"; -} diff --git a/src/lib/words.rs b/src/lib/words.rs deleted file mode 100644 index 659a528..0000000 --- a/src/lib/words.rs +++ /dev/null @@ -1,208 +0,0 @@ -//! Structures for `words` requests and responses. - -use crate::{ - check::serialize_option_vec_string, - error::{Error, Result}, -}; -#[cfg(feature = "cli")] -use clap::{Args, Parser, Subcommand}; -use serde::{Deserialize, Serialize}; - -/// Parse `v` if valid word. -/// -/// A valid word is any string slice that does not contain any whitespace -/// -/// # Examples -/// -/// ``` -/// # use languagetool_rust::words::parse_word; -/// assert!(parse_word("word").is_ok()); -/// -/// assert!(parse_word("some words").is_err()); -/// ``` -pub fn parse_word(v: &str) -> Result { - if !v.contains(' ') { - return Ok(v.to_string()); - } - Err(Error::InvalidValue( - "The value should be a word that does not contain any whitespace".to_string(), - )) -} - -/// Login arguments required by the API. -#[cfg_attr(feature = "cli", derive(Args))] -#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize, Hash)] -#[serde(rename_all = "camelCase")] -#[non_exhaustive] -pub struct LoginArgs { - /// Your username as used to log in at languagetool.org. - #[cfg_attr( - feature = "cli", - clap(short = 'u', long, required = true, env = "LANGUAGETOOL_USERNAME") - )] - pub username: String, - /// [Your API key](https://languagetool.org/editor/settings/api). - #[cfg_attr( - feature = "cli", - clap(short = 'k', long, required = true, env = "LANGUAGETOOL_API_KEY") - )] - pub api_key: String, -} - -/// LanguageTool GET words request. -/// -/// List words in the user's personal dictionaries. -#[cfg_attr(feature = "cli", derive(Args))] -#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize, Hash)] -#[non_exhaustive] -pub struct WordsRequest { - /// Offset of where to start in the list of words. - #[cfg_attr(feature = "cli", clap(long, default_value = "0"))] - offset: isize, - /// Maximum number of words to return. - #[cfg_attr(feature = "cli", clap(long, default_value = "10"))] - pub limit: isize, - /// Login arguments. - #[cfg_attr(feature = "cli", clap(flatten))] - #[serde(flatten)] - pub login: LoginArgs, - /// Comma-separated list of dictionaries to include words from; uses special - /// default dictionary if this is unset. - #[cfg_attr(feature = "cli", clap(long))] - #[serde(serialize_with = "serialize_option_vec_string")] - pub dicts: Option>, -} - -/// Copy of [`WordsRequest`], but used to CLI only. -/// -/// This is a temporary solution, until [#3165](https://github.com/clap-rs/clap/issues/3165) is -/// closed. -#[cfg(feature = "cli")] -#[derive(Args, Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] -#[non_exhaustive] -pub struct WordsRequestArgs { - /// Offset of where to start in the list of words. - #[cfg_attr(feature = "cli", clap(long, default_value = "0"))] - offset: isize, - /// Maximum number of words to return. - #[cfg_attr(feature = "cli", clap(long, default_value = "10"))] - pub limit: isize, - /// Login arguments. - #[cfg_attr(feature = "cli", clap(flatten))] - #[serde(flatten)] - pub login: Option, - /// Comma-separated list of dictionaries to include words from; uses special - /// default dictionary if this is unset. - #[cfg_attr(feature = "cli", clap(long))] - #[serde(serialize_with = "serialize_option_vec_string")] - pub dicts: Option>, -} - -#[cfg(feature = "cli")] -impl From for WordsRequest { - #[inline] - fn from(args: WordsRequestArgs) -> Self { - Self { - offset: args.offset, - limit: args.limit, - login: args.login.unwrap(), - dicts: args.dicts, - } - } -} - -/// LanguageTool POST words add request. -/// -/// Add a word to one of the user's personal dictionaries. Please note that this -/// feature is considered to be used for personal dictionaries which must not -/// contain more than 500 words. If this is an issue for you, please contact us. -#[cfg_attr(feature = "cli", derive(Args))] -#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize, Hash)] -#[non_exhaustive] -pub struct WordsAddRequest { - /// The word to be added. Must not be a phrase, i.e., cannot contain white - /// space. The word is added to a global dictionary that applies to all - /// languages. - #[cfg_attr(feature = "cli", clap(required = true, value_parser = parse_word))] - pub word: String, - /// Login arguments. - #[cfg_attr(feature = "cli", clap(flatten))] - #[serde(flatten)] - pub login: LoginArgs, - /// Name of the dictionary to add the word to; non-existent dictionaries are - /// created after calling this; if unset, adds to special default - /// dictionary. - #[cfg_attr(feature = "cli", clap(long))] - #[serde(skip_serializing_if = "Option::is_none")] - pub dict: Option, -} - -/// LanguageTool POST words delete request. -/// -/// Remove a word from one of the user's personal dictionaries. -#[cfg_attr(feature = "cli", derive(Args))] -#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize, Hash)] -#[non_exhaustive] -pub struct WordsDeleteRequest { - /// The word to be removed. - #[cfg_attr(feature = "cli", clap(required = true, value_parser = parse_word))] - pub word: String, - /// Login arguments. - #[cfg_attr(feature = "cli", clap(flatten))] - #[serde(flatten)] - pub login: LoginArgs, - /// Name of the dictionary to add the word to; non-existent dictionaries are - /// created after calling this; if unset, adds to special default - /// dictionary. - #[cfg_attr(feature = "cli", clap(long))] - #[serde(skip_serializing_if = "Option::is_none")] - pub dict: Option, -} - -/// Words' optional subcommand. -#[cfg(feature = "cli")] -#[derive(Clone, Debug, Subcommand)] -pub enum WordsSubcommand { - /// Add a word to some user's list. - Add(WordsAddRequest), - /// Remove a word from some user's list. - Delete(WordsDeleteRequest), -} - -/// Retrieve some user's words list. -#[cfg(feature = "cli")] -#[derive(Debug, Parser)] -#[clap(args_conflicts_with_subcommands = true)] -#[clap(subcommand_negates_reqs = true)] -pub struct WordsCommand { - /// Actual GET request. - #[command(flatten)] - pub request: WordsRequestArgs, - /// Optional subcommand. - #[command(subcommand)] - pub subcommand: Option, -} - -/// LanguageTool GET words response. -#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] -#[non_exhaustive] -pub struct WordsResponse { - /// List of words. - pub words: Vec, -} - -/// LanguageTool POST word add response. -#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] -#[non_exhaustive] -pub struct WordsAddResponse { - /// `true` if word was correctly added. - pub added: bool, -} - -/// LanguageTool POST word delete response. -#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] -#[non_exhaustive] -pub struct WordsDeleteResponse { - /// `true` if word was correctly removed. - pub deleted: bool, -} diff --git a/src/bin.rs b/src/main.rs similarity index 59% rename from src/bin.rs rename to src/main.rs index 9c628bb..307093f 100644 --- a/src/bin.rs +++ b/src/main.rs @@ -10,5 +10,9 @@ async fn main() { } async fn try_main() -> Result<()> { - Cli::parse().execute().await + let cli = Cli::parse(); + pretty_env_logger::formatted_builder() + .filter_level(cli.verbose.log_level_filter()) + .init(); + cli.execute().await } diff --git a/src/parsers/html.rs b/src/parsers/html.rs new file mode 100644 index 0000000..2230777 --- /dev/null +++ b/src/parsers/html.rs @@ -0,0 +1,105 @@ +//! Parse the contents of HTML files into a format parseable by the LanguageTool +//! API. + +use ego_tree::NodeRef; +use scraper::{Html, Node}; + +use crate::{ + api::check::{Data, DataAnnotation}, + parsers::IGNORE, +}; + +/// Parse the contents of an HTML file into a text format to be sent to the +/// LanguageTool API. +#[must_use] +pub fn parse_html(file_content: &str) -> Data<'static> { + let mut annotations: Vec = vec![]; + + fn handle_node(annotations: &mut Vec, node: NodeRef<'_, Node>) { + let n = node.value(); + match n { + Node::Element(el) => { + match el.name() { + "head" | "script" | "style" => {}, + + "code" => { + annotations.push(DataAnnotation::new_interpreted_markup( + "...", + IGNORE, + )); + }, + + "img" => { + annotations.push(DataAnnotation::new_interpreted_markup("", IGNORE)); + }, + + s => { + match s { + "p" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "li" | "td" | "th" + | "div" => { + annotations.push(DataAnnotation::new_interpreted_markup( + format!("<{s}>"), + "\n\n", + )); + for node in node.children() { + handle_node(annotations, node); + } + annotations.push(DataAnnotation::new_interpreted_markup( + format!(""), + "\n\n", + )); + }, + _ => { + annotations.push(DataAnnotation::new_markup(format!("<{s}>"))); + for node in node.children() { + handle_node(annotations, node); + } + annotations.push(DataAnnotation::new_markup(format!(""))); + }, + } + }, + } + }, + + Node::Text(t) => { + let mut text = t.trim().to_owned(); + if !text.is_empty() { + let mut chars = t.chars(); + + // Maintain leading/trailing white space, but only a single space + if chars.next().is_some_and(|c| c.is_whitespace()) { + while text.chars().last().is_some_and(|c| c.is_whitespace()) { + text.pop(); + } + text.insert(0, ' '); + } + if chars.last().is_some_and(|c| c.is_whitespace()) { + text.push(' '); + } + + annotations.push(DataAnnotation::new_text(text)) + } else { + annotations.push(DataAnnotation::new_text("\n\n")); + } + }, + + Node::Comment(c) => { + let comment = c.to_string(); + + annotations.push(DataAnnotation::new_interpreted_markup( + format!("",), + format!("\n\n{comment}\n\n"), + )); + }, + + _ => {}, + } + } + + let document = Html::parse_document(file_content); + for node in document.root_element().children() { + handle_node(&mut annotations, node); + } + + Data::from_iter(annotations) +} diff --git a/src/parsers/markdown.rs b/src/parsers/markdown.rs new file mode 100644 index 0000000..88ddd61 --- /dev/null +++ b/src/parsers/markdown.rs @@ -0,0 +1,151 @@ +//! Parse the contents of Markdown files into a format parseable by the +//! LanguageTool API. + +use crate::{ + api::check::{Data, DataAnnotation}, + parsers::IGNORE, +}; + +/// Parse the contents of a Markdown file into a text format to be sent to the +/// LanguageTool API. +#[must_use] +pub fn parse_markdown(file_content: &str) -> Data<'_> { + use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd}; + + let mut annotations: Vec = vec![]; + + // Stack to keep track of the current "tag" context + let mut tags = vec![]; + + Parser::new_ext(file_content, Options::all()).for_each(|event| { + match event { + Event::Start(tag) => { + match tag { + // Start list items + Tag::List(_) | Tag::Item => { + annotations.push(DataAnnotation::new_text("- ")); + }, + _ => {}, + } + + tags.push(tag); + }, + Event::End(tag) => { + match tag { + // Separate list items and table cells + TagEnd::List(_) | TagEnd::Item | TagEnd::TableRow | TagEnd::TableHead => { + annotations.push(DataAnnotation::new_text("\n")); + }, + TagEnd::TableCell => { + annotations.push(DataAnnotation::new_text(" | ")); + }, + _ => {}, + }; + + if tags + .last() + .is_some_and(|t| TagEnd::from(t.to_owned()) == tag) + { + tags.pop(); + }; + }, + + Event::Html(s) | Event::InlineHtml(s) => { + let data = super::html::parse_html(s.as_ref()).annotation.into_iter(); + annotations.extend(data); + }, + + Event::Text(mut s) => { + // Add space between sentences + if s.chars() + .last() + .is_some_and(|c| matches!(c, '.' | '!' | '?')) + { + s = pulldown_cmark::CowStr::from(s.to_string() + " "); + } + + let Some(tag) = tags.last() else { + annotations.push(DataAnnotation::new_text(s.to_owned())); + return; + }; + + match tag { + Tag::Heading { level, .. } => { + let s = format!("{s}\n"); + annotations.push(DataAnnotation::new_text(format!( + "{} {s}\n", + "#".repeat(*level as usize) + ))); + }, + + Tag::Emphasis => { + annotations + .push(DataAnnotation::new_interpreted_markup(format!("_{s}_"), s)) + }, + Tag::Strong => { + annotations.push(DataAnnotation::new_interpreted_markup( + format!("**{s}**"), + s, + )) + }, + Tag::Strikethrough => { + annotations + .push(DataAnnotation::new_interpreted_markup(format!("~{s}~"), s)) + }, + + Tag::Link { + title, dest_url, .. + } => { + annotations.push(DataAnnotation::new_interpreted_markup( + format!("[{title}]({dest_url})"), + title.to_string(), + )); + }, + + // No changes necessary + Tag::Paragraph + | Tag::List(_) + | Tag::Item + | Tag::BlockQuote + | Tag::TableCell => { + annotations.push(DataAnnotation::new_text(s)); + }, + + // Just markup + Tag::CodeBlock(_) | Tag::Image { .. } => { + annotations.push(DataAnnotation::new_markup(s)); + }, + _ => {}, + } + }, + Event::Code(s) => { + annotations.push(DataAnnotation::new_interpreted_markup(s, IGNORE)); + }, + + Event::HardBreak => { + annotations.push(DataAnnotation::new_text("\n\n")); + }, + Event::SoftBreak => { + if let Some(last) = annotations.last() { + // Don't add space if the last text already ends with a space + if last + .text + .as_ref() + .is_some_and(|t| t.chars().last().is_some_and(|c| c.is_ascii_whitespace())) + || last.interpret_as.as_ref().is_some_and(|t| { + t.chars().last().is_some_and(|c| c.is_ascii_whitespace()) + }) + { + return; + }; + } + + annotations.push(DataAnnotation::new_text(" ")); + }, + + Event::FootnoteReference(_) | Event::TaskListMarker(_) | Event::Rule => {}, + }; + }); + + Data::from_iter(annotations) +} diff --git a/src/parsers/mod.rs b/src/parsers/mod.rs new file mode 100644 index 0000000..bc4f51a --- /dev/null +++ b/src/parsers/mod.rs @@ -0,0 +1,14 @@ +//! Utilities for parsing the contents of different file types into a format +//! representation that can be parsed by the LanguageTool API. + +#![cfg(feature = "html")] +pub mod html; + +#[cfg(feature = "markdown")] +pub mod markdown; + +#[cfg(feature = "typst")] +pub mod typst; + +/// Pattern that is ignored by the LanguageTool API. +const IGNORE: &str = "_"; diff --git a/src/parsers/typst.rs b/src/parsers/typst.rs new file mode 100644 index 0000000..812bf24 --- /dev/null +++ b/src/parsers/typst.rs @@ -0,0 +1,101 @@ +//! Parse the contents of Typst files into a format parseable by the +//! LanguageTool API. + +use crate::api::check::{Data, DataAnnotation}; + +/// Parse the contents of a Typst file into a text format to be sent to the +/// LanguageTool API. +pub fn parse_typst(file_content: impl AsRef) -> Data<'static> { + use typst_syntax::{parse, SyntaxKind, SyntaxNode}; + + let mut annotations: Vec = vec![]; + + let parent = parse(file_content.as_ref()); + let mut nodes: Vec<&SyntaxNode> = parent.children().rev().collect(); + + while let Some(node) = nodes.pop() { + let kind = node.kind(); + + // MARKUP NODES + match kind { + // Pure markup + SyntaxKind::SetRule + | SyntaxKind::Ident + | SyntaxKind::ShowRule + | SyntaxKind::Raw + | SyntaxKind::Code + | SyntaxKind::CodeBlock + | SyntaxKind::Math + | SyntaxKind::Equation + | SyntaxKind::Ref + | SyntaxKind::LetBinding + | SyntaxKind::FieldAccess + | SyntaxKind::FuncCall + | SyntaxKind::Args => { + let mut markup = node.text().to_string(); + if markup.is_empty() { + let mut stack: Vec<&SyntaxNode> = node.children().rev().collect(); + while let Some(n) = stack.pop() { + if n.text().is_empty() { + stack.extend(n.children().rev()); + } else { + markup += n.text(); + } + } + } + + annotations.push(DataAnnotation::new_markup(markup)); + continue; + }, + // Markup with valid text interpretations + SyntaxKind::Heading + | SyntaxKind::Markup + | SyntaxKind::EnumItem + | SyntaxKind::ListItem + | SyntaxKind::Emph + | SyntaxKind::Strong => { + let (mut full_text, mut interpreted_as) = (String::new(), String::new()); + let mut stack: Vec<&SyntaxNode> = node.children().rev().collect(); + + while let Some(n) = stack.pop() { + if n.text().is_empty() { + stack.extend(n.children().rev()); + } else { + if matches!(n.kind(), SyntaxKind::Text | SyntaxKind::Space) { + interpreted_as += n.text(); + } + full_text += n.text(); + } + } + + annotations.push(DataAnnotation::new_interpreted_markup( + full_text, + interpreted_as, + )); + continue; + }, + _ => {}, + } + + // NESTED NODES + if node.children().count() > 0 && !matches!(kind, SyntaxKind::Args | SyntaxKind::FuncCall) { + nodes.extend(node.children().rev()); + continue; + } + + // TEXT + if matches!( + kind, + SyntaxKind::Text + | SyntaxKind::SmartQuote + | SyntaxKind::BlockComment + | SyntaxKind::LineComment + | SyntaxKind::Space + | SyntaxKind::Parbreak + ) { + annotations.push(DataAnnotation::new_text(node.text().to_string())); + }; + } + + Data::from_iter(annotations) +} diff --git a/tests/cli.rs b/tests/cli.rs index 314ef24..3ca3e98 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -1,10 +1,35 @@ +use std::path::PathBuf; + use assert_cmd::Command; -use predicates::{boolean::OrPredicate, str::contains}; +use predicates::{ + boolean::OrPredicate, + str::{contains, is_empty, is_match}, +}; + +lazy_static::lazy_static! { + static ref PATH_ROOT: PathBuf = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + static ref PATH_SAMPLE_FILES: PathBuf = PATH_ROOT.join("tests").join("sample_files"); +} + +const PATH_FILTERS: [(&str, &str); 1] = [(r" --> .*[\/].*\n", " --> [path]\n")]; +macro_rules! assert_snapshot { + ($label: expr, $snap: expr) => { + insta::with_settings!({filters => PATH_FILTERS}, { + insta::assert_snapshot!( + $label, + $snap + ); + }); + }; +} + +fn get_cmd() -> Command { + Command::cargo_bin("ltrs").unwrap() +} #[test] fn test_basic_check_text() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("check") .arg("-t") .arg("\"some text that is given as text\"") @@ -12,10 +37,31 @@ fn test_basic_check_text() { assert.success(); } +#[test] +fn test_basic_check_no_errors() { + let assert = get_cmd() + .arg("check") + .arg("-l") + .arg("en-US") + .arg("-t") + .arg("\"I am a star.\"") + .assert(); + assert + .success() + .stdout(contains("No errors were found in provided text")); +} + +#[test] +fn test_basic_check_empty_text() { + let assert = get_cmd().arg("check").arg("--text=").assert(); + assert + .success() + .stderr(is_match(r".*WARN.* No input text was provided, skipping.").unwrap()); +} + #[test] fn test_basic_check_data() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("check") .arg("-d") .arg( @@ -28,8 +74,7 @@ fn test_basic_check_data() { #[test] fn test_basic_check_wrong_data_1() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("check") .arg("-d") .arg("\"some text that is given as text\"") @@ -39,15 +84,13 @@ fn test_basic_check_wrong_data_1() { #[test] fn test_basic_check_wrong_data_2() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd.arg("check").arg("-d").arg("\"{}\"").assert(); + let assert = get_cmd().arg("check").arg("-d").arg("\"{}\"").assert(); assert.failure().stderr(contains("invalid value")); } #[test] fn test_basic_check_wrong_data_3() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("check") .arg("-d") .arg("\"some text that is given as text\"") @@ -57,14 +100,26 @@ fn test_basic_check_wrong_data_3() { #[test] fn test_basic_check_piped() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("check") - .write_stdin("some text that is piped") + .write_stdin("some text that is written to stdin") .assert(); assert.success(); } +#[test] +fn test_basic_check_stdin_verbose() { + let assert = get_cmd() + .arg("check") + .arg("-v") + .arg("-l") + .arg("en-US") + .write_stdin("I am a starr.") + .assert(); + // We only write if terminal is TTY + assert.success().stderr(is_empty()); +} + #[test] fn test_basic_check_file() { use std::io::Write; @@ -72,8 +127,10 @@ fn test_basic_check_file() { let mut file = tempfile::NamedTempFile::new().unwrap(); writeln!(file, "Some text with a error inside.").unwrap(); - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd.arg("check").arg(file.path().to_str().unwrap()).assert(); + let assert = get_cmd() + .arg("check") + .arg(file.path().to_str().unwrap()) + .assert(); assert.success(); } @@ -87,8 +144,7 @@ fn test_basic_check_files() { let mut file2 = tempfile::NamedTempFile::new().unwrap(); writeln!(file2, "Another text with an eror.").unwrap(); - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("check") .arg(file1.path().to_str().unwrap()) .arg(file2.path().to_str().unwrap()) @@ -96,10 +152,29 @@ fn test_basic_check_files() { assert.success(); } +#[test] +fn test_basic_check_files_with_empty_file() { + use std::io::Write; + + let mut file1 = tempfile::NamedTempFile::new().unwrap(); + writeln!(file1, "Some text with a error inside.").unwrap(); + + let file2 = tempfile::NamedTempFile::new().unwrap(); + + let assert = get_cmd() + .arg("check") + .arg("-v") + .arg(file1.path().to_str().unwrap()) + .arg(file2.path().to_str().unwrap()) + .assert(); + assert + .success() + .stderr(is_match(r".*INFO.* Skipping empty file: ").unwrap()); +} + #[test] fn test_basic_check_unexisting_file() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("check") .arg("some_file_path_that_should_not_exist.txt") .assert(); @@ -108,8 +183,7 @@ fn test_basic_check_unexisting_file() { #[test] fn test_check_with_language() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("check") .arg("-t") .arg("\"some text that is given as text\"") @@ -121,8 +195,7 @@ fn test_check_with_language() { #[test] fn test_check_with_wrong_language() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("check") .arg("-t") .arg("\"some text that is given as text\"") @@ -134,8 +207,7 @@ fn test_check_with_wrong_language() { #[test] fn test_check_with_unexisting_language() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("check") .arg("-t") .arg("\"some text that is given as text\"") @@ -150,8 +222,7 @@ fn test_check_with_unexisting_language() { #[test] fn test_check_with_username_and_key() { // TODO: remove the "invalid request" predicate as of LT 6.0 - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("check") .arg("-t") .arg("\"some text that is given as text\"") @@ -168,8 +239,7 @@ fn test_check_with_username_and_key() { #[test] fn test_check_with_username_only() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("check") .arg("-t") .arg("\"some text that is given as text\"") @@ -183,8 +253,7 @@ fn test_check_with_username_only() { #[test] fn test_check_with_key_only() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("check") .arg("-t") .arg("\"some text that is given as text\"") @@ -198,8 +267,7 @@ fn test_check_with_key_only() { #[test] fn test_check_with_dict() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("check") .arg("-t") .arg("\"some text that is given as text\"") @@ -211,8 +279,7 @@ fn test_check_with_dict() { #[test] fn test_check_with_dicts() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("check") .arg("-t") .arg("\"some text that is given as text\"") @@ -224,8 +291,7 @@ fn test_check_with_dicts() { #[test] fn test_check_with_preferred_variant() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("check") .arg("-t") .arg("\"some text that is given as text\"") @@ -237,8 +303,7 @@ fn test_check_with_preferred_variant() { #[test] fn test_check_with_preferred_variants() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("check") .arg("-t") .arg("\"some text that is given as text\"") @@ -250,8 +315,7 @@ fn test_check_with_preferred_variants() { #[test] fn test_check_with_language_and_preferred_variant() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("check") .arg("-t") .arg("\"some text that is given as text\"") @@ -268,8 +332,7 @@ fn test_check_with_language_and_preferred_variant() { #[test] fn test_check_with_enabled_rule() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("check") .arg("-t") .arg("\"some text that is given as text\"") @@ -281,8 +344,7 @@ fn test_check_with_enabled_rule() { #[test] fn test_check_with_enabled_rules() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("check") .arg("-t") .arg("\"some text that is given as text\"") @@ -294,8 +356,7 @@ fn test_check_with_enabled_rules() { #[test] fn test_check_with_disabled_rule() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("check") .arg("-t") .arg("\"some text that is given as text\"") @@ -307,8 +368,7 @@ fn test_check_with_disabled_rule() { #[test] fn test_check_with_disabled_rules() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("check") .arg("-t") .arg("\"some text that is given as text\"") @@ -320,8 +380,7 @@ fn test_check_with_disabled_rules() { #[test] fn test_check_with_enabled_category() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("check") .arg("-t") .arg("\"some text that is given as text\"") @@ -333,8 +392,7 @@ fn test_check_with_enabled_category() { #[test] fn test_check_with_enabled_categories() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("check") .arg("-t") .arg("\"some text that is given as text\"") @@ -346,8 +404,7 @@ fn test_check_with_enabled_categories() { #[test] fn test_check_with_disabled_category() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("check") .arg("-t") .arg("\"some text that is given as text\"") @@ -359,8 +416,7 @@ fn test_check_with_disabled_category() { #[test] fn test_check_with_disabled_categories() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("check") .arg("-t") .arg("\"some text that is given as text\"") @@ -372,8 +428,7 @@ fn test_check_with_disabled_categories() { #[test] fn test_check_with_enabled_only_rule() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("check") .arg("-t") .arg("\"some text that is given as text\"") @@ -386,8 +441,7 @@ fn test_check_with_enabled_only_rule() { #[test] fn test_check_with_enabled_only_category() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("check") .arg("-t") .arg("\"some text that is given as text\"") @@ -400,8 +454,7 @@ fn test_check_with_enabled_only_category() { #[test] fn test_check_with_enabled_only_without_enabled() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("check") .arg("-t") .arg("\"some text that is given as text\"") @@ -412,8 +465,7 @@ fn test_check_with_enabled_only_without_enabled() { #[test] fn test_check_with_picky_level() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("check") .arg("-t") .arg("\"some text that is given as text\"") @@ -425,8 +477,7 @@ fn test_check_with_picky_level() { #[test] fn test_check_with_unexisting_level() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("check") .arg("-t") .arg("\"some text that is given as text\"") @@ -438,23 +489,20 @@ fn test_check_with_unexisting_level() { #[test] fn test_languages() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd.arg("languages").assert(); + let assert = get_cmd().arg("languages").assert(); assert.success(); } #[test] fn test_ping() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd.arg("ping").assert(); + let assert = get_cmd().arg("ping").assert(); assert.success().stdout(contains("PONG! Delay: ")); } #[test] fn test_words() { // TODO: remove the "invalid request" predicate as of LT 6.0 - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("words") .arg("--username") .arg("user") @@ -470,8 +518,7 @@ fn test_words() { #[test] fn test_words_add() { // TODO: remove the "invalid request" predicate as of LT 6.0 - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("words") .arg("add") .arg("--username") @@ -488,8 +535,7 @@ fn test_words_add() { #[test] fn test_words_delete() { - let mut cmd = Command::cargo_bin("ltrs").unwrap(); - let assert = cmd + let assert = get_cmd() .arg("words") .arg("delete") .arg("--username") @@ -503,3 +549,45 @@ fn test_words_delete() { contains("invalid request"), )); } + +#[cfg_attr(not(feature = "snapshots"), ignore)] +#[test] +fn test_check_file_typst() { + let output = get_cmd() + .arg("check") + .arg(PATH_SAMPLE_FILES.join("example.typ")) + .output() + .unwrap(); + assert_snapshot!( + "autodetect_typst_file", + String::from_utf8(output.stdout).unwrap() + ); +} + +#[cfg_attr(not(feature = "snapshots"), ignore)] +#[test] +fn test_check_file_html() { + let output = get_cmd() + .arg("check") + .arg(PATH_SAMPLE_FILES.join("example.html")) + .output() + .unwrap(); + assert_snapshot!( + "autodetect_html_file", + String::from_utf8(output.stdout).unwrap() + ); +} + +#[cfg_attr(not(feature = "snapshots"), ignore)] +#[test] +fn test_check_file_markdown() { + let output = get_cmd() + .arg("check") + .arg(PATH_ROOT.join("README.md")) + .output() + .unwrap(); + assert_snapshot!( + "autodetect_markdown_file", + String::from_utf8(output.stdout).unwrap() + ); +} diff --git a/tests/match_positions.rs b/tests/match_positions.rs index eb47b4e..cc5d814 100644 --- a/tests/match_positions.rs +++ b/tests/match_positions.rs @@ -1,7 +1,4 @@ -use languagetool_rust::{ - check::{CheckRequest, CheckResponseWithContext}, - server::ServerClient, -}; +use languagetool_rust::api::{check, server::ServerClient}; #[macro_export] macro_rules! test_match_positions { @@ -10,9 +7,9 @@ macro_rules! test_match_positions { async fn $name() -> Result<(), Box> { let client = ServerClient::from_env_or_default(); - let req = CheckRequest::default().with_text($text.to_string()); + let req = check::Request::default().with_text($text); let resp = client.check(&req).await.unwrap(); - let resp = CheckResponseWithContext::new(req.get_text(), resp); + let resp = check::ResponseWithContext::new(req.get_text(), resp); let expected = vec![$(($x, $y)),*]; let got = resp.iter_match_positions(); diff --git a/tests/sample_files/example.html b/tests/sample_files/example.html new file mode 100644 index 0000000..d0de15a --- /dev/null +++ b/tests/sample_files/example.html @@ -0,0 +1,123 @@ + + + + +/home/rolv/Documents/zk/program/i4BJbPUbyR_stremio.md + + + + + +al + + + + + +

+ Fork me? Fork you, @octocat! Here is a link: makarainen.net +

+ + +#[cfg(feature = "html")] +pub fn parse_html(file_content: impl AsRef<str>) -> String { + use html_parser::{ElementVariant, Node}; + use select::{document::Document, node::Data, predicate}; + + let mut txt = String::new(); + + + +

Hello world

+ + +
+
+ Task +
+
+
+
+ Test an incorect spling +
+
+
+
+ +
+ +

+ Sean made a change +

+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Header 1Header 2Header 3Header 4
Row 1 Col 1Row 1 Col 2Row 1 Col 3Row 1 Col 4
Row 2 Col 1Row 2 Col 2Row 2 Col 3Row 2 Col 4
Row 3 Col 1Row 3 Col 2Row 3 Col 3Row 3 Col 4
+
+ + + diff --git a/tests/sample_files/example.typ b/tests/sample_files/example.typ new file mode 100644 index 0000000..8aae2ad --- /dev/null +++ b/tests/sample_files/example.typ @@ -0,0 +1,147 @@ +#set page(width: 10cm, height: auto) + += Introduction +In this report, we will explore the +various factors that influence _fluid +dynamics_ in glaciers and how they +contribute to the formation and +behaviour of these natural structures. + ++ The climate + - Temperatre + - Precipitation ++ The topography ++ The geology + +Glaciers as the one shown in +@glaciers will cease to exist if +we don't take action soon! + +#figure( + image("glacier.jpg", width: 70%), + caption: [ + _Glaciers_ form an important part + of the earth's climate system. + ], +) + + += Methods +We follow the glacier melting models +established in @glacier-melt. + +#bibliography("works.bib") + +The flow rate of a glacier is given +by the following equation: + +$ Q = rho A v + "time offset" $ + +Total displaced soil by glacial flow: + +$ 7.32 beta + + sum_(i=0)^nabla + (Q_i (a_i - epsilon)) / 2 $ + += Tables + +/* Text in a comment +* block. */ +// Text in a regular comment. + +#table( + columns: (1fr, auto, auto), + inset: 10pt, + align: horizon, + table.header( + [], [*Volume*], [*Parameters*], + ), + image("cylinder.svg"), + $ pi h (D^2 - d^2) / 4 $, + [ + $h$: height \ + $D$: outer radius \ + $d$: inner radius + ], + image("tetrahedron.svg"), + $ sqrt(2) / 12 a^3 $, + [$a$: edge length] +) + +#set table( + stroke: none, + gutter: 0.2em, + fill: (x, y) => + if x == 0 or y == 0 { gray }, + inset: (right: 1.5em), +) + +#show table.cell: it => { + if it.x == 0 or it.y == 0 { + set text(white) + strong(it) + } else if it.body == [] { + // Replace empty cells with 'N/A' + pad(..it.inset)[_N/A_] + } else { + it + } +} + +#let a = table.cell( + fill: green.lighten(60%), +)[A] +#let b = table.cell( + fill: aqua.lighten(60%), +)[B] + +#table( + columns: 4, + [], [Exam 1], [Exam 2], [Exam 3], + + [John], [], a, [], + [Mary], [], a, a, + [Robert], b, a, b, +) + += Code blocks + +Adding `rbx` to `rcx` gives +the desired result. + +What is ```rust fn main()``` in Rust +would be ```c int main()``` in C. + +```rust +fn main() { + println!("Hello World!"); +} +``` + +This has ``` `backticks` ``` in it +(but the spaces are trimmed). And +``` here``` the leading space is +also trimmed. + += Fibonacci sequence +The Fibonacci sequence is defined through the +recurrence relation $F_n = F_(n-1) + F_(n-2)$. +It can also be expressed in _closed form:_ + +$ F_n = round(1 / sqrt(5) phi.alt^n), quad + phi.alt = (1 + sqrt(5)) / 2 $ + +#let count = 8 +#let nums = range(1, count + 1) +#let fib(n) = ( + if n <= 2 { 1 } + else { fib(n - 1) + fib(n - 2) } +) + +The first #count numbers of the sequence are: + +#align(center, table( + columns: count, + ..nums.map(n => $F_#n$), + ..nums.map(n => str(fib(n))), +)) diff --git a/tests/snapshots/cli__autodetect_html_file.snap b/tests/snapshots/cli__autodetect_html_file.snap new file mode 100644 index 0000000..43fb0bb --- /dev/null +++ b/tests/snapshots/cli__autodetect_html_file.snap @@ -0,0 +1,25 @@ +--- +source: tests/cli.rs +expression: "String::from_utf8(output.stdout).unwrap()" +--- +error[MORFOLOGIK_RULE_EN_US]: Possible spelling mistake found. + --> [path] + | +4 | [path] + | +5 | ...ask
Test an incorect spling
... + | ^^^^^^^^ Possible spelling mistake + | -------- help: incorrect + | +error[MORFOLOGIK_RULE_EN_US]: Possible spelling mistake found. + --> [path] + | +5 | ...>
Test an incorect spling
... + | ^^^^^^ Possible spelling mistake + | ------ help: spring, spying, sling, spline, splint, ... (2 not shown) + | diff --git a/tests/snapshots/cli__autodetect_markdown_file.snap b/tests/snapshots/cli__autodetect_markdown_file.snap new file mode 100644 index 0000000..56f083c --- /dev/null +++ b/tests/snapshots/cli__autodetect_markdown_file.snap @@ -0,0 +1,109 @@ +--- +source: tests/cli.rs +expression: "String::from_utf8(output.stdout).unwrap()" +--- +error[COMMA_PARENTHESIS_WHITESPACE]: Don’t put a space before the full stop. + --> [path] + | +6 | ...ges and is free to use, more on that on_ [](https://languagetool.org/)_. __There is a public API (with a free tie... + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use of whitespace before comma and before/after parentheses + | --------------------------------- help: . + | +error[WHITESPACE_RULE]: Possible typo: you repeated a whitespace + --> [path] + | +11 | ...rs very easily via Rust code! _Crates.io docs.rs codecov- - [](#about) - [](#cli-referenc... + | ^^^^^^^^^ Whitespace repetition (bad formatting) + | --------- help: + | +error[COMMA_PARENTHESIS_WHITESPACE]: Don’t put a space before the full stop. + --> [path] + | +13 | ...on. Installation guidelines can be found [](https://www.docker.com/get-started/). On Linux platforms, you might need to c... + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use of whitespace before comma and before/after parentheses + | ----------------------------------------- help: . + | +error[COMMA_PARENTHESIS_WHITESPACE]: Don’t put a space before the full stop. + --> [path] + | +14 | ...vent the _sudo privilege issue_ by doing [](https://docs.docker.com/engine/install/linux-postinstall/). ## API Reference + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use of whitespace before comma and before/after parentheses + | --------------------------------------------------------------- help: . + | +error[COMMA_PARENTHESIS_WHITESPACE]: Don’t put a space before the full stop. + --> [path] + | +5 | ... or crate, then we recommend reading the [](https://docs.rs/languagetool-rust). To use LanguageTool-Rust in your Rust p... + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use of whitespace before comma and before/after parentheses + | --------------------------------------- help: . + | +error[MORFOLOGIK_RULE_EN_US]: Possible spelling mistake found. + --> [path] + | +5 | ...piling LTRS. #### Default Features - - **cli**: Adds command-line related methods for ... + | ^^^^^^^ Possible spelling mistake + | ------- help: CLI, Clip, CGI, CPI, CSI, ... (1428 not shown) + | +error[MORFOLOGIK_RULE_EN_US]: Possible spelling mistake found. + --> [path] + | +9 | ...ate**, **color**, **multithreaded**. - **native-tls**: Enables TLS functionality provided by ... + | ^^^^^^^^^^^^^^ Possible spelling mistake + | -------------- help: natives, natively, nativists + | +error[MORFOLOGIK_RULE_EN_US]: Possible spelling mistake found. + --> [path] + | +3 | ...annotate results from check request. - **cli-complete**: Adds commands to generate completion f... + | ^^^^^^^^^^^^^^^^ Possible spelling mistake + | ---------------- help: incomplete + | +error[MORFOLOGIK_RULE_EN_US]: Possible spelling mistake found. + --> [path] + | +6 | ...shells. This feature also activates the **cli** feature. Enter ltrs completions --help ... + | ^^^^^^^ Possible spelling mistake + | ------- help: CLI, clip, CGI, CPI, CSI, ... (1461 not shown) + | +error[MORFOLOGIK_RULE_EN_US]: Possible spelling mistake found. + --> [path] + | +10 | ...es color outputting in the terminal. If **cli** feature is also enabled, the --color= [path] + | +13 | ...d**: Enables multithreaded requests. - **native-tls-vendored**: Enables the vendored feature of native... + | ^^^^^^^^^^^^^^^^^^^^^^^ Possible spelling mistake + | ----------------------- help: native-TLS-vendored + | +error[MORFOLOGIK_RULE_EN_US]: Possible spelling mistake found. + --> [path] + | +14 | ...u are planning to use HTTPS servers. - **undoc**: Adds more fields to JSON responses tha... + | ^^^^^^^^^ Possible spelling mistake + | --------- help: undo, undock, undos + | +error[THE_CC]: It appears that a noun is missing after “the”. + --> [path] + | +16 | ... JSON responses that are not present in the [](https://languagetool.org/http-api/sw... + | ^^^ the and + | --- + | +error[CONSECUTIVE_SPACES]: It seems like there are too many consecutive spaces here. + --> [path] + | +16 | ...ON responses that are not present in the [](https://languagetool.org/http-api/swagger-ui/#!/default/) but might be present in some cases. All ... + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Two consecutive spaces + | -------------------------------------------------------------- help: + | +error[MORFOLOGIK_RULE_EN_US]: Possible spelling mistake found. + --> [path] + | +20 | ...cases. All added fields are stored in a hashmap as JSON values. ## Related Projects ... + | ^^^^^^^ Possible spelling mistake + | ------- help: hash map + | diff --git a/tests/snapshots/cli__autodetect_typst_file.snap b/tests/snapshots/cli__autodetect_typst_file.snap new file mode 100644 index 0000000..f63887b --- /dev/null +++ b/tests/snapshots/cli__autodetect_typst_file.snap @@ -0,0 +1,74 @@ +--- +source: tests/cli.rs +expression: "String::from_utf8(output.stdout).unwrap()" +--- +error[MORFOLOGIK_RULE_EN_US]: Possible spelling mistake. ‘behaviour’ is British English. + --> [path] + | +7 | ...ow they contribute to the formation and behaviour of these natural structures. + The cli... + | ^^^^^^^^^ Possible spelling mistake + | --------- help: behavior + | +error[MORFOLOGIK_RULE_EN_US]: Possible spelling mistake found. + --> [path] + | +9 | ...behaviour of these natural structures. + The climate - Temperatre - Precipitation + The topography + The... + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Possible spelling mistake + | ----------------------------- help: Temperature, Temperate + | +error[COMMA_PARENTHESIS_WHITESPACE]: Don’t put a space before the full stop. + --> [path] + | +8 | ...he glacier melting models established in @glacier-melt. bibliography("works.bib") The flow ra... + | ^^^^^^^^^^^^^^^ Use of whitespace before comma and before/after parentheses + | --------------- help: . + | +error[CONSECUTIVE_SPACES]: It seems like there are too many consecutive spaces here. + --> [path] + | +1 | = Code blocks Adding `rbx` to `rcx` gives the desired result. What... + | ^^^^^^^ Two consecutive spaces + | ------- help: + | +error[CONSECUTIVE_SPACES]: It seems like there are too many consecutive spaces here. + --> [path] + | +1 | = Code blocks Adding `rbx` to `rcx` gives the desired result. What is ```ru... + | ^^^^^^^ Two consecutive spaces + | ------- help: + | +error[TO_NON_BASE]: The verb after “to” should be in the base form as part of the to-infinitive. A verb can take many forms, but the base form is always used in the to-infinitive. + --> [path] + | +2 | = Code blocks Adding `rbx` to `rcx` gives the desired result. What is ```rust fn... + | ^^^^^ 'to' + non-base form + | ----- help: give + | +error[CONSECUTIVE_SPACES]: It seems like there are too many consecutive spaces here. + --> [path] + | +4 | ...`rcx` gives the desired result. What is ```rust fn main()``` in Rust would be ```c int main()``` in C... + | ^^^^^^^^^^^^^^^^^^^^^^ Two consecutive spaces + | ---------------------- help: + | +error[CONSECUTIVE_SPACES]: It seems like there are too many consecutive spaces here. + --> [path] + | +5 | ...is ```rust fn main()``` in Rust would be ```c int main()``` in C. ```rust fn main() { println!(... + | ^^^^^^^^^^^^^^^^^^^^ Two consecutive spaces + | -------------------- help: + | +error[CONSECUTIVE_SPACES]: It seems like there are too many consecutive spaces here. + --> [path] + | +8 | ...rintln!("Hello World!"); } ``` This has ``` `backticks` ``` in it (but the spaces are trimmed). And ... + | ^^^^^^^^^^^^^^^^^^^^^ Two consecutive spaces + | --------------------- help: + | +error[COMMA_PARENTHESIS_WHITESPACE]: Don’t put a space before the full stop. + --> [path] + | +18 | ... defined through the recurrence relation $F_n = F_(n-1) + F_(n-2)$. It can also be expressed in _closed for... + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use of whitespace before comma and before/after parentheses + | --------------------------- help: . + |