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..8890c40 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,69 @@ +# 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 Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - 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 PDM + uses: pdm-project/setup-pdm@v4 + with: + python-version: '3.11' + cache: true + + - name: Install Rust nightly + uses: dtolnay/rust-toolchain@nightly + with: + components: clippy,rustfmt + + - name: Install dependencies + run: | + pdm install -G test,github-action + + - 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/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 1b1aeb4..f20f9b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,106 +5,158 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.0.0](https://github.com/jeertmans/languagetool-rust/compare/v2.1.4...v2.1.5) + +- Added support for Mardown and Typst files. + [#116](https://github.com/jeertmans/languagetool-rust/pull/116) + +### Chore + +- **Breaking** Stopped (de)serializing structs that did not need. + [#116](https://github.com/jeertmans/languagetool-rust/pull/116) +- **Breaking** Completly reorganized the crate. + [#116](https://github.com/jeertmans/languagetool-rust/pull/116) + ## [2.1.4](https://github.com/jeertmans/languagetool-rust/compare/v2.1.3...v2.1.4) ### Fixed -- Fixed serializing of `interpretAs` in `data`. [#103](https://github.com/jeertmans/languagetool-rust/pull/103) +- 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) ### Chore -- Moved LanguageTool docker image to service in GitHub action. [#87](https://github.com/jeertmans/languagetool-rust/pull/87) -- Added automatic benchmarks comparison to CI. [#89](https://github.com/jeertmans/languagetool-rust/pull/89) -- Improving test coverage. [#88](https://github.com/jeertmans/languagetool-rust/pull/88) +- Moved LanguageTool docker image to service in GitHub action. + [#87](https://github.com/jeertmans/languagetool-rust/pull/87) +- Added automatic benchmarks comparison to CI. + [#89](https://github.com/jeertmans/languagetool-rust/pull/89) +- Improving test coverage. + [#88](https://github.com/jeertmans/languagetool-rust/pull/88) ### Fixed -- Allow text starting with hyphens. [#100](https://github.com/jeertmans/languagetool-rust/pull/100) +- 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 ### 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 ### 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 ### 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 ### 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 @@ -114,31 +166,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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 can publish. + [#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 diff --git a/Cargo.lock b/Cargo.lock index f9a22dd..5bb28de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,16 +44,15 @@ dependencies = [ [[package]] name = "anstream" -version = "0.3.2" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", - "is-terminal", "utf8parse", ] @@ -78,17 +77,17 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] name = "anstyle-wincon" -version = "1.0.2" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c677ab05e09154296dd37acecd46420c17b9713e8366facafa8fc0885167cf4c" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ "anstyle", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -218,43 +217,41 @@ dependencies = [ [[package]] name = "clap" -version = "4.3.21" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c27cdf28c0f604ba3f512b0c9a409f8de8513e4816705deb0498b627e7c3a3fd" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ "clap_builder", "clap_derive", - "once_cell", ] [[package]] name = "clap_builder" -version = "4.3.21" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08a9f1ab5e9f01a9b81f202e8562eb9a10de70abf9eaeac1be465c28b75aa4aa" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ "anstream", "anstyle", "clap_lex", - "once_cell", "strsim", "terminal_size", ] [[package]] name = "clap_complete" -version = "4.3.2" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc443334c81a804575546c5a8a79b4913b50e28d69232903604cada1de817ce" +checksum = "dd79504325bf38b10165b02e89b4347300f855f273c4cb30c4a3209e6583275e" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.3.12" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ "heck", "proc-macro2", @@ -264,9 +261,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.5.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] name = "colorchoice" @@ -274,6 +271,29 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "comemo" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df6916408a724339aa77b18214233355f3eb04c42eb895e5f8909215bd8a7a91" +dependencies = [ + "comemo-macros", + "once_cell", + "parking_lot", + "siphasher", +] + +[[package]] +name = "comemo-macros" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8936e42f9b4f5bdfaf23700609ac1f11cb03ad4c1ec128a4ee4fd0903e228db" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -381,6 +401,15 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "ecow" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54bfbb1708988623190a6c4dbedaeaf0f53c20c6395abd6a01feb327b3146f4b" +dependencies = [ + "serde", +] + [[package]] name = "either" version = "1.9.0" @@ -404,7 +433,7 @@ checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" dependencies = [ "errno-dragonfly", "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -551,6 +580,15 @@ dependencies = [ "slab", ] +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + [[package]] name = "gimli" version = "0.27.3" @@ -590,9 +628,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" @@ -691,17 +729,6 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys", -] - [[package]] name = "ipnet" version = "2.8.0" @@ -715,8 +742,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi", - "rustix 0.38.7", - "windows-sys", + "rustix", + "windows-sys 0.48.0", ] [[package]] @@ -755,6 +782,7 @@ dependencies = [ "futures", "is-terminal", "predicates", + "pulldown-cmark", "reqwest", "serde", "serde_json", @@ -762,6 +790,7 @@ dependencies = [ "termcolor", "thiserror", "tokio", + "typst-syntax", ] [[package]] @@ -778,15 +807,19 @@ checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] name = "linux-raw-sys" -version = "0.3.8" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" [[package]] -name = "linux-raw-sys" -version = "0.4.5" +name = "lock_api" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] [[package]] name = "log" @@ -832,7 +865,7 @@ checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -953,6 +986,29 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.4.1", + "smallvec", + "windows-targets 0.48.1", +] + [[package]] name = "percent-encoding" version = "2.3.0" @@ -1038,18 +1094,37 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" dependencies = [ "unicode-ident", ] +[[package]] +name = "pulldown-cmark" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0530d13d87d1f549b66a3e8d0c688952abe5994e204ed62615baaf25dc029c" +dependencies = [ + "bitflags 2.3.3", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d8f9aa0e3cbcfaf8bf00300004ee3b72f74770f9cbac93f6928771f613276b" + [[package]] name = "quote" -version = "1.0.32" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -1085,6 +1160,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "regex" version = "1.9.3" @@ -1157,20 +1241,6 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" -[[package]] -name = "rustix" -version = "0.37.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.3.8", - "windows-sys", -] - [[package]] name = "rustix" version = "0.38.7" @@ -1180,8 +1250,8 @@ dependencies = [ "bitflags 2.3.3", "errno", "libc", - "linux-raw-sys 0.4.5", - "windows-sys", + "linux-raw-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1205,7 +1275,7 @@ version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1239,18 +1309,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.183" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c" +checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.183" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816" +checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" dependencies = [ "proc-macro2", "quote", @@ -1280,6 +1350,12 @@ dependencies = [ "serde", ] +[[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.8" @@ -1289,6 +1365,12 @@ 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.4.9" @@ -1306,20 +1388,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.28" +version = "2.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" dependencies = [ "proc-macro2", "quote", @@ -1334,9 +1416,9 @@ checksum = "dc02fddf48964c42031a0b3fe0428320ecf3a73c401040fc0096f97794310651" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", - "rustix 0.38.7", - "windows-sys", + "redox_syscall 0.3.5", + "rustix", + "windows-sys 0.48.0", ] [[package]] @@ -1350,12 +1432,12 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.2.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" +checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" dependencies = [ - "rustix 0.37.23", - "windows-sys", + "rustix", + "windows-sys 0.48.0", ] [[package]] @@ -1423,7 +1505,7 @@ dependencies = [ "pin-project-lite", "socket2 0.5.3", "tokio-macros", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1493,6 +1575,32 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "typst-syntax" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "367d86bf18f0363146bea1ea76fad19b54458695fdfad5e74ead3ede574b75fe" +dependencies = [ + "comemo", + "ecow", + "once_cell", + "serde", + "unicode-ident", + "unicode-math-class", + "unicode-script", + "unicode-segmentation", + "unscanny", +] + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.13" @@ -1505,6 +1613,12 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +[[package]] +name = "unicode-math-class" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d246cf599d5fae3c8d56e04b20eb519adb89a8af8d0b0fbcded369aa3647d65" + [[package]] name = "unicode-normalization" version = "0.1.22" @@ -1514,12 +1628,30 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-script" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8d71f5726e5f285a935e9fe8edfd53f0491eb6e9a5774097fdabee7cd8c9cd" + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + [[package]] name = "unicode-width" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "unscanny" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9df2af067a7953e9c3831320f35c1cc0600c30d44d9f7a12b01db1cd88d6b47" + [[package]] name = "url" version = "2.4.0" @@ -1543,6 +1675,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "wait-timeout" version = "0.2.0" @@ -1690,7 +1828,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.1", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.5", ] [[package]] @@ -1699,13 +1846,29 @@ version = "0.48.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] [[package]] @@ -1714,42 +1877,90 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + [[package]] name = "windows_aarch64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + [[package]] name = "windows_i686_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + [[package]] name = "windows_i686_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + [[package]] name = "windows_x86_64_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + [[package]] name = "windows_x86_64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" + [[package]] name = "winreg" version = "0.10.1" diff --git a/Cargo.toml b/Cargo.toml index 03208db..cdbaf23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,20 +5,22 @@ 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.4", features = ["cargo", "derive", "env", "wrap_help"], optional = true} +clap_complete = {version = "^4.5.2", optional = true} is-terminal = {version = "0.4.3", optional = true} +pulldown-cmark = {version = "0.10.2", optional = true} reqwest = {version = "^0.11", default-features = false, features = ["json"]} 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} +typst-syntax = {version = "^0.11.0", optional = true} [dev-dependencies] assert_cmd = "2.0.11" @@ -36,14 +38,17 @@ color = ["annotate-snippets?/color", "dep:termcolor"] default = ["cli", "native-tls"] docker = [] full = ["cli-complete", "docker", "unstable"] +markdown = ["dep:pulldown-cmark"] multithreaded = ["dep:tokio"] native-tls = ["reqwest/native-tls"] native-tls-vendored = ["reqwest/native-tls-vendored"] +pulldown-cmark = ["dep:pulldown-cmark"] +typst = ["dep:typst-syntax"] unstable = [] [lib] name = "languagetool_rust" -path = "src/lib/lib.rs" +path = "src/lib.rs" [package] authors = ["Jérome Eertmans "] @@ -70,5 +75,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 f094c89..003d1ad 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`: @@ -143,7 +160,7 @@ 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()); + .with_text("Some phrase with a smal mistake".to_string()); # codespell:ignore smal println!( "{}", @@ -155,30 +172,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/src/lib/check.rs b/src/api/check.rs similarity index 97% rename from src/lib/check.rs rename to src/api/check.rs index 500af0a..41f2512 100644 --- a/src/lib/check.rs +++ b/src/api/check.rs @@ -1,6 +1,8 @@ //! Structures for `check` requests and responses. -use super::error::{Error, Result}; +#[cfg(feature = "cli")] +use std::path::PathBuf; + #[cfg(feature = "annotate")] use annotate_snippets::{ display_list::{DisplayList, FormatOptions}, @@ -9,8 +11,8 @@ use annotate_snippets::{ #[cfg(feature = "cli")] use clap::{Args, Parser, ValueEnum}; use serde::{Deserialize, Serialize, Serializer}; -#[cfg(feature = "cli")] -use std::path::PathBuf; + +use super::error::{Error, Result}; /// Requests @@ -257,7 +259,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] @@ -384,7 +386,7 @@ 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, PartialEq, Eq, Serialize, Hash)] #[serde(rename_all = "camelCase")] #[non_exhaustive] pub struct CheckRequest { @@ -650,7 +652,29 @@ fn parse_filename(s: &str) -> Result { } } +/// Support file types. +#[cfg(feature = "cli")] +#[derive(Clone, Debug, Default, ValueEnum)] +#[non_exhaustive] +pub enum FileType { + /// Auto. + #[default] + Auto, + /// Markdown. + Markdown, + /// Typst. + Typst, +} + /// 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; +/// - raw text, if `-- [FILE]...` are provided. Note that some file types will +/// use a +/// - raw text, through stdin, if nothing is provided. #[cfg(feature = "cli")] #[derive(Debug, Parser)] pub struct CheckCommand { @@ -660,14 +684,6 @@ pub struct CheckCommand { #[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, @@ -677,12 +693,17 @@ pub struct CheckCommand { /// 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, + /// Specify the files type to use the correct parser. + /// + /// If set to auto, the type is guessed from the filename extension. + #[clap(long, default_value = "default", ignore_case = true, value_enum)] + 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 [`CheckRequest`]. + #[command(flatten, next_help_heading = "Request options")] + pub request: CheckRequest, } #[cfg(test)] diff --git a/src/lib/languages.rs b/src/api/languages.rs similarity index 85% rename from src/lib/languages.rs rename to src/api/languages.rs index 92724bf..ffa9173 100644 --- a/src/lib/languages.rs +++ b/src/api/languages.rs @@ -2,6 +2,9 @@ use serde::{Deserialize, Serialize}; +/// LanguageTool GET languages request. +pub struct Request; + #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] #[non_exhaustive] @@ -18,4 +21,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..ee62576 --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,64 @@ +/// 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 resquests might return undocumented fields. Those are deserialized +/// to the `undocumented` field. +pub mod check; +pub mod languages; +pub mod server; +pub mod words; + +/// A HTTP client for making requests to some LanguageTool server. +pub struct Client { + /// Server's hostname. + hostname: String, + /// Server's port. + port: Option, + /// Inner client to perform HTTP requets. + client: reqwest::Client, +} + +impl Default for Client { + fn default() -> Self { + Self { + hostname: "https://api.languagetoolplus.com".to_string(), + ..Default::default() + } + } +} + +impl Client { + /// Construct a HTTP url base on the current hostname, optional port, + /// and provided endpoint. + #[inline] + 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::() + } + + /// Send a words request to the server and await for the response. + pub async fn languages(&self, request: &languages::Request) -> Result { + self.client + .get(self.url("/languages")) + .query(request) + .send() + .await? + .json::() + } +} diff --git a/src/lib/server.rs b/src/api/server.rs similarity index 100% rename from src/lib/server.rs rename to src/api/server.rs diff --git a/src/lib/words.rs b/src/api/words.rs similarity index 56% rename from src/lib/words.rs rename to src/api/words.rs index 659a528..68d8dbd 100644 --- a/src/lib/words.rs +++ b/src/api/words.rs @@ -1,9 +1,8 @@ //! Structures for `words` requests and responses. -use crate::{ - check::serialize_option_vec_string, - error::{Error, Result}, -}; +use crate::{Error, Result}; + +use super::check::serialize_option_vec_string; #[cfg(feature = "cli")] use clap::{Args, Parser, Subcommand}; use serde::{Deserialize, Serialize}; @@ -55,13 +54,19 @@ pub struct LoginArgs { #[cfg_attr(feature = "cli", derive(Args))] #[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize, Hash)] #[non_exhaustive] -pub struct WordsRequest { +pub struct Request { /// Offset of where to start in the list of words. - #[cfg_attr(feature = "cli", clap(long, default_value = "0"))] - offset: isize, + /// + /// 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. - #[cfg_attr(feature = "cli", clap(long, default_value = "10"))] - pub limit: isize, + /// + /// 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)] @@ -70,20 +75,21 @@ pub struct WordsRequest { /// 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 [`WordsRequest`], but used to CLI only. +/// Copy of [`Request`], 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 { +pub struct RequestArgs { /// Offset of where to start in the list of words. #[cfg_attr(feature = "cli", clap(long, default_value = "0"))] - offset: isize, + pub offset: isize, /// Maximum number of words to return. #[cfg_attr(feature = "cli", clap(long, default_value = "10"))] pub limit: isize, @@ -99,9 +105,9 @@ pub struct WordsRequestArgs { } #[cfg(feature = "cli")] -impl From for WordsRequest { +impl From for Request { #[inline] - fn from(args: WordsRequestArgs) -> Self { + fn from(args: RequestArgs) -> Self { Self { offset: args.offset, limit: args.limit, @@ -111,54 +117,6 @@ impl From for WordsRequest { } } -/// 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)] @@ -177,7 +135,7 @@ pub enum WordsSubcommand { pub struct WordsCommand { /// Actual GET request. #[command(flatten)] - pub request: WordsRequestArgs, + pub request: RequestArgs, /// Optional subcommand. #[command(subcommand)] pub subcommand: Option, @@ -186,23 +144,80 @@ pub struct WordsCommand { /// LanguageTool GET words response. #[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] #[non_exhaustive] -pub struct WordsResponse { +pub struct Response { /// 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, +pub mod add { + 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, + } } -/// 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, +pub mod delete { + 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/check.rs b/src/check.rs new file mode 100644 index 0000000..4fc96b7 --- /dev/null +++ b/src/check.rs @@ -0,0 +1,1308 @@ +//! Structures for `check` requests and responses. + +use std::collections::HashMap; +#[cfg(feature = "cli")] +use std::path::PathBuf; + +#[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 serde::{Deserialize, Serialize, Serializer}; +use serde_json::Value; + +use super::error::{Error, Result}; + +/// Requests + +/// Parse `v` is valid language code. +/// +/// A valid language code is usually +/// - a two character string matching pattern `[a-z]{2} +/// - 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. +/// +/// Therefore, a valid language code must match the following: +/// +/// - `[a-zA-Z]{2,3}(-[a-zA-Z]{2}(-[a-zA-Z]+)*)?` +/// +/// or +/// +/// - "auto" +/// +/// > Note: a valid language code does not mean that it exists. +/// +/// # Examples +/// +/// ``` +/// # use languagetool_rust::check::parse_language_code; +/// assert!(parse_language_code("en").is_ok()); +/// +/// assert!(parse_language_code("en-US").is_ok()); +/// +/// assert!(parse_language_code("en-us").is_ok()); +/// +/// assert!(parse_language_code("ca-ES-valencia").is_ok()); +/// +/// assert!(parse_language_code("abcd").is_err()); +/// +/// assert!(parse_language_code("en_US").is_err()); +/// +/// assert!(parse_language_code("fr-french").is_err()); +/// +/// assert!(parse_language_code("some random text").is_err()); +/// ``` +#[cfg(feature = "cli")] +pub fn parse_language_code(v: &str) -> Result { + #[inline] + fn is_match(v: &str) -> bool { + let mut splits = v.split('-'); + + match splits.next() { + Some(s) + if (s.len() == 2 || s.len() == 3) && s.chars().all(|c| c.is_ascii_alphabetic()) => { + }, + _ => return false, + } + + match splits.next() { + Some(s) if s.len() != 2 || s.chars().any(|c| !c.is_ascii_alphabetic()) => return false, + Some(_) => (), + None => return true, + } + for s in splits { + if !s.chars().all(|c| c.is_ascii_alphabetic()) { + return false; + } + } + true + } + + if v == "auto" || is_match(v) { + Ok(v.to_string()) + } else { + Err(Error::InvalidValue( + "The value should be `\"auto\"` or match regex pattern: \ + ^[a-zA-Z]{2,3}(-[a-zA-Z]{2}(-[a-zA-Z]+)*)?$" + .to_string(), + )) + } +} + +/// Utility function to serialize a optional vector a strings +/// into a comma separated list of strings. +/// +/// This is required by reqwest's RequestBuilder, otherwise it +/// will not work. +pub(crate) fn serialize_option_vec_string( + v: &Option>, + serializer: S, +) -> std::result::Result +where + S: Serializer, +{ + match v { + Some(v) if v.len() == 1 => serializer.serialize_str(&v[0]), + Some(v) if v.len() > 1 => { + let size = v.iter().map(|s| s.len()).sum::() + v.len() - 1; + let mut string = String::with_capacity(size); + + string.push_str(&v[0]); + + for s in &v[1..] { + string.push(','); + string.push_str(s); + } + + serializer.serialize_str(string.as_ref()) + }, + _ => serializer.serialize_none(), + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, Hash)] +#[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, + #[serde(skip_serializing_if = "Option::is_none")] + /// Text that should be treated as markup. + pub markup: Option, + #[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()), + } + } +} + +impl DataAnnotation { + /// Instantiate a new `DataAnnotation` with text only. + #[inline] + #[must_use] + pub fn new_text(text: String) -> Self { + Self { + interpret_as: None, + markup: None, + text: Some(text), + } + } + + /// Instantiate a new `DataAnnotation` with markup only. + #[inline] + #[must_use] + pub fn new_markup(markup: String) -> Self { + Self { + interpret_as: None, + markup: Some(markup), + text: None, + } + } + + /// Instantiate a new `DataAnnotation` with markup and its interpretation. + #[inline] + #[must_use] + pub fn new_interpreted_markup(markup: String, interpret_as: String) -> Self { + Self { + interpret_as: Some(interpret_as), + markup: Some(markup), + text: None, + } + } +} + +#[cfg(test)] +mod data_annotation_tests { + + use crate::check::DataAnnotation; + + #[test] + fn test_text() { + let da = DataAnnotation::new_text("Hello".to_string()); + + assert_eq!(da.text.unwrap(), "Hello".to_string()); + assert!(da.markup.is_none()); + assert!(da.interpret_as.is_none()); + } + + #[test] + fn test_markup() { + let da = DataAnnotation::new_markup("Hello".to_string()); + + assert!(da.text.is_none()); + assert_eq!(da.markup.unwrap(), "Hello".to_string()); + assert!(da.interpret_as.is_none()); + } + + #[test] + fn test_interpreted_markup() { + let da = + DataAnnotation::new_interpreted_markup("Hello".to_string(), "Hello".to_string()); + + assert!(da.text.is_none()); + assert_eq!(da.markup.unwrap(), "Hello".to_string()); + assert_eq!(da.interpret_as.unwrap(), "Hello".to_string()); + } +} + +/// Alternative text to be checked. +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub struct Data { + /// Vector of markup text, see [`DataAnnotation`]. + pub annotation: Vec, +} + +impl> FromIterator for Data { + fn from_iter>(iter: I) -> Self { + let annotation = iter.into_iter().map(std::convert::Into::into).collect(); + Data { annotation } + } +} + +impl Serialize for Data { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + let mut map = std::collections::HashMap::new(); + map.insert("annotation", &self.annotation); + + serializer.serialize_str(&serde_json::to_string(&map).unwrap()) + } +} + +#[cfg(feature = "cli")] +impl std::str::FromStr for Data { + type Err = Error; + + fn from_str(s: &str) -> Result { + let v: Self = serde_json::from_str(s)?; + Ok(v) + } +} + +/// Possible levels for additional rules. +/// +/// Currently, `Level::Picky` adds additional rules +/// with respect to `Level::Default`. +#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Hash)] +#[cfg_attr(feature = "cli", derive(ValueEnum))] +#[serde(rename_all = "lowercase")] +#[non_exhaustive] +pub enum Level { + /// Default level. + #[default] + Default, + /// Picky level. + Picky, +} + +impl Level { + /// Return `true` if current level is the default one. + /// + /// # Examples + /// + /// ``` + /// # use languagetool_rust::check::Level; + /// + /// let level: Level = Default::default(); + /// + /// assert!(level.is_default()); + /// ``` + #[must_use] + pub fn is_default(&self) -> bool { + *self == Level::default() + } +} + +/// Split a string into as few fragments as possible, where each fragment +/// contains (if possible) a maximum of `n` characters. Pattern str `pat` is +/// used for splitting. +/// +/// # Examples +/// +/// ``` +/// # use languagetool_rust::check::split_len; +/// let s = "I have so many friends. +/// They are very funny. +/// I think I am very lucky to have them. +/// One day, I will write them a poem. +/// But, in the meantime, I write code. +/// "; +/// +/// let split = split_len(&s, 40, "\n"); +/// +/// assert_eq!(split.join(""), s); +/// assert_eq!( +/// split, +/// vec![ +/// "I have so many friends.\n", +/// "They are very funny.\n", +/// "I think I am very lucky to have them.\n", +/// "One day, I will write them a poem.\n", +/// "But, in the meantime, I write code.\n" +/// ] +/// ); +/// +/// let split = split_len(&s, 80, "\n"); +/// +/// assert_eq!( +/// split, +/// vec![ +/// "I have so many friends.\nThey are very funny.\n", +/// "I think I am very lucky to have them.\nOne day, I will write them a poem.\n", +/// "But, in the meantime, I write code.\n" +/// ] +/// ); +/// +/// let s = "I have so many friends. +/// They are very funny. +/// I think I am very lucky to have them. +/// +/// One day, I will write them a poem. +/// But, in the meantime, I write code. +/// "; +/// +/// let split = split_len(&s, 80, "\n\n"); +/// +/// println!("{:?}", split); +/// +/// assert_eq!( +/// split, +/// vec![ +/// "I have so many friends.\nThey are very funny.\nI think I am very lucky to have \ +/// them.\n\n", +/// "One day, I will write them a poem.\nBut, in the meantime, I write code.\n" +/// ] +/// ); +/// ``` +#[must_use] +pub fn split_len<'source>(s: &'source str, n: usize, pat: &str) -> Vec<&'source str> { + let mut vec: Vec<&'source str> = Vec::with_capacity(s.len() / n); + let mut splits = s.split_inclusive(pat); + + let mut start = 0; + let mut i = 0; + + if let Some(split) = splits.next() { + vec.push(split); + } else { + return Vec::new(); + } + + for split in splits { + let new_len = vec[i].len() + split.len(); + if new_len < n { + vec[i] = &s[start..start + new_len]; + } else { + vec.push(split); + start += vec[i].len(); + i += 1; + } + } + + vec +} + +/// 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). +#[cfg_attr(feature = "cli", derive(Args))] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Hash)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Request { + /// 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, + /// 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. + #[cfg_attr(feature = "cli", clap(short = 'd', long, conflicts_with = "text"))] + #[serde(skip_serializing_if = "Option::is_none")] + 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 + ) + )] + 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") + )] + #[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. + /// + /// 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"))] + #[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")] + 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 Request { + #[inline] + fn default() -> Request { + Request { + text: Default::default(), + data: Default::default(), + 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(), + } + } +} + +#[inline] +fn is_false(b: &bool) -> bool { + !(*b) +} + +impl Request { + /// 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); + 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 { + self.data = Some(data); + self.text = None; + self + } + + /// 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)?)) + } + + /// Set the language of the text / data. + #[must_use] + pub fn with_language(mut self, language: String) -> Self { + self.language = language; + self + } + + /// Return a copy of 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 { + 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(), + )); + } + } + Ok(text) + } else { + Err(Error::InvalidRequest( + "missing either text or data field".to_string(), + )) + } + } + + /// Return a copy of the text within the request. + /// 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 { + self.try_get_text().unwrap() + } + + /// Split this request into multiple, using [`split_len`] function to split + /// text. + /// + /// # 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()))?; + + Ok(split_len(text.as_str(), n, pat) + .iter() + .map(|text_fragment| self.clone().with_text(text_fragment.to_string())) + .collect()) + } + + /// Split this request into multiple, using [`split_len`] function to split + /// text. + /// 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 { + 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())) + } +} + +/// Supported file types. +#[cfg(feature = "cli")] +#[derive(Clone, Debug, Default, ValueEnum)] +#[non_exhaustive] +pub enum FileTypeOptions { + /// Auto. + #[default] + Auto, + /// Text. + Text, + /// Markdown. + Markdown, + /// Typst. + Typst, +} + +pub enum FileType {} + +/// 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; +/// - raw text, if `-- [FILE]...` are provided. Note that some file types will +/// use a +/// - raw text, through stdin, if nothing is provided. +#[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, + /// 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, default_value = "default", ignore_case = true, value_enum)] + 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: Request, +} + +#[cfg(test)] +mod request_tests { + + use crate::Request; + + #[test] + fn test_with_text() { + let req = Request::default().with_text("hello".to_string()); + + assert_eq!(req.text.unwrap(), "hello".to_string()); + assert!(req.data.is_none()); + } + + #[test] + fn test_with_data() { + let req = Request::default().with_text("hello".to_string()); + + assert_eq!(req.text.unwrap(), "hello".to_string()); + assert!(req.data.is_none()); + } +} + +/// Responses + +/// Detected language from check request. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[non_exhaustive] +pub struct DetectedLanguage { + /// Language code, e.g., `"sk-SK"` for Slovak. + pub code: String, + /// Language name, e.g., `"Slovak"`. + pub name: String, + /// Undocumented fields. + /// + /// Examples are: + /// + /// - 'confidence', the confidence level, from 0 to 1; + /// - 'source', the source file for the language detection. + #[cfg(feature = "undoc")] + #[serde(flatten)] + pub undocumented: HashMap, +} + +/// Language information in check response. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct LanguageResponse { + /// Language code, e.g., `"sk-SK"` for Slovak. + pub code: String, + /// Detected language from provided request. + pub detected_language: DetectedLanguage, + /// Language name, e.g., `"Slovak"`. + pub name: String, +} + +/// Match context in check response. +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +#[non_exhaustive] +pub struct Context { + /// Length of the match. + pub length: usize, + /// Char index at which the match starts. + pub offset: usize, + /// Contextual text around the match. + pub text: String, +} + +/// More context, post-processed in check response. +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +#[non_exhaustive] +pub struct MoreContext { + /// Line number where match occurred. + pub line_number: usize, + /// Char index at which the match starts on the current line. + pub line_offset: usize, +} + +/// Possible replacement for a given match in check response. +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +#[non_exhaustive] +pub struct Replacement { + /// Possible replacement value. + pub value: String, +} + +impl From for Replacement { + fn from(value: String) -> Self { + Self { value } + } +} + +impl From<&str> for Replacement { + fn from(value: &str) -> Self { + value.to_string().into() + } +} + +/// A rule category. +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +#[non_exhaustive] +pub struct Category { + /// Category id. + pub id: String, + /// Category name. + pub name: String, +} + +/// A possible url of a rule in a check response. +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +#[non_exhaustive] +pub struct Url { + /// Url value. + pub value: String, +} + +/// The rule that was not satisfied in a given match. +#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Rule { + /// Rule category. + pub category: Category, + /// Rule description. + pub description: String, + /// Rule id. + pub id: String, + /// Issue type. + pub issue_type: String, + /// Rule source file. + /// Rule sub id. + pub sub_id: Option, + /// Rule list of urls. + pub urls: Option>, + /// Undocumented fields. + /// + /// Examples are: + /// + /// - 'is_premium', indicate if the rule is from the premium API; + /// - 'source_file', the source file of the rule. + #[cfg(feature = "undoc")] + #[serde(flatten)] + pub undocumented: HashMap, +} + +/// Type of a given match. +#[derive(PartialEq, Eq, Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Type { + /// Type name. + pub type_name: String, +} + +/// Grammatical error match. +#[derive(PartialEq, Eq, Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Match { + /// Match context. + pub context: Context, + /// Match length. + pub length: usize, + /// Error message. + pub message: String, + /// 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. + pub offset: usize, + /// List of possible replacements (if applies). + pub replacements: Vec, + /// Match rule that was not satisfied. + pub rule: Rule, + /// Sentence in which the error was found. + pub sentence: String, + /// Short message about the error. + pub short_message: String, + /// Undocumented fields. + /// + /// Examples are: + /// + /// - 'type', the match type; + /// - 'context_for_sure_match', unknown; + /// - 'ignore_for_incomplete_sentence', unknown; + #[cfg(feature = "undoc")] + #[serde(flatten)] + pub undocumented: HashMap, +} + +/// LanguageTool software details. +#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Software { + /// LanguageTool API version. + pub api_version: usize, + /// Some information about build date. + pub build_date: String, + /// Name (should be `"LanguageTool"`). + pub name: String, + /// Tell whether the server uses premium API or not. + pub premium: bool, + /// Sentence that indicates if using premium API would find more errors. + #[cfg(feature = "unstable")] + pub premium_hint: Option, + /// Unknown: please fill a [PR](https://github.com/jeertmans/languagetool-rust/pulls) of your + /// know that this attribute is used for. + pub status: String, + /// LanguageTool version. + pub version: String, +} + +/// Warnings about check response. +#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Warnings { + /// Indicate if results are incomplete. + pub incomplete_results: bool, +} + +/// LanguageTool POST check response. +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Response { + /// Language information. + pub language: LanguageResponse, + /// List of error matches. + pub matches: Vec, + /// Ranges ([start, end]) of sentences. + #[cfg(feature = "unstable")] + pub sentence_ranges: Option>, + /// LanguageTool software information. + pub software: Software, + /// Possible warnings. + #[cfg(feature = "unstable")] + pub warnings: Option, +} + +impl Response { + /// Return an iterator over matches. + pub fn iter_matches(&self) -> std::slice::Iter<'_, Match> { + self.matches.iter() + } + + /// Return an iterator over mutable matches. + pub fn iter_matches_mut(&mut self) -> std::slice::IterMut<'_, Match> { + self.matches.iter_mut() + } + + /// Creates an annotated string from current response. + #[cfg(feature = "annotate")] + #[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(); + } + let replacements: Vec<_> = self + .matches + .iter() + .map(|m| { + m.replacements.iter().fold(String::new(), |mut acc, r| { + if !acc.is_empty() { + acc.push_str(", "); + } + acc.push_str(&r.value); + acc + }) + }) + .collect(); + + let snippets = self.matches.iter().zip(replacements.iter()).map(|(m, r)| { + Snippet { + title: Some(Annotation { + label: Some(&m.message), + id: Some(&m.rule.id), + annotation_type: AnnotationType::Error, + }), + footer: vec![], + slices: vec![Slice { + source: &m.context.text, + line_start: 1 + text.chars().take(m.offset).filter(|c| *c == '\n').count(), + origin, + fold: true, + annotations: vec![ + SourceAnnotation { + label: &m.rule.description, + annotation_type: AnnotationType::Error, + range: (m.context.offset, m.context.offset + m.context.length), + }, + SourceAnnotation { + label: r, + annotation_type: AnnotationType::Help, + range: (m.context.offset, m.context.offset + m.context.length), + }, + ], + }], + opt: FormatOptions { + color, + ..Default::default() + }, + } + }); + + let mut annotation = String::new(); + + for snippet in snippets { + if !annotation.is_empty() { + annotation.push('\n'); + } + annotation.push_str(&DisplayList::from(snippet).to_string()); + } + annotation + } +} + +/// 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 ResponseWithContext { + /// Original text that was checked by LT. + pub text: String, + /// Check response. + pub response: Response, + /// Text's length. + pub text_length: usize, +} + +impl ResponseWithContext { + /// Bind a check response with its original text. + #[must_use] + pub fn new(text: String, response: Response) -> Self { + let text_length = text.chars().count(); + Self { + text, + response, + text_length, + } + } + + /// Return an iterator over matches. + pub fn iter_matches(&self) -> std::slice::Iter<'_, Match> { + self.response.iter_matches() + } + + /// Return an iterator over mutable matches. + pub fn iter_matches_mut(&mut self) -> std::slice::IterMut<'_, Match> { + self.response.iter_matches_mut() + } + + /// 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>> { + self.into() + } + + /// Append a check response to the current while + /// adjusting the matches' offsets. + /// + /// This is especially useful when a text was split in multiple requests. + #[must_use] + pub fn append(mut self, mut other: Self) -> Self { + let offset = self.text_length; + for m in other.iter_matches_mut() { + m.offset += offset; + } + + #[cfg(feature = "unstable")] + if let Some(ref mut sr_other) = other.response.sentence_ranges { + match self.response.sentence_ranges { + Some(ref mut sr_self) => { + sr_self.append(sr_other); + }, + None => { + std::mem::swap( + &mut self.response.sentence_ranges, + &mut other.response.sentence_ranges, + ); + }, + } + } + + self.response.matches.append(&mut other.response.matches); + self.text.push_str(other.text.as_str()); + self.text_length += other.text_length; + self + } +} + +impl From for Response { + #[allow(clippy::needless_borrow)] + fn from(mut resp: ResponseWithContext) -> Self { + let iter: MatchPositions<'_, std::slice::IterMut<'_, Match>> = (&mut resp).into(); + + for (line_number, line_offset, m) in iter { + 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> { + text_chars: std::str::Chars<'source>, + matches: T, + line_number: usize, + line_offset: usize, + offset: usize, +} + +impl<'source> From<&'source ResponseWithContext> + for MatchPositions<'source, std::slice::Iter<'source, Match>> +{ + fn from(response: &'source ResponseWithContext) -> Self { + MatchPositions { + text_chars: response.text.chars(), + matches: response.iter_matches(), + line_number: 1, + line_offset: 0, + offset: 0, + } + } +} + +impl<'source> From<&'source mut ResponseWithContext> + for MatchPositions<'source, std::slice::IterMut<'source, Match>> +{ + fn from(response: &'source mut ResponseWithContext) -> Self { + MatchPositions { + text_chars: response.text.chars(), + matches: response.response.iter_matches_mut(), + line_number: 1, + line_offset: 0, + offset: 0, + } + } +} + +impl<'source, T> MatchPositions<'source, T> { + /// Set the line number to a give value. + /// + /// By default, the first line number is 1. + pub fn set_line_number(mut self, line_number: usize) -> Self { + self.line_number = line_number; + self + } + + fn update_line_number_and_offset(&mut self, m: &Match) { + let n = m.offset - self.offset; + for _ in 0..n { + match self.text_chars.next() { + Some('\n') => { + self.line_number += 1; + self.line_offset = 0; + }, + None => { + panic!( + "text is shorter than expected, are you sure this text was the one used \ + for the check request?" + ) + }, + _ => self.line_offset += 1, + } + } + self.offset = m.offset; + } +} + +impl<'source> Iterator for MatchPositions<'source, std::slice::Iter<'source, Match>> { + type Item = (usize, usize, &'source Match); + + fn next(&mut self) -> Option { + if let Some(m) = self.matches.next() { + self.update_line_number_and_offset(m); + Some((self.line_number, self.line_offset, m)) + } else { + None + } + } +} + +impl<'source> Iterator for MatchPositions<'source, std::slice::IterMut<'source, Match>> { + type Item = (usize, usize, &'source mut Match); + + fn next(&mut self) -> Option { + if let Some(m) = self.matches.next() { + self.update_line_number_and_offset(m); + Some((self.line_number, self.line_offset, m)) + } else { + None + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Debug)] + enum Token<'source> { + Text(&'source str), + 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()) { + Token::Text(s) + } else { + Token::Skip(s) + } + } + } + + impl<'source> From> for DataAnnotation { + 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()), + } + } + } + + #[test] + fn test_data_annotation() { + let words: Vec<&str> = "My name is Q34XY".split(' ').collect(); + let data: Data = words.iter().map(|w| Token::from(*w)).collect(); + + 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()), + ], + }; + + assert_eq!(data, expected_data); + } + + #[test] + fn test_serialize_option_vec_string() { + use serde::Serialize; + + #[derive(Serialize)] + struct Foo { + #[serde(serialize_with = "serialize_option_vec_string")] + values: Option>, + } + + impl Foo { + fn new(values: I) -> Self + where + I: IntoIterator, + T: ToString, + { + Self { + values: Some(values.into_iter().map(|v| v.to_string()).collect()), + } + } + fn none() -> Self { + Self { values: None } + } + } + + let got = serde_json::to_string(&Foo::new(vec!["en-US", "de-DE"])).unwrap(); + assert_eq!(got, r#"{"values":"en-US,de-DE"}"#); + + let got = serde_json::to_string(&Foo::new(vec!["en-US"])).unwrap(); + assert_eq!(got, r#"{"values":"en-US"}"#); + + let got = serde_json::to_string(&Foo::new(Vec::::new())).unwrap(); + assert_eq!(got, r#"{"values":null}"#); + + let got = serde_json::to_string(&Foo::none()).unwrap(); + assert_eq!(got, r#"{"values":null}"#); + } +} + +/// Annotate a response by using its request context. +pub fn annotate(response: &Response, request: &Request) -> String {} diff --git a/src/lib/cli.rs b/src/cli.rs similarity index 99% rename from src/lib/cli.rs rename to src/cli.rs index 03e24e0..5aa65ee 100644 --- a/src/lib/cli.rs +++ b/src/cli.rs @@ -2,6 +2,13 @@ //! //! 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 std::io::{self, Write}; + +use clap::{CommandFactory, Parser, Subcommand}; +use is_terminal::IsTerminal; +#[cfg(feature = "annotate")] +use termcolor::WriteColor; +use termcolor::{ColorChoice, StandardStream}; use crate::{ check::CheckResponseWithContext, @@ -9,12 +16,6 @@ use crate::{ 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. /// @@ -57,7 +58,7 @@ pub struct Cli { #[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)] + #[command(flatten, next_help_heading = "Server options")] pub server_cli: ServerCli, /// Subcommand. #[command(subcommand)] diff --git a/src/lib/docker.rs b/src/docker.rs similarity index 99% rename from src/lib/docker.rs rename to src/docker.rs index e467340..a040d41 100644 --- a/src/lib/docker.rs +++ b/src/docker.rs @@ -1,10 +1,12 @@ //! Structures and methods to easily manipulate Docker images, especially for //! LanguageTool applications. -use crate::error::{exit_status_error, Error, Result}; +use std::process::{Command, Output, Stdio}; + #[cfg(feature = "cli")] use clap::{Args, Parser}; -use std::process::{Command, Output, Stdio}; + +use crate::error::{exit_status_error, Error, Result}; /// Commands to pull, start and stop a `LanguageTool` container using Docker. #[cfg_attr(feature = "cli", derive(Args))] diff --git a/src/lib/error.rs b/src/error.rs similarity index 99% rename from src/lib/error.rs rename to src/error.rs index 8ac3572..9890117 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. diff --git a/src/lib/lib.rs b/src/lib.rs similarity index 65% rename from src/lib/lib.rs rename to src/lib.rs index ed743c3..a28f029 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,12 @@ //! 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, - }, -}; diff --git a/src/bin.rs b/src/main.rs similarity index 100% rename from src/bin.rs rename to src/main.rs